Simplified Using Generic Repository Pattern + UnitOfWork On Clean Architecture No CQRS
Clean Architecture has become a popular in modern software development, particularly for .NET developers. It emphasizes a separation of concerns and promotes a scalable and maintainable code. This article providing a thorough overview and practical boilerplate code to help .NET developers implement these concepts effectively.
As per title mention. I will not use the
CQRS
pattern π. I will provide the simple code as possible.
Code Structure Planning
To implement Clean Architecture
in a .NET application, I typically divide the project into several layers:
Core / Domain Layer: Contains the business logic and entities. It should be completely independent of other layers. Me personally, use the
Domain
words as it precisely state the overall entities.Application Layer: Implements the use cases and application-specific logic. It depends on the Core Layer and defines interfaces for interaction with the outer layers.
Infrastructure Layer: Manages the data access, external services, and any other infrastructure concerns. It implements the interfaces defined in the Application Layer and depends on it.
Presentation Layer: Handles user interface and user interaction. It communicates with the Application Layer to perform actions and display results.
Presentation Layer can be your
API
/Web Application (MVC, Blazor, Razor Pages)
Dependency Rule (β οΈimportant)
In Clean Architecture, the dependency rule dictates that dependencies can only point inward. This means that:
Domain Layer: Should be independent and not have any dependencies on other layers. It is the most stable part of the application.
Application Layer: Can depend on the Domain layer. It uses the Domain layer's entities and business logic to fulfill application-specific requirements.
Infrastructure Layer: Can depend on both the Application and Domain layers. However, it should not introduce dependencies that affect the core Domain layer. The Infrastructure layer provides implementations for the abstractions defined in the Application layer and Domain layer.
Create A Solution
First, create a blank solution with CleanArchitectureRepositoryPatternDemo
(for demo purposes) project as per below screenshot.
Next, I will add Web API project under the solution. I will use the Use Controllers
since i familiar with it. (Still In progress of learning minimal api) π.
Kindly create the rest of the classlib
for the remaining layers : Infrastructure Layer
,Application Layer
,Core / Domain Layer
. Until your project will look like below.
Application Layer Add Reference
Domain Layer What π±
Yes guys!! Refer back to the dependency rules on item 2 and 3.
So in our Application.csproj
already reference Domain Layer as per below :-
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>
IUnitOfWork, why bro?
Ya I get you, there is a lot of debate for this. An abstraction over abstraction, but for me i want to easily manage all the transaction under one hood managing database transactions.
Method: Task<int> SaveChangesAsync(CancellationToken cancellationToken)
: Asynchronously saves all changes made in the unit of work. It returns a Task that resolves to an int
, which typically represents the number of state entries written to the database. The CancellationToken
parameter allows the operation to be cancelled if needed.
Create an Interface for IUnitOfWork.cs
,IGenericRepository<T>.cs
and IStudentRepository.cs
This is a straight forward Generic Repository. You can add anther function for your needs. For me this is enough π.
using System.Data;
using System.Linq.Expressions;
namespace Application.Interfaces
{
public interface IGenericRepository<T> where T : class
{
IQueryable<T> GetAll();
IQueryable<T> Find(Expression<Func<T, bool>> expression);
Task<T> GetIdAsync(int id, CancellationToken cancellation);
Task<T> AddAsync(T entity, CancellationToken cancellation);
Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellation);
Task UpdateAsync(T entity, CancellationToken cancellation);
Task UpdateRangeAsync(IEnumerable<T> entities, CancellationToken cancellation);
Task RemoveAsync(T entity, CancellationToken cancellation);
IDbConnection QueryConnection { get; }
}
}
Simple implementation for IStudentRepository.cs
, the implementation will added later.
using Domain.Entities;
namespace Application.Interfaces
{
public interface IStudentRepository
{
// .. simple implementation, you can add yours! π
Task<IReadOnlyList<Student>> GetAllStudentAsync(CancellationToken cancellation);
Task<Student> FindStudentByNo(string studentNo, CancellationToken cancellation);
Task CreateAsync(Student student, CancellationToken cancellation);
Task UpdateStudentAsync(Student student, CancellationToken cancellation);
Task RemoveAsync(int Id, CancellationToken cancellation);
}
}
Simple implementation for IStudentSubjectRespository.cs
public interface IStudentSubjectRespository
{
Task<StudentSubject> CreateSubjectAsync(StudentSubject studentSubject, CancellationToken cancellation);
}
namespace Application.Interfaces
{
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
}
Settings Up Dependencies for Presentation / API Layer
The Presentation Layer will reference to Instructure Layer and Application Layer. See below source for API.csproj
:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
</Project>
For the database connection refer appsettings.json
as per below :
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "DataSource=app.db;Cache=Shared"
}
}
Settings up the Entities / Domain Layer
This is a very simple demo where the structure of the entities is design for brevity guys.
I just created a simple common abstract class BaseEntity.cs
and BaseAuditableEntity.cs
as per below.
This can easily be modified to be
BaseEntity<T>
and public T likeguid
Id to support different key types. Using non-generic integer types for simplicity for this demo purposes ya hehe.
public abstract class BaseEntity
{
public int Id { get; set; }
}
namespace Domain.Common
{
public abstract class BaseAuditableEntity : BaseEntity
{
public DateTimeOffset Created { get; set; }
public string? CreatedBy { get; set; }
public DateTimeOffset LastModified { get; set; }
public string? LastModifiedBy { get; set; }
}
}
Next Setup a simple Entity Student.cs
and StudentSubject.cs
as of our demo as per below.
using Domain.Common;
namespace Domain.Entities
{
public class Student : BaseAuditableEntity
{
public Student()
{
StudentSubjects = Array.Empty<StudentSubject>();
}
public string Name { get; set; }
public string StudentNo { get; set; }
public ICollection<StudentSubject> StudentSubjects { get; set; }
}
}
Properties:
- Name: A string property to store the student's name.
- StudentNo: A string property to store the student's identification number.
- StudentSubjects: A collection of StudentSubject objects representing the subjects associated with the student.
using Domain.Common;
using System.ComponentModel.DataAnnotations.Schema;
namespace Domain.Entities
{
public class StudentSubject : BaseAuditableEntity
{
public int StudentId { get; set; }
[ForeignKey(nameof(StudentId))]
public Student Student { get; set; }
public string Name { get; set; }
}
}
Properties:
- Name: A string property to store the subject's name.
- StudentId : A foreign Key to
Student
table.
Setup Infrastructure layer (EF Core)
Install dependencies package for the Infrastucture layer or you can copy the ItemGroup
as per below and paste to your Infrastructure.csproj
file.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Add Application Layer reference
to Infrastructure Layer
Your latest Infrastructure.csproj
info should be like this.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Application\Application.csproj" />
</ItemGroup>
</Project>
Should we add Domain Layer into our Infrastructure Layer?
No Need! Remember our Dependency Rules
Domain Layer: Should be independent and not have any dependencies on other layers. It is the most stable part of the application.
Remember earlier in my post, i mention to add Reference Domain to Application layer. Adding Application Layer to Infrastructure Layer is enough where Domain Layer will automatically reference as well. Cool Eh π. So the Domain Layer remain intact independently.
Create our DemoContext.cs
implementation :
Nothing fancy, but i want you guys to take noted on override method Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
which inheris from the IUnitOfWork.cs
interface.
using Application.Interfaces;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Data
{
public class DemoContext : DbContext, IUnitOfWork
{
public DemoContext(DbContextOptions<DemoContext> options) : base(options)
{
}
public DbSet<Student> Students { get; set; }
public DbSet<StudentSubject> StudentSubject { get; set; }
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return base.SaveChangesAsync(cancellationToken);
}
}
}
Implementation of GenericRepository.cs
namespace Infrastructure.Repositories
{
internal class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly DemoContext _appContext;
private readonly DbSet<T> _dbSet;
protected GenericRepository(DemoContext appContext)
{
_appContext = appContext;
_dbSet = _appContext.Set<T>();
}
public IDbConnection QueryConnection => _appContext.Database.GetDbConnection();
public virtual async Task<T> AddAsync(T entity, CancellationToken cancellation)
{
await _dbSet.AddAsync(entity, cancellation);
return entity;
}
public virtual async Task AddRangeAsync(IEnumerable<T> entities, CancellationToken cancellation)
{
await _dbSet.AddRangeAsync(entities, cancellation);
}
public virtual IQueryable<T> Find(Expression<Func<T, bool>> expression)
{
return _dbSet.Where(expression);
}
public virtual IQueryable<T> GetAll()
{
return _dbSet;
}
public virtual async Task<T?> GetIdAsync(int id, CancellationToken cancellation)
{
return await _dbSet.FindAsync(id, cancellation);
}
public virtual Task RemoveAsync(T entity, CancellationToken cancellation)
{
_dbSet.Remove(entity);
return Task.CompletedTask;
}
public virtual Task UpdateAsync(T entity, CancellationToken cancellation)
{
_dbSet.Update(entity);
return Task.CompletedTask;
}
public virtual Task UpdateRangeAsync(IEnumerable<T> entities, CancellationToken cancellation)
{
_dbSet.UpdateRange(entities);
return Task.CompletedTask;
}
}
}
Implements StudentRepository.cs
using Application.Interfaces;
using Domain.Entities;
using Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Repositories
{
internal sealed class StudentRepository : GenericRepository<Student>, IStudentRepository
{
private readonly DemoContext _appContext;
private readonly ILogger<StudentRepository> _logger;
public StudentRepository(DemoContext appContext, ILogger<StudentRepository> logger) : base(appContext)
{
_appContext = appContext;
_logger = logger;
}
public async Task<Student> CreateAsync(Student student, CancellationToken cancellation)
{
try
{
return await AddAsync(student, cancellation);
}
catch (Exception ex)
{
_logger.LogError(ex, "Create Student Error");
throw;
}
}
public async Task<Student?> FindStudentByNo(string studentNo, CancellationToken cancellation)
{
return await GetAll().AsNoTracking()
.FirstOrDefaultAsync(x => x.StudentNo == studentNo, cancellation);
}
public async Task<IReadOnlyList<Student>> GetAllStudentAsync(CancellationToken cancellation)
{
return await GetAll().AsNoTracking().ToListAsync(cancellation);
}
public async Task RemoveAsync(int Id, CancellationToken cancellation)
{
try
{
var student = await GetIdAsync(Id, cancellation);
if (student is null)
{
throw new Exception("Record Not Exists");
}
await RemoveAsync(student, cancellation);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Remove");
throw;
}
}
public async Task UpdateAsync(Student student, CancellationToken cancellation)
{
try
{
await UpdateAsync(student, cancellation);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Upadate");
throw;
}
}
}
}
Implements StudentSubjectRepository.cs
using Application.Interfaces;
using Domain.Entities;
using Infrastructure.Data;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Repositories
{
internal sealed class StudentSubjectRepository : GenericRepository<StudentSubject>, IStudentSubjectRespository
{
private readonly DemoContext _appContext;
private readonly ILogger<StudentSubjectRepository> _logger;
public StudentSubjectRepository(DemoContext appContext, ILogger<StudentSubjectRepository> logger) : base(appContext)
{
_appContext = appContext;
_logger = logger;
}
public async Task<StudentSubject> CreateSubjectAsync(StudentSubject studentSubject, CancellationToken cancellation)
{
try
{
return await AddAsync(studentSubject, cancellation);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Create Subject");
throw;
}
}
}
}
Setup ExtensionMethod RegisterDI.cs
to Register Dependencies For Our Infrastructure Layer:
using Application.Interfaces;
using Infrastructure.Data;
using Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Infrastructure
{
public static class RegisterDI
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<DemoContext>((serviceProvider, opt) =>
{
//... you guys shouls use appSettings.json or the best one is Environment Variable β οΈ
string? connectionString = config.GetConnectionString("DefaultConnection");
opt.UseSqlite(connectionString, opt =>
{
opt.CommandTimeout(30);
});
});
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
services.AddScoped<IUnitOfWork>((sp) => sp.GetRequiredService<DemoContext>());
services.AddScoped<IStudentRepository, StudentRepository>();
services.AddScoped<IStudentSubjectRespository, StudentSubjectRepository>();
return services;
}
}
}
Generate The Code First Migrations
For this demo, i will use SQLite Database for simplicity. Lets power up our package manage console and type as per below :
dotnet ef migrations add "initial" -p .\Infrastructure\Infrastructure.csproj -s .\API\API.csproj -o .\Data\Migrations\
- dotnet ef migrations add "initial" : setup our migrations by tag the name with "initial"
- -p .\Infrastructure\Infrastructure.csproj : project that refers to change contains our
DbContext
which is Infrastructure Layer. - -s .\API\API.csproj : set our starup project which is our Presentation Layer or our Web API project.
- -o .\Data\Migrations\ : Indicate that i want the output for this migrations code to specific folder. which is more manageable π.
Next, we will execute the code migration initial
with below script :
dotnet ef database update "initial" -p .\Infrastructure\Infrastructure.csproj -s .\API\API.csproj
Implement Student Service on Application Layer
Next, let implement the Student Service at the application layer to be used by Presentation Layer later. Lets create StudentService.cs
as per below :
using Application.Common.Student;
using Application.Interfaces;
using Domain.Entities;
using MapsterMapper;
using Microsoft.Extensions.Logging;
namespace Application.Services
{
public sealed class StudentService
{
private readonly IStudentRepository _studentRepository;
private readonly IStudentSubjectRespository _studentSubjectRepo;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<StudentService> _logger;
private readonly IMapper _mapper;
public StudentService(IStudentRepository studentRepository, IUnitOfWork unitOfWork, ILogger<StudentService> logger, IStudentSubjectRespository studentSubjectRepo, IMapper mapper)
{
_studentRepository = studentRepository;
_unitOfWork = unitOfWork;
_logger = logger;
_studentSubjectRepo = studentSubjectRepo;
_mapper = mapper;
}
public async Task<IReadOnlyList<ListStudentResponse>> GetStudentsAsync(CancellationToken cancellationToken)
{
try
{
var results = await _studentRepository.GetAllStudentAsync(cancellationToken);
return _mapper.Map<IReadOnlyList<ListStudentResponse>>(results);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error GetAllStudents");
return Array.Empty<ListStudentResponse>();
}
}
public async Task<Student?> CreateStudentAsync(CreateStudentRequest createStudentRequest, CancellationToken cancellationToken)
{
try
{
var student = new Student
{
Name = createStudentRequest.Name,
StudentNo = createStudentRequest.StudentNo,
};
await _studentRepository.CreateAsync(student, cancellationToken);
var studentSubject = new StudentSubject
{
Name = createStudentRequest.Subject.name,
Student = student
};
await _studentSubjectRepo.CreateSubjectAsync(studentSubject, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
//.. we also can have domain event if u prefer the CQRS pattern
return student;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error Create Student");
return null; //there is a better way to return this by using Result<T>. for this demo just return null
}
}
public async Task<Student?> FindByStudentNo(string studentNo, CancellationToken cancellation)
{
return await _studentRepository.FindStudentByNo(studentNo, cancellation);
}
}
}
StudentService.cs
Class Summary:
Purpose: Manages student-related operations such as fetching and creating student records.
Dependencies:
- IStudentRepository: Interface for accessing student data.
- IStudentSubjectRespository: Interface for accessing studentsubject data.
- IUnitOfWork: Interface for handling transaction commits.
- ILogger
: Logger for error handling and diagnostics. - IMapper: This is for mapping the domain entitities to DTO / ViewModel to the client.
Notice i implement Mapster for mapping object type response to the client. In this demo, i just use for
GetStudentsAsync
function which map toIReadOnlyList<ListStudentResponse>
instead of usingStudent
type itself.
Implementation of Mapster Mapping for our demo. Im using the static class MapsterConfig.cs
as per below.
using Application.Common.Student;
using Domain.Entities;
using Mapster;
namespace Application.Mappings
{
public static class MapsterConfig
{
public static void ConfigureMapster()
{
TypeAdapterConfig<Student, ListStudentResponse>
.NewConfig()
.Map(dst => dst.StudentSubjectResponse, src => src.StudentSubjects);
}
}
}
So to make Mapster working to the response in our code can refer to the StudentService.cs
back above or my snippet below :-
var results = await _studentRepository.GetAllStudentAsync(cancellationToken); return _mapper.Map<IReadOnlyList
>(results); // .. map automatically like a charmπ
Then we register our Application Service thru the dependencies inject. Thus, lets create a new file name called RegisterDI.cs
and the implementation as per below :
using Application.Mappings;
using Application.Services;
using Mapster;
using Microsoft.Extensions.DependencyInjection;
namespace Application
{
public static class RegisterDI
{
public static IServiceCollection AddApplicationService(this IServiceCollection services)
{
services.AddMapster();
MapsterConfig.ConfigureMapster();
services.AddScoped<StudentService>();
return services;
}
}
}
Web API implementation
Next, lets wired up StudentController.cs
for the API to be access thru other clients.
using Application.Common.Student;
using Application.Services;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class StudentController : ControllerBase
{
private readonly StudentService _studentService;
private readonly ILogger<StudentController> _logger;
public StudentController(StudentService studentService, ILogger<StudentController> logger)
{
_studentService = studentService;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> GetAllStudents(CancellationToken cancellationToken)
{
return Ok(await _studentService.GetStudentsAsync(cancellationToken));
}
[HttpGet("{studentNo}")]
public async Task<IActionResult> GetByStudentNo(string studentNo, CancellationToken cancellationToken)
{
var result = await _studentService.FindByStudentNo(studentNo, cancellationToken);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpPost]
public async Task<IActionResult> CreateStudent(CreateStudentRequest createStudentRequest, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var stud = await _studentService.CreateStudentAsync(createStudentRequest, cancellationToken);
if (stud is null)
{
return BadRequest();
}
return Ok(stud.Id);
}
}
}
Since everything has been setup nicely, now its time to run our API project and the swagger full shown as below.
Lets create our first record with the POST
method. Structured the data as per below :
{
"name": "Faris",
"studentNo": "faris123",
"subject": {
"name": "Maths for dummies π"
}
}
Response from POST
API.
Now, let try to check the GET
method to list all of the data that has been saved. Response from GET
API.
Pretty Neat ah! hehe.
Conclusion
This is totally one of my long post of writing this content. I hope you guys like it and give me a support. This demo, is totally for beginner where i did this for the brevity of the content. So everyone will get the idea how its work and how to implements.
Source Code
Dont worry the source code is here Source β€οΈ.