Introduction
The Repository Pattern is one of the most popular design patterns used for abstracting the data persistence in the database or retrieving from the database. The fundamental objective of the Repository Pattern is to help you decouple the data access and business logic layers of an application.
In this article, we’ll discuss the Repository Pattern. We'll learn why it is considered an anti-pattern and when we can use it in applications. Also, we'll illustrate its implementation with code examples in C#.
Let’s start.
Pre-requisites
You’ll need the following tools to deal with code examples,
- Visual Studio 2019 Community Edition (download)
- SQL Server 2019 Developer Edition (download)
- Entity Developer (download)
The Community Edition of Visual Studio 2019 and the Developer Edition of SQL Server 2019 are free editions. Entity Developer offers a fully functional 30-days Free Trial which we’ll use in our scenarios.
What is a Repository Pattern and Why We Should Care?
A Repository Pattern is a design pattern used to decouple the application's business logic and data access layers.
It was first introduced as part of Domain-Driven Design in 2004. Since then, it has become very popular. Now, it is the design pattern of choice for abstracting calls from the application to the underlying database.
But why is Repository Pattern so essential?
A repository, in essence, acts as a bridge between your application's domain and data mapping layers. When used correctly, it improves testability, code extensibility, and maintenance. With the Repository design pattern applied, the business logic layer of the application does not need to understand how data persistence works beneath the surface.
In other words, a repository abstracts the data storage and retrieval mechanism from the application.
This isolation enables the developers to focus on the business logic components rather than write boilerplate code to perform CRUD operations against databases. It also helps in unit testing the application's code since the business logic code is abstracted from the data access logic. You can change the data access code without impacting the work of the application works.
Assume that you introduce new database objects (tables, stored procedures, etc.). You can create a corresponding entity class in your application and write a few lines of data mapping code. You might (though, rarely) also need to change the database type altogether (from Oracle to SQL Server, PostgreSQL, etc.).
By leveraging the Repository Pattern, you can build and test the data access logic and the business logic of an application separately. It helps you to adhere to the Don't Repeat Yourself (DRY) principle since you don't need to repeat the code for performing CRUD operations.
In that case, you would need to change your data access code – update the repository classes. You might also want to change a few lines of code in the application, but that would be minimal.
What is an Anti-Pattern?
An anti-pattern is usually an ineffective solution to a problem. Anti-patterns are ineffective programming techniques that create issues rather than solve them and emerge due to over-engineering, incorrect application of design patterns, not following recommended practices, etc. On the other hand, anti-patterns are recurring solutions to common software application problems. Some common examples are spaghetti code, dead code, God object, etc.
The objective observation of functional and non-functional requirements will help you choose correct application patterns, frameworks, and platforms. You might not select a design pattern simply because you saw someone else use it or because someone told you there was no harm in using it. But an anti-pattern can help you determine the appropriate pattern you can use for your problem statement and available solutions.
An Extra Layer of Abstraction
One of the biggest downsides of the Repository Pattern is adding an extra layer of abstraction which eventually can become overkill for your application. Besides, you would typically need to create a repository for each entity in your application.
Things deteriorate as you include additional methods and complex search capabilities in your repository. You'll wind up with a repository that closely matches the permanent storage layer in use underneath. As an example, you might need methods such as FindProductById, FindCustomerById, etc. Such methods are present in the mature ORM frameworks. It implies that you are creating an abstraction on top of another abstraction for no good reason.
Downsides of a Generic Repository
In an application, the domain model and the persistence model have separate roles. The domain model’s behavior deals with real-world issues and solutions. The persistence model serves to represent how the application's data is saved in the data storage.
The Repository Pattern should encapsulate the persistence logic and conceal the underlying implementations of the data storage ways. The operations in repositories should be expressive rather than generic.
For instance, you cannot have a Generic Repository containing operations that you may use in any situation. As a result of this needless abstraction, the generic repository design becomes an anti-pattern.
A Generic Repository does not provide a meaningful contract. Therefore, you need to create a specific repository that extends the Generic Repository and offers a precise set of operations relevant to that particular entity.
Create a new ASP.NET Core Web API Project
Earlier, we mentioned the necessary tools to proceed to the practical scenarios. The time has come to use those tools.
First, we need to create a new ASP.NET Core Web API project,
- Open Visual Studio 2019.
- Click Create a new project.
- Select ASP.NET Core Web Application and click Next.
- Specify the project name and location to store that project in your system. Optionally, checkmark the Place solution and project in the same directory checkbox.
- Click Create.
- In the Create a new ASP.NET Core Web Application window, select API as the project template.
- Select ASP.NET Core 3.1 or later as the version.
- Disable the Configure for HTTPS and Enable Docker Support options (uncheck them).
- Since we won’t use authentication in this example, specify authentication as No Authentication.
- Click Create to finish the process.
We’ll use this project in this article.
A Generic Repository is an Anti-Pattern
You can take advantage of the Entity Developer tool to generate the Entity Data Model and repository classes. Using this tool simplifies the tasks significantly.
Select the project that we have created earlier. Specify Repository and Unit of Work as the code generation template when creating the Entity Data Model:
Figure 1: Specify the Code Generation Template in Entity Developer
This would generate the Irepository and IProductRepository interfaces, and the EntityFrameworkRepository (the generic repository) and ProductRepository classes. It would also generate the entity classes and the unit of work classesandinterfaces.
The Generic Repository generated by Entity Developer would look like below:
public partial class EntityFrameworkRepository < T > : IRepository < T > where T: class {
private DbContext context;
protected DbSet < T > objectSet;
public EntityFrameworkRepository(DbContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
this.context = context;
this.objectSet = context.Set < T > ();
}
public virtual void Add(T entity) {
if (entity == null) {
throw new ArgumentNullException("entity");
}
objectSet.Add(entity);
}
public virtual void Remove(T entity) {
if (entity == null) {
throw new ArgumentNullException("entity");
}
objectSet.Remove(entity);
}
public DbContext Context {
get {
return context;
}
}
}
The code generator will generate the IRepositoryinterface as well:
public partial interface IRepository < T > {
void Add(T entity);
void Remove(T entity);
}
You can register an instance of the EntityFrameworkRepositoryclass as a scoped service to use it in the controller classes or elsewhere in the application.
public void ConfigureServices(IServiceCollection services) {
services.AddScoped < IRepository < Product >> (x => {
return new EntityFrameworkRepository < Product > (new DataModel());
});
services.AddControllers();
}
Now, you can use the dependency injection in your controllers to retrieve this instance.
The Generic Repository works fine as long as you perform simple CRUD operations. If you need specific methods, such as GetAllExpiredProducts, you’ll have to write a custom code in the ProductRepositoryclass. The generated class would look as follows:
public partial class ProductRepository {}
Here you have to write your own implementation of the GetAllExpiredProducts.
So, besides the generic repository that you can use in simple cases only, you'll always need specific repository classes for each entity class in your application to address such issues.
Use the Repository Pattern for applications that don’t perform complex operations.
Summary
In this article, we've discussed the pros, cons, and some common pitfalls you may face when using the Repository Pattern. Hope this information will be helpful in your further work.