The goal of the Repository pattern is to allow consumers to query the database in an object-oriented way. Usually, this implies caching objects and filtering data dynamically. EF Core represents this concept with a DbSet<T> and provides dynamic filtering using LINQ and the IQueryable<T> interface.People also use the term repository to represent the Table Data Gateway pattern, which is another pattern that models a class that gives us access to a single table in a database and provides access to operations such as creating, updating, deleting, and fetching entities from that database table. Both patterns are from the Patterns of Enterprise Application Architecture and are extensively used.Homegrown custom implementations usually follow the Table Data Gateway pattern more than the Repository pattern. They are based on an interface that looks like the following code and contains methods to create, update, delete, and read entities. They can have a base entity or not, in this case, IEntity<TId>. The Id property can also be generic or not:
public interface IRepository<T, TId>
where T : class, IEntity<TId>
{
Task<IEnumerable<T>> AllAsync(CancellationToken cancellationToken);
Task<T?> GetByIdAsync(TId id, CancellationToken cancellationToken);
Task<T> CreateAsync(T entity, CancellationToken cancellationToken);
Task UpdateAsync(T entity, CancellationToken cancellationToken);
Task DeleteAsync(TId id, CancellationToken cancellationToken);
}
public interface IEntity<TId>
{
TId Id { get; }
}
One thing that often happens with those table data gateways is that people add a save method to the interface. As long as you update a single entity, it should be fine. However, that makes transactions that cross multiple repositories harder to manage or dependent on the underlying implementation (breaking abstraction). To commit or revert such transactions, we can leverage the Unit of Work pattern, moving the save method from the table data gateway there.For example, when using EF Core, we can use DbSet<Product> (the db.Products property) to add new products to the database, like this:
db.Products.Add(new Data.Product
{
Id = 1,
Name = “Banana”,
QuantityInStock = 50
});
For the querying part, the easiest way to find a single product is to use it like this:
var product = _db.Products.Find(productId);
However, we could use LINQ instead:
_db.Products.Single(x => x.Id == productId);
These are some of the querying capabilities that a repository should provide. EF Core seamlessly translates LINQ into the configured provider expectations like SQL, adding extended filtering capabilities.Of course, with EF Core, we can query collections of items, fetching all products and projecting them as domain objects like this:
_db.Products.Select(p => new Domain.Product
{
Id = p.Id,
Name = p.Name,
QuantityInStock = p.QuantityInStock
});
We can also filter further using LINQ here; for example, by querying all the products that are out of stock:
var outOfStockProducts = _db.Products
.Where(p => p.QuantityInStock == 0);
We could also allow a margin for error, like so:
var mostLikelyOutOfStockProducts = _db.Products
.Where(p => p.QuantityInStock < 3);
We now have briefly explored how to use the EF Core implementation of the Repository pattern, DbSet<T>. These few examples might seem trivial, but it would require considerable effort to implement custom repositories on par with EF Core’s features.EF Core’s unit of work, the DbContext class, contains the save methods to persist the modifications done to all its DbSet<T> properties (the repositories). Homebrewed implementations often feature such methods on the repository itself, making cross-repository transactions harder to handle and leading to bloated repositories containing tons of operation-specific methods to handle such cases.Now that we understand the concept behind the Repository pattern, let’s jump into an overview of the Unit of Work pattern before going back to layering.