Repository Pattern C# ultimate guide: Entity Framework Core, Clean Architecture, DTOs, Dependency Injection, CQRS

Bob Code
20 min readJan 29, 2024

--

This is the sequel to my post on Entity Framework Core

Here is the GitHub Repo used to implement the Repo Pattern

Bob Code Original

Foreword

If you have been writing code and interacting with a database using an ORM, chances are you’ve heard of the Repo Pattern.

Yet, it’s one of the most divise pattern out there.

Please do comment if you have any questions or suggestions! Very much appreciated :)

Is it even a pattern actually? If you’re wondering, check this blog

Also, as with every pattern, there are tons of different implementations :) soe very situation calls for different measures

Bob Code

Isn’t Entity Framework Core already using repository pattern/ unit of work?

Actually, entity Framework Core (EF Core) is already designed around the Unit of Work and Repository patterns!

So before you decide to implement it, just be mindful of its potential issues.

Issues with Using the Repository/Unit of Work Pattern with EF Core

Repository Return Types: Repositories typically return IEnumerable or IQueryable, which rely on lazy loading to fetch data.

Lazy Loading Problem: Lazy loading can cause multiple round-trips to the database for each related entity, leading to the N+1 query problem.

Complexity of Generic Repositories: While a generic repository can simplify code, it often becomes complex and less efficient as specific requirements increase, necessitating additional repositories.

Potential fixes

Eager Loading: Use the Include method to load related data in a single query, reducing round trips.

var orders = context.Orders
.Include(o => o.Customer)
.ToList();

Explicit Loading: Manually trigger the loading of related entities

var order = context.Orders.FirstOrDefault();
context.Entry(order).Reference(o => o.Customer).Load();

Issues with These Fixes

Class-Specific Names: Using .Load or .Include requires specifying the class name of the related entity, which is not feasible with a generic repository, leading to the need for specific repositories.

Potential Fix

Using Expression<Func<T, bool>> for Flexible Filtering

To create more flexible and reusable queries, you can incorporate an Expression<Func<T, bool>> parameter in your repository methods.

This allows for generic queries such as filtering by ID or name thus making your repository more generic to specific implementation (get by id or by name)

public async Task<T> GetAsync(Expression<Func<T, bool>>? filter = null, bool tracked = true)
{
IQueryable<T> query = _context.Set<T>();
if (filter != null)
{
query = query.Where(filter);
}
if (!tracked)
{
query = query.AsNoTracking();
}
return await query.FirstOrDefaultAsync();
}

Filter by ID

var orderId = 1;
var orderById = await orderRepository.GetAsync(o => o.Id == orderId);

Filter by Name

var customerName = "John Doe";
var orderByName = await orderRepository.GetAsync(o => o.Customer.Name == customerName);

Trade-off?

As with every solution, it’s about the situation at hand.

As a rule of thumb, it’s great using this pattern if you have lots of repositories that basically perform the same actions.

Here’s a potential trade-off solution to get the most of this pattern:

  • Have a specific Repository with eager or explicit loading for data that is often queried
  • Maintain a generic repository with Func<> for flexible includes and Expression<Func<T, bool>> for filters for less frequently queried data

If the number of specific repositories increases significantly, reconsider the use of the Repository/Unit of Work pattern.

So why would you use the repo/pattern with EF Core?

  • Abstraction (DI principle) & Separation of concerns: interface between data access and business
  • Testing: Repositories can be easily mocked
  • Decoupling: It becomes easier to switch ORM or even database with — less — little code change which fits into the DDD philosophy

Now that we have covered the basics, let’s actually delve into its implementation!

Agenda

  • Clean Architecture introduction
  • Repository Pattern introduction
  • Generic Repository
  • Unit of work
  • Repository pattern Example in C# With code
  • CQRS and Repository Pattern
  • Sources

Clean Architecture introduction

Clean architecture is a method to keep your solution well organised so that it respects core software engineering concepts such as :

  • Maintanability
  • Separation of concerns
  • Inversion of control

With all ideas, some people love it some hate it. If you want to learn more on this topic, you can look at this blog

Nevertheless, Clean Architecture is a good start to understand the repository pattern :)

