DEV Community

Cover image for Use Action Filters to cut down your context.SaveChanges calls
Jon Hilton
Jon Hilton

Posted on • Originally published at jonhilton.net

Use Action Filters to cut down your context.SaveChanges calls

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.

Want to get these articles first? Click here to have me email them to you :-)

Discussion (6)

Collapse
jamesmh profile image
James Hickey

Does this work because the DB context is a scoped DI service?

Assuming you had, let's say, multiple services that individually made changes via the DB context - all those changes would be saved in bulk at once?

Sounds like that would be a potential performance benefit in generic scenarios too eh?

Very interesting approach 😉

Collapse
jonhilt profile image
Jon Hilton Author

Hi James, yep you've got it.

This essentially gives you a transaction per web request, so you can be confident the operation either worked or it didn't (and you won't get into a "half n half" state where some things are persisted to the database but others aren't).

By default ASP.NET Core will commit everything in one transaction when you call SaveChanges, so every modified entity will persisted in one go :-)

Collapse
jamesmh profile image
James Hickey

Nice - did not know that! Thanks for the extra details about the transaction.

This tactic has lots of other interesting benefits 👌

Collapse
garfbradaz profile image
Gareth Bradley

I like this idea alot.

Collapse
leolima32 profile image
Leonardo Ferreira

Great article! Really nice approach. What can I do in this scenario if I want to return the generated id from the insertion?

Collapse
jonhilt profile image
Jon Hilton Author

Thanks Leonardo,

As to your question. Yeah that's a little tricky.

I've tended towards using GUIDs recently for this exact reason. I don't like the idea of having to round-trip to the database to get an id for an entity.

Apart from anything it makes testing harder because the database ends up being involved in something which could otherwise be tested entirely by spinning up instances of entities in memory and exercising them without ever hitting the database.

I guess a pragmatic solution is to still call SaveChanges where absolutely necessary (to generate an id for example) but try to engineer away from it where you can :-)