Click here to Skip to main content
15,881,455 members
Articles / Web Development / HTML

Guidance for the Implementation of Repository Pattern and Unit of Work with ASP.NET Core Application

Rate me:
Please Sign up or sign in to vote.
3.88/5 (15 votes)
25 Jan 2019CPOL10 min read 40.2K   1.1K   20   13
Guidance for the Implementation of repository pattern and unit of work with ASP.NET Core application

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

  1. Clean and more readable code
  2. Classes or Objects are loosely coupled
  3. Mocking object is easy

Image 1

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.

  1. User Domain
  2. 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:

  1. User repository
  2. Product repository

To create UserRepository and ProductRepository, create classes that will implement the repository interface IUserRepository, IProductRepository respectively.

IUserRepository

C#
public interface IUserRepository
    {
        void AddUser(User user);
        IEnumerable<User> GetUsers();
        bool DeleteUser(long userId);
        User GetUser(long Id);
    }

IProductRepository

C#
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

C#
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

C#
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

C#
public interface IUnitOfWork
   {
       IUserRepository User { get; }
       IProductRepository Product { get; }
       Task<int> CompleteAsync();
       int Complete();
   }

UnitOfWork

C#
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

C#
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

C#
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()
       {
           // May implement role based access
           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:

C#
// Inject BL
    services.AddScoped<IUser, BLUser>();
    services.AddScoped<IProduct, BLProduct>();
    // Inject unit of work
    services.AddScoped<IUnitOfWork, UnitOfWork>();

After registration, we need to inject this dependency in the controller. Please refer to the below code.

ProductController

C#
[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;
    }

    // GET: api/Product
    [HttpGet]
    public IEnumerable<ProductModel> Get()
    {
        var products = blProduct.GetProducts();
        return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
    }

    // GET: api/Product/5
    [HttpGet("{id}")]
    public IEnumerable<ProductModel> Get(int userId)
    {
        var products = blProduct.GetUserProducts(userId);
        return mapper.Map<IEnumerable<Product>, IEnumerable<ProductModel>>(products);
    }

    // POST: api/Product
    [HttpPost]
    public void Post([FromBody] ProductModel product)
    {
    }

    // DELETE: api/ApiWithActions/5
    [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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
India India
Chennai, India.

Comments and Discussions

 
QuestionMessage Closed Pin
2-Mar-22 20:53
Ravi Kiran 42-Mar-22 20:53 
QuestionUnit test: How to test a method which takes parameters? Pin
xhon23-Nov-20 2:21
xhon23-Nov-20 2:21 
AnswerRe: Unit test: How to test a method which takes parameters? Pin
Jeevanandan J17-Dec-20 21:11
Jeevanandan J17-Dec-20 21:11 
QuestionConnection string & "Products.Persistence" library reference added in "Products" Pin
Tisham Ahuja2-Dec-19 17:55
Tisham Ahuja2-Dec-19 17:55 
AnswerRe: Connection string & "Products.Persistence" library reference added in "Products" Pin
Jeevanandan J24-Sep-20 20:46
Jeevanandan J24-Sep-20 20:46 
QuestionDispose method not working Pin
Free Lancher13-Oct-19 21:07
Free Lancher13-Oct-19 21:07 
AnswerRe: Dispose method not working Pin
Jeevanandan J24-Sep-20 20:50
Jeevanandan J24-Sep-20 20:50 
QuestionHow to do the add , update and delete crud functions in controller Pin
Member 141860803-Jul-19 6:02
Member 141860803-Jul-19 6:02 
AnswerRe: How to do the add , update and delete crud functions in controller Pin
Jeevanandan J26-Aug-19 7:14
Jeevanandan J26-Aug-19 7:14 
GeneralUnit Test Pin
carlos hevia colinas4-Feb-19 4:49
carlos hevia colinas4-Feb-19 4:49 
GeneralRe: Unit Test Pin
Jeevanandan J11-Feb-19 21:42
Jeevanandan J11-Feb-19 21:42 
Questionrepository is evil ? Pin
kiquenet.com4-Feb-19 2:09
professionalkiquenet.com4-Feb-19 2:09 
AnswerRe: repository is evil ? Pin
Jeevanandan J12-Feb-19 0:33
Jeevanandan J12-Feb-19 0:33 
GeneralMy vote of 4 Pin
adnanfu24-Jan-19 22:50
adnanfu24-Jan-19 22:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.