Introduction
In this article, we will cover the onion architecture using the ASP.Net 6 Web API. Onion architecture term is introduced by Jeffrey Palermo in 2008. This architecture provides us a better way to build applications using this architecture our applications are better testable, maintainable, and dependable on infrastructures like databases and services. Onion architecture solves common problems like coupling and separation of concerns.
What is Onion architecture?
In onion architecture, we have the domain layer, repository layer, service layer, and presentation layer. Onion architecture solves the problem that we face during the enterprise applications like coupling and separations of concerns. Onion architecture also solves the problem that we confronted in three-tier architecture and N-Layer architecture. In Onion architecture, our layer communicates with each other using interfaces.
Layers in Onion Architecture
Onion architecture uses the concept of the layer but is different from N-layer architecture and 3-Tier architecture.
Domain Layer
This layer lies in the center of the architecture where we have application entities which are the application model classes or database model classes. Using the code first approach in the application development using Asp.net core these entities are used to create the tables in the database.
Repository Layer
The repository layer act as a middle layer between the service layer and model objects. We will maintain all the database migrations and database context Objects in this layer. We will add the interfaces that consist of the data access pattern for reading and writing operations with the database.
Service Layer
This layer is used to communicate with the presentation and repository layer. The service layer holds all the business logic of the entity. In this layer services interfaces are kept separate from their implementation for loose coupling and separation of concerns.
Presentation Layer
In the case of the API Presentation layer that presents us the object data from the database using the HTTP request in the form of JSON Object. But in the case of front-end applications, we present the data using the UI by consuming the APIS.
Advantages of Onion Architecture
- Onion architecture provides us with the batter maintainability of code because code depends on layers.
- It provides us with better testability for unit tests, we can write the separate test cases in layers without affecting the other module in the application.
- Using the onion architecture our application is loosely coupled because our layers communicate with each other using the interface.
- Domain entities are the core and center of the architecture and have access to databases and UI Layer.
- A complete implementation would be provided to the application at run time.
- The external layer never depends on the external layer.
Implementation of Onion Architecture
Now we are going to develop the project using the Onion Architecture
First, you need to create the Asp.net Core web API project using visual studio. After creating the project, we will add our layer to the project. After adding all the layers our project structure will look like this.
Project Structure.
Domain Layer
This layer lies in the center of the architecture where we have application entities which are the application model classes or database model classes. Using the code first approach in the application development using Asp.net core these entities are used to create the tables in the database
For the Domain layer, we need to add the library project to our application.
Right click on the Solution and then click on add option.
Add the library project to your solution
Name this project as Domain Layer
Models Folder
First, you need to add the Models folder that will be used to create the database entities. In the Models folder, we will create the following database entities.
Base Entity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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;
}
}
}
Department
Department entity will extend the base entity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainLayer.Models {
public class Departments: BaseEntity {
public int Id {
get;
set;
}
public string DepartmentName {
get;
set;
}
public int StudentId {
get;
set;
}
public Student Students {
get;
set;
}
}
}
Result
Result Entity will extend the base entity
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainLayer.Models {
public class Resultss: BaseEntity {
[Key]
public int Id {
get;
set;
}
public string ? ResultStatus {
get;
set;
}
public int StudentId {
get;
set;
}
public Student Students {
get;
set;
}
}
}
Students
Student Entity will extend the base entity
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainLayer.Models {
public class Student: BaseEntity {
[Key]
public int Id {
get;
set;
}
public string ? Name {
get;
set;
}
public string ? Address {
get;
set;
}
public string ? Emial {
get;
set;
}
public string ? City {
get;
set;
}
public string ? State {
get;
set;
}
public string ? Country {
get;
set;
}
public int ? Age {
get;
set;
}
public DateTime ? BirthDate {
get;
set;
}
}
}
SubjectGpa
Subject GPA Entity will extend the Base Entity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainLayer.Models {
public class SubjectGpas: BaseEntity {
public int Id {
get;
set;
}
public string SubjectName {
get;
set;
}
public float Gpa {
get;
set;
}
public string SubjectPassStatus {
get;
set;
}
public int StudentId {
get;
set;
}
public Student Students {
get;
set;
}
}
}
Data Folder
Add the Data in the domain that is used to add the database context class. The database context class is used to maintain the session with the underlying database using which you can perform the CRUD operation.
Write click on the application and create the class ApplicationDbContext.cs
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DomainLayer.Data {
public class ApplicationDbContext: DbContext {
public ApplicationDbContext(DbContextOptions < ApplicationDbContext > options): base(options) {}
protected override void OnModelCreating(ModelBuilder builder) {
base.OnModelCreating(builder);
}
public DbSet < Student > Students {
get;
set;
}
public DbSet < Departments > Departments {
get;
set;
}
public DbSet < SubjectGpas > SubjectGpas {
get;
set;
}
public DbSet < Resultss > Results {
get;
set;
}
}
}
After Adding the DbSet properties we need to add the migration using the package manager console and run the command Add-Migration.
add-migration OnionArchitectureV1
After executing the commands now, you need to update the database by executing the update-database Command
Migration Folder
After executing both commands now you can see the migration folder in the domain layer.
Repository Layer
The repository layer act as a middle layer between the service layer and model objects. We will maintain all the database migrations and database context Objects in this layer. We will add the interfaces that consist the of data access pattern for reading and writing operations with the database.
Now we will work on Repository Layer. We will follow the same project as we did for the Domain layer. Add the library project in your application and give a name to that project Repository layer.
But here we need to add the project reference of the Domain layer in the repository layer. Write click on the project and then click the Add button after that we will add the project references in the Repository layer.
Click on project reference now and select the Domain layer.
Now we need to add the two folders.
IRepository
IRepsitory folder contains the generic interface, using this interface we will create the CRUD operation for our all entities.
Generic Interface will extend the Base-Entity for using some properties. The code for the generic interface is given below.
using DomainLayer.Models;
namespace RepositoryLayer.IRepository {
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();
}
}
Repository
Now we create the Generic repository that extends the Generic IRepository Interface. The code for the generic repository is given below
using DomainLayer.Data;
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using RepositoryLayer.IRepository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RepositoryLayer.Repository {
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();
}
}
}
We will use this repository in our service layer.
Service Layer
This layer is used to communicate with the presentation and repository layer. The service layer holds all the business logic of the entity. In this layer services interfaces are kept separate from their implementation for loose coupling and separation of concerns.
Now we need to add a new project to our solution that will be the service layer. We will follow the same process for adding the library project in our application, but here we need some extra work after adding the project we need to add the reference of the Repository Layer.
Now our service layer contains the reference of the repository layer.
In the Service layer, we will create the two folders.
- ICustomServices
- CustomServices
ICustom Service
Now in the ICustomServices folder, we will create the ICustomServices Interface, this interface holds the signature of the method. We will implement these methods in the customs service code of the ICustomServices Interface given below.
using DomainLayer.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServiceLayer.ICustomServices {
public interface ICustomService < T > where T: class {
IEnumerable < T > GetAll();
T Get(int Id);
void Insert(T entity);
void Update(T entity);
void Delete(T entity);
void Remove(T entity);
}
}
Custom Service
In the custom service folder, we will create the custom service class that inherits the ICustomService interface code of the custom service class is given below. We will write the crud for our all entities.
For every service, we will write the CRUD operation using our generic repository.
Department Service
using DomainLayer.Models;
using RepositoryLayer.IRepository;
using ServiceLayer.ICustomServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServiceLayer.CustomServices {
public class DepartmentsService: ICustomService < Departments > {
private readonly IRepository < Departments > _studentRepository;
public DepartmentsService(IRepository < Departments > studentRepository) {
_studentRepository = studentRepository;
}
public void Delete(Departments entity) {
try {
if (entity != null) {
_studentRepository.Delete(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public Departments Get(int Id) {
try {
var obj = _studentRepository.Get(Id);
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public IEnumerable < Departments > GetAll() {
try {
var obj = _studentRepository.GetAll();
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public void Insert(Departments entity) {
try {
if (entity != null) {
_studentRepository.Insert(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Remove(Departments entity) {
try {
if (entity != null) {
_studentRepository.Remove(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Update(Departments entity) {
try {
if (entity != null) {
_studentRepository.Update(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
}
}
Student Service
using DomainLayer.Models;
using RepositoryLayer.IRepository;
using ServiceLayer.ICustomServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServiceLayer.CustomServices {
public class StudentService: ICustomService < Student > {
private readonly IRepository < Student > _studentRepository;
public StudentService(IRepository < Student > studentRepository) {
_studentRepository = studentRepository;
}
public void Delete(Student entity) {
try {
if (entity != null) {
_studentRepository.Delete(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public Student Get(int Id) {
try {
var obj = _studentRepository.Get(Id);
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public IEnumerable < Student > GetAll() {
try {
var obj = _studentRepository.GetAll();
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public void Insert(Student entity) {
try {
if (entity != null) {
_studentRepository.Insert(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Remove(Student entity) {
try {
if (entity != null) {
_studentRepository.Remove(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Update(Student entity) {
try {
if (entity != null) {
_studentRepository.Update(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
}
}
Result Service
using DomainLayer.Models;
using RepositoryLayer.IRepository;
using ServiceLayer.ICustomServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServiceLayer.CustomServices {
public class ResultService: ICustomService < Resultss > {
private readonly IRepository < Resultss > _studentRepository;
public ResultService(IRepository < Resultss > studentRepository) {
_studentRepository = studentRepository;
}
public void Delete(Resultss entity) {
try {
if (entity != null) {
_studentRepository.Delete(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public Resultss Get(int Id) {
try {
var obj = _studentRepository.Get(Id);
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public IEnumerable < Resultss > GetAll() {
try {
var obj = _studentRepository.GetAll();
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public void Insert(Resultss entity) {
try {
if (entity != null) {
_studentRepository.Insert(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Remove(Resultss entity) {
try {
if (entity != null) {
_studentRepository.Remove(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Update(Resultss entity) {
try {
if (entity != null) {
_studentRepository.Update(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
}
}
Subject GPA Service
using DomainLayer.Models;
using RepositoryLayer.IRepository;
using ServiceLayer.ICustomServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ServiceLayer.CustomServices {
public class SubjectGpasService: ICustomService < SubjectGpas > {
private readonly IRepository < SubjectGpas > _studentRepository;
public SubjectGpasService(IRepository < SubjectGpas > studentRepository) {
_studentRepository = studentRepository;
}
public void Delete(SubjectGpas entity) {
try {
if (entity != null) {
_studentRepository.Delete(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public SubjectGpas Get(int Id) {
try {
var obj = _studentRepository.Get(Id);
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public IEnumerable < SubjectGpas > GetAll() {
try {
var obj = _studentRepository.GetAll();
if (obj != null) {
return obj;
} else {
return null;
}
} catch (Exception) {
throw;
}
}
public void Insert(SubjectGpas entity) {
try {
if (entity != null) {
_studentRepository.Insert(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Remove(SubjectGpas entity) {
try {
if (entity != null) {
_studentRepository.Remove(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
public void Update(SubjectGpas entity) {
try {
if (entity != null) {
_studentRepository.Update(entity);
_studentRepository.SaveChanges();
}
} catch (Exception) {
throw;
}
}
}
}
Presentation Layer
The presentation layer is our final layer that presents the data to the front-end user on every HTTP request.
In the case of the API presentation layer that presents us the object data from the database using the HTTP request in the form of JSON Object. But in the case of front-end applications, we present the data using the UI by consuming the APIS.
The presentation layer is the default Asp.net core web API project Now we need to add the project references of all the layers as we did before.
Dependency Injection
Now we need to add the dependency Injection of our all services in the program.cs class
Modify Program.cs File
The code of the Startup Class is given below
using DomainLayer.Data;
using DomainLayer.Models;
using Microsoft.EntityFrameworkCore;
using RepositoryLayer.IRepository;
using RepositoryLayer.Repository;
using ServiceLayer.CustomServices;
using ServiceLayer.ICustomServices;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
//Sql Dependency Injection
var ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext < ApplicationDbContext > (options => options.UseSqlServer(ConnectionString));
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
#region Service Injected
builder.Services.AddScoped(typeof(IRepository < > ), typeof(Repository < > ));
builder.Services.AddScoped < ICustomService < Student > , StudentService > ();
builder.Services.AddScoped < ICustomService < Resultss > , ResultService > ();
builder.Services.AddScoped < ICustomService < Departments > , DepartmentsService > ();
builder.Services.AddScoped < ICustomService < SubjectGpas > , SubjectGpasService > ();
#endregion
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Controllers
Controllers are used to handle the HTTP request. Now we need to add the student controller that will interact will our service layer and display the data to the users.
Code of the Controller is given below.
using DomainLayer.Data;
using DomainLayer.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RepositoryLayer.IRepository;
using ServiceLayer.ICustomServices;
namespace OnionArchitectureInAspNetCore6WebAPI.Controllers {
[Route("api/[controller]")]
[ApiController]
public class StudentsController: ControllerBase {
private readonly ICustomService < Student > _customService;
private readonly ApplicationDbContext _applicationDbContext;
public StudentsController(ICustomService < Student > customService, ApplicationDbContext applicationDbContext) {
_customService = customService;
_applicationDbContext = applicationDbContext;
}
[HttpGet(nameof(GetStudentById))]
public IActionResult GetStudentById(int Id) {
var obj = _customService.Get(Id);
if (obj == null) {
return NotFound();
} else {
return Ok(obj);
}
}
[HttpGet(nameof(GetAllStudent))]
public IActionResult GetAllStudent() {
var obj = _customService.GetAll();
if (obj == null) {
return NotFound();
} else {
return Ok(obj);
}
}
[HttpPost(nameof(CreateStudent))]
public IActionResult CreateStudent(Student student) {
if (student != null) {
_customService.Insert(student);
return Ok("Created Successfully");
} else {
return BadRequest("Somethingwent wrong");
}
}
[HttpPost(nameof(UpdateStudent))]
public IActionResult UpdateStudent(Student student) {
if (student != null) {
_customService.Update(student);
return Ok("Updated SuccessFully");
} else {
return BadRequest();
}
}
[HttpDelete(nameof(DeleteStudent))]
public IActionResult DeleteStudent(Student student) {
if (student != null) {
_customService.Delete(student);
return Ok("Deleted Successfully");
} else {
return BadRequest("Something went wrong");
}
}
}
}
Output
Now we will run the project and will see the output using the swagger.
Now we can see when we hit the GetAllStudent Endpoint we can see the data of students from the database in the form of JSON projects.
Conclusion
In this article, we have implemented the Onion architecture using the Entity Framework and Code First approach. We have now the knowledge of how the layer communicates with each other’s in onion architecture and how we can write the Generic code for the Interface repository and services. Now we can develop our project using onion architecture for API Development OR MVC Core Based projects.