βš™οΈ

Aspnetcore Principal Engineer Guide

.NET Ecosystem Advanced 8 min read 1500 words

ASP.NET Core - Principal Engineer Deep Dive

A comprehensive guide covering ASP.NET Core internals, advanced patterns, and production best practices for Principal/Staff Engineers.


Table of Contents

  1. Request Pipeline Architecture
  2. Dependency Injection Deep Dive
  3. Authentication & Authorization
  4. Performance & Caching
  5. API Design Advanced Patterns
  6. Background Processing
  7. Advanced DI Patterns
  8. File Streaming & Large Data Transfer

1. Request Pipeline Architecture

1.1 Routing Internals

Endpoint Routing vs Conventional Routing

ASP.NET Core 3.0+ uses Endpoint Routing as the default routing mechanism, replacing the older conventional routing.

Conventional Routing (Legacy):

// Startup.cs - Old approach (ASP.NET Core 2.x)
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Endpoint Routing (Modern):

// Program.cs - Modern approach (ASP.NET Core 3.0+)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

app.UseRouting();           // Adds EndpointRoutingMiddleware
app.UseAuthorization();     // Middleware can now access endpoint metadata
app.MapControllers();       // Adds EndpointMiddleware

app.Run();

Key Differences:

Aspect Conventional Routing Endpoint Routing
Route Resolution During MVC middleware Before any middleware runs
Metadata Access Only in MVC pipeline Any middleware can access
Performance Route matching per request DFA-based, O(1) matching
Flexibility Limited to MVC Works with any endpoint type

[INTERNALS] Route Matching Algorithm - DFA-Based Matcher

The DfaMatcherBuilder constructs a Deterministic Finite Automaton (DFA) for route matching:

// How the DFA matcher works internally
// Routes are compiled into a state machine at startup

// Example routes:
// /api/users/{id}
// /api/users/profile
// /api/products/{id}

// The DFA creates a tree structure:
// Root
// └── api
//     └── users
//         β”œβ”€β”€ {id}      -> UserController.GetById
//         └── profile   -> UserController.GetProfile
//     └── products
//         └── {id}      -> ProductController.GetById

[BENCHMARK] Route Matching Performance:

Scenario Conventional DFA-Based
10 routes ~2.5ΞΌs ~0.8ΞΌs
100 routes ~15ΞΌs ~0.9ΞΌs
1000 routes ~120ΞΌs ~1.0ΞΌs

The DFA approach provides O(1) matching regardless of route count.

Route Constraints and Custom Constraints

Built-in Constraints:

// Common route constraints
[HttpGet("users/{id:int}")]                    // Integer only
[HttpGet("users/{id:guid}")]                   // GUID format
[HttpGet("users/{name:alpha}")]                // Letters only
[HttpGet("users/{name:minlength(3)}")]         // Minimum length
[HttpGet("users/{id:range(1,100)}")]           // Value range
[HttpGet("products/{slug:regex(^[a-z-]+$)}")]  // Regex pattern

Custom Route Constraint Implementation:

// Custom constraint for valid slugs
public class SlugRouteConstraint : IRouteConstraint
{
    private static readonly Regex SlugRegex = new(
        @"^[a-z0-9]+(?:-[a-z0-9]+)*$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    public bool Match(
        HttpContext? httpContext,
        IRouter? route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var value) || value == null)
            return false;

        var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture);
        return !string.IsNullOrEmpty(stringValue) && SlugRegex.IsMatch(stringValue);
    }
}

// Registration
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("slug", typeof(SlugRouteConstraint));
});

// Usage
[HttpGet("articles/{slug:slug}")]
public IActionResult GetArticle(string slug) => Ok();

Attribute Routing Internals and Metadata

How Attribute Routing Works:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // Route: GET /api/users
    [HttpGet]
    public IActionResult GetAll() => Ok();

    // Route: GET /api/users/5
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id) => Ok();

    // Route: GET /api/users/5/orders
    [HttpGet("{id:int}/orders")]
    public IActionResult GetUserOrders(int id) => Ok();
}

[INTERNALS] Endpoint Metadata:

// Accessing endpoint metadata in middleware
app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();

    if (endpoint != null)
    {
        // Access metadata
        var authorizeData = endpoint.Metadata.GetMetadata<IAuthorizeData>();
        var httpMethods = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>();
        var routePattern = (endpoint as RouteEndpoint)?.RoutePattern;

        Console.WriteLine($"Endpoint: {endpoint.DisplayName}");
        Console.WriteLine($"Route: {routePattern?.RawText}");
        Console.WriteLine($"Methods: {string.Join(", ", httpMethods?.HttpMethods ?? [])}");
    }

    await next();
});

LinkGenerator and URL Generation

public class OrderService
{
    private readonly LinkGenerator _linkGenerator;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public OrderService(LinkGenerator linkGenerator, IHttpContextAccessor httpContextAccessor)
    {
        _linkGenerator = linkGenerator;
        _httpContextAccessor = httpContextAccessor;
    }

    public string GenerateOrderUrl(int orderId)
    {
        // Generate absolute URL
        return _linkGenerator.GetUriByAction(
            _httpContextAccessor.HttpContext!,
            action: "GetById",
            controller: "Orders",
            values: new { id = orderId },
            scheme: "https");
        // Result: https://example.com/api/orders/123
    }

    public string GenerateOrderPath(int orderId)
    {
        // Generate relative path
        return _linkGenerator.GetPathByAction(
            action: "GetById",
            controller: "Orders",
            values: new { id = orderId });
        // Result: /api/orders/123
    }
}

1.2 Middleware Pipeline

Middleware Execution Order and RequestDelegate Chain

The middleware pipeline is a chain of RequestDelegate functions:

// Each middleware is essentially:
public delegate Task RequestDelegate(HttpContext context);

// The pipeline builds a chain like this:
// Request -> M1 -> M2 -> M3 -> Endpoint -> M3 -> M2 -> M1 -> Response

Visualization of Pipeline Flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     REQUEST ENTERS                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ExceptionHandler Middleware                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  HTTPS Redirection Middleware                        β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚  β”‚
β”‚  β”‚  β”‚  Static Files Middleware                      β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  Routing Middleware                    β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  Authentication Middleware      β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β”‚  Authorization Middlewareβ”‚   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β”‚  β”‚    ENDPOINT       β”‚   β”‚   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚   β”‚  β”‚
β”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     RESPONSE EXITS                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Short-Circuiting the Pipeline

When to Short-Circuit:

// Example: Rate limiting middleware that short-circuits
public class RateLimitingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IRateLimiter _rateLimiter;

    public RateLimitingMiddleware(RequestDelegate next, IRateLimiter rateLimiter)
    {
        _next = next;
        _rateLimiter = rateLimiter;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var clientId = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

        if (!_rateLimiter.TryAcquire(clientId))
        {
            // SHORT-CIRCUIT: Don't call next middleware
            context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.Response.Headers.RetryAfter = "60";
            await context.Response.WriteAsJsonAsync(new
            {
                error = "Rate limit exceeded",
                retryAfter = 60
            });
            return; // Pipeline ends here
        }

        // Continue to next middleware
        await _next(context);
    }
}

Common Short-Circuit Scenarios:

  1. Authentication failure - Return 401
  2. Authorization failure - Return 403
  3. Rate limiting - Return 429
  4. Static files - Serve file, don’t continue
  5. Health checks - Return health status
  6. CORS preflight - Handle OPTIONS request

Terminal vs Non-Terminal Middleware

Terminal Middleware (doesn’t call next()):

// Terminal middleware - handles the request completely
app.Map("/health", app =>
{
    app.Run(async context =>
    {
        // This is terminal - never calls next
        await context.Response.WriteAsJsonAsync(new { status = "healthy" });
    });
});

Non-Terminal Middleware (always calls next()):

// Non-terminal middleware - passes request along
app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();

    await next(); // Always call next

    stopwatch.Stop();
    context.Response.Headers["X-Response-Time"] = $"{stopwatch.ElapsedMilliseconds}ms";
});

Creating Custom Middleware

Class-Based Middleware (Recommended):

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    // Constructor injection - called once at startup
    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // InvokeAsync - called per request
    // Can inject scoped services here
    public async Task InvokeAsync(HttpContext context, IUserService userService)
    {
        var requestId = Guid.NewGuid().ToString("N")[..8];

        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = requestId,
            ["UserId"] = userService.GetCurrentUserId()
        }))
        {
            _logger.LogInformation(
                "Request {Method} {Path} started",
                context.Request.Method,
                context.Request.Path);

            var stopwatch = Stopwatch.StartNew();

            try
            {
                await _next(context);
            }
            finally
            {
                stopwatch.Stop();
                _logger.LogInformation(
                    "Request {Method} {Path} completed with {StatusCode} in {ElapsedMs}ms",
                    context.Request.Method,
                    context.Request.Path,
                    context.Response.StatusCode,
                    stopwatch.ElapsedMilliseconds);
            }
        }
    }
}

// Extension method for clean registration
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestLoggingMiddleware>();
    }
}

// Usage
app.UseRequestLogging();

Inline Middleware (Simple Cases):

// Use for simple, one-off middleware
app.Use(async (context, next) =>
{
    context.Response.Headers["X-Frame-Options"] = "DENY";
    context.Response.Headers["X-Content-Type-Options"] = "nosniff";
    await next();
});

Middleware vs Filters Comparison

Aspect Middleware Filters
Scope All requests Only MVC/API requests
Access HttpContext only ActionContext, model binding
Order Pipeline order Filter pipeline order
Short-circuit Don’t call next() Set Result property
DI Constructor + InvokeAsync Constructor only
Use Cases Cross-cutting concerns MVC-specific concerns

When to Use What:

// USE MIDDLEWARE for:
// - Logging all requests
// - CORS
// - Authentication
// - Rate limiting
// - Response compression

// USE FILTERS for:
// - Model validation
// - Action-specific authorization
// - Exception handling for actions
// - Caching action results
// - Modifying action arguments

[INTERNALS] How UseMiddleware Works

// Simplified version of how UseMiddleware works internally
public static IApplicationBuilder UseMiddleware<TMiddleware>(
    this IApplicationBuilder app,
    params object[] args)
{
    return app.Use(next =>
    {
        // Get the InvokeAsync method
        var invokeMethod = typeof(TMiddleware).GetMethod("InvokeAsync")
            ?? typeof(TMiddleware).GetMethod("Invoke");

        // Create middleware instance with RequestDelegate
        var ctorArgs = new object[] { next }.Concat(args).ToArray();
        var middleware = ActivatorUtilities.CreateInstance(
            app.ApplicationServices,
            typeof(TMiddleware),
            ctorArgs);

        // Return the delegate that invokes the middleware
        return async context =>
        {
            // Resolve scoped services for InvokeAsync parameters
            var serviceProvider = context.RequestServices;
            var parameters = invokeMethod.GetParameters()
                .Skip(1) // Skip HttpContext
                .Select(p => serviceProvider.GetRequiredService(p.ParameterType))
                .Prepend(context)
                .ToArray();

            await (Task)invokeMethod.Invoke(middleware, parameters);
        };
    });
}

[PRODUCTION] Middleware Ordering Best Practices

var app = builder.Build();

// 1. Exception handling - MUST be first
app.UseExceptionHandler("/error");

// 2. HSTS - Before any response
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

// 3. HTTPS Redirection
app.UseHttpsRedirection();

// 4. Static files - Short-circuits for static content
app.UseStaticFiles();

// 5. Cookie policy
app.UseCookiePolicy();

// 6. Routing - Matches endpoints
app.UseRouting();

