Agilix
Published in

Agilix

Entity Framework Core: One transaction per server roundtrip

Some months ago, I wrote ASP.NET Core, One transaction per server roundtrip. Now, working with EF Core, I thought it would be swell to write a follow-up article using Entity Framework Core.

To persist data, using the DbContext class, a simple call to SaveChanges is required. But sometimes, in a workflow (registering a user), multiple saves are required. Your next piece of business logic might require previously saved data from the DbContext, to get access to this data, you need to call SaveChanges first. Otherwise, the DbContext won’t pick this up.

Example

We’ll cover a complex registration process in this article, to demonstrate the benefits of having one transaction for this workflow.

When a new member registers, we’ll save the member data (first name, last name), create three goody bag shipments (welcome package, first year and fifth year-packages) and bank transfers for the lifetime of two years, this can be done in monthly, quarterly or yearly installments.

As you see, this logical unit of work contains writing data to three entities, member, shipments and banktransfers.

Setup

The goal for this article is to handle this logic in a REST call, and roll back the entire transaction in case anything goes wrong. We’ll configure an EF Core DbContext to use a scoped transaction and wrap the transaction in an MVC filter that catches any exception during the scoped HTTP Request/Response execution. We’ll have three services in this solution, member, shipment and banktransfer. They will consume a scoped DbContext that operates on the scoped transaction, providing us with the benefit of the ability to call SaveChanges and access our temporary saved data in each service.

Pipeline setup

The TransactionFilter will work as a cover for our logic, it wraps every HTTP REST call in a transaction. In the best case, everything succeeds and the transaction is committed, data is saved to the database.

The REST call looks as follows.

A member is created using the MemberService, we get the memberId as a result, this is used to call the ShipmentService and BankTransferService, in order to fill the Foreign Keys from the Shipment and BankTransfer to the Member.

You need to consider the ShipmentService and BankTransfer to be isolated services, they have the responsibility to validate the existence of the member that is passed to them. In order to validate the existence of a member, the member needs to be persisted, or at least added to the DbContext. This is why it is critical for each service to save its changes to the DbContext. By telling EF Core’s DbContext to use an external transaction, EF Core defers the saving of the data to the transaction. This allows us to commit or rollback all changes made within that transaction from a coordinating piece of middleware, the TransactionFilter

With this setup, an error occuring during this worflow will result in a rollback of the data saved in that workflow.

Rollback of data, when the BankTransfer Service throws an error

Ready for some code ? The ServiceCollectionExtensions will handle the hard work of registering our services, DbContext using a transaction and the TransactionFilter:

This simplyfies our Startup file:

The TransactionFilter class looks simple enough, it is just an MVC filter that’s inserted at the start of the MVC pipeline.

Whenever an underlying middleware throws an exception, the entire transaction for the REST call is rolled back.

A fully working example can be found in the accompanied GitHub Repo.

To run the example, you just need to modify the appsettings.json file to configure the connection string. The database is generated automatically using EF Core migrations as you can see in the Program file using the WebHostExtensions.

Using PostMan, we can test our application.

And the data will be created.

Rollback of data, when the BankTransfer Service throws an error

If we submit an invalid bankTransferRecurrency type in the JSON payload, the Enum can’t be parsed in the BankTransferService and throws an exception in the last part of the pipeline.

As expected, the transaction is rolled back and no data is persisted.

Please check out my previous article if you need a more in-depth explanation.

Working with migrations

When using EF Migrations in combination with this setup, you might run into errors when applying the migrations during the application startup.
An easy fix for this might be to run your migrations as part of a separate process, in another console app.

To get migrations working using this setup, you’ll need to set up the resolution of the RegisterDbContext. If we resolve this service, it will come configured to run within a transaction.
This will conflict with the migrate method. To fix this, you’ll need to register the RegisterDbContext using another key and resolve this key when applying the migrations.

First, we’ll create a marker interface called IMigrationContext, this will indicate that whatever is implemented by this interface, will contain the logic to migrate a database.

We’ll let the RegisterDbContext inherit this marker interface.

Now let’s write the plumbing to provide this context using the IMigrationContext key, the following code is added to the ServiceCollectionExtensions.cs file.

You see, we’re re-registering the RegisterDbContext to the service collection, but with another key (IMigrationContext) and another configuration.

This is a powerful technique, it can be used to request a single service using various different configurations, using different registrations by registering them using different marker interfaces.

Now, we can apply the EF Migrations upon application startup, the Startup.cs file will look like this.

Happy migrations!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store