Single Transaction per Request ASP.NET Core With EntityFramework

In this article, we’ll configure ASP.NET to open a single transaction per request and commit it when the request finishes.

single transaction with ef in asp.net

Most APIs work with DB under the hood, and It’s bad practice to commit changes separately a few times during HTTP requests. It can cause a lot of round trips to the database just for one HTTP request. Also, it leads to corrupted data if something goes wrong. The Best approach would be to open a single transaction and only when everything goes well commit the transaction. In such a case, we appreciate data integrity, and if something goes wrong with that transaction, we can revert all changes.

Let’s review an example which related to the real world. Assuming that we have an application with a data layer where we enclose all work with data. I’ll use EntityFramework (EF) as ORM for the database in my example. It doesn’t matter what kind of database to use. We’ll skip everything and concentrate on general logic for a single transaction for simplicity.

Let’s define an example DB context:

1
2
3
public class ExampleContext : DbContext
{
}

It was intentionally left empty because, in our example, we don’t care about what happens inside context.

We need to define an attribute that allows handling all DB change requests in a single transaction. It’s our responsibility to use the next attribute and mark proper actions with a single transaction. We can’t add a global filter because some actions use DB context in read-only mode, and we don’t need to define any transaction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SingleTransactionAttribute : ActionFilterAttribute
{
private readonly ExampleContext _context;

public SingleTransactionAttribute(ExampleContext context)
{
this._context = context;
}

public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var transaction = await this._context.Database.BeginTransactionAsync();
ActionExecutedContext executedContext = await next();
var hasError = executedContext.Exception == null || executedContext.ExceptionHandled;
if (hasError && executedContext.ModelState.IsValid)
{
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
}
}

In a SingleTransactionAttribute, we override the OnActionExecutionAsync method. It allows us to add behavior before and right after the action is executed. We need to begin a new transaction before action execution, and when the action completes without errors, we need to commit the transaction.

Also, as you see, put SaveChangesAsync in this attribute. In order to skip calling it all around the code, we have a single place where we save all changes and commit the transaction. Our business logic code knows nothing when to invoke SaveChangesAsync.

Such assumption strongly relies on the Object tracking mechanism under the hood of EntityFramework, but it’s a topic for another story.

Take a look at basic EntityFramework knowledge

Besides this attribute, we also need to configure Dependency Injection to make a scope per user request and provide a single DbContext instance during the scope:

1
2
3
4
5
6
7
8
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<ExampleContext>();
services.AddScoped<SingleTransactionAttribute>();
}
...

Here is an example of how to do it during application startup. We add both DbContext and SingleTransactionAttribute to the service as scoped items. And rely on default ASP.NET behavior to use the client request as a scope.

I hope this article was useful for someone.