// 7. CORS - Must be after routing, before auth
app.UseCors();

// 8. Request localization
app.UseRequestLocalization();

// 9. Authentication - Establishes identity
app.UseAuthentication();

// 10. Authorization - Checks permissions
app.UseAuthorization();

// 11. Session
app.UseSession();

// 12. Response caching
app.UseResponseCaching();

// 13. Response compression
app.UseResponseCompression();

// 14. Custom middleware
app.UseRequestLogging();

// 15. Endpoints
app.MapControllers();

app.Run();

[DEBUGGING] Diagnosing Middleware Issues

// Add diagnostic middleware to trace pipeline execution
public class DiagnosticMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _name;
    private readonly ILogger _logger;

    public DiagnosticMiddleware(RequestDelegate next, string name, ILoggerFactory loggerFactory)
    {
        _next = next;
        _name = name;
        _logger = loggerFactory.CreateLogger($"Middleware.{name}");
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogDebug("[{Name}] Entering - Request: {Method} {Path}",
            _name, context.Request.Method, context.Request.Path);

        var sw = Stopwatch.StartNew();

        await _next(context);

        sw.Stop();

        _logger.LogDebug("[{Name}] Exiting - Status: {StatusCode}, Duration: {Ms}ms",
            _name, context.Response.StatusCode, sw.ElapsedMilliseconds);
    }
}

// Wrap existing middleware for diagnosis
app.UseMiddleware<DiagnosticMiddleware>("BeforeAuth");
app.UseAuthentication();
app.UseMiddleware<DiagnosticMiddleware>("AfterAuth");
app.UseAuthorization();
app.UseMiddleware<DiagnosticMiddleware>("AfterAuthz");

2. Dependency Injection Deep Dive

2.1 Service Lifetimes Internals

Singleton, Scoped, Transient - Internal Implementation

// Registration examples
services.AddSingleton<ISingletonService, SingletonService>();
services.AddScoped<IScopedService, ScopedService>();
services.AddTransient<ITransientService, TransientService>();

How Each Lifetime Works Internally:

// SINGLETON - One instance for application lifetime
// Stored in root IServiceProvider
// Created on first request, reused for all subsequent requests
public class SingletonService
{
    public Guid Id { get; } = Guid.NewGuid();
    // Same Id for entire application lifetime
}

// SCOPED - One instance per scope (typically per HTTP request)
// Stored in IServiceScope's IServiceProvider
// Created on first request within scope, reused within same scope
public class ScopedService
{
    public Guid Id { get; } = Guid.NewGuid();
    // Same Id within one HTTP request, different across requests
}

// TRANSIENT - New instance every time
// Never stored, always created fresh
public class TransientService
{
    public Guid Id { get; } = Guid.NewGuid();
    // Different Id every time it's resolved
}

[INTERNALS] ServiceProviderEngineScope

// Simplified view of how scopes work internally
internal class ServiceProviderEngineScope : IServiceScope, IServiceProvider
{
    private readonly ConcurrentDictionary<ServiceCacheKey, object> _resolvedServices;
    private readonly ServiceProviderEngine _engine;

    // Scoped services are cached here
    public object GetService(Type serviceType)
    {
        var descriptor = _engine.GetServiceDescriptor(serviceType);

        return descriptor.Lifetime switch
        {
            ServiceLifetime.Singleton => _engine.RootScope.GetOrCreate(serviceType),
            ServiceLifetime.Scoped => _resolvedServices.GetOrAdd(
                new ServiceCacheKey(serviceType),
                _ => CreateService(descriptor)),
            ServiceLifetime.Transient => CreateService(descriptor),
            _ => throw new InvalidOperationException()
        };
    }
}

Captive Dependency Problem and Detection

The Problem:

// WRONG: Scoped service injected into Singleton
public class SingletonService
{
    private readonly IScopedService _scopedService; // CAPTIVE!

    public SingletonService(IScopedService scopedService)
    {
        _scopedService = scopedService;
        // This scoped service will live as long as the singleton
        // All requests will share the same "scoped" instance
    }
}

Detection with ValidateScopes:

var builder = WebApplication.CreateBuilder(args);

// Enable scope validation (automatic in Development)
builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;  // Throws if captive dependency detected
    options.ValidateOnBuild = true; // Validates at startup, not first request
});

// This will throw at startup:
// "Cannot consume scoped service 'IScopedService' from singleton 'SingletonService'"

Scoped Service in Singleton Safely (IServiceScopeFactory)

public class SingletonBackgroundService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<SingletonBackgroundService> _logger;

    public SingletonBackgroundService(
        IServiceScopeFactory scopeFactory,
        ILogger<SingletonBackgroundService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Create a new scope for each iteration
            using (var scope = _scopeFactory.CreateScope())
            {
                // Resolve scoped services within the scope
                var dbContext = scope.ServiceProvider
                    .GetRequiredService<ApplicationDbContext>();

                var userService = scope.ServiceProvider
                    .GetRequiredService<IUserService>();

                await ProcessUsersAsync(dbContext, userService, stoppingToken);
            } // Scope disposed, scoped services disposed

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }

    private async Task ProcessUsersAsync(
        ApplicationDbContext dbContext,
        IUserService userService,
        CancellationToken ct)
    {
        var users = await dbContext.Users
            .Where(u => u.NeedsProcessing)
            .ToListAsync(ct);

        foreach (var user in users)
        {
            await userService.ProcessAsync(user, ct);
        }
    }
}

[BENCHMARK] Performance Impact of Service Lifetimes

// Benchmark results (resolving 1 million times)
// Hardware: Intel i7-10700K, 32GB RAM

| Lifetime   | Resolution Time | Memory Allocations |
|------------|----------------|--------------------|
| Singleton  | ~15ms          | 1 allocation       |
| Scoped     | ~180ms         | 1 per scope        |
| Transient  | ~850ms         | 1,000,000 allocs   |

// Takeaway: Use Singleton when thread-safe
// Use Transient sparingly for stateless, lightweight services

[PRODUCTION] Memory Leak Patterns and Detection

// MEMORY LEAK PATTERN 1: Singleton holding request data
public class BadSingletonCache
{
    // This grows unbounded - MEMORY LEAK
    private readonly Dictionary<string, UserData> _cache = new();

    public void CacheUser(string requestId, UserData data)
    {
        _cache[requestId] = data; // Never removed!
    }
}

// FIX: Use IMemoryCache with expiration
public class GoodSingletonCache
{
    private readonly IMemoryCache _cache;

    public GoodSingletonCache(IMemoryCache cache) => _cache = cache;

    public void CacheUser(string key, UserData data)
    {
        _cache.Set(key, data, TimeSpan.FromMinutes(10));
    }
}

// MEMORY LEAK PATTERN 2: Event handler in Singleton
public class BadEventSubscriber
{
    public BadEventSubscriber(IEventBus eventBus)
    {
        eventBus.OnMessage += HandleMessage; // Never unsubscribed!
    }

    private void HandleMessage(Message msg) { }
}

// FIX: Implement IDisposable
public class GoodEventSubscriber : IDisposable
{
    private readonly IEventBus _eventBus;

    public GoodEventSubscriber(IEventBus eventBus)
    {
        _eventBus = eventBus;
        _eventBus.OnMessage += HandleMessage;
    }

    public void Dispose()
    {
        _eventBus.OnMessage -= HandleMessage;
    }
}

2.2 Configuration System Internals

Configuration Providers and Precedence Order

Configuration providers are loaded in order, with later providers overriding earlier ones:

var builder = WebApplication.CreateBuilder(args);

// Default provider order (last wins):
// 1. appsettings.json
// 2. appsettings.{Environment}.json
// 3. User Secrets (Development only)
// 4. Environment Variables
// 5. Command Line Arguments

// Custom provider order:
builder.Configuration
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
    .AddJsonFile("appsettings.local.json", optional: true) // Local overrides
    .AddEnvironmentVariables()
    .AddEnvironmentVariables("MYAPP_") // Prefixed env vars
    .AddCommandLine(args)
    .AddUserSecrets<Program>(optional: true); // Last = highest priority

Configuration Precedence Example:

// appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=prod-server;Database=mydb"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

// appsettings.Development.json
{
  "ConnectionStrings": {
    "Default": "Server=localhost;Database=mydb_dev"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Debug"  // Overrides Warning
    }
  }
}

// Environment Variable: ConnectionStrings__Default=Server=docker-db;Database=mydb
// Result: "Server=docker-db;Database=mydb" (env var wins)

IOptions vs IOptionsSnapshot vs IOptionsMonitor

// Configuration class
public class EmailSettings
{
    public string SmtpServer { get; set; } = "";
    public int Port { get; set; } = 587;
    public string FromAddress { get; set; } = "";
}

// Registration
services.Configure<EmailSettings>(configuration.GetSection("Email"));

// Or with validation
services.AddOptions<EmailSettings>()
    .Bind(configuration.GetSection("Email"))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Fail fast on startup

IOptions - Singleton, no reload:

public class EmailService
{
    private readonly EmailSettings _settings;

    // IOptions<T> is Singleton - settings never change
    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value; // Read once, cached forever
    }
}

IOptionsSnapshot - Scoped, reloads per request:

public class EmailService
{
    private readonly EmailSettings _settings;

    // IOptionsSnapshot<T> is Scoped - reads fresh value per request
    public EmailService(IOptionsSnapshot<EmailSettings> options)
    {
        _settings = options.Value; // Fresh value for this request
    }
}

IOptionsMonitor - Singleton with change notifications:

public class EmailService : IDisposable
{
    private EmailSettings _settings;
    private readonly IDisposable? _changeListener;

    // IOptionsMonitor<T> is Singleton but can detect changes
    public EmailService(IOptionsMonitor<EmailSettings> options)
    {
        _settings = options.CurrentValue;

        // Subscribe to changes
        _changeListener = options.OnChange(newSettings =>
        {
            _settings = newSettings;
            Console.WriteLine("Email settings reloaded!");
        });
    }

    public void Dispose() => _changeListener?.Dispose();
}

Comparison Table:

Feature IOptions IOptionsSnapshot IOptionsMonitor
Lifetime Singleton Scoped Singleton
Reload support No Yes (per request) Yes (real-time)
Change notifications No No Yes
Performance Best Good Good
Use case Static config Request-scoped Long-running services

Configuration Validation

DataAnnotations Validation:

public class EmailSettings
{
    [Required]
    [Url]
    public string SmtpServer { get; set; } = "";

    [Range(1, 65535)]
    public int Port { get; set; } = 587;

    [Required]
    [EmailAddress]
    public string FromAddress { get; set; } = "";

    [Required]
    [MinLength(8)]
    public string Password { get; set; } = "";
}

// Registration with validation
services.AddOptions<EmailSettings>()
    .Bind(configuration.GetSection("Email"))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Throws on startup if invalid

Custom Validation with IValidateOptions:

public class EmailSettingsValidator : IValidateOptions<EmailSettings>
{
    public ValidateOptionsResult Validate(string? name, EmailSettings options)
    {
        var failures = new List<string>();

        if (string.IsNullOrEmpty(options.SmtpServer))
            failures.Add("SMTP server is required");

        if (options.Port is < 1 or > 65535)
            failures.Add("Port must be between 1 and 65535");

        if (!options.FromAddress.Contains('@'))
            failures.Add("FromAddress must be a valid email");

        // Custom business rule validation
        if (options.SmtpServer.Contains("gmail") && options.Port != 587)
            failures.Add("Gmail SMTP requires port 587");

        return failures.Count > 0
            ? ValidateOptionsResult.Fail(failures)
            : ValidateOptionsResult.Success;
    }
}

