ASP.Net Core 7: Unit Of Work with the Repository pattern boilerplate. Part 1

Maxim Stanciu
9 min readDec 8, 2022

--

Intro

There are many articles on the net about the UoW and Repository patterns implementation. This is an attempt to consolidate all the knowledge and crystalize the solution project using asp.net core v.7

This implementation does not include the decomposition of layers but pursuit the DDD-like style of coding including the CQRS (Mediator) and EntityFramework Code-First approach.

Nowadays seems become a golden standard to follow the “Clean Architecture” principles, so we’re willing to follow this as well.

In this part we overview how to:

  • Setup a new Web API Project (.net core 7)
  • Create the Domain Entities Layer
  • Create the Resource models for Domain Entities and configure AutoMapper profiles (as a part of the Presentation Layer)
  • Create a Persistence Layer using EntityFramework Core + SQLite
  • Implement the UnitOfWork and Repository patterns

Step #1 — Create a new WebAPI Project

First of all Open Visual Studio and select a new empty Web API Project. The initial project configuration should be as in the screenshots below:

As the next step we need to clean up the boilerplate VS project by executing the steps below:

  • Delete ./WeatherForecast.cs file.
  • Delete ./Controllers/WeatherForecastController.cs file
  • Add the new NuGet dependencies by right click on the “Dependencies” folder of the project and select “Manage NuGet packages” option:

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Sqlite
Microsoft.EntityFrameworkCore.Tools
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection

Configure the Domain Entities

We’re using the Entity Framework Code First strategy, so create a couple of entities classes for our repositories:

  • Create a new folder ./Entities
  • Create a new file ./Entities/IEntity.cs which is an “Aggregate Root” with the code as below:
using System.ComponentModel.DataAnnotations;

namespace UoWDemo.Entities
{
public record IEntity
{
[Key]
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
}

This class can be a huge improvement since you don't need to perform run-time type checks prior to casting a reference because you don't need a cast anymore.

For example, Add(TEntity) without generics would look as Add(Entity) which would force the runtime to perform an upcast (f.e. Add(new DerivedEntity()) which upcasts DerivedEntity to Entity).

  • Create a new file ./Entities/Person.cs
using System.ComponentModel.DataAnnotations.Schema;
namespace UoWDemo.Entities
{
public record Address : IEntity
{
public string Country { get; init; } = default;
public string POBox { get; init; }
public string City { get; init; }
public string Street { get; init; }
public string Apartment { get; init; }

[ForeignKey("Person")]
public int PersonId { get; init; }
public virtual Person Person { get; init; }
}
}
  • Create a new file ./Entities/Address.cs
namespace UoWDemo.Entities
{
public record Person : IEntity
{
public string FirstName { get; init; }
public string LastName { get; init; }

public virtual ICollection<Address> Addresses { get; init; }
}
}

Our example is a model containing a Person entity and an Address entity. We would never access an Address entity directly from the model as it does not make sense without the context of an associated Customer. So we could say that Customer and Address together form an aggregate and that Customer is an aggregate root.

In the Context of a Repository the Aggregate Root is an Entity with no parent Entity. It contains zero, One or Many Child Entities whose existence is dependent upon the Parent for it’s identity. That’s a One To Many relationship in a Repository. Those Child Entities are plain Aggregates. Aggregate root encapsulates multiple classes. you can manipulate the whole hierarchy only through the main object.

Resource Models for our Domain Entities

The RESTful API is about working with Resources, also knowns as DTOs, but for consistency purposes in our demo, while we’re implementing the Richardson Maturity Level 2. Let's create the basic Read Resource models for each Entity model in order to flatter the response objects structure.

  • Create a new folder ./Resources
  • Create class file ./Resources/AddressResource.cs
using UoWDemo.Entities;

namespace UoWDemo.Resources
{
public class AddressResource
{
public int Id { get; init; }
public string Country { get; init; }
public string POBox { get; init; }
public string City { get; init; }
public string Street { get; init; }
public string Apartment { get; init; }
public Person Person { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
}
  • Create class file ./Resources/PersonResource.cs
using UoWDemo.Entities;

namespace UoWDemo.Resources
{
public class PersonResource
{
public int Id { get; init; }
public string FirstName { get; init; }
public string LastName { get; init; }
public IList<Address>? Addresses { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
}

Adding AutoMapper and Profiling

Now we need to configure the mapping between the fields of Domain Entity models and Resource models in both directions using the AutoMapper library. AutoMapper is a specific tool that works because it enforces a convention. But at the same time, it assumes that your destination types are a subset of the source type. It assumes that everything on your destination type is meant to be mapped. It assumes that the destination member names follow the exact name of the source type. It assumes that you want to flatten complex models into simple ones… It's always worth reading & follows its usage guidelines.