Every developer these days

Let’s first review how clean architecture works

Clean architecture diagrams

The famous picture you’ve most likely already seen
Some like to slice it into vertical layers

Clean architecture layers

DM: Data Model, DTO: Data Transfer Objects

As presented above, we have 4 layers, in our use-case we are going to focus on 3

  • A presentation layer (UI) : that interacts with external requests (e.g. API/ UI)
  • A business layer (application layer): that implements use cases or business logic. This is where your handlers are (if you use CQRS/MediatR), validations and mapping takes place
  • An infrastructure layer (Data access): This is where you interact with the Database with tools such as DbContext

Why using DTOs?

We just discussed the 3 layers that we will be using, and you might already notice that the data access layer will not be referenced by the presentation layer.

So to reflect the entities in your presentation layer, you will use DTOs.

What are DTOs? Just the same classes as your entities with perhaps more/fewer fields

// Inner Layer (Entities)
namespace Domain.Entities
{
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
public double UserBalance { get; set; }
// Other user-related properties
}
}
// Outer Layer (DTOs in Interface Adapters)
namespace Application.DTOs
{
public class UserDto
{
public int UserId { get; set; }
public string UserName { get; set; }
// Only include properties needed for data transfer
}
}

Their sole purpose is to keep the layers separated so that if one entity changes, we don’t have to change the API or the UI.

Also, we might not want to share all the information to the outter layer (e.g. User Balance)

This however adds a complexity because we need to do the mapping. Thankfully, tools such as Automapper can alievate this.

So now that we have the basics of clean architecture let’s move on to the repository pattern

Repository Pattern introduction

Without Repository pattern

In a given project, one creates a DbContext and then uses this context in the application layer.

This however might lead to some issues such as:

  • Testability: How do you test your DbContext?
  • Repetitive Code: You would have to recreate a repository for every DbSet
  • Direct dependencies: By directly working with the DbContext you violate the Dependency Inversion principle (Your app layer now dependes on the DbContext module and not its abstraction)
  • Change ORM: If you want to use Dapper instead of EF Core, then we would have to make lots of changes

Why use the Repository pattern?

The idea is that with one interface we abstract the DbContext, why would we want to do that?

Again let’s review our issues mentioned above

  • Testability: Now we can test the DbContext with this interface
  • DRY: We have one object with several implementations
  • Separation of concerns: Data and business are kept separate and their dependencies are managed via Dependency Injection of the repositories in the application layer
  • Changing ORM: Now we can easily replace our dbContext to implement another ORM without changing the whole logic

What are the disadvantages?

With every pattern, there are pros and cons

  • Complexity

The most obvious one is that it adds another level of abstraction and thus complexity.

  • Orchestration

The second that actually derives from the added complexity is that you now need to ensure that your interfaces need to well implemented, registered in the dependency-injection container

Also you’re going to need to map your DTOs and entities, tools like auto-mapper might not work when working with generic expressions such as Func.

Finally, you will need to ensure that each implementation saves its work in the right order (by implement the unit of work)

Bob Code

How to implement the Repository Pattern with Clean Architecture?

Again, one pattern has many implementations :) Here’s my take

  • Infrastructure Layer: Create a generic repository that takes a DbContext and DbSet
  • Application Layer: Create services that use the generic repo and pass the respective entity and map it to the DTO (add some cross domain logic here such as validation, logging, exception handling…)
  • Controller Layer: Implement and use the service

So let’s see how we would implement our Repository Pattern in Code

1/ First we create the interface

public interface IStudentRepository : IDisposable
{
IEnumerable<Student> GetStudents();
Student GetStudentByID(int studentId);
void InsertStudent(Student student);
void DeleteStudent(int studentID);
void UpdateStudent(Student student);
void Save();
}

2/ We implement it