// Registration
services.AddSingleton<IValidateOptions<EmailSettings>, EmailSettingsValidator>();

Named Options Pattern

// Configuration
{
  "Storage": {
    "Azure": {
      "ConnectionString": "azure-connection",
      "ContainerName": "azure-container"
    },
    "AWS": {
      "ConnectionString": "aws-connection",
      "BucketName": "aws-bucket"
    }
  }
}

// Registration
services.Configure<StorageOptions>("Azure", config.GetSection("Storage:Azure"));
services.Configure<StorageOptions>("AWS", config.GetSection("Storage:AWS"));

// Usage
public class StorageService
{
    private readonly StorageOptions _azureOptions;
    private readonly StorageOptions _awsOptions;

    public StorageService(IOptionsSnapshot<StorageOptions> options)
    {
        _azureOptions = options.Get("Azure");
        _awsOptions = options.Get("AWS");
    }
}

[INTERNALS] How Options are Resolved from DI

// Simplified internal implementation
internal class OptionsManager<TOptions> : IOptions<TOptions>
    where TOptions : class
{
    private readonly IOptionsFactory<TOptions> _factory;
    private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache = new();

    public TOptions Value => Get(Options.DefaultName);

    public TOptions Get(string? name)
    {
        name ??= Options.DefaultName;

        return _cache.GetOrAdd(name, n => new Lazy<TOptions>(
            () => _factory.Create(n))).Value;
    }
}

internal class OptionsFactory<TOptions> : IOptionsFactory<TOptions>
    where TOptions : class
{
    private readonly IEnumerable<IConfigureOptions<TOptions>> _configures;
    private readonly IEnumerable<IValidateOptions<TOptions>> _validators;

    public TOptions Create(string name)
    {
        var options = Activator.CreateInstance<TOptions>();

        // Apply all configurations
        foreach (var configure in _configures)
        {
            if (configure is IConfigureNamedOptions<TOptions> named)
                named.Configure(name, options);
            else
                configure.Configure(options);
        }

        // Run all validators
        foreach (var validator in _validators)
        {
            var result = validator.Validate(name, options);
            if (result.Failed)
                throw new OptionsValidationException(name, typeof(TOptions), result.Failures);
        }

        return options;
    }
}

[PRODUCTION] Configuration Reload Scenarios

// Scenario: Feature flags that need to change without restart
public class FeatureFlagsService
{
    private readonly IOptionsMonitor<FeatureFlags> _flags;
    private readonly ILogger<FeatureFlagsService> _logger;

    public FeatureFlagsService(
        IOptionsMonitor<FeatureFlags> flags,
        ILogger<FeatureFlagsService> logger)
    {
        _flags = flags;
        _logger = logger;

        // Log when flags change
        _flags.OnChange(newFlags =>
        {
            _logger.LogInformation(
                "Feature flags updated: DarkMode={DarkMode}, BetaFeatures={Beta}",
                newFlags.EnableDarkMode,
                newFlags.EnableBetaFeatures);
        });
    }

    public bool IsDarkModeEnabled() => _flags.CurrentValue.EnableDarkMode;
    public bool AreBetaFeaturesEnabled() => _flags.CurrentValue.EnableBetaFeatures;
}

// appsettings.json (can be changed at runtime)
{
  "FeatureFlags": {
    "EnableDarkMode": true,
    "EnableBetaFeatures": false
  }
}

3. Authentication & Authorization Deep Dive

3.1 JWT Authentication Internals

Token Generation with Claims and Signing

public class JwtTokenService
{
    private readonly JwtSettings _settings;
    private readonly ILogger<JwtTokenService> _logger;

    public JwtTokenService(IOptions<JwtSettings> settings, ILogger<JwtTokenService> logger)
    {
        _settings = settings.Value;
        _logger = logger;
    }

    public TokenResponse GenerateTokens(User user, IEnumerable<string> roles)
    {
        var accessToken = GenerateAccessToken(user, roles);
        var refreshToken = GenerateRefreshToken();

        return new TokenResponse
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken,
            ExpiresIn = (int)_settings.AccessTokenExpiration.TotalSeconds
        };
    }

    private string GenerateAccessToken(User user, IEnumerable<string> roles)
    {
        var securityKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_settings.SecretKey));

        var credentials = new SigningCredentials(
            securityKey,
            SecurityAlgorithms.HmacSha256);

        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new(JwtRegisteredClaimNames.Iat,
                DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
                ClaimValueTypes.Integer64),
            new("name", user.DisplayName),
            new("tenant_id", user.TenantId.ToString())
        };

        // Add role claims
        claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: claims,
            notBefore: DateTime.UtcNow,
            expires: DateTime.UtcNow.Add(_settings.AccessTokenExpiration),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    private string GenerateRefreshToken()
    {
        var randomNumber = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }
}

JWT Configuration and Validation Pipeline

// Program.cs
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        // Validate the issuer
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],

        // Validate the audience
        ValidateAudience = true,
        ValidAudience = builder.Configuration["Jwt:Audience"],

        // Validate the signing key
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!)),

        // Validate the token expiration
        ValidateLifetime = true,
        ClockSkew = TimeSpan.FromMinutes(1), // Allow 1 minute clock skew

        // Custom validation
        LifetimeValidator = (notBefore, expires, token, parameters) =>
        {
            return expires > DateTime.UtcNow;
        }
    };

    // Events for custom handling
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception is SecurityTokenExpiredException)
            {
                context.Response.Headers["X-Token-Expired"] = "true";
            }
            return Task.CompletedTask;
        },

        OnTokenValidated = async context =>
        {
            // Additional validation (e.g., check if user is still active)
            var userService = context.HttpContext.RequestServices
                .GetRequiredService<IUserService>();

            var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
            if (userId == null || !await userService.IsUserActiveAsync(userId))
            {
                context.Fail("User is not active");
            }
        },

        OnChallenge = context =>
        {
            // Custom response for 401
            context.HandleResponse();
            context.Response.StatusCode = 401;
            context.Response.ContentType = "application/json";
            return context.Response.WriteAsJsonAsync(new
            {
                error = "Unauthorized",
                message = "Invalid or missing token"
            });
        }
    };
});

Refresh Token Rotation Implementation

public class RefreshTokenService
{
    private readonly ApplicationDbContext _context;
    private readonly JwtTokenService _tokenService;
    private readonly ILogger<RefreshTokenService> _logger;

    public async Task<TokenResponse?> RefreshTokenAsync(
        string accessToken,
        string refreshToken,
        CancellationToken ct = default)
    {
        // 1. Validate the expired access token (without lifetime validation)
        var principal = GetPrincipalFromExpiredToken(accessToken);
        if (principal == null)
        {
            _logger.LogWarning("Invalid access token provided for refresh");
            return null;
        }

        var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (userId == null) return null;

        // 2. Find and validate the refresh token
        var storedToken = await _context.RefreshTokens
            .Include(rt => rt.User)
            .FirstOrDefaultAsync(rt =>
                rt.Token == refreshToken &&
                rt.UserId == userId, ct);

        if (storedToken == null)
        {
            _logger.LogWarning("Refresh token not found for user {UserId}", userId);
            return null;
        }

        if (storedToken.IsRevoked)
        {
            // Possible token theft - revoke all tokens for this user
            _logger.LogWarning(
                "Attempted use of revoked refresh token for user {UserId}. " +
                "Revoking all tokens.", userId);

            await RevokeAllUserTokensAsync(userId, ct);
            return null;
        }

        if (storedToken.IsExpired)
        {
            _logger.LogWarning("Expired refresh token used for user {UserId}", userId);
            return null;
        }

        // 3. Rotate the refresh token
        storedToken.Revoked = DateTime.UtcNow;
        storedToken.RevokedReason = "Rotated";

        var user = storedToken.User;
        var roles = await _context.UserRoles
            .Where(ur => ur.UserId == userId)
            .Select(ur => ur.Role.Name)
            .ToListAsync(ct);

        // 4. Generate new tokens
        var newTokens = _tokenService.GenerateTokens(user, roles!);

        // 5. Store new refresh token
        var newRefreshToken = new RefreshToken
        {
            Token = newTokens.RefreshToken,
            UserId = userId,
            Created = DateTime.UtcNow,
            Expires = DateTime.UtcNow.AddDays(7),
            ReplacedByToken = storedToken.Token // Track token chain
        };

        _context.RefreshTokens.Add(newRefreshToken);
        await _context.SaveChangesAsync(ct);

        _logger.LogInformation(
            "Refresh token rotated for user {UserId}", userId);

        return newTokens;
    }

    private ClaimsPrincipal? GetPrincipalFromExpiredToken(string token)
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = false, // Don't validate expiration
            ValidIssuer = _settings.Issuer,
            ValidAudience = _settings.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(_settings.SecretKey))
        };

        var tokenHandler = new JwtSecurityTokenHandler();

        try
        {
            var principal = tokenHandler.ValidateToken(
                token,
                tokenValidationParameters,
                out var securityToken);

            if (securityToken is not JwtSecurityToken jwtToken ||
                !jwtToken.Header.Alg.Equals(
                    SecurityAlgorithms.HmacSha256,
                    StringComparison.InvariantCultureIgnoreCase))
            {
                return null;
            }

            return principal;
        }
        catch
        {
            return null;
        }
    }

    private async Task RevokeAllUserTokensAsync(string userId, CancellationToken ct)
    {
        var tokens = await _context.RefreshTokens
            .Where(rt => rt.UserId == userId && !rt.IsRevoked)
            .ToListAsync(ct);

        foreach (var token in tokens)
        {
            token.Revoked = DateTime.UtcNow;
            token.RevokedReason = "Security - Potential token theft";
        }

        await _context.SaveChangesAsync(ct);
    }
}

Token Revocation Strategies

// Strategy 1: Token Blacklist (Redis)
public class TokenBlacklistService
{
    private readonly IDistributedCache _cache;

    public async Task RevokeTokenAsync(string jti, TimeSpan remaining)
    {
        await _cache.SetStringAsync(
            $"revoked:{jti}",
            "revoked",
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = remaining
            });
    }

    public async Task<bool> IsRevokedAsync(string jti)
    {
        var result = await _cache.GetStringAsync($"revoked:{jti}");
        return result != null;
    }
}

// Strategy 2: Token Versioning
public class User
{
    public string Id { get; set; }
    public int TokenVersion { get; set; } = 1;
    // Increment TokenVersion to invalidate all tokens
}

// Include version in token claims, validate on each request

[SECURITY] Common JWT Vulnerabilities and Mitigations

// VULNERABILITY 1: Algorithm confusion attack
// Attack: Changing "alg" header from RS256 to HS256 using public key as secret

// MITIGATION: Always specify expected algorithm
options.TokenValidationParameters = new TokenValidationParameters
{
    ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256 },
    // Or for asymmetric:
    // ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 }
};

// VULNERABILITY 2: None algorithm attack
// Attack: Setting "alg" to "none" to bypass signature validation

// MITIGATION: .NET validates by default, but ensure:
options.TokenValidationParameters = new TokenValidationParameters
{
    RequireSignedTokens = true,
    ValidateIssuerSigningKey = true
};

// VULNERABILITY 3: Weak secret key
// Attack: Brute-forcing short/weak secrets

// MITIGATION: Use strong keys (256+ bits)
// Generate secure key:
// var key = new byte[64];
// RandomNumberGenerator.Fill(key);
// var base64Key = Convert.ToBase64String(key);

// VULNERABILITY 4: Token stored in localStorage (XSS vulnerable)
// Attack: XSS script reads token from localStorage

