In this article, we will learn more about the onion architecture and its advantages. We're building a RESTful API that follows the onion architecture, using ASP.NET Core and .NET 5.
The onion architecture is also commonly referred to as "Clean Architecture" or "Ports and Adapters". These architectural approaches are just variations on the same theme.
We have prepared a project that follows the onion architecture that we will use in the rest of the article.To download it you can visit ourOnion architecture in ASP.NET CoreRepository.
Let us begin!
What is onion architecture?
Onion architecture is a form of layered architecture and we can visualize these layers as concentric circles. Hence the architecture name Onion. Onion architecture was first introduced by Jeffrey Palermo to overcome the problems of the traditional N-tier architecture approach.
There are several ways to split the onion, but we choose the following approach where we split the architecture into 4 layers:
- domain layer
- duty shift
- infrastructure layer
- presentation slide
Conceptually, we can consider the infrastructure and the presentation layer to be on the same hierarchical level.
Now let's go ahead and take a closer look at each layer to see why we're introducing them and what we're going to create within that layer:
Advantages of onion architecture
Let's take a look at what are the advantages of the onion architecture and why we want to implement it in our projects.
All layers strictly interact through the interfaces defined in the subsequent layers. The dependency flow goes to the onion core. We explain why this is important in the next section.
Using dependency inversion throughout the project, depending on abstractions (interfaces) instead of implementations, allows us to transparently change the implementation at runtime. We rely on compile-time abstractions that give us strict working contracts, and we get the implementation at run-time.
Testability is very high with the onion architecture since everything is based on abstractions. Abstractions can easily be mocked with a mock library likemoq🇧🇷 For more information on unit testing your projects in ASP.NET Core, see this articleTest MVC controllers in ASP.NET Core.
We can write business logic without worrying about implementation details. If we need something from an external system or service, we can simply interact with it and use it. We don't have to worry about how this is implemented. The upper layers of Onion transparently take care of the implementation of this interface.
dependency flow
The main idea behind onion architecture is dependency flow, or rather how layers interact with each other. The deeper the layer is in the onion, the fewer dependencies it has.
The domain layer has no direct dependency on the external layers. In a way, it is isolated from the outside world. All outer layers can reference layers directly below in the hierarchy.
We can conclude that all dependencies flow inwards in the onion architecture. But we have to ask ourselves, why is this important?
Dependency flow determines what a particular layer in the onion architecture can do. Because it depends on the levels below in the hierarchy, it can only call methods exposed by levels below.
We can use lower layers of onion architecture to define contracts or interfaces🇧🇷 The outer layers of the architecture implement these interfaces. This means that at the domain level we don't worry about infrastructure details like the database or external services.
With this approach, we can encapsulate all the rich business logic in the domain and service layers without having to know implementation details. In the service layer, we only rely on the interfaces defined by the underlying layer, the domain layer.
Enough theory, let's look at some code. We have already prepared a working project for you and we will look at each of the projects in the solution and talk about how they fit into the onion architecture.
solution structure
Let's take a look at the structure of the solution we're going to use:
As we can see, it doesn't existThe network
Project, which is our ASP.NET Core app, and six class libraries. EITHERDomain
The project will keep the domain level implementation. EITHERServices
miServices.Abstractions
will be our implementation of the service layer. EITHERpersistence
Project will be our infrastructure layer, and thepresentation
Project will be the implementation of the presentation layer.
domain layer
The domain layer is the heart of the onion architecture. In this layer we usually define the main aspects of our domain:
- entities
- Repository Interfaces
- exceptions
- domain services
These are just some examples of what we could define in the domain layer. Depending on your needs, we can be more or less strict. We have to realize that everything is a compromise in software development.
Let's start by looking at entity classes.owner
miinvoice
, underentities
Pasta:
public class owner { public guid id { get; define; } public string name { get; define; } public DateTime DateOfBirth { get; define; } public string Straighten { get; define; } public ICollection<Account> Accounts { get; define; }}
public class account { public guid id { get; define; } public DateTime DateCreated { get; define; } public string account type { get; define; } public guid ownerid { get; define; }}
The entities defined at the domain level capture the important information describing the problem domain.
At this point we have to ask what happens to the behavior? Isn't an anemic domain model bad?
It depends. If you have very complex business logic, it would make sense to encapsulate it in our domain entities. But for most applications, it's usually easier to start with a simpler domain model and only introduce complexity when the design calls for it.
Next we will see thoseIOwnerRepository
miIAccountRepository
Interfaces within the renderlocations
Pasta:
public interface IOwnerRepository{ Task<IEnumerable<Owner>> GetAllAsync(CancellationToken cancelrationToken = default); Task<Owner> GetByIdAsync(Guid Owner ID, CancellationToken cancellationToken = default); insert void(owner owner); void Delete(owner owner);}
public interface IAccountRepository{ Task<IEnumerable<Cuenta>> GetAllByOwnerIdAsync(Guid OwnerId, CancellationToken cancelToken = default); Task<Account> GetByIdAsync(Guid accountId, CancellationToken cancellationToken = default); void Insert(Account Account); Void Eliminator(account account);}
For more information on the repository pattern and asynchronous implementation, seeImplementing an asynchronous generic repository in ASP.NET Core.
In the same folder we also find theIWorkUnit
Interface:
public interface IUnitOfWork{ Task<int> SaveChangesAsync(CancellationToken cancelToken = default);}
Notice that we define thecancellation token
argument as an optional value and give it themodel
Value. With this approach, if we don't have acancellation token
Values aCancellationToken.Ninguno
are made available to us. This way we can ensure that our asynchronous calls making thecancellation token
it will always work.
Domain Exceptions
Now let's look at some of the custom exceptions we have in theexceptions
Pasta.
there is a summaryBadRequestException
Class:
public abstract class BadRequestException: exception { protected BadRequestException(string message): base(message) {}}
and the summaryNotFoundException
Class:
public abstract class NotFoundException: exception { protected NotFoundException(string message): base(message) {}}
There are also some exception classes that inherit from abstract exceptions to describe specific scenarios that may occur in your application:
closed public class AccountDoesNotBelongToOwnerException: BadRequestException{ public AccountDoesNotBelongToOwnerException(Guid OwnerId, Guid accountId): base($"Account with id {accountId} does not belong to owner with id {ownerId}") { }}
public closed class OwnerNotFoundException : NotFoundException{ public OwnerNotFoundException(Guid OwnerId): base($"Could not find owner with id {ownerId}") { }}
public closed class AccountNotFoundException: NotFoundException{ public AccountNotFoundException(Guid accountId): base($"The account with id {accountId} was not found.") { }}
These exceptions are handled by the upper layers of our architecture. We use them in a global exception handler that returns the appropriate HTTP status code based on the type of exception thrown.
If you're interested in learning more about how to implement global exception handling, be sure to stop byGlobal error handling in ASP.NET Core Web API.
At this point we know how to define the domain layer. With that said, let's move on to the service layer and see how to use it to implement real business logic.
duty shift
The service layer is directly above the domain layer, which means it has a reference to the domain layer. The service layer is divided into two projects,Services.Abstractions
miServices
.
noServices.Abstractions
project contains the definitions of the service interfaces that encapsulate the core business logic. Also, we use thecontracts
Project to define the data transfer objects (DTOs) that we will use with the service interfaces.
Let's look at those firstIOwnerService
miIAccountService
Interfaces:
öffentliche Schnittstelle IOwnerService{ Task<IEnumerable<OwnerDto>> GetAllAsync(CancellationToken cancelrationToken = default); Task<OwnerDto> GetByIdAsync(Guid Owner ID, CancellationToken cancellationToken = default); Task<OwnerDto> CreateAsync(OwnerForCreationDto ownForCreationDto, CancellationToken cancellationToken = default); UpdateAsync-Aufgabe (Guid-Besitzer-ID, OwnerForUpdateDto ownForUpdateDto, CancellationToken cancellationToken = default); Aufgabe DeleteAsync(Guid Owner ID, CancellationToken cancellationToken = default);}
public interface IAccountService{ Task<IEnumerable<AccountDto>> GetAllByOwnerIdAsync(Guid OwnerId, CancellationToken cancellationToken = padrão); Task<AccountDto> GetByIdAsync(Guid OwnerId, Guid accountId, CancellationToken cancellationToken); Task<AccountDto> CreateAsync(ID of the owner of Guid, AccountForCreationDto accountForCreationDto, CancellationToken cancelationToken = padrão); Tarefa DeleteAsync(ID of the owner of Guid, ID of the key of Guid, CancellationToken cancelationToken = predeterminado);}
Also, we can see that there is oneIServiceManager
Interface that acts as a wrapper around the two interfaces we created earlier:
public interface IServiceManager{ IOwnerService OwnerService { get; } IAccountService AccountService { get; }}
Next we will see how these interfaces are implemented withinServices
Project.
let's start with thatowner service
:
Interne Klasseneinstellung OwnerService : IOwnerService{ private readonly IRepositoryManager _repositoryManager; public OwnerService(IRepositoryManager repositoryManager) => _repositoryManager = repositoryManager; Tarefa assíncrona pública<IEnumerable<OwnerDto>> GetAllAsync(CancellationToken cancelToken = default) { var Owners = await _repositoryManager.OwnerRepository.GetAllAsync(cancellationToken); var proprietáriosDto = proprietários.Adapt<IEnumerable<OwnerDto>>(); devolver proprietáriosDto; } tarea asincrónica public<OwnerDto> GetByIdAsync(ID de propietario, CancellationToken cancelationToken = predeterminado) { var propietario = esperar _repositoryManager.OwnerRepository.GetByIdAsync(ownerId, cancelationToken); if (el propietario is null) { throw new OwnerNotFoundException(ownerId); } var propietarioDto = propietario.Adapt<OwnerDto>(); devolver proprietárioDto; } public async Task<OwnerDto> CreateAsync(OwnerForCreationDto ownForCreationDto, CancellationToken CancelationToken = Predeterminado) { var propietario = propietarioForCreationDto.Adapt<Owner>(); _repositoryManager.OwnerRepository.Insert (Eigentum); esperar _repositoryManager.UnitOfWork.SaveChangesAsync(cancellationToken); volver propietario.Adapt<OwnerDto>(); } public async Tarefa UpdateAsync(ID propietario, OwnerForUpdateDto propietarioForUpdateDto, CancellationToken cancelToken = Predeterminado) { var propietario = await _repositoryManager.OwnerRepository.GetByIdAsync(ownerId, cancelToken); if (el propietario is null) { throw new OwnerNotFoundException(ownerId); } propietario.Nombre = propietarioParaActualizarDto.Nombre; propietario.FechaDeNacimiento = propietarioParaActualizarDto.FechaDeNacimiento; propietario.Dirección = propietarioParaActualizarDto.Dirección; esperar _repositoryManager.UnitOfWork.SaveChangesAsync(cancellationToken); } Tarefa assíncrona public DeleteAsync(Guid ownId, CancellationToken cancelToken = default) { var own = await _repositoryManager.OwnerRepository.GetByIdAsync(ownerId, cancelToken); if (el propietario is null) { throw new OwnerNotFoundException(ownerId); } _repositoryManager.OwnerRepository.Remove(propietario); esperar _repositoryManager.UnitOfWork.SaveChangesAsync(cancellationToken); }}
So let's examine those Account service
Class:
sealed inner class AccountService : IAccountService{ private readonly IRepositoryManager _repositoryManager; public AccountService(IRepositoryManager repositoryManager) => _repositoryManager = repositoryManager; Public Async Task<IEnumerable<AccountDto>> GetAllByOwnerIdAsync(OwnerId, CancellationToken CancellationToken = Default) { var accounts = expected _repositoryManager.AccountRepository.GetAllByOwnerIdAsync(OwnerId, CancellationToken); var cuentasDto = cuentas.Adapt<IEnumerable<CuentaDto>>(); return container to; } public asynchronous task <AccountDto> GetByIdAsync(ID OwnerId, Guid AccountId, CancellationToken CancelToken) { var Owner = await _repositoryManager.OwnerRepository.GetByIdAsync(ownerId, CancelToken); if (owner is null) { throw new OwnerNotFoundException(ownerId); } var account = wait _repositoryManager.AccountRepository.GetByIdAsync(accountId, cancelToken); if (to account is null) { throw new AccountNotFoundException(accountId); } if (account.OwnerId != owner.Id) { throw new AccountDoesNotBelongToOwnerException(owner.Id, account.Id); } var contaDto = count.Adapt<ContaDto>(); back contact; } public async Task<AccountDto> CreateAsync(ownerID, AccountForCreationDto accountForCreationDto, CancellationToken CancellationToken = pattern) { var owner = wait _repositoryManager.OwnerRepository.GetByIdAsync(ownerId, cancellingToken); if (owner is null) { throw new OwnerNotFoundException(ownerId); } var account = accountParaCreaciónDto.Adapt<Account>(); account.IdOwner = owner.Id; _repositoryManager.AccountRepository.Insert(Account); await _repositoryManager.UnitOfWork.SaveChangesAsync(cancellationToken); Return number.Adapt<CuentaDto>(); } public async Tarefa DeleteAsync(ownerid, guidaccountid, CancellationToken CancelToken = default) { var owner = await _repositoryManager.OwnerRepository.GetByIdAsync(ownerid, CancelToken); if (owner is null) { throw new OwnerNotFoundException(ownerId); } var account = wait _repositoryManager.AccountRepository.GetByIdAsync(accountId, cancelToken); if (to account is null) { throw new AccountNotFoundException(accountId); } if (account.OwnerId != owner.Id) { throw new AccountDoesNotBelongToOwnerException(owner.Id, account.Id); } _repositoryManager.AccountRepository.Remove(Account); await _repositoryManager.UnitOfWork.SaveChangesAsync(cancellationToken); }}
and finally theSupervisor
:
Select the ServiceManager public class: IServiceManager{ private readonly Lazy<IOwnerService> _lazyOwnerService; privado solo lectura Lazy<IAccountService> _lazyAccountService; public ServiceManager(IRepositoryManager repositoryManager) { _lazyOwnerService = new Lazy<IOwnerService>(() => new OwnerService(repositoryManager)); _lazyAccountService = new Lazy<IAccountService>(() => new AccountService(repositoryManager)); } public IOwnerService OwnerService => _lazyOwnerService.Value; public IAccountService AccountService => _lazyAccountService.Value;}
The interesting part with theSupervisor
Implementation is that we use the power ofLazy
Class to ensure delayed initialization of our services. This means that our service instances are only created when we access them for the first time, not before.
What is the motivation for splitting the duty shift?
Why do we have so many problems with splitting our service interfaces and implementations into two separate projects?
As you can see, we mark the service implementations with theintern
keyword, meaning they are not publicly available outside ofServices
Project. On the other hand, service interfaces are public.
Remember what we said about dependency flow?
With this approach, we make it very clear what the top onion layers can and can't do. It is easy to overlook here that theServices.Abstractions
Project has no reference to theDomain
Project.
That is, if a higher layer references theServices.Abstractions
project can only call methods provided by this project. We'll see later why this is very useful when we get to the presentation layer.
infrastructure layer
The infrastructure layer has to take care of encapsulating everything related to the external systems or services that our application interacts with. These external services can be:
- Database
- Identity Provider
- message queue
- email service
There are more examples, but I hope you get the point. We hide all implementation details in infrastructure layer as it sits on top of onion architecture while all lower layers depend on interfaces (abstractions).
First, let's look at the Entity Framework database context in theRepositoryDbConextNameRepositoryDbConextName
Class:
public closed class RepositoryDbContext : DbContext{ public RepositoryDbContext(options DbContextOptions): base(options) { } public DbSet<Owner> Owners { get; define; } public accounts DbSet<Account> { get; define; } Protected Override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.ApplyConfigurationsFromAssembly(typeof(RepositoryDbContext).Assembly);}
As you can see, the implementation is extremely simple. However insideOnModelCreating
method, we set our database context based on the entity setting of the same assembly.
Next, let's look at entity configurations that implement theIEntityTypeConfiguration<T>
Interface. We can find them insidethe settings
Pasta:
Sealed inner class OwnerConfiguration: IEntityTypeConfiguration<Owner>{ public void Configure(EntityTypeBuilder<Owner> builder) { builder.ToTable(nameof(Owner)); constructor.HasKey(owner => owner.Id); builder.Property(account => account.Id).ValueGeneratedOnAdd(); constructor.Property(owner => owner.name).HasMaxLength(60); constructor.Property(owner => owner.DateOfBirth).IsRequired(); constructor.Property(owner => owner. address). HasMaxLength(100); builder.HasMany(owner => owner.Accounts) .WithOne() .HasForeignKey(account => account.OwnerId) .OnDelete(DeleteBehavior.Cascade); 🇧🇷
AccountConfiguration der versiegelten inneren Klasse: IEntityTypeConfiguration<Account>{ public void Configure(EntityTypeBuilder<Account> builder) { builder.ToTable(nameof(Account)); constructor.HasKey(account => account.Id); builder.Property(account => account.Id).ValueGeneratedOnAdd(); constructor.Property(account => account.TipoCuenta).HasMaxLength(50); constructor.Property(account => account.Creation Date).IsRequired(); }}
Great, now that the database context is set up, we can move on to the repositories.
Let's look at the repository implementations within thelocations
Binder. The repositories implement the interfaces we defined inDomain
Project:
sealed inner class OwnerRepository: IOwnerRepository{ private readonly RepositoryDbContext _dbContext; Public OwnerRepository (RepositoryDbContext dbContext) => _dbContext = dbContext; public asynchronous task<IEnumerable<Owner>> GetAllAsync(CancellationToken cancelToken = default) => expected _dbContext.Owners.Include(x => x.Accounts).ToListAsync(cancellationToken); public async task <Owner> GetByIdAsync(OwnerId, CancellationToken CancellationToken = Default) => expected _dbContext.Owners.Include(x => x.Accounts).FirstOrDefaultAsync(x => x.Id == OwnerId, CancellationToken); public void Insert(Owners owners) => _dbContext.Owners.Add(Owners); public void Remove(Owners owners) => _dbContext.Owners.Remove(Owners);}
Interne Klasseneinstellung AccountRepository : IAccountRepository{ private readonly RepositoryDbContext _dbContext; public AccountRepository (RepositoryDbContext dbContext) => _dbContext = dbContext; tarea asincrónica pública<IEnumerable<Cuenta>> GetAllByOwnerIdAsync(ID de propietario, CancellationToken cancelacionToken = predeterminado) => espera _dbContext.Accounts.Where(x => x.OwnerId == OwnerId).ToListAsync(cancellationToken); public async Task<Account> GetByIdAsync(Guid accountId, CancellationToken cancelToken = padrão) => await _dbContext.Accounts.FirstOrDefaultAsync(x => x.Id == accountId, cancelToken); public void Insertar(Cuenta cuenta) => _dbContext.Accounts.Add(cuenta); public void Remove(Conta) => _dbContext.Accounts.Remove(conta);}
For information on implementing a repository pattern using Entity Framework Core, see this articleASP.NET Core-Web-API – Repositorymuster.
Great, we're done with the infrastructure layer. Now we have just one more layer to complete our onion architecture implementation.
presentation slide
The purpose of the presentation layer is to present the entry point of our system so that consumers can interact with the data. We can implement this layer in many ways, for example by creating a REST API, gRPC, etc.
We use a web API built with ASP.NET Core to create a set of RESTful API endpoints to modify domain entities and allow consumers to retrieve data.
However, we're going to do something different than what you're typically used to when building web APIs. By convention, controllers are defined inController
Folders in the web application.
Why is this a problem? Since ASP.NET Core uses dependency injection everywhere, we need to have a reference to all projects in the web application project solution. This allows us to configure our services within thebeginning
Class.
While this is exactly what we want to do, it has one major design flaw. What's stopping our controllers from putting whatever they want in the constructor? anything!
clean drivers
With ASP.NET Core's default approach, we can't stop anyone from putting what they need in a controller. So how can we enforce some stricter rules about what controllers can do?
Remember how we divided the service layer into theServices.Abstractions
miServices
projects? That was part of the puzzle.
We create a project calledpresentation
and give you a hint to theMicrosoft.AspNetCore.Mvc.Core
NuGet package so you have access to itbase controller
Class. This is how we can create our controllers in this project.
Let's seeOwnersController
within the projectController
Pasta:
[ApiController][Route("api/propietarios")]Public class OwnersController : ControllerBase{ private readonly IServiceManager _serviceManager; public OwnersController (IServiceManager serviceManager) => _serviceManager = serviceManager; [HttpGet] tarea asincrónica public <IActionResult> GetOwners (CancellationToken cancelToken) { var propietarios = esperar _serviceManager.OwnerService.GetAllAsync (cancellationToken); zurück Ok (propietarios); } [HttpGet("{ownerId:guid}")] tarea asincrónica public<IActionResult> GetOwnerById(Guid ownId, CancellationToken cancelationToken) { var ownDto = await _serviceManager.OwnerService.GetByIdAsync(ownerId, cancelationToken); Rückgabe Ok (propietarioDto); } [HttpPost] public async Task<IActionResult> CreateOwner([FromBody] OwnerForCreationDto ownForCreationDto) { var ownDto = await _serviceManager.OwnerService.CreateAsync(ownerForCreationDto); return CreatedAtAction(nameof(GetOwnerById), new { ownId = ownDto.Id }, ownDto); } [HttpPut("{ownerId:guid}")] tarea asincrónica public<IActionResult> UpdateOwner(Guid ownId, [FromBody] OwnerForUpdateDto ownForUpdateDto, CancellationToken cancelToken) { await _serviceManager.OwnerService.UpdateAsync(ownerId, ownForUpdateDto, cancelToken); return SemConteudo(); aufrechtzuerhalten. return SemConteudo(); }}
And let's also take a look atAccount-Controller
:
[ApiController][Route("api/propietarios/{ownerId:guid}/cuentas")]öffentliche Klasse AccountsController : ControllerBase{ private readonly IServiceManager _serviceManager; public AccountsController (IServiceManager serviceManager) => _serviceManager = serviceManager; [HttpGet] tarea asincrónica public <IActionResult> GetAccounts(ID de propietario, CancellationToken cancelToken) { var accountsDto = await _serviceManager.AccountService.GetAllByOwnerIdAsync(ownerId, cancelationToken); Rückgabe Ok (contasDto); aufrechtzuerhalten. Rückgabe Ok (contaDto); } [HttpPost] tarea asincrónica pública <IActionResult> CreateAccount (ID de propietario de Guid, [FromBody] AccountForCreationDto accountForCreationDto, CancellationToken cancelationToken) { var respuesta = esperar _serviceManager.AccountService.CreateAsync(ownerId, accountForCreationDto, cancellingToken); return CreatedAtAction(nameof(GetAccountById), novo { OwnerId = respuesta.OwnerId, accountId = respuesta.Id }, respuesta); } [HttpDelete("{accountId:guid}")] public async Task<IActionResult> DeleteAccount(Property ID, Guid de cuenta, CancellationToken cancelationToken) { await _serviceManager.AccountService.DeleteAsync(ownerId, accountId, cancelToken); return SemConteudo(); }}
By now it should be clear that thepresentation
project will only have a reference to theServices.Abstraction
Project. and from theServices.Abstractions
project does not refer to any other project, we have enforced a very strict set of methods that we can call in our controllers.
The obvious benefit of the onion architecture is that our controller methods become very lightweight. Just a few lines of code at most. This is the true beauty of onion architecture. We moved all of the important business logic to the service layer.
Great, we've seen how the presentation layer is implemented.
But how are we going to use the controller if it doesn't exist in the web application? Well, let's move on to the next section to find out.
build onion
Congratulations if you've made it this far.We'll show you how to implement the domain layer, the service layer, and the infrastructure layer. In addition, we show the implementation of the presentation layer, which decouples the controllers from the main web application.
Only one small problem remains. The app doesn't work at all! We haven't seen how to connect any of our dependencies.
Configuration of Services
Let's see how to register all required service dependencies within thebeginning
Class for .NET 5 in The network
Project. Let's take a lookconfigure services
Method:
public void ConfigureServices(IServiceCollection services){ services.AddControllers() .AddApplicationPart(typeof(Presentation.AssemblyReference).Assembly); services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Titel = "Web", Version = "v1" })); services.AddScoped<IServiceManager, ServiceManager>(); services.AddScoped<IRepositoryManager, RepositoryManager>(); services.AddDbContextPool<RepositoryDbContext>(builder => { var connectionString = Configuration.GetConnectionString("Database"); builder.UseNpgsql(connectionString); }); services.AddTransient<ExceptionHandlingMiddleware>();}
For .NET 6 we would add slightly modified code to the Program class:
builder.Services.AddControllers() .AddApplicationPart(typeof(Presentation.AssemblyReference).Assembly); builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Titel = "Web", Version = "v1" })); builder.Services.AddScoped<IServiceManager, ServiceManager>(); builder.Services.AddScoped<IRepositoryManager, RepositoryManager>(); builder.Services.AddDbContextPool<RepositoryDbContext>(builder =>{ var connectionString = Configuration.GetConnectionString("Database"); builder.UseNpgsql(connectionString);});builder.Services.AddTransient<ExceptionHandlingMiddleware>();
The most important part of the code is:
builder.Services.AddControllers() .AddApplicationPart(typeof(Presentation.AssemblyReference).Assembly);
Without this line of code, the Web API wouldn't work. This line of code finds all controllers within thepresentation
project and configure them with the framework. They are treated the same as if they were conventionally defined.
Great, we've seen how to connect all of our application's dependencies. However, there are still a few things to consider.
Creating a global exception handler
Remember that we have two abstract exception classesBadRequestException
miNotFoundException
within the domain layer? Let's see how we can put them to good use.
Let's seethere global Exception-Handler Exception Handling Middleware
class found in theMiddleware
Pasta:
Interne Klasse von ExceptionHandlingMiddleware: IMiddleware{ private readonly ILogger<ExceptionHandlingMiddleware> _logger; public ExceptionHandlingMiddleware(ILogger<ExceptionHandlingMiddleware> Registrator) => _logger = Registrator; Öffentliche asynchrone Aufgabe InvokeAsync (HttpContext-Kontext, RequestDelegate next) { try { await next (context); } catch (Ausnahme e) { _logger.LogError(e, e.Message); aguarde HandleExceptionAsync(contexto, e); } } tarefa assíncrona estática privada HandleExceptionAsync(HttpContext httpContext, excepción de excepción) { httpContext.Response.ContentType = "application/json"; httpContext.Response.StatusCode = Ausnahmeoption { BadRequestException => StatusCodes.Status400BadRequest, NotFoundException => StatusCodes.Status404NotFound, _ => StatusCodes.Status500InternalServerError }; var respuesta = novo { erro = exceção.Mensagem }; aguarde httpContext.Response.WriteAsync(JsonSerializer.Serialize(reposta)); }}
Notice that we create a switch expression around the exception instance and then do a pattern match based on the exception type. Next, let's change the HTTP status code of the response based on the specific exception type.
For more information on the switch expression and other useful C# features, seeC# tips to improve code quality and performance.
So we have to record thatException Handling Middleware
with the ASP.NET Core middleware pipeline to make this work properly:
...app.UseMiddleware<ExceptionHandlingMiddleware>();...
We also need to register our middleware implementation within theConfigureService
method ofbeginning
Class:
services.AddTransient<ExceptionHandlingMiddleware>();
Via a .NET 6:
builder.Services.AddTransient<ExceptionHandlingMiddleware>();
without registering theException Handling Middleware
With the dependency container we would get a runtime exception and we don't want that to happen.
Management of database migrations
Let's look at one final design tweak that will make it easier for everyone to use, and then we're done.
To make it easier to download the application code and run the application locally, we use Docker. WithStauer
We'll package our ASP.NET Core app in a Docker container. we also useComposition of the Docker
to bundle our web application container with a container running the PostgreSQL database image. This way we don't have to have PostgreSQL installed on our system.
However, since both the web app and the database server run in containers, how do we then create the actual database for the app to use?
We could create a startup script, connect to the Docker container while the database server is running, and run the script. However, this is a lot of manual work and error-prone. Luckily there is a better way.
To do this elegantly, we use the Entity Framework Core migrations and run the migrations from our code when the app starts. To see how we achieved this, take a look atprogram
not classroomThe network
Project:
Public class program { Public static async Task Main(cadena[] args) { var webHost = CreateHostBuilder(args). To build (); Protection ApplyMigrations(webHost.Services); aguarde webHost.RunAsync(); } tarefa assíncrona estática privada ApplyMigrations(IServiceProvider serviceProvider) { using var scope = serviceProvider.CreateScope(); aguarde usando RepositoryDbContext dbContext = scope.ServiceProvider.GetRequiredService<RepositoryDbContext>(); aguarde dbContext.Database.MigrateAsync(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());}
The best thing about this approach is that if we later create new migrations, the migrations will be applied automatically. We don't have to think about moving on. learn more about itMigrations and data seeding with EF Coreand .NET 5 and .NET 6look at this articleData migrations and seeding with Entity Framework Core.
Running the application
Amazing work! We have connected all of our onion architecture implementation layers together and our application is now ready to go.
We can launch the application by clicking on the buttonComposition of the Docker
Visual learning button. make sure, thatdocker-compose
project is set to your startup project. This will automatically start the containers for the web application and database server for us:
Then we can open the browser in thehttps://localhost:5001/arrogancia
Address where we can find themarrogance
User interface:
Here we can test our API endpoints and verify that everything is working correctly.
Conclusion
In this article, we learned about the onion architecture. We explain our vision of the architecture by dividing it into domain, service, infrastructure and presentation layers.
We started at the domain level, where we looked at our entity definitions and repository interfaces and exceptions.
Next, we saw how the service layer was created, where we encapsulate our business logic.
Next we look at the infrastructure layer, where the implementations of the repository interfaces reside, as well as the EF database context.
And finally, we saw how our presentation layer is implemented as a separate project by decoupling the controllers from the main web application.Then we'll explain how we can connect all layers together using an ASP.NET Core Web API.
Until the next article,
My best wishes.
FAQs
What is onion architecture in asp net core? ›
The Onion architecture is a form of layered architecture and we can visualize these layers as concentric circles. Hence the name Onion architecture. The Onion architecture was first introduced by Jeffrey Palermo, to overcome the issues of the traditional N-layered architecture approach.
Is clean architecture same as onion architecture? ›Clean architecture
The core concepts are similar to Onion Architecture, but it has a slightly different terminology. Here, the domain model is referred to as an “entity”. Entity contains business-specific rules and logic, while the application operation specific logic sits in the use case.
Onion Architecture is comprised of multiple concentric layers interfacing with each other towards the core that represents the domain. It is based on the inversion of control principle. The architecture does not focus on underlying technology or frameworks but the actual domain models.
How do I create an onion architecture in .NET core? ›- Step 1: Download extension from project template. ...
- Step 2: Create Project. ...
- Step 3: Select Onion Architecture project template. ...
- Step 4: Project is ready.
- Step 5: Configure connection string in appsettings.json. ...
- Step 6: Create Database (Sample is for Microsoft SQL Server) ...
- Step 7: Build and run application.
Onion is an architectural pattern for a system, whereas DDD is a way to design a subset of the objects in the system. The two can exist without eachother, so neither is a subset of the other. If you were to use them together - then as a whole the part that is designed using DDD would be a subset of the entire system.
What are the three main types of architecture? ›There are three systems of architecture, known as orders, the Doric, the Ionic and the Corinthian, the later being a variation of the Ionic, differing only in the form of the capital.
What are the different 3-tier architecture? ›Three-tier architecture is a well-established software application architecture that organizes applications into three logical and physical computing tiers: the presentation tier, or user interface; the application tier, where data is processed; and the data tier, where the data associated with the application is ...
What is the onion model used for? ›The onion model in computing is used as a metaphor for the complex structure of information systems. The system is split into layers to make it easier to understand. A simple example is to start with the program, operating system and hardware layers. Each of these layers can then be subdivided.
Is MVC an onion architecture? ›The main aim of Onion architecture was to make applications loosely coupled and create proper separation of concerns in an application. It makes development, testing, and maintenance very easy. Many traditional architectures exist in the world of web.
What is a 3 tier architecture in ASP NET? ›Three-layer architecture is dividing the project into three layers that are User interface layer, business layer and data(database) layer where we separate UI, logic, and data in three divisions.
What is the onion principle? ›
The onion principle is an analogy to help explain how small unresolved issues or dysfunction can become more difficult to fix later down the track. It's an analogy to help explain why it can take longer to fix a longstanding issue. The picture of an onion lends itself to having layers.