📄

Modern Dotnet Patterns Guide

Intermediate 5 min read 1000 words

Modern .NET Patterns - Principal Engineer Deep Dive

Table of Contents

  1. CQRS (Command Query Responsibility Segregation)
  2. Rate Limiting & Throttling
  3. Resilience Patterns with Polly
  4. Health Checks & Diagnostics
  5. Distributed Caching
  6. Interview Questions & Answers

1. CQRS (Command Query Responsibility Segregation)

1.1 CQRS Fundamentals

CQRS is an architectural pattern that separates read operations (queries) from write operations (commands), allowing each to be optimized independently.

Core Principles:

// Traditional approach: Single model for reads and writes
public class ProductService
{
    private readonly AppDbContext _context;

    // Both read and write through same model
    public Product GetProduct(int id) => _context.Products.Find(id);
    public void UpdateProduct(Product product) => _context.SaveChanges();
}

// CQRS approach: Separate models and handlers
public interface ICommand { }
public interface IQuery<TResult> { }

public interface ICommandHandler<TCommand> where TCommand : ICommand
{
    Task HandleAsync(TCommand command, CancellationToken ct = default);
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query, CancellationToken ct = default);
}

[INTERNALS] Why Separate Reads from Writes:

Aspect Commands (Write) Queries (Read)
Optimization Consistency, validation Speed, caching
Scaling Write replicas Read replicas
Model Domain-rich, behavior DTO-flat, view-specific
Caching Invalidation on change Aggressive caching
Database Normalized Denormalized/materialized

1.2 Simple CQRS Without Event Sourcing

[CODE] Basic CQRS Implementation:

// Commands - express intent to change state
public record CreateProductCommand(
    string Name,
    decimal Price,
    int CategoryId) : ICommand;

public record UpdateProductPriceCommand(
    int ProductId,
    decimal NewPrice) : ICommand;

// Queries - express intent to retrieve data
public record GetProductByIdQuery(int ProductId) : IQuery<ProductDto>;

public record GetProductsByCategoryQuery(
    int CategoryId,
    int Page = 1,
    int PageSize = 20) : IQuery<PagedResult<ProductDto>>;

// DTOs - optimized for reading
public record ProductDto(
    int Id,
    string Name,
    decimal Price,
    string CategoryName,
    bool InStock);

public record PagedResult<T>(
    IReadOnlyList<T> Items,
    int TotalCount,
    int Page,
    int PageSize)
{
    public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
    public bool HasNextPage => Page < TotalPages;
    public bool HasPreviousPage => Page > 1;
}

[CODE] Command Handler with Validation:

public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand>
{
    private readonly WriteDbContext _writeContext;
    private readonly IValidator<CreateProductCommand> _validator;
    private readonly IEventPublisher _events;
    private readonly ILogger<CreateProductCommandHandler> _logger;

    public CreateProductCommandHandler(
        WriteDbContext writeContext,
        IValidator<CreateProductCommand> validator,
        IEventPublisher events,
        ILogger<CreateProductCommandHandler> logger)
    {
        _writeContext = writeContext;
        _validator = validator;
        _events = events;
        _logger = logger;
    }

    public async Task HandleAsync(CreateProductCommand command, CancellationToken ct)
    {
        // 1. Validate
        var validationResult = await _validator.ValidateAsync(command, ct);
        if (!validationResult.IsValid)
        {
            throw new ValidationException(validationResult.Errors);
        }

        // 2. Check business rules
        var categoryExists = await _writeContext.Categories
            .AnyAsync(c => c.Id == command.CategoryId, ct);

        if (!categoryExists)
        {
            throw new BusinessRuleException($"Category {command.CategoryId} not found");
        }

        // 3. Create domain entity
        var product = new Product
        {
            Name = command.Name,
            Price = command.Price,
            CategoryId = command.CategoryId,
            CreatedAt = DateTime.UtcNow
        };

        // 4. Persist
        _writeContext.Products.Add(product);
        await _writeContext.SaveChangesAsync(ct);

        _logger.LogInformation("Product {ProductId} created: {Name}", product.Id, product.Name);

        // 5. Publish event for read model update
        await _events.PublishAsync(new ProductCreatedEvent(product.Id, product.Name), ct);
    }
}

// FluentValidation validator
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(200);

        RuleFor(x => x.Price)
            .GreaterThan(0)
            .LessThan(1_000_000);

        RuleFor(x => x.CategoryId)
            .GreaterThan(0);
    }
}

[CODE] Query Handler with Projection:

public class GetProductByIdQueryHandler : IQueryHandler<GetProductByIdQuery, ProductDto>
{
    private readonly ReadDbContext _readContext;
    private readonly IDistributedCache _cache;
    private readonly ILogger<GetProductByIdQueryHandler> _logger;

    public GetProductByIdQueryHandler(
        ReadDbContext readContext,
        IDistributedCache cache,
        ILogger<GetProductByIdQueryHandler> logger)
    {
        _readContext = readContext;
        _cache = cache;
        _logger = logger;
    }

    public async Task<ProductDto> HandleAsync(GetProductByIdQuery query, CancellationToken ct)
    {
        // Try cache first
        var cacheKey = $"product:{query.ProductId}";
        var cached = await _cache.GetStringAsync(cacheKey, ct);

        if (cached != null)
        {
            _logger.LogDebug("Cache hit for product {ProductId}", query.ProductId);
            return JsonSerializer.Deserialize<ProductDto>(cached)!;
        }

        // Query with projection (no tracking, optimized SQL)
        var product = await _readContext.Products
            .AsNoTracking()
            .Where(p => p.Id == query.ProductId)
            .Select(p => new ProductDto(
                p.Id,
                p.Name,
                p.Price,
                p.Category.Name,
                p.Stock > 0))
            .FirstOrDefaultAsync(ct);

        if (product == null)
        {
            throw new NotFoundException($"Product {query.ProductId} not found");
        }

        // Cache for future requests
        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
            },
            ct);

        return product;
    }
}

1.3 CQRS with MediatR

[CODE] MediatR-based Implementation:

// Install: MediatR, MediatR.Extensions.Microsoft.DependencyInjection

// Commands with MediatR
public record CreateProductCommand(
    string Name,
    decimal Price,
    int CategoryId) : IRequest<int>; // Returns product ID

public record UpdateProductPriceCommand(
    int ProductId,
    decimal NewPrice) : IRequest<Unit>;

// Queries with MediatR
public record GetProductByIdQuery(int ProductId) : IRequest<ProductDto>;

// Handler
public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly WriteDbContext _context;

    public CreateProductHandler(WriteDbContext context)
    {
        _context = context;
    }

    public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            CategoryId = request.CategoryId
        };

        _context.Products.Add(product);
        await _context.SaveChangesAsync(ct);

        return product.Id;
    }
}