// MITIGATION: Use HttpOnly cookies for refresh tokens
services.ConfigureApplicationCookie(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Strict;
});

// VULNERABILITY 5: Long-lived access tokens
// Attack: Stolen token remains valid too long

// MITIGATION: Short access tokens + refresh token rotation
var accessTokenExpiration = TimeSpan.FromMinutes(15);
var refreshTokenExpiration = TimeSpan.FromDays(7);

3.2 Authorization Pipeline Internals

Policy-Based Authorization

// Define policies
builder.Services.AddAuthorization(options =>
{
    // Simple policy with required claim
    options.AddPolicy("EmailVerified", policy =>
        policy.RequireClaim("email_verified", "true"));

    // Policy with multiple requirements (AND logic)
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin")
              .RequireClaim("department", "IT"));

    // Policy with custom requirement
    options.AddPolicy("MinimumAge", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(18)));

    // Fallback policy for all endpoints
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

IAuthorizationHandler Execution Flow

// Custom requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }
    public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
}

// Handler for the requirement
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst("date_of_birth");

        if (dateOfBirthClaim == null)
        {
            // Don't call context.Fail() - let other handlers try
            return Task.CompletedTask;
        }

        if (!DateTime.TryParse(dateOfBirthClaim.Value, out var dateOfBirth))
        {
            return Task.CompletedTask;
        }

        var age = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-age)) age--;

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// Registration
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

Resource-Based Authorization

// Resource requirement
public class DocumentAuthorizationRequirement : IAuthorizationRequirement
{
    public string Permission { get; }
    public DocumentAuthorizationRequirement(string permission) => Permission = permission;
}

// Handler that accesses the resource
public class DocumentAuthorizationHandler
    : AuthorizationHandler<DocumentAuthorizationRequirement, Document>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        DocumentAuthorizationRequirement requirement,
        Document resource)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;

        var isAuthorized = requirement.Permission switch
        {
            "Read" => resource.IsPublic || resource.OwnerId == userId ||
                      resource.SharedWith.Contains(userId),
            "Write" => resource.OwnerId == userId,
            "Delete" => resource.OwnerId == userId &&
                       context.User.IsInRole("Admin"),
            _ => false
        };

        if (isAuthorized)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

// Usage in controller
[ApiController]
[Route("api/[controller]")]
public class DocumentsController : ControllerBase
{
    private readonly IAuthorizationService _authorizationService;
    private readonly IDocumentRepository _repository;

    [HttpGet("{id}")]
    public async Task<IActionResult> GetDocument(int id)
    {
        var document = await _repository.GetByIdAsync(id);
        if (document == null) return NotFound();

        var authResult = await _authorizationService.AuthorizeAsync(
            User,
            document,
            new DocumentAuthorizationRequirement("Read"));

        if (!authResult.Succeeded)
        {
            return Forbid();
        }

        return Ok(document);
    }
}

Permission-Based Authorization with Policies

// Define permissions
public static class Permissions
{
    public const string UsersRead = "users:read";
    public const string UsersWrite = "users:write";
    public const string UsersDelete = "users:delete";
    public const string ReportsView = "reports:view";
    public const string ReportsExport = "reports:export";
}

// Permission requirement
public class PermissionRequirement : IAuthorizationRequirement
{
    public string Permission { get; }
    public PermissionRequirement(string permission) => Permission = permission;
}

// Permission handler
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    private readonly IPermissionService _permissionService;

    public PermissionHandler(IPermissionService permissionService)
    {
        _permissionService = permissionService;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionRequirement requirement)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (userId == null) return;

        var hasPermission = await _permissionService
            .HasPermissionAsync(userId, requirement.Permission);

        if (hasPermission)
        {
            context.Succeed(requirement);
        }
    }
}

// Policy provider for dynamic policies
public class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
    private readonly DefaultAuthorizationPolicyProvider _fallbackProvider;

    public PermissionPolicyProvider(IOptions<AuthorizationOptions> options)
    {
        _fallbackProvider = new DefaultAuthorizationPolicyProvider(options);
    }

    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
        if (policyName.StartsWith("Permission:"))
        {
            var permission = policyName.Substring("Permission:".Length);
            var policy = new AuthorizationPolicyBuilder()
                .AddRequirements(new PermissionRequirement(permission))
                .Build();
            return Task.FromResult<AuthorizationPolicy?>(policy);
        }

        return _fallbackProvider.GetPolicyAsync(policyName);
    }

    public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
        _fallbackProvider.GetDefaultPolicyAsync();

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() =>
        _fallbackProvider.GetFallbackPolicyAsync();
}

// Custom attribute for cleaner syntax
public class RequirePermissionAttribute : AuthorizeAttribute
{
    public RequirePermissionAttribute(string permission)
    {
        Policy = $"Permission:{permission}";
    }
}

// Usage
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    [RequirePermission(Permissions.UsersRead)]
    public IActionResult GetUsers() => Ok();

    [HttpPost]
    [RequirePermission(Permissions.UsersWrite)]
    public IActionResult CreateUser() => Ok();

    [HttpDelete("{id}")]
    [RequirePermission(Permissions.UsersDelete)]
    public IActionResult DeleteUser(int id) => Ok();
}

[INTERNALS] How AuthorizeAttribute is Processed

// The authorization flow:
// 1. AuthorizationMiddleware runs after UseRouting()
// 2. It gets the endpoint and its metadata
// 3. Finds all IAuthorizeData (AuthorizeAttribute, etc.)
// 4. Builds combined policy from all attributes
// 5. Calls IAuthorizationService.AuthorizeAsync()
// 6. If failed, returns 401 (not authenticated) or 403 (not authorized)

// Simplified internal flow:
public class AuthorizationMiddleware
{
    public async Task Invoke(HttpContext context)
    {
        var endpoint = context.GetEndpoint();
        if (endpoint == null)
        {
            await _next(context);
            return;
        }

        // Get all authorize data from endpoint metadata
        var authorizeData = endpoint.Metadata
            .GetOrderedMetadata<IAuthorizeData>();

        if (!authorizeData.Any())
        {
            await _next(context);
            return;
        }

        // Combine all policies
        var policy = await AuthorizationPolicy
            .CombineAsync(_policyProvider, authorizeData);

        // Authorize
        var result = await _authorizationService
            .AuthorizeAsync(context.User, endpoint, policy);

        if (!result.Succeeded)
        {
            if (!context.User.Identity?.IsAuthenticated ?? true)
            {
                await context.ChallengeAsync(); // 401
            }
            else
            {
                await context.ForbidAsync(); // 403
            }
            return;
        }

        await _next(context);
    }
}

4. Performance & Caching

4.1 Output Caching Deep Dive

Output Caching vs Response Caching vs Distributed Caching

Feature Output Caching Response Caching Distributed Caching
Location Server-side Client/Proxy External store
Control Full server control HTTP headers Manual
Invalidation Tag-based, manual TTL only Manual
Sharing Same server Per client Across servers
Use Case API responses Static content Session, complex data

Output Caching Implementation (.NET 7+)

// Program.cs
builder.Services.AddOutputCache(options =>
{
    // Default policy
    options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(10)));

    // Named policies
    options.AddPolicy("CacheForOneHour", builder =>
        builder.Expire(TimeSpan.FromHours(1)));

    options.AddPolicy("CacheByUser", builder =>
        builder.SetVaryByHeader("Authorization")
               .Expire(TimeSpan.FromMinutes(5)));

    options.AddPolicy("CacheWithTags", builder =>
        builder.Tag("products")
               .Expire(TimeSpan.FromMinutes(30)));
});

var app = builder.Build();
app.UseOutputCache();

// Usage
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IOutputCacheStore _cacheStore;

    [HttpGet]
    [OutputCache(PolicyName = "CacheWithTags")]
    public async Task<IActionResult> GetProducts()
    {
        // This response will be cached with "products" tag
        return Ok(await _repository.GetAllAsync());
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct(ProductDto dto)
    {
        await _repository.CreateAsync(dto);

        // Invalidate all cached responses with "products" tag
        await _cacheStore.EvictByTagAsync("products", default);

        return Created();
    }
}

Cache Key Generation

// Custom cache key policy
public class CustomCachePolicy : IOutputCachePolicy
{
    public ValueTask CacheRequestAsync(
        OutputCacheContext context,
        CancellationToken cancellation)
    {
        var attemptCache = AttemptOutputCaching(context);
        context.EnableOutputCaching = attemptCache;
        context.AllowCacheLookup = attemptCache;
        context.AllowCacheStorage = attemptCache;
        context.AllowLocking = true;

        // Add custom cache key parts
        context.CacheVaryByRules.QueryKeys = new[] { "page", "pageSize" };
        context.CacheVaryByRules.HeaderNames = new[] { "Accept-Language" };
        context.CacheVaryByRules.VaryByPrefix =
            context.HttpContext.User.FindFirst("tenant_id")?.Value ?? "default";

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        var request = context.HttpContext.Request;

        // Only cache GET and HEAD
        if (!HttpMethods.IsGet(request.Method) &&
            !HttpMethods.IsHead(request.Method))
        {
            return false;
        }

        // Don't cache authenticated requests (unless policy allows)
        if (context.HttpContext.User.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }

    public ValueTask ServeFromCacheAsync(
        OutputCacheContext context,
        CancellationToken cancellation) => ValueTask.CompletedTask;

    public ValueTask ServeResponseAsync(
        OutputCacheContext context,
        CancellationToken cancellation)
    {
        var response = context.HttpContext.Response;

        // Don't cache error responses
        if (response.StatusCode >= 400)
        {
            context.AllowCacheStorage = false;
        }

        return ValueTask.CompletedTask;
    }
}

// Registration
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("Custom", new CustomCachePolicy());
});

[BENCHMARK] Caching Performance Numbers

// Benchmark: 10,000 requests to /api/products

| Scenario                  | Avg Latency | RPS      | CPU Usage |
|--------------------------|-------------|----------|-----------|
| No caching               | 45ms        | 220      | 85%       |
| Output cache (memory)    | 2ms         | 5,000    | 15%       |
| Output cache (Redis)     | 8ms         | 1,250    | 25%       |
| Response cache (client)  | 0.5ms*      | N/A      | 5%        |

// *Client-side, no server request

[PRODUCTION] Cache Stampede Prevention

// Problem: Cache expires, 1000 concurrent requests hit database
// Solution: Lock while regenerating cache

public class CacheStampedePreventionService
{
    private readonly IDistributedCache _cache;
    private readonly IDistributedLockProvider _lockProvider;

    public async Task<T?> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan expiration,
        CancellationToken ct = default) where T : class
    {
        // Try to get from cache
        var cached = await _cache.GetStringAsync(key, ct);
        if (cached != null)
        {
            return JsonSerializer.Deserialize<T>(cached);
        }

        // Acquire lock to prevent stampede
        await using var lockHandle = await _lockProvider
            .TryAcquireLockAsync($"lock:{key}", TimeSpan.FromSeconds(30), ct);

        if (lockHandle == null)
        {
            // Another process is regenerating, wait and retry
            await Task.Delay(100, ct);
            return await GetOrSetAsync(key, factory, expiration, ct);
        }

        // Double-check after acquiring lock
        cached = await _cache.GetStringAsync(key, ct);
        if (cached != null)
        {
            return JsonSerializer.Deserialize<T>(cached);
        }

        // Generate new value
        var value = await factory();

        if (value != null)
        {
            await _cache.SetStringAsync(
                key,
                JsonSerializer.Serialize(value),
                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = expiration
                },
                ct);
        }

        return value;
    }
}

