Implementing Onion Architecture In ASP.NET Core 3.0

Introduction

 
Onion Architecture is used to overcome both separation of concern and tightly coupling issues in our application.
 
Separation of Concern (Soc)
 
Wikipedia says “In computer science, separation of concerns (SoC) is a design principle for separating a computer program into distinct sections, such that each section addresses a separate concern. A concern is a set of information that affects the code of a computer program.”
 
SoC applies to web applications to separate the application into different layers, for example based on MVC architecture the application will be separated into Model, View and Controller.
 
Tightly Coupled
 
Tightly coupled object is an object that needs to know about another object. Tightly-coupled objects are highly dependant on each other’s interfaces. If we change one object, we need to change the other object as well.
 
Onion Architecture contains four layers,
  1. Domain Layer
  2. Repository Layer
  3. Service Layer
  4. UI/Presentation Layer
Domain/Data Access Layer
 
This is the core layer of the application, the classes in this layer are used to create a table in the database, basically this layer is used to build an entity.
 
Repository Layer
 
This is a generic repository layer which acts as an abstract layer between the data access and business layer of the application which makes a more loosely coupled approach to data access.
 
Service Layer
 
This layer is used communicate between UI and repository layers using Interface. We can call it as a business layer since it holds the business logic for an entity.
 
UI Layer
 
The UI layer can be Web API or Web application or any other UI application. This layer contains the implementation of DI (Dependency Inversion) principle to build a loosely coupled application.
 
Prerequisites
 
Software
 
Visual Studio 2019 16.3.4 & later
 

Onion Architecture in ASP.NET Core 3.0 

 
To implement the onion architecture, we need to create four projects, three class library projects for Domain, Repository and service layer respectively and one ASP.NET Core API project for UI layer.
 
Project Structure 
 
Implementing Onion Architecture In ASP.NET Core 3.0 
 
Step 1 - Create the project for Domain/Data Access Layer
 
Create a Class Library (.NET Core) Project using Visual Studio as shown in the below figure.
 
 Implementing Onion Architecture In ASP.NET Core 3.0
 
In my case I named it as OA_DataAccess.
 
OA_DataAccess class contains three entities, the BaseEntity which holds a common property and the other two entities, which are Product and Product Details.
 
BaseEntity.cs 
  1. public class BaseEntity  
  2.     {  
  3.         public int ProductId { get; set; }  
  4.      }  
This class will hold the common properties used in product and product details entity, right now we have only ProductId, but it will grow as the application grows.
 
Product.cs
  1. public class Product : BaseEntity  
  2.     {  
  3.         public string ProductName { get; set; }  
  4.         public virtual ProductDetails ProductDetails { get; set; }  
  5.     }  
This class inherits from BaseEntity.
 
ProductMap.cs
  1. public class ProductMap  
  2.     {  
  3.         public ProductMap(EntityTypeBuilder<Product> entityBuilder)  
  4.         {  
  5.   
  6.             entityBuilder.HasKey(p => p.ProductId);  
  7.             entityBuilder.HasOne(p => p.ProductDetails).WithOne(p => p.Product).HasForeignKey<ProductDetails>(x => x.ProductId);  
  8.         }  
This class will hold the configuration of Product Entity which is used to create a database table.
 
Make sure Microsoft.EntityFrameworkCore package is added using NuGet package manager.
 
ProductDetail.cs
  1. public class ProductDetails : BaseEntity  
  2.     {        
  3.         public int StockAvailable { get; set; }  
  4.         public decimal Price { get; set; }  
  5.         public virtual Product Product{get;set;}  
  6.     }  
This class Inherits from BaseEntity.
 
ProductDetailMap.cs
  1. public class ProductDetailMap  
  2.   {  
  3.       public ProductDetailMap(EntityTypeBuilder<ProductDetails> entityBuilder)  
  4.       {  
  5.           entityBuilder.HasKey(p => p.ProductId);              
  6.           entityBuilder.Property(p => p.StockAvailable).IsRequired();  
  7.           entityBuilder.Property(p => p.Price);  
  8.       }  
  9.   }  
This class will hold the configuration of ProductDetail Entity which Is used to create a database table for the entity.
 
Step 2 - Create the project for Repository Layer
 
Create one more class library project, in my case I named it as OA_Repository. This layer contains a Datacontext class. The ADO.NET Entity Framework code first approach is used to create a data access context class.
 
Create a class file and name it as ApplicationContext.cs.
  1. public class ApplicationContext :DbContext  
  2.  {  
  3.      public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)  
  4.      {  
  5.      }  
  6.      protected override void OnModelCreating(ModelBuilder modelBuilder)  
  7.      {  
  8.          base.OnModelCreating(modelBuilder);  
  9.          new ProductMap(modelBuilder.Entity<Product>());  
  10.          new ProductDetailMap(modelBuilder.Entity<ProductDetails>());  
  11.      }  
  12.  }  