namespace ContosoUniversity.DAL
{
public class StudentRepository : IStudentRepository, IDisposable
{
private SchoolContext context;

public StudentRepository(SchoolContext context)
{
this.context = context;
}

public IEnumerable<Student> GetStudents()
{
return context.Students.ToList();
}

public Student GetStudentByID(int id)
{
return context.Students.Find(id);
}

public void InsertStudent(Student student)
{
context.Students.Add(student);
}

public void DeleteStudent(int studentID)
{
Student student = context.Students.Find(studentID);
context.Students.Remove(student);
}

public void UpdateStudent(Student student)
{
context.Entry(student).State = EntityState.Modified;
}

public void Save()
{
context.SaveChanges();
}

private bool disposed = false;

protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
context.Dispose();
}
}
this.disposed = true;
}

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

3/ Use it in your application layer

// Insert
studentRepository.InsertStudent(student);

// Save
studentRepository.Save();

// Get by id
varstudent = studentRepository.GetStudentByID(id);

That’s great, but what if we have dozens of classes? Wouldn’t it be nice to have a truly generic repository, in which we would just pass the DbSet?

Generic Repository

This is where the Generic Repository comes in, instead of having a repository specific to a class, we will make it generic.

  • Create a generic Repository interface
public interface IGenericRepository<T> where T : class
{
Task<List<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null, bool tracked = true);
Task<T> GetAsync(Expression<Func<T, bool>>? filter = null, bool tracked = true);
Task CreateAsync(T entity);
Task RemoveAsync(T entity);
Task SaveAsync();
}
  • Create a generic Repository
public class GenericRepository<TEntity>: IGenericRepository<TEntity> where TEntity : class
{
internal SchoolContext context;
internal DbSet<TEntity> dbSet;

public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = 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 = dbSet;

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 GetByID(object id)
{
return dbSet.Find(id);
}

public virtual void Insert(TEntity entity)
{
dbSet.Add(entity);
}

public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}

public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
{
dbSet.Attach(entityToDelete);
}
dbSet.Remove(entityToDelete);
}

public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}

How to implement more specific methods in the repository pattern?

This generic repository will handle typical CRUD methods.

When a particular entity type has special requirements, such as more complex filtering or ordering, you can create a derived class that has additional methods for that type.

Let’s look at one use case, we want to be able to retrieve brand by name, but only for the class brand

Solution: we create a derived

public class BrandRepository(ProductPricingDbContext context) : GenericRepository<Brand>(context), IBrandRepository
{
public async Task<Brand?> GetByName(string name)
{
return await _dbSet
.AsTracking()
.Where(b => b.BrandName == name)
.SingleOrDefaultAsync();
}
}

How to make methods generic in Repository pattern?

Let’s say you have several tables in your database, each with its own id, however some are string and other are int.

How do you create a getbyid method that caters to all?

  • 1st solution: use object
public async Task<TEntity> GetByID(object id)
{
return await dbSet.FindAsync(id);
}

However, using object can lead to potential boxing/unboxing overhead and may not be as type-safe as using generics.

  • 2nd solution: use Linq Expression
  public async Task<T> GetFirstAsync(Expression<Func<T, bool>> expression)
{
return await _dbContext.Set<T>().FirstOrDefaultAsync(expression);
}

This method retrieves the first entity that satisfies the given expression.

Using LINQ expressions provides more flexibility and type-safety compared to using object as the ID.

Some issues are:

  • SQL Injection

If the expressions are constructed using user input (e.g., in dynamic filtering scenarios), there’s a risk of injection attacks such as SQL injection.

Ensure proper validation and sanitation of input to prevent such security vulnerabilities.

  • Mapping

AutoMapper is primarily designed to map properties from one object to another, and it doesn’t directly support mapping expressions

  • 3rd solution: is it really a good idea?

The solutions above are meant to provide more flexibility and reusability in your code.

However, this can lead to some issues regarding security, mapping and performance.

Each situation will dictate what works best!

Bob Code

If Expression<Func<T, bool>> expression sounds confusing, you’re not the only one!

Repository pattern Example in C# With code

As a reminder you can find the repo here :)

(CQRS coming soon!)

And the diagram

Use case introduction

We are using a Brand class and we want to have a service that passes this class and does the mapping with its DTO using that service whilst keep the repository generic.

Folder Structure

We have 3 applications:

  • API: Controller Layer => API project
  • Business: Use Case Layer => Class project
  • DataAccess: Database Layer => class project

All communication between the database and controller go through the use case layer.

Data Access Layer

