|
/// <summary> |
|
/// Throttles duplicate requests. |
|
/// Based loosely on <see href="http://stackoverflow.com/a/21011273/427899"/> |
|
/// </summary> |
|
public sealed class AsyncDuplicateLock |
|
{ |
|
/// <summary> |
|
/// The collection of semaphore slims. |
|
/// </summary> |
|
private static readonly ConcurrentDictionary<object, SemaphoreSlim> SemaphoreSlims |
|
= new ConcurrentDictionary<object, SemaphoreSlim>(); |
|
|
|
/// <summary> |
|
/// Locks against the given key. |
|
/// </summary> |
|
/// <param name="key"> |
|
/// The key that identifies the current object. |
|
/// </param> |
|
/// <returns> |
|
/// The disposable <see cref="Task"/>. |
|
/// </returns> |
|
public IDisposable Lock(object key) |
|
{ |
|
DisposableScope releaser = new DisposableScope( |
|
key, |
|
s => |
|
{ |
|
SemaphoreSlim locker; |
|
if (SemaphoreSlims.TryRemove(s, out locker)) |
|
{ |
|
locker.Release(); |
|
locker.Dispose(); |
|
} |
|
}); |
|
|
|
SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1)); |
|
semaphore.Wait(); |
|
return releaser; |
|
} |
|
|
|
/// <summary> |
|
/// Asynchronously locks against the given key. |
|
/// </summary> |
|
/// <param name="key"> |
|
/// The key that identifies the current object. |
|
/// </param> |
|
/// <returns> |
|
/// The disposable <see cref="Task"/>. |
|
/// </returns> |
|
public Task<IDisposable> LockAsync(object key) |
|
{ |
|
DisposableScope releaser = new DisposableScope( |
|
key, |
|
s => |
|
{ |
|
SemaphoreSlim locker; |
|
if (SemaphoreSlims.TryRemove(s, out locker)) |
|
{ |
|
locker.Release(); |
|
locker.Dispose(); |
|
} |
|
}); |
|
|
|
Task<IDisposable> releaserTask = Task.FromResult(releaser as IDisposable); |
|
SemaphoreSlim semaphore = SemaphoreSlims.GetOrAdd(key, new SemaphoreSlim(1, 1)); |
|
|
|
Task waitTask = semaphore.WaitAsync(); |
|
|
|
return waitTask.IsCompleted |
|
? releaserTask |
|
: waitTask.ContinueWith( |
|
(_, r) => (IDisposable)r, |
|
releaser, |
|
CancellationToken.None, |
|
TaskContinuationOptions.ExecuteSynchronously, |
|
TaskScheduler.Default); |
|
} |
|
|
|
/// <summary> |
|
/// The disposable scope. |
|
/// </summary> |
|
private sealed class DisposableScope : IDisposable |
|
{ |
|
/// <summary> |
|
/// The key |
|
/// </summary> |
|
private readonly object key; |
|
|
|
/// <summary> |
|
/// The close scope action. |
|
/// </summary> |
|
private readonly Action<object> closeScopeAction; |
|
|
|
/// <summary> |
|
/// Initializes a new instance of the <see cref="DisposableScope"/> class. |
|
/// </summary> |
|
/// <param name="key"> |
|
/// The key. |
|
/// </param> |
|
/// <param name="closeScopeAction"> |
|
/// The close scope action. |
|
/// </param> |
|
public DisposableScope(object key, Action<object> closeScopeAction) |
|
{ |
|
this.key = key; |
|
this.closeScopeAction = closeScopeAction; |
|
} |
|
|
|
/// <summary> |
|
/// Disposes the scope. |
|
/// </summary> |
|
public void Dispose() |
|
{ |
|
this.closeScopeAction(this.key); |
|
} |
|
} |
|
} |
Can two app domains running a single website exist at the same time pointing to a single location? I guess with virtual paths that is possible but it is not something I've considered. I'm ok with locally scoped at the moment though as I'm more concerned about getting the lock itself correct. Definitely need to look at cross app domain though.
So yeah.. I want to lock per key. And most definitely Async.
Basically the process should be.