
Deep dive into production-grade API development patterns including dependency injection, middleware architecture, error handling, and implementing robust health checks.
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.
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.
ASP.NET Core's dependency injection is powerful but easy to misuse. I've refined my approach through numerous deployments.
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;
}
}
Middleware is where cross-cutting concerns get handled consistently across all endpoints.
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);
}
}
Kubernetes and load balancers rely on health checks to determine if your application is functioning.
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);
}
}
}
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.
Scaling ASP.NET Core Applications with Docker and Kubernetes: A Practical Guide
A comprehensive guide to containerizing ASP.NET Core applications, managing them with Docker Compose for development, and orchestrating production deployments with Kubernetes.
CI/CD Pipeline Automation: From Code Commit to Production in Minutes
Practical guide to implementing automated CI/CD pipelines using GitHub Actions, Docker, and cloud deployments for ASP.NET Core applications.