We use SqlServer's application lock functionality for distributed locking. This is especially convenient if SqlServer is already a part of your stack.
To make this easier to work with from .NET, I created a NuGet package which makes it easy to use this functionality. The library also supports other backends such as Postgres, Azure blob storage, and Redis.
With this library, the code looks like:
var @lock = new SqlDistributedLock("my_lock_name", connectionString); using (@lock.Acquire()) { // critical region } Because the underlying SqlServer functionality is very flexible, there are also overloads supporting TryAcquire semantics, timeouts, and async locking.