Make sure Microsoft.EntityFrameworkCore package is added using NuGet package manager.
 
Now create an Interface and name it as IRepository.cs.
  1. public interface IRepository<T> where T : BaseEntity  
  2.     {  
  3.         T Get(int id);  
  4.         IEnumerable<T> GetAll();  
  5.           
  6.     }  
Let’s implement IRepository, create a new class file and name it as Repository.cs
  1. public class Repository<T> : IRepository<T> where T : BaseEntity  
  2.     {  
  3.         private readonly ApplicationContext context;  
  4.         private readonly DbSet<T> entities;  
  5.         public Repository(ApplicationContext context)  
  6.         {  
  7.             this.context = context;  
  8.             entities = context.Set<T>();  
  9.         }  
  10.         public IEnumerable<T> GetAll()  
  11.         {  
  12.             return entities.AsEnumerable();  
  13.         }  
  14.         public T Get(int id)  
  15.         {  
  16.             return entities.SingleOrDefault(p =>p.ProductId  == id);  
  17.         }  
  18.     }  
This a generic repository which implements IRepository to build a loosely coupled application, ASP.NET Core is designed in the way to support and leverage dependency injection.
 
Step 3 - Create the project for Service Layer 
 
This layer communicates between repository and presentation layer. Let’s create one more class project and name it as OA_Service. This project contains an interface and classes which implement the interface.
 
Create a new interface in OA_Service project and name it as IProductService.cs
  1. public interface IProductService  
  2.     {  
  3.         IEnumerable<OA_DataAccess.Product> GetProduct();  
  4.         OA_DataAccess.Product GetProduct(int id);  
  5.     }  
Create a new interface in OA_Service project and name it as IProductDetailService.cs
  1. public interface IProductDetailsService  
  2.     {  
  3.           
  4.         OA_DataAccess.ProductDetails GetProductDetail(int id);  
  5.     }  
Create a new class and name it as ProductService.cs to Implement IProductService.cs.
  1. public class ProductService: IProductService  
  2.     {  
  3.         private IRepository<Product> productRepository;  
  4.         private IRepository<ProductDetails> productDetailRepository;  
  5.   
  6.         public ProductService(IRepository<Product> productRepository, IRepository<ProductDetails> productDetailRepository)  
  7.         {  
  8.             this.productRepository = productRepository;  
  9.             this.productDetailRepository = productDetailRepository;  
  10.         }  
  11.         public IEnumerable<Product> GetProduct()  
  12.         {  
  13.             return productRepository.GetAll();  
  14.         }  
  15.   
  16.         public Product GetProduct(int id)  
  17.         {  
  18.             return productRepository.Get(id);  
  19.         }  
  20.     }  
You can notice we have parameterized constructor which is used to inject dependencies in the object.
 
GetProduct() method is used to get a ProductList from Product Repository.
 
GetProduct(int id) method is used to get a record from Product Repository based on ID.
 
Create a new class and name it as ProductDetailService.cs to Implement IProductDetailService.cs.
  1. public class ProductDetailsService: IProductDetailsService  
  2.     {  
  3.         private IRepository<ProductDetails> productDetailsRepository;  
  4.   
  5.         public ProductDetailsService(IRepository<ProductDetails> productDetailsRepository)  
  6.         {  
  7.             this.productDetailsRepository = productDetailsRepository;  
  8.         }  
  9.   
  10.         public ProductDetails GetProductDetail(int id)  
  11.         {  
  12.             return productDetailsRepository.Get(id);  
  13.         }  
  14.     }  
