Search code examples
c#asp.net-mvcthread-safetysingletonninject

Thread safe singleton with async operation


I have an ASP.NET MVC5 application using Ninject for DI. I have a message that is displayed at the top of every page. The message is retrieved from a web service, using an async operation. The message itself is small and updated infrequently, so I want to cache it in memory.

I've created a simple service that is configured as DI singleton. I had a nice thing going ReaderWriterLock, but that doesn't support async. So I've tried recreate the same thing with SemaphoreSlim. This is what I've got:

    public class ExampleService {

        private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
        private DateTime? lastAccess = null;
        private string cachedMessage = null;

        public async Task<string> GetMessageAsync() {
            if (lastAccess.HasValue && lastAccess.Value > DateTime.UtcNow.AddHours(-1)) {
                return cachedMessage;
            }

            var writeable = semaphore.CurrentCount == 1;

            await semaphore.WaitAsync();
            try {
                if (writeable) {
                    // Do async stuff
                }
            }
            finally {
                semaphore.Release();
            }

            return cachedMessage;
        }

        ~ExampleService() {
            if (semaphore != null) semaphore.Dispose();
        }

    }

The goal is to have all calls wait until cacheMessage is populated, then give it to everyone. The problem with my solution is that once the write call has completed, all the waiting reads are effectively stuck in a queue being released one by one, while new reads get to jump the queue completely.

What is a better way of doing this?

Update

Based on this post, a lot of people appear to advocate just using LazyCache. Making the implementation:

    public class ExampleService {

        private readonly IAppCache cache;

        public ExampleService(IAppCache cache) {
            this.cache = cache;
        }

        private async Task<string> GetMessageFromServerAsync() {
            // result = async stuff
            return result;
        }

        public async Task<string> GetMessageAsync() {
            return await cache.GetOrAddAsync("MessageKey", GetMessageFromServerAsync);
        }

    }

Solution

  • Based on this post, a lot of people appear to advocate just using LazyCache. Making the implementation:

    public class ExampleService {
    
        private readonly IAppCache cache;
    
        public ExampleService(IAppCache cache) {
            this.cache = cache;
        }
    
        private async Task<string> GetMessageFromServerAsync() {
            // result = async stuff
            return result;
        }
    
        public async Task<string> GetMessageAsync() {
            return await cache.GetOrAddAsync("MessageKey", GetMessageFromServerAsync);
        }
    
    }