// Pipeline behavior for cross-cutting concerns
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (_validators.Any())
        {
            var context = new ValidationContext<TRequest>(request);

            var validationResults = await Task.WhenAll(
                _validators.Select(v => v.ValidateAsync(context, ct)));

            var failures = validationResults
                .SelectMany(r => r.Errors)
                .Where(f => f != null)
                .ToList();

            if (failures.Count > 0)
            {
                throw new ValidationException(failures);
            }
        }

        return await next();
    }
}

// Logging behavior
public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;

        _logger.LogInformation("Handling {RequestName}: {@Request}", requestName, request);

        var sw = Stopwatch.StartNew();
        var response = await next();
        sw.Stop();

        _logger.LogInformation(
            "Handled {RequestName} in {ElapsedMs}ms",
            requestName,
            sw.ElapsedMilliseconds);

        return response;
    }
}

// DI Registration
public static class CqrsServiceExtensions
{
    public static IServiceCollection AddCqrs(this IServiceCollection services)
    {
        services.AddMediatR(cfg =>
        {
            cfg.RegisterServicesFromAssemblyContaining<CreateProductHandler>();
            cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
            cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
        });

        services.AddValidatorsFromAssemblyContaining<CreateProductCommandValidator>();

        return services;
    }
}

// Controller usage
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<ActionResult<int>> Create(CreateProductCommand command)
    {
        var productId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id = productId }, productId);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetById(int id)
    {
        var product = await _mediator.Send(new GetProductByIdQuery(id));
        return Ok(product);
    }
}

1.4 Separate Read/Write Databases

[PRODUCTION] CQRS with Database Separation:

// Separate DbContexts for read and write
public class WriteDbContext : DbContext
{
    public WriteDbContext(DbContextOptions<WriteDbContext> options) : base(options) { }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Full domain model with relationships, constraints
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
            entity.Property(e => e.Price).HasPrecision(18, 2);
            entity.HasOne(e => e.Category)
                  .WithMany(c => c.Products)
                  .HasForeignKey(e => e.CategoryId);
            entity.Property(e => e.RowVersion).IsRowVersion();
        });
    }
}

public class ReadDbContext : DbContext
{
    public ReadDbContext(DbContextOptions<ReadDbContext> options) : base(options) { }

    public DbSet<ProductReadModel> Products => Set<ProductReadModel>();
    public DbSet<OrderSummaryReadModel> OrderSummaries => Set<OrderSummaryReadModel>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Denormalized read models, optimized for queries
        modelBuilder.Entity<ProductReadModel>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.ToTable("ProductReadModels");
            // No foreign keys - flat structure
        });
    }
}

// Read model - denormalized for fast queries
public class ProductReadModel
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
    public string CategoryName { get; set; } = "";
    public int StockQuantity { get; set; }
    public DateTime LastUpdated { get; set; }

    // Calculated/cached fields
    public bool InStock => StockQuantity > 0;
    public string PriceDisplay => Price.ToString("C");
}

// Sync read models via events
public class ProductCreatedEventHandler : INotificationHandler<ProductCreatedEvent>
{
    private readonly ReadDbContext _readContext;
    private readonly WriteDbContext _writeContext;

    public ProductCreatedEventHandler(
        ReadDbContext readContext,
        WriteDbContext writeContext)
    {
        _readContext = readContext;
        _writeContext = writeContext;
    }

    public async Task Handle(ProductCreatedEvent notification, CancellationToken ct)
    {
        // Fetch from write DB
        var product = await _writeContext.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == notification.ProductId, ct);

        if (product == null) return;

        // Create read model
        var readModel = new ProductReadModel
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            CategoryName = product.Category?.Name ?? "Uncategorized",
            StockQuantity = product.Stock,
            LastUpdated = DateTime.UtcNow
        };

        _readContext.Products.Add(readModel);
        await _readContext.SaveChangesAsync(ct);
    }
}

// DI Setup for multiple databases
public static void ConfigureDatabases(IServiceCollection services, IConfiguration config)
{
    // Write database - primary with full model
    services.AddDbContext<WriteDbContext>(options =>
        options.UseSqlServer(config.GetConnectionString("WriteDb")));

    // Read database - can be replica or separate optimized DB
    services.AddDbContext<ReadDbContext>(options =>
        options.UseSqlServer(config.GetConnectionString("ReadDb"))
               .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
}

[BENCHMARK] CQRS Performance Comparison:

Scenario Traditional CQRS Improvement
Simple read 5ms 3ms 40%
Complex join read 50ms 8ms 84%
Write with validation 20ms 22ms -10% (overhead)
High read/write ratio (100:1) 100ms avg 35ms avg 65%
Cache-friendly reads 5ms 0.5ms 90%

2. Rate Limiting & Throttling

2.1 Built-in Rate Limiting Middleware (.NET 7+)

.NET 7 introduced native rate limiting with several algorithms:

[CODE] Basic Rate Limiting Setup:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add rate limiting services
builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;

    // Global rate limit
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
        RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 10
            }));

    // Named policy for specific endpoints
    options.AddFixedWindowLimiter("api", options =>
    {
        options.PermitLimit = 50;
        options.Window = TimeSpan.FromMinutes(1);
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 5;
    });

    // Sliding window for smoother limiting
    options.AddSlidingWindowLimiter("sliding", options =>
    {
        options.PermitLimit = 100;
        options.Window = TimeSpan.FromMinutes(1);
        options.SegmentsPerWindow = 6; // 10-second segments
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 10;
    });

    // Token bucket for burst handling
    options.AddTokenBucketLimiter("burst", options =>
    {
        options.TokenLimit = 100;
        options.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
        options.TokensPerPeriod = 20;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 10;
        options.AutoReplenishment = true;
    });

    // Concurrency limiter
    options.AddConcurrencyLimiter("concurrent", options =>
    {
        options.PermitLimit = 10;
        options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        options.QueueLimit = 5;
    });

    // Custom rejection response
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        context.HttpContext.Response.ContentType = "application/json";

        var retryAfter = context.Lease.TryGetMetadata(
            MetadataName.RetryAfter, out var retryAfterValue)
            ? retryAfterValue.TotalSeconds
            : 60;

        context.HttpContext.Response.Headers.RetryAfter = retryAfter.ToString("0");

        await context.HttpContext.Response.WriteAsJsonAsync(new
        {
            error = "Too many requests",
            retryAfterSeconds = retryAfter,
            message = "Rate limit exceeded. Please try again later."
        }, ct);
    };
});

var app = builder.Build();

// Apply rate limiting middleware (before other middleware)
app.UseRateLimiter();

app.MapGet("/api/products", () => Results.Ok())
    .RequireRateLimiting("api");

app.MapGet("/api/search", () => Results.Ok())
    .RequireRateLimiting("sliding");