First create your models

using System.ComponentModel.DataAnnotations;

public class Brand
{
[Key]
public int BrandId { get; set; }
public string BrandName { get; set; }

public ICollection<Model> Models { get; set; }
}



public class Model
{
[Key]
public int ModelId { get; set; }
public string ModelName { get; set; }

public int BrandId { get; set; }
public Brand Brand { get; set; }
}

Second create your dbContext

We are going to add some mock data for testing

using BrandApplication.DataAccess.Models;
using Microsoft.EntityFrameworkCore;

namespace BrandApplication.DataAccess
{
public class BrandDbContext : DbContext
{
public BrandDbContext(DbContextOptions<BrandDbContext> options) : base(options)
{
}

public DbSet<Brand> Brands { get; set; }
public DbSet<Model> Models { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new FluentConfiguration.Brand_FluentConfiguration());
modelBuilder.Entity<Brand>().HasData(new Brand { BrandId = 1, BrandName = "Brand 1" });
}
}
}

Then, create your table configurations


internal class Brand_FluentConfiguration : IEntityTypeConfiguration<Brand>
{
public void Configure(EntityTypeBuilder<Brand> modelBuilder)
{
modelBuilder
.HasMany<Model>(b => b.Models)
.WithOne(b => b.Brand)
.HasForeignKey(b => b.BrandId)
.OnDelete(DeleteBehavior.Cascade);
}
}

This line in DbContext ensures this config is applied

modelBuilder.ApplyConfiguration(new FluentConfiguration.Brand_FluentConfiguration());

Once this is done, run the commands to create your migration

add-migration amazingMigration

Check the changes file, then apply the migration to the database

update-database

Note you will need to pass the connection string like so at first

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=nameofyourdb;Database=nameofyourserver;TrustServerCertificate=True;Trusted_Connection=True;");
}

Data Access Layer Generic Repo

Now we want our repo to be testable and reusable, so first we need an interface


public interface IGenericRepository<T> where T : class
{
Task AddAsync(T entity);
Task<T> GetByIdAsync(int id);
Task<List<T?>> GetAllAsync(bool tracked = true);
Task UpdateAsync(T entity);
Task DeleteByIdAsync(int id);
Task SaveAsync();
}

Second, we need to implement it to use our DbContext

public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly BrandDbContext _databaseContext;
private readonly DbSet<T> _dbSet;

public GenericRepository(BrandDbContext context)
{
_databaseContext = context;
_dbSet = context.Set<T>();
}

public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await SaveAsync();
}

public async Task DeleteByIdAsync(int id)
{
var entityToDelete = await _dbSet.FindAsync(id);

if (entityToDelete != null)
{
_dbSet.Remove(entityToDelete);
await SaveAsync();
}
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}

public async Task<List<T>> GetAllAsync(bool tracked = true)
{
IQueryable<T> query = _dbSet;

if (!tracked)
{
query = query.AsNoTracking();
}

return await query.ToListAsync();
}

public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await SaveAsync();
}

public async Task SaveAsync()
{
await _databaseContext.SaveChangesAsync();
}
}

Business Layer

This is where we map the DTOs (used by the controller) with the entities (in the database)

We also create the services that will be responsible for inserting the class into the GenericRepo

We start with creating and mapping our DTOs

public class BrandDto
{
public int BrandId { get; set; }
public string BrandName { get; set; }

public ICollection<ModelDto> Models { get; set; }

}

public class ModelDto
{
public int ModelId { get; set; }
public string ModelName { get; set; }

public int BrandId { get; set; }
public BrandDto Brand { get; set; }
}

We use automapper for the mapping

using AutoMapper;
using BrandApplication.Business.DTOs;
using BrandApplication.DataAccess.Models;

namespace BrandApplication.Business.Mappings
{
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Brand, BrandDto>().ReverseMap();
CreateMap<Model, ModelDto>().ReverseMap();
}
}
}

Now we can create our services, first we create the interfaces:

  • one for read only operations
  • another for all operations
