Implement Clean Architecture on .NET
Tags: dotnet, architecture, efcore, api
Published: 2021-11-05 03:48:06

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](https://es.slideshare.net/cristophercoronado7/introduccin-a-clean-architecture-en-netpptx).

##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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irs47ba18ebwn6hfa2on.png)
 
Create the following folders:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k9qtabqg8q0w1vdq2a0a.png)

Install AutoMapper.Extensions.Microsoft.DependencyInjection.
Create DependencyInjection class.

```csharp
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.
```csharp
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.
```csharp
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.

```csharp
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.
```csharp
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.
```csharp
using System;

namespace Store.ApplicationCore.Exceptions
{
    public class NotFoundException : Exception
    {
    }
}
```

In Utils folder, create DateUtil class.
```csharp
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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l4rcfhsb9obq9g2p9gcp.png)
 
Install Microsoft.EntityFrameworkCore.SqlServer.
Right click on Store.Infrastucture project / Add / Project Reference ... / Check Store.ApplicationCore / OK
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jnt7bz176au83zi17xfm.png)
 

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.
```csharp
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.
```csharp
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:
```csharp
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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wf42ctfgfhzijirhcge.png) 

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phs5qd9d8y40joff1k99.png)

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
```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.
```csharp
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`.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r68jd3a7v83dqd43w17.png)
 
In Store.Infrastructure project, a Migrations folder with 2 files inside were created.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q2r5530qf4cukfxxvd33.png)
 
Then, from the Package Manager Console, execute `Update-Database`.
From Controllers, add a controller named ProductsController
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qkd38lq8xjlz2mtg7zlc.png)
 
```csharp
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();
            }
        }
    }
}
```

Now, you can test the API.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9fi4fo4pm56eyxz06ykd.png)
 
You can find the source code [here](https://github.com/cristofima/StoreCleanArchitecture-NET/releases/tag/v2.0.0).

**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.