4.2 Minimal APIs vs Controllers

Architecture Comparison

// MINIMAL API
var app = WebApplication.Create(args);

app.MapGet("/api/products", async (IProductService service) =>
    await service.GetAllAsync());

app.MapGet("/api/products/{id}", async (int id, IProductService service) =>
    await service.GetByIdAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());

app.MapPost("/api/products", async (CreateProductDto dto, IProductService service) =>
{
    var product = await service.CreateAsync(dto);
    return Results.Created($"/api/products/{product.Id}", product);
});

// CONTROLLER-BASED
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _service;

    public ProductsController(IProductService service) => _service = service;

    [HttpGet]
    public async Task<IActionResult> GetAll() =>
        Ok(await _service.GetAllAsync());

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id) =>
        await _service.GetByIdAsync(id) is Product product
            ? Ok(product)
            : NotFound();

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductDto dto)
    {
        var product = await _service.CreateAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

[BENCHMARK] Performance Comparison

// Benchmark: Simple GET endpoint returning JSON

| Metric               | Minimal API | Controllers | Difference |
|---------------------|-------------|-------------|------------|
| Requests/sec        | 145,000     | 125,000     | +16%       |
| Latency (p50)       | 0.68ms      | 0.79ms      | -14%       |
| Latency (p99)       | 1.2ms       | 1.5ms       | -20%       |
| Memory (per request)| 1.2KB       | 1.8KB       | -33%       |
| Startup time        | 180ms       | 250ms       | -28%       |

// Note: Differences decrease as endpoint complexity increases
// For complex business logic, the difference is negligible

Feature Comparison Matrix

Feature Minimal APIs Controllers
Model binding Yes Yes
Validation Manual/FluentValidation DataAnnotations built-in
Filters Endpoint filters Action/Result filters
API versioning Manual Built-in support
OpenAPI/Swagger Yes Yes
Model state Manual Automatic
Content negotiation Yes Yes
Testability Good Excellent
Large project organization Challenging Excellent

When to Use Each

// USE MINIMAL APIS:
// - Microservices with few endpoints
// - Simple CRUD APIs
// - High-performance requirements
// - Serverless functions
// - Prototypes and POCs

// USE CONTROLLERS:
// - Large applications with many endpoints
// - Complex validation requirements
// - Need for extensive filtering
// - Team familiar with MVC patterns
// - Complex authorization scenarios
// - API versioning requirements

// HYBRID APPROACH (Best of both):
var app = builder.Build();

// Simple endpoints as Minimal APIs
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));

// Complex endpoints in Controllers
app.MapControllers();

5. API Design Advanced Patterns

5.1 API Versioning Strategies

URL Path Versioning

// Setup
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

// Controller
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok(new { version = "1.0", data = "old format" });

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok(new { version = "2.0", data = new { enhanced = true } });
}

// URLs:
// GET /api/v1/products -> GetV1()
// GET /api/v2/products -> GetV2()

Header Versioning (Without URL Changes)

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ApiVersionReader = new HeaderApiVersionReader("X-API-Version");
});

// Request:
// GET /api/products
// X-API-Version: 2.0

Media Type Versioning

builder.Services.AddApiVersioning(options =>
{
    options.ApiVersionReader = new MediaTypeApiVersionReader("version");
});

// Request:
// GET /api/products
// Accept: application/json;version=2.0

Combined Versioning Strategy

builder.Services.AddApiVersioning(options =>
{
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-API-Version"),
        new QueryStringApiVersionReader("api-version"),
        new MediaTypeApiVersionReader("version"));
});

// All of these work:
// GET /api/v2/products
// GET /api/products?api-version=2.0
// GET /api/products (with X-API-Version: 2.0)
// GET /api/products (with Accept: application/json;version=2.0)

[PRODUCTION] Version Deprecation

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0", Deprecated = true)] // Marked as deprecated
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
    // V1 endpoints will include header:
    // api-deprecated-versions: 1.0
}

// Sunset header middleware
public class SunsetMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        await next(context);

        var apiVersion = context.GetRequestedApiVersion();
        if (apiVersion?.MajorVersion == 1)
        {
            context.Response.Headers["Sunset"] = "Sat, 31 Dec 2024 23:59:59 GMT";
            context.Response.Headers["Deprecation"] = "true";
            context.Response.Headers["Link"] =
                "</api/v2/products>; rel=\"successor-version\"";
        }
    }
}

5.2 Request/Response Handling

Logging Request/Response Bodies Safely

public class RequestResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
    private readonly ISensitiveDataMasker _masker;

    public async Task InvokeAsync(HttpContext context)
    {
        // Log request
        var requestBody = await ReadRequestBodyAsync(context.Request);
        var maskedRequestBody = _masker.Mask(requestBody);

        _logger.LogInformation(
            "Request {Method} {Path} Body: {Body}",
            context.Request.Method,
            context.Request.Path,
            maskedRequestBody);

        // Capture response
        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;

        await _next(context);

        // Log response
        var responseBodyText = await ReadResponseBodyAsync(context.Response);
        var maskedResponseBody = _masker.Mask(responseBodyText);

        _logger.LogInformation(
            "Response {StatusCode} Body: {Body}",
            context.Response.StatusCode,
            maskedResponseBody);

        // Copy response back
        responseBody.Seek(0, SeekOrigin.Begin);
        await responseBody.CopyToAsync(originalBodyStream);
    }

    private async Task<string> ReadRequestBodyAsync(HttpRequest request)
    {
        request.EnableBuffering();
        request.Body.Position = 0;

        using var reader = new StreamReader(
            request.Body,
            Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            leaveOpen: true);

        var body = await reader.ReadToEndAsync();
        request.Body.Position = 0;

        return body;
    }

    private async Task<string> ReadResponseBodyAsync(HttpResponse response)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        var text = await new StreamReader(response.Body).ReadToEndAsync();
        response.Body.Seek(0, SeekOrigin.Begin);
        return text;
    }
}

[SECURITY] Sensitive Data Masking

public interface ISensitiveDataMasker
{
    string Mask(string input);
}

public class SensitiveDataMasker : ISensitiveDataMasker
{
    private static readonly Regex[] SensitivePatterns = new[]
    {
        // Credit card numbers
        new Regex(@"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", RegexOptions.Compiled),

        // SSN
        new Regex(@"\b\d{3}[-]?\d{2}[-]?\d{4}\b", RegexOptions.Compiled),

        // Email (partial mask)
        new Regex(@"\b[\w.-]+@[\w.-]+\.\w+\b", RegexOptions.Compiled),

        // Phone numbers
        new Regex(@"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", RegexOptions.Compiled),
    };

    private static readonly string[] SensitiveFields = new[]
    {
        "password", "token", "secret", "apikey", "api_key",
        "authorization", "credit_card", "ssn", "cvv"
    };

    public string Mask(string input)
    {
        if (string.IsNullOrEmpty(input)) return input;

        var masked = input;

        // Mask patterns
        foreach (var pattern in SensitivePatterns)
        {
            masked = pattern.Replace(masked, match =>
            {
                var value = match.Value;
                if (value.Contains('@'))
                {
                    // Email: show first 2 chars
                    var atIndex = value.IndexOf('@');
                    return value[..2] + "***" + value[atIndex..];
                }
                // Other: show last 4 chars
                return "****" + value[^4..];
            });
        }

        // Mask JSON fields
        try
        {
            var json = JsonDocument.Parse(masked);
            masked = MaskJsonDocument(json);
        }
        catch
        {
            // Not JSON, continue
        }

        return masked;
    }

    private string MaskJsonDocument(JsonDocument doc)
    {
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });

        MaskJsonElement(doc.RootElement, writer);

        writer.Flush();
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    private void MaskJsonElement(JsonElement element, Utf8JsonWriter writer)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                writer.WriteStartObject();
                foreach (var property in element.EnumerateObject())
                {
                    writer.WritePropertyName(property.Name);
                    if (SensitiveFields.Any(f =>
                        property.Name.Contains(f, StringComparison.OrdinalIgnoreCase)))
                    {
                        writer.WriteStringValue("***REDACTED***");
                    }
                    else
                    {
                        MaskJsonElement(property.Value, writer);
                    }
                }
                writer.WriteEndObject();
                break;

            case JsonValueKind.Array:
                writer.WriteStartArray();
                foreach (var item in element.EnumerateArray())
                {
                    MaskJsonElement(item, writer);
                }
                writer.WriteEndArray();
                break;

            default:
                element.WriteTo(writer);
                break;
        }
    }
}

File Streaming with Chunked Transfer Encoding

[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
    [HttpGet("{id}/download")]
    public async Task<IActionResult> DownloadFile(
        int id,
        [FromServices] IFileService fileService,
        CancellationToken ct)
    {
        var fileInfo = await fileService.GetFileInfoAsync(id, ct);
        if (fileInfo == null) return NotFound();

        // Stream large files without loading entirely into memory
        var stream = await fileService.OpenReadStreamAsync(id, ct);

        return File(
            stream,
            fileInfo.ContentType,
            fileInfo.FileName,
            enableRangeProcessing: true); // Enables partial content (206)
    }

    [HttpGet("export")]
    public async IAsyncEnumerable<ProductDto> ExportProducts(
        [FromServices] IProductRepository repository,
        [EnumeratorCancellation] CancellationToken ct)
    {
        // Stream results as they're fetched
        await foreach (var product in repository.StreamAllAsync(ct))
        {
            yield return product;
        }
    }
}

// Repository implementation
public class ProductRepository : IProductRepository
{
    public async IAsyncEnumerable<ProductDto> StreamAllAsync(
        [EnumeratorCancellation] CancellationToken ct)
    {
        await foreach (var product in _context.Products
            .AsNoTracking()
            .AsAsyncEnumerable()
            .WithCancellation(ct))
        {
            yield return new ProductDto(product);
        }
    }
}

6. Background Processing

6.1 BackgroundService Deep Dive

IHostedService Lifecycle

public interface IHostedService
{
    // Called when the application host is ready to start the service
    // Should start background work (don't block!)
    Task StartAsync(CancellationToken cancellationToken);

    // Called when the application host is performing a graceful shutdown
    // Should stop background work and clean up
    Task StopAsync(CancellationToken cancellationToken);
}

BackgroundService vs IHostedService

// IHostedService - Full control
public class CustomHostedService : IHostedService
{
    private Task? _executingTask;
    private CancellationTokenSource? _cts;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executingTask = ExecuteAsync(_cts.Token);

        // Return immediately, don't await
        return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executingTask == null) return;

        _cts?.Cancel();

        // Wait for the task to complete or timeout
        await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
    }

    private async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            await DoWorkAsync(ct);
            await Task.Delay(TimeSpan.FromMinutes(1), ct);
        }
    }
}

