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 :-)
Top comments (6)
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 😉
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 :-)
Nice - did not know that! Thanks for the extra details about the transaction.
This tactic has lots of other interesting benefits 👌
I like this idea alot.
Great article! Really nice approach. What can I do in this scenario if I want to return the generated id from the insertion?
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 :-)