Complete example of clean architecture in C# and .net core 7

DHEERAJ KUMAR
5 min readMay 8, 2023
clean architecture

First, let’s start with the project structure. We’ll have three main layers:

  • Domain Layer: Contains the business logic of the application.
  • Infrastructure Layer: Contains the implementation of the interfaces defined in the domain layer.
  • Presentation Layer: Contains the UI logic and the communication between the user and the system.

The following is the directory structure:

├── Domain
│ ├── Entities
│ ├── Interfaces
│ └── Services
├── Infrastructure
│ ├── Data
│ ├── Services
│ └── Repositories
├── Presentation
│ ├── Controllers
│ ├── DTOs
│ ├── Extensions
│ └── Middleware
├── Application.csproj
└── Application.Tests.csproj

Let’s take a closer look at each layer:

Domain Layer

├── Domain
│ ├── Entities
│ ├── Interfaces
│ └── Services

The domain layer contains the business logic of the application. This layer has no dependencies on any other layer. It defines the entities, interfaces, and services used throughout the application.

namespace Domain.Entities
{
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}

namespace Domain.Interfaces
{
public interface IUserRepository
{
Task<User> GetUserById(int id);
Task<List<User>> GetAllUsers();
Task CreateUser(User user);
Task UpdateUser(User user);
Task DeleteUser(int id);
}
}

namespace Domain.Services
{
public interface IUserService
{
Task<User> GetUserById(int id);
Task<List<User>> GetAllUsers();
Task CreateUser(User user);
Task UpdateUser(User user);
Task DeleteUser(int id);
}
}

Infrastructure Layer

├── Infrastructure
│ ├── Data
│ ├── Services
│ └── Repositories

The infrastructure layer contains the implementation of the interfaces defined in the domain layer. This layer has dependencies on the domain layer. It provides concrete implementations of the domain layer interfaces and handles all external dependencies.

namespace Infrastructure.Repositories
{
public class UserRepository : IUserRepository
{
private readonly IDbContext _dbContext;

public UserRepository(IDbContext dbContext)
{
_dbContext = dbContext;
}

public async Task<User> GetUserById(int id)
{
return await _dbContext.Users.FindAsync(id);
}

public async Task<List<User>> GetAllUsers()
{
return await _dbContext.Users.ToListAsync();
}

public async Task CreateUser(User user)
{
await _dbContext.Users.AddAsync(user);
await _dbContext.SaveChangesAsync();
}

public async Task UpdateUser(User user)
{
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
}

public async Task DeleteUser(int id)
{
var user = await GetUserById(id);
_dbContext.Users.Remove(user);
await _dbContext.SaveChangesAsync();
}
}
}

namespace Infrastructure.Services
{
public class UserService : IUserService
{
private readonly IUserRepository _userRepository;

public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}

public async Task<User> GetUserById(int id)
{
return await _userRepository.GetUserById(id);
}

public async Task<List<User>> GetAllUsers()
{
return await _userRepository.GetAllUsers();
}

public async Task CreateUser(User user)
{
await _userRepository.CreateUser(user);
}

public async Task UpdateUser(User user)
{
await _userRepository.UpdateUser(user);
}

public async Task DeleteUser(int id)
{
await _userRepository.DeleteUser(id);
}
}
}

namespace Infrastructure.Data
{
public class ApplicationDbContext : DbContext, IDbContext
{
public DbSet<User> Users { get; set; }

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}

public async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
}
}
}

Presentation Layer

├── Presentation
│ ├── Controllers
│ ├── DTOs
│ ├── Extensions
│ └── Middleware

The presentation layer contains the UI logic and the communication between the user and the system. This layer has dependencies on the domain and infrastructure layers. It handles all the incoming and outgoing requests.

namespace Presentation.DTOs
{
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
}

namespace Presentation.Controllers {
[ApiController]
[Route(“api/[controller]”)]
public class UsersController: ControllerBase {
private readonly IUserService _userService;
public UsersController(IUserService userService) {
_userService = userService;
}
[HttpGet(“{id}”)]
public async Task < ActionResult < UserDto >> GetUserById(int id) {
var user = await _userService.GetUserById(id);
if (user == null) {
return NotFound();
}
return new UserDto {
Id = user.Id,
Name = user.Name,
Email = user.Email
};
}
[HttpGet]
public async Task < ActionResult < List < UserDto >>> GetAllUsers() {
var users = await _userService.GetAllUsers();
if (users == null || users.Count == 0) {
return NotFound();
}
return users.Select(u => new UserDto {
Id = u.Id,
Name = u.Name,
Email = u.Email
}).ToList();
}
[HttpPost]
public async Task < ActionResult < UserDto >> CreateUser(UserDto userDto) {
var user = new User {
Name = userDto.Name,
Email = userDto.Email
};
await _userService.CreateUser(user);
return CreatedAtAction(nameof(GetUserById), new {
id = user.Id
}, new UserDto {
Id = user.Id,
Name = user.Name,
Email = user.Email
});
}
[HttpPut(“{id}”)]
public async Task < IActionResult > UpdateUser(int id, UserDto userDto) {
var user = await _userService.GetUserById(id);
if (user == null) {
return NotFound();
}
user.Name = userDto.Name;
user.Email = userDto.Email;
await _userService.UpdateUser(user);
return NoContent();
}
[HttpDelete(“{id}”)]
public async Task < IActionResult > DeleteUser(int id) {
var user = await _userService.GetUserById(id);
if (user == null) {
return NotFound();
}
await _userService.DeleteUser(id);
return NoContent();
}
}
}

//
namespace Presentation.Extensions {
public static class ApplicationBuilderExtensions {
public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder app) {
app.UseExceptionHandler(errorApp => {
errorApp.Run(async context => {
var exceptionHandlerPathFeature = context.Features.Get < IExceptionHandlerPathFeature > ();
var exception = exceptionHandlerPathFeature.Error;
var problemDetails = new ProblemDetails {
Instance = context.Request.Path,
Status = StatusCodes.Status500InternalServerError,
Title = “An error occurred”,
Detail = exception.Message
};
context.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
context.Response.ContentType = “application/problem+json”;
await context.Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
});
}
}
}
}

// middleware

namespace Presentation.Middleware {
public class ExceptionHandlingMiddleware {
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext) {
try {
await _next(httpContext);
} catch (Exception ex) {
httpContext.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
httpContext.Response.ContentType = “application/json”;
var response = new ErrorResponse {
ErrorMessage = ex.Message,
StatusCode = httpContext.Response.StatusCode
};
var responseJson = JsonSerializer.Serialize(response);
await httpContext.Response.WriteAsync(responseJson);
}
}
}
}

In the Presentation layer, we have a `DTO` folder that contains the Data Transfer Objects for the User model. These are used to transfer data between the client and the server.

The `Controllers` folder contains the endpoints for handling user requests. These endpoints use the `IUserService` interface to interact with the domain layer.

The `Extensions` folder contains the `UseExceptionHandling` method, which is used to handle exceptions globally in the application.

The `Middleware` folder contains the `ExceptionHandlingMiddleware` class, which is used to handle exceptions in the HTTP pipeline.

This concludes the implementation of the Clean Architecture pattern in C# and .NET Core 7. As you can see, the pattern separates concerns and makes the code modular and testable.

--

--

DHEERAJ KUMAR

.Net Core Expert and an Experienced Full Stack Engineer (nodejs+Angular 9) with a demonstrated history of working in the product development software company.