public interface IReadServiceAsync<TEntity, TDto> where TEntity : class where TDto : class
{
Task<IEnumerable<TDto>> GetAllAsync();
Task<TDto> GetByIdAsync(int id);
}
 public interface IGenericServiceAsync<TEntity, TDto> : IReadServiceAsync<TEntity, TDto>
where TEntity : class
where TDto : class

{
Task AddAsync(TDto dto);
Task DeleteAsync(int id);
Task UpdateAsync(TDto dto);
}

Then we implement them, passing our Generic Repo and automapper

public class ReadServiceAsync<TEntity, TDto> : IReadServiceAsync<TEntity, TDto>
where TEntity : class
where TDto : class
{
private readonly IGenericRepository<TEntity> _genericRepository;
private readonly IMapper _mapper;

public ReadServiceAsync(IGenericRepository<TEntity> genericRepository, IMapper mapper) : base()
{
_genericRepository = genericRepository;
_mapper = mapper;
}
public async Task<IEnumerable<TDto>> GetAllAsync()
{
try
{
var result = await _genericRepository.GetAllAsync();

if (result.Any())
{
return _mapper.Map<IEnumerable<TDto>>(result);
}
else
{
throw new EntityNotFoundException($"No {typeof(TDto).Name}s were found");
}

}
catch (EntityNotFoundException ex)
{
var message = $"Error retrieving all {typeof(TDto).Name}s";

throw new EntityNotFoundException(message, ex);
}
}

public async Task<TDto> GetByIdAsync(int id)
{
try
{
var result = await _genericRepository.GetByIdAsync(id);

if (result is null)
{
throw new EntityNotFoundException($"Entity with ID {id} not found.");
}

return _mapper.Map<TDto>(result);
}

catch (EntityNotFoundException ex)
{
var message = $"Error retrieving {typeof(TDto).Name} with Id: {id}";

throw new EntityNotFoundException(message, ex);
}
}
}
 public class GenericServiceAsync<TEntity, TDto> : ReadServiceAsync<TEntity, TDto>, IGenericServiceAsync<TEntity, TDto>
where TEntity : class
where TDto : class
{
private readonly GenericRepository<TEntity> _genericRepository;
private readonly IMapper _mapper;

public GenericServiceAsync(GenericRepository<TEntity> genericRepository, IMapper mapper) : base(genericRepository, mapper)
{
_genericRepository = genericRepository;
_mapper = mapper;
}

public async Task AddAsync(TDto dto)
{
await _genericRepository.AddAsync(_mapper.Map<TEntity>(dto));
}

public async Task DeleteAsync(int id)
{
await _genericRepository.DeleteByIdAsync(id);
}

public async Task UpdateAsync(TDto dto)
{
var entity = _mapper.Map<TEntity>(dto);
await _genericRepository.UpdateAsync(entity);
}
}

Now comes the moment when we actually have to pass a class to the model

public class BrandMapping : ReadServiceAsync<Brand, BrandDto>, IBrandMapping
{
public BrandMapping(IGenericRepository<Brand> genericRepository, IMapper mapper) : base(genericRepository, mapper)
{
}
}

Great now everything is set!

Controller Layer

We will start with doing all the Dependency Injection and appsettings in program.cs

///// Data Base Configuration
builder.Services.AddScoped<BrandDbContext>();

builder.Services.AddDbContext<BrandDbContext>(options =>
{
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly("Plutus.ProductPricing.DataAccess");
});
});

//// AutoMapper Configuration
builder.Services.AddAutoMapper(typeof(MappingProfile));

//// Generic Repository
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));

//// Generic Services
builder.Services.AddScoped(typeof(IReadServiceAsync<,>), typeof(ReadServiceAsync<,>));
builder.Services.AddScoped(typeof(IGenericServiceAsync<,>), typeof(GenericServiceAsync<,>));

//////////////////////////////////// Services ////////////////////////////////////

// Asset Mappings
builder.Services.AddScoped(typeof(IBrandMapping), typeof(BrandMapping));
Bob Code

In appsettings add

"ConnectionStrings": {
"DefaultConnection": "Server=nameofyourserver\\SQLEXPRESS;Database=nameofyourdb;TrustServerCertificate=True;Trusted_Connection=True;"
}

Now that we have everything ready we can create the controller that will use our Brand Service!

