Section 5: Repository Pattern

 


Section 5: Repository Pattern and Unit of Work


Introduction:


In the previous section section-4-pushing-model-to-database, we have seen how we create a table in the DB using the Entity Framework. In this section, we shall learn about Repository Pattern.


What is a Repository Pattern?


We have created a Data-Access layer using the Entity Framework. There should be a communication between the Data-Access (DB) layer and the controller (we shall see what controller in the next section). This communication consists of series of operations. A repository pattern helps in this communication.

In the Repository pattern, we will create a layer between the data access layer and business logic. Think of this layer as a collection of objects in the memory (like a cache), and this layer should have "generic" methods such as Add, Delete, and Get. This "generic" layer is independent of the ORM framework (such as Entity Framework, Dapper) and the database (files, tables, JSON). Hence, if we are changing the ORM framework or the Database, we will have to write "NO" extra code for the CRUD operations. I hope you got the reason why we are using the Repository pattern.

This generic layer is also independent of any application which supports CRUD operations. But, if we need a more application-specific operation, such as "getting the Top Hotels in my City", I would create an application-specific repository, say, HotelRepostity, and derive it from the generic repository. By doing so, HotelRepostity has derived the basic CRUD operations methods. In addition to these methods, HotelRepository will now create a new method call GetTopHotels,

By separating the operations in this way, we minimize the duplicate code for CRUD operations and also helps us in Unit Testing. 

Creating this additional layer will help minimize the duplication of query logic: for instance, if we have to get top Hotels in the city (and this query is very frequent in the code), we need to add the same query many times the code. Using a repository pattern, we will create a function called, GetTopHotelsOfCity() then use this method whenever we need to retrieve top hotels. 

Another most important benefit is that we will decouple our application from the persistence framework, such as Entity Framework. If we need to use a newer framework in our application in the future, we don't have to make major code changes; it would just require a few changes in the framework.

Repository Pattern



Implementation of the Repository Pattern:

Design:

  • Create a generic repository, say, IRepository that has basic CRUD methods.
  • Next, create a repository for the Hotel. This repository should inherit from IRepository. We need this only if we need to add a custom query to Hotel. I will be adding GetTopOldestHotels() as its custom method.

To start with the implementation, we need to implement IRepository. This will be a "generic" interface, which means that it won't depend on our (rather any) application. So, IRepository interface will have following functions: Get(), GetAll(), Find(), Add(), AddRange(), Delete(), DeleteRange().

 public interface IRepository<TemplateEntity> where TemplateEntity: class
    {
        TemplateEntity Get(int id);
        IEnumerable<TemplateEntity> GetAll();
        IEnumerable<TemplateEntity> Find(Expression<Func<TEntity, bool>> predicate);

        // This method was not in the videos, but I thought it would be useful to add.
        TemplateEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate);

        void Add(TemplateEntity entity);
        void AddRange(IEnumerable<TemplateEntity> entities);
        
        void Remove(TemplateEntityentity);
        void RemoveRange(IEnumerable<TemplateEntity> entities);
    }

Next, we will have a Repository class that will define IRepository methods.

 public class Repository<TemplateEntity> : IRepository<TemplateEntity> where TemplateEntity : class
    {
        protected readonly DbContext Context;
        protected readonly DbSet<TemplateEntity> Entity;

        public Repository(DbContext context)
        {
            Context = context;
            Entity = Context.Set<TemplateEntity>();
        }

        public TemplateEntity Get(int id)
        {
            return Entity.Find(id);
        }

        public IEnumerable<TemplateEntity> GetAll()
        {
            return Entity.ToList();
        }

        public void Add(TemplateEntity entity)
        {
            Entity.Add(entity);
        }

        public void AddRange(IEnumerable<TemplateEntity> entities)
        {
            Entity.AddRange(entities);
        }

        public void Remove(TemplateEntity entity)
        {
            Entity.Remove(entity);
        }

        public void RemoveRange(IEnumerable<TemplateEntity> entities)
        {
            Entity.RemoveRange(entities);
        }

        IEnumerable<TemplateEntity> IRepository<TemplateEntity>.Find(Expression<Func<TemplateEntity, bool>> predicate)
        {
            return Entity.Where(predicate);
        }

        TemplateEntity IRepository<TemplateEntity>.SingleOrDefault(Expression<Func<TemplateEntity, bool>> predicate)
        {
            return Entity.SingleOrDefault(predicate);
        }
    }

We have implemented IRepository and Repository. These are generic and can be used for any application. Next, we will implement IHotelRepositaryIHotelRepositary will inherit from IRepository, and also this class will implement its custom methods. As it's custom methods, I will implement GetTopOldestHotels() method. Hence, IHotelRepositary will have all methods of IRepository, and the GetTopOldestHotels() method. 

    public interface IHotelRepository: IRepository<Hotel>
    {
        IEnumerable<Hotel> GetTopOldestHotels(int count);
    }

As the next step, we need to implement HotelRepository to implement its methods.

 public class HotelRepository: Repository<Hotel>, IHotelRepository
    {
        public HotelRepository(HotelDBContext context)
           : base(context)
        {
        }
        public IEnumerable<Hotel> GetTopOldestHotels(int count)
        {
            return HotelDBContext.Hotels.OrderByDescending(c => c.Established).Take(count).ToList();
        }

        public HotelDBContext HotelDBContext
        {
            get { return Context as HotelDBContext; }
        }
    }

Unit of Work

What is the Unit of Work?

As the name says, it is a unit of one transaction (work). By using the Unit of Work, the entire(unit) transaction will fail or succeed. This way, it will help to manage the transaction.

To make it simpler, consider you have two repositories: Hotel and Restaurants. Both Hotel and Restaurants are different repositories and will have their own DBContext. What is the problem if they have their own DBContext? If I save the Restaurants object to the DB and, for some reason, the Hotel Objects are not saved, then we will have inconsistent data in the DB. If we use the Unit of Work, then both Hotel and Restaurants will share the same DBConext. When a set of operations is complete, we will call Unit of Work's methods, Complete/Save. The entire flow/work/transaction is then saved to the DB, or the transaction is failed. This will help us to have a consistent DB.

To implement the Unit Of Work, we will define an interface IUnitOfWork. This interface will have a reference to IHotelRepository. With the current example, you don't see the importance of Unit Of Work, but if we had another Repository, IResturantsRepository, IUnitOfWork would reference IResturantsRepository too. Hence, both these repositories would share the DBContext, which will help to maintain data consistency

    public interface IUnitOfWork: IDisposable
    {
        IHotelRepository Courses { get; }
        int Complete();
    }

Next, We shall implement the class, UnitOfWork, which would inherit the IUnitOfWork and implement its methods.

public class UnitOfWork : IUnitOfWork
    {
        private readonly HotelDBContext _context;

        public UnitOfWork(HotelDBContext context)
        {
            _context = context;
            Hotel = new HotelRepository(_context);
        }

        public IHotelRepository Hotel { get; private set; }

        public int Complete()
        {
            return _context.SaveChanges();
        }

        public void Dispose()
        {
            _context.Dispose();
        }
    }

We shall see how to invoke these methods in the later Sections.

Outroduction:

In this section, we have seen the benefits of IRepository and Unit of Work. In the next section, let us see how we expose vulnerabilities by returning the Model to the View/Client.