Mục tiêu: Hiểu sâu pipeline xử lý request, DI container, configuration system, và cách chọn giữa Minimal APIs vs Controllers.
1. Middleware Pipeline Deep Dive
RequestDelegate Chain
csharp// Mỗi middleware là 1 function: HttpContext → Task
// Pipeline = chain of RequestDelegates
// Bản chất:
public delegate Task RequestDelegate(HttpContext context);
// Middleware nhận RequestDelegate tiếp theo, trả về RequestDelegate mới
// next() gọi middleware kế tiếp
Request → [Exception] → [HTTPS] → [Static] → [Routing] → [Auth] → [Endpoint]
│
Response ← [Exception] ← [HTTPS] ← [Static] ← [Routing] ← [Auth] ← [Endpoint]
Viết Custom Middleware
csharp// Cách 1: Convention-based (phổ biến nhất)
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
// Constructor inject: Singleton lifetime!
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
// InvokeAsync inject: Scoped/Transient services OK
public async Task InvokeAsync(HttpContext context, IDbConnection db)
{
var sw = Stopwatch.StartNew();
// Trước next() → Request đang đi vào
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Response-Time"] = $"{sw.ElapsedMilliseconds}ms";
return Task.CompletedTask;
});
await _next(context); // Gọi middleware tiếp theo
// Sau next() → Response đang đi ra
sw.Stop();
_logger.LogInformation(
"{Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
context.Response.StatusCode,
sw.ElapsedMilliseconds);
}
}
// Cách 2: IMiddleware (Scoped lifetime, DI-friendly)
public class TransactionMiddleware : IMiddleware
{
private readonly AppDbContext _db; // Injected per-request (Scoped)
public TransactionMiddleware(AppDbContext db) => _db = db;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await using var transaction = await _db.Database.BeginTransactionAsync();
try
{
await next(context);
if (context.Response.StatusCode < 400)
await transaction.CommitAsync();
else
await transaction.RollbackAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
// Đăng ký IMiddleware - PHẢI register trong DI
builder.Services.AddScoped<TransactionMiddleware>();
app.UseMiddleware<TransactionMiddleware>();
// Cách 3: Inline middleware (cho prototyping)
app.Use(async (context, next) =>
{
if (context.Request.Headers.ContainsKey("X-Debug"))
{
context.Response.Headers["X-Debug-Info"] = "enabled";
}
await next(context);
});
Middleware Branching
csharp// Map: branch pipeline theo path prefix
app.Map("/api", apiApp =>
{
apiApp.UseRateLimiter();
apiApp.UseAuthentication();
apiApp.UseAuthorization();
apiApp.UseEndpoints(e => e.MapControllers());
});
app.Map("/health", healthApp =>
{
healthApp.Run(async ctx => await ctx.Response.WriteAsync("OK"));
});
// MapWhen: branch theo condition
app.MapWhen(
context => context.Request.Headers.ContainsKey("X-Custom-Header"),
branch => branch.UseMiddleware<CustomHeaderMiddleware>()
);
// UseWhen: conditional middleware nhưng KHÔNG branch
// (vẫn tiếp tục pipeline chính sau đó)
app.UseWhen(
context => context.Request.Path.StartsWithSegments("/api"),
branch => branch.UseMiddleware<ApiKeyMiddleware>()
);
Short-Circuit Middleware
csharp// app.Run() = terminal middleware, KHÔNG gọi next()
app.Run(async context =>
{
await context.Response.WriteAsync("Hello Terminal!");
});
// Static files middleware: short-circuit nếu tìm thấy file
app.UseStaticFiles(); // Nếu file tồn tại → trả về, KHÔNG gọi next()
// Response caching: short-circuit nếu có cached response
app.UseResponseCaching();
2. Dependency Injection Deep Dive
Service Lifetimes
csharp// Singleton: 1 instance cho toàn bộ app lifetime
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// → Dùng cho: stateless services, connection pools, configuration
// Scoped: 1 instance per request (per DI scope)
builder.Services.AddScoped<IOrderService, OrderService>();
// → Dùng cho: DbContext, Unit of Work, per-request state
// Transient: mỗi lần inject = instance mới
builder.Services.AddTransient<IEmailBuilder, EmailBuilder>();
// → Dùng cho: lightweight, stateless, short-lived services
⚠️ Captive Dependency Problem
csharp// ❌ CRITICAL BUG: Singleton giữ reference đến Scoped service
builder.Services.AddSingleton<IOrderProcessor, OrderProcessor>();
builder.Services.AddScoped<AppDbContext>();
public class OrderProcessor : IOrderProcessor
{
private readonly AppDbContext _db; // 💥 Captive dependency!
public OrderProcessor(AppDbContext db) => _db = db;
// _db là Scoped nhưng bị giữ lại mãi bởi Singleton
// → DbContext KHÔNG bao giờ được dispose
// → Memory leak, stale data, connection exhaustion
}
// ✅ FIX 1: Dùng IServiceScopeFactory
public class OrderProcessor(IServiceScopeFactory scopeFactory) : IOrderProcessor
{
public async Task ProcessAsync(Guid orderId)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// db sẽ được dispose khi scope kết thúc
}
}
// ✅ FIX 2: Enable validation (Development)
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // Detect captive dependencies
options.ValidateOnBuild = true; // Validate tất cả registrations khi build
});
Advanced DI Patterns
csharp// 1. Keyed Services (.NET 8)
builder.Services.AddKeyedSingleton<INotificationSender, EmailSender>("email");
builder.Services.AddKeyedSingleton<INotificationSender, SmsSender>("sms");
builder.Services.AddKeyedSingleton<INotificationSender, PushSender>("push");
public class NotificationService(
[FromKeyedServices("email")] INotificationSender emailSender,
[FromKeyedServices("sms")] INotificationSender smsSender)
{
public Task NotifyAsync(string channel, string message) => channel switch
{
"email" => emailSender.SendAsync(message),
"sms" => smsSender.SendAsync(message),
_ => throw new ArgumentException($"Unknown channel: {channel}")
};
}
// 2. Factory Registration
builder.Services.AddScoped<IPaymentGateway>(sp =>
{
var config = sp.GetRequiredService<IOptions<PaymentConfig>>().Value;
return config.Provider switch
{
"stripe" => new StripeGateway(config.ApiKey),
"paypal" => new PayPalGateway(config.ClientId, config.Secret),
_ => throw new InvalidOperationException($"Unknown provider: {config.Provider}")
};
});
// 3. Decorator Pattern
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.Decorate<IOrderRepository, CachedOrderRepository>();
// CachedOrderRepository nhận IOrderRepository (SqlOrderRepository) qua DI
// Cần package: Scrutor
// 4. Open Generics
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
// IRepository<Order> → EfRepository<Order>
// IRepository<Product> → EfRepository<Product>
// Tự động cho mọi T
// 5. Multiple implementations
builder.Services.AddScoped<IValidator, NameValidator>();
builder.Services.AddScoped<IValidator, EmailValidator>();
builder.Services.AddScoped<IValidator, PhoneValidator>();
// Inject tất cả:
public class ValidationService(IEnumerable<IValidator> validators)
{
public async Task<ValidationResult> ValidateAsync(object model)
{
var errors = new List<string>();
foreach (var validator in validators)
{
var result = await validator.ValidateAsync(model);
errors.AddRange(result.Errors);
}
return new ValidationResult(errors);
}
}
3. Configuration & Options Pattern
Configuration Sources (Thứ Tự Ưu Tiên)
Ưu tiên THẤP → CAO (sau ghi đè trước):
1. appsettings.json
2. appsettings.{Environment}.json
3. User Secrets (Development only)
4. Environment Variables
5. Command-line arguments
6. Azure Key Vault / In-memory
Options Pattern
csharp// appsettings.json
{
"Email": {
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SenderName": "MyApp",
"MaxRetries": 3
}
}
// Options class
public class EmailOptions
{
public const string SectionName = "Email";
public required string SmtpHost { get; init; }
public required int SmtpPort { get; init; }
public required string SenderName { get; init; }
public int MaxRetries { get; init; } = 3;
}
// Registration với validation
builder.Services.AddOptions<EmailOptions>()
.BindConfiguration(EmailOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart(); // Fail fast nếu config invalid
IOptions vs IOptionsSnapshot vs IOptionsMonitor
csharp// IOptions<T> - Singleton, đọc 1 lần khi resolve
// → Dùng cho config KHÔNG thay đổi sau startup
public class PaymentService(IOptions<PaymentOptions> options)
{
private readonly PaymentOptions _config = options.Value; // cached
}
// IOptionsSnapshot<T> - Scoped, đọc lại mỗi request
// → Dùng trong Scoped/Transient services, config có thể thay đổi
public class ReportService(IOptionsSnapshot<ReportOptions> options)
{
public void Generate()
{
var config = options.Value; // fresh per request
}
}
// IOptionsMonitor<T> - Singleton, notify khi thay đổi
// → Dùng trong Singleton services, cần react khi config thay đổi
public class CacheService : IDisposable
{
private readonly IDisposable? _changeListener;
private CacheOptions _config;
public CacheService(IOptionsMonitor<CacheOptions> options)
{
_config = options.CurrentValue;
_changeListener = options.OnChange(newConfig =>
{
_config = newConfig;
// React: clear cache, reinitialize connections, etc.
});
}
public void Dispose() => _changeListener?.Dispose();
}
Named Options
csharp// Khi cần nhiều config cùng type
builder.Services.Configure<StorageOptions>("azure",
builder.Configuration.GetSection("Storage:Azure"));
builder.Services.Configure<StorageOptions>("aws",
builder.Configuration.GetSection("Storage:AWS"));
public class StorageService(IOptionsSnapshot<StorageOptions> options)
{
public void UploadToAzure()
{
var azureConfig = options.Get("azure");
}
public void UploadToAws()
{
var awsConfig = options.Get("aws");
}
}
4. Minimal APIs vs Controllers
So Sánh Chi Tiết
| Feature | Minimal APIs | Controllers |
|---|---|---|
| Routing | app.MapGet("/api/products", ...) | [Route("api/[controller]")] |
| Model Binding | Parameters, [AsParameters] | [FromBody], [FromQuery], etc. |
| Filters | Endpoint Filters (new) | Action Filters, Result Filters |
| Validation | Manual / EndpointFilter | [ApiController] auto-validate |
| OpenAPI | .WithOpenApi() | [ProducesResponseType] |
| Performance | ~12% faster (less overhead) | Chuẩn MVC pattern |
| AOT | ✅ Full support | ⚠️ Limited |
| Khi nào | Microservices, simple APIs | Large APIs, team conventions |
Minimal API Patterns
csharp// Structured Minimal API với Route Groups
var app = builder.Build();
var api = app.MapGroup("/api").RequireAuthorization();
api.MapGroup("/products")
.MapProductEndpoints()
.WithTags("Products");
api.MapGroup("/orders")
.MapOrderEndpoints()
.WithTags("Orders");
// Extension method cho từng module
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create).AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete).RequireAuthorization("Admin");
return group;
}
private static async Task<Ok<List<ProductDto>>> GetAll(
AppDbContext db,
[AsParameters] ProductFilter filter,
CancellationToken ct)
{
var products = await db.Products
.Where(p => filter.Category == null || p.Category == filter.Category)
.Skip(filter.Skip).Take(filter.Take)
.Select(p => p.ToDto())
.ToListAsync(ct);
return TypedResults.Ok(products);
}
private static async Task<Results<Ok<ProductDto>, NotFound>> GetById(
int id, AppDbContext db, CancellationToken ct)
{
var product = await db.Products.FindAsync([id], ct);
return product is null
? TypedResults.NotFound()
: TypedResults.Ok(product.ToDto());
}
private static async Task<Created<ProductDto>> Create(
CreateProductRequest request,
AppDbContext db,
CancellationToken ct)
{
var product = request.ToEntity();
db.Products.Add(product);
await db.SaveChangesAsync(ct);
return TypedResults.Created($"/api/products/{product.Id}", product.ToDto());
}
}
// [AsParameters] - clean parameter binding
public record ProductFilter(
string? Category,
int Skip = 0,
int Take = 20);
Endpoint Filters (Tương Đương Action Filters)
csharp// Validation filter
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var model = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (model is null)
return TypedResults.BadRequest("Request body is required");
var validator = context.HttpContext.RequestServices
.GetService<IValidator<T>>();
if (validator is not null)
{
var result = await validator.ValidateAsync(model);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(
result.ToDictionary());
}
}
return await next(context);
}
}
5. Filters Pipeline (Controllers)
Middleware Pipeline
│
┌──────▼──────┐
│Authorization │ Filter
│ Filter │
└──────┬──────┘
┌──────▼──────┐
│ Resource │ Filter (caching, short-circuit)
│ Filter │
└──────┬──────┘
┌──────▼──────┐
│ Action │ Filter (before/after action)
│ Filter │
└──────┬──────┘
┌──────▼──────┐
│ ACTION │ ← Controller method
│ EXECUTION │
└──────┬──────┘
┌──────▼──────┐
│ Exception │ Filter (chỉ catch action exceptions)
│ Filter │
└──────┬──────┘
┌──────▼──────┐
│ Result │ Filter (before/after result execution)
│ Filter │
└─────────────┘
csharp// Global Exception Filter
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) => _logger = logger;
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Unhandled exception");
var problemDetails = context.Exception switch
{
NotFoundException ex => new ProblemDetails
{
Status = 404,
Title = "Not Found",
Detail = ex.Message
},
ValidationException ex => new ProblemDetails
{
Status = 422,
Title = "Validation Error",
Detail = ex.Message,
Extensions = { ["errors"] = ex.Errors }
},
_ => new ProblemDetails
{
Status = 500,
Title = "Internal Server Error",
Detail = "An unexpected error occurred"
}
};
context.Result = new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status
};
context.ExceptionHandled = true;
}
}
// Register globally
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
Tổng Kết Session 03
| Concept | Key Takeaway |
|---|---|
| Middleware | Chain of RequestDelegate, thứ tự đăng ký rất quan trọng |
| DI Lifetimes | Singleton/Scoped/Transient, cẩn thận Captive Dependencies |
| Options Pattern | IOptions (Singleton), IOptionsSnapshot (Scoped), IOptionsMonitor (reactive) |
| Minimal APIs | Performance tốt hơn, AOT-ready, phù hợp microservices |
| Controllers | Convention-based, filters pipeline, phù hợp large teams |
Bài tập: Viết 1 custom middleware
CorrelationIdMiddlewaretự generateX-Correlation-Idnếu request không có, và propagate vàoILoggerscope. Kiểm tra log output có correlation ID.