alastairtree / LazyCache

An easy to use thread safe in-memory caching service with a simple developer friendly API for c#
https://nuget.org/packages/LazyCache
MIT License
1.71k stars 159 forks source link

How to Proactively Repopulate Cached Items with EF? #158

Closed jasonhill closed 3 years ago

jasonhill commented 3 years ago

The wiki has a great example of using ImmediateExpiration and RegisterPostEvictionCallback to refresh cached items immediately when they expire. I tried using this in our application where we retrieve an expensive object graph via EF but get an exception because the EF context that the repository uses (via DI) has been disposed. We are not disposing the context manually.

System.ObjectDisposedException: 'Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.

Does anyone have sample code or any information about how to handle this scenario?

rgvlee commented 3 years ago

Was the dbcontext created within a scope that has been disposed?

jasonhill commented 3 years ago

The repository is added as a scoped service and the DB context is injected into the constructor of the repository.

On Mon, 28 Jun 2021 at 16:46, Lee Anderson @.***> wrote:

Was the dbcontext created within a scope that has been disposed?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/alastairtree/LazyCache/issues/158#issuecomment-869406791, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZDZCHHWCVQSTRZJ3P6LTVALFHANCNFSM47NFXSKQ .

EnricoMassone commented 3 years ago

@jasonhill I guess you are referring to this sample, correct ? Can you please add a little more context ? Are you working with ASP.NET core ? Which version ? Are you using the built-in Microsoft dependency injection container ? Are you fetching the cached item via GetOrAdd inside of an action method ?

jasonhill commented 3 years ago

Yes, that is the sample that I am referring to. We are using .Net 5 with the built in DI container.

The repository is pretty straightforward:

private readonly ApplicationDbContext _context; public SomeRepository(ApplicationDbContext context) { _context = context; }

The repository is added to the services like this:

services.AddScoped<ISomeRepository, SomeRepository>();

On Tue, 29 Jun 2021 at 16:35, Enrico Massone @.***> wrote:

@jasonhill https://github.com/jasonhill I guess you are referring to this sample https://github.com/alastairtree/LazyCache/wiki/API-documentation-(v-2.x)#using-immediateexpiration-and-registerpostevictioncallback-to-refresh-a-cached-item-automatically, correct ? Can you please add a little more context ? Are you working with ASP.NET core ? Which version ? Are you using the built-in Microsoft dependency injection container ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/alastairtree/LazyCache/issues/158#issuecomment-870279703, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZD5L6IKOXLVVYPHILYLTVFSRTANCNFSM47NFXSKQ .

EnricoMassone commented 3 years ago

Yes, that is the sample that I am referring to. We are using .Net 5 with the built in DI container. The repository is pretty straightforward: private readonly ApplicationDbContext _context; public SomeRepository(ApplicationDbContext context) { _context = context; } The repository is added to the services like this: services.AddScoped<ISomeRepository, SomeRepository>(); On Tue, 29 Jun 2021 at 16:35, Enrico Massone @.***> wrote: @jasonhill https://github.com/jasonhill I guess you are referring to this sample https://github.com/alastairtree/LazyCache/wiki/API-documentation-(v-2.x)#using-immediateexpiration-and-registerpostevictioncallback-to-refresh-a-cached-item-automatically, correct ? Can you please add a little more context ? Are you working with ASP.NET core ? Which version ? Are you using the built-in Microsoft dependency injection container ? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#158 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZD5L6IKOXLVVYPHILYLTVFSRTANCNFSM47NFXSKQ .

Where are you injecting the ISomeRepository in order to use it and populate the cache ? I mean, are you using it from a controller or from an hosted service ?

jasonhill commented 3 years ago

Yes, it is being injected into a controller.

On Tue, 29 Jun 2021 at 16:49, Enrico Massone @.***> wrote:

Yes, that is the sample that I am referring to. We are using .Net 5 with the built in DI container. The repository is pretty straightforward: private readonly ApplicationDbContext _context; public SomeRepository(ApplicationDbContext context) { context = context; } The repository is added to the services like this: services.AddScoped<ISomeRepository, SomeRepository>(); … <#m-7534070757249894381_> On Tue, 29 Jun 2021 at 16:35, Enrico Massone @.***> wrote: @jasonhill https://github.com/jasonhill https://github.com/jasonhill I guess you are referring to this sample https://github.com/alastairtree/LazyCache/wiki/API-documentation-(v-2.x)#using-immediateexpiration-and-registerpostevictioncallback-to-refresh-a-cached-item-automatically, correct ? Can you please add a little more context ? Are you working with ASP.NET core ? Which version ? Are you using the built-in Microsoft dependency injection container ? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#158 (comment) https://github.com/alastairtree/LazyCache/issues/158#issuecomment-870279703>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZD5L6IKOXLVVYPHILYLTVFSRTANCNFSM47NFXSKQ .

Where are you injecting the ISomeRepository in order to use it and populate the cache ? I mean, are you using it from a controller or from an hosted service ?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/alastairtree/LazyCache/issues/158#issuecomment-870287571, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZDZ35NB3BXZAQTAYOT3TVFUIBANCNFSM47NFXSKQ .

EnricoMassone commented 3 years ago

Try the following solution. You basically need to create a new scope each time you need to resolve the repository service and fetch the items from the database.

[ApiController]
public HomeController : ControllerBase 
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IAppCache _cache;

    public HomeController(IServiceScopeFactory scopeFactory, IAppCache cache) 
    {
        _scopeFactory = scopeFactory;
        _cache = cache;
    }

    [HttpGet("")]
    public IActionResult Index()
        {
        var products = _cache.GetOrAdd("my-key", () => GetProducts(), GetOptions());
        return this.Ok(products);
    }

    private Product[] GetProducts()
    {
        using var scope = _scopeFactory.CreateScope();
        var container = scope.ServiceProvider;

        var repository = container.GetRequiredService<IProductRepository>();
        return repository.GetAll();
    }

    private MemoryCacheEntryOptions GetOptions()
    {
        //ensure the cache item expires exactly on 30s (and not lazily on the next access)
        var options = new LazyCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromSeconds(30), ExpirationMode.ImmediateExpiration);

        // as soon as it expires, re-add it to the cache
        options.RegisterPostEvictionCallback((keyEvicted, value, reason, state) =>
        {
            // dont re-add if running out of memory or it was forcibly removed
            if (reason == EvictionReason.Expired  || reason == EvictionReason.TokenExpired) {
                _cache.GetOrAdd(keyEvicted, _ => GetProducts(), GetOptions()); //calls itself to get another set of options!
            }
        });
        return options;
    }
}

On your first attempt you have probably injected the IProductRepositorydirectly in your controller. It's fine doing so, but you need to consider that the repository is registered as a scoped dependency and that the scope is represented by the lifetime of the incoming HTTP request. Once your ASP.NET core application has produced a response for the HTTP request itself, the scope terminates and the object representing the scope itself is disposed. By doing so, any transient and scoped service resolved inside of that scope will be disposed (this is true only for the services which need disposal, the EF context is disposable and it is registered as a scoped service, so it is one of the service being disposed when the lifetime of the scope is done). The problem arises when the post eviction callback for the expired cache entry is executed. This is done after the HTTP request has been completed and the dependency injection scope has been disposed, so inside of the post eviction callback you are using a repository object which contains an already disposed EF context object. The disposed EF context then throws an ObjectDisposedExceptionwhen you try to use it inside of your repository.

In order to solve this mess, you need to manually create a DI scope each time you want to fetch the products from the database, in order to repopulate the cache. By doing so, each time you access the database you have a fresh new DI scope and, as a consequence, a fresh new copy of the EF context object. Doing so, you never end up using an already disposed EF context object.

jasonhill commented 3 years ago

That's great - thanks for your help.

On Tue, 29 Jun 2021 at 17:15, Enrico Massone @.***> wrote:

Try the following solution. You basically need to create a new scope each time you need to resolve the repository service and fetch all the products.

[ApiController] public HomeController : ControllerBase { private readonly IServiceScopeFactory _scopeFactory; private readonly IAppCache _cache;

public HomeController(IServiceScopeFactory scopeFactory, IAppCache cache) { _scopeFactory = scopeFactory; _cache = cache; }

[HttpGet("")] public IActionResult Index() { var products = _cache.GetOrAdd("my-key", () => GetProducts(), GetOptions()); return this.Ok(products); }

private Product[] GetProducts() { using var scope = _scopeFactory.CreateScope(); var container = scope.ServiceProvider;

  var repository = container.GetRequiredService<IProductRepository>();
  return repository.GetAll();

}

private MemoryCacheEntryOptions GetOptions() { //ensure the cache item expires exactly on 30s (and not lazily on the next access) var options = new LazyCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30), ExpirationMode.ImmediateExpiration);

  // as soon as it expires, re-add it to the cache
  options.RegisterPostEvictionCallback((keyEvicted, value, reason, state) =>
  {
      // dont re-add if running out of memory or it was forcibly removed
      if (reason == EvictionReason.Expired  || reason == EvictionReason.TokenExpired) {
          _cache.GetOrAdd(keyEvicted, _ => GetProducts(), GetOptions()); //calls itself to get another set of options!
      }
  });
  return options;

} }

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/alastairtree/LazyCache/issues/158#issuecomment-870337720, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKDZD6ES7XCBRJL4H252RDTVFXIDANCNFSM47NFXSKQ .

alastairtree commented 3 years ago

Seem like this is fixed - thanks to everyone for helping. Closing.