Showing posts with label Clean Architecture. Show all posts
Showing posts with label Clean Architecture. Show all posts

Wednesday, 24 December 2025

Building Clean APIs with ASP.NET Core and Entity Framework

API Architecture Banner
Building Clean APIs with ASP.NET Core

Best practices for designing maintainable, scalable, and secure REST APIs

Creating an API that works is easy. Creating an API that is maintainable, testable, and pleasant to consume is an art. In the .NET ecosystem, ASP.NET Core combined with Entity Framework (EF) Core gives us a powerful toolkit, but it's up to us to use it wisely.

This guide covers the essential patterns for building clean, professional-grade APIs, moving beyond simple CRUD to robust enterprise architecture.

1. Separation of Concerns: The Service Layer

One of the most common mistakes is putting business logic directly inside Controllers. This makes code hard to test and reuse. Instead, aim for a clear layered architecture.

Clean Architecture Diagram

The Onion Architecture: Dependencies point inwards.

❌ The Bad Way (Fat Controller)

[HttpPost]
public IActionResult CreateOrder(OrderDto dto) {
    // Validation logic mixed with business logic
    if(dto.Amount < 0) return BadRequest();
    
    var order = new Order { ... };
    _dbContext.Orders.Add(order); // Direct DB access in controller
    _dbContext.SaveChanges();
    return Ok(order);
}

✅ The Clean Way (Service Pattern)

[HttpPost]
public async Task<IActionResult> CreateOrder(OrderDto dto) {
    // Controller only handles HTTP concerns
    var result = await _orderService.CreateOrderAsync(dto);
    return Ok(result);
}

2. Global Error Handling with Middleware

Don't wrap every controller action in a try-catch block. It clutters your code. Instead, use Middleware to catch exceptions globally and return a standardized error response.

Middleware Pipeline

The Request Pipeline: Logic flows through middleware layers.

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;

    public ExceptionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        try
        {
            await _next(httpContext);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(httpContext, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

        return context.Response.WriteAsync(new ErrorDetails()
        {
            StatusCode = context.Response.StatusCode,
            Message = "Internal Server Error from the custom middleware."
        }.ToString());
    }
}

3. Validation with FluentValidation

Data annotations (Attributes) can get messy. FluentValidation allows you to separate validation rules from your models, keeping your DTOs clean.

public class OrderValidator : AbstractValidator<OrderDto> 
{
    public OrderValidator() 
    {
        RuleFor(x => x.ProductName).NotEmpty();
        RuleFor(x => x.Amount).GreaterThan(0).WithMessage("Price must be positive");
    }
}

4. Use DTOs (Data Transfer Objects)

Never expose your database entities directly to the API client. It creates a tight coupling between your database schema and your public interface.

Scenario: You add a `PasswordHash` column to your User table. If you return the Entity directly, you just leaked everyone's password hashes!

Always map your Entites to DTOs. Libraries like AutoMapper or manual mapping work great.

Conclusion

Building clean APIs is about discipline. By separating concerns, implementing global error handling, and using DTOs, you create a codebase that your team will enjoy working on for years to come.

The Full Stack Guide: Connecting Angular with a .NET Backend

The Full Stack Guide: Connecting Angular with .NET ...