// BackgroundService - Simplified
public class SimpleBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // BackgroundService handles StartAsync/StopAsync for you
        while (!stoppingToken.IsCancellationRequested)
        {
            await DoWorkAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Graceful Shutdown Implementation

public class GracefulBackgroundService : BackgroundService
{
    private readonly ILogger<GracefulBackgroundService> _logger;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly Channel<WorkItem> _queue;

    public GracefulBackgroundService(
        ILogger<GracefulBackgroundService> logger,
        IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
        _queue = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.Wait
        });
    }

    public async ValueTask QueueWorkAsync(WorkItem item, CancellationToken ct = default)
    {
        await _queue.Writer.WriteAsync(item, ct);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Background service starting");

        try
        {
            await foreach (var workItem in _queue.Reader.ReadAllAsync(stoppingToken))
            {
                try
                {
                    using var scope = _scopeFactory.CreateScope();
                    await ProcessWorkItemAsync(workItem, scope.ServiceProvider, stoppingToken);
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    // Graceful shutdown requested during work item processing
                    _logger.LogInformation(
                        "Shutdown requested, saving work item {Id} for later",
                        workItem.Id);

                    // Re-queue or persist for later processing
                    await SaveUnprocessedWorkAsync(workItem);
                    throw;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error processing work item {Id}", workItem.Id);
                }
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Background service shutting down gracefully");
        }

        // Drain remaining items before shutdown
        while (_queue.Reader.TryRead(out var remainingItem))
        {
            await SaveUnprocessedWorkAsync(remainingItem);
        }

        _logger.LogInformation("Background service stopped");
    }

    private async Task ProcessWorkItemAsync(
        WorkItem item,
        IServiceProvider services,
        CancellationToken ct)
    {
        var processor = services.GetRequiredService<IWorkItemProcessor>();
        await processor.ProcessAsync(item, ct);
    }

    private async Task SaveUnprocessedWorkAsync(WorkItem item)
    {
        // Save to database or message queue for later processing
        using var scope = _scopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService<IWorkItemRepository>();
        await repository.SaveForLaterAsync(item);
    }
}

Error Handling with Polly

public class ResilientBackgroundService : BackgroundService
{
    private readonly ILogger<ResilientBackgroundService> _logger;
    private readonly AsyncRetryPolicy _retryPolicy;
    private readonly AsyncCircuitBreakerPolicy _circuitBreaker;

    public ResilientBackgroundService(ILogger<ResilientBackgroundService> logger)
    {
        _logger = logger;

        _retryPolicy = Policy
            .Handle<HttpRequestException>()
            .Or<TimeoutException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: attempt =>
                    TimeSpan.FromSeconds(Math.Pow(2, attempt)),
                onRetry: (exception, timespan, attempt, context) =>
                {
                    _logger.LogWarning(
                        exception,
                        "Retry {Attempt} after {Delay}s",
                        attempt,
                        timespan.TotalSeconds);
                });

        _circuitBreaker = Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromMinutes(1),
                onBreak: (exception, duration) =>
                {
                    _logger.LogError(
                        exception,
                        "Circuit breaker opened for {Duration}",
                        duration);
                },
                onReset: () =>
                {
                    _logger.LogInformation("Circuit breaker reset");
                });
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var policy = Policy.WrapAsync(_retryPolicy, _circuitBreaker);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await policy.ExecuteAsync(async ct =>
                {
                    await ProcessWorkAsync(ct);
                }, stoppingToken);
            }
            catch (BrokenCircuitException)
            {
                _logger.LogWarning("Circuit is open, waiting before retry");
                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                _logger.LogError(ex, "Unhandled exception in background service");
                await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
            }

            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
}

[INTERNALS] Host Shutdown Sequence

// Shutdown sequence:
// 1. SIGTERM/Ctrl+C received
// 2. IHostApplicationLifetime.StopApplication() called
// 3. ApplicationStopping triggered
// 4. All IHostedService.StopAsync() called (reverse order of startup)
// 5. ApplicationStopped triggered
// 6. Host disposed

// Configure shutdown timeout
builder.Host.ConfigureHostOptions(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});

// Handle shutdown events
public class ShutdownHandler : IHostedService
{
    private readonly IHostApplicationLifetime _lifetime;
    private readonly ILogger<ShutdownHandler> _logger;

    public ShutdownHandler(
        IHostApplicationLifetime lifetime,
        ILogger<ShutdownHandler> logger)
    {
        _lifetime = lifetime;
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _lifetime.ApplicationStarted.Register(() =>
            _logger.LogInformation("Application started"));

        _lifetime.ApplicationStopping.Register(() =>
            _logger.LogInformation("Application stopping..."));

        _lifetime.ApplicationStopped.Register(() =>
            _logger.LogInformation("Application stopped"));

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken) =>
        Task.CompletedTask;
}

7. Advanced DI Patterns

7.1 Injection Patterns Comparison

Constructor Injection (Recommended)

public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<OrderService> _logger;

    // Dependencies declared in constructor - explicit and testable
    public OrderService(
        IOrderRepository orderRepository,
        IPaymentService paymentService,
        ILogger<OrderService> logger)
    {
        _orderRepository = orderRepository;
        _paymentService = paymentService;
        _logger = logger;
    }
}

Method Injection ([FromServices])

[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
    // Use method injection for rarely-used dependencies
    [HttpGet("sales")]
    public async Task<IActionResult> GetSalesReport(
        [FromServices] ISalesReportGenerator generator,
        [FromServices] IReportCache cache,
        [FromQuery] DateTime from,
        [FromQuery] DateTime to)
    {
        var cacheKey = $"sales:{from:yyyyMMdd}:{to:yyyyMMdd}";

        var report = await cache.GetOrCreateAsync(cacheKey, async () =>
            await generator.GenerateAsync(from, to));

        return Ok(report);
    }

    // This action doesn't need ISalesReportGenerator
    [HttpGet("inventory")]
    public async Task<IActionResult> GetInventoryReport(
        [FromServices] IInventoryReportGenerator generator)
    {
        return Ok(await generator.GenerateAsync());
    }
}

Factory Patterns

// Generic factory interface
public interface IFactory<T>
{
    T Create();
}

// Implementation
public class DbContextFactory : IFactory<ApplicationDbContext>
{
    private readonly IServiceProvider _serviceProvider;

    public DbContextFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public ApplicationDbContext Create()
    {
        return _serviceProvider.GetRequiredService<ApplicationDbContext>();
    }
}

// Usage in singleton service
public class SingletonService
{
    private readonly IFactory<ApplicationDbContext> _dbContextFactory;

    public SingletonService(IFactory<ApplicationDbContext> dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }

    public async Task DoWorkAsync()
    {
        // Create new DbContext for this operation
        using var context = _dbContextFactory.Create();
        await context.Users.ToListAsync();
    }
}

// Or use the built-in IDbContextFactory
public class AnotherSingletonService
{
    private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

    public AnotherSingletonService(IDbContextFactory<ApplicationDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task DoWorkAsync()
    {
        await using var context = await _contextFactory.CreateDbContextAsync();
        await context.Users.ToListAsync();
    }
}

Func and Lazy Injection

// Func<T> - Create new instance on demand
public class ServiceWithFuncInjection
{
    private readonly Func<IExpensiveService> _expensiveServiceFactory;

    public ServiceWithFuncInjection(Func<IExpensiveService> expensiveServiceFactory)
    {
        _expensiveServiceFactory = expensiveServiceFactory;
    }

    public void DoWork()
    {
        if (SomeCondition)
        {
            // Only creates the service when needed
            var service = _expensiveServiceFactory();
            service.Process();
        }
    }
}

// Registration for Func<T>
services.AddTransient<IExpensiveService, ExpensiveService>();
services.AddTransient<Func<IExpensiveService>>(sp =>
    () => sp.GetRequiredService<IExpensiveService>());

// Lazy<T> - Delayed initialization
public class ServiceWithLazyInjection
{
    private readonly Lazy<IExpensiveService> _expensiveService;

    public ServiceWithLazyInjection(Lazy<IExpensiveService> expensiveService)
    {
        _expensiveService = expensiveService;
    }

    public void DoWork()
    {
        if (SomeCondition)
        {
            // Initialized on first access, then cached
            _expensiveService.Value.Process();
        }
    }
}

// Registration for Lazy<T>
services.AddTransient<Lazy<IExpensiveService>>(sp =>
    new Lazy<IExpensiveService>(() => sp.GetRequiredService<IExpensiveService>()));

[BENCHMARK] Performance Comparison

// Benchmark: Resolving dependencies 100,000 times

| Pattern              | Time    | Allocations | Notes |
|---------------------|---------|-------------|-------|
| Constructor (cached) | 0.8ms   | 1           | Best for frequently used |
| Constructor (new)    | 85ms    | 100,000     | Transient services |
| [FromServices]       | 92ms    | 100,000     | Slight overhead vs ctor |
| Func<T>              | 95ms    | 200,000     | Extra delegate allocation |
| Lazy<T>              | 1.2ms   | 1           | Best for conditional use |
| IServiceScopeFactory | 180ms   | 200,000     | Scope creation overhead |

[PRODUCTION] When to Use Each Pattern

// CONSTRUCTOR INJECTION
// Use for: Required dependencies that are used in most methods
// Pros: Clear dependencies, easy to test, compile-time checking
// Cons: All dependencies created even if not used

// METHOD INJECTION ([FromServices])
// Use for: Optional or rarely-used dependencies in controllers
// Pros: Only resolved when needed
// Cons: Less discoverable, harder to test

// FUNC<T> / LAZY<T>
// Use for: Expensive services that may not be needed
// Pros: Delayed creation
// Cons: More complex, extra allocations

// ISERVICESCOPEFACTORY
// Use for: Scoped services in singletons (background services)
// Pros: Proper scope management
// Cons: Manual scope handling, more code

// FACTORY PATTERN
// Use for: Complex object creation, runtime decisions
// Pros: Flexible, testable
// Cons: More interfaces to maintain

8. File Streaming & Large Data Transfer

8.1 Chunked File Downloads

File Result Types Comparison

ASP.NET Core provides several ways to return files from API endpoints:

// FileContentResult - Entire file loaded in memory
// Use for: Small files only (< 10MB)
[HttpGet("download/small/{id}")]
public async Task<IActionResult> DownloadSmall(int id)
{
    var content = await _fileService.GetFileContentAsync(id);
    return File(content, "application/octet-stream", "document.pdf");
}

// FileStreamResult - Stream without loading entire file
// Use for: Medium files, sequential reading
[HttpGet("download/medium/{id}")]
public async Task<IActionResult> DownloadMedium(int id)
{
    var stream = await _fileService.GetFileStreamAsync(id);
    return File(stream, "application/octet-stream", "document.pdf");
}

// PhysicalFileResult - Serve file directly from disk
// Use for: Static files, best performance
[HttpGet("download/physical/{id}")]
public IActionResult DownloadPhysical(int id)
{
    var filePath = _fileService.GetFilePath(id);
    return PhysicalFile(filePath, "application/octet-stream", "document.pdf");
}

// VirtualFileResult - Files from wwwroot
[HttpGet("download/virtual/{fileName}")]
public IActionResult DownloadVirtual(string fileName)
{
    return File($"~/files/{fileName}", "application/octet-stream");
}

Performance Comparison:

Method Memory Usage Best For Supports Range
FileContentResult High (full file) Small files < 10MB No
FileStreamResult Low (buffered) Medium files Yes*
PhysicalFileResult Minimal Disk files, large files Yes
VirtualFileResult Minimal wwwroot files Yes

*With proper configuration

[CODE] Chunked Download with Resume Support (HTTP 206)

[ApiController]
[Route("api/[controller]")]
public class FileDownloadController : ControllerBase
{
    private readonly IFileStorageService _storage;
    private readonly ILogger<FileDownloadController> _logger;
    private const int DefaultChunkSize = 1024 * 1024; // 1MB chunks

    public FileDownloadController(
        IFileStorageService storage,
        ILogger<FileDownloadController> logger)
    {
        _storage = storage;
        _logger = logger;
    }

