Combining repository pattern and unit of work using EntityFramework Core

Emre Teoman
Borda Technology

--

When we have started to develop a layered architecture by following clean architecture rules, an ORM(Object-Relational Mapping) structure was needed between the application layer and data access layer, for various reasons such as separating object model from persistence model. When the development platform is based on .NET Core along with Entity Framework Core (EF Core) the need to form a structure between these layers has emerged.

Lets take a look to the definition of Microsoft:

Entity Framework (EF) Core is a lightweight, extensible, open source and cross-platform version of the popular Entity Framework data access technology. EF Core can serve as an object-relational mapper (O/RM), which:

- Enables .NET developers to work with a database using .NET objects.

- Eliminates the need for most of the data-access code that typically needs to be written.

Although EF Core is a capable and developer-friendly framework, it is not always advantageous to use DbContext directly in the application layer. Let’s give some examples of these disadvantageous cases:

Abstraction of data access layer: EF Core is able to work with widespread relational database systems. At that point, EF Core itself can be thought of as an abstraction. However, if you want to use a different persistence technology other than EF Core for various reasons, this may cause the entire application layer to change.

To avoid code repetition: It may be necessary to group the same data access operations under a class or method to avoid code duplication.

Data ‘modification/filter’ ‘before/after’ on ‘read/write’ operations: There may be a need to intervene in data reading and writing such as handling soft delete or auditing records.

Hiding persistence layer models from the application layer: Layers should hide implementation details from other layers. The application layer should not be affected by changing data access technologies.

All of the examples given above can be extended; in order to eliminate these disadvantages, the use of the repository pattern can be a viable solution. Currently, there are many discussions about whether or not to use the repository pattern with EF Core, and strong arguments are put forward for both cases. Due to the topic of this article, we will be looking at the scenario where we use the repository pattern. If you simply search for EF Core with the repository pattern, you will come across a lot of examples that include the implementation of the generic repository pattern. Even if the repository pattern violates single responsibility principle, it is widely used. Let’s assume we are using the generic repository pattern and look at a widely used generic repository interface.

The example shown above is a simple generic repository used for CRUD (Create-Read-Update-Delete) operations. Methods and parameters in the interface can be varied. However, the focus of this article is the Save method.

While developing ASP.NET Core Web API, we use dependency injection very widely. To use EF Core, we register our DbContext class at startup as follows:

As a best practice, DbContext is registered as scoped service because singleton lifetime means only one connection for all requests and transient lifetime means losing transaction functionality. The picture below explains the lifetime of .NET Core services.

Taken from the book Entity Framework Core In Action, Jon P. Smith

After the DbContext class is registered as scoped, all data access operations are made over the same connection during the request. What about Save methods in repositories? Let’s write an implementation of the interface given above.

Let’s make this a little more interesting. EF Core is already implementing the repository pattern. How? Take a look into DbSets in DbContext. We can make data access via these DbSets. In addition to that, EF Core applies the unit of work pattern also with SaveChanges() method.

All changes applied over the same DbContext instance will be executed atomically with a single SaveChanges. Either all will be committed or rollbacked. So does the Save method in the generic repository pattern provide this? Let’s continue with an example:

ARepository aRepo = new ARepository();
BRepository bRepo = new BRepository();
aRepo.Add(something);
bRepo.Add(otherthing);
aRepo.Save();
bRepo.Save();

What is the problem in this example? Since DbContext is registered as scoped, both saves will call the SaveChanges method over the same DbContext instance. In order to apply both additions to the database, we made unnecessary database access and execution. It can be handled only one Save. The part that confuses is which save method should be called: aRepo.Save() or bRepo.Save().

ARepository aRepo = new ARepository();
BRepository bRepo = new BRepository();
aRepo.Add(something);
bRepo.Add(otherthing);
aRepo.Save(); // or bRepo.Save();

Any of them can be called but it is definitely not clean. The other point is about atomicity. What would happen if both changes had to be made atomically? Again, which save should be called? DbContext can do that using only SaveChanges. At this point, it is necessary to take the responsibility of the save operation from the repository and move it to another structure: UnitOfWork! Accordingly, a UnitOfWork definition and implementation can be made as follows.

Basically, UnitOfWork is only responsible for calling SaveChanges. Let’s update our example with the unit of work implementation:

ARepository aRepo = new ARepository();
BRepository bRepo = new BRepository();
IUnitOfWork uow = new UnitOfWork();
aRepo.Add(something);
bRepo.Add(otherthing);
uow.Complete();

In the above structure, we have used the repository pattern by combining the unit of work pattern. Thus, the repository and unit of work patterns are decoupled like in EF Core itself. To keep it simple, we have just mentioned the SaveChanges part, but as you can guess, the UnitOfWork class will also be used for transaction management.

Here, there is a complete example of UnitOfWork class that includes transaction management.

Assumed that application’s DbContext is registered in Startup.

To summarize, if we want to use EF Core via a repository pattern in layered clean architecture, it will not be enough by itself. Using a separate unit of work structure for SaveChanges and transactions will be a cleaner solution.

Thanks for reading.

--

--