dotnet-aspnet-core
Build, debug, modernize, or review ASP.NET Core applications with correct hosting, middleware, security, configuration, logging, and deployment patterns on current .NET.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-aspnet-core
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
ASP.NET Core
Trigger On
- working on ASP.NET Core apps, services, or middleware
- changing auth, routing, configuration, hosting, or deployment behavior
- deciding between ASP.NET Core sub-stacks such as Blazor, Minimal APIs, or controller APIs
- debugging request pipeline issues
- modernizing legacy ASP.NET to ASP.NET Core
Documentation
- ASP.NET Core Overview
- ASP.NET Core Middleware
- ASP.NET Core Best Practices
- Configuration in ASP.NET Core
- Authentication and Authorization
References
- patterns.md - Detailed middleware patterns, security patterns, configuration patterns, DI patterns, error handling patterns, and logging patterns
- anti-patterns.md - Common ASP.NET Core mistakes including HttpClient misuse, async anti-patterns, configuration errors, DI issues, middleware ordering problems, and security vulnerabilities
Workflow
Detect the real hosting shape first:
- top-level
Program.csstructure - middleware order and registration
- auth model (Identity, JWT, OAuth, cookies)
- endpoint registrations and routing
- top-level
Follow the correct middleware order:
ExceptionHandler → HttpsRedirection → Static Files → Routing → CORS → Authentication → Authorization → Rate Limiting → Response Caching → Custom Middleware → EndpointsUse built-in patterns correctly:
- Prefer
IOptions<T>/IOptionsSnapshot<T>for configuration - Use
ILogger<T>for structured logging - Use
IHttpClientFactoryfor HTTP clients (nevernew HttpClient()) - Use
IHostedService/BackgroundServicefor background work
- Prefer
Route specialized work to specific skills:
- UI and components →
dotnet-blazor - Real-time →
dotnet-signalr - RPC →
dotnet-grpc - New HTTP APIs →
dotnet-minimal-apis(prefer unless controllers needed) - Controller APIs →
dotnet-web-api
- UI and components →
Validate with build, tests, and targeted endpoint checks.
Middleware Patterns
Correct Order Matters
var app = builder.Build();
app.UseExceptionHandler("/error"); // 1. Catch all exceptions
app.UseHsts(); // 2. Security headers
app.UseHttpsRedirection(); // 3. HTTPS redirect
app.UseStaticFiles(); // 4. Serve static files
app.UseRouting(); // 5. Route matching
app.UseCors(); // 6. CORS policy
app.UseAuthentication(); // 7. Who are you?
app.UseAuthorization(); // 8. Can you access?
app.UseRateLimiter(); // 9. Rate limiting
app.UseResponseCaching(); // 10. Response cache
app.MapControllers(); // 11. Endpoints
Custom Middleware Pattern
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
await _next(context);
_logger.LogInformation("Request {Path} completed in {Elapsed}ms",
context.Request.Path, sw.ElapsedMilliseconds);
}
}
Configuration Patterns
Strongly-Typed Options
// appsettings.json
{
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587
}
}
// Registration
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
// Usage
public class EmailService(IOptions<EmailSettings> options)
{
private readonly EmailSettings _settings = options.Value;
}
Environment-Based Configuration
builder.Configuration
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<Program>(optional: true);
Security Patterns
Authentication Setup
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
Authorization Policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("MinAge18", policy =>
policy.RequireClaim("Age", "18", "19", "20")); // simplified
});
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
new HttpClient() |
Socket exhaustion | IHttpClientFactory |
Sync-over-async (Task.Result) |
Thread pool starvation | await properly |
Storing secrets in appsettings.json |
Security risk | User Secrets, Key Vault |
| Catching all exceptions silently | Hides bugs | Use IExceptionHandler |
async void in middleware |
Crashes process | async Task |
| Missing HTTPS redirect | Security risk | UseHttpsRedirection() |
Performance Best Practices
- Use async/await everywhere — avoid sync blocking calls
- Pool DbContext properly — use scoped lifetime
- Enable response compression —
UseResponseCompression() - Use output caching —
UseOutputCache()for .NET 7+ - Profile with diagnostic tools — Visual Studio Diagnostic Tools, PerfView
- Avoid allocations in hot paths — use
Span<T>, pooling
Deliver
- production-credible ASP.NET Core code and config
- a clear request pipeline and hosting story
- verification that matches the affected endpoints and middleware
- security headers and HTTPS configured correctly
Validate
- middleware order is intentional and documented
- security and configuration changes are explicit
- endpoint behavior is covered by tests or smoke checks
- no blocking calls in async context
- secrets are not committed to source control
- health checks are implemented for production readiness
Files (dotnet-skills)
-
references
-
anti-patterns.md 26.8 KB
# ASP.NET Core Anti-Patterns Reference This document catalogs common mistakes and anti-patterns in ASP.NET Core development, with explanations of why they are problematic and how to fix them. ## Table of Contents - [HttpClient Anti-Patterns](#httpclient-anti-patterns) - [Async Anti-Patterns](#async-anti-patterns) - [Configuration and Secrets Anti-Patterns](#configuration-and-secrets-anti-patterns) - [Dependency Injection Anti-Patterns](#dependency-injection-anti-patterns) - [Middleware Anti-Patterns](#middleware-anti-patterns) - [Error Handling Anti-Patterns](#error-handling-anti-patterns) - [Security Anti-Patterns](#security-anti-patterns) - [Performance Anti-Patterns](#performance-anti-patterns) - [Controller Anti-Patterns](#controller-anti-patterns) - [Database and EF Core Anti-Patterns](#database-and-ef-core-anti-patterns) --- ## HttpClient Anti-Patterns ### Creating HttpClient Instances Directly **Anti-Pattern:** ```csharp public class WeatherService { public async Task<Weather> GetWeatherAsync(string city) { using var client = new HttpClient(); // BAD: Creates new socket each time var response = await client.GetAsync($"https://api.weather.com/{city}"); return await response.Content.ReadFromJsonAsync<Weather>(); } } ``` **Problem:** Creating `HttpClient` directly leads to socket exhaustion. Each `HttpClient` instance opens new TCP connections and disposing it does not immediately release sockets (they stay in TIME_WAIT state). **Correct Approach:** ```csharp // Registration builder.Services.AddHttpClient<IWeatherService, WeatherService>(client => { client.BaseAddress = new Uri("https://api.weather.com/"); }); // Service public class WeatherService : IWeatherService { private readonly HttpClient _client; public WeatherService(HttpClient client) { _client = client; } public async Task<Weather> GetWeatherAsync(string city) { var response = await _client.GetAsync(city); return await response.Content.ReadFromJsonAsync<Weather>(); } } ``` ### Using Static HttpClient Without Respecting DNS Changes **Anti-Pattern:** ```csharp public static class ApiClient { private static readonly HttpClient _client = new HttpClient { BaseAddress = new Uri("https://api.example.com/") }; public static async Task<string> GetDataAsync() { return await _client.GetStringAsync("data"); } } ``` **Problem:** Static HttpClient instances cache DNS indefinitely. If the server's IP address changes, the client will continue to use the old IP. **Correct Approach:** ```csharp // Use IHttpClientFactory which handles DNS rotation builder.Services.AddHttpClient("api", client => { client.BaseAddress = new Uri("https://api.example.com/"); }) .SetHandlerLifetime(TimeSpan.FromMinutes(5)); // Rotate handlers periodically ``` --- ## Async Anti-Patterns ### Sync-Over-Async (Blocking on Async Code) **Anti-Pattern:** ```csharp public class UserController : ControllerBase { [HttpGet("{id}")] public IActionResult GetUser(int id) { // BAD: Blocking on async code var user = _userService.GetUserAsync(id).Result; // Or equally bad: var user2 = _userService.GetUserAsync(id).GetAwaiter().GetResult(); // Or: var user3 = Task.Run(() => _userService.GetUserAsync(id)).Result; return Ok(user); } } ``` **Problem:** Blocking on async code can cause deadlocks (especially with synchronization contexts) and wastes thread pool threads. The thread waits instead of being released to handle other requests. **Correct Approach:** ```csharp [HttpGet("{id}")] public async Task<IActionResult> GetUser(int id) { var user = await _userService.GetUserAsync(id); return Ok(user); } ``` ### Async Void **Anti-Pattern:** ```csharp public class LoggingMiddleware { private readonly RequestDelegate _next; public LoggingMiddleware(RequestDelegate next) { _next = next; } public async void Invoke(HttpContext context) // BAD: async void { await LogRequestAsync(context); await _next(context); } } ``` **Problem:** `async void` methods cannot be awaited, exceptions cannot be caught, and if an exception occurs it will crash the process. **Correct Approach:** ```csharp public async Task InvokeAsync(HttpContext context) { await LogRequestAsync(context); await _next(context); } ``` ### Not Using ConfigureAwait in Library Code **Anti-Pattern (in library/shared code):** ```csharp public class DataService { public async Task<Data> GetDataAsync() { var result = await _repository.QueryAsync(); // May capture unnecessary context return ProcessData(result); } } ``` **Problem:** In library code, not using `ConfigureAwait(false)` captures the synchronization context unnecessarily, which can cause performance issues and potential deadlocks when called from UI applications. **Correct Approach (for library code):** ```csharp public async Task<Data> GetDataAsync() { var result = await _repository.QueryAsync().ConfigureAwait(false); return ProcessData(result); } ``` Note: In ASP.NET Core application code (controllers, services directly serving requests), this is less critical since ASP.NET Core does not have a synchronization context. ### Async Over Sync **Anti-Pattern:** ```csharp public Task<int> CalculateAsync(int value) { return Task.Run(() => Calculate(value)); // BAD: Wrapping sync in Task.Run } public Task SaveAsync(Data data) { Save(data); return Task.CompletedTask; // BAD: Fake async } ``` **Problem:** Wrapping synchronous code in `Task.Run` wastes thread pool threads. Returning `Task.CompletedTask` after sync work is misleading and provides no benefit. **Correct Approach:** ```csharp // Either keep it synchronous if there's nothing to await public int Calculate(int value) { return value * 2; } // Or use truly async operations public async Task SaveAsync(Data data) { await _dbContext.SaveChangesAsync(); } ``` --- ## Configuration and Secrets Anti-Patterns ### Storing Secrets in appsettings.json **Anti-Pattern:** ```json { "ConnectionStrings": { "Database": "Server=prod.db.com;User=admin;Password=SuperSecret123!" }, "Jwt": { "Key": "MySuperSecretKey12345" }, "ThirdPartyApi": { "ApiKey": "sk-live-xxxxxxxxxxxxx" } } ``` **Problem:** Secrets committed to source control are exposed to anyone with repository access. They persist in git history even if removed later. **Correct Approach:** ```csharp // Development: User Secrets // In terminal: dotnet user-secrets set "Jwt:Key" "development-key" // Production: Environment variables or secret stores builder.Configuration .AddEnvironmentVariables() .AddAzureKeyVault(new Uri("https://myvault.vault.azure.net/"), new DefaultAzureCredential()); // appsettings.json should only contain non-sensitive defaults { "ConnectionStrings": { "Database": "" // Placeholder, real value from env/vault } } ``` ### Hardcoding Configuration Values **Anti-Pattern:** ```csharp public class EmailService { public async Task SendAsync(string to, string subject, string body) { using var client = new SmtpClient("smtp.company.com", 587); // BAD: Hardcoded client.Credentials = new NetworkCredential("noreply@company.com", "password123"); // ... } } ``` **Correct Approach:** ```csharp public class EmailService { private readonly EmailSettings _settings; public EmailService(IOptions<EmailSettings> options) { _settings = options.Value; } public async Task SendAsync(string to, string subject, string body) { using var client = new SmtpClient(_settings.SmtpServer, _settings.Port); // ... } } ``` ### Reading Configuration in Constructor **Anti-Pattern:** ```csharp public class ReportService { private readonly string _connectionString; public ReportService(IConfiguration configuration) { // BAD: Reading raw configuration in constructor _connectionString = configuration["ConnectionStrings:Reports"]; } } ``` **Problem:** Bypasses the options pattern, no validation, harder to test, service knows about configuration structure. **Correct Approach:** ```csharp public class ReportService { private readonly ReportSettings _settings; public ReportService(IOptions<ReportSettings> options) { _settings = options.Value; } } ``` --- ## Dependency Injection Anti-Patterns ### Captive Dependency (Scoped in Singleton) **Anti-Pattern:** ```csharp builder.Services.AddSingleton<ICacheService, CacheService>(); builder.Services.AddScoped<IUserContext, HttpUserContext>(); public class CacheService : ICacheService { private readonly IUserContext _userContext; // BAD: Scoped in singleton public CacheService(IUserContext userContext) { _userContext = userContext; // This instance will be captured forever } } ``` **Problem:** A scoped service injected into a singleton is "captured" and reused for all requests, leading to stale data, wrong user context, and thread safety issues. **Correct Approach:** ```csharp public class CacheService : ICacheService { private readonly IServiceScopeFactory _scopeFactory; public CacheService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory) { using var scope = _scopeFactory.CreateScope(); var userContext = scope.ServiceProvider.GetRequiredService<IUserContext>(); // Use userContext safely within this scope } } ``` ### Service Locator Pattern **Anti-Pattern:** ```csharp public class OrderService { private readonly IServiceProvider _serviceProvider; public OrderService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task ProcessOrderAsync(Order order) { // BAD: Service locator hides dependencies var emailService = _serviceProvider.GetRequiredService<IEmailService>(); var inventoryService = _serviceProvider.GetRequiredService<IInventoryService>(); var paymentService = _serviceProvider.GetRequiredService<IPaymentService>(); } } ``` **Problem:** Hides dependencies, makes testing harder, class can request any service at any time, breaks explicit dependency declaration. **Correct Approach:** ```csharp public class OrderService { private readonly IEmailService _emailService; private readonly IInventoryService _inventoryService; private readonly IPaymentService _paymentService; public OrderService( IEmailService emailService, IInventoryService inventoryService, IPaymentService paymentService) { _emailService = emailService; _inventoryService = inventoryService; _paymentService = paymentService; } } ``` ### Circular Dependencies **Anti-Pattern:** ```csharp public class ServiceA { public ServiceA(ServiceB serviceB) { } } public class ServiceB { public ServiceB(ServiceA serviceA) { } // BAD: Circular dependency } ``` **Problem:** Container cannot resolve circular dependencies. This indicates a design problem. **Correct Approach:** ```csharp // Option 1: Extract shared logic into a third service public class SharedService { } public class ServiceA { public ServiceA(SharedService shared) { } } public class ServiceB { public ServiceB(SharedService shared) { } } // Option 2: Use lazy injection public class ServiceA { private readonly Lazy<ServiceB> _serviceB; public ServiceA(Lazy<ServiceB> serviceB) { _serviceB = serviceB; } } // Option 3: Use events/mediator pattern to decouple ``` --- ## Middleware Anti-Patterns ### Wrong Middleware Order **Anti-Pattern:** ```csharp var app = builder.Build(); app.MapControllers(); // BAD: Endpoints before auth app.UseAuthentication(); app.UseAuthorization(); app.UseRouting(); // BAD: Routing after endpoints app.UseExceptionHandler("/error"); // BAD: Exception handler last ``` **Problem:** Middleware order matters. Authentication before routing means route data is not available. Exception handler at the end will not catch exceptions from endpoints. **Correct Approach:** ```csharp app.UseExceptionHandler("/error"); // First - catches all exceptions app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); // After routing app.UseAuthorization(); // After authentication app.MapControllers(); // Last ``` ### Blocking Operations in Middleware **Anti-Pattern:** ```csharp public class ValidationMiddleware { private readonly RequestDelegate _next; public ValidationMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // BAD: Synchronous blocking read using var reader = new StreamReader(context.Request.Body); var body = reader.ReadToEnd(); // BAD: Blocking database call var isValid = _validator.Validate(body).Result; await _next(context); } } ``` **Correct Approach:** ```csharp public async Task InvokeAsync(HttpContext context) { context.Request.EnableBuffering(); using var reader = new StreamReader(context.Request.Body, leaveOpen: true); var body = await reader.ReadToEndAsync(); context.Request.Body.Position = 0; var isValid = await _validator.ValidateAsync(body); await _next(context); } ``` ### Not Calling Next Unintentionally **Anti-Pattern:** ```csharp public async Task InvokeAsync(HttpContext context) { if (SomeCondition(context)) { await _next(context); } // BAD: If condition is false, pipeline stops silently } ``` **Problem:** Pipeline stops without response, leading to hanging requests or empty responses. **Correct Approach:** ```csharp public async Task InvokeAsync(HttpContext context) { if (!SomeCondition(context)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsJsonAsync(new { error = "Validation failed" }); return; // Explicit short-circuit with response } await _next(context); } ``` --- ## Error Handling Anti-Patterns ### Swallowing Exceptions **Anti-Pattern:** ```csharp public async Task<Order> GetOrderAsync(int id) { try { return await _repository.GetByIdAsync(id); } catch (Exception) { return null; // BAD: Exception swallowed, bug hidden } } ``` **Problem:** Hides bugs, makes debugging impossible, caller does not know something went wrong. **Correct Approach:** ```csharp public async Task<Order?> GetOrderAsync(int id) { try { return await _repository.GetByIdAsync(id); } catch (SqlException ex) when (ex.Number == 2) // Specific exception { _logger.LogWarning(ex, "Database timeout while getting order {OrderId}", id); throw new ServiceUnavailableException("Database temporarily unavailable", ex); } } ``` ### Exposing Exception Details to Users **Anti-Pattern:** ```csharp [HttpGet("{id}")] public async Task<IActionResult> GetUser(int id) { try { return Ok(await _userService.GetByIdAsync(id)); } catch (Exception ex) { // BAD: Stack trace and internal details exposed return StatusCode(500, new { error = ex.Message, stackTrace = ex.StackTrace, innerException = ex.InnerException?.Message }); } } ``` **Problem:** Exposes internal system details, potential security vulnerability, helps attackers understand system internals. **Correct Approach:** ```csharp // Use global exception handler with environment-aware responses public class GlobalExceptionHandler : IExceptionHandler { private readonly IHostEnvironment _environment; private readonly ILogger<GlobalExceptionHandler> _logger; public async ValueTask<bool> TryHandleAsync( HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { _logger.LogError(exception, "Unhandled exception"); var problemDetails = new ProblemDetails { Status = StatusCodes.Status500InternalServerError, Title = "An error occurred", Detail = _environment.IsDevelopment() ? exception.Message : null }; await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); return true; } } ``` ### Catch-All Without Logging **Anti-Pattern:** ```csharp try { await ProcessPaymentAsync(order); } catch (Exception) { // BAD: No logging, no context throw; } ``` **Correct Approach:** ```csharp try { await ProcessPaymentAsync(order); } catch (PaymentException ex) { _logger.LogWarning(ex, "Payment failed for order {OrderId}: {Reason}", order.Id, ex.Reason); throw; } catch (Exception ex) { _logger.LogError(ex, "Unexpected error processing payment for order {OrderId}", order.Id); throw; } ``` --- ## Security Anti-Patterns ### Missing HTTPS Redirection **Anti-Pattern:** ```csharp var app = builder.Build(); app.MapControllers(); // BAD: No HTTPS enforcement app.Run(); ``` **Correct Approach:** ```csharp if (!app.Environment.IsDevelopment()) { app.UseHsts(); } app.UseHttpsRedirection(); app.MapControllers(); ``` ### Disabled CORS (Allow All) **Anti-Pattern:** ```csharp builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() // BAD: Any site can access .AllowAnyMethod() .AllowAnyHeader(); }); }); ``` **Correct Approach:** ```csharp builder.Services.AddCors(options => { options.AddPolicy("Production", policy => { policy.WithOrigins( "https://app.example.com", "https://admin.example.com") .WithMethods("GET", "POST", "PUT", "DELETE") .WithHeaders("Authorization", "Content-Type") .AllowCredentials(); }); }); ``` ### SQL Injection via String Concatenation **Anti-Pattern:** ```csharp public async Task<User> GetUserAsync(string username) { // BAD: SQL injection vulnerability var sql = $"SELECT * FROM Users WHERE Username = '{username}'"; return await _context.Users.FromSqlRaw(sql).FirstOrDefaultAsync(); } ``` **Correct Approach:** ```csharp public async Task<User> GetUserAsync(string username) { // Parameterized query return await _context.Users .FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}") .FirstOrDefaultAsync(); // Or use LINQ return await _context.Users .Where(u => u.Username == username) .FirstOrDefaultAsync(); } ``` ### Missing Authorization on Sensitive Endpoints **Anti-Pattern:** ```csharp [HttpDelete("{id}")] public async Task<IActionResult> DeleteUser(int id) // BAD: No authorization { await _userService.DeleteAsync(id); return NoContent(); } ``` **Correct Approach:** ```csharp [Authorize(Policy = "AdminOnly")] [HttpDelete("{id}")] public async Task<IActionResult> DeleteUser(int id) { await _userService.DeleteAsync(id); return NoContent(); } ``` ### Storing Passwords in Plain Text **Anti-Pattern:** ```csharp public async Task<User> CreateUserAsync(string email, string password) { var user = new User { Email = email, Password = password // BAD: Plain text password }; await _context.SaveChangesAsync(); return user; } ``` **Correct Approach:** ```csharp // Use ASP.NET Core Identity or proper password hashing public async Task<User> CreateUserAsync(string email, string password) { var user = new User { Email = email, PasswordHash = _passwordHasher.HashPassword(null, password) }; await _context.SaveChangesAsync(); return user; } ``` --- ## Performance Anti-Patterns ### Not Using Async I/O **Anti-Pattern:** ```csharp [HttpGet] public IActionResult GetReport() { // BAD: Synchronous database call blocks thread var data = _context.Reports.ToList(); var file = File.ReadAllBytes("template.pdf"); // BAD: Sync file I/O return Ok(GenerateReport(data, file)); } ``` **Correct Approach:** ```csharp [HttpGet] public async Task<IActionResult> GetReport() { var data = await _context.Reports.ToListAsync(); var file = await File.ReadAllBytesAsync("template.pdf"); return Ok(GenerateReport(data, file)); } ``` ### Loading Entire Collections into Memory **Anti-Pattern:** ```csharp public async Task<decimal> GetTotalRevenueAsync() { // BAD: Loads all orders into memory var orders = await _context.Orders.ToListAsync(); return orders.Sum(o => o.Total); } ``` **Correct Approach:** ```csharp public async Task<decimal> GetTotalRevenueAsync() { // Computed in database return await _context.Orders.SumAsync(o => o.Total); } ``` ### N+1 Query Problem **Anti-Pattern:** ```csharp public async Task<List<OrderDto>> GetOrdersAsync() { var orders = await _context.Orders.ToListAsync(); return orders.Select(o => new OrderDto { Id = o.Id, // BAD: Each access triggers a separate query CustomerName = o.Customer.Name, Items = o.Items.Select(i => i.Name).ToList() }).ToList(); } ``` **Correct Approach:** ```csharp public async Task<List<OrderDto>> GetOrdersAsync() { return await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .Select(o => new OrderDto { Id = o.Id, CustomerName = o.Customer.Name, Items = o.Items.Select(i => i.Name).ToList() }) .ToListAsync(); } ``` ### Not Using Response Compression **Anti-Pattern:** ```csharp var app = builder.Build(); app.MapControllers(); // BAD: Large responses sent uncompressed ``` **Correct Approach:** ```csharp builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add<BrotliCompressionProvider>(); options.Providers.Add<GzipCompressionProvider>(); }); builder.Services.Configure<BrotliCompressionProviderOptions>(options => { options.Level = CompressionLevel.Fastest; }); app.UseResponseCompression(); app.MapControllers(); ``` --- ## Controller Anti-Patterns ### Fat Controllers **Anti-Pattern:** ```csharp [HttpPost] public async Task<IActionResult> CreateOrder(CreateOrderRequest request) { // BAD: Business logic in controller if (request.Items.Count == 0) return BadRequest("Order must have items"); var customer = await _context.Customers.FindAsync(request.CustomerId); if (customer == null) return NotFound("Customer not found"); var order = new Order { CustomerId = request.CustomerId }; foreach (var item in request.Items) { var product = await _context.Products.FindAsync(item.ProductId); if (product == null) return BadRequest($"Product {item.ProductId} not found"); if (product.Stock < item.Quantity) return BadRequest($"Insufficient stock for {product.Name}"); product.Stock -= item.Quantity; order.Items.Add(new OrderItem { ProductId = product.Id, Quantity = item.Quantity, Price = product.Price }); } order.Total = order.Items.Sum(i => i.Price * i.Quantity); _context.Orders.Add(order); await _context.SaveChangesAsync(); await _emailService.SendOrderConfirmationAsync(customer.Email, order); return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order); } ``` **Correct Approach:** ```csharp [HttpPost] public async Task<IActionResult> CreateOrder(CreateOrderRequest request) { var result = await _orderService.CreateOrderAsync(request); return result.Match<IActionResult>( success => CreatedAtAction(nameof(GetOrder), new { id = success.OrderId }, success), notFound => NotFound(notFound.Message), validation => BadRequest(validation.Errors)); } ``` ### Not Using Action Filters **Anti-Pattern:** ```csharp [HttpPost] public async Task<IActionResult> Create(CreateRequest request) { // BAD: Manual validation in every action if (!ModelState.IsValid) { return BadRequest(ModelState); } // ... } ``` **Correct Approach:** ```csharp // Configure globally builder.Services.Configure<ApiBehaviorOptions>(options => { options.SuppressModelStateInvalidFilter = false; // Enable automatic validation }); // Or use a filter public class ValidateModelAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } } ``` --- ## Database and EF Core Anti-Patterns ### Using DbContext as Singleton **Anti-Pattern:** ```csharp builder.Services.AddSingleton<MyDbContext>(); // BAD: DbContext is not thread-safe ``` **Problem:** `DbContext` is not thread-safe and tracks entities. Using it as singleton causes concurrency issues and memory leaks. **Correct Approach:** ```csharp builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString)); // Default is scoped lifetime ``` ### Not Disposing DbContext (Manual Creation) **Anti-Pattern:** ```csharp public class ReportService { private readonly MyDbContext _context = new MyDbContext(); // BAD: Never disposed } ``` **Correct Approach:** ```csharp public class ReportService { private readonly MyDbContext _context; public ReportService(MyDbContext context) { _context = context; // Injected, container manages lifetime } } ``` ### Tracking Entities When Not Needed **Anti-Pattern:** ```csharp public async Task<List<ProductDto>> GetProductsAsync() { // BAD: Tracking enabled for read-only query var products = await _context.Products.ToListAsync(); return products.Select(p => new ProductDto { Name = p.Name }).ToList(); } ``` **Correct Approach:** ```csharp public async Task<List<ProductDto>> GetProductsAsync() { return await _context.Products .AsNoTracking() .Select(p => new ProductDto { Name = p.Name }) .ToListAsync(); } ``` ### Lazy Loading in Web Applications **Anti-Pattern:** ```csharp builder.Services.AddDbContext<MyDbContext>(options => options.UseLazyLoadingProxies() // BAD: Hidden N+1 queries .UseSqlServer(connectionString)); ``` **Problem:** Lazy loading causes unexpected database queries, N+1 problems, and makes it hard to track what data is being loaded. **Correct Approach:** ```csharp // Use explicit loading with Include/ThenInclude var orders = await _context.Orders .Include(o => o.Customer) .Include(o => o.Items) .ThenInclude(i => i.Product) .ToListAsync(); // Or use projection var orderDtos = await _context.Orders .Select(o => new OrderDto { Id = o.Id, CustomerName = o.Customer.Name }) .ToListAsync(); ``` -
patterns.md 29.3 KB
# ASP.NET Core Patterns Reference This document provides detailed patterns for middleware, security, and configuration in ASP.NET Core applications. ## Table of Contents - [Middleware Patterns](#middleware-patterns) - [Security Patterns](#security-patterns) - [Configuration Patterns](#configuration-patterns) - [Dependency Injection Patterns](#dependency-injection-patterns) - [Error Handling Patterns](#error-handling-patterns) - [Logging Patterns](#logging-patterns) --- ## Middleware Patterns ### Pipeline Architecture The ASP.NET Core request pipeline consists of middleware components that process requests in sequence. Each middleware can: - Handle the request and short-circuit the pipeline - Pass the request to the next middleware - Execute code before and after the next middleware ### Canonical Middleware Order ```csharp var app = builder.Build(); // 1. Exception handling - must be first to catch all exceptions app.UseExceptionHandler("/error"); // 2. HSTS - HTTP Strict Transport Security header app.UseHsts(); // 3. HTTPS redirection - before any content is served app.UseHttpsRedirection(); // 4. Static files - serve before routing for performance app.UseStaticFiles(); // 5. Routing - must precede authentication/authorization app.UseRouting(); // 6. CORS - after routing, before auth app.UseCors("PolicyName"); // 7. Authentication - identify the user app.UseAuthentication(); // 8. Authorization - verify access rights app.UseAuthorization(); // 9. Rate limiting - protect endpoints app.UseRateLimiter(); // 10. Response caching - cache authorized responses app.UseResponseCaching(); // 11. Output caching (.NET 7+) - server-side caching app.UseOutputCache(); // 12. Custom middleware - business logic app.UseMiddleware<CustomMiddleware>(); // 13. Endpoints - terminal middleware app.MapControllers(); app.MapRazorPages(); app.MapBlazorHub(); ``` ### Class-Based Middleware Pattern ```csharp public class RequestTimingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestTimingMiddleware> _logger; public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var stopwatch = Stopwatch.StartNew(); try { await _next(context); } finally { stopwatch.Stop(); _logger.LogInformation( "Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}", context.Request.Method, context.Request.Path, stopwatch.ElapsedMilliseconds, context.Response.StatusCode); } } } // Extension method for clean registration public static class RequestTimingMiddlewareExtensions { public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder builder) { return builder.UseMiddleware<RequestTimingMiddleware>(); } } ``` ### Convention-Based Middleware with Dependencies ```csharp public class TenantResolutionMiddleware { private readonly RequestDelegate _next; public TenantResolutionMiddleware(RequestDelegate next) { _next = next; } // Scoped services can be injected into InvokeAsync public async Task InvokeAsync( HttpContext context, ITenantService tenantService, ILogger<TenantResolutionMiddleware> logger) { var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); if (string.IsNullOrEmpty(tenantId)) { context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsJsonAsync(new { error = "Tenant header required" }); return; } var tenant = await tenantService.GetTenantAsync(tenantId); if (tenant is null) { context.Response.StatusCode = StatusCodes.Status404NotFound; await context.Response.WriteAsJsonAsync(new { error = "Tenant not found" }); return; } context.Items["Tenant"] = tenant; logger.LogDebug("Resolved tenant {TenantId}", tenantId); await _next(context); } } ``` ### Inline Middleware for Simple Cases ```csharp // Use for simple, non-reusable middleware app.Use(async (context, next) => { context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); await next(context); }); // Terminal middleware (does not call next) app.Map("/health", app => app.Run(async context => { context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new { status = "healthy" }); })); ``` ### Conditional Middleware ```csharp // Apply middleware only for specific paths app.UseWhen( context => context.Request.Path.StartsWithSegments("/api"), appBuilder => appBuilder.UseMiddleware<ApiLoggingMiddleware>()); // Branch the pipeline app.MapWhen( context => context.Request.Query.ContainsKey("debug"), appBuilder => appBuilder.UseMiddleware<DebugMiddleware>()); ``` ### Short-Circuiting Middleware ```csharp public class MaintenanceModeMiddleware { private readonly RequestDelegate _next; private readonly IOptions<MaintenanceOptions> _options; public MaintenanceModeMiddleware(RequestDelegate next, IOptions<MaintenanceOptions> options) { _next = next; _options = options; } public async Task InvokeAsync(HttpContext context) { if (_options.Value.IsEnabled) { context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; context.Response.Headers.RetryAfter = _options.Value.RetryAfterSeconds.ToString(); await context.Response.WriteAsJsonAsync(new { message = "Service under maintenance", estimatedReturn = _options.Value.EstimatedReturn }); return; // Short-circuit - do not call next } await _next(context); } } ``` --- ## Security Patterns ### JWT Authentication Setup ```csharp builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)), ClockSkew = TimeSpan.FromMinutes(1) // Reduce default 5-minute skew }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception is SecurityTokenExpiredException) { context.Response.Headers.Append("Token-Expired", "true"); } return Task.CompletedTask; }, OnTokenValidated = context => { // Add custom claims or perform additional validation return Task.CompletedTask; } }; }); ``` ### Cookie Authentication with Security Best Practices ```csharp builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.Cookie.Name = "AppAuth"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromHours(8); options.SlidingExpiration = true; options.LoginPath = "/auth/login"; options.LogoutPath = "/auth/logout"; options.AccessDeniedPath = "/auth/access-denied"; options.Events = new CookieAuthenticationEvents { OnValidatePrincipal = async context => { // Validate user still exists and is active var userService = context.HttpContext.RequestServices .GetRequiredService<IUserService>(); var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (userId is null || !await userService.IsUserActiveAsync(userId)) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync(); } } }; }); ``` ### Policy-Based Authorization ```csharp builder.Services.AddAuthorization(options => { // Simple role-based policy options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); // Claim-based policy options.AddPolicy("VerifiedEmail", policy => policy.RequireClaim("email_verified", "true")); // Multiple requirements (AND) options.AddPolicy("SeniorAdmin", policy => policy.RequireRole("Admin") .RequireClaim("experience_years", "5", "6", "7", "8", "9", "10")); // Custom requirement options.AddPolicy("MinimumAge", policy => policy.Requirements.Add(new MinimumAgeRequirement(18))); // Resource-based authorization setup options.AddPolicy("DocumentOwner", policy => policy.Requirements.Add(new DocumentOwnerRequirement())); }); // Custom requirement public class MinimumAgeRequirement : IAuthorizationRequirement { public int MinimumAge { get; } public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge; } // Handler for custom requirement public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumAgeRequirement requirement) { var birthDateClaim = context.User.FindFirst("birth_date"); if (birthDateClaim is null) { return Task.CompletedTask; } if (DateTime.TryParse(birthDateClaim.Value, out var birthDate)) { var age = DateTime.Today.Year - birthDate.Year; if (birthDate.Date > DateTime.Today.AddYears(-age)) age--; if (age >= requirement.MinimumAge) { context.Succeed(requirement); } } return Task.CompletedTask; } } // Register handler builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>(); ``` ### Resource-Based Authorization ```csharp public class DocumentOwnerRequirement : IAuthorizationRequirement { } public class DocumentOwnerHandler : AuthorizationHandler<DocumentOwnerRequirement, Document> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, DocumentOwnerRequirement requirement, Document resource) { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (userId == resource.OwnerId) { context.Succeed(requirement); } return Task.CompletedTask; } } // Usage in controller public class DocumentsController : ControllerBase { private readonly IAuthorizationService _authorizationService; public DocumentsController(IAuthorizationService authorizationService) { _authorizationService = authorizationService; } [HttpPut("{id}")] public async Task<IActionResult> Update(int id, DocumentUpdateDto dto) { var document = await _repository.GetByIdAsync(id); if (document is null) return NotFound(); var authResult = await _authorizationService.AuthorizeAsync( User, document, "DocumentOwner"); if (!authResult.Succeeded) { return Forbid(); } // Proceed with update return Ok(); } } ``` ### Security Headers Middleware ```csharp public class SecurityHeadersMiddleware { private readonly RequestDelegate _next; public SecurityHeadersMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // Prevent MIME type sniffing context.Response.Headers.XContentTypeOptions = "nosniff"; // Prevent clickjacking context.Response.Headers.XFrameOptions = "DENY"; // XSS protection (legacy browsers) context.Response.Headers["X-XSS-Protection"] = "1; mode=block"; // Referrer policy context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin"; // Permissions policy context.Response.Headers["Permissions-Policy"] = "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"; // Content Security Policy (customize based on your needs) context.Response.Headers.ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;"; await _next(context); } } ``` ### CORS Configuration ```csharp builder.Services.AddCors(options => { // Named policy for specific origins options.AddPolicy("AllowSpecificOrigins", policy => { policy.WithOrigins( "https://app.example.com", "https://admin.example.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); // Policy for API clients options.AddPolicy("ApiClients", policy => { policy.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()!) .WithHeaders("Authorization", "Content-Type", "X-Requested-With") .WithMethods("GET", "POST", "PUT", "DELETE") .SetPreflightMaxAge(TimeSpan.FromMinutes(10)); }); }); // Apply globally or per-endpoint app.UseCors("AllowSpecificOrigins"); // Or per-controller [EnableCors("ApiClients")] public class ApiController : ControllerBase { } ``` ### Rate Limiting (.NET 7+) ```csharp builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; // Fixed window limiter options.AddFixedWindowLimiter("fixed", opt => { opt.Window = TimeSpan.FromMinutes(1); opt.PermitLimit = 100; opt.QueueLimit = 10; opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; }); // Sliding window limiter options.AddSlidingWindowLimiter("sliding", opt => { opt.Window = TimeSpan.FromMinutes(1); opt.SegmentsPerWindow = 6; opt.PermitLimit = 100; }); // Token bucket limiter options.AddTokenBucketLimiter("token", opt => { opt.TokenLimit = 100; opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10); opt.TokensPerPeriod = 10; }); // Per-user rate limiting options.AddPolicy("per-user", context => { var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: userId, factory: _ => new FixedWindowRateLimiterOptions { Window = TimeSpan.FromMinutes(1), PermitLimit = 60 }); }); options.OnRejected = async (context, token) => { context.HttpContext.Response.ContentType = "application/json"; await context.HttpContext.Response.WriteAsJsonAsync(new { error = "Too many requests", retryAfter = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter) ? retryAfter.TotalSeconds : 60 }, token); }; }); app.UseRateLimiter(); // Apply to endpoints app.MapGet("/api/data", () => "data") .RequireRateLimiting("per-user"); ``` --- ## Configuration Patterns ### Strongly-Typed Options with Validation ```csharp public class EmailSettings { public const string SectionName = "Email"; [Required] public string SmtpServer { get; set; } = string.Empty; [Range(1, 65535)] public int Port { get; set; } = 587; [Required, EmailAddress] public string FromAddress { get; set; } = string.Empty; public bool UseSsl { get; set; } = true; [Range(1, 300)] public int TimeoutSeconds { get; set; } = 30; } // Registration with validation builder.Services.AddOptions<EmailSettings>() .Bind(builder.Configuration.GetSection(EmailSettings.SectionName)) .ValidateDataAnnotations() .ValidateOnStart(); // Fail fast on startup if invalid // Custom validation builder.Services.AddOptions<DatabaseSettings>() .Bind(builder.Configuration.GetSection("Database")) .Validate(settings => { return !string.IsNullOrEmpty(settings.ConnectionString) && settings.MaxPoolSize >= settings.MinPoolSize; }, "Database configuration is invalid") .ValidateOnStart(); ``` ### Options Interfaces Usage ```csharp public class EmailService { // IOptions<T> - singleton, read once at startup public EmailService(IOptions<EmailSettings> options) { var settings = options.Value; } } public class NotificationService { // IOptionsSnapshot<T> - scoped, re-reads config per request public NotificationService(IOptionsSnapshot<EmailSettings> options) { var settings = options.Value; } } public class BackgroundEmailProcessor : BackgroundService { // IOptionsMonitor<T> - singleton with change notifications private readonly IOptionsMonitor<EmailSettings> _optionsMonitor; private EmailSettings _currentSettings; public BackgroundEmailProcessor(IOptionsMonitor<EmailSettings> optionsMonitor) { _optionsMonitor = optionsMonitor; _currentSettings = optionsMonitor.CurrentValue; _optionsMonitor.OnChange(settings => { _currentSettings = settings; // Handle configuration change }); } } ``` ### Multi-Environment Configuration ```csharp var builder = WebApplication.CreateBuilder(args); // Configuration sources (later sources override earlier ones) builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true) .AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true) // Git-ignored local overrides .AddEnvironmentVariables() .AddCommandLine(args); // Development-only sources if (builder.Environment.IsDevelopment()) { builder.Configuration.AddUserSecrets<Program>(); } // Production secret stores if (builder.Environment.IsProduction()) { builder.Configuration.AddAzureKeyVault( new Uri($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/"), new DefaultAzureCredential()); } ``` ### Named Options Pattern ```csharp // appsettings.json { "Storage": { "Primary": { "ConnectionString": "...", "ContainerName": "primary" }, "Backup": { "ConnectionString": "...", "ContainerName": "backup" } } } // Registration builder.Services.Configure<StorageSettings>("Primary", builder.Configuration.GetSection("Storage:Primary")); builder.Services.Configure<StorageSettings>("Backup", builder.Configuration.GetSection("Storage:Backup")); // Usage with IOptionsSnapshot public class StorageService { private readonly StorageSettings _primarySettings; private readonly StorageSettings _backupSettings; public StorageService(IOptionsSnapshot<StorageSettings> options) { _primarySettings = options.Get("Primary"); _backupSettings = options.Get("Backup"); } } ``` ### Post-Configure and Configure All ```csharp // Post-configure runs after all Configure calls builder.Services.PostConfigure<EmailSettings>(settings => { // Apply defaults or computed values if (string.IsNullOrEmpty(settings.FromAddress)) { settings.FromAddress = "noreply@example.com"; } }); // Configure all instances of an options type builder.Services.ConfigureAll<StorageSettings>(settings => { settings.TimeoutSeconds = 30; // Apply to all named instances }); ``` --- ## Dependency Injection Patterns ### Service Lifetimes ```csharp // Singleton - one instance for application lifetime builder.Services.AddSingleton<ICacheService, MemoryCacheService>(); // Scoped - one instance per HTTP request builder.Services.AddScoped<IUserContext, HttpUserContext>(); builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>(); // Transient - new instance every time builder.Services.AddTransient<IEmailBuilder, EmailBuilder>(); ``` ### Factory Pattern Registration ```csharp // Simple factory builder.Services.AddScoped<IPaymentProcessor>(sp => { var config = sp.GetRequiredService<IOptions<PaymentSettings>>().Value; return config.Provider switch { "Stripe" => new StripePaymentProcessor(config), "PayPal" => new PayPalPaymentProcessor(config), _ => throw new InvalidOperationException($"Unknown provider: {config.Provider}") }; }); // Named factory with keyed services (.NET 8+) builder.Services.AddKeyedScoped<IPaymentProcessor, StripePaymentProcessor>("stripe"); builder.Services.AddKeyedScoped<IPaymentProcessor, PayPalPaymentProcessor>("paypal"); // Usage public class CheckoutService([FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor) { // ... } ``` ### Decorator Pattern ```csharp // Manual decoration builder.Services.AddScoped<IRepository, SqlRepository>(); builder.Services.Decorate<IRepository, CachingRepository>(); builder.Services.Decorate<IRepository, LoggingRepository>(); // Extension method implementation public static class ServiceCollectionExtensions { public static IServiceCollection Decorate<TInterface, TDecorator>( this IServiceCollection services) where TDecorator : TInterface { var descriptor = services.Single(d => d.ServiceType == typeof(TInterface)); services.Add(new ServiceDescriptor( typeof(TInterface), sp => { var inner = (TInterface)ActivatorUtilities.CreateInstance( sp, descriptor.ImplementationType!); return ActivatorUtilities.CreateInstance<TDecorator>(sp, inner); }, descriptor.Lifetime)); services.Remove(descriptor); return services; } } ``` ### HttpClientFactory Patterns ```csharp // Typed client builder.Services.AddHttpClient<IGitHubClient, GitHubClient>(client => { client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0"); }) .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(GetCircuitBreakerPolicy()); // Named client builder.Services.AddHttpClient("weather", client => { client.BaseAddress = new Uri("https://api.weather.com/"); client.Timeout = TimeSpan.FromSeconds(30); }); // Resilience policies with Polly static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); } static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)); } ``` --- ## Error Handling Patterns ### Global Exception Handler (.NET 8+) ```csharp public class GlobalExceptionHandler : IExceptionHandler { private readonly ILogger<GlobalExceptionHandler> _logger; private readonly IHostEnvironment _environment; public GlobalExceptionHandler( ILogger<GlobalExceptionHandler> logger, IHostEnvironment environment) { _logger = logger; _environment = environment; } public async ValueTask<bool> TryHandleAsync( HttpContext httpContext, Exception exception, CancellationToken cancellationToken) { var (statusCode, title) = exception switch { ValidationException => (StatusCodes.Status400BadRequest, "Validation Error"), NotFoundException => (StatusCodes.Status404NotFound, "Not Found"), UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"), ForbiddenException => (StatusCodes.Status403Forbidden, "Forbidden"), ConflictException => (StatusCodes.Status409Conflict, "Conflict"), _ => (StatusCodes.Status500InternalServerError, "Internal Server Error") }; _logger.LogError(exception, "Exception occurred: {Message}", exception.Message); var problemDetails = new ProblemDetails { Status = statusCode, Title = title, Instance = httpContext.Request.Path, Detail = _environment.IsDevelopment() ? exception.Message : null }; if (exception is ValidationException validationException) { problemDetails.Extensions["errors"] = validationException.Errors; } httpContext.Response.StatusCode = statusCode; await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); return true; } } // Registration builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); builder.Services.AddProblemDetails(); app.UseExceptionHandler(); ``` ### Problem Details Configuration ```csharp builder.Services.AddProblemDetails(options => { options.CustomizeProblemDetails = context => { context.ProblemDetails.Instance = context.HttpContext.Request.Path; context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier; context.ProblemDetails.Extensions["timestamp"] = DateTime.UtcNow; if (context.HttpContext.RequestServices.GetService<IHostEnvironment>()?.IsDevelopment() == true) { context.ProblemDetails.Extensions["machineName"] = Environment.MachineName; } }; }); ``` --- ## Logging Patterns ### Structured Logging with Serilog ```csharp builder.Host.UseSerilog((context, loggerConfig) => { loggerConfig .ReadFrom.Configuration(context.Configuration) .Enrich.FromLogContext() .Enrich.WithMachineName() .Enrich.WithEnvironmentName() .Enrich.WithProperty("Application", "MyApp") .WriteTo.Console(new JsonFormatter()) .WriteTo.Seq(context.Configuration["Seq:ServerUrl"]!); }); // Request logging middleware app.UseSerilogRequestLogging(options => { options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms"; options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); diagnosticContext.Set("UserId", httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value); }; }); ``` ### High-Performance Logging with Source Generators ```csharp public static partial class LogMessages { [LoggerMessage( EventId = 1001, Level = LogLevel.Information, Message = "Processing order {OrderId} for customer {CustomerId}")] public static partial void ProcessingOrder( ILogger logger, string orderId, string customerId); [LoggerMessage( EventId = 1002, Level = LogLevel.Warning, Message = "Order {OrderId} processing delayed, retry attempt {Attempt}")] public static partial void OrderProcessingDelayed( ILogger logger, string orderId, int attempt); [LoggerMessage( EventId = 1003, Level = LogLevel.Error, Message = "Failed to process order {OrderId}")] public static partial void OrderProcessingFailed( ILogger logger, string orderId, Exception exception); } // Usage public class OrderProcessor { private readonly ILogger<OrderProcessor> _logger; public OrderProcessor(ILogger<OrderProcessor> logger) { _logger = logger; } public async Task ProcessAsync(Order order) { LogMessages.ProcessingOrder(_logger, order.Id, order.CustomerId); try { // Process order } catch (Exception ex) { LogMessages.OrderProcessingFailed(_logger, order.Id, ex); throw; } } } ``` ### Correlation and Scoped Logging ```csharp public class CorrelationIdMiddleware { private readonly RequestDelegate _next; public CorrelationIdMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger) { var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString(); context.Response.Headers["X-Correlation-Id"] = correlationId; using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId, ["RequestPath"] = context.Request.Path.ToString() })) { await _next(context); } } } ```
-
-
SKILL.md 7.2 KB
--- name: dotnet-aspnet-core version: "1.0.0" category: "Web" description: "Build, debug, modernize, or review ASP.NET Core applications with correct hosting, middleware, security, configuration, logging, and deployment patterns on current .NET." compatibility: "Requires an ASP.NET Core project or solution." --- # ASP.NET Core ## Trigger On - working on ASP.NET Core apps, services, or middleware - changing auth, routing, configuration, hosting, or deployment behavior - deciding between ASP.NET Core sub-stacks such as Blazor, Minimal APIs, or controller APIs - debugging request pipeline issues - modernizing legacy ASP.NET to ASP.NET Core ## Documentation - [ASP.NET Core Overview](https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-10.0) - [ASP.NET Core Middleware](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-10.0) - [ASP.NET Core Best Practices](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices?view=aspnetcore-10.0) - [Configuration in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-10.0) - [Authentication and Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-10.0) ### References - [patterns.md](references/patterns.md) - Detailed middleware patterns, security patterns, configuration patterns, DI patterns, error handling patterns, and logging patterns - [anti-patterns.md](references/anti-patterns.md) - Common ASP.NET Core mistakes including HttpClient misuse, async anti-patterns, configuration errors, DI issues, middleware ordering problems, and security vulnerabilities ## Workflow 1. **Detect the real hosting shape first:** - top-level `Program.cs` structure - middleware order and registration - auth model (Identity, JWT, OAuth, cookies) - endpoint registrations and routing 2. **Follow the correct middleware order:** ``` ExceptionHandler → HttpsRedirection → Static Files → Routing → CORS → Authentication → Authorization → Rate Limiting → Response Caching → Custom Middleware → Endpoints ``` 3. **Use built-in patterns correctly:** - Prefer `IOptions<T>` / `IOptionsSnapshot<T>` for configuration - Use `ILogger<T>` for structured logging - Use `IHttpClientFactory` for HTTP clients (never `new HttpClient()`) - Use `IHostedService` / `BackgroundService` for background work 4. **Route specialized work to specific skills:** - UI and components → `dotnet-blazor` - Real-time → `dotnet-signalr` - RPC → `dotnet-grpc` - New HTTP APIs → `dotnet-minimal-apis` (prefer unless controllers needed) - Controller APIs → `dotnet-web-api` 5. **Validate with build, tests, and targeted endpoint checks.** ## Middleware Patterns ### Correct Order Matters ```csharp var app = builder.Build(); app.UseExceptionHandler("/error"); // 1. Catch all exceptions app.UseHsts(); // 2. Security headers app.UseHttpsRedirection(); // 3. HTTPS redirect app.UseStaticFiles(); // 4. Serve static files app.UseRouting(); // 5. Route matching app.UseCors(); // 6. CORS policy app.UseAuthentication(); // 7. Who are you? app.UseAuthorization(); // 8. Can you access? app.UseRateLimiter(); // 9. Rate limiting app.UseResponseCaching(); // 10. Response cache app.MapControllers(); // 11. Endpoints ``` ### Custom Middleware Pattern ```csharp public class RequestTimingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestTimingMiddleware> _logger; public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var sw = Stopwatch.StartNew(); await _next(context); _logger.LogInformation("Request {Path} completed in {Elapsed}ms", context.Request.Path, sw.ElapsedMilliseconds); } } ``` ## Configuration Patterns ### Strongly-Typed Options ```csharp // appsettings.json { "EmailSettings": { "SmtpServer": "smtp.example.com", "Port": 587 } } // Registration builder.Services.Configure<EmailSettings>( builder.Configuration.GetSection("EmailSettings")); // Usage public class EmailService(IOptions<EmailSettings> options) { private readonly EmailSettings _settings = options.Value; } ``` ### Environment-Based Configuration ```csharp builder.Configuration .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) .AddEnvironmentVariables() .AddUserSecrets<Program>(optional: true); ``` ## Security Patterns ### Authentication Setup ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; }); ``` ### Authorization Policies ```csharp builder.Services.AddAuthorization(options => { options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")); options.AddPolicy("MinAge18", policy => policy.RequireClaim("Age", "18", "19", "20")); // simplified }); ``` ## Anti-Patterns to Avoid | Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | `new HttpClient()` | Socket exhaustion | `IHttpClientFactory` | | Sync-over-async (`Task.Result`) | Thread pool starvation | `await` properly | | Storing secrets in `appsettings.json` | Security risk | User Secrets, Key Vault | | Catching all exceptions silently | Hides bugs | Use `IExceptionHandler` | | `async void` in middleware | Crashes process | `async Task` | | Missing HTTPS redirect | Security risk | `UseHttpsRedirection()` | ## Performance Best Practices 1. **Use async/await everywhere** — avoid sync blocking calls 2. **Pool DbContext properly** — use scoped lifetime 3. **Enable response compression** — `UseResponseCompression()` 4. **Use output caching** — `UseOutputCache()` for .NET 7+ 5. **Profile with diagnostic tools** — Visual Studio Diagnostic Tools, PerfView 6. **Avoid allocations in hot paths** — use `Span<T>`, pooling ## Deliver - production-credible ASP.NET Core code and config - a clear request pipeline and hosting story - verification that matches the affected endpoints and middleware - security headers and HTTPS configured correctly ## Validate - middleware order is intentional and documented - security and configuration changes are explicit - endpoint behavior is covered by tests or smoke checks - no blocking calls in async context - secrets are not committed to source control - health checks are implemented for production readiness -
skills-dotnet-v3.2.zip 567.2 KB · in bundle
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.