Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to get correct DbContext reference in ASP.NET Core action filters

I wanted to call the entity framework's SaveChangesAsync() for every request in an IAsyncActionFilter.

Here is my IAsyncActionFilter:

DbContextSaveChangesFilter.cs

public class DbContextSaveChangesFilter : IAsyncActionFilter
{
    private readonly DbContext _dbContext;

    public DbContextSaveChangesFilter(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) 
    {
        var result = await next();
        if (result.Exception == null || result.ExceptionHandled)
        {
            await _dbContext.SaveChangesAsync();
        }
    }
}

So, the SaveChangesAsync() will be called after any changes are made by my service methods:

Service.cs

private readonly DbContext _dbContext
public MyService(DbContext dbContext) {
    _dbContext = dbContext;
}

public void SomeServiceMethod() {
    var myEntity = _dbContext.MyEntities.First();
    myEntity.Name = model.Name;
}

Problem

The problem is that I am getting a two different references to _dbContext in the service and in the filter. That is, the _dbContext that is injected into my service is different instance from the one injected in the filter.

Calling _dbContext.GetHashCode() in the filter and in the service results in two different numbers.

Versions

.NET Core: 2.2.402
ASP.NET:   2.2.0
EF Core:   2.2.6
Sql Server:2019 EXPRESS

EDIT: This is how I am configuring my DbContext in Startup.cs

services.AddDbContext<MyDbContext>(opts =>
{
    opts.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

and here is how I am registering my filter inside Startup.cs in ConfigureServices() method:

services.AddMvc(o =>
{    
    o.Filters.Add(typeof(DbContextSaveChangesFilter));
});
like image 413
Leone Avatar asked Oct 12 '25 12:10

Leone


2 Answers

First, you can't do this with IAsyncActionFilter, because OnActionExecutionAsync is

Called asynchronously before the action, after model binding is complete.

So you're too early, which probably means that the filter has received its own context instance from the DI container. Which was also to be expected since the filter is asynchronous. But even with a shared instance SaveChanges wouldn't do anything at that point.

So if you want to do this you need to implement IActionFilter, and use its OnActionExecuted method. I think the DI container will supply you with the same context as the one in the controller, when it's registered as scoped to the request. If not, you might want to play around with ActionExecutingContext's Controller property, but that always requires code constructs in which the controller's context must be publicly (or at least internally) accessible.

As you see, I'm not spelling this out in code, because I think you shouldn't do this. Call SaveChanges visibly where it's needed and nowhere else. You certainly don't want to risk saving changes that aren't supposed to be saved (it's easily overlooked). Nor do you want to call SaveChanges when a method only reads data.

like image 118
Gert Arnold Avatar answered Oct 14 '25 00:10

Gert Arnold


I suspect you are actually injecting MyDbContext into MyService (not DbContext as shown - DbContext would not have a MyEntities property).

The AddDbContext method only adds an instance of MyDbContext to your pipeline, not DbContext.

Try to either:

  1. Change DbContextSaveChangesFilter to take an instance of MyDbContext instead of DbContext:

    private readonly MyDbContext _dbContext;
    
    public DbContextSaveChangesFilter(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    

    or:

  2. Tell your pipeline to also resolve DbContext to MyDbContext:

    services.AddScoped<DbContext, MyDbContext>();
    
like image 41
crgolden Avatar answered Oct 14 '25 00:10

crgolden