Blog
Oct 20, 2025 - 10 MIN READ
Building Resilient APIs: Advanced ASP.NET Core Patterns for Production

Building Resilient APIs: Advanced ASP.NET Core Patterns for Production

Deep dive into production-grade API development patterns including dependency injection, middleware architecture, error handling, and implementing robust health checks.

Your Name

Your Name

As a senior C# developer, I've learned that writing code that works locally is fundamentally different from building APIs that operate reliably in production. This article explores the advanced patterns and architectural decisions that transformed my APIs from fragile proof-of-concepts to resilient systems handling millions of requests.

The Foundation: Understanding Production Requirements

Production environments present challenges that don't exist in development. Your API must handle graceful degradation, recover from transient failures, process requests reliably, and provide visibility into its health and performance. Building for these requirements requires a systematic approach.

Phase 1: Dependency Injection and Configuration

ASP.NET Core's dependency injection is powerful but easy to misuse. I've refined my approach through numerous deployments.

Structuring Your Service Configuration

Rather than dumping all service registrations in Program.cs, I organize them into extension methods:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApplicationServices(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        // Data Access
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddScoped<IOrderRepository, OrderRepository>();

        // Business Logic
        services.AddScoped<IUserService, UserService>();
        services.AddScoped<IOrderService, OrderService>();

        // Infrastructure
        services.AddHttpClient<IExternalPaymentService, ExternalPaymentService>()
            .AddTransientHttpErrorPolicy()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: attempt => 
                    TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100));

        return services;
    }
}

Phase 2: Middleware Architecture and Error Handling

Middleware is where cross-cutting concerns get handled consistently across all endpoints.

Custom Error Handling Middleware

public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception exception)
        {
            _logger.LogError(
                exception,
                "An unhandled exception occurred. Path: {Path}, Method: {Method}",
                context.Request.Path,
                context.Request.Method);

            await HandleExceptionAsync(context, exception);
        }
    }

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

        var response = new ErrorResponse();

        switch (exception)
        {
            case ValidationException validationEx:
                context.Response.StatusCode = StatusCodes.Status400BadRequest;
                response = new ErrorResponse
                {
                    Message = "Validation failed",
                    Errors = validationEx.Errors
                };
                break;

            case NotFoundException notFoundEx:
                context.Response.StatusCode = StatusCodes.Status404NotFound;
                response = new ErrorResponse
                {
                    Message = notFoundEx.Message
                };
                break;

            default:
                context.Response.StatusCode = 
                    StatusCodes.Status500InternalServerError;
                response = new ErrorResponse
                {
                    Message = "An internal error occurred",
                    TraceId = context.TraceIdentifier
                };
                break;
        }

        return context.Response.WriteAsJsonAsync(response);
    }
}

Phase 3: Implementing Robust Health Checks

Kubernetes and load balancers rely on health checks to determine if your application is functioning.

Advanced Health Check Implementation

services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<RedisHealthCheck>("redis")
    .AddCheck<ExternalApiHealthCheck>("external_api");

public class DatabaseHealthCheck : IHealthCheck
{
    private readonly ApplicationDbContext _context;

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var canConnect = await _context.Database
                .CanConnectAsync(cancellationToken);

            if (!canConnect)
            {
                return HealthCheckResult.Unhealthy(
                    "Cannot connect to database");
            }

            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                exception: ex);
        }
    }
}

Conclusion

Building production-grade APIs is about more than writing code—it's about anticipating failure points and implementing defensive patterns. The architecture I've shared represents years of learning from production failures and outages.

Built with Nuxt UI • © 2025 Behnam Nouri