[Route("api/[controller]")]
[ApiController]
public class BrandController : ControllerBase
{
private readonly IBrandMapping _service;

public BrandController(IBrandMapping service)
{
_service = service;
}

[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<BrandDto>>> GetAllBrands()
{
await _service.GetAllAsync();
return Ok();
}

[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<BrandDto>> GetBrandByID(int id)
{
if (id < 1)
{
return BadRequest("Id must be greater than 0");
}

return Ok(await _service.GetByIdAsync(id));
}
}

Unit of work

So what is the role of the unit of work in repository pattern?

We’ve seen above that:

  • We have 1 generic repositories
  • This in turn will be used by all your entities

Problem = how do you coordinate and ensure that all implementations save their work in the right order?

The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context.

That way, when a unit of work is complete you can call the SaveChanges method on that instance of the context and be assured that all related changes will be coordinated

All that the class needs is a Save method and a property for each repository.

Remember Unit of Work makes sense for actions that perform a change in your database that needs to be saved, aka a write

So we basically need a common method that handles the save for us, this is called the Unit Of Work

Bob Code

Let’s refactor our generic repository code to Unit Of Work

  • 1/ Create Interface
public interface IUnitOfWork
{
Task SaveChangesAsync();
IGenericRepository<T> Repository<T>() where T : class;
}
  • 2/ Implement it with your DbContext
public class UnitOfWork : IUnitOfWork
{
private readonly BrandDbContext _dbContext;

public UnitOfWork(BrandDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task SaveChangesAsync()
{
await _dbContext.SaveChangesAsync();
}
public IGenericRepository<T> Repository<T>() where T : class
{
return new GenericRepository<T>(_dbContext);
}
}
  • 3/ Let’s update our services

Read Service

public class ReadServiceAsync<TEntity, TDto> : IReadServiceAsync<TEntity, TDto>
where TEntity : class
where TDto : class
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;

public ReadServiceAsync(IUnitOfWork unitOfWork, IMapper mapper) : base()
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<IEnumerable<TDto>> GetAllAsync()
{
try
{
var result = await _unitOfWork.Repository<TEntity>().GetAllAsync();

if (result.Any())
{
return _mapper.Map<IEnumerable<TDto>>(result);
}
else
{
throw new EntityNotFoundException($"No {typeof(TDto).Name}s were found");
}

}
catch (EntityNotFoundException ex)
{
var message = $"Error retrieving all {typeof(TDto).Name}s";

throw new EntityNotFoundException(message, ex);
}
}

public async Task<TDto> GetByIdAsync(int id)
{
try
{
var result = await _unitOfWork.Repository<TEntity>().GetByIdAsync(id);

if (result is null)
{
throw new EntityNotFoundException($"Entity with ID {id} not found.");
}

return _mapper.Map<TDto>(result);
}

catch (EntityNotFoundException ex)
{
var message = $"Error retrieving {typeof(TDto).Name} with Id: {id}";

throw new EntityNotFoundException(message, ex);
}
}
}

Generic Service

public class GenericServiceAsync<TEntity, TDto> : ReadServiceAsync<TEntity, TDto>, IGenericServiceAsync<TEntity, TDto>
where TEntity : class
where TDto : class
{
private readonly IMapper _mapper;
private readonly IUnitOfWork _unitOfWork;


public GenericServiceAsync(IUnitOfWork unitOfWork, IMapper mapper) : base(unitOfWork, mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}

public async Task AddAsync(TDto dto)
{
await _unitOfWork.Repository<TEntity>().AddAsync(_mapper.Map<TEntity>(dto));
await _unitOfWork.SaveChangesAsync();
}

public async Task DeleteAsync(int id)
{
await _unitOfWork.Repository<TEntity>().DeleteByIdAsync(id);
await _unitOfWork.SaveChangesAsync();
}

public async Task UpdateAsync(TDto dto)
{
var entity = _mapper.Map<TEntity>(dto);
await _unitOfWork.Repository<TEntity>().UpdateAsync(entity);
await _unitOfWork.SaveChangesAsync();
}
}

Mapping Brand Class to Brand_DTO

public class BrandService : ReadServiceAsync<Brand, BrandDto>, IBrandService
{
public BrandService(IUnitOfWork unitOf, IMapper mapper) : base(unitOf, mapper)
{
}
}

The controller stays the same!

Don’t forget to add the Interface to the DI container

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));

