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:
Setting Up the Project
1. Install Necessary Packages
Before you start coding, you'll need to install the following NuGet packages:
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
Benefits of This Approach
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 */\*