Here is the GitHub Repo used to implement the Repo Pattern

None

Agenda

  • Introduction
  • Theory: How to use the Repository Pattern in C#?
  • Implementation: Example with code
  • CQRS or not CQRS?
  • Sources

Introduction

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
None
The famous picture you've most likely already seen
None

DTOs (Data Transfer Objects)

None
DM: Data Model, DTO: Data Transfer Objects

In Clean Architecture, DTOs play a crucial role in maintaining a clear separation of concerns between layers.

Basically they reflect your entities or domain.

They are meant for the outter layer (UI or API)

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, although this often done with a tool such as automapper

// 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
    }
}

Repository Pattern introduction

None
The theory

Advantages of using the Repository Pattern

None

Instead of having one repository, we create an interface that makes it generic and then implement it.

Why?

The idea is that with one interface we abstract the DbContext

Why would we want to do that?

  • Testability: Now we can test the DbContext with this interface
  • Separation of concerns: data and business are kept separate
  • DRY: We have one object with several implementations

What are the disadvantages?

  • Adds another level of abstraction and thus complexity
None

How is the Repository Pattern often used in Clean Architecture?

None
  • The repository pattern can be used within the outermost layer of Clean Architecture, specifically in the Interface Adapters layer or even the Use Cases layer.
  • Repositories provide an abstraction for data access, allowing the application to interact with databases or other external data sources without exposing the details of the storage mechanism to the core business logic.
  • Repositories are part of the infrastructure layer in Clean Architecture, responsible for implementing the interfaces defined in the use cases or interface adapters layer.

Why is the repository pattern popular with C#?

  • C# and the repository patterns arecommonly used in the implementation of Clean Architecture due to their strong support for object-oriented programming and features like interfaces and dependency injection.
  • The repository pattern, when used within Clean Architecture in C#, contributes to the separation of concerns by isolating data access logic from the core business logic.
  • Clean Architecture in C# allows for the development of highly modular and maintainable code, with the repository pattern ensuring that data access operations can be easily replaced or extended without affecting the rest of the application.
None
Author: Mario Sanoguera de Lorenzo

Theory: How to use Repository pattern with Clean Architecture in C#?

None

Using the repository pattern in .NET typically involves the following steps:

  1. Define Repository Interfaces:
  • Create interfaces that define the contract for data access operations. These interfaces often include methods for CRUD operations.
public interface IRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

2. Implement Repository Classes:

  • Create concrete classes that implement the repository interfaces. These classes contain the actual logic for interacting with the data store.
public class UserRepository : IRepository<User>
{
    // Implement CRUD operations
}

3. Dependency Injection:

  • Use dependency injection to inject instances of the repository interfaces into the components that need access to data. This allows for better decoupling and testability.
public class UserService
{
    private readonly IRepository<User> _userRepository;

    public UserService(IRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }

    // Use _userRepository to perform data access operations
}
None

ORM Integration:

  • If you are working with databases, you might integrate an Object-Relational Mapping (ORM) framework like Entity Framework to simplify data access.
public class UserRepositoryEF : IRepository<User>
{
    private readonly DbContext _dbContext;

    public UserRepositoryEF(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    // Implement CRUD operations using Entity Framework
}

Unit Testing:

  • Utilise the repository pattern's flexibility for unit testing by creating mock implementations of the repository interfaces. This allows you to test components without involving the actual data store.
public class MockUserRepository : IRepository<User>
{
    // Implement mock CRUD operations for testing
}

Application Integration:

  • Integrate the repository classes into your application's layers, adhering to the principles of the chosen architecture (e.g., Clean Architecture).
public class UserController
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    // Use _userService to interact with user data
}

Implementation: Repository pattern Example in C# With code

As a reminder you can find the repo here :)

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.

Here's how it works in our use case

None

Folder Structure

None

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

None

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=DVT-CHANGEMENOW\SQLEXPRESS;Database=CodingWiki;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

using System.Linq.Expressions;

namespace BrandApplication.DataAccess.Interfaces
{
    public interface IGenericRepository<T> where T : class
    {
        Task AddAsync(T entity);
        Task<T> GetByIdAsync(int id);
        Task<T> GetAsync(Expression<Func<T, bool>> filter = null, bool tracked = true);
        Task<List<T>> GetAllAsync(Expression<Func<T, bool>> filter = null, bool tracked = true);
        Task UpdateAsync(T entity);
        Task DeleteByIdAsync(int id);
        Task SaveAsync();
    }
}

Second, we need to implement it to use our DbContext

using BrandApplication.DataAccess.Interfaces;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace BrandApplication.DataAccess.Repositories
{
    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<T> GetAsync(Expression<Func<T, bool>> filter = null, bool tracked = true)
        {
            IQueryable<T> query = _dbSet;

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

            if (filter != null)
            {
                query = query.Where(filter);
            }
            return await query.FirstOrDefaultAsync();
        }
        public async Task<List<T>> GetAllAsync(Expression<Func<T, bool>> filter = null, bool tracked = true)
        {
            IQueryable<T> query = _dbSet;

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

            if (filter != null)
            {
                query = query.Where(filter);
            }

            return await query.ToListAsync();
        }

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

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

Business Layer

None

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(Expression<Func<TDto, bool>> filter = null);
    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(Expression<Func<TDto, bool>> filter = null)
    {
        var result = await _genericRepository.GetAllAsync(_mapper.Map<Expression<Func<TEntity, bool>>>(filter), false);
        return _mapper.Map<IEnumerable<TDto>>(result);
    }

    public async Task<TDto> GetByIdAsync(int id)
    {
        var result = await _genericRepository.GetByIdAsync(id);
        return _mapper.Map<TDto>(result);
    }
}
public class GenericServiceAsync<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 = 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<IEnumerable<TDto>> GetAllAsync(Expression<Func<TDto, bool>> filter = null)
    {
        var result = _genericRepository.GetAllAsync(_mapper.Map<Expression<Func<TEntity, bool>>>(filter), false);
        return _mapper.Map<IEnumerable<TDto>>(result);
    }

    public async Task<TDto> GetByIdAsync(int id)
    {
        var result =  await _genericRepository.GetByIdAsync(id);
        return _mapper.Map<TDto>(result);
    }

    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!

API Layer

None

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));

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));
    }
}

CQRS or not CQRS?

CQRS is often used in combination with the repository pattern

None

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?

None
  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