Step 4 - Create ASP.NET WEB API application for UI/Presentation Layer
 
This is an external layer, in our case it is a ASP.NET CORE WEB API project where we are going to create a REST service which is exposed to external clients.
 
Right Click on Solution Add->Project->New Project
 
Choose the template ASP.NET Core Web Application as shown in the below figure.
 
Implementing Onion Architecture In ASP.NET Core 3.0 
 
Next, name the project, in my case it is OA_WebAPI.
 
Implementing Onion Architecture In ASP.NET Core 3.0 
 
Next, choose the API template as shown in the below figure.
 
Implementing Onion Architecture In ASP.NET Core 3.0 
 
Make sure the target framework is ASP.NET 3.0.
 
Once the project is created, go to Startup.cs file and add the below code under ConfigureService method.
  1. services.AddDbContext<ApplicationContext>(options =>   options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));  
  2.             services.AddScoped(typeof(IRepository<>), typeof(Repository<>));  
  3.             services.AddTransient<IProductService, ProductService>();  
  4.             services.AddTransient<IProductDetailsService, ProductDetailsService>();  
Where the application context, repository and services are registered to the dependency injection during application start up.
 
The Default connection string is defined in appsettings.json file.
  1. "ConnectionStrings": {  
  2.     "DefaultConnection""Data Source=DESKTOP-585QGBN;Initial Catalog=OATestDB;User ID=sa; Password=[give your SQL instance password]"  
  3.   },  
Create a New Web API controller, In my case ProductController.cs.
  1. private readonly IProductService productService;  
  2.         private readonly IProductDetailsService productDetailsService;  
  3.   
  4.         public ProductController(IProductService productService, IProductDetailsService productDetailsService)  
  5.         {  
  6.             this.productService = productService;  
  7.             this.productDetailsService = productDetailsService;  
  8.         }  
  9.      
  10.    [HttpGet]  
  11.         public List<ProductDetails> Get()  
  12.         {  
  13.             List<ProductDetails> productDetails = new List<ProductDetails>();  
  14.             var prodcutList=productService.GetProduct().ToList();  
  15.             foreach(var product in prodcutList)  
  16.             {  
  17.                 var productDetailList = productDetailsService.GetProductDetail(product.ProductId);  
  18.                 ProductDetails details = new ProductDetails  
  19.                 {  
  20.                     ProductId = product.ProductId,  
  21.                     ProductName = product.ProductName,  
  22.                     Price = productDetailList.Price,  
  23.                     StockAvailable = productDetailList.StockAvailable,  
  24.                 };  
  25.                 productDetails.Add(details);  
  26.             }  
  27.             return productDetails;  
  28.         }  
Now it’s time to create database using migration. Go to package manager console which is available in the bottom task bar of our Visual Studio, or else we can access it from Tools-> NuGet Package Manager-> Package Manager Console.
 
Note
We need to add a migration for our OA_Repository Project, before adding a migration make sure you have added the following packages in the OA_Repository as given below,
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Relational
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
Choose OA_Repository Project from Package Manager Console and run > Add-Migration MyInitialMigration which will create a set of tables based on our models.
 
Implementing Onion Architecture In ASP.NET Core 3.0 
 
Next run > Update-Database to apply the migration to the database.
 
Now Switch to SSMS where you can notice the database and tables which are newly created based on the migration.
 
Implementing Onion Architecture In ASP.NET Core 3.0
 
Now let’s insert a record in Product and ProductDetails table to test the GET Action in product controller which is used to return a product detail.
 
Implementing Onion Architecture In ASP.NET Core 3.0
 

Summary

 
We have seen what Onion Architecture  isand how to implement it in ASP.NET Core 3.0.
 
I hope you have enjoyed this article. Your valuable feedback, questions, or comments about this are always welcomed.