  • First, we need to initialize the AutoMapper library itself. Adding the line below in ./Program.cs class file.
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
  • Create a new folder ./Mapper
  • Create a new class file ./Mapper/PersonProfile.cs
using AutoMapper;
using UoWDemo.Entities;
using UoWDemo.Resources;

namespace UoWDemo.Mapper
{
public class PersonProfile : Profile
{
public PersonProfile()
{
CreateMap<Person, PersonResource>()
.ForMember(t => t.Id, o => o.MapFrom(t => t.Id))
.ForMember(t => t.FirstName, o => o.MapFrom(t => t.FirstName))
.ForMember(t => t.LastName, o => o.MapFrom(t => t.LastName))
.ForMember(t => t.CreatedAt, o => o.MapFrom(t => t.CreatedAt))
.ForMember(t => t.UpdatedAt, o => o.MapFrom(t => t.UpdatedAt))
.ForMember(t => t.Addresses, o => o.MapFrom(t => t.Addresses))
.ReverseMap();
}
}
}
  • Create a new class file ./Mapper/AddressProfile.cs
using AutoMapper;
using UoWDemo.Entities;
using UoWDemo.Resources;

namespace UoWDemo.Mapper
{
public class AddressProfile : Profile
{
public AddressProfile()
{
CreateMap<Address, AddressResource>()
.ForMember(t => t.Id, o => o.MapFrom(t => t.Id))
.ForMember(t => t.Country, o => o.MapFrom(t => t.Country))
.ForMember(t => t.POBox, o => o.MapFrom(t => t.POBox))
.ForMember(t => t.City, o => o.MapFrom(t => t.City))
.ForMember(t => t.Street, o => o.MapFrom(t => t.Street))
.ForMember(t => t.Apartment, o => o.MapFrom(t => t.Apartment))
.ForMember(t => t.CreatedAt, o => o.MapFrom(t => t.CreatedAt))
.ForMember(t => t.UpdatedAt, o => o.MapFrom(t => t.UpdatedAt))
.ForMember(t => t.Person, o => o.MapFrom(t => t.Person))
.ReverseMap();
}
}
}

As a result resource object becomes a flat structure that exposes externally only those fields which are mandatory for the specific actions.

Configure the Persistence Layer using SQLite

  • Create the new folder ./Persistence
  • Update the file ./appsettings.json with the new “ConnectionStrings” section as below:
"ConnectionStrings": {
"MainConnectionString": "DataSource=MainDb.db"
}
  • Create a new interface file ./Persistence/IMainDbContext.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace UoWDemo.Persistence
{
public interface IMainDbContext : IDisposable
{
EntityEntry Entry(object entity);
DbSet<TEntity> Set<TEntity>() where TEntity : class;
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
}
  • Create a new class file ./Persistence/MainDbContext.cs
using Microsoft.EntityFrameworkCore;
using UoWDemo.Entities;

namespace UoWDemo.Persistence
{
public class MainDbContext : DbContext, IMainDbContext
{
public MainDbContext(DbContextOptions<MainDbContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
foreach (var item in ChangeTracker.Entries<IEntity>().AsEnumerable())
{
//Auto Timestamp
item.Entity.CreatedAt = DateTime.Now;
item.Entity.UpdatedAt = DateTime.Now;
}
return base.SaveChangesAsync(cancellationToken);
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().ToTable("Persons");
modelBuilder.Entity<Address>().ToTable("Addresses");

//Seed some dummy data
modelBuilder.Entity<Person>().HasData(
new Person { Id = 1, FirstName = "Jordan", LastName = "Davila", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now },
new Person { Id = 2, FirstName = "Giovanni", LastName = "Krueger", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now },
new Person { Id = 3, FirstName = "Marjorie", LastName = "Nolan", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now });

modelBuilder.Entity<Address>().HasData(
new Address { Id = 1, Country = "Portuguese", POBox = "1900-349", City = "Lisboa", Street = "Yango Avenida, Quadra 25", Apartment = "3", PersonId = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now },
new Address { Id = 2, Country = "Portuguese", POBox = "1900-123", City = "Faro", Street = "Braga Rua, Quadra 01", Apartment = "54", PersonId = 2, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now },
new Address { Id = 3, Country = "Portuguese", POBox = "1900-73", City = "Albufeira", Street = "Moraes Alameda, Casa 2", Apartment = "", PersonId = 3, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now });
}
}
}
  • Next, you need to inject the DbContext which represents an abstraction above various types of Database providers, the interface of IMainDbContext which uses SQLite DB and initialize it by modifying the ./Startup.cs file:
builder.Services.AddDbContext<MainDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("MainConnectionString"))
);

builder.Services.AddScoped<IMainDbContext, MainDbContext>();
  • In order to implement the EF migrations you need to run the following command in a “Package Manager Console” window (you can also see the output below):
PM> Add-Migration Initial
Build started...
Build succeeded.
To undo this action, use Remove-Migration.
PM> Add-Migration Initial
Build started...
Build succeeded.
To undo this action, use Remove-Migration.

You can also note that the new folder ./Migration has been created and contains 2 new files: ./Migrations/MainDbContextModelSnapshot.cs and ./Migrations/20221208175639_Initial.cs.

