Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

Whiteapp ASP.NET Core using Onion Architecture

5.00/5 (14 votes)
19 Jul 2020MIT7 min read 50K  
WhiteApp or QuickApp API solution template built on Onion Architecture
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

  1. Entity Framework Core
  2. AutoMapper
  3. Dependency injection
  4. Logging-Serilog/ NLog/ StackExchange.Exceptional
  5. CQRS with MediatR
  6. Fluent Validations
  7. Authentication
  8. Swagger/ OpenAPI
  9. Error handling
  10. Unit testing via NUnit
  11. Integration testing via NUnit
  12. Versioning

Table of Contents

  1. What is Onion Architecture
  2. Difference between Design pattern vs Architecture pattern vs Architecture Style
  3. Benefits of Onion Architecture
  4. Disadvantages of Onion Architecture
  5. Layers of the Onion Architecture
  6. Getting Started with Onion Architecture
    1. Step 1: Download and install Visual Studio extension from project template
    2. Step 2: Create Project
    3. Step 3: Select Onion Architecture project template
    4. Step 4: Project is ready
    5. Step 5: Configure connection string in appsettings.json
    6. Step 6: Create Database (Sample is for Microsoft SQL Server)
    7. Step 7: Build and run application
  7. Diving inside the code snippets
  8. What problem does this solution solve?
  9. How does this help someone else?
  10. How does the code actually work?
  11. References
  12. Conclusion
  13. History

What is Onion Architecture

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.

Image 1

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

Image 2

Difference between Design Pattern vs Architecture Pattern vs Architecture Style

  • 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

Image 3

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.

Benefits of Onion Architecture

  • 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

Disadvantages of Onion Architecture

  • 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

Layers of the Onion Architecture

In this approach, we can see that all the Layers are dependent only on the Core Layers.

Image 4

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

Getting Started with Onion Architecture

Step 1: Download and Install Visual Studio Extension from Project Template

Download and install Visual Studio extension from Microsoft marketplace.

Step 2: Create Project

Select project type as API, and select Onion Architecture.

Image 5

Step 3: Select Onion Architecture Project Template

Select project type as API, and select Onion Architecture.

Image 6

Step 4: Project Is Ready

Image 7

Step 5: Configure Connection String in appsettings.json

Configure connection string based on your SQL Server. In demo project, we are using MS SQL Server.

JSON
"ConnectionStrings": {
      "OnionArchConn": "Data Source=(local)\\SQLexpress;
       Initial Catalog=OnionDb;Integrated Security=True"
      },

Step 6: Create Database (Sample is for Microsoft SQL Server)

As we are using EF Core, Code first approach:

Image 8

For Code First approach (to run this application, use Code First approach):

Update-Database

Step 7: Build and Run Application

Swagger UI:

Image 9

Diving inside the code snippets

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

Image 10

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)

C#
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

C#
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
{
    // This constructor is used of runit testing
    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; }

    // In case table not required with primary constraint, OrderDetail table has no primary key
    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.

Image 11

Below code snippets for GetAllCustomerQuery and CreateCustomerCommand w.r.t file name. Instead of writing Mediator design pattern, we can make use MediatR libraries

 

C#
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)

 

Image 12

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

C#
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

C#
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

C#
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

C#
[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));
    }
}

What problem does this solution solve

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

  1. Entity Framework Core
  2. AutoMapper
  3. Dependency injection
  4. Logging-Serilog/ NLog/ StackExchange.Exceptional
  5. CQRS with MediatR
  6. Fluent Validations
  7. Authentication
  8. Swagger/ OpenAPI
  9. Error handling
  10. Unit testing via NUnit
  11. Integration testing via NUnit
  12. Versioning

How does this help someone else

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)

How does the code actually work

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. 

Conclusion

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.

References

History

  • 10th July, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License