Introduction
When implementing repository pattern after learning, I encountered many problems for proper implementation. But I didn't find a full solution anywhere for the proper implementation. This pushed me to write an article.
This article will guide you through creating a small application using repository pattern with Unit of Work in ASP.NET Core. This article is basically targeted at beginner to intermediate level programmers. In this article, I want to provide an overall picture of the implementation.
Here, I don't want to provide details for generic repository pattern implementation. Whenever I search for the repository pattern implementation, I came across lots of samples with the generic repository pattern.
After completing this article, you will get a proper understanding of the implementation for specific repository pattern.
Repository Pattern
A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
Repository pattern is useful when we want to encapsulate the logic to access data source. Here, Repository describes class or component to access the data source.
The repository acts as a mediator between the data source and the business layers of the application. It queries the data source for the data, maps the data from the data source to a business entity, and persists changes in the business entity to the data source.
Why do we want to encapsulate?
When we want to decouple data access functionality from the business layer, then we move to Repository pattern.
Generic Repository is useful when it defines generic methods for the most common types of data operation, such as updating, fetching and deleting.
In some scenarios, we may not need a common operation for all type of repositories.
So we need specific repositories. This is based on the project that we are going to implement.
In the Repository pattern implementation, Business logic to Data access logic and API to Business Logic talk to each other using interfaces. Data access layer hides the details of data access from the business logic. In detail notation, business logic can access the data access layer without having knowledge of data source.
For example, the Business layer does not know whether the Data access layer uses LINQ to SQL or ADO.NET, etc.
Advantages
The following are key benefits of the Repository pattern.
Isolate Data Access Logic
Data access functionalities are centralised. So the business layer will not have knowledge about where the data comes from. It may come from any data source or cache or mock data.
Unit Testing
Based on the previous, this would understand that the business layer doesn't have knowledge about where the data comes from. It's easy to mock the data access layer. So this will help us to write Unit test for the business logic.
Can't we write any test for the Data Access Layer? Why not? We can write an integration test for this layer.
Caching
Since Data Access functionalities are centralised, we can implement caching for this layer.
Data Source Migration
We can easily migrate from one data source to another data source. This will not affect our business logic when we migrate.
Complex Queries Are Encapsulated
Complex queries are encapsulated and moved to this layer. So queries are reused from the Business layer.
When any developer is strong in writing queries, she/he can independently work on the queries and another developer can concentrate on the business logic.
Thumb Rule of the Implementation
- Each repository should be implemented based on Domain and not based on the Database Entity.
- Each repository should not contact each other.
IQueryable
should not be a return type of the repository pattern implementation. They should return only the IEnumerable
. - They should not save/delete/add any data to the database. All the details should do in memory. We may think about how we can do crud operations. Here, the Unit Of Work plays that role. Unit Of Work will save details to database or rollback. What are the advantages of this? This will save multiple transactions that happened in the repository in a single shot.
- Data Layer should not implement business logic. Business logic should be implemented in the Business Layer. They should return the representation of data and business layer should encapsulate return or decapsulate requst.
Project Structure
The following is our project structure that we are going to implement. Please download the sample from the link. Here PL uses Angular application. ASP.NET Core has been used for API and the Business Layer, then Data Access Layer.
Business Layer and Data Access Layer will have separate contracts (interfaces). Business Layer and Data Access Layer will depend on the abstraction not with the concrete implementation.
This is because of Dependency Injection. So no layer will have knowledge about another layer. This is easy when we mockup and do testing.
- Presentation Layer (PL)
- API
- Business Layer (BL)
- Data Access Layer (DAL)
Please refer to the following image for the Application Flow. PL will contact API. API will contact BL. BL will contact DAL.
We are going to do a loosely coupled implementation. Business Layer will not know about the data access layer. API will not know about the BL.
For this implementation, we are going to implement Dependency Injection (DI).
Dependency Injection (DI)
What is dependency injection?
The higher level module should not depend on the lower level module. Dependency Injection is mainly for injecting the concrete implementation into a class that is using abstraction, i.e., interface inside. This enables the development of the loosely coupled code.
In detail, if your ClassA
needs to use a ClassB
, make our ClassA
aware of an IClassB
interface instead of a ClassB
. Through this execution, we can change the implementation of the ClassB
many times without breaking the host code.
Advantages of DI
- Clean and more readable code
- Classes or Objects are loosely coupled
- Mocking object is easy
Using the Code
Consider the following sample for this implementation.
- CRUD operation for the user
- CRUD operation for the product
- Add or remove product to/from user. Only one product can be assigned to the user.
Data Access Layer
Now we have to identify the domains for the problem. Based on the above sample, we identified two Domains.
- User Domain
- Product Domain
Based on the thumb rule, we need to create a repository based on the domain. So in this sample, we are going to create two repositories for the above two domains:
- User repository
- Product repository
To create UserRepository
and ProductRepository
, create classes that will implement the repository interface IUserRepository
, IProductRepository
respectively.
IUserRepository
public interface IUserRepository
{
void AddUser(User user);
IEnumerable<User> GetUsers();
bool DeleteUser(long userId);
User GetUser(long Id);
}
IProductRepository
public interface IProductRepository
{
void AddProduct(Product product);
Product GetProduct(long id);
IEnumerable<Product> GetProducts();
bool DeleteProduct(long productId);
IEnumerable<Product> GetUserProducts(long userId);
void AddProductToUser(long userId, long productId);
}
Now create concrete classes that will implement the abstractions, i.e., interface.
These concrete classes will have the actual implementation. Here, we can notice that:
- Every Add or delete is implemented in memory, not in the data source
- There is no update to the Data source.
UserRepository
public class UserRepository : IUserRepository
{
private readonly AppDbContext context;
public UserRepository(AppDbContext dbContext)
{
this.context = dbContext;
}
public void AddUser(User user)
{
context.Users.Add(user);
}
public bool DeleteUser(long userId)
{
var removed = false;
User user = GetUser(userId);
if (user != null)
{
removed = true;
context.Users.Remove(user);
}
return removed;
}
public User GetUser(long Id)
{
return context.Users.Where(u => u.Id == Id).FirstOrDefault();
}
public IEnumerable<User> GetUsers()
{
return context.Users;
}
}
ProductRepository
public class ProductRepository : IProductRepository
{
private readonly AppDbContext context;
public ProductRepository(AppDbContext dbContext)
{
this.context = dbContext;
}
public void AddProduct(Product product)
{
context.Products.Add(product);
}
public void AddProductToUser(long userId, long productId)
{
context.UserProducts.Add(new UserProduct()
{
ProductId = productId,
UserId = userId
});
}
public bool DeleteProduct(long productId)
{
var removed = false;
Product product = GetProduct(productId);
if (product != null)
{
removed = true;
context.Products.Remove(product);
}
return removed;
}
public Product GetProduct(long id)
{
return context.Products.Where(p => p.Id == id).FirstOrDefault();
}
public IEnumerable<Product> GetProducts()
{
return context.Products;
}
public IEnumerable<Product> GetUserProducts(long userId)
{
return context.UserProducts
.Include(up => up.Product)
.Where(up => up.UserId == userId)
.Select(p => p.Product)
.AsEnumerable();
}
}
Unit Of Work(UOW)
From the above implementation, we can understand that the repository should be used:
- to read data from the data source
- to add/remove data in memory
Then how the add/update/delete will affect the data source? Here the UOW plays that role. UOW knows about each repository. This helps to achieve multiple transactions at a time.
For this implementation, need to achieve as above. Create a concrete UnitOfWork
that will implement the abstraction, i.e., interface IUnitOfWork
.
IUnitOfWork
public interface IUnitOfWork
{
IUserRepository User { get; }
IProductRepository Product { get; }
Task<int> CompleteAsync();
int Complete();
}
UnitOfWork
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext dbContext;
public UnitOfWork(AppDbContext dbContext)
{
this.dbContext = dbContext;
}
private IUserRepository _User;
private IProductRepository _Product;
public IUserRepository User
{
get
{
if (this._User == null)
{
this._User = new UserRepository(dbContext);
}
return this._User;
}
}
public IProductRepository Product
{
get
{
if (this._Product == null)
{
this._Product = new ProductRepository(dbContext);
}
return this._Product;
}
}
public async Task<int> CompleteAsync()
{
return await dbContext.SaveChangesAsync();
}
public int Complete()
{
return dbContext.SaveChanges();
}
public void Dispose() => dbContext.Dispose();
}
We have done repository pattern implementation with UOW for the DAL.
The following would be silly. After doing this, I had a confusion about how we need to get data from another repository to check before saving data. For example, when adding the product to the user, check whether the user or product exists or not.
This scenario will violate the rule, i.e., repositories should not interact within them. What happened? What should I do now? Here, my understanding was wrong. Business logic should not present in the repository pattern. This is only an encapsulation of data access. Every logic validations should be moved to Business Layer. Business Layer will know about all the repository that will take care of the validation.
Business Layer
Now we need to concentrate on the Business Layer. In this layer, we are going to inject the UOW instead of all the necessary repositories. UOW knows about all the repositories and we can access using UOW.
For example, to implement the Product
's BL, we are going to create an interface IProduct
and need to create a concrete class BLProduct
that will implement the IProduct
.
Below in BLProduct
, all the necessary validations and business logic have been done and we can notice in AddProductToUser
method as an example for the multiple repository usages.
IProduct
public interface IProduct
{
Product UpsertProduct(Product product);
IEnumerable<Product> GetProducts();
bool DeleteProduct(long productId);
IEnumerable<Product> GetUserProducts(long userId);
bool AddProductToUser(long userId, long productId);
}
BLProduct
public class BLProduct : IProduct
{
private readonly IUnitOfWork uow;
public BLProduct(IUnitOfWork uow)
{
this.uow = uow;
}
public bool AddProductToUser(long userId, long productId)
{
if (userId <= default(int))
throw new ArgumentException("Invalid user id");
if (productId <= default(int))
throw new ArgumentException("Invalid product id");
if (uow.Product.GetProduct(productId) == null)
throw new InvalidOperationException("Invalid product");
if (uow.User.GetUser(userId) == null)
throw new InvalidOperationException("Invalid user");
var userProducts = uow.Product.GetUserProducts(userId);
if (userProducts.Any(up => up.Id == productId))
throw new InvalidOperationException("Products are already mapped");
uow.Product.AddProductToUser(userId, productId);
uow.Complete();
return true;
}
public bool DeleteProduct(long productId)
{
if (productId <= default(int))
throw new ArgumentException("Invalid produt id");
var isremoved = uow.Product.DeleteProduct(productId);
if (isremoved)
uow.Complete();
return isremoved;
}
public IEnumerable<Product> GetProducts()
{
return uow.Product.GetProducts();
}
public IEnumerable<Product> GetUserProducts(long userId)
{
if (userId <= default(int))
throw new ArgumentException("Invalid user id");
return uow.Product.GetUserProducts(userId);
}
public Product UpsertProduct(Product product)
{
if (product == null)
throw new ArgumentException("Invalid product details");
if (string.IsNullOrWhiteSpace(product.Name))
throw new ArgumentException("Invalid product name");
var _product = uow.Product.GetProduct(product.Id);
if (_product == null)
{
_product = new Product
{
Name = product.Name
};
uow.Product.AddProduct(_product);
}
else
{
_product.Name = product.Name;
}
uow.Complete();
return _product;
}
}
Here in AddProductToUser
method, I want to add a product to a user. So before adding product to the user, I have done the following validations in the method:
- Parameter validations
- Check whether the product is deleted or not
- Check whether the user exists or not
- Check whether the product is already added to the user or not
- Finally, add the product to the collections
After doing the above steps, finally, save the user product.
In UpsertProduct
method, we are going to achieve add or update. If the product is not available, then add. If the product is available, then update. For this:
- Need to check for the valid values
- Then try to get the product and check product is available
- If it is not available, then add to the collection
- If it is available, then update the necessary value in the collection
After doing the above, then save the values.
What does it mean? It helps to control when we can do save values. We did not save immediately when we are adding or updating. We can do many more operations here, then finally we can save.
API
As we are in the flow, we can see that we have done the DAL and BL. Now we inject the BL in the API and do the necessary action.
Here, I am using the ASP.NET CORE. We need to register the dependency in service container as below:
services.AddScoped<IUser, BLUser>();
services.AddScoped<IProduct, BLProduct>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
After registration, we need to inject this dependency in the controller. Please refer to the below code.
ProductController
[Route("api/Product")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IMapper mapper;
private readonly IProduct blProduct;
public ProductController(IMapper mapper, IProduct product)
{
this.mapper = mapper;
this.blProduct = product;
}
[HttpGet]
public IEnumerable<ProductModel> Get()
{
var products = blProduct.GetProducts();
return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
}
[HttpGet("{id}")]
public IEnumerable<ProductModel> Get(int userId)
{
var products = blProduct.GetUserProducts(userId);
return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
}
[HttpPost]
public void Post([FromBody] ProductModel product)
{
}
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
Controversy
When I start to learn and implement the repository pattern, I found many articles that we should not implement Repository pattern with Entity Framework (EF).
Why?
Because EF is implemented with the Repository Pattern and Unit of Work. Why do we need a Layer to another layer which is implementing with the same pattern?
Yeah, this sounds good. Right?
My Conclusion
Yeah, the above is a good point. After thinking about the following, I have concluded that we are not wrong when implementing the repository pattern with the EF.
- In future, if we are going to migrate the ORM for any kind of issue, then our implementation part gives a better solution for the migration.
- We can move our complex and bulk queries inside the DAL.
- When we are going to do Unit test, then this implementation gives an easy way to mock the DAL.
- We can concentrate only on the DAL for Caching implementation.
Points of Interest
When I start to implement the repository pattern, I did not find proper guidance. I hope the article will provide a proper idea for the developer who is seeking the proper way of implementation.
Github
References