I'm trying to understand why I get ArgumentNull exception on Monitor.Enter method.
The calling code only uses a single instance of NamedLocker.
I'm using a lock to protect from accessing the shared resource. Is the semaphore being disposed causing the null reference exception?
System.ArgumentNullException: Value cannot be null.
at System.Threading.Monitor.Enter(Object obj)
at System.Threading.SemaphoreSlim.<WaitUntilCountOrTimeoutAsync>d__31.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
The exception seems to be coming when doing a WaitAsync after getting the Semaphore.
Following is my code:
public class NamedLocker
{
    private static readonly Dictionary<string, SemaphoreSlim> _lockDict =
        new Dictionary<string, SemaphoreSlim>();
    private readonly Config config;
    private static readonly object semLock = new object();
    public NamedLocker(Config config)
    {
        this.config = config;
    }
    public async Task RunWithLock(string name, Func<Task> body)
    {
        try
        {
            _ = await GetOrCreateSemaphore(name)
               .WaitAsync(TimeSpan.FromSeconds(5))
               .ConfigureAwait(false);
            await body().ConfigureAwait(false);
        }
        finally
        {
            _ = Task.Run(async () =>
            {
                await Task.Delay(this.config.LockDelayMilliseconds)
                    .ConfigureAwait(false);
                RemoveLock(name);
            });
        }
    }
    public void RemoveLock(string key)
    {
        SemaphoreSlim outSemaphoreSlim;
        lock (semLock)
        {
            if (_lockDict.TryGetValue(key, out outSemaphoreSlim))
            {
                _lockDict.Remove(key);
                outSemaphoreSlim.Release();
                outSemaphoreSlim.Dispose();
            }
        }
    }
    private SemaphoreSlim GetOrCreateSemaphore(string key)
    {
        SemaphoreSlim outSemaphoreSlim;
        lock (semLock)
        {
            if (!_lockDict.TryGetValue(key, out outSemaphoreSlim))
            {
                outSemaphoreSlim = new SemaphoreSlim(1, 1);
                _lockDict[key] = outSemaphoreSlim;
            }
            return outSemaphoreSlim;
        }
    }
}
This is the test I used to validate the exception:
[TestMethod]
public async Task PPP()
{
    var namedLocker = new NamedLocker(new Config
    {
        LockDelayMilliseconds = 100
    });
    Func<Task> funcToRunInsideLock = async () => await Task.Delay(300);
    var tasks = Enumerable.Range(0, 100).Select(
        i => namedLocker.RunWithLock((i % 2).ToString(), funcToRunInsideLock));
    await Task.WhenAll(tasks).ConfigureAwait(false);
}
Edit:
Removing the Dispose() in finally block (inside RemoveLock) makes it work without the null exception.
I don't understand why this would be case.