This article will explain one of the most used software architectural patterns, the 3 layers architecture. Besides the theoretical explanation of the 3 layers architecture, it will also be given a practical example using .NET 6 in a Web API Project.
What is a 3-Layered Architecture?
The 3 layers architecture was created based on the n-tiered patterns, it groups its main software development components in three different horizontal layers. Based on your software application type you may find yourself with the need to have more than 3 organizational layers, but that is not a problem as far as you may have those specific layers as sub-layers of one of those main layers.
The 3-layered architecture can be applied in any project type or size, and how you are organizing each layer inside your project does not matter. What matters is to have those layers visible and grouped, each layer has to be easily identifiable. In some projects, we have each layer on a separate and independent project, which facilitates parallel development. And in other projects, we have all the 3 layers sharing the same project.
Each layer is responsible to validate its input data and provide a meaningful response.
The first layer is the presentation layer or the interface layer. This layer is responsible for making the bridge between the user and our application, its responsibility is to receive and validate user input and to provide a meaningful response.
The second layer is the application layer, business logic layer, or middle layer. This layer is responsible to apply business logic and transform the input data to our database, also this layer should transport the response from the database to the first layer.
The third layer is the data-access layer, or data layer, back-end layer, or database layer. This layer is responsible for manipulating and managing data, no matter where this data comes from. The data source could be a database, a file, or another software application.
The most important part of any design pattern is to have its pattern explicit, and easy to understand, resulting in increased maintainability. With the 3-layered architecture pattern, it is very important to have each layer's role understood and its role limits well-stabilized.
Some of the most common ways to organize each layer:
- Naming, using its layer's suffix.
- Separated projects.
- Folders with their layer's name on them.
Benefits of using 3-Layered Architecture
As this is an architectural design pattern, the biggest benefit will be well-organized architecture. Being easier for a Software Development team to work together on it and give maintenance, but the main benefits are listed below:
- Loosely coupled, being easier to unit test or replacing an entire layer.
- Unit testing, make use of mocks to test the smallest units of your project.
- Maintainability, easier to understand where the changes in the code have to be made.
- Scalability, with any tier being called independent from the others.
- Reliability, if one layer fails will not impact the others.
- Security, as the presentation layer, does not communicate directly with the data layer.
3 Layers Architecture Implementation Step by Step in .NET 6
The first layer, The Presentation Layer
The first layer is responsible to interact with our end user, and should not have any business logic validated here. Must be as simple as possible.
Nugets packages to install:
Project configuration & Dependency Injection
using Application.Managers;
using Application.Managers.Interfaces;
using Data.DataAccess;
using Data.DataAccess.Interfaces;
using Data.Models;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
builder.Services.AddScoped<ICategoryManager, CategoryManager>();
builder.Services.AddScoped<IEmployeeManager, EmployeeManager>();
builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddDbContext<NorthwindContext>(options =>
options.UseSqlServer("Data Source=;Initial Catalog=Northwind;Integrated Security=True"));
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();
The controllers
[Route("api/[controller]")]
[ApiController]
public class CategoryController : ControllerBase
{
private ICategoryManager CategoryManager { get; }
public CategoryController(ICategoryManager categorymanager)
{
this.CategoryManager = categorymanager;
}
[HttpGet]
public IEnumerable<CategoryDto> GetCategories()
{
return CategoryManager.GetCategories();
}
[HttpGet("{categoryId:int}")]
public CategoryDto GetCategoryById(int categoryId)
{
return CategoryManager.GetCategoryById(categoryId);
}
[HttpPost]
public CategoryDto PostCategory([FromBody] CategoryDto category)
{
return CategoryManager.CreateCategory(category);
}
}
[Route("api/[controller]")]
[ApiController]
public class EmployeeController : ControllerBase
{
private IEmployeeManager EmployeeManager { get; }
public EmployeeController(IEmployeeManager employeeManager)
{
this.EmployeeManager = employeeManager;
}
[HttpGet]
public IEnumerable<EmployeeDto> GetEmployees()
{
return EmployeeManager.GetEmployees();
}
[HttpGet("{employeeId:int}")]
public EmployeeDto GetEmployeeById(int employeeId)
{
return EmployeeManager.GetEmployeeById(employeeId);
}
[HttpPost]
public EmployeeDto PostEmployee([FromBody] EmployeeDto employee)
{
return EmployeeManager.CreateEmployee(employee);
}
}
The second layer, the Application Layer
Here you place your business validation and logic.
Nugets packages to install:
- AutoMapper
- AutoMapper.Extensions.Microsoft.DependencyInjection
AutoMapper Profiles
public class CategoryProfile:Profile
{
public CategoryProfile()
{
CreateMap<CategoryDto, Category>()
.ForMember(dest => dest.CategoryName,
opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.CategoryId,
opt => opt.Ignore());
CreateMap<Category, CategoryDto>()
.ForMember(dest => dest.Name,
opt => opt.MapFrom(src => src.CategoryName));
}
}
public class EmployeeProfile : Profile
{
public EmployeeProfile()
{
CreateMap<EmployeeDto, Employee>()
.ForMember(dest => dest.FirstName,
opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.EmployeeId,
opt => opt.Ignore());
CreateMap<Employee, EmployeeDto>()
.ForMember(dest => dest.Name,
opt => opt.MapFrom(src => src.FirstName));
}
}
DTO classes
public class CategoryDto
{
public CategoryDto()
{
}
public string Name { get; set; } = null!;
public string? Description { get; set; }
}
public class EmployeeDto
{
public string LastName { get; set; } = null!;
public string Name { get; set; } = null!;
public string? Title { get; set; }
public string? TitleOfCourtesy { get; set; }
}
Managers Interfaces
public interface ICategoryManager
{
CategoryDto CreateCategory(CategoryDto category);
IEnumerable<CategoryDto> GetCategories();
CategoryDto GetCategoryById(int categoryId);
}
public interface IEmployeeManager
{
EmployeeDto CreateEmployee(EmployeeDto employee);
IEnumerable<EmployeeDto> GetEmployees();
EmployeeDto GetEmployeeById(int employeeId);
}
Managers classes
public class CategoryManager : ICategoryManager
{
private ICategoryRepository CategoryRepository { get; }
private IMapper Mapper { get; }
public CategoryManager(ICategoryRepository categoryRepository, IMapper mapper)
{
this.CategoryRepository = categoryRepository;
this.Mapper = mapper;
}
public IEnumerable<CategoryDto> GetCategories()
{
var response = this.CategoryRepository.GetCategories();
return Mapper.Map<IEnumerable<CategoryDto>>(response);
}
public CategoryDto GetCategoryById(int categoryId)
{
var response = this.CategoryRepository.GetCategoryById(categoryId);
return Mapper.Map<CategoryDto>(response);
}
public CategoryDto CreateCategory(CategoryDto category)
{
var entity = Mapper.Map<Category>(category);
var response = this.CategoryRepository.CreateCategory(entity);
return Mapper.Map<CategoryDto>(response);
}
}
public class EmployeeManager : IEmployeeManager
{
private IEmployeeRepository EmployeeRepository { get; }
private IMapper Mapper { get; }
public EmployeeManager(IEmployeeRepository employeeRepository, IMapper mapper)
{
this.EmployeeRepository = employeeRepository;
this.Mapper = mapper;
}
public IEnumerable<EmployeeDto> GetEmployees()
{
var response = this.EmployeeRepository.GetEmployees();
return Mapper.Map<IEnumerable<EmployeeDto>>(response);
}
public EmployeeDto GetEmployeeById(int employeeId)
{
var response = this.EmployeeRepository.GetEmployeeById(employeeId);
return Mapper.Map<EmployeeDto>(response);
}
public EmployeeDto CreateEmployee(EmployeeDto employee)
{
var entity = Mapper.Map<Employee>(employee);
var response = this.EmployeeRepository.CreateEmployee(entity);
return Mapper.Map<EmployeeDto>(response);
}
}
The third layer, the Data Layer
The third layer is responsible to connect to our database, we will be using Entity Framework as the object-database mapper. If you do not know how to set up your database access you can check my previous article explaining about Entity Framework Database first approach, as follows:
Nugets packages to install:
- EntityFramework
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
The Repository Interfaces
public interface ICategoryRepository
{
IEnumerable<Category> GetCategories();
Category GetCategoryById(int categoryId);
Category CreateCategory(Category category);
}
public interface IEmployeeRepository
{
IEnumerable<Employee> GetEmployees();
Employee GetEmployeeById(int employeeId);
Employee CreateEmployee(Employee employee);
}
The Repository Classes
public class CategoryRepository : ICategoryRepository
{
private NorthwindContext DbContext { get; }
public CategoryRepository(NorthwindContext dbContext)
{
this.DbContext = dbContext;
}
public IEnumerable<Category> GetCategories()
{
return this.DbContext.Categories;
}
public Category GetCategoryById(int categoryId)
{
return DbContext.Categories.FirstOrDefault(x => x.CategoryId.Equals(categoryId));
}
public Category CreateCategory(Category category)
{
this.DbContext.Categories.Add(category);
this.DbContext.SaveChanges();
return category;
}
}
public class EmployeeRepository : IEmployeeRepository
{
private NorthwindContext DbContext { get; }
public EmployeeRepository(NorthwindContext dbContext)
{
this.DbContext = dbContext;
}
public IEnumerable<Employee> GetEmployees()
{
return this.DbContext.Employees;
}
public Employee GetEmployeeById(int employeeId)
{
return this.DbContext.Employees.FirstOrDefault(x => x.EmployeeId.Equals(employeeId));
}
public Employee CreateEmployee(Employee employee)
{
this.DbContext.Employees.Add(employee);
this.DbContext.SaveChanges();
return employee;
}
}
Congratulations, you have successfully created a Web API project using the famous 3-layered architecture.
External References