Use Action Filters to cut down your context.SaveChanges calls

November 19, 2018 · 2 minute read

If you use Entity Framework Core you will probably find yourself writing something like this over and over and over again…

context.SaveChangesAsync();

Where context refers to an instance of your Entity Framework Database Context.

This raises a few problems.

First of all it’s way too easy to forget and wind up spending minutes (or even hours!) figuring out why your changes aren’t being saved.

Secondly it gets everywhere and adds to the overall amount of “infrastructural” code in your code base which isn’t actually part of your key business logic.

Thirdly (and perhaps most importantly) it tends to wind up being used in inconsistent ways.

So instead of saving everything logically, in one place, you can easily end up with multiple saves occurring at different times and different layers of your application.

This can make your application somewhat unpredictable and also lead to inconsistent state.

If one part of your code fails, but other code has already saved its state to the DB, you’ve now got a job to figure out what was saved and what wasn’t.

ASP.NET Core Action Filters to the rescue

Here’s an alternative.

Use an Action Filter to always make a single call to SaveChanges when any of your MCV Action methods finish executing.

This way you can be sure that the action will either succeed (and everything will be persisted in a predictable fashion) or fail (and nothing will be persisted, leaving the user free to try again, or complain!)

public class DBSaveChangesFilter : IAsyncActionFilter
{
    private readonly MyContext _dbContext;

    public DBSaveChangesFilter(MyContext dbContext)
    {
        _dbContext = dbContext;
    }

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

You can register this in startup.cs and forget all about invoking SaveChanges() anywhere else in your code (and focus on your features instead!).

public void ConfigureServices(IServiceCollection services){

    // rest of code omitted

    services
        .AddMvc(opt => opt.Filters.Add(typeof(DBSaveChangesFilter)))
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

This gives you a handy “transaction per action” and helps avoid your database getting into an inconsistent state.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.