    [HttpGet("{fileId}")]
    public async Task<IActionResult> DownloadFile(
        Guid fileId,
        CancellationToken cancellationToken)
    {
        var fileInfo = await _storage.GetFileInfoAsync(fileId, cancellationToken);
        if (fileInfo == null)
            return NotFound();

        var fileLength = fileInfo.Length;
        var contentType = fileInfo.ContentType ?? "application/octet-stream";

        // Check for Range header (resume support)
        var rangeHeader = Request.Headers.Range;

        if (rangeHeader.Count > 0 && RangeHeaderValue.TryParse(rangeHeader, out var range))
        {
            return await HandleRangeRequestAsync(
                fileId, fileInfo, range, contentType, cancellationToken);
        }

        // Full file download
        Response.Headers.AcceptRanges = "bytes";
        Response.Headers.ContentLength = fileLength;

        var stream = await _storage.OpenReadAsync(fileId, cancellationToken);
        return File(stream, contentType, fileInfo.FileName, enableRangeProcessing: true);
    }

    private async Task<IActionResult> HandleRangeRequestAsync(
        Guid fileId,
        FileMetadata fileInfo,
        RangeHeaderValue range,
        string contentType,
        CancellationToken cancellationToken)
    {
        var fileLength = fileInfo.Length;
        var rangeItem = range.Ranges.FirstOrDefault();

        if (rangeItem == null)
            return BadRequest("Invalid range");

        var from = rangeItem.From ?? 0;
        var to = rangeItem.To ?? fileLength - 1;

        // Validate range
        if (from >= fileLength || to >= fileLength || from > to)
        {
            Response.Headers.ContentRange = $"bytes */{fileLength}";
            return StatusCode(416); // Range Not Satisfiable
        }

        var length = to - from + 1;

        _logger.LogInformation(
            "Serving partial content: bytes {From}-{To}/{Total}",
            from, to, fileLength);

        Response.StatusCode = 206; // Partial Content
        Response.Headers.ContentRange = $"bytes {from}-{to}/{fileLength}";
        Response.Headers.AcceptRanges = "bytes";
        Response.ContentLength = length;
        Response.ContentType = contentType;

        var stream = await _storage.OpenReadAsync(fileId, cancellationToken);
        stream.Seek(from, SeekOrigin.Begin);

        // Read only the requested portion
        await CopyPartialStreamAsync(stream, Response.Body, length, cancellationToken);

        return new EmptyResult();
    }

    private static async Task CopyPartialStreamAsync(
        Stream source,
        Stream destination,
        long bytesToCopy,
        CancellationToken cancellationToken)
    {
        var buffer = ArrayPool<byte>.Shared.Rent(81920);
        try
        {
            var bytesRemaining = bytesToCopy;
            while (bytesRemaining > 0)
            {
                var bytesToRead = (int)Math.Min(buffer.Length, bytesRemaining);
                var bytesRead = await source.ReadAsync(
                    buffer.AsMemory(0, bytesToRead), cancellationToken);

                if (bytesRead == 0)
                    break;

                await destination.WriteAsync(
                    buffer.AsMemory(0, bytesRead), cancellationToken);

                bytesRemaining -= bytesRead;
            }
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

[PRODUCTION] Progress Reporting for Large Downloads

public class ProgressStream : Stream
{
    private readonly Stream _innerStream;
    private readonly IProgress<long> _progress;
    private long _bytesRead;

    public ProgressStream(Stream innerStream, IProgress<long> progress)
    {
        _innerStream = innerStream;
        _progress = progress;
    }

    public override async ValueTask<int> ReadAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken = default)
    {
        var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken);
        _bytesRead += bytesRead;
        _progress.Report(_bytesRead);
        return bytesRead;
    }

    // Implement other Stream members delegating to _innerStream...
    public override bool CanRead => _innerStream.CanRead;
    public override bool CanSeek => _innerStream.CanSeek;
    public override bool CanWrite => _innerStream.CanWrite;
    public override long Length => _innerStream.Length;
    public override long Position
    {
        get => _innerStream.Position;
        set => _innerStream.Position = value;
    }
    public override void Flush() => _innerStream.Flush();
    public override int Read(byte[] buffer, int offset, int count) =>
        _innerStream.Read(buffer, offset, count);
    public override long Seek(long offset, SeekOrigin origin) =>
        _innerStream.Seek(offset, origin);
    public override void SetLength(long value) => _innerStream.SetLength(value);
    public override void Write(byte[] buffer, int offset, int count) =>
        _innerStream.Write(buffer, offset, count);
}

8.2 Chunked File Uploads

IFormFile Limitations

// IFormFile buffers ENTIRE file in memory/disk before your code runs
// BAD for large files - memory exhaustion risk!
[HttpPost("upload/simple")]
public async Task<IActionResult> UploadSimple(IFormFile file)
{
    // By the time this executes, the entire file is already buffered
    // For a 2GB file, you need 2GB+ memory available
    using var stream = file.OpenReadStream();
    await _storage.SaveAsync(stream);
    return Ok();
}

[CODE] Streaming Large Uploads with MultipartReader

[ApiController]
[Route("api/[controller]")]
public class FileUploadController : ControllerBase
{
    private readonly IFileStorageService _storage;
    private readonly ILogger<FileUploadController> _logger;

    // Disable form value model binding to enable streaming
    [HttpPost("upload/stream")]
    [DisableFormValueModelBinding]
    [RequestSizeLimit(10L * 1024 * 1024 * 1024)] // 10GB limit
    [RequestFormLimits(MultipartBodyLengthLimit = 10L * 1024 * 1024 * 1024)]
    public async Task<IActionResult> UploadStreaming(CancellationToken cancellationToken)
    {
        if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
        {
            return BadRequest("Not a multipart request");
        }

        var boundary = MultipartRequestHelper.GetBoundary(
            MediaTypeHeaderValue.Parse(Request.ContentType),
            defaultFormOptions.MultipartBoundaryLengthLimit);

        var reader = new MultipartReader(boundary, Request.Body);
        var section = await reader.ReadNextSectionAsync(cancellationToken);

        var uploadedFiles = new List<UploadedFileInfo>();

        while (section != null)
        {
            var hasContentDisposition = ContentDispositionHeaderValue.TryParse(
                section.ContentDisposition, out var contentDisposition);

            if (hasContentDisposition &&
                contentDisposition!.DispositionType.Equals("form-data") &&
                !string.IsNullOrEmpty(contentDisposition.FileName.Value))
            {
                var fileName = contentDisposition.FileName.Value;
                var sanitizedFileName = Path.GetFileName(fileName);

                _logger.LogInformation("Streaming upload: {FileName}", sanitizedFileName);

                // Stream directly to storage - never fully in memory
                var fileId = await _storage.SaveStreamAsync(
                    section.Body,
                    sanitizedFileName,
                    section.ContentType ?? "application/octet-stream",
                    cancellationToken);

                uploadedFiles.Add(new UploadedFileInfo
                {
                    Id = fileId,
                    FileName = sanitizedFileName
                });
            }

            section = await reader.ReadNextSectionAsync(cancellationToken);
        }

        return Ok(uploadedFiles);
    }
}

// Attribute to disable model binding for streaming
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}

// Helper class
public static class MultipartRequestHelper
{
    public static bool IsMultipartContentType(string? contentType)
    {
        return !string.IsNullOrEmpty(contentType) &&
               contentType.Contains("multipart/", StringComparison.OrdinalIgnoreCase);
    }

    public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
    {
        var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;

        if (string.IsNullOrWhiteSpace(boundary))
            throw new InvalidDataException("Missing content-type boundary");

        if (boundary.Length > lengthLimit)
            throw new InvalidDataException($"Boundary too long: {boundary.Length} > {lengthLimit}");

        return boundary;
    }
}

[CODE] Chunk-Based Upload with Resume Capability

[ApiController]
[Route("api/[controller]")]
public class ChunkedUploadController : ControllerBase
{
    private readonly IChunkedUploadService _uploadService;

    // Step 1: Initialize upload session
    [HttpPost("init")]
    public async Task<IActionResult> InitializeUpload([FromBody] InitUploadRequest request)
    {
        var session = await _uploadService.CreateSessionAsync(
            request.FileName,
            request.TotalSize,
            request.ChunkSize);

        return Ok(new
        {
            session.UploadId,
            session.ChunkSize,
            TotalChunks = session.TotalChunks
        });
    }

    // Step 2: Upload individual chunks
    [HttpPost("{uploadId}/chunk/{chunkIndex}")]
    [RequestSizeLimit(10 * 1024 * 1024)] // 10MB per chunk
    public async Task<IActionResult> UploadChunk(
        Guid uploadId,
        int chunkIndex,
        CancellationToken cancellationToken)
    {
        var session = await _uploadService.GetSessionAsync(uploadId);
        if (session == null)
            return NotFound("Upload session not found");

        if (chunkIndex < 0 || chunkIndex >= session.TotalChunks)
            return BadRequest("Invalid chunk index");

        // Check if chunk already uploaded (for resume)
        if (session.UploadedChunks.Contains(chunkIndex))
        {
            return Ok(new { Status = "Already uploaded", chunkIndex });
        }

        await _uploadService.SaveChunkAsync(
            uploadId,
            chunkIndex,
            Request.Body,
            cancellationToken);

        var progress = await _uploadService.GetProgressAsync(uploadId);

        return Ok(new
        {
            ChunkIndex = chunkIndex,
            UploadedChunks = progress.UploadedChunksCount,
            TotalChunks = session.TotalChunks,
            PercentComplete = progress.PercentComplete
        });
    }

    // Step 3: Get upload status (for resume)
    [HttpGet("{uploadId}/status")]
    public async Task<IActionResult> GetStatus(Guid uploadId)
    {
        var session = await _uploadService.GetSessionAsync(uploadId);
        if (session == null)
            return NotFound();

        return Ok(new
        {
            session.UploadId,
            session.FileName,
            session.TotalSize,
            UploadedChunks = session.UploadedChunks.ToList(),
            MissingChunks = Enumerable.Range(0, session.TotalChunks)
                .Except(session.UploadedChunks).ToList(),
            IsComplete = session.IsComplete
        });
    }

    // Step 4: Complete upload (assemble chunks)
    [HttpPost("{uploadId}/complete")]
    public async Task<IActionResult> CompleteUpload(Guid uploadId)
    {
        var session = await _uploadService.GetSessionAsync(uploadId);
        if (session == null)
            return NotFound();

        if (!session.IsComplete)
        {
            var missing = Enumerable.Range(0, session.TotalChunks)
                .Except(session.UploadedChunks).ToList();
            return BadRequest(new { Error = "Upload incomplete", MissingChunks = missing });
        }

        var fileId = await _uploadService.AssembleChunksAsync(uploadId);

        return Ok(new { FileId = fileId, session.FileName });
    }
}

// Service implementation
public class ChunkedUploadService : IChunkedUploadService
{
    private readonly IDistributedCache _cache;
    private readonly IFileStorageService _storage;
    private readonly string _tempPath;

    public async Task<UploadSession> CreateSessionAsync(
        string fileName, long totalSize, int chunkSize)
    {
        var session = new UploadSession
        {
            UploadId = Guid.NewGuid(),
            FileName = fileName,
            TotalSize = totalSize,
            ChunkSize = chunkSize,
            TotalChunks = (int)Math.Ceiling((double)totalSize / chunkSize),
            CreatedAt = DateTime.UtcNow,
            ExpiresAt = DateTime.UtcNow.AddHours(24)
        };

        await _cache.SetStringAsync(
            $"upload:{session.UploadId}",
            JsonSerializer.Serialize(session),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = session.ExpiresAt
            });

        return session;
    }

