In this article, you will see a WhiteApp or QuickApp API solution template which is built on Onion Architecture with all essential features using .NET Core.
Introduction
Whiteapp meaning kick-off project which will have all essential things integrated to it. When we have to start with a new project, then we should think of business. When business requirement is ready, then we have to start focusing the business and we should be ready with basic / essential libraries.
We will see some important libraries order wise, which should be integrated to our project for kick-off.
Essentials Libraries/ Steps to be Ready for ASP.NET Core
- Entity Framework Core
- AutoMapper
- Dependency injection
- Logging-Serilog/ NLog/ StackExchange.Exceptional
- CQRS with MediatR
- Fluent Validations
- Authentication
- Swagger/ OpenAPI
- Error handling
- Unit testing via NUnit
- Integration testing via NUnit
- Versioning
Table of Contents
- What is Onion Architecture
- Difference between Design pattern vs Architecture pattern vs Architecture Style
- Benefits of Onion Architecture
- Disadvantages of Onion Architecture
- Layers of the Onion Architecture
- Getting Started with Onion Architecture
- Step 1: Download and install Visual Studio 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
- Diving inside the code snippets
- What problem does this solution solve?
- How does this help someone else?
- How does the code actually work?
- References
- Conclusion
- History
It is Architecture pattern which is introduced by Jeffrey Palermo in 2008, which will solve problems in maintaining application. In traditional architecture, where we use to implement by Database centeric architecture.
Onion Architecture is based on the inversion of control principle. It's composed of domain concentric architecture where layers interface with each other towards the Domain (Entities/Classes).
Main benefit of Onion architecture is higher flexibility and de-coupling. In this approach, we can see that all the Layers are dependent only on the Domain layer (or sometimes, it called as Core layer).
- An Architectural Style is the application design at the highest level of abstraction
- An Architectural Pattern is a way to implement an Architectural Style
- A Design Pattern is a way to solve a localised problem
For example:
- What you want to implement in your project or requirement like CRUD operation with high level of abstraction is Architecture style
- How are you going to implement it is Architecture pattern
- The problems you will encounter and are going to solve is Design pattern
Onion Architecture is based on Architecture pattern.
- Testability: As it decoupled all layers, so it is easy to write test case for each components
- Adaptability/Enhance: Adding new way to interact with application is very easy
- Sustainability: We can keep all third party libraries in Infrastructure layer and hence maintainence will be easy
- Database Independent: Since database is separated from data access, it is quite easy switch database providers
- Clean code: As business logic is away from presentation layer, it is easy to implement UI (like React, Angular or Blazor)
- Well organised: Project is well organised for better understanding and for onboarding for new joinee to project
- Domain layer will be heavy: Lots of logic will be implemented in Domain layer (sometimes called as Core layer)
- Bunch of layers: For implementing small application like CRUD operation, then too bunch of layers should be implemented
In this approach, we can see that all the Layers are dependent only on the Core Layers.
- Domain layer: Domain Layers (Core layer) is implemented in center and never depends on any other layer. Therefore, what we do is that we create interfaces to Persistence layer and these interfaces get implemented in the external layers. This is also known and DIP or Dependency Inversion Principle.
- Persistence layer: In Persistence layer where we implement reposistory design pattern. In our project, we have implement Entityframework which already implements a repository design pattern.
DbContext
will be UoW (Unit of Work) and each DbSet is the repository. This interacts with our database using dataproviders. - Service layer: Service layer (or also called as Application layer) where we can implement business logic. For OLAP/OLTP process, we can implement CQRS design pattern. In our project, we have implemented CQRS design pattern on top of Mediator design pattern via MediatR libraries.
In case you want to implement email feature logic, we define an IMailService
in the Service Layer.
Using DIP, it is easily possible to switch the implementations. This helps build scalable applications. - Infrastructure Layer: In this layer, we add our third party libraries like JWT Tokens Authentication or Serilog for logging, etc. so that all the third libraries will be in one place. In our project, we have implemented almost all important libraries, you can plug & play (add/remove) based on your project requirement in StartUp.cs file.
- Presentation Layer: This can be WebApi or UI.
You will understand more when we start implementing project below.
Download and install Visual Studio extension from Microsoft marketplace.
Select project type as API, and select Onion Architecture.
Select project type as API, and select Onion Architecture.
Configure connection string based on your SQL Server. In demo project, we are using MS SQL Server.
"ConnectionStrings": {
"OnionArchConn": "Data Source=(local)\\SQLexpress;
Initial Catalog=OnionDb;Integrated Security=True"
},
As we are using EF Core, Code first approach:
For Code First approach (to run this application, use Code First approach):
Update-Database
Swagger UI:
Will look inside all layers
Domain layer code snippets
First will see domain layer, here will create entities based on requirement. We will assume requirement as below diagram. Customer application where Customer with Order 1 to many relationship, similar Supplier with Product 1 to many relationship and OrderDetail table with many to many relationship
Create below entities w.r.t entities file name as shown below. To create 1 to many relationship in Entityframework core, Add List of Orders properties in Customer. Repeat same for Supplier, Category, Product and OrderDetail (Refer source code)
public class BaseEntity
{
[Key]
public int Id { get; set; }
}
public class Customer : BaseEntity
{
public Customer()
{
Orders = new List<Order>();
}
public string CustomerName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public List<Order> Orders { get; set; }
}
public class Order : BaseEntity
{
public Customer Customers { get; set; }
public int CustomerId { get; set; }
public int EmployeeId { get; set; }
public DateTime OrderDate { get; set; }
public DateTime RequiredDate { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
Persistence layer code snippets
In Persistence layer, will create ApplicationDbContext and IApplicationDbContext for separation of concern (or incase of Dapper or any other libraries you can use reposistory design pattern).
Create below IApplicationDbContext and ApplicationDbContext w.r.t file name
public interface IApplicationDbContext
{
DbSet<Category> Categories { get; set; }
DbSet<Customer> Customers { get; set; }
DbSet<Order> Orders { get; set; }
DbSet<Product> Products { get; set; }
DbSet<Supplier> Suppliers { get; set; }
Task<int> SaveChangesAsync();
}
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderDetail>().HasKey(o => new { o.OrderId, o.ProductId });
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseSqlServer("DataSource=app.db");
}
}
public async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
}
}
This layer contains
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
Service layer code snippets
In Service layer, in our code sample will implement CQRS design pattern on top of Mediator design pattern via MediatR libraries. In case for business logic, we can use same layer.
Below code snippets for GetAllCustomerQuery and CreateCustomerCommand w.r.t file name. Instead of writing Mediator design pattern, we can make use MediatR libraries
public class GetAllCustomerQuery : IRequest<IEnumerable<Customer>>
{
public class GetAllCustomerQueryHandler : IRequestHandler<GetAllCustomerQuery, IEnumerable<Customer>>
{
private readonly IApplicationDbContext _context;
public GetAllCustomerQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<IEnumerable<Customer>> Handle(GetAllCustomerQuery request, CancellationToken cancellationToken)
{
var customerList = await _context.Customers.ToListAsync();
if (customerList == null)
{
return null;
}
return customerList.AsReadOnly();
}
}
}
public class CreateCustomerCommand : IRequest<int>
{
public string CustomerName { get; set; }
public string ContactName { get; set; }
public string ContactTitle { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string Region { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Fax { get; set; }
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateCustomerCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateCustomerCommand request, CancellationToken cancellationToken)
{
var customer = new Customer();
customer.CustomerName = request.CustomerName;
customer.ContactName = request.ContactName;
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer.Id;
}
}
}
Similar to CreateCustomerCommand class, create for UpdateCustomerCommand and DeleteCustomerByIdCommand (Refer source code)
Infrastructure layer code snippets
In Infrastructure layer, we can see all third party libraries in this layer. All also implement extension methods for Configure and ServiceConfigure
Below code snippets Extension methods instead of configure in StartUp.cs file, which will make our code clean in presentation layer
Below configure having EntityFrameworkCore, Swagger, Versioning, etc libraries
public static class ConfigureServiceContainer
{
public static void AddDbContext(this IServiceCollection serviceCollection,
IConfiguration configuration, IConfigurationRoot configRoot)
{
serviceCollection.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("OnionArchConn") ?? configRoot["ConnectionStrings:OnionArchConn"])
);
}
public static void AddAddScopedServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddScoped<IApplicationDbContext>(provider => provider.GetService<ApplicationDbContext>());
}
public static void AddTransientServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IMailService, MailService>();
}
public static void AddSwaggerOpenAPI(this IServiceCollection serviceCollection)
{
serviceCollection.AddSwaggerGen(setupAction =>
{
setupAction.SwaggerDoc(
"OpenAPISpecification",
new Microsoft.OpenApi.Models.OpenApiInfo()
{
Title = "Customer API",
Version = "1",
Description = "Through this API you can access customer details",
Contact = new Microsoft.OpenApi.Models.OpenApiContact()
{
Email = "amit.naik8103@gmail.com",
Name = "Amit Naik",
Url = new Uri("https://amitpnk.github.io/")
},
License = new Microsoft.OpenApi.Models.OpenApiLicense()
{
Name = "MIT License",
Url = new Uri("https://opensource.org/licenses/MIT")
}
});
var xmlCommentsFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlCommentsFullPath = Path.Combine(AppContext.BaseDirectory, xmlCommentsFile);
setupAction.IncludeXmlComments(xmlCommentsFullPath);
});
}
public static void AddMailSetting(this IServiceCollection serviceCollection,
IConfiguration configuration)
{
serviceCollection.Configure<MailSettings>(configuration.GetSection("MailSettings"));
}
public static void AddController(this IServiceCollection serviceCollection)
{
serviceCollection.AddControllers().AddNewtonsoftJson();
}
public static void AddVersion(this IServiceCollection serviceCollection)
{
serviceCollection.AddApiVersioning(config =>
{
config.DefaultApiVersion = new ApiVersion(1, 0);
config.AssumeDefaultVersionWhenUnspecified = true;
config.ReportApiVersions = true;
});
}
}
Similar configure to ConfigureContainer.cs file
public static class ConfigureContainer
{
public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<CustomExceptionMiddleware>();
}
public static void ConfigureSwagger(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(setupAction =>
{
setupAction.SwaggerEndpoint("/swagger/OpenAPISpecification/swagger.json", "Onion Architecture API");
setupAction.RoutePrefix = "OpenAPI";
});
}
public static void ConfigureSwagger(this ILoggerFactory loggerFactory)
{
loggerFactory.AddSerilog();
}
}
Presentation layer code snippets
In StartUp.cs file, you can see code is very clean in ConfigureServices and Configure methods
public class Startup
{
private readonly IConfigurationRoot configRoot;
public Startup(IConfiguration configuration)
{
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
Configuration = configuration;
IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json");
configRoot = builder.Build();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddController();
services.AddDbContext(Configuration, configRoot);
services.AddAutoMapper();
services.AddAddScopedServices();
services.AddTransientServices();
services.AddSwaggerOpenAPI();
services.AddMailSetting(Configuration);
services.AddMediatorCQRS();
services.AddVersion();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory log)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.ConfigureCustomExceptionMiddleware();
app.ConfigureSwagger();
log.AddSerilog();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
In CustomerController.cs file, via MediatR libraries we can send or get the data using CQRS pattern
[ApiController]
[Route("api/v{version:apiVersion}/Customer")]
[ApiVersion("1.0")]
public class CustomerController : ControllerBase
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
[HttpPost]
public async Task<IActionResult> Create(CreateCustomerCommand command)
{
return Ok(await Mediator.Send(command));
}
[HttpGet]
[Route("")]
public async Task<IActionResult> GetAll()
{
return Ok(await Mediator.Send(new GetAllCustomerQuery()));
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
return Ok(await Mediator.Send(new GetCustomerByIdQuery { Id = id }));
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
return Ok(await Mediator.Send(new DeleteCustomerByIdCommand { Id = id }));
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, UpdateCustomerCommand command)
{
if (id != command.Id)
{
return BadRequest();
}
return Ok(await Mediator.Send(command));
}
}
White app solution inclues all essential libraries with best practice, which will helps quick start project. Developer can concentrate on Business requirement and build entities. This helps save lots of development time.
Below are some essentials libraries which is already included in project with best practice built on Onion architecture
- Entity Framework Core
- AutoMapper
- Dependency injection
- Logging-Serilog/ NLog/ StackExchange.Exceptional
- CQRS with MediatR
- Fluent Validations
- Authentication
- Swagger/ OpenAPI
- Error handling
- Unit testing via NUnit
- Integration testing via NUnit
- Versioning
This solution helps for developer who can save development time and concentrate on Business module. And if he/she with less experience then it helps to maintain best practices in project (like clean code)
This is project template which is hosted in marketplace.visualstudio.com. Download this extension from marketplace and install in your visual studio. While creating project select this template.
In this article, we have seen what is Onion Architecture and the difference between Design, Architecture pattern and Architecture Style. Project template will help us to quick start application.
- 10th July, 2020: Initial version