DEV Community

Cover image for Implement Clean Architecture on .NET
Cristopher Coronado
Cristopher Coronado

Posted on • Updated on

Implement Clean Architecture on .NET

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

Image description

Create the following folders:

Image description

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

In Exceptions folder, create NotFoundException class.

using System;

namespace Store.ApplicationCore.Exceptions
{
    public class NotFoundException : Exception
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Create Infrastructure project

Create a "Class library project" with .NET 6, named Store.Infrastructure.
Create the following structure:
Image description

Install Microsoft.EntityFrameworkCore.SqlServer.
Right click on Store.Infrastucture project / Add / Project Reference ... / Check Store.ApplicationCore / OK
Image description

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; }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

Right click on Store.WebApi / Set as Startup project.
At the top, click on Debug / Start Without Debugging.

Image description

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;"
  }
}

Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Open Package Manager Console and select Store.Infrastructure project as default. Execute Add-Migration InitialCreate -Context StoreContext.
Image description

In Store.Infrastructure project, a Migrations folder with 2 files inside were created.
Image description

Then, from the Package Manager Console, execute Update-Database.
From Controllers, add a controller named ProductsController
Image description

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();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, you can test the API.
Image description

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)

Collapse
 
zznobzz profile image
ZZnOB • Edited

In DependencyInjection (Core and Infrastructure) classes we have an error ;)
Image Error

Collapse
 
cristofima profile image
Cristopher Coronado • Edited

The DependencyInjection class must be static. In this article and in GitHub are static.

Collapse
 
zznobzz profile image
ZZnOB

Ohhh. I lost this moment...
Thanks!

Collapse
 
bohdanstupak1 profile image
Info Comment hidden by post author - thread only accessible via permalink
Bohdan Stupak

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