How to Build APIs using ASP.NET Core, a clean architectural approach, and the decorator design pattern

Introduction

Implementing the Decorator Pattern in an ASP.NET Core Web API with Clean Architecture involves structuring your application into layers and using decorators to add functionality to specific methods or classes. Here's a simplified example of a CarCompany CRUD operation using Clean Architecture. This example assumes you already have a basic understanding of Clean Architecture and ASP.NET Core.

Create Solutions and Projects

Start by creating a new ASP.NET Core Web API solution with projects for each layer.

  1. CarCompany.Application (Application Layer)
  2. CarCompany.Domain (Domain Layer)
  3. CarCompany.Infrastructure (Infrastructure Layer)
  4. CarCompany.WebApi (Presentation Layer)

Define the Domain Model

In the CarCompany.Domain project, define the Car entity.

// CarCompany.Domain/Entities/Car.cs
namespace CarCompany.Domain.Entities
{
    public class Car
    {
        public int Id { get; set; }
        public string Model { get; set; }
    }
}

Create Repository Interface

Define a repository interface in the CarCompany.Domain project.

// CarCompany.Domain/Interfaces/ICarRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using CarCompany.Domain.Entities;

namespace CarCompany.Domain.Interfaces
{
    public interface ICarRepository
    {
        Task<IEnumerable<Car>> GetAllAsync();
        Task<Car> GetByIdAsync(int id);
        Task<int> AddAsync(Car car);
        Task UpdateAsync(Car car);
        Task DeleteAsync(int id);
    }
}

Implement Repository

Create a repository implementation in the CarCompany.Infrastructure project.

// CarCompany.Infrastructure/Repositories/CarRepository.cs
using System.Collections.Generic;
using System.Threading.Tasks;
using CarCompany.Domain.Entities;
using CarCompany.Domain.Interfaces;
using Microsoft.EntityFrameworkCore;

public class CarRepository : ICarRepository
{
    private readonly YourDbContext _dbContext;

    public CarRepository(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<IEnumerable<Car>> GetAllAsync()
    {
        return await _dbContext.Cars.ToListAsync();
    }

    public async Task<Car> GetByIdAsync(int id)
    {
        return await _dbContext.Cars.FindAsync(id);
    }

    public async Task<int> AddAsync(Car car)
    {
        _dbContext.Cars.Add(car);
        await _dbContext.SaveChangesAsync();
        return car.Id;
    }

    public async Task UpdateAsync(Car car)
    {
        _dbContext.Entry(car).State = EntityState.Modified;
        await _dbContext.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var car = await _dbContext.Cars.FindAsync(id);
        if (car != null)
        {
            _dbContext.Cars.Remove(car);
            await _dbContext.SaveChangesAsync();
        }
    }
}

Implement Application Service

Create an application service in the CarCompany.Application project.

// CarCompany.Application/Services/CarService.cs

using System.Collections.Generic;
using System.Threading.Tasks;
using CarCompany.Domain.Entities;
using CarCompany.Domain.Interfaces;

public class CarService : ICarService
{
    private readonly ICarRepository _carRepository;

    public CarService(ICarRepository carRepository)
    {
        _carRepository = carRepository;
    }

    public async Task<IEnumerable<Car>> GetAllAsync()
    {
        return await _carRepository.GetAllAsync();
    }

    public async Task<Car> GetByIdAsync(int id)
    {
        return await _carRepository.GetByIdAsync(id);
    }

    public async Task<int> AddAsync(Car car)
    {
        return await _carRepository.AddAsync(car);
    }

    public async Task UpdateAsync(Car car)
    {
        await _carRepository.UpdateAsync(car);
    }

    public async Task DeleteAsync(int id)
    {
        await _carRepository.DeleteAsync(id);
    }
}

Create Controller

Implement a controller in the CarCompany.WebApi project.

// CarCompany.WebApi/Controllers/CarController.cs
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
using CarCompany.Application.Services;
using CarCompany.Domain.Entities;

[Route("api/[controller]")]
[ApiController]
public class CarController: ControllerBase
{
    private readonly ICarService _carService;

    public CarController(ICarService carService)
    {
        _carService = carService;
    }

    [HttpGet]
    public async Task<IEnumerable<Car>> GetAll()
    {
        return await _carService.GetAllAsync();
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Car>> GetById(int id)
    {
        var car = await _carService.GetByIdAsync(id);
        if (car == null)
            return NotFound();

        return car;
    }

    [HttpPost]
    public async Task<ActionResult<int>> Add(Car car)
    {
        var id = await _carService.AddAsync(car);
        return CreatedAtAction(nameof(GetById), new { id }, id);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, Car car)
    {
        if (id != car.Id)
            return BadRequest();

        await _carService.UpdateAsync(car);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await _carService.DeleteAsync(id);
        return NoContent();
    }
}

Implement Decorator

Now, let's implement a decorator for the ICarService interface to add logging functionality.

// CarCompany.Application/Decorators/CarServiceLoggingDecorator.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CarCompany.Domain.Entities;
using CarCompany.Domain.Interfaces;
using Microsoft.Extensions.Logging;

public class CarServiceLoggingDecorator : ICarService
{
    private readonly ICarService _carService;
    private readonly ILogger<CarServiceLoggingDecorator> _logger;

    public CarServiceLoggingDecorator(ICarService carService, ILogger<CarServiceLoggingDecorator> logger)
    {
        _carService = carService;
        _logger = logger;
    }

    public async Task<IEnumerable<Car>> GetAllAsync()
    {
        _logger.LogInformation("Getting all cars");
        return await _carService.GetAllAsync();
    }

    public async Task<Car> GetByIdAsync(int id)
    {
        _logger.LogInformation($"Getting car with id {id}");
        return await _carService.GetByIdAsync(id);
    }

    public async Task<int> AddAsync(Car car)
    {
        _logger.LogInformation("Adding a new car");
        return await _carService.AddAsync(car);
    }

    public async Task UpdateAsync(Car car)
    {
        _logger.LogInformation($"Updating car with id {car.Id}");
        await _carService.UpdateAsync(car);
    }

    public async Task DeleteAsync(int id)
    {
        _logger.LogInformation($"Deleting car with id {id}");
        await _carService.DeleteAsync(id);
    }
}

Configure Dependency Injection

In the Startup.cs file of the CarCompany.WebApi project, configure dependency injection.

services.AddScoped<ICarRepository, CarRepository>();
services.AddScoped<ICarService, CarService>();
services.Decorate<ICarService, CarServiceLoggingDecorator>();

This example demonstrates a simple Decorator Pattern with Clean Architecture in an ASP.NET Core Web API. It includes layers for the domain, application, and infrastructure, and a decorator for logging added to the ICarService interface. Remember to replace placeholders like YourDbContext with your actual implementation details.

Conclusion

Implementing the CarServiceLoggingDecorator utilizing the ILogger interface enhances the logging capabilities within the ASP.NET Core Web API application. By incorporating the decorator pattern, we've seamlessly extended the functionality of the ICarService interface to include comprehensive logging statements for each CRUD operation. This approach adheres to Clean Architecture principles, fostering a modular and maintainable codebase with a clear separation of concerns. Using dependency injection and the ILogger interface exemplifies a robust and scalable design, promoting effective debugging, monitoring, and error tracking. This logging decorator is a valuable addition to the overall architecture, providing insights into the execution flow of CarCompany CRUD operations and contributing to a more resilient and transparent application.