Home > Software engineering >  How should I cache data from database to be thread safe by EF Core?
How should I cache data from database to be thread safe by EF Core?

Time:12-27

I have a list of data, it is not large (about 400 items). It is stored in the MySQL database, and I want to cache it to ASP.NET Core to prevent reading it frequently. What's more, the data will refresh every hour.

Here is my code:

public class IPCheckService
{
        readonly SIComponent.Database.BLContext Context;       

        public IPCheckService(SIComponent.Database.BLContext Context)
        { 
            this.Context = Context;
        }       

        List<SIComponent.Database.IPAndUAModel> _IPBlackList = null;

        public List<SIComponent.Database.IPAndUAModel> IPBlackList 
        {
            get  
            {
                if (DateTime.Now - LastUpdateTime > TimeSpan.FromHours(2))
                {                                    
                    if (Context.IpBlackList.Count() != _IPRangeBlackList.Count)
                    {
                        _IPRangeBlackList = new List<SIComponent.Database.IPAndUAModel>(Context.IpBlackList);
                    }                
                    LastUpdateTime = DateTime.Now;
                }

                if (_IPBlackList == null)
                {
                    _IPBlackList = new List<SIComponent.Database.IPAndUAModel>(Context.IPBlackList);
                }

                return _IPBlackList;
            }
        }     
           
        DateTime LastUpdateTime = DateTime.Now;       
    }

What's more, the codes above are part of the service and it is a Singleton service.

After the program ran, the exception middleware I made will sometimes catch this exception:

An attempt was made to use the model while it was being created. A DbContext instance cannot be used inside 'OnModelCreating' in any way that makes use of the model that is being created.

at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
at Microsoft.EntityFrameworkCore.DbContext.get_Model()
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityType() at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.CheckState()
at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.get_EntityQueryable() at Microsoft.EntityFrameworkCore.Internal.InternalDbSet1.System.Collections.Generic.IEnumerable.GetEnumerator()
at System.Collections.Generic.List1..ctor(IEnumerable1 collection)
at abc.Services.IPCheckService.get_IPBlackList() in C:\Project\abc\Services\IPCheckService.cs:line 12
at abc.Middlewares.IPAccessCheck(IPModel IP) in C:\Project\abc\Middlewares\IPAccessCheckMiddleware.cs:line 701
at lambda_method9(Closure , Object)
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.<Invoke>g__AwaitMatcher|8_0(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task1 matcherTask) at abc.Middlewares.ExceptionMiddleware.Invoke(HttpContext context) in C:\Project\abc\Middlewares\ExceptionMiddleware.cs:line 26

I read some articles about this exception and found it seems because instance members of DbContext and related classes are not guaranteed to be thread-safe.

However, I found nothing article about how to solve this. How should I solve it? Thank you.

CodePudding user response:

You cannot use DbContext in such way. It is usually registered as scoped service, but you are trying to inject it into singletone. Also there are problems with multithreading, you have to guard retrieving data.

Well there is my variant:

public class IPCheckService
{
    private readonly IServiceProvider _serviceProvider;       
    private DateTime _lastUpdateTime;       
    private List<SIComponent.Database.IPAndUAModel> _IPBlackList = null;

    public IPCheckService(IServiceProvider serviceProvider)
    { 
        _serviceProvider = serviceProvider;
    }       

    public IReadOnlyList<SIComponent.Database.IPAndUAModel> IPBlackList 
    {
        get  
        {
            // you need lock because several threads may access this property concurrently.
            lock(this)
            {
                if (_IPBlackList != null && (DateTime.Now - _lastUpdateTime).TotalHours < 1)
                {
                    return  _IPBlackList;
                }

                // context will be create within scope and automatically disposed
                using (var scope = serviceProvider.CreateScope())
                {   
                    var context = scope.ServiceProvider.GetService<SIComponent.Database.BLContext>();

                    _IPBlackList = context.IPBlackList.ToList();           
                    _lastUpdateTime = DateTime.Now;
                }

                return _IPBlackList
            }
        }
    }     
}
  • Related