CQRS and Repository Pattern

CQRS or not CQRS?

CQRS is often used in combination with the repository pattern

First off what is CQRS?

  • CQRS advocates separating the handling of commands (write operations) and queries (read operations).
  • Command handlers focus on state changes, often involving the use of repositories for data persistence.
  • Query handlers are responsible for retrieving data, and they may interact with specialised read models.

Why is CQRS often used wtih the Repository Pattern?

  1. Separation of Concerns:
  • CQRS separates command and query operations, while the Repository Pattern separates data access logic.

2. Scalability:

  • CQRS allows independent scaling of command and query sides; the Repository Pattern adapts to various data storage mechanisms, supporting scalability.

3. Flexibility and Extensibility:

  • CQRS permits different models for reading and writing; the Repository Pattern abstracts data access, allowing flexibility in storage mechanisms.

4. Testability:

  • CQRS encourages separate models for testing; the Repository Pattern supports unit testing with mock repositories.

5. Maintainability:

  • CQRS separates concerns, and the Repository Pattern organizes data access logic, contributing to a maintainable codebase.

6. Single Responsibility Principle (SRP):

  • CQRS aligns with SRP for command and query responsibilities; the Repository Pattern isolates data access, supporting SOLID principles.

7. Clear Boundaries:

  • CQRS establishes clear boundaries between write and read operations; the Repository Pattern defines boundaries between application logic and data access.

Disadvantages of using CQRS

  1. Increased Complexity:
  • With CQRS, there is a mapping to be done between commands and handlers
  • This introduces additional architectural complexity, potentially making the system harder to understand and maintain.
  • CQRS basically adds another layer of complexity

3. Data Synchronization Challenges:

  • Maintaining consistency between command and query models may lead to challenges in data synchronisation, requiring careful design and coordination.

4. Increased Development Effort:

  • As a result, introducing new features will mean creating commands and handlers

5. Potential for Misuse:

  • Incorrectly applying CQRS to simple or small-scale systems may introduce unnecessary complexity, leading to misuse of the pattern.

CQRS implemented with repository pattern

  • The CQRS write command would use the WriteService that implements only write queries
  • The CQRS read command would use the ReadService that implements only read queries

The read side

Repo

// Repository Implementation for Read Side
public class OrderReadModelRepository : IRepository<OrderReadModel>
{
private readonly List<OrderReadModel> _orderReadModels = new List<OrderReadModel>();

public OrderReadModel GetById(int id)
{
return _orderReadModels.FirstOrDefault(readModel => readModel.OrderId == id);
}

public void Add(OrderReadModel entity)
{
_orderReadModels.Add(entity);
}

// Implement other repository methods
}

Service

// Read Service (Query Side)
public class ReadService
{
private readonly IRepository<OrderReadModel> _orderReadModelRepository;

public ReadService(IRepository<OrderReadModel> orderReadModelRepository)
{
_orderReadModelRepository = orderReadModelRepository;
}

public OrderReadModel GetOrderById(int orderId)
{
return _orderReadModelRepository.GetById(orderId);
}

// Other read operations
}

Write Side

Repo

// Repository Implementation for Write Side
public class OrderRepository : IRepository<Order>
{
private readonly List<Order> _orders = new List<Order>();

public Order GetById(int id)
{
return _orders.FirstOrDefault(order => order.OrderId == id);
}

public void Add(Order entity)
{
_orders.Add(entity);
}

// Implement other repository methods
}

Service

// Write Service (Command Side)
public class WriteService
{
private readonly IRepository<Order> _orderRepository;

public WriteService(IRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
}

public void CreateOrder(string customerName)
{
Order newOrder = new Order { OrderId = _orderRepository.GetAll().Count() + 1, CustomerName = customerName };
_orderRepository.Add(newOrder);
}

// Other write operations
}

Sources

--

--

Bob Code

All things related to Memes, .Net/C#, Azure, DevOps and Microservices