Repository Pattern
Repository, as it implies, is a place where things are stored and can be found and retrieved whenever needed. Similarly, in computing terms, a repository is a central location in which data for a specific type of entity or entities are stored and managed either in a form of collection or table.
Most of the applications we developed in the past or will develop in the future need to persist the data in any kind of storage. I am sure those applications must have business logic, data access layer implemented, and must connect between business logic and data access layer to access the data persevered in storage whenever needed. It may not be obvious at first sight, but connecting business logic to data access layer and vice-versa can lead to a lot of duplication in terms of,
- Data Handling
This includes the creation of the same business logic objects from different places and connects with the data access layer to retrieve raw data, filter and make it useful.
- Data Retrieval
This includes writing queries to retrieve all the data or some specific information about a single subject from different places.
- Data Persistence
This includes implementing a logic to persist data as needed throughout different modules and classes of our application.
In this article, we will talk about one of the most popular patterns, i.e., Repository Pattern which is designed to deal with how to connect and decouple business logic and persistence (data storage) and eventually helps us in resolving problems we described above. As defined, repository patterns allow us to:
- define a repository which acts as a mediator between the domain and data mapping layers using a collection-like interface for accessing domain objects.
- define a repository which functions in two ways: data retrieval and data persistence.
Repository Pattern – Deep Dive
As we discussed, repository acts as a mediator between the domain (business layer) and data persistence layer and functions in two ways; i.e., Data Retrieval and Data Persistence.
Data Retrieval
When used to retrieve data from persistence, business layer makes a call to repository method with parameters, then repository will contact the data persistence to retrieve the raw data from the persistence. The persistence will return raw data then the repository will take this data, do the necessary transformations and construct the objects with the data and finally return them as a set of objects (like an array of objects or a collection object as defined) to the Business Logic.
Data Persistence
When used to persist data into the persistence, business layer makes a call to repository method and passes on the data object, then repository extracts the information from an object, performs the necessary transformations and finally repository will persist the data extracted from the objects into the persistence and optionally cache them in a local in-memory list.
When to Use Repository Pattern
Repository Pattern is useful when you want your domain objects (or entities) to be persistence ignorant but have the flexibility to map your data to the choice of your data store e.g. SQL Server, Oracle, NoSQL databases, cache etc. The physical model of the stored data might vary from store to store but not the logical model. So, a repository plays the role of mapping a logical model to the physical model and vice versa. ORM (Object Relational Mapping) tools like Entity Framework do help us to achieve this and we could make use of it wherever possible in building your domain-specific repositories.
Now let’s have a look at the practical example using EntityFramework. Imagine we have an Order Management software where we have Order objects and OrderItem objects. OrderItem belongs to Order and we must find a way to persist them and to retrieve them.
First, design the database table structure, as shown below.
As we are using entity framework (ORM) we need to define DbContext class that acts as a bridge between domain or entity classes and the database. This DbContext class will be used to carry out the following activities:
- Querying: Converts LINQ-to-Entities queries to SQL query and sends them to the database.
- Change Tracking: Keeps track of changes that occurred on the entities after querying from the database.
- Persisting Data: Performs the Insert, Update and Delete operations to the database, based on entity states.
- Caching: Provides first level caching by default. It stores the entities which have been retrieved during the lifetime of a context class.
- Manage Relationship: Manages relationships using CSDL, MSL, and SSDL in Db-First or Model-First approach and using fluent API configurations in Code-First approach.
- Object Materialization: Converts raw data from the database into entity objects.
Now generate a OrderManagementDbContext inheriting Entity Framework DbContext class by adding ADO.Net Entity Data Model (.edmx file).
- public partial class OrderManagementDbContext : DbContext
- {
- public OrderManagementDbContext()
- : base("name=OrderManagementDbContext")
- {
- }
-
- protected override void OnModelCreating(DbModelBuilder modelBuilder)
- {
- throw new UnintentionalCodeFirstException();
- }
-
- public virtual DbSet<Order> Orders { get; set; }
- public virtual DbSet<OrderItem> OrderItems { get; set; }
- }
Now, as mentioned above, create a repository for Order and OrderItem to store in-memory data in form of collections. At the bare minimum, each of the repositories should be able to provide CRUD (Create Read Update Delete) options, for which we will have a generic interface defined and have an implementation which will be inherited from, for specific repositories.
- public interface IRepository<TEntity> where TEntity : class
- {
- IEnumerable<TEntity> Get(
- Expression<Func<TEntity, bool>> filter = null,
- Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
- string includeProperties = "");
-
-
-
-
-
-
- TEntity Get(int id);
-
-
-
-
-
- IEnumerable<TEntity> GetAll();
-
-
-
-
-
-
- IEnumerable<TEntity> Find(System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate);
-
-
-
-
-
-
- TEntity SingleOrDefault(System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate);
-
-
-
-
-
- TEntity FirstOrDefault();
-
-
-
-
-
- void Add(TEntity entity);
-
-
-
-
-
- void AddRange(IEnumerable<TEntity> entities);
-
-
-
-
-
- void Remove(TEntity entity);
-
-
-
-
-
- void RemoveRange(IEnumerable<TEntity> entities);
-
-
-
-
-
- void RemoveEntity(TEntity entityToDelete);
-
-
-
-
-
- void UpdateEntity(TEntity entityToUpdate);
-
- }
- public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : class
- {
- protected readonly OrderManagementDbContext Context;
-
- protected DbSet<TEntity> Entities;
-
-
-
-
-
-
-
-
-
-
- public BaseRepository()
- {
- Context = new OrderManagementDbContext();
- Entities = Context.Set<TEntity>();
- }
-
- public virtual IEnumerable<TEntity> Get(
- Expression<Func<TEntity, bool>> filter = null,
- Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
- string includeProperties = "")
- {
- IQueryable<TEntity> query = Entities;
-
- if (filter != null)
- {
- query = query.Where(filter);
- }
-
- foreach (var includeProperty in includeProperties.Split
- (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
- {
- query = query.Include(includeProperty);
- }
-
- if (orderBy != null)
- {
- return orderBy(query).ToList();
- }
- else
- {
- return query.ToList();
- }
- }
-
-
-
-
-
-
-
- public virtual TEntity Get(int id)
- {
-
-
- return Entities.Find(id);
- }
-
-
-
-
-
- public IEnumerable<TEntity> GetAll()
- {
- return Entities.ToList();
- }
-
-
-
-
-
-
- public IEnumerable<TEntity> Find(System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate)
- {
- return Entities.Where(predicate);
- }
-
-
-
-
-
-
- public TEntity SingleOrDefault(System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate)
- {
- return Entities.Where(predicate).SingleOrDefault();
- }
-
-
-
-
-
- public TEntity FirstOrDefault()
- {
- return Entities.SingleOrDefault();
- }
-
-
-
-
-
- public void Add(TEntity entity)
- {
- Entities.Add(entity);
- Context.SaveChanges();
- }
-
-
-
-
-
- public void AddRange(IEnumerable<TEntity> entities)
- {
- Entities.AddRange(entities);
- Context.SaveChanges();
- }
-
-
-
-
-
- public void Remove(TEntity entity)
- {
- Entities.Remove(entity);
- Context.SaveChanges();
- }
-
-
-
-
-
- public void RemoveRange(IEnumerable<TEntity> entities)
- {
- Entities.RemoveRange(entities);
- }
-
-
-
-
-
-
- public virtual void RemoveEntity(TEntity entityToDelete)
- {
- if (Context.Entry(entityToDelete).State == EntityState.Detached)
- {
- Entities.Attach(entityToDelete);
- }
- Entities.Remove(entityToDelete);
- Context.SaveChanges();
- }
-
-
-
-
-
- public virtual void UpdateEntity(TEntity entityToUpdate)
- {
- Entities.Attach(entityToUpdate);
- Context.Entry(entityToUpdate).State = EntityState.Modified;
- Context.SaveChanges();
- }
- }
-
- public class OrderRepository : BaseRepository<Order>
- {
-
- }
-
- public class OrderItemRepository : BaseRepository<OrderItem>
- {
-
- }
Now look at the main program which calls different methods of repository,
- class Program
- {
- static void Main(string[] args)
- {
- OrderRepository orderRepository = new OrderRepository();
-
- GetOrders();
-
- Order order = new Order
- {
- OrderId = Guid.NewGuid().ToString(),
- OrderDate = DateTime.Now,
- OrderStatus = "In Process"
- };
-
- orderRepository.Add(order);
- order.OrderItems = new List<OrderItem>
- {
- new OrderItem{ OrderId = order.OrderId, ItemName = "Item 1",OrderItemId = Guid.NewGuid().ToString(), Quantity = 1 },
- new OrderItem{ OrderId = order.OrderId, ItemName = "Item 2",OrderItemId = Guid.NewGuid().ToString(), Quantity = 4 }
- };
-
- OrderItemRepository orderItemRepository = new OrderItemRepository();
- orderItemRepository.AddRange(order.OrderItems);
-
- GetOrders();
-
- Console.ReadLine();
- }
-
- private static void GetOrders()
- {
- OrderRepository orderRepository = new OrderRepository();
- OrderItemRepository orderItemRepository = new OrderItemRepository();
- var orders = orderRepository.GetAll();
-
- IEnumerable<Order> orderDatas = orders.ToList();
- if (!orderDatas.Any())
- {
- Console.WriteLine("No Order Placed");
- }
- else
- {
- foreach (var orderData in orderDatas)
- {
- Console.WriteLine("Order {0} placed and currently is {1}", orderData.OrderId, orderData.OrderStatus);
-
- var orderItems = orderItemRepository.Find(x => x.OrderId == orderData.OrderId);
- IEnumerable<OrderItem> items = orderItems as OrderItem[] ?? orderItems.ToArray();
- if (!items.Any()) continue;
- Console.WriteLine("Items included in the order:");
- foreach (var orderItem in items)
- {
- Console.WriteLine(orderItem.ItemName);
- }
- }
- }
- }
- }
Output
As we can see in the above output window that data is inserted into both the tables - Order and OrderItem. In the above example, it was needed to insert data into two tables, Order and OrderItem, through repositories and we can see in the above output window that data is inserted into both the tables, Order and OrderItem, through repositories.
In the above example, we inserted data into two tables separately, but it could lead to a problem where data is inserted into the one table successfully but failed to insert into the other table. So, to avoid this kind of problem, we need to have data all saved in one single transaction, which means if any error occurs in inserting or updating the data we should rollback the complete transaction. UnitOfWork (UOW), is the common pattern that is used for this scenario, by passing around a context object that knows how to commit/save after a set of activities which we will understand and learn in Part 2.
Summary
Finally, as we have seen in the above example, this design pattern can be used for all types of lists and as you start using it, you will see its usefulness. Basically, whenever you must work with several objects of the same type, you should consider introducing a Repository for them. Repositories are specialized by object type and not general. Repository helps in resolving issues of data handling, data persistence, and data retrieval by keeping implementation at one place instead of doing it in many places in the application.