I'm a big fan of just adding a simple script like `git describe --always --dirty > VERSION` in the output files of the deployment process. Or otherwise including it in bundle or executable metadata. (Subtly reformatting it to pass to `dotnet build -p:Version $GITDESCRIBEISH`, for instance, or putting it into a .json file that a web service will statically serve.) Similarly outputting `git describe --always --dirty` into CI/CD logs can be very handy.
You can use git describe's output to exactly name a commit. You can also use annotated tags to tag semver releases or marver (Marketing Version) releases and if you output the git describe to users that care about semver or marver they see confidence in your deployment system when you deploy exactly a tag versus deploy something like a hotfix after it. But git describe is also useful for lower environments because it will have useful output when not directly on a tag.
(ETA: For a monorepo you can still use semver/marver annotated tags with `git describe --match subproject/` to use a tree structure for tags.)