In this article, we are going to cover the Onion architecture in ASP.Net 5.0. As we all know, it’s a newly launched framework officially released in the month of November. Here I am sharing the link to install the SDK for .Net 5
What we are going to cover in this .NET 5 Onion Architecture?
What is Onion Architecture
Layers of Onion Architecture
Implementation of Onion Architecture
Pros and Cons
What is Onion Architecture?
A large portion of the customary design raises basic issues of tight coupling and partition of concerns. Onion Architecture was acquainted by Jeffrey Palermo with giving a superior method to construct applications in the context of better testability, practicality, and constancy. Onion Architecture tends to the difficulties confronted with 3-tier and n-tier architectures and gives an answer for normal issues. Onion design layers associate with one another by utilizing the Interfaces.
Layers of Onion Architecture
Basically, it uses the concept of Layers but they are different from 3-tier and N-tier Layers. Let’s deep dive into each of these layers.
Domain Layer
It exists at the center part of the Onion architecture where it consists of all application domain entities which are nothing but database models created by the code first approach. In this project, I have used Fluent API in creating the table schema using Entity Framework
Repository Layer
The repository layer acts as a middle layer between the services and Model objects and in this layer, we will maintain all the Database migrations and application Data context object and in this layer, we typically add interfaces that will consist of data access pattern of reading and write operations involving a database.
Services Layer
This layer is used to communicate between the Repository layer and Main Project where it consists of exposable APIs. The Service layer also could hold business logic for an entity. In this layer, the service interfaces are kept separate from their implementation for loose coupling and also the separation of concerns.
UI Layer
The UI is nothing but a front-end application that will communicate with this API.
Implementation of Onion Architecture
Create a New project
After clicking on the Next button add the project name and solution name and click on create button
Choose the ASP.Net Core 5.0 template in the drop-down and also make sure to check the Enable Open API Support for default Swagger implementation in your project
Default project will be created and now we need 3 empty Class library(.Net Core) projects inside this application as
DomainLayer
RepositoryLayer
ServicesLayer
We will start first with Domain Layer
Domain Layer
Packages used in this Layer
Microsoft.EntityFrameworkCore(5.0.3) Microsoft.EntityFrameworkCore.Relational(5.0.3)
Create a folder named Models and inside that create Customer Class and BaseEntity class where customer class invokes this base entity
BaseEntity.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace DomainLayer.Models
{
public class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public bool IsActive { get; set; }
}
}
Customer.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace DomainLayer.Models
{
public class Customer : BaseEntity
{
public string CustomerName { get; set; }
public string PurchasesProduct { get; set; }
public string PaymentType { get; set; }
}
}
So, now we will create the actual table creation with this customer & base entity class. For that create a separate folder, EntityMapper, where we will maintain all our table schemas inside this folder
CustomerMap.cs
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DomainLayer.EntityMapper
{
public class CustomerMap : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasKey(x => x.Id)
.HasName("pk_customerid");
builder.Property(x => x.Id).ValueGeneratedOnAdd()
.HasColumnName("id")
.HasColumnType("INT");
builder.Property(x => x.PurchasesProduct)
.HasColumnName("purchased_product")
.HasColumnType("NVARCHAR(100)")
.IsRequired();
builder.Property(x => x.PaymentType)
.HasColumnName("payment_type")
.HasColumnType("NVARCHAR(50)")
.IsRequired();
builder.Property(x => x.CreatedDate)
.HasColumnName("created_date")
.HasColumnType("datetime");
builder.Property(x => x.ModifiedDate)
.HasColumnName("modified_date")
.HasColumnType("datetime");
builder.Property(x => x.IsActive)
.HasColumnName("is_active")
.HasColumnType("bit");
}
}
}
Domain Layer Structure
Repository Layer
Packages used in this Layer
Microsoft.EntityFrameworkCore(5.0.3) Microsoft.EntityFrameworkCore.Design(5.0.3) Microsoft.EntityFrameworkCore.SqlServer(5.0.3) Microsoft.EntityFrameworkCore.Tools(5.0.3)
So far we designed our table in the Domain layer now will create the same table using migration commands in SQL DB. let’s create the connection string in our main project.
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"myconn": "server=YOUR Server Name; database=onionarcDb;Trusted_Connection=True;"
}
}
Setup the connection in startup.cs file under the ConfigureMethod
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "OnionArchitecture", Version = "v1" });
});
#region Connection String
services.AddDbContext<ApplicationDbContext>(item => item.UseSqlServer(Configuration.GetConnectionString("myconn")));
#endregion
}
Now switch back to Repository Layer and create a DataContext file where it represents a session with the database and can be used to query and save instances of your entities
ApplicationDbContext.cs
using DomainLayer.EntityMapper;
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
namespace RepositoryLayer
{
public partial class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomerMap());
base.OnModelCreating(modelBuilder);
}
}
}
Let’s create the table in SQL using the migration commands. Open the package manager console and switch the default project to Repositorylayer and execute the below commands one after another.
Commands to execute
Add-Migration ‘CreateCustomerTable’ Update-database
ASP.Net Core is designed in such a way to support dependency injection. Now we create a generic repository interface for the entity operations so that we can see the loosely coupled application. Below is the code snippet
IRepository.cs
using DomainLayer.Models;
using System;
using System.Collections.Generic;
using System.Text;
namespace RepositoryLayer.RespositoryPattern
{
public interface IRepository<T> where T : BaseEntity
{
IEnumerable<T> GetAll();
T Get(int Id);
void Insert(T entity);
void Update(T entity);
void Delete(T entity);
void Remove(T entity);
void SaveChanges();
}
}
Create the repository class to perform the database operations which inherit the IRepository interface.
Repository.cs
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace RepositoryLayer.RespositoryPattern
{
public class Repository<T> : IRepository<T> where T: BaseEntity
{
#region property
private readonly ApplicationDbContext _applicationDbContext;
private DbSet<T> entities;
#endregion
#region Constructor
public Repository(ApplicationDbContext applicationDbContext)
{
_applicationDbContext = applicationDbContext;
entities = _applicationDbContext.Set<T>();
}
#endregion
public void Delete(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
entities.Remove(entity);
_applicationDbContext.SaveChanges();
}
public T Get(int Id)
{
return entities.SingleOrDefault(c => c.Id == Id);
}
public IEnumerable<T> GetAll()
{
return entities.AsEnumerable();
}
public void Insert(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
entities.Add(entity);
_applicationDbContext.SaveChanges();
}
public void Remove(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
entities.Remove(entity);
}
public void SaveChanges()
{
_applicationDbContext.SaveChanges();
}
public void Update(T entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
entities.Update(entity);
_applicationDbContext.SaveChanges();
}
}
}
Repository Layer Structure
Service Layer
This contains the Core Business Logic as part of our project which acts as a layer between the Repositorylayer and Controller.
ICustomerService.cs
using DomainLayer.Models;
using System.Collections.Generic;
namespace ServicesLayer.CustomerService
{
public interface ICustomerService
{
IEnumerable<Customer> GetAllCustomers();
Customer GetCustomer(int id);
void InsertCustomer(Customer customer);
void UpdateCustomer(Customer customer);
void DeleteCustomer(int id);
}
}
CustomerService.cs
using DomainLayer.Models;
using RepositoryLayer.RespositoryPattern;
using System;
using System.Collections.Generic;
using System.Text;
namespace ServicesLayer.CustomerService
{
public class CustomerService : ICustomerService
{
#region Property
private IRepository<Customer> _repository;
#endregion
#region Constructor
public CustomerService(IRepository<Customer> repository)
{
_repository = repository;
}
#endregion
public IEnumerable<Customer> GetAllCustomers()
{
return _repository.GetAll();
}
public Customer GetCustomer(int id)
{
return _repository.Get(id);
}
public void InsertCustomer(Customer customer)
{
_repository.Insert(customer);
}
public void UpdateCustomer(Customer customer)
{
_repository.Update(customer);
}
public void DeleteCustomer(int id)
{
Customer customer = GetCustomer(id);
_repository.Remove(customer);
_repository.SaveChanges();
}
}
}
Configure these services in the startup.cs file
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using RepositoryLayer;
using RepositoryLayer.RespositoryPattern;
using ServicesLayer.CustomerService;
namespace OnionArchitecture
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "OnionArchitecture", Version = "v1" });
});
#region Connection String
services.AddDbContext<ApplicationDbContext>(item => item.UseSqlServer(Configuration.GetConnectionString("myconn")));
#endregion
#region Services Injected
services.AddScoped(typeof(IRepository<>),typeof(Repository<>));
services.AddTransient<ICustomerService, CustomerService>();
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "OnionArchitecture v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Create the API Methods in the customer controller which are exposable to UI (Front end app)
CustomerController.cs
using DomainLayer.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using ServicesLayer;
using ServicesLayer.CustomerService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace OnionArchitecture.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class CustomerController : ControllerBase
{
#region Property
private readonly ICustomerService _customerService;
#endregion
#region Constructor
public CustomerController(ICustomerService customerService)
{
_customerService = customerService;
}
#endregion
[HttpGet(nameof(GetCustomer))]
public IActionResult GetCustomer(int id)
{
var result = _customerService.GetCustomer(id);
if(result is not null)
{
return Ok(result);
}
return BadRequest("No records found");
}
[HttpGet(nameof(GetAllCustomer))]
public IActionResult GetAllCustomer()
{
var result = _customerService.GetAllCustomers();
if (result is not null)
{
return Ok(result);
}
return BadRequest("No records found");
}
[HttpPost(nameof(InsertCustomer))]
public IActionResult InsertCustomer(Customer customer)
{
_customerService.InsertCustomer(customer);
return Ok("Data inserted");
}
[HttpPut(nameof(UpdateCustomer))]
public IActionResult UpdateCustomer(Customer customer)
{
_customerService.UpdateCustomer(customer);
return Ok("Updation done");
}
[HttpDelete(nameof(DeleteCustomer))]
public IActionResult DeleteCustomer(int Id)
{
_customerService.DeleteCustomer(Id);
return Ok("Data Deleted");
}
}
}
Onion Architecture Project Structure
Let’s run and test this application to check the output in swagger or postman.
As I have already inserted one record in the database by using InserCustomer API we will see the data by executing Get ALL Customers API.
Pros and Cons in Onion Architecture
Following are the advantages of actualizing Onion Architecture:
Onion Architecture layers are associated through interfaces. Implantations are given during run time.
Application engineering is based on top of an area model.
All outer reliance, similar to data set admittance and administration calls, are addressed in outside layers.
No conditions of the Internal layer with outer layers.
Couplings are towards the middle.
Adaptable and feasible and convenient design.
No compelling reason to make normal and shared activities.
Can be immediately tried in light of the fact that the application center doesn’t rely upon anything.
A couple of disadvantages of Onion Architecture as follows:
Difficult to comprehend for amateurs, expectation to absorb information included. Modelers generally jumble up parting obligations between layers.
Intensely utilized interfaces
Hope this article helps you in a clear understanding of Onion Architecture. Please find the Source Code here
…. Keep Learning !!!