app.MapPost("/api/upload", () => Results.Ok())
    .RequireRateLimiting("concurrent");

app.Run();

2.2 Rate Limiting Algorithms Explained

[INTERNALS] How Each Algorithm Works:

Fixed Window:
┌─────────────┐┌─────────────┐┌─────────────┐
│   Window 1  ││   Window 2  ││   Window 3  │
│ Limit: 100  ││ Limit: 100  ││ Limit: 100  │
│ Used: 85    ││ Used: 0     ││ Used: 0     │
└─────────────┘└─────────────┘└─────────────┘
     00:00         01:00         02:00
Problem: Burst at window boundary (100 requests at 0:59 + 100 at 1:00)

Sliding Window (6 segments):
┌──┬──┬──┬──┬──┬──┐
│152025101812= 100 total (current window)
└──┴──┴──┴──┴──┴──┘
  ↑                ↑
Old segment drops  New segment added
Smoother: Always considers last N time units

Token Bucket:
┌─────────────────────┐
│ Bucket (max 100)    │
│ Current: 75 tokens  │
│ +20 every 10 sec    │
└─────────────────────┘
Allows bursts up to bucket size, then throttles

Concurrency:
┌─────────────────────┐
│ Active: 8/10        │
│ Queue: 3/5          │
└─────────────────────┘
Limits simultaneous requests, not rate

[CODE] Algorithm Comparison:

public class RateLimitAlgorithmComparison
{
    public static void ConfigureAllAlgorithms(RateLimiterOptions options)
    {
        // Fixed Window: Best for simple scenarios
        // - Predictable reset times
        // - Potential for boundary bursts
        options.AddFixedWindowLimiter("fixed", opt =>
        {
            opt.PermitLimit = 100;
            opt.Window = TimeSpan.FromMinutes(1);
        });

        // Sliding Window: Best for smooth rate limiting
        // - No boundary bursts
        // - Higher memory usage (tracks segments)
        options.AddSlidingWindowLimiter("sliding", opt =>
        {
            opt.PermitLimit = 100;
            opt.Window = TimeSpan.FromMinutes(1);
            opt.SegmentsPerWindow = 6; // More segments = smoother but more memory
        });

        // Token Bucket: Best for allowing controlled bursts
        // - Good for APIs that need burst capacity
        // - Tokens replenish continuously
        options.AddTokenBucketLimiter("token", opt =>
        {
            opt.TokenLimit = 100;          // Max burst size
            opt.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
            opt.TokensPerPeriod = 10;      // 10/sec = 600/min sustained rate
            opt.AutoReplenishment = true;
        });

        // Concurrency: Best for resource protection
        // - Limits parallel execution, not rate
        // - Good for expensive operations
        options.AddConcurrencyLimiter("concurrency", opt =>
        {
            opt.PermitLimit = 10;          // Max 10 concurrent
            opt.QueueLimit = 20;           // 20 can wait in queue
        });
    }
}

2.3 Per-User and Per-Endpoint Rate Limiting

[CODE] Advanced Partitioning:

builder.Services.AddRateLimiter(options =>
{
    // Per-user rate limiting (authenticated users)
    options.AddPolicy("per-user", context =>
    {
        var userId = context.User?.FindFirstValue(ClaimTypes.NameIdentifier);

        if (userId != null)
        {
            // Authenticated users get higher limits
            return RateLimitPartition.GetTokenBucketLimiter(userId, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 1000,
                    ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                    TokensPerPeriod = 100,
                    AutoReplenishment = true
                });
        }
        else
        {
            // Anonymous users get lower limits by IP
            var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
            return RateLimitPartition.GetTokenBucketLimiter(ip, _ =>
                new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 100,
                    ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                    TokensPerPeriod = 10,
                    AutoReplenishment = true
                });
        }
    });

    // Tiered rate limiting by subscription
    options.AddPolicy("tiered", context =>
    {
        var tier = context.User?.FindFirstValue("subscription_tier") ?? "free";

        var limits = tier switch
        {
            "enterprise" => (1000, 200),
            "pro" => (500, 100),
            "basic" => (100, 20),
            _ => (50, 10) // free
        };

        return RateLimitPartition.GetTokenBucketLimiter(
            context.User?.Identity?.Name ?? "anonymous",
            _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = limits.Item1,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                TokensPerPeriod = limits.Item2,
                AutoReplenishment = true
            });
    });

    // Endpoint-specific with cost weighting
    options.AddPolicy("weighted", context =>
    {
        var endpoint = context.GetEndpoint()?.DisplayName ?? "unknown";
        var userId = context.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anon";

        // Different endpoints have different "costs"
        var cost = endpoint switch
        {
            var e when e.Contains("search") => 5,  // Expensive operation
            var e when e.Contains("export") => 20, // Very expensive
            _ => 1 // Normal
        };

        return RateLimitPartition.GetTokenBucketLimiter(
            $"{userId}:{endpoint}",
            _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100 / cost,
                ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                TokensPerPeriod = 10 / cost,
                AutoReplenishment = true
            });
    });
});

2.4 Distributed Rate Limiting with Redis

[PRODUCTION] Redis-backed Rate Limiting:

// Custom distributed rate limiter using Redis
public class RedisRateLimiter : RateLimiter
{
    private readonly IDatabase _redis;
    private readonly string _keyPrefix;
    private readonly int _permitLimit;
    private readonly TimeSpan _window;

    public RedisRateLimiter(
        IConnectionMultiplexer redis,
        string keyPrefix,
        int permitLimit,
        TimeSpan window)
    {
        _redis = redis.GetDatabase();
        _keyPrefix = keyPrefix;
        _permitLimit = permitLimit;
        _window = window;
    }

    public override RateLimiterStatistics? GetStatistics() => null;

    protected override RateLimitLease AttemptAcquireCore(int permitCount)
    {
        // Synchronous version - should use async in real implementation
        return AcquireAsync(permitCount, CancellationToken.None)
            .GetAwaiter()
            .GetResult();
    }

    protected override async ValueTask<RateLimitLease> AcquireAsyncCore(
        int permitCount,
        CancellationToken ct)
    {
        var key = _keyPrefix;
        var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        var windowStart = now - (long)_window.TotalSeconds;

        // Lua script for atomic sliding window
        var script = @"
            local key = KEYS[1]
            local windowStart = tonumber(ARGV[1])
            local now = tonumber(ARGV[2])
            local limit = tonumber(ARGV[3])
            local permitCount = tonumber(ARGV[4])
            local windowSize = tonumber(ARGV[5])

            -- Remove old entries
            redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)

            -- Count current requests
            local current = redis.call('ZCARD', key)