    public async Task SaveChunkAsync(
        Guid uploadId, int chunkIndex, Stream data, CancellationToken ct)
    {
        var chunkPath = GetChunkPath(uploadId, chunkIndex);
        Directory.CreateDirectory(Path.GetDirectoryName(chunkPath)!);

        await using var fileStream = new FileStream(
            chunkPath,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            81920,
            FileOptions.Asynchronous);

        await data.CopyToAsync(fileStream, ct);

        // Update session
        var session = await GetSessionAsync(uploadId);
        session!.UploadedChunks.Add(chunkIndex);
        await UpdateSessionAsync(session);
    }

    public async Task<Guid> AssembleChunksAsync(Guid uploadId)
    {
        var session = await GetSessionAsync(uploadId);
        var finalPath = Path.Combine(_tempPath, $"{uploadId}_final");

        await using (var finalStream = new FileStream(
            finalPath, FileMode.Create, FileAccess.Write))
        {
            for (int i = 0; i < session!.TotalChunks; i++)
            {
                var chunkPath = GetChunkPath(uploadId, i);
                await using var chunkStream = File.OpenRead(chunkPath);
                await chunkStream.CopyToAsync(finalStream);
            }
        }

        // Move to permanent storage
        var fileId = await _storage.ImportFileAsync(finalPath, session.FileName);

        // Cleanup chunks
        await CleanupChunksAsync(uploadId);

        return fileId;
    }

    private string GetChunkPath(Guid uploadId, int chunkIndex) =>
        Path.Combine(_tempPath, uploadId.ToString(), $"chunk_{chunkIndex:D6}");
}

[SECURITY] File Upload Security

public class FileUploadSecurityService
{
    private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".jpg", ".jpeg", ".png", ".gif"
    };

    private static readonly Dictionary<string, byte[]> FileSignatures = new()
    {
        { ".pdf", new byte[] { 0x25, 0x50, 0x44, 0x46 } },         // %PDF
        { ".jpg", new byte[] { 0xFF, 0xD8, 0xFF } },                // JPEG
        { ".png", new byte[] { 0x89, 0x50, 0x4E, 0x47 } },          // PNG
        { ".gif", new byte[] { 0x47, 0x49, 0x46 } },                // GIF
        { ".zip", new byte[] { 0x50, 0x4B, 0x03, 0x04 } },          // ZIP/DOCX/XLSX
    };

    public async Task<FileValidationResult> ValidateFileAsync(
        Stream fileStream,
        string fileName,
        long maxSize)
    {
        var result = new FileValidationResult { IsValid = true };

        // 1. Check extension
        var extension = Path.GetExtension(fileName);
        if (!AllowedExtensions.Contains(extension))
        {
            result.IsValid = false;
            result.Errors.Add($"Extension '{extension}' not allowed");
            return result;
        }

        // 2. Check file size
        if (fileStream.Length > maxSize)
        {
            result.IsValid = false;
            result.Errors.Add($"File size {fileStream.Length} exceeds limit {maxSize}");
            return result;
        }

        // 3. Validate file signature (magic bytes)
        if (FileSignatures.TryGetValue(extension, out var expectedSignature))
        {
            var actualSignature = new byte[expectedSignature.Length];
            await fileStream.ReadAsync(actualSignature);
            fileStream.Position = 0; // Reset for later use

            if (!actualSignature.SequenceEqual(expectedSignature))
            {
                result.IsValid = false;
                result.Errors.Add("File content doesn't match extension");
                return result;
            }
        }

        // 4. Sanitize filename
        result.SanitizedFileName = SanitizeFileName(fileName);

        return result;
    }

    private static string SanitizeFileName(string fileName)
    {
        // Remove path traversal attempts
        fileName = Path.GetFileName(fileName);

        // Remove dangerous characters
        var invalidChars = Path.GetInvalidFileNameChars();
        foreach (var c in invalidChars)
        {
            fileName = fileName.Replace(c, '_');
        }

        // Limit length
        if (fileName.Length > 255)
        {
            var ext = Path.GetExtension(fileName);
            fileName = fileName[..(255 - ext.Length)] + ext;
        }

        return fileName;
    }
}

8.3 Streaming API Responses

IAsyncEnumerable for Large Result Sets

[ApiController]
[Route("api/[controller]")]
public class DataExportController : ControllerBase
{
    private readonly IDataService _dataService;

    // Stream results without loading all in memory
    [HttpGet("export/users")]
    public async IAsyncEnumerable<UserDto> ExportUsers(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        await foreach (var user in _dataService.GetAllUsersAsync(cancellationToken))
        {
            yield return new UserDto
            {
                Id = user.Id,
                Email = user.Email,
                Name = user.Name
            };
        }
    }

    // NDJSON streaming for large datasets
    [HttpGet("export/orders")]
    [Produces("application/x-ndjson")]
    public async Task ExportOrdersNdjson(CancellationToken cancellationToken)
    {
        Response.ContentType = "application/x-ndjson";

        await foreach (var order in _dataService.GetAllOrdersAsync(cancellationToken))
        {
            var json = JsonSerializer.Serialize(order);
            await Response.WriteAsync(json + "\n", cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);
        }
    }
}

Server-Sent Events (SSE) Implementation

[ApiController]
[Route("api/[controller]")]
public class EventsController : ControllerBase
{
    private readonly IEventBroadcaster _broadcaster;

    [HttpGet("stream")]
    public async Task StreamEvents(CancellationToken cancellationToken)
    {
        Response.Headers.ContentType = "text/event-stream";
        Response.Headers.CacheControl = "no-cache";
        Response.Headers.Connection = "keep-alive";

        await foreach (var evt in _broadcaster.GetEventsAsync(cancellationToken))
        {
            await Response.WriteAsync($"id: {evt.Id}\n", cancellationToken);
            await Response.WriteAsync($"event: {evt.Type}\n", cancellationToken);
            await Response.WriteAsync($"data: {JsonSerializer.Serialize(evt.Data)}\n\n", cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);
        }
    }
}

[CODE] Streaming Database Results to Client

public class StreamingDataService : IDataService
{
    private readonly ApplicationDbContext _context;

    public async IAsyncEnumerable<User> GetAllUsersAsync(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        // AsAsyncEnumerable streams results without loading all in memory
        await foreach (var user in _context.Users
            .AsNoTracking()
            .AsAsyncEnumerable()
            .WithCancellation(cancellationToken))
        {
            yield return user;
        }
    }

    // With batching for better performance
    public async IAsyncEnumerable<User> GetUsersInBatchesAsync(
        int batchSize,
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var lastId = 0;

        while (true)
        {
            var batch = await _context.Users
                .AsNoTracking()
                .Where(u => u.Id > lastId)
                .OrderBy(u => u.Id)
                .Take(batchSize)
                .ToListAsync(cancellationToken);

            if (batch.Count == 0)
                yield break;

            foreach (var user in batch)
            {
                yield return user;
            }

            lastId = batch[^1].Id;
        }
    }
}

[BENCHMARK] Memory Usage Comparison

// Test: Export 1 million records

// Approach 1: Load all in memory
// Memory: ~2.5 GB peak
// Time to first byte: 45 seconds
// Total time: 48 seconds
[HttpGet("all")]
public async Task<IActionResult> GetAll()
{
    var data = await _context.Records.ToListAsync();
    return Ok(data);
}

// Approach 2: IAsyncEnumerable streaming
// Memory: ~50 MB peak (constant)
// Time to first byte: 50 milliseconds
// Total time: 52 seconds
[HttpGet("stream")]
public async IAsyncEnumerable<Record> GetStream()
{
    await foreach (var record in _context.Records.AsAsyncEnumerable())
    {
        yield return record;
    }
}

// Approach 3: NDJSON with flush
// Memory: ~50 MB peak (constant)
// Time to first byte: 50 milliseconds
// Total time: 50 seconds
// Best for: Client that processes line-by-line

[PRODUCTION] CDN Integration and Edge Caching

[ApiController]
[Route("api/files")]
public class CdnOptimizedFileController : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetFile(Guid id)
    {
        var file = await _storage.GetFileInfoAsync(id);
        if (file == null) return NotFound();

        // Generate ETag for caching
        var etag = $"\"{file.Hash}\"";

        // Check If-None-Match for 304 response
        if (Request.Headers.IfNoneMatch.ToString() == etag)
        {
            return StatusCode(304);
        }

        Response.Headers.ETag = etag;
        Response.Headers.CacheControl = "public, max-age=31536000, immutable";
        Response.Headers.Vary = "Accept-Encoding";

        // For CDN: Add surrogate headers
        Response.Headers["Surrogate-Key"] = $"file-{id}";
        Response.Headers["Surrogate-Control"] = "max-age=86400";

        return PhysicalFile(
            file.Path,
            file.ContentType,
            file.FileName,
            enableRangeProcessing: true);
    }

    // Purge CDN cache
    [HttpPost("{id}/purge")]
    [Authorize(Roles = "Admin")]
    public async Task<IActionResult> PurgeCache(Guid id)
    {
        await _cdnService.PurgeByKeyAsync($"file-{id}");
        return NoContent();
    }
}

Interview Questions & Answers

Q1: How does the ASP.NET Core middleware pipeline work?

Answer: The middleware pipeline is a chain of RequestDelegate components that process HTTP requests and responses. Each middleware can:

  1. Pass the request to the next middleware by calling await next()
  2. Short-circuit the pipeline by not calling next()
  3. Perform work before and after the next middleware

Key points:

  • Order matters - middleware executes in registration order
  • The pipeline is bidirectional - middleware can execute code before AND after next()
  • UseRouting() adds endpoint routing, UseAuthorization() needs endpoint metadata from routing

Q2: Explain the difference between IOptions, IOptionsSnapshot, and IOptionsMonitor.

Answer:

  • IOptions: Singleton lifetime, reads configuration once at startup, no reload support
  • IOptionsSnapshot: Scoped lifetime, reads fresh configuration per request, good for web scenarios
  • IOptionsMonitor: Singleton lifetime with change notification callbacks, good for long-running services that need to react to configuration changes

Q3: How would you safely use a scoped service inside a singleton?

Answer: Use IServiceScopeFactory to create a new scope:

public class SingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public async Task DoWorkAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
        await scopedService.ProcessAsync();
    }
}

Q4: What is refresh token rotation and why is it important?

Answer: Refresh token rotation is a security practice where each time a refresh token is used, it’s invalidated and a new one is issued. This limits the window of opportunity for stolen refresh tokens. If a rotated token is used again, it indicates potential token theft, and all tokens for that user should be revoked.

Q5: How does resource-based authorization differ from policy-based authorization?

Answer:

  • Policy-based: Authorization decision made without knowing the specific resource (e.g., β€œuser must be admin”)
  • Resource-based: Authorization decision depends on the specific resource being accessed (e.g., β€œuser must be owner of this document”)

Resource-based requires injecting IAuthorizationService and calling AuthorizeAsync with the resource instance.


Summary

This guide covered the essential ASP.NET Core internals and patterns that Principal Engineers should master:

  1. Request Pipeline: Endpoint routing with DFA matching, middleware ordering and short-circuiting
  2. Dependency Injection: Service lifetimes, captive dependencies, IServiceScopeFactory pattern
  3. Configuration: Options pattern variants, validation, configuration reload
  4. Security: JWT implementation, refresh token rotation, resource-based authorization
  5. Performance: Output caching, cache invalidation strategies, Minimal APIs vs Controllers
  6. API Design: Versioning strategies, request/response logging with data masking
  7. Background Processing: Graceful shutdown, error handling with Polly
  8. File Streaming: Chunked downloads/uploads, resume support, IAsyncEnumerable streaming, CDN integration

Each section includes internals, benchmarks, production patterns, and debugging tips to help you build robust, scalable ASP.NET Core applications.

πŸ“š Related Articles