In this article, we will learn about Clean Architecture and then we will implement a .Net5 sample application.
UI Application:
Clean Architecture:
Clean Architecture core building blocks are:- Application Core
- Infrastructure
- UI Application
Clean Architecture lives on the dependency inversion principle. In general business, logic depends on the data access layer or infrastructure layer. But in clean architecture, it is inverted, which means data access layers or infrastructure layers depend on the business logic layer(which means Application Core). So with the dependency inversion technique it easy to configure 'Unit Test' or 'Integrating Test'.
Application Core:
- Application Core is a top layer or parent layer which will not depend on any other layer. So other layers like Infrastructure or UI depend on the 'Application' core.
- Application Core contains 'Entites', 'DTOs', 'Interfaces', 'BusinessLogics', etc.
- So while creating projects if we want we can split 'Application Core' into 2 separate projects like 'Domain' and 'Application'. So 'Domain' project mostly contains 'Entites'(table entities) then 'Application' project contains 'DTO's', 'Interfaces', 'BusinessLogics',etc. So the 'Application' project depends on the 'Domain'. So 'Domain' project can be sharable with other projects as well since it is the parent of layers. But please remember this splitting is only optional.
Infrastructure:
Infrastructure deals with 'DataBases', 'External API Calls', 'Cache', etc. Basically, infrastructure deals with all external resources. Infrastructure depends on the 'Interface' inside of the 'Application Core'. Because of the dependency inversion, our 'Application Core' will be loosely coupled which is easy to test.
UI Application:
UI Application consumes the 'Application Core' to produce the results. In a real-time scenario UI Application never depends on the infrastructure layer, but we have to reference the infrastructure layer into the UI project in the case to register the services dependency injection. So UI project should not use any code of the infrastructure layer other than dependency injection.
Project Structure:
Now we will implement a sample of web API that builds on top of the clean architecture. So here required example dotnet projects look like:
- CleanArchitecture.Sample.Domain - 'Application Core' - A class library template project.
- CleanArchitecture.Sample.Application - 'Application Core' - A class library template project.
- CleanArchitecture.Sample.Infrastructure - 'Infrastructure' - A class library template project.
- CleanArchitecture.Sample.Api - 'UI/ API Application' - A web API template project.
Add Project References:
Now let's add library references to the respective dependent projects.
Application Core:
We split 'Application Core' into 2 projects like 'Domain' and 'Application'.
The 'CleanArchitecture.Sample.Domain' project doesn't have any dependent library. So no project references to this project.
The 'CleanArchitecture.Sample.Application' project depends on 'CleanArchitecture.Sample.Domain'.
So the 'Application' project needs to add the references of the 'Domain' project.
Infrastructure:
The 'CleanArchitecture.Sample.Infrastructure' project depends on 2 projects like 'CleanArchitecture.Sample.Domain', 'CleanArchitecture.Sample.Application'.The 'Infrastructure' depends on the 'Domain' project for table entities. The 'Infrastructure' depends on the 'Application' project for the interfaces.
UI/Api Project:
The 'CleanArchitecture.Sample.Api' project depends on 2 projects like 'CleanArchitecture.Sample.Application', 'CleanArchitecture.Sample.Infrastructure'. The 'Api' project depends on the 'Application' project to consume the business logic. The 'Api' project technically not depends on the 'Infrastructure' project, but we do add the reference because to register the 'Infrastructure' services with the dependency injection.
Extend And Regiser Dependency Injection:
Let's extend our dependency injection into the 'CleanArchitecture.Sample.Application' and 'CleanArchitecture.Sample.Infrastructure' projects. So let's install the below nugets into the both the projects.
Package Manager Commands:
Install-Package Microsoft.Extensions.DependencyInjection -Version 5.0.1
Install-Package Microsoft.Extensions.Configuration -Version 5.0.0
CLI Commands:
dotnet add package Microsoft.Extensions.DependencyInjection --version 5.0.1
dotnet add package Microsoft.Extensions.Configuration --version 5.0.0
Now create dependency injection extension method into the 'CleanArchitecture.Sample.Application' project.CleanArchitecture.Sample.Application/Injection.cs:
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace CleanArchitecture.Sample.Application { public static class Injectcion { public static IServiceCollection RegisterApplicationServices( this IServiceCollection service, IConfiguration configuration) { return service; } } }Now create dependency injection extension method into the 'CleanArchitecture.Sample.Infrastructure' project.
CleanArchitecture.Sample.Infrastructure/Injection.cs:
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace CleanArchitecture.Sample.Infrastructure { public static class Injectcion { public static IServiceCollection RegisterInfrastructerServices( this IServiceCollection service, IConfiguration configuration) { return service; } } }Now register the newly created DI extension method into the 'CleanArchitecture.Sample.Api' project.
CleanArchitecture.Sample.Api/Startup.cs:(ConfigureServices method)
services.RegisterApplicationServices(Configuration); services.RegisterInfrastructerServices(Configuration);
Create A Entity In Domain Project:
We know that our 'Domain' project contains our table entities. So let's create a entity like 'Gadget.cs'.
CleanArchitecture.Sample.Domain/Entities/Gadget.cs:
namespace CleanArchitecture.Sample.Domain.Entities { public class Gadgets { public int Id { get; set; } public string ProductName { get; set; } public string Brand { get; set; } public decimal Cost { get; set; } public string Type { get; set; } } }
Create DbContext Interface In Application Project:
We know that all 'Interfaces' will be created within the 'Application' project. First, let's install the entity framework core nuget into the 'Application' project.
Package Manager Command:
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.6
CLI Command:
dotnet add package Microsoft.EntityFrameworkCore --version 5.0.6
Now let's create the 'Interface' of DbContext into the 'Application' project.CleanArchitecture.Sample.Application/Contracts/Data/ImyWorldDbContext:
using CleanArchitecture.Sample.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Sample.Application.Contracts.Data { public interface IMyWorldDbContext { public DbSet<Gadgets> Gadgets { get; } } }
Implement DbContext In Infrastructure Project:
We know 'Infrastructure' project meant for to work with external services like 'Database', 'Cache', 'Api Calls', etc.
First let's install the entityframework core and SQL server nugets into our 'Infrastructure' project.
Package Manager:
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.6
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.6
CLI Command:
dotnet add package Microsoft.EntityFrameworkCore --version 5.0.6
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 5.0.6
Now let's implement the DbContext into the 'Infrastructure' projecte.CleanArchitecture.Sample.Infrastructure/Data/MyWorldDbContext.cs:
using CleanArchitecture.Sample.Application.Contracts.Data; using CleanArchitecture.Sample.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace CleanArchitecture.Sample.Infrastructure.Data { public class MyWorldDbContext : DbContext, IMyWorldDbContext { public MyWorldDbContext(DbContextOptions<MyWorldDbContext> options) : base(options) { } public DbSet<Gadgets> Gadgets { get; set; } } }Register the DbContext services into the 'Injection.cs' file.
CleanArchitecture.Sample.Infrastructure/Injection.cs:
services.AddDbContext<MyWorldDbContext>(options => { options.UseSqlServer(configuration.GetConnectionString("MyWorldDbConnection")); }); services.AddScoped<IMyWorldDbContext>(optiont => optiont.GetService<MyWorldDbContext>());
Note: Add database connection string in 'appsettings.json' file in 'Api' Project.
Add Business Logic In Application Project:
First, let's create a data transfer object(DTO) in our 'Application' Project.
CleanArchitecture.Sample.Application/DTOs/GadgetDto.cs:
namespace CleanArchitecture.Sample.Application.DTOs { public class GadgetDto { public int Id { get; set; } public string ProductName { get; set; } public string Brand { get; set; } public decimal Cost { get; set; } public string Type { get; set; } } }Now let's add an interface for our business logic into our 'Application' project.
CleanArchitecture.Sample.Application/Contracts/Logics:
using System.Collections.Generic; using CleanArchitecture.Sample.Application.DTOs; namespace CleanArchitecture.Sample.Application.Contracts.Logics { public interface IGadgetLogic { List<GadgetDto> GetAll(); } }Now add the implementation for our business logic into our 'Application' project.
CleanArchitecture.Sample.Application/Logics:
using System.Collections.Generic; using System.Linq; using CleanArchitecture.Sample.Application.Contracts.Data; using CleanArchitecture.Sample.Application.Contracts.Logics; using CleanArchitecture.Sample.Application.DTOs; namespace CleanArchitecture.Sample.Application.Logics { public class GadgetLogic : IGadgetLogic { private readonly IMyWorldDbContext _myWorldDbContext; public GadgetLogic(IMyWorldDbContext myWorldDbContext) { _myWorldDbContext = myWorldDbContext; } public List<GadgetDto> GetAll() { return _myWorldDbContext.Gadgets .Select(_ => new GadgetDto{ Brand = _.Brand, Cost = _.Cost, Id = _.Id, ProductName = _.ProductName, Type = _.Type }).ToList(); } } }Now register our business logic into the dependency injection services.
ClearArchitecture.Sample.Application/Injection.cs:
service.AddScoped<IGadgetLogic, GadgetLogic>();
Create API Endpoint:
The final step is to create a sample endpoint to see some results.
CleanArchitecture.Sample.Api/Controllers/TestController.cs:
using CleanArchitecture.Sample.Application.Contracts.Logics; using Microsoft.AspNetCore.Mvc; namespace CleanArchitecture.Sample.Api.Controllers { [ApiController] [Route("[controller]")] public class TestController : ControllerBase { private readonly IGadgetLogic _gadgetLogic; public TestController(IGadgetLogic gadgetLogic) { _gadgetLogic = gadgetLogic; } [HttpGet] [Route("all-gadgets")] public IActionResult GetGadgets() { return Ok(_gadgetLogic.GetAll()); } } }That's all about setting up the dotnet application with clean architecture.
Video Session:
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information on Clean Architecture in .Net5 application. I love to have your feedback, suggestions, and better techniques in the comment section below.
one more time thanks!!
ReplyDeleteBuilding a client package for API consumers is a common practice. How do you approach the request objects on a controller that would need to be shared with the client library? Do they go into a separate project or end up in the Domain layer?
ReplyDeleteInitial description i mention 'DTO'(or Vm means viewmodels) will be added in Application project
DeleteUI project depenpends on application project so they consume DTO's
So i believe no need to create seperate project for them since 'DTO' might not be usefull to share with other projects.
Thanks for the reply. Let me explain my scenario a little better. When using Clean Architecture within an API solution there is no UI, instead we have a WebApi layer that exposes the controllers and makes use of the relevant DTOs. To make integration with the API easier, a client library which wraps HttpClient is created and published to a local nuget. The benefit being the consumer simply injects the library and uses service.GetResource(someDto) without knowing any details such as GET or POST or PUT. My question was more so around 'someDto'. In this scenario the DTO is shared between the WebApi and Client.
DeleteOk i agree for this case then we have to create one more project for dtos
ReplyDeleteIf you need to save something to the Gadget-table, do you need to have a repository in between the service and the dbcontext since the interface to the dbcontext can't save to db?
ReplyDelete