Clean Architecture and Command Pattern in ASP.NET Core API Implementation

Introduction

Implementing the Command Pattern in an ASP.NET Core Web API with Clean Architecture involves creating layers for different concerns like Application, Domain, and Infrastructure. Below is a basic outline to guide you through the process. Note that this is a simplified example, and you may need to adjust it based on your specific requirements.

Step 1. Project Structure

Create the following projects in your solution.

  1. CarCompany.API: This is your Web API project.
  2. CarCompany.Application: Application services and command handlers go here.
  3. CarCompany.Domain: Entities, Aggregates, and Domain Services go here.
  4. CarCompany.Infrastructure: Repositories, Database Context, and other infrastructure-specific code go here.

Step 2. Define Domain Entities

// Sardar Mudassar Ali Khan
// CarCompany.Domain/Entities/Car.cs
public class Car
{
    public int Id { get; set; }
    public string Model { get; set; }
}

Step 3. Define Commands

// Sardar Mudassar Ali Khan
// CarCompany.Application/Commands/CreateCarCommand.cs
public class CreateCarCommand: ICommand<int>
{
    public string Model { get; set; }
}

public class UpdateCarCommand: ICommand
{
    public int Id { get; set; }
    public string Model { get; set; }
}

public class DeleteCarCommand: ICommand
{
    public int Id { get; set; }
}

Step 4. Define Command Handlers

// Sardar Mudassar Ali Khan
// CarCompany.Application/Handlers/CreateCarCommandHandler.cs
public class CreateCarCommandHandler: ICommandHandler<CreateCarCommand, int>
{
    private readonly ICarRepository _carRepository;

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

    public async Task<int> Handle(CreateCarCommand request, CancellationToken cancellationToken)
    {
        var car = new Car { Model = request.Model };
        await _carRepository.AddAsync(car);
        return car.Id;
    }
}

// CarCompany.Application/Handlers/UpdateCarCommandHandler.cs
public class UpdateCarCommandHandler: ICommandHandler<UpdateCarCommand>
{
    private readonly ICarRepository _carRepository;

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

    public async Task Handle(UpdateCarCommand request, CancellationToken cancellationToken)
    {
        var car = await _carRepository.GetByIdAsync(request.Id);
        if (car == null)
        {
            throw new NotFoundException($"Car with ID {request.Id} not found.");
        }

        car.Model = request.Model;

        await _carRepository.UpdateAsync(car);
    }
}

// CarCompany.Application/Handlers/DeleteCarCommandHandler.cs
public class DeleteCarCommandHandler: ICommandHandler<DeleteCarCommand>
{
    private readonly ICarRepository _carRepository;

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

    public async Task Handle(DeleteCarCommand request, CancellationToken cancellationToken)
    {
        var car = await _carRepository.GetByIdAsync(request.Id);
        if (car == null)
        {
            throw new NotFoundException($"Car with ID {request.Id} not found.");
        }

        await _carRepository.DeleteAsync(car);
    }
}

Step 5. Implement Command Dispatcher

// Sardar Mudassar Ali Khan
// CarCompany.Application/Commands/CommandDispatcher.cs
public class CommandDispatcher: ICommandDispatcher
{
    private readonly IServiceProvider _serviceProvider;

    public CommandDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task<TResponse> Dispatch<TCommand, TResponse>(TCommand command)
        where TCommand: ICommand<TResponse>
    {
        var handler = _serviceProvider.GetService<ICommandHandler<TCommand, TResponse>>();
        return await handler.Handle(command);
    }

    public async Task Dispatch<TCommand>(TCommand command) where TCommand : ICommand
    {
        var handler = _serviceProvider.GetService<ICommandHandler<TCommand>>();
        await handler.Handle(command);
    }
}

Step 6. API Controller

// Sardar Mudassar Ali Khan
// CarCompany.API/Controllers/CarController.cs
[ApiController]
[Route("api/[controller]")]
public class CarController: ControllerBase
{
    private readonly ICommandDispatcher _commandDispatcher;

    public CarController(ICommandDispatcher commandDispatcher)
    {
        _commandDispatcher = commandDispatcher;
    }

    [HttpPost]
    public async Task<IActionResult> CreateCar([FromBody] CreateCarCommand command)
    {
        var carId = await _commandDispatcher.Dispatch<CreateCarCommand, int>(command);
        return Ok(carId);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateCar(int id, [FromBody] UpdateCarCommand command)
    {
        command.Id = id;
        await _commandDispatcher.Dispatch(command);
        return Ok();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteCar(int id)
    {
        var command = new DeleteCarCommand { Id = id };
        await _commandDispatcher.Dispatch(command);
        return Ok();
    }
}

Step 7. Configure Dependency Injection

In your Startup. cs:

// Sardar Mudassar Ali Khan
// CarCompany.API/Startup.cs
public void ConfigureServices(IServiceCollection services)
{

    services.AddScoped<ICommandDispatcher, CommandDispatcher>();
    services.AddScoped<ICommandHandler<CreateCarCommand, int>, CreateCarCommandHandler>();
    services.AddScoped<ICommandHandler<UpdateCarCommand>, UpdateCarCommandHandler>();
    services.AddScoped<ICommandHandler<DeleteCarCommand>, DeleteCarCommandHandler>();

}

This is a basic example to illustrate the structure. Depending on your application's complexity, you may need to introduce additional layers, such as DTOs, Validators, QueryHandlers, and more. Additionally, error handling and validation should be enhanced according to your needs.

Conclusion

The implementation of the Command Pattern in the ASP.NET Core Web API for the Car Company CRUD operations, following the principles of Clean Architecture, provides a structured and modular solution. The separation of concerns into different layers, such as Application, Domain, and Infrastructure, facilitates maintainability and scalability. The use of command handlers for each operation, error handling with specific exceptions, and dependency injection enhance the code's readability and testability. This design promotes a clear separation of responsibilities, making it easier to extend and modify the system in the future while adhering to best practices in software architecture and design patterns.