  • After the successful Initial Migration we need to run the Seed of database using the command:
PM> Update-Database

Which basically implements the metadata structure in migration files into a specified database. As a result, you can see the ./MainDb.db file appeared. This is an SQLite database, which can be viewed by any database viewer. I would suggest a free DBeaver. Basically, you can see how EntityFramework created the whole database schema for us.

Defining Repository and UnitOfWork (UoW)

In order to initialize the Repository:

  • Create a new folder ./Repositories
  • Create a new interface file ./Repositories/IRepository.cs
using System.Linq.Expressions;
using UoWDemo.Entities;

namespace UoWDemo.Repositories
{
public interface IRepository
{
Task<T?> GetById<T>(int id) where T : IEntity;
IQueryable<T> FindQueryable<T>(Expression<Func<T, bool>> expression, Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null) where T : IEntity;
Task<List<T>> FindListAsync<T>(Expression<Func<T, bool>>? expression, Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null, CancellationToken cancellationToken = default) where T : class;
Task<List<T>> FindAllAsync<T>(CancellationToken cancellationToken) where T : IEntity;
Task<T?> SingleOrDefaultAsync<T>(Expression<Func<T, bool>> expression, string includeProperties) where T : IEntity;
T Add<T>(T entity) where T : IEntity;
void Update<T>(T entity) where T : IEntity;
void UpdateRange<T>(IEnumerable<T> entities) where T : IEntity;
void Delete<T>(T entity) where T : IEntity;
}
}
  • Create a class file ./Repositories/Repository.cs
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using UoWDemo.Entities;
using UoWDemo.Persistence;

namespace UoWDemo.Repositories
{
public class Repository : IRepository
{
private readonly IMainDbContext _dbContext;

public Repository(IMainDbContext dbContext)
{
_dbContext = dbContext;
}

public async Task<T?> GetById<T>(int id) where T : IEntity
{
return await _dbContext.Set<T>().FindAsync(id);
}

public IQueryable<T> FindQueryable<T>(Expression<Func<T, bool>> expression,
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null) where T : IEntity
{
var query = _dbContext.Set<T>().Where(expression);
return orderBy != null ? orderBy(query) : query;
}

public Task<List<T>> FindListAsync<T>(Expression<Func<T, bool>>? expression, Func<IQueryable<T>,
IOrderedQueryable<T>>? orderBy = null, CancellationToken cancellationToken = default)
where T : class
{
var query = expression != null ? _dbContext.Set<T>().Where(expression) : _dbContext.Set<T>();
return orderBy != null
? orderBy(query).ToListAsync(cancellationToken)
: query.ToListAsync(cancellationToken);
}

public Task<List<T>> FindAllAsync<T>(CancellationToken cancellationToken) where T : IEntity
{
return _dbContext.Set<T>().ToListAsync(cancellationToken);
}

public Task<T?> SingleOrDefaultAsync<T>(Expression<Func<T, bool>> expression, string includeProperties) where T : IEntity
{
var query = _dbContext.Set<T>().AsQueryable();

query = includeProperties.Split(new char[] { ',' },
StringSplitOptions.RemoveEmptyEntries).Aggregate(query, (current, includeProperty)
=> current.Include(includeProperty));

return query.SingleOrDefaultAsync(expression);
}

public T Add<T>(T entity) where T : IEntity
{
return _dbContext.Set<T>().Add(entity).Entity;
}

public void Update<T>(T entity) where T : IEntity
{
_dbContext.Entry(entity).State = EntityState.Modified;
}

public void UpdateRange<T>(IEnumerable<T> entities) where T : IEntity
{
_dbContext.Set<T>().UpdateRange(entities);
}

public void Delete<T>(T entity) where T : IEntity
{
_dbContext.Set<T>().Remove(entity);
}
}
}

Repository Pattern:
Mediates between the domain and data mapping layers. It allows you to provide the CRUD operations over a record(s) out of datasets, and then have those records to work on acting like an in-memory domain entity object collection, providing a more object-oriented approach to the persistence layer.

In order to initialize Unit Of Work:

  • Create an interface file ./Repositories/IUnitOfWork.cs
namespace UoWDemo.Repositories
{
public interface IUnitOfWork : IDisposable
{
IRepository Repository();
Task<int> CommitAsync(CancellationToken cancellationToken);
}
}
  • Create a class file ./Repositories/UnitOfWork.cs
using UoWDemo.Persistence;

namespace UoWDemo.Repositories
{
public class UnitOfWork : IUnitOfWork
{
private readonly IMainDbContext _databaseContext;
private bool _disposed;

public UnitOfWork(IMainDbContext databaseContext)
{
_databaseContext = databaseContext;
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

public IRepository Repository()
{
return new Repository(_databaseContext);
}

public Task<int> CommitAsync(CancellationToken cancellationToken)
{
return _databaseContext.SaveChangesAsync(cancellationToken);
}

~UnitOfWork()
{
Dispose(false);
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
if (disposing)
_databaseContext.Dispose();
_disposed = true;
}
}
}
  • Update the ./Startup.cs file with a new line to inject our UnitOfWork interface:
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

UnitOfWork:
Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
https://www.martinfowler.com/eaaCatalog/unitOfWork.html

The source code repository: https://github.com/maximstanciu/UoWDemo

This is the end of part 1.

--

--