
Aspnet Core
- 50 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
aspnet-core is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- aspnet-core
- AI & Agent Building
- AI-coding skill
Aspnet Core by the numbers
- 50 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #7,245 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill aspnet-coreAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 50 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
1. Detect the real hosting shape first:
- top-level
Program.csstructure - 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 → Endpoints3. Use 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
4. Route specialized work to specific skills:
- UI and components →
blazor - Real-time →
signalr - RPC →
grpc - New HTTP APIs →
minimal-apis(prefer unless controllers needed) - Controller APIs →
web-api
5. Validate with build, tests, and targeted endpoint checks.
Current Upstream Notes
- ASP.NET Core
v9.0.17is a servicing release with branding and dependency updates rather than a new programming model. Keep existing middleware, hosting, and endpoint guidance intact, then rerun build/tests after package updates. - The current Microsoft Learn overview for
aspnetcore-10.0remains the routing entry point for choosing between Blazor, Minimal APIs, controller APIs, SignalR, and gRPC.
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. EndpointsCustom 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
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
{
"version": "1.0.1",
"category": "Web",
"package_prefix": "Microsoft.AspNetCore"
}
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
- Async Anti-Patterns
- Configuration and Secrets Anti-Patterns
- Dependency Injection Anti-Patterns
- Middleware Anti-Patterns
- Error Handling Anti-Patterns
- Security Anti-Patterns
- Performance Anti-Patterns
- Controller Anti-Patterns
- Database and EF Core Anti-Patterns
---
HttpClient Anti-Patterns
Creating HttpClient Instances Directly
Anti-Pattern:
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:
// 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:
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:
// 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:
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:
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
return Ok(user);
}Async Void
Anti-Pattern:
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:
public async Task InvokeAsync(HttpContext context)
{
await LogRequestAsync(context);
await _next(context);
}Not Using ConfigureAwait in Library Code
Anti-Pattern (in library/shared code):
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):
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:
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:
// 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:
{
"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:
// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
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 lastProblem: 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:
app.UseExceptionHandler("/error"); // First - catches all exceptions
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // After routing
app.UseAuthorization(); // After authentication
app.MapControllers(); // LastBlocking Operations in Middleware
Anti-Pattern:
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:
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:
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:
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:
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:
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:
[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:
// 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:
try
{
await ProcessPaymentAsync(order);
}
catch (Exception)
{
// BAD: No logging, no context
throw;
}Correct Approach:
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:
var app = builder.Build();
app.MapControllers(); // BAD: No HTTPS enforcement
app.Run();Correct Approach:
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.MapControllers();Disabled CORS (Allow All)
Anti-Pattern:
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin() // BAD: Any site can access
.AllowAnyMethod()
.AllowAnyHeader();
});
});Correct Approach:
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:
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:
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:
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(int id) // BAD: No authorization
{
await _userService.DeleteAsync(id);
return NoContent();
}Correct Approach:
[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:
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:
// 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:
[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:
[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:
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:
public async Task<decimal> GetTotalRevenueAsync()
{
// Computed in database
return await _context.Orders.SumAsync(o => o.Total);
}N+1 Query Problem
Anti-Pattern:
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:
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:
var app = builder.Build();
app.MapControllers(); // BAD: Large responses sent uncompressedCorrect Approach:
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:
[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:
[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:
[HttpPost]
public async Task<IActionResult> Create(CreateRequest request)
{
// BAD: Manual validation in every action
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// ...
}Correct Approach:
// 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:
builder.Services.AddSingleton<MyDbContext>(); // BAD: DbContext is not thread-safeProblem: DbContext is not thread-safe and tracks entities. Using it as singleton causes concurrency issues and memory leaks.
Correct Approach:
builder.Services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(connectionString));
// Default is scoped lifetimeNot Disposing DbContext (Manual Creation)
Anti-Pattern:
public class ReportService
{
private readonly MyDbContext _context = new MyDbContext(); // BAD: Never disposed
}Correct Approach:
public class ReportService
{
private readonly MyDbContext _context;
public ReportService(MyDbContext context)
{
_context = context; // Injected, container manages lifetime
}
}Tracking Entities When Not Needed
Anti-Pattern:
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:
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:
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:
// 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();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
- Security Patterns
- Configuration Patterns
- Dependency Injection Patterns
- Error Handling 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
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
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
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
// 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
// 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
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
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
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
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
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
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
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+)
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
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
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
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
// 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
// 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
// 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
// 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
// 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
// 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+)
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
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
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
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
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);
}
}
}