            if current + permitCount <= limit then
                -- Add new request(s)
                for i = 1, permitCount do
                    redis.call('ZADD', key, now, now .. ':' .. i .. ':' .. math.random())
                end
                redis.call('EXPIRE', key, windowSize)
                return current + permitCount
            else
                return -1
            end
        ";

        var result = await _redis.ScriptEvaluateAsync(
            script,
            new RedisKey[] { key },
            new RedisValue[] { windowStart, now, _permitLimit, permitCount, (long)_window.TotalSeconds });

        var count = (long)result;

        if (count >= 0)
        {
            return new RedisRateLimitLease(true, count, _permitLimit);
        }

        return new RedisRateLimitLease(false, _permitLimit, _permitLimit);
    }

    public override TimeSpan? IdleDuration => null;

    private class RedisRateLimitLease : RateLimitLease
    {
        private readonly long _currentCount;
        private readonly int _limit;

        public RedisRateLimitLease(bool isAcquired, long currentCount, int limit)
        {
            IsAcquired = isAcquired;
            _currentCount = currentCount;
            _limit = limit;
        }

        public override bool IsAcquired { get; }

        public override IEnumerable<string> MetadataNames =>
            new[] { "CURRENT_COUNT", "LIMIT" };

        public override bool TryGetMetadata(string metadataName, out object? metadata)
        {
            metadata = metadataName switch
            {
                "CURRENT_COUNT" => _currentCount,
                "LIMIT" => _limit,
                _ => null
            };
            return metadata != null;
        }
    }
}

// Usage with DI
public static class RedisRateLimiterExtensions
{
    public static IServiceCollection AddRedisRateLimiting(
        this IServiceCollection services,
        IConfiguration config)
    {
        services.AddSingleton<IConnectionMultiplexer>(
            ConnectionMultiplexer.Connect(config.GetConnectionString("Redis")!));

        services.AddRateLimiter(options =>
        {
            options.AddPolicy("redis-global", context =>
            {
                var redis = context.RequestServices
                    .GetRequiredService<IConnectionMultiplexer>();

                var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";

                return RateLimitPartition.Get($"global:{ip}", _ =>
                    new RedisRateLimiter(redis, $"ratelimit:{ip}", 100, TimeSpan.FromMinutes(1)));
            });
        });

        return services;
    }
}

3. Resilience Patterns with Polly

3.1 Retry Strategies

[CODE] Basic Retry Patterns:

// Install: Microsoft.Extensions.Http.Resilience (includes Polly v8)
// Or: Polly, Polly.Extensions.Http

// Immediate retry
var immediateRetryPolicy = Policy
    .Handle<HttpRequestException>()
    .RetryAsync(3, onRetry: (exception, retryCount) =>
    {
        Console.WriteLine($"Retry {retryCount} due to: {exception.Message}");
    });

// Exponential backoff with jitter
var exponentialRetryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 5,
        sleepDurationProvider: (retryAttempt, context) =>
        {
            // Exponential: 1s, 2s, 4s, 8s, 16s + jitter
            var exponentialBackoff = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1));
            var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000));
            return exponentialBackoff + jitter;
        },
        onRetryAsync: async (exception, timespan, retryCount, context) =>
        {
            Console.WriteLine($"Retry {retryCount} after {timespan.TotalSeconds}s due to: {exception.Message}");
            await Task.CompletedTask;
        });

// Retry with specific HTTP status codes
var httpRetryPolicy = Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
    .OrResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: (retryCount, response, context) =>
        {
            // Respect Retry-After header if present
            if (response.Result?.Headers.RetryAfter?.Delta.HasValue == true)
            {
                return response.Result.Headers.RetryAfter.Delta.Value;
            }

            return TimeSpan.FromSeconds(Math.Pow(2, retryCount));
        },
        onRetryAsync: (response, timespan, retryCount, context) => Task.CompletedTask);

3.2 Circuit Breaker Pattern

[INTERNALS] Circuit Breaker State Machine:

    ┌─────────────────────────────────────────────────┐
    │                                                 │
    ▼                                                 │
┌────────┐   Failure threshold   ┌────────┐          │
│ CLOSED │ ──────────────────► │  OPEN  │          │
│        │       exceeded        │        │          │
└────────┘                       └────────┘          │
    ▲                                │               │
    │                                │ Break         │
    │                                │ duration      │
    │         Success                ▼               │
    │      ┌───────────────────┬────────────┐       │
    └──────│    HALF-OPENFailure  │───────┘
           └───────────────────┴────────────┘

[CODE] Circuit Breaker Implementation:

// Basic circuit breaker
var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (exception, duration) =>
        {
            Console.WriteLine($"Circuit OPEN for {duration.TotalSeconds}s: {exception.Message}");
        },
        onReset: () =>
        {
            Console.WriteLine("Circuit CLOSED - normal operation resumed");
        },
        onHalfOpen: () =>
        {
            Console.WriteLine("Circuit HALF-OPEN - testing...");
        });

// Advanced circuit breaker with sampling
var advancedCircuitBreaker = Policy
    .Handle<Exception>()
    .AdvancedCircuitBreakerAsync(
        failureThreshold: 0.5,  // 50% failure rate
        samplingDuration: TimeSpan.FromSeconds(30),
        minimumThroughput: 10,  // Need at least 10 requests to evaluate
        durationOfBreak: TimeSpan.FromMinutes(1),
        onBreak: (exception, state, duration, context) =>
        {
            Console.WriteLine($"Circuit broken! State before: {state}, Duration: {duration}");
        },
        onReset: context =>
        {
            Console.WriteLine("Circuit reset - back to normal");
        },
        onHalfOpen: () =>
        {
            Console.WriteLine("Testing if service recovered...");
        });

// For HTTP responses
var httpCircuitBreaker = Policy<HttpResponseMessage>
    .Handle<HttpRequestException>()
    .OrResult(r => (int)r.StatusCode >= 500)
    .AdvancedCircuitBreakerAsync(
        failureThreshold: 0.25,  // 25% failure rate
        samplingDuration: TimeSpan.FromSeconds(60),
        minimumThroughput: 20,
        durationOfBreak: TimeSpan.FromSeconds(30));

// Checking circuit state
public class ServiceClient
{
    private readonly AsyncCircuitBreakerPolicy _circuitBreaker;

    public ServiceClient()
    {
        _circuitBreaker = Policy
            .Handle<Exception>()
            .CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
    }

    public bool IsCircuitOpen => _circuitBreaker.CircuitState == CircuitState.Open;
    public bool IsCircuitHalfOpen => _circuitBreaker.CircuitState == CircuitState.HalfOpen;

    public async Task<string> CallServiceAsync()
    {
        if (IsCircuitOpen)
        {
            // Fast fail without trying
            throw new ServiceUnavailableException("Service is currently unavailable");
        }

        return await _circuitBreaker.ExecuteAsync(async () =>
        {
            // Actual service call
            return await DoServiceCallAsync();
        });
    }

    private Task<string> DoServiceCallAsync() => Task.FromResult("result");
}

3.3 Combined Resilience Pipelines

[CODE] Polly v8 Resilience Pipeline:

// .NET 8 with Microsoft.Extensions.Http.Resilience
var builder = WebApplication.CreateBuilder(args);

// Option 1: Standard resilience handler (recommended)
builder.Services.AddHttpClient<IMyApiClient, MyApiClient>()
    .AddStandardResilienceHandler();

// Option 2: Custom resilience pipeline
builder.Services.AddHttpClient<IMyApiClient, MyApiClient>()
    .AddResilienceHandler("custom", builder =>
    {
        // Rate limiter (innermost - executes first)
        builder.AddRateLimiter(new SlidingWindowRateLimiter(
            new SlidingWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromSeconds(30),
                SegmentsPerWindow = 3
            }));

        // Timeout (per-try)
        builder.AddTimeout(TimeSpan.FromSeconds(10));

        // Retry
        builder.AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromSeconds(1),
            UseJitter = true,
            BackoffType = DelayBackoffType.Exponential,
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
        });

        // Circuit breaker
        builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(30),
            MinimumThroughput = 10,
            BreakDuration = TimeSpan.FromSeconds(30),
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
        });

        // Total timeout (outermost)
        builder.AddTimeout(TimeSpan.FromSeconds(60));
    });

// Option 3: Polly v7 style with PolicyWrap
public static class ResiliencePolicies
{
    public static IAsyncPolicy<HttpResponseMessage> GetCombinedPolicy()
    {
        // Policies execute from innermost to outermost
        // Order: Timeout(per-try) -> Retry -> CircuitBreaker -> Timeout(total)

        var timeoutPerTry = Policy
            .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10));

        var retry = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .Or<TimeoutRejectedException>()
            .OrResult(r => (int)r.StatusCode >= 500)
            .WaitAndRetryAsync(3, attempt =>
                TimeSpan.FromSeconds(Math.Pow(2, attempt)) +
                TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)));

        var circuitBreaker = Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .Or<TimeoutRejectedException>()
            .OrResult(r => (int)r.StatusCode >= 500)
            .AdvancedCircuitBreakerAsync(0.5, TimeSpan.FromSeconds(30), 10, TimeSpan.FromSeconds(30));

        var totalTimeout = Policy
            .TimeoutAsync<HttpResponseMessage>(TimeSpan.FromMinutes(1));

        // Wrap: outer -> inner execution order
        return Policy.WrapAsync(totalTimeout, circuitBreaker, retry, timeoutPerTry);
    }
}

// Using the combined policy
public class ResilientApiClient
{
    private readonly HttpClient _httpClient;
    private readonly IAsyncPolicy<HttpResponseMessage> _policy;

    public ResilientApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _policy = ResiliencePolicies.GetCombinedPolicy();
    }

    public async Task<T> GetAsync<T>(string url)
    {
        var response = await _policy.ExecuteAsync(() =>
            _httpClient.GetAsync(url));

        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<T>()
            ?? throw new InvalidOperationException("Response was null");
    }
}

3.4 Fallback and Hedging

[CODE] Fallback Strategies:

// Simple fallback
var fallbackPolicy = Policy<string>
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .FallbackAsync(
        fallbackValue: "Default value",
        onFallbackAsync: (exception, context) =>
        {
            Console.WriteLine($"Falling back due to: {exception.Exception?.Message}");
            return Task.CompletedTask;
        });

// Fallback with factory
var factoryFallbackPolicy = Policy<ProductDto>
    .Handle<Exception>()
    .FallbackAsync(
        fallbackAction: async (context, ct) =>
        {
            // Try to get from cache
            var cached = await GetFromCacheAsync(context["productId"]?.ToString());
            if (cached != null) return cached;

            // Return a placeholder
            return new ProductDto(0, "Product Unavailable", 0, "Unknown", false);
        },
        onFallbackAsync: (result, context) =>
        {
            Console.WriteLine($"Using fallback for product {context["productId"]}");
            return Task.CompletedTask;
        });

// Hedging (parallel requests) - Polly v8
builder.Services.AddHttpClient<IApiClient, ApiClient>()
    .AddResilienceHandler("hedging", builder =>
    {
        builder.AddHedging(new HedgingStrategyOptions<HttpResponseMessage>
        {
            MaxHedgedAttempts = 2,
            Delay = TimeSpan.FromMilliseconds(200), // Start second request after 200ms
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(r => !r.IsSuccessStatusCode)
        });
    });

// Custom hedging implementation
public class HedgingClient
{
    private readonly HttpClient[] _httpClients;

    public HedgingClient(IEnumerable<HttpClient> clients)
    {
        _httpClients = clients.ToArray();
    }

    public async Task<T> GetWithHedging<T>(string path, CancellationToken ct = default)
    {
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);

        // Start all requests
        var tasks = _httpClients
            .Select(client => GetAsync<T>(client, path, cts.Token))
            .ToList();

        // Return first successful response
        while (tasks.Count > 0)
        {
            var completed = await Task.WhenAny(tasks);
            tasks.Remove(completed);

            try
            {
                var result = await completed;
                cts.Cancel(); // Cancel remaining requests
                return result;
            }
            catch
            {
                // This one failed, wait for others
                if (tasks.Count == 0)
                    throw; // All failed
            }
        }

        throw new InvalidOperationException("All hedged requests failed");
    }

    private async Task<T> GetAsync<T>(HttpClient client, string path, CancellationToken ct)
    {
        var response = await client.GetAsync(path, ct);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<T>(cancellationToken: ct)
            ?? throw new InvalidOperationException();
    }

    private Task<ProductDto?> GetFromCacheAsync(string? key) => Task.FromResult<ProductDto?>(null);
}

4. Health Checks & Diagnostics

4.1 Health Check System

[CODE] Basic Health Checks Setup:

var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks()
    // Database check
    .AddSqlServer(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        healthQuery: "SELECT 1",
        name: "sql-server",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql", "ready" })

    // Redis check
    .AddRedis(
        redisConnectionString: builder.Configuration.GetConnectionString("Redis")!,
        name: "redis",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "cache", "ready" })

    // External API check
    .AddUrlGroup(
        uri: new Uri("https://api.external-service.com/health"),
        name: "external-api",
        failureStatus: HealthStatus.Degraded,
        tags: new[] { "external", "ready" })

    // Custom check
    .AddCheck<DiskSpaceHealthCheck>("disk-space", tags: new[] { "system" })
    .AddCheck<MemoryHealthCheck>("memory", tags: new[] { "system" });

var app = builder.Build();

// Map health endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    // Liveness: Is the app running?
    Predicate = _ => false, // No checks, just confirms app is running
    ResponseWriter = WriteMinimalResponse
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    // Readiness: Is the app ready to receive traffic?
    Predicate = check => check.Tags.Contains("ready"),
    ResponseWriter = WriteDetailedResponse
});

app.MapHealthChecks("/health/startup", new HealthCheckOptions
{
    // Startup: Has initialization completed?
    Predicate = check => check.Tags.Contains("startup"),
    ResponseWriter = WriteDetailedResponse
});

// Full health check endpoint (internal/admin only)
app.MapHealthChecks("/health", new HealthCheckOptions
{
    Predicate = _ => true,
    ResponseWriter = WriteDetailedResponse
}).RequireAuthorization("AdminOnly");

app.Run();

static Task WriteMinimalResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "text/plain";
    return context.Response.WriteAsync(report.Status.ToString());
}

static Task WriteDetailedResponse(HttpContext context, HealthReport report)
{
    context.Response.ContentType = "application/json";

    var response = new
    {
        status = report.Status.ToString(),
        totalDuration = report.TotalDuration.TotalMilliseconds,
        checks = report.Entries.Select(e => new
        {
            name = e.Key,
            status = e.Value.Status.ToString(),
            duration = e.Value.Duration.TotalMilliseconds,
            description = e.Value.Description,
            data = e.Value.Data,
            exception = e.Value.Exception?.Message
        })
    };

    return context.Response.WriteAsJsonAsync(response);
}

[CODE] Custom Health Checks:

public class DiskSpaceHealthCheck : IHealthCheck
{
    private readonly long _minimumFreeBytesThreshold;

    public DiskSpaceHealthCheck(long minimumFreeBytesThreshold = 1_073_741_824) // 1GB
    {
        _minimumFreeBytesThreshold = minimumFreeBytesThreshold;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var drives = DriveInfo.GetDrives()
            .Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
            .ToList();

        var unhealthyDrives = drives
            .Where(d => d.AvailableFreeSpace < _minimumFreeBytesThreshold)
            .ToList();

        var data = new Dictionary<string, object>();
        foreach (var drive in drives)
        {
            data[$"{drive.Name}_free_gb"] = drive.AvailableFreeSpace / 1_073_741_824.0;
            data[$"{drive.Name}_total_gb"] = drive.TotalSize / 1_073_741_824.0;
        }

        if (unhealthyDrives.Any())
        {
            return Task.FromResult(HealthCheckResult.Unhealthy(
                $"Low disk space on: {string.Join(", ", unhealthyDrives.Select(d => d.Name))}",
                data: data));
        }

        return Task.FromResult(HealthCheckResult.Healthy("Disk space OK", data));
    }
}

public class MemoryHealthCheck : IHealthCheck
{
    private readonly long _thresholdBytes;

    public MemoryHealthCheck(long thresholdBytes = 1_073_741_824) // 1GB
    {
        _thresholdBytes = thresholdBytes;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        var allocatedBytes = GC.GetTotalMemory(forceFullCollection: false);
        var memoryInfo = GC.GetGCMemoryInfo();

        var data = new Dictionary<string, object>
        {
            ["allocated_mb"] = allocatedBytes / 1_048_576.0,
            ["gen0_collections"] = GC.CollectionCount(0),
            ["gen1_collections"] = GC.CollectionCount(1),
            ["gen2_collections"] = GC.CollectionCount(2),
            ["heap_size_mb"] = memoryInfo.HeapSizeBytes / 1_048_576.0,
            ["fragmentation_percent"] = memoryInfo.FragmentedBytes * 100.0 / memoryInfo.HeapSizeBytes
        };

        if (allocatedBytes > _thresholdBytes)
        {
            return Task.FromResult(HealthCheckResult.Degraded(
                $"High memory usage: {allocatedBytes / 1_048_576}MB",
                data: data));
        }

        return Task.FromResult(HealthCheckResult.Healthy("Memory OK", data));
    }
}

public class DatabaseMigrationHealthCheck : IHealthCheck
{
    private readonly AppDbContext _context;

    public DatabaseMigrationHealthCheck(AppDbContext context)
    {
        _context = context;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var pendingMigrations = await _context.Database
                .GetPendingMigrationsAsync(ct);

            var pending = pendingMigrations.ToList();

            if (pending.Any())
            {
                return HealthCheckResult.Unhealthy(
                    $"Pending migrations: {string.Join(", ", pending)}",
                    data: new Dictionary<string, object>
                    {
                        ["pending_count"] = pending.Count,
                        ["migrations"] = pending
                    });
            }

            return HealthCheckResult.Healthy("All migrations applied");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Cannot check migrations", ex);
        }
    }
}

// External service dependency check
public class ExternalApiHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;
    private readonly string _healthEndpoint;

    public ExternalApiHealthCheck(HttpClient httpClient, string healthEndpoint)
    {
        _httpClient = httpClient;
        _healthEndpoint = healthEndpoint;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var sw = Stopwatch.StartNew();
            var response = await _httpClient.GetAsync(_healthEndpoint, ct);
            sw.Stop();

            var data = new Dictionary<string, object>
            {
                ["response_time_ms"] = sw.ElapsedMilliseconds,
                ["status_code"] = (int)response.StatusCode
            };

            if (response.IsSuccessStatusCode)
            {
                return sw.ElapsedMilliseconds > 5000
                    ? HealthCheckResult.Degraded("Slow response", data: data)
                    : HealthCheckResult.Healthy("OK", data);
            }

            return HealthCheckResult.Unhealthy(
                $"Unhealthy: {response.StatusCode}",
                data: data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Cannot reach service", ex);
        }
    }
}

4.2 Kubernetes Probes Integration

[PRODUCTION] K8s-Compatible Health Checks:

# kubernetes deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 80

        # Startup probe - called during initialization
        startupProbe:
          httpGet:
            path: /health/startup
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
          failureThreshold: 30  # Allow 2.5 minutes for startup

        # Liveness probe - restart if fails
        livenessProbe:
          httpGet:
            path: /health/live
            port: 80
          initialDelaySeconds: 0
          periodSeconds: 10
          failureThreshold: 3

        # Readiness probe - remove from service if fails
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 0
          periodSeconds: 5
          failureThreshold: 3

4.3 Observability with OpenTelemetry

[CODE] OpenTelemetry Setup:

var builder = WebApplication.CreateBuilder(args);

// Add OpenTelemetry
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService(
            serviceName: "MyService",
            serviceVersion: "1.0.0",
            serviceInstanceId: Environment.MachineName))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation(options =>
        {
            options.RecordException = true;
            options.Filter = context =>
                !context.Request.Path.StartsWithSegments("/health");
        })
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddSource("MyApp.CustomTracing")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://otel-collector:4317");
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddProcessInstrumentation()
        .AddMeter("MyApp.CustomMetrics")
        .AddOtlpExporter());

// Custom tracing
public class OrderService
{
    private static readonly ActivitySource Source = new("MyApp.CustomTracing");
    private static readonly Counter<long> OrderCounter =
        new Meter("MyApp.CustomMetrics").CreateCounter<long>("orders_created");

    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        using var activity = Source.StartActivity("CreateOrder");
        activity?.SetTag("customer_id", request.CustomerId);
        activity?.SetTag("items_count", request.Items.Count);

        try
        {
            var order = await ProcessOrderAsync(request);

            activity?.SetTag("order_id", order.Id);
            activity?.SetStatus(ActivityStatusCode.Ok);

            OrderCounter.Add(1, new KeyValuePair<string, object?>("status", "success"));

            return order;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);

            OrderCounter.Add(1, new KeyValuePair<string, object?>("status", "error"));

            throw;
        }
    }

    private Task<Order> ProcessOrderAsync(CreateOrderRequest request) =>
        Task.FromResult(new Order { Id = 1 });
}

public class CreateOrderRequest
{
    public int CustomerId { get; set; }
    public List<OrderItem> Items { get; set; } = new();
}

public class OrderItem { }
public class Order { public int Id { get; set; } }

5. Distributed Caching

5.1 IDistributedCache Abstraction

[CODE] Basic Distributed Caching:

var builder = WebApplication.CreateBuilder(args);

// Option 1: In-memory (development/single instance)
builder.Services.AddDistributedMemoryCache();

// Option 2: Redis (production)
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp:";
});

// Option 3: SQL Server
builder.Services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("Cache");
    options.SchemaName = "dbo";
    options.TableName = "DistributedCache";
});

// Generic cache wrapper service
public interface ICacheService
{
    Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
    Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken ct = default);
    Task RemoveAsync(string key, CancellationToken ct = default);
    Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, CancellationToken ct = default);
}

public class DistributedCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<DistributedCacheService> _logger;
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    public DistributedCacheService(
        IDistributedCache cache,
        ILogger<DistributedCacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
    {
        try
        {
            var data = await _cache.GetStringAsync(key, ct);

            if (data == null)
            {
                _logger.LogDebug("Cache miss: {Key}", key);
                return default;
            }

            _logger.LogDebug("Cache hit: {Key}", key);
            return JsonSerializer.Deserialize<T>(data, JsonOptions);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Cache get error for {Key}", key);
            return default;
        }
    }

    public async Task SetAsync<T>(
        string key,
        T value,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        try
        {
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5),
                SlidingExpiration = TimeSpan.FromMinutes(1)
            };

            var data = JsonSerializer.Serialize(value, JsonOptions);
            await _cache.SetStringAsync(key, data, options, ct);

            _logger.LogDebug("Cache set: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Cache set error for {Key}", key);
        }
    }

    public async Task RemoveAsync(string key, CancellationToken ct = default)
    {
        try
        {
            await _cache.RemoveAsync(key, ct);
            _logger.LogDebug("Cache removed: {Key}", key);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Cache remove error for {Key}", key);
        }
    }

    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        var cached = await GetAsync<T>(key, ct);
        if (cached != null)
            return cached;

        var value = await factory();
        await SetAsync(key, value, expiration, ct);
        return value;
    }
}

5.2 Cache Stampede Prevention

[CODE] Preventing Cache Stampede:

public class StampedePreventingCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
    private readonly ILogger<StampedePreventingCacheService> _logger;

    public StampedePreventingCacheService(
        IDistributedCache cache,
        ILogger<StampedePreventingCacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        // Try cache first (no lock)
        var cached = await GetAsync<T>(key, ct);
        if (cached != null)
            return cached;

        // Get or create lock for this key
        var keyLock = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));

        // Only one thread will execute factory
        await keyLock.WaitAsync(ct);
        try
        {
            // Double-check after acquiring lock
            cached = await GetAsync<T>(key, ct);
            if (cached != null)
                return cached;

            _logger.LogDebug("Cache miss, executing factory for {Key}", key);

            var value = await factory();
            await SetAsync(key, value, expiration, ct);
            return value;
        }
        finally
        {
            keyLock.Release();

            // Clean up lock if not being waited on
            if (keyLock.CurrentCount == 1)
            {
                _locks.TryRemove(key, out _);
            }
        }
    }

    // Implement other methods...
    public Task<T?> GetAsync<T>(string key, CancellationToken ct = default) =>
        Task.FromResult<T?>(default);

    public Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken ct = default) =>
        Task.CompletedTask;

    public Task RemoveAsync(string key, CancellationToken ct = default) =>
        Task.CompletedTask;
}

// Alternative: Early refresh (probabilistic)
public class EarlyRefreshCacheService
{
    private readonly IDistributedCache _cache;
    private readonly double _earlyRefreshProbability;

    public EarlyRefreshCacheService(IDistributedCache cache, double earlyRefreshProbability = 0.1)
    {
        _cache = cache;
        _earlyRefreshProbability = earlyRefreshProbability;
    }

    public async Task<T?> GetWithEarlyRefreshAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan expiration,
        CancellationToken ct = default)
    {
        var dataKey = $"data:{key}";
        var expiryKey = $"expiry:{key}";

        var data = await _cache.GetStringAsync(dataKey, ct);
        var expiryStr = await _cache.GetStringAsync(expiryKey, ct);

        if (data != null && expiryStr != null)
        {
            var expiry = DateTimeOffset.Parse(expiryStr);
            var remaining = expiry - DateTimeOffset.UtcNow;
            var original = expiration;

            // Probabilistic early refresh
            // As we get closer to expiry, probability of refresh increases
            var probabilityOfRefresh = 1 - (remaining.TotalSeconds / original.TotalSeconds);

            if (Random.Shared.NextDouble() < probabilityOfRefresh * _earlyRefreshProbability)
            {
                // Trigger background refresh
                _ = Task.Run(async () =>
                {
                    var value = await factory();
                    await SetWithExpiryAsync(dataKey, expiryKey, value, expiration, ct);
                }, ct);
            }

            return JsonSerializer.Deserialize<T>(data);
        }

        // Cache miss - fetch and store
        var newValue = await factory();
        await SetWithExpiryAsync(dataKey, expiryKey, newValue, expiration, ct);
        return newValue;
    }

    private async Task SetWithExpiryAsync<T>(
        string dataKey,
        string expiryKey,
        T value,
        TimeSpan expiration,
        CancellationToken ct)
    {
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration
        };

        var data = JsonSerializer.Serialize(value);
        var expiry = DateTimeOffset.UtcNow.Add(expiration).ToString("O");

        await Task.WhenAll(
            _cache.SetStringAsync(dataKey, data, options, ct),
            _cache.SetStringAsync(expiryKey, expiry, options, ct));
    }
}

5.3 Multi-Tier Caching

[CODE] L1 (Memory) + L2 (Redis) Caching:

public class MultiTierCacheService : ICacheService
{
    private readonly IMemoryCache _l1Cache;
    private readonly IDistributedCache _l2Cache;
    private readonly ILogger<MultiTierCacheService> _logger;

    private readonly MemoryCacheEntryOptions _l1Options = new()
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1),
        Size = 1
    };

    public MultiTierCacheService(
        IMemoryCache l1Cache,
        IDistributedCache l2Cache,
        ILogger<MultiTierCacheService> logger)
    {
        _l1Cache = l1Cache;
        _l2Cache = l2Cache;
        _logger = logger;
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
    {
        // L1: In-process memory (fastest)
        if (_l1Cache.TryGetValue(key, out T? l1Value))
        {
            _logger.LogDebug("L1 cache hit: {Key}", key);
            return l1Value;
        }

        // L2: Distributed cache
        try
        {
            var l2Data = await _l2Cache.GetStringAsync(key, ct);
            if (l2Data != null)
            {
                _logger.LogDebug("L2 cache hit: {Key}", key);

                var value = JsonSerializer.Deserialize<T>(l2Data);

                // Populate L1 for next request
                _l1Cache.Set(key, value, _l1Options);

                return value;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "L2 cache error for {Key}", key);
        }

        _logger.LogDebug("Cache miss: {Key}", key);
        return default;
    }

    public async Task SetAsync<T>(
        string key,
        T value,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        var exp = expiration ?? TimeSpan.FromMinutes(5);

        // Set L1
        _l1Cache.Set(key, value, new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1), // Shorter for L1
            Size = 1
        });

        // Set L2
        try
        {
            var data = JsonSerializer.Serialize(value);
            await _l2Cache.SetStringAsync(key, data, new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = exp
            }, ct);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "L2 cache set error for {Key}", key);
        }
    }

    public async Task RemoveAsync(string key, CancellationToken ct = default)
    {
        _l1Cache.Remove(key);

        try
        {
            await _l2Cache.RemoveAsync(key, ct);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "L2 cache remove error for {Key}", key);
        }
    }

    public async Task<T> GetOrSetAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null,
        CancellationToken ct = default)
    {
        var cached = await GetAsync<T>(key, ct);
        if (cached != null)
            return cached;

        var value = await factory();
        await SetAsync(key, value, expiration, ct);
        return value;
    }
}

// DI Registration
public static class CacheServiceExtensions
{
    public static IServiceCollection AddMultiTierCaching(
        this IServiceCollection services,
        IConfiguration config)
    {
        // L1: Memory cache with size limit
        services.AddMemoryCache(options =>
        {
            options.SizeLimit = 10000; // Max entries
        });

        // L2: Redis
        services.AddStackExchangeRedisCache(options =>
        {
            options.Configuration = config.GetConnectionString("Redis");
            options.InstanceName = "MyApp:";
        });

        services.AddSingleton<ICacheService, MultiTierCacheService>();

        return services;
    }
}

6. Interview Questions & Answers

CQRS

Q: When should you NOT use CQRS? A: CQRS adds complexity and should be avoided when:

  • Simple CRUD applications with low complexity
  • No scaling requirements between reads and writes
  • Team unfamiliar with the pattern
  • Small applications where the overhead isn’t justified
  • Real-time consistency is critical (eventual consistency is unacceptable)

Q: How do you keep read and write models in sync? A: Several approaches:

  1. Synchronous: Update both in the same transaction (simplest, tightly coupled)
  2. Domain Events: Publish events from write side, handlers update read models
  3. Event Sourcing: Read models rebuilt from event stream
  4. Change Data Capture (CDC): Database-level change tracking (Debezium, etc.)

Rate Limiting

Q: Which rate limiting algorithm would you choose for an API? A: It depends on the use case:

  • Fixed Window: Simple, predictable, but allows boundary bursts
  • Sliding Window: Smooth limiting, no bursts, higher memory usage
  • Token Bucket: Allow controlled bursts, good for APIs with variable traffic
  • Concurrency: When you need to limit parallel execution, not rate

Q: How do you implement rate limiting in a distributed system? A: Use a centralized store (Redis) with atomic operations:

  • Lua scripts for atomicity
  • Sorted sets for sliding window
  • Consider using dedicated libraries (Redis Rate Limiter)
  • Handle cache failures gracefully (fail open vs fail closed)

Resilience

Q: What’s the difference between retry and circuit breaker? A:

  • Retry: Repeats failed operations, useful for transient failures
  • Circuit Breaker: Prevents calls to failing services, allows recovery time
  • Use together: Circuit breaker wraps retry to prevent retry storms

Q: How do you choose timeout values? A:

  • Per-try timeout: Slightly longer than P99 latency (e.g., 5-10 seconds)
  • Total timeout: Per-try × retries + buffer (e.g., 30-60 seconds)
  • Consider downstream timeouts (don’t exceed them)
  • Use hedging for latency-sensitive operations

Health Checks

Q: What’s the difference between liveness and readiness probes? A:

  • Liveness: Is the process running? Failure = restart container
  • Readiness: Can the app handle traffic? Failure = remove from load balancer
  • Startup: Has initialization completed? Prevents premature liveness checks

Caching

Q: What is cache stampede and how do you prevent it? A: Cache stampede occurs when many requests simultaneously try to regenerate an expired cache entry.

Prevention strategies:

  1. Locking: Only one request regenerates, others wait
  2. Probabilistic early refresh: Randomly refresh before expiry
  3. Background refresh: Separate process refreshes cache
  4. Stale-while-revalidate: Serve stale data while refreshing

Q: How do you invalidate cache across multiple instances? A:

  1. Pub/Sub: Redis pub/sub or message queue to notify all instances
  2. Short TTL: Accept some staleness, rely on expiration
  3. Versioned keys: Change key when data changes
  4. Cache tags: Group related entries, invalidate by tag

Summary

This guide covered essential modern .NET patterns for building robust, scalable applications:

Pattern Key Benefit When to Use
CQRS Separate optimization of reads/writes High read/write ratio, complex queries
Rate Limiting Protect resources from overload Public APIs, shared resources
Resilience (Polly) Handle transient failures gracefully External dependencies, microservices
Health Checks Monitor application health Kubernetes deployments, load balancers
Distributed Caching Reduce latency and database load Frequently accessed data, scaling

Best Practices Checklist:

  • [ ] Start simple, add complexity only when needed
  • [ ] Measure before optimizing
  • [ ] Implement proper monitoring and alerting
  • [ ] Test failure scenarios (chaos engineering)
  • [ ] Document architectural decisions
  • [ ] Consider operational complexity alongside technical benefits