In the end I implemented a Service as a Singleton that I registered with DI.
public class SingleInstanceRegisterService: IDisposable
{
private ConcurrentDictionary<SingleInstance,SingleInstance> dict = new ConcurrentDictionary<SingleInstance, SingleInstance>();
AutoResetEvent arv;
Timer timer;
public SingleInstanceRegisterService()
{
arv = new AutoResetEvent(false);
timer = new Timer(this.Cleanup, arv, 0, 60000);
}
public bool TryAdd(SingleInstance i)
{
return dict.TryAdd(i, i);
}
public bool ContainsKey(SingleInstance i)
{
return dict.ContainsKey(i);
}
public bool TryRemove(SingleInstance i)
{
SingleInstance x;
return dict.TryRemove(i, out x);
}
public void Cleanup(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
if (dict != null)
{
foreach(SingleInstance i in dict.Keys)
{
if (i.Expiry < DateTimeOffset.Now)
{
TryRemove(i);
}
}
}
autoEvent.Set();
}
public void Dispose()
{
timer?.Dispose();
timer = null;
}
}
The SingleInstance is then defined as:
public class SingleInstance: IEquatable<SingleInstance>
{
public string UserSubject;
public string FullMethodName;
public DateTimeOffset Expiry;
public SingleInstance(User u, MethodInfo m, DateTimeOffset exp)
{
UserSubject = u.UserSubject.ToString();
FullMethodName = m.DeclaringType.FullName + "." + m.Name;
Expiry = exp;
}
public bool Equals(SingleInstance other)
{
return (other != null &&
other.FullMethodName == this.FullMethodName &&
other.UserSubject == this.UserSubject);
}
public override bool Equals(object other)
{
return this.Equals(other as SingleInstance);
}
public override int GetHashCode()
{
return (UserSubject.GetHashCode() + FullMethodName.GetHashCode());
}
}
To use it we do the following.
_single is my dependency injected SingleInstanceRegisterService.
var thisInstance = new SingleInstance(user, method, DateTimeOffset.UtcNow.AddMinutes(1));
if (_single.TryAdd(thisInstance))
{
// tell the database to kick off some long running process
DoLongRunningDatabaseProcess();
// remove lock on running process
_single.TryRemove(thisInstance);
}
If the TryAdd fails to add it then it must already be running. Note that there is a timer that will automatically clear out process locks that have expired.