EngineeringSenior DeveloperTechnical Sessions

Session 03: ASP.NET Core Internals

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

FeatureMinimal APIsControllers
Routingapp.MapGet("/api/products", ...)[Route("api/[controller]")]
Model BindingParameters, [AsParameters][FromBody], [FromQuery], etc.
FiltersEndpoint Filters (new)Action Filters, Result Filters
ValidationManual / EndpointFilter[ApiController] auto-validate
OpenAPI.WithOpenApi()[ProducesResponseType]
Performance~12% faster (less overhead)Chuẩn MVC pattern
AOT✅ Full support⚠️ Limited
Khi nàoMicroservices, simple APIsLarge 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

ConceptKey Takeaway
MiddlewareChain of RequestDelegate, thứ tự đăng ký rất quan trọng
DI LifetimesSingleton/Scoped/Transient, cẩn thận Captive Dependencies
Options PatternIOptions (Singleton), IOptionsSnapshot (Scoped), IOptionsMonitor (reactive)
Minimal APIsPerformance tốt hơn, AOT-ready, phù hợp microservices
ControllersConvention-based, filters pipeline, phù hợp large teams

Bài tập: Viết 1 custom middleware CorrelationIdMiddleware tự generate X-Correlation-Id nếu request không có, và propagate vào ILogger scope. Kiểm tra log output có correlation ID.