Building a Clean Architecture with Dapper, Mediator, and Minimal APIs in .NET Core
Hamza zeryouh

Building a Clean Architecture with Dapper, Mediator, and Minimal APIs in .NET Core



In this article, we will walk through setting up a clean, maintainable, and scalable architecture for an application using Minimal APIs, Dapper, and Mediator in .NET Core. We’ll explain how each part of this architecture fits together to promote separation of concerns, maintainability, and testability.

Key Concepts

Before diving into the code, let’s briefly explain the concepts we’ll be using:

  • Clean Architecture: Clean Architecture is a pattern that focuses on separation of concerns, ensuring that your application is independent of frameworks, databases, and UI. It uses layers such as Core, Application, Infrastructure, and UI.
  • Minimal APIs: Minimal APIs in .NET 6 and beyond provide a lightweight approach to building APIs. With Minimal APIs, we can quickly define HTTP endpoints without the need for a full-fledged controller class, making it ideal for small applications and microservices.
  • Dapper: Dapper is a micro ORM (Object-Relational Mapper) that allows you to interact with your database in a simple and performant way. It’s faster and more lightweight than other ORMs like Entity Framework, making it a great choice for applications that prioritize speed and simplicity.
  • Mediator Pattern: The Mediator pattern allows you to decouple request-handling logic from the rest of your application by sending requests through a mediator. This reduces dependencies and makes the code more modular. MediatR is the .NET library that implements the Mediator pattern.


Setting Up the Project

1. Install Necessary Packages

Before you start coding, you'll need to install the following NuGet packages:

  • MediatR: To implement the Mediator pattern.
  • Dapper: For fast database interactions.
  • MediatR.Extensions.Microsoft.DependencyInjection: For integrating MediatR with ASP.NET Core's dependency injection system.

Install the packages using the following commands:

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
dotnet add package Dapper
        

2. Create the Project Structure

Here’s a simplified version of the Clean Architecture we will be following:

- Application
  - Commands
  - Queries
  - Handlers
- Core
  - Entities
  - Interfaces
- Infrastructure
  - Repositories
  - Data (Dapper-related logic)
- WebApi
  - Program.cs
  - Minimal API Endpoints
        

3. Register MediatR in Program.cs

In the Program.cs file, register MediatR and the necessary dependencies for dependency injection:

var builder = WebApplication.CreateBuilder(args);

// Register MediatR
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());

// Register Repositories and Services
builder.Services.AddSingleton<IProductRepository>(new DapperProductRepository(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ProductService>();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

var app = builder.Build();
        

4. Define Commands, Queries, and Handlers

Product Queries (For Retrieving Data)

Queries are used to fetch data. We’ll create a GetProductQuery to fetch a product by ID.

public class GetProductQuery : IRequest<ProductDto>
{
    public int Id { get; set; }

    public GetProductQuery(int id)
    {
        Id = id;
    }
}
        

Product Commands (For Modifying Data)

Commands are used to modify data. For example, CreateProductCommand will create a new product.

public class CreateProductCommand : IRequest<int>
{
    public string Name { get; set; }
    public decimal Price { get; set; }

    public CreateProductCommand(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}
        

Product Handlers (For Handling Commands/Queries)

The handler contains the business logic for executing the queries or commands. Here’s an example of a query handler for getting a product by ID:

public class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>
{
    private readonly IProductRepository _productRepository;
    private readonly IMapper _mapper;

    public GetProductQueryHandler(IProductRepository productRepository, IMapper mapper)
    {
        _productRepository = productRepository;
        _mapper = mapper;
    }

    public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetByIdAsync(request.Id);
        return _mapper.Map<ProductDto>(product);
    }
}
        

5. Dapper Repository for Data Access

We’ll implement a DapperProductRepository to handle database operations using Dapper. This will interact directly with the database to retrieve and manipulate product data.

public class DapperProductRepository : IProductRepository
{
    private readonly string _connectionString;

    public DapperProductRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            var query = "SELECT * FROM Products WHERE Id = @Id";
            return await connection.QuerySingleOrDefaultAsync<Product>(query, new { Id = id });
        }
    }

    public async Task<int> AddAsync(Product product)
    {
        using (var connection = new SqlConnection(_connectionString))
        {
            var query = "INSERT INTO Products (Name, Price) VALUES (@Name, @Price); SELECT CAST(SCOPE_IDENTITY() as int)";
            return await connection.QuerySingleAsync<int>(query, product);
        }
    }
}
        

6. Configure Minimal API Endpoints

In Program.cs, define your minimal API endpoints that map to the MediatR handlers. These endpoints will handle HTTP requests and delegate the work to the appropriate MediatR request handlers.

var app = builder.Build();

// Minimal API Endpoints using Mediator
app.MapGet("/products/{id}", async (int id, IMediator mediator) =>
{
    var query = new GetProductQuery(id);
    var product = await mediator.Send(query);
    return product is null ? Results.NotFound() : Results.Ok(product);
});

app.MapPost("/products", async (ProductDto productDto, IMediator mediator) =>
{
    var command = new CreateProductCommand(productDto.Name, productDto.Price);
    var productId = await mediator.Send(command);
    return Results.Created($"/products/{productId}", productId);
});

app.MapPut("/products/{id}", async (int id, ProductDto productDto, IMediator mediator) =>
{
    var command = new UpdateProductCommand(id, productDto.Name, productDto.Price);
    var updated = await mediator.Send(command);
    return updated ? Results.NoContent() : Results.NotFound();
});

app.MapDelete("/products/{id}", async (int id, IMediator mediator) =>
{
    var command = new DeleteProductCommand(id);
    var deleted = await mediator.Send(command);
    return deleted ? Results.NoContent() : Results.NotFound();
});

app.Run();
        

How It All Works Together

  1. Mediator: The MediatR library acts as the mediator between your API controllers (or minimal API endpoints) and the business logic (command/queries). When an API request comes in, it sends a request (like a CreateProductCommand) through MediatR. MediatR then delegates the request to the corresponding handler.
  2. Minimal APIs: Minimal APIs allow us to quickly set up HTTP endpoints that delegate the business logic to the Mediator. This is very lightweight and avoids the overhead of traditional MVC controllers.
  3. Dapper: Dapper is used as the Data Access Layer for interacting with the database. It allows us to execute SQL queries and map the results to C# objects.
  4. Clean Architecture: By adhering to Clean Architecture principles, we separate concerns between the Core (domain logic), Application (business logic and use cases), and Infrastructure (data access). This leads to a more maintainable and scalable solution.


Benefits of This Approach

  • Separation of Concerns: Mediator ensures that each component has a single responsibility and communicates through well-defined interfaces. The database logic is abstracted behind the repository layer, and the minimal API layer is only responsible for HTTP-related concerns.
  • Maintainability: The code is easier to maintain and extend because each layer has clear responsibilities. For example, if we need to change the way products are retrieved, we only need to update the DapperProductRepository without affecting the API endpoints or handlers.
  • Testability: The Mediator pattern makes it easier to mock dependencies for unit testing. Each component (commands, queries, and handlers) can be tested independently.


Conclusion

Using MediatR with Dapper, Minimal APIs, and Clean Architecture in .NET Core provides a scalable and maintainable solution. This approach allows for decoupling business logic, data access, and HTTP request handling, which results in cleaner, more modular code that’s easier to test and extend.

By leveraging Mediator to handle commands and queries, Dapper for efficient database interactions, and Minimal APIs for lightweight HTTP endpoints, you can create a performant and maintainable application in .NET Core.

follow me thanks */\*

要查看或添加评论,请登录

Hamza Zeryouh的更多文章

社区洞察

其他会员也浏览了