Stats
Actions
Tags
ASP.NET Core architecture patterns: Minimal API vs Controller-based, DI configuration, validation, error handling, caching, background services, Polly resilience, rate limiting, and observability.
How this skill is triggered — by the user, by Claude, or both
Slash command
/everything-claude-code:aspnetcore-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
ASP.NET Core architecture and API patterns for scalable, production-grade services.
ASP.NET Core architecture and API patterns for scalable, production-grade services.
// ✅ Endpoint groups for organization
var orders = app.MapGroup("/api/orders").RequireAuthorization();
orders.MapGet("/", async (IOrderService svc, CancellationToken ct) =>
Results.Ok(await svc.ListAsync(ct)));
orders.MapGet("/{id:int}", async (int id, IOrderService svc, CancellationToken ct) =>
{
var order = await svc.GetByIdAsync(id, ct);
return order is null ? Results.NotFound() : Results.Ok(order);
});
orders.MapPost("/", async (
[FromBody] CreateOrderRequest request,
IValidator<CreateOrderRequest> validator,
IOrderService svc,
CancellationToken ct) =>
{
var validation = await validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return Results.ValidationProblem(validation.ToDictionary());
var order = await svc.CreateAsync(request, ct);
return Results.Created($"/api/orders/{order.Id}", order);
});
[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrderService orderService) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<IReadOnlyList<OrderDto>>> List(
CancellationToken ct)
=> Ok(await orderService.ListAsync(ct));
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> Get(int id, CancellationToken ct)
{
var order = await orderService.GetByIdAsync(id, ct);
return order is null ? NotFound() : Ok(order);
}
[HttpPost]
public async Task<ActionResult<OrderDto>> Create(
[FromBody] CreateOrderRequest request, CancellationToken ct)
{
var order = await orderService.CreateAsync(request, ct);
return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
}
}
// ✅ Scoped: one per request (DbContext, repositories, services)
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();
// ✅ Singleton: shared state (caches, clients)
builder.Services.AddSingleton<IEmailClient, SendGridClient>();
// ✅ Transient: lightweight, stateless
builder.Services.AddTransient<IPasswordHasher, BCryptPasswordHasher>();
// ✅ Options pattern
builder.Services.Configure<PaymentOptions>(
builder.Configuration.GetSection("Payment"));
// ✅ Named HttpClient with Polly
builder.Services.AddHttpClient<IPaymentClient, StripeClient>(client =>
{
client.BaseAddress = new Uri(config["Stripe:BaseUrl"]!);
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddStandardResilienceHandler();
// ✅ FluentValidation for complex business rules
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).GreaterThan(0);
RuleFor(x => x.Items).NotEmpty()
.ForEach(item => item.ChildRules(r =>
{
r.RuleFor(i => i.ProductId).GreaterThan(0);
r.RuleFor(i => i.Quantity).InclusiveBetween(1, 100);
}));
}
}
// Register globally
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// ✅ RFC 7807 ProblemDetails
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
app.UseExceptionHandler();
app.UseStatusCodePages();
// Custom exception handler
public class GlobalExceptionHandler(IProblemDetailsService problemDetailsService)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context, Exception exception, CancellationToken ct)
{
var (status, title) = exception switch
{
NotFoundException => (404, "Resource not found"),
ValidationException => (400, "Validation failed"),
_ => (500, "An unexpected error occurred")
};
return await problemDetailsService.TryWriteAsync(new()
{
HttpContext = context,
ProblemDetails = { Status = status, Title = title },
Exception = exception
});
}
}
// ✅ IMemoryCache for single-instance
builder.Services.AddMemoryCache();
public class CachedOrderService(IOrderRepository repo, IMemoryCache cache)
{
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
=> await cache.GetOrCreateAsync($"order:{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return await repo.GetByIdAsync(id, ct);
});
}
// ✅ IDistributedCache with Redis for multi-instance
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = config["Redis:ConnectionString"]);
// ✅ BackgroundService for long-running work
public class OrderProcessingService(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessingService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await using var scope = scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingOrdersAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
builder.Services.AddHostedService<OrderProcessingService>();
// ✅ Standard resilience pipeline (retry + circuit breaker)
builder.Services.AddHttpClient<IPaymentClient, StripeClient>()
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
});
// ✅ Custom pipeline
builder.Services.AddResiliencePipeline("database", builder =>
{
builder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential
});
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiter.QueueLimit = 10;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
app.UseRateLimiter();
orders.RequireRateLimiting("api");
// ✅ OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter())
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddOtlpExporter());
// ✅ Health Checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>()
.AddRedis(config["Redis:ConnectionString"]!);
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new() { Predicate = check => check.Tags.Contains("ready") });
// ✅ Correct middleware order
app.UseExceptionHandler(); // 1. Catch all errors
app.UseHttpsRedirection(); // 2. Redirect HTTP
app.UseHsts(); // 3. HSTS headers
app.UseRouting(); // 4. Route matching
app.UseAuthentication(); // 5. Who are you?
app.UseAuthorization(); // 6. What can you do?
app.UseRateLimiter(); // 7. Rate limiting
app.MapControllers(); // 8. Dispatch
npx claudepluginhub jed1978/everything-claude-codeCreates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.