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