Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asp.net Core DI: Using SemaphoreSlim for write AND read operations with Singleton

I am re-tooling an ASP.NET CORE 2.2 app to avoid using the service locator pattern in conjunction with static classes. Double bad!

The re-tooling is involving the creation and injection of Singleton object as a repository for some global data. The idea here to avoid hits to my SQL server for some basic/global data that gets used over and over again in requests. However, this data needs to be updated on an hourly basis (not just at app startup). So, to manage the situation I am using SemaphoreSlim to handle one-at-a-time access to the data objects.

Here is a paired down sketch of what what I'm doing:

namespace MyApp.Global
{

    public interface IMyGlobalDataService
    {
        Task<List<ImportantDataItem>> GetFilteredDataOfMyList(string prop1);
        Task LoadMyImportantDataListAsync();
    }

    public class MyGlobalDataService: IMyGlobalDataService
    {
        private MyDbContext _myDbContext;

        private readonly SemaphoreSlim myImportantDataLock = new SemaphoreSlim(1, 1);
        private List<ImportantDataItem> myImportantDataList { get; set; }
        public async Task<List<ImportantDataItem>> GetFilteredDataOfMyList(string prop1)
        {
            List<ImportantDataItem> list;
            myImportantDataLock.WaitAsync();
            try
            {
                list = myImportantDataList.Where(itm => itm.Prop1 == prop1).ToList();
            }
            finally
            {
                myImportantDataLock.Release();
            }
            return list;
        }

        public async Task LoadMyImportantDataListAsync()
        {
            // this method gets called when the Service is created and once every hour thereafter

            myImportantDataLock.WaitAsync();
            try
            {
                this.MyImportantDataList = await _myDbContext.ImportantDataItems.ToListAsync();
            }
            finally
            {
                myImportantDataLock.Release();
            }
            return;
        }


        public MyGlobalDataService(MyDbContext myDbContext) {
            _myDbContext = myDbContext;
        };
    }
}

So in effect I am using the SemaphoreSlim to limit to one-thread-at-a-time access, for both READ and UPDATING to myImportantDataList. This is really uncertain territory for me. Does this seem like an appropriate approach to handle my injection of a global data Singleton throughout my app? Or should I expect insane thread locking/blocking?

like image 919
brando Avatar asked Mar 05 '23 09:03

brando


2 Answers

The problem with using SemaphoreSlim is scalability.

As this is in a web application, it's fair to assume that you want to have the potential for more than one reader to access the data simultaneously. However, you are (understandably) limiting the number of requests for the semaphore that can be generated concurrently to 1 (to prevent concurrent read and write requests). This means you will serialize all reads too.

You need to use something like ReaderWriterLockSlim to allow multiple threads for reading, but ensure exclusive access for writing.

like image 97
Creyke Avatar answered Mar 16 '23 00:03

Creyke


Creyke's answer hit the nail on the head for me: using ReaderWriterLockSlim. So I've marked it as the accepted answer. But I am posting my revised solution in case it might be helpful to anyone. Important to note that I'm using the following package to provide async functionality to ReaderWriterLockSlim: https://www.nuget.org/packages/Nito.AsyncEx/

using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Text;

namespace MyApp.Global
{

    public interface IMyGlobalDataService
    {
        Task<List<ImportantDataItem>> GetFilteredDataOfMyList(string prop1);
        Task LoadMyImportantDataListAsync();
    }

    public class MyGlobalDataService : IMyGlobalDataService
    {
        private MyDbContext _myDbContext;

        private readonly AsyncReaderWriterLock myImportantDataLock = new AsyncReaderWriterLock();
        private List<ImportantDataItem> myImportantDataList { get; set; }
        public async Task<List<ImportantDataItem>> GetFilteredDataOfMyList(string prop1)
        {
            List<ImportantDataItem> list;
            using (await myImportantDataLock.ReaderLockAsync())
            {
                list = myImportantDataList.Where(itm => itm.Prop1 == prop1).ToList();
            }
            return list;
        }

        public async Task LoadMyImportantDataListAsync()
        {
            // this method gets called when the Service is created and once every hour thereafter

            using (await myImportantDataLock.WriterLockAsync())
            {
                this.MyImportantDataList = await _myDbContext.ImportantDataItems.ToListAsync();
            }
            
            return;
        }


        public MyGlobalDataService(MyDbContext myDbContext)
        {
            _myDbContext = myDbContext;
        };
    }
}
like image 24
brando Avatar answered Mar 16 '23 00:03

brando