'C# ASP.NET Core 6 & EF Core database service is already disposed in static method

I am extending the [Authorize] attribute to run custom authorization logic. Everything looks to be working - that is, it compiles - except when I try to get the related information from the database.

I created a static class, with a static method, I can call from within the attribute OnAuthorization method. The static method checks the cache for the information, or gets it directly from the database if it's not in cache yet.

public static class CacheHelper
{
    private static IMemoryCache? _memoryCache;
    private static RoleService _roleService;

    public static void Configure(IMemoryCache memoryCache, RoleService roleService)
    {
        _memoryCache = memoryCache;
        _roleService = roleService;
    }

    public static Dictionary<Permissions, byte> GetRolesPermissions(int roleId)
    {
        if (_memoryCache.TryGetValue(roleId, out string permissions))
        {
            return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
        } 
        else
        {
            string alsoPermissions = _roleService.Get(roleId).Permissions;
            _memoryCache.Set(roleId, alsoPermissions);
            return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
        }
    }
}

The configuration gets set up in Program.cs:

CacheHelper.Configure(app.Services.GetRequiredService<IMemoryCache>(), serviceScope.ServiceProvider.GetRequiredService<RoleService>());

Except I get an error when it tries to get the data:

System.ObjectDisposedException: 'Cannot access a disposed context instance...

I have not seen this with any of my other many services running smoothly.

I think it has something to do with the way I'm configuring the service for the static class... What is the correct way to get at the service from within the static class?



Solution 1:[1]

A unit of work pattern essentially helps cover scoping a DbContext, and there are several examples & implementations out there. The one I use for EF is the DbContextScope from Medhime which was developed for EF6 though there are forks for EF Core. (https://github.com/mehdime/DbContextScope)

At a very basic level to write yourself and have full transparency to the workings would be to inject a DbContextFactory, though the goal would be to have the DbContext's scope (lifetime) managed at a high enough level, and a common desire for teams is to avoid adding EF references to their top-level projects. It also makes sense to abstract the DbContext/DbSets in the event that you want to unit test your business logic as they can be somewhat clumsy to mock.

The very simple example: (No UoW, just scoping a DbContext)

public static Dictionary<Permissions, byte> GetRolesPermissions(int roleId)
{
    if (_memoryCache.TryGetValue(roleId, out string permissions))
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    
    using(var context = new ServiceDbContext())
    {
        string alsoPermissions = _roleService.GetPermissionsForRole(roleId, context);
        _memoryCache.Set(roleId, alsoPermissions);
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    }
}

This scopes a DbContext instance and provides it to any calls that will need a DbContext. If any of those services make modifications, a context.SaveChanges() can be applied at the end of the scope, handling any exceptions.

The downsides of this approach is that the DbContext is not injected so it cannot be substituted to test this service. It also requires that the service methods all need to be passed a reference for the DbContext since they cannot neatly accept an injected dependency. This ensures that where calls to multiple services and such are done with the same DbContext instance.

The next example: (DbContextScopeFactory)

public static Dictionary<Permissions, byte> GetRolesPermissions(int roleId)
{
    if (_memoryCache.TryGetValue(roleId, out string permissions))
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    
    using(var contextScope = _contextScopeFactory.Create())
    {
        string alsoPermissions = _roleService.GetPermissionsForRole(roleId, contextScope);
        _memoryCache.Set(roleId, alsoPermissions);
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    }
}

A ContextScope is a simple wrapper interface and implementation for the DbContext(s) which can expose a new instance of a DbContext as well as an overarching SaveChanges() method. This essentially forms a Unit of work. The ContextScopeFactory can be injected, though in the case of a Singleton pattern static implementation that's not a big issue/concern. The downside is that this scope still needs to be passed to each service method.

Unit of Work: (Medhime DbContextScope)

public static Dictionary<Permissions, byte> GetRolesPermissions(int roleId)
{
    if (_memoryCache.TryGetValue(roleId, out string permissions))
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    
    using(var contextScope = _dbContextScopeFactory.Create())
    {
        string alsoPermissions = _roleService.GetPermissionsForRole(roleId);
        _memoryCache.Set(roleId, alsoPermissions);
        return JsonSerializer.Deserialize<Dictionary<Permissions, byte>>(permissions);
    }
}

This looks very similar, however note that the DbContextScope does not need to be passed into the service method. The DbContextScope is provided by an injected DbContextScopeFactory similar to the previous example, however the service accepts an injected IAmbientDbContextScopeLocator which it uses to resolve a DbContextScope and get the relevant DbContext instance or instances.

Example Service:

public class RoleService : IRoleService
{
    private readonly IAmbientDbContextScopeLocator _contextScopeLocator = null;

    private ServiceDbContext Context
    {
        get { return _contextScopeLocator.Get<ServiceDbContext>(); }
    }

    public RoleService(IAmbientDbContextScopeLocator contextScopeLocator)
    {
        _contextScopeLocator = contextScopeLocator ?? throw new ArgumentNullException("contextScopeLocator");
    }

    // ...
}

Here your service can interact with a DbContext instance so long as it is called within a scope. Medhime's implementation accommodates both a Read/Write context scope (Create) and a read-only one (CreateReadOnly) so that implementations have a bit more control over whether calls should be updating data or not. You can also create scopes within scopes for more fine control where you might want to perform a DB operation (such as logging) without "poisoning" or being dependent on whether the outer scope is committed successfully or not. (DbContextScopeOption.ForceCreateNew)

Services can even call SaveChanges() on the DbContext instance within the scope to validate & handle exceptions, but it is at the scope level (DbContextScope) outside of all of the calls that ultimately commits the changes or not to the DB /w DbContextScope.SaveChanges().

It can take a moment to wrap your head around the Scope Factory and Ambient Scope Locator, but it has been by far the sleekest and most non-intrusive implementation for a unit of work that allows for more fine grained control of the "scope" of the work unit.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Steve Py