Clean Architecture is a software design pattern that helps developers create maintainable, scalable, and testable applications. It enforces separation of concerns by dividing the application into distinct layers. In this article, we will explore how to set up and run a Clean Architecture-based .NET Core API locally.
1. Setting Up Clean Architecture Folder Structure
A Clean Architecture solution typically consists of the following layers:
- Domain: Core business logic and entities.
- Application: Use cases and service interfaces.
- Infrastructure: Data persistence, external service integration.
- Presentation: API controllers and UI.
1.1 Creating the Solution
Create a new .NET Core solution and projects using the CLI:
dotnet new sln -n CleanArchitectureExample
cd CleanArchitectureExample
dotnet new classlib -n Domain
dotnet new classlib -n Application
dotnet new classlib -n Infrastructure
dotnet new webapi -n Presentation
dotnet sln add Domain Application Infrastructure Presentation
1.2 Defining Project Dependencies
Update dependencies to follow Clean Architecture principles:
dotnet add Application reference Domain
dotnet add Infrastructure reference Application
dotnet add Presentation reference Application
dotnet add Presentation reference Infrastructure
This ensures the correct flow of dependencies, keeping Domain and Application layers free of external dependencies.
2. Implementing Clean Architecture Layers
2.1 Domain Layer
The Domain Layer contains core entities and business rules. Define an entity inside the Domain
project:
namespace Domain.Entities
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
2.2 Application Layer
The Application Layer defines use cases and service interfaces. Create a service interface:
namespace Application.Interfaces
{
public interface IProductService
{
Task<IEnumerable<Product>> GetProductsAsync();
Task<Product> GetProductByIdAsync(int id);
}
}
2.3 Infrastructure Layer
The Infrastructure Layer handles data persistence and external services. Implement the product service:
using Application.Interfaces;
using Domain.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Infrastructure.Services
{
public class ProductService : IProductService
{
private static readonly List<Product> _products = new()
{
new Product { Id = 1, Name = "Laptop", Price = 1200 },
new Product { Id = 2, Name = "Phone", Price = 800 }
};
public async Task<IEnumerable<Product>> GetProductsAsync() => await Task.FromResult(_products);
public async Task<Product> GetProductByIdAsync(int id) => await Task.FromResult(_products.FirstOrDefault(p => p.Id == id));
}
}
2.4 Presentation Layer
The Presentation Layer exposes API endpoints. Register services and configure dependency injection in Startup.cs
:
using Application.Interfaces;
using Infrastructure.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
app.Run();
Add a controller:
using Application.Interfaces;
using Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Presentation.Controllers
{
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<IEnumerable<Product>> GetProducts() => await _productService.GetProductsAsync();
[HttpGet("{id}")]
public async Task<Product> GetProduct(int id) => await _productService.GetProductByIdAsync(id);
}
}
3. Writing Unit and Integration Tests
3.1 Setting Up Unit Tests
Create a new test project:
dotnet new xunit -n Application.Tests
dotnet add Application.Tests reference Application
dotnet add Application.Tests reference Domain
Write a unit test for ProductService
:
using Application.Interfaces;
using Domain.Entities;
using Infrastructure.Services;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
public class ProductServiceTests
{
private readonly IProductService _service = new ProductService();
[Fact]
public async Task GetProductsAsync_ShouldReturnProducts()
{
var products = await _service.GetProductsAsync();
Assert.NotEmpty(products);
}
[Fact]
public async Task GetProductByIdAsync_ShouldReturnProduct()
{
var product = await _service.GetProductByIdAsync(1);
Assert.NotNull(product);
Assert.Equal("Laptop", product.Name);
}
}
3.2 Writing Integration Tests
Create a test project for API integration tests:
dotnet new xunit -n Presentation.Tests
dotnet add Presentation.Tests reference Presentation
Write an integration test:
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Threading.Tasks;
using Xunit;
public class ProductsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsControllerTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProducts_ShouldReturnSuccess()
{
var response = await _client.GetAsync("/api/products");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
Conclusion
In this article, we covered:
- Setting up a Clean Architecture folder structure
- Implementing Domain, Application, Infrastructure, and Presentation layers
- Writing unit and integration tests for a robust .NET Core API
By following Clean Architecture principles, we ensure modularity, testability, and maintainability in our .NET applications. Start building your Clean Architecture-based .NET Core API today!