In this article we are going to learn an introduction about Clean Architecture on .NET. We are going to create 3 projects (Application Core, Infrastructure and Web Api).
You can find the slides here.
Prerequisites:
- Visual Studio 2022 with .NET 6 SDK
- SQL Server Database
1. Create Application Core project
Create a blank solution named "StoreCleanArchitecture" and add a solution folder named "src", inside this create a "Class library project" (create the src folder the directory project as well) with .NET Standard 2.1
Create the following folders:
Install AutoMapper.Extensions.Microsoft.DependencyInjection.
Create DependencyInjection class.
using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace Store.ApplicationCore { public static class DependencyInjection { public static IServiceCollection AddApplicationCore(this IServiceCollection services) { services.AddAutoMapper(Assembly.GetExecutingAssembly()); return services; } } }
In Entities folder, create Product class.
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Store.ApplicationCore.Entities { public class Product { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [MaxLength(30)] public string Name { get; set; } public string Description { get; set; } public int Stock { get; set; } public double Price { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } }
In DTOs folder, create Product class to specify the requests and response.
using System; using System.ComponentModel.DataAnnotations; namespace Store.ApplicationCore.DTOs { public class CreateProductRequest { [Required] [StringLength(30, MinimumLength = 3)] public string Name { get; set; } [Required] public string Description { get; set; } [Required] [Range(0.01, 1000)] public double Price { get; set; } } public class UpdateProductRequest : CreateProductRequest { [Required] [Range(0, 100)] public int Stock { get; set; } } public class ProductResponse { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int Stock { get; set; } public double Price { get; set; } } }
In Mappings folder, create GeneralProfile class. This is useful to map automatically from the Request to the Entity and from the Entity to the Response.
using AutoMapper; using Store.ApplicationCore.DTOs; using Store.ApplicationCore.Entities; namespace Store.ApplicationCore.Mappings { public class GeneralProfile : Profile { public GeneralProfile() { CreateMap<CreateProductRequest, Product>(); CreateMap<Product, ProductResponse>(); } } }
In Interfaces folder, create IProductRepository interface. Here we create the methods for the CRUD.
using Store.ApplicationCore.DTOs; using System.Collections.Generic; namespace Store.ApplicationCore.Interfaces { public interface IProductRepository { List<ProductResponse> GetProducts(); ProductResponse GetProductById(int productId); void DeleteProductById(int productId); ProductResponse CreateProduct(CreateProductRequest request); ProductResponse UpdateProduct(int productId, UpdateProductRequest request); } }
In Exceptions folder, create NotFoundException class.
using System; namespace Store.ApplicationCore.Exceptions { public class NotFoundException : Exception { } }
In Utils folder, create DateUtil class.
using System; namespace Store.ApplicationCore.Utils { public class DateUtil { public static DateTime GetCurrentDate() { return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.Local); } } }
2. Create Infrastructure project
Create a "Class library project" with .NET 6, named Store.Infrastructure.
Create the following structure:
Install Microsoft.EntityFrameworkCore.SqlServer.
Right click on Store.Infrastucture project / Add / Project Reference ... / Check Store.ApplicationCore / OK
In Contexts folder, create StoreContext class. Here we add Product entity to the DbSets in order to communicate with the database to the Products table.
using Microsoft.EntityFrameworkCore; using Store.ApplicationCore.Entities; namespace Store.Infrastructure.Persistence.Contexts { public class StoreContext : DbContext { public StoreContext(DbContextOptions<StoreContext> options) : base(options) { } public DbSet<Product> Products { get; set; } } }
In Repositories folder, create ProductRepository class.
using AutoMapper; using Store.ApplicationCore.DTOs; using Store.ApplicationCore.Entities; using Store.ApplicationCore.Exceptions; using Store.ApplicationCore.Interfaces; using Store.ApplicationCore.Utils; using Store.Infrastructure.Persistence.Contexts; using System.Collections.Generic; using System.Linq; namespace Store.Infrastructure.Persistence.Repositories { public class ProductRepository : IProductRepository { private readonly StoreContext storeContext; private readonly IMapper mapper; public ProductRepository(StoreContext storeContext, IMapper mapper) { this.storeContext = storeContext; this.mapper = mapper; } public ProductResponse CreateProduct(CreateProductRequest request) { var product = this.mapper.Map<Product>(request); product.Stock = 0; product.CreatedAt = product.UpdatedAt = DateUtil.GetCurrentDate(); this.storeContext.Products.Add(product); this.storeContext.SaveChanges(); return this.mapper.Map<ProductResponse>(product); } public void DeleteProductById(int productId) { var product = this.storeContext.Products.Find(productId); if (product != null) { this.storeContext.Products.Remove(product); this.storeContext.SaveChanges(); } else { throw new NotFoundException(); } } public ProductResponse GetProductById(int productId) { var product = this.storeContext.Products.Find(productId); if (product != null) { return this.mapper.Map<ProductResponse>(product); } throw new NotFoundException(); } public List<ProductResponse> GetProducts() { return this.storeContext.Products.Select(p => this.mapper.Map<ProductResponse>(p)).ToList(); } public ProductResponse UpdateProduct(int productId, UpdateProductRequest request) { var product = this.storeContext.Products.Find(productId); if (product != null) { product.Name = request.Name; product.Description = request.Description; product.Price = request.Price; product.Stock = request.Stock; product.UpdatedAt = DateUtil.GetCurrentDate(); this.storeContext.Products.Update(product); this.storeContext.SaveChanges(); return this.mapper.Map<ProductResponse>(product); } throw new NotFoundException(); } } }
In DependencyInjection class, add the following:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Store.ApplicationCore.Interfaces; using Store.Infrastructure.Persistence.Contexts; using Store.Infrastructure.Persistence.Repositories; namespace Store.Infrastructure { public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) { var defaultConnectionString = configuration.GetConnectionString("DefaultConnection"); services.AddDbContext<StoreContext>(options => options.UseSqlServer(defaultConnectionString)); services.AddScoped<IProductRepository, ProductRepository>(); return services; } } }
There we are configuring the db context and adding IProductRepository to the services collection as Scoped.
3. Create Web Api project
Create a "Web Api project" with .NET 6, named Store.WebApi.
Right click on Store.WebApi / Set as Startup project.
At the top, click on Debug / Start Without Debugging.
Remove WeatherForecast and WeatherForecastController files.
Add the references to the Store.ApplicationCore and Store.Infrastructure projects.
Add the connection string to SQL Server in appsettings.json
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=localhost;Database=DemoStore;Trusted_Connection=True;" } }
In Program class, add the extensions for Application Core and Infrastructure.
using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Store.ApplicationCore; using Store.Infrastructure; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddApplicationCore(); builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();
Open Package Manager Console and select Store.Infrastructure project as default. Execute Add-Migration InitialCreate -Context StoreContext
.
In Store.Infrastructure project, a Migrations folder with 2 files inside were created.
Then, from the Package Manager Console, execute Update-Database
.
From Controllers, add a controller named ProductsController
using Microsoft.AspNetCore.Mvc; using Store.ApplicationCore.DTOs; using Store.ApplicationCore.Exceptions; using Store.ApplicationCore.Interfaces; using System.Collections.Generic; namespace Store.WebApi.Controllers { [Route("api/[controller]")] [ApiController] public class ProductsController : Controller { private readonly IProductRepository productRepository; public ProductsController(IProductRepository productRepository) { this.productRepository = productRepository; } [HttpGet] public ActionResult<List<ProductResponse>> GetProducts() { return Ok(this.productRepository.GetProducts()); } [HttpGet("{id}")] public ActionResult GetProductById(int id) { try { var product = this.productRepository.GetProductById(id); return Ok(product); } catch (NotFoundException) { return NotFound(); } } [HttpPost] public ActionResult Create(CreateProductRequest request) { var product = this.productRepository.CreateProduct(request); return Ok(product); } [HttpPut("{id}")] public ActionResult Update(int id, UpdateProductRequest request) { try { var product = this.productRepository.UpdateProduct(id, request); return Ok(product); } catch (NotFoundException) { return NotFound(); } } [HttpDelete("{id}")] public ActionResult Delete(int id) { try { this.productRepository.DeleteProductById(id); return NoContent(); } catch (NotFoundException) { return NotFound(); } } } }
You can find the source code here.
Thanks for reading
Thank you very much for reading, I hope you found this article interesting and may be useful in the future. If you have any questions or ideas that you need to discuss, it will be a pleasure to be able to collaborate and exchange knowledge together.
Top comments (4)
In DependencyInjection (Core and Infrastructure) classes we have an error ;)

The DependencyInjection class must be static. In this article and in GitHub are static.
Ohhh. I lost this moment...
Thanks!
To be honest, after reading the article I'm not sure how this is clean architecture, not N-tier application. More sophisticated example that includes some domain logic would definitely help your case.
Some comments have been hidden by the post's author - find out more