🧩

Modern Patterns Cheatsheet

Design Patterns Intermediar 3 min citire 400 cuvinte

Modern .NET Patterns - Quick Reference Cheatsheet

Principal Engineer Interview Quick Reference - CQRS, Rate Limiting, Resilience, Health Checks, Distributed Caching


1. CQRS (Command Query Responsibility Segregation)

Core Concept

Commands → Write Model (full entity) → Write Database
Queries  → Read Model (DTOs/projections) → Read Database (optional)

MediatR Setup

// Command
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items)
    : IRequest<Guid>;

// Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
    {
        var order = new Order(cmd.CustomerId, cmd.Items);
        await _writeDb.Orders.AddAsync(order, ct);
        await _writeDb.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Query
public record GetOrderQuery(Guid Id) : IRequest<OrderDto>;

// Usage
var orderId = await _mediator.Send(new CreateOrderCommand(...));
var order = await _mediator.Send(new GetOrderQuery(orderId));

When to Use CQRS

Use CQRS Don’t Use CQRS
Complex domain logic Simple CRUD apps
Different read/write scaling Small team/project
Event Sourcing needed Low traffic
High read:write ratio Tight deadlines

2. Rate Limiting (.NET 7+)

Built-in Limiters

Limiter Use Case Key Config
Fixed Window Simple quota per period PermitLimit, Window
Sliding Window Smoother distribution PermitLimit, Window, SegmentsPerWindow
Token Bucket Burst-friendly APIs TokenLimit, ReplenishmentPeriod, TokensPerPeriod
Concurrency Limit parallel requests PermitLimit, QueueLimit

Quick Setup

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
        opt.QueueLimit = 10;
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
    });

    options.RejectionStatusCode = 429;
});

app.UseRateLimiter();

// Apply to endpoint
app.MapGet("/api/data", () => "OK")
   .RequireRateLimiting("api");

Per-User Rate Limiting

options.AddPolicy("per-user", context =>
    RateLimitPartition.GetFixedWindowLimiter(
        partitionKey: context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString(),
        factory: _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 10,
            Window = TimeSpan.FromSeconds(10)
        }));

3. Resilience Patterns (Polly v8)

Pipeline Builder Pattern

var pipeline = new ResiliencePipelineBuilder<HttpResponseMessage>()
    .AddRetry(new RetryStrategyOptions<HttpResponseMessage>
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromMilliseconds(500),
        BackoffType = DelayBackoffType.Exponential,
        UseJitter = true,
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .Handle<HttpRequestException>()
            .HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(30)
    })
    .AddTimeout(TimeSpan.FromSeconds(10))
    .Build();

var result = await pipeline.ExecuteAsync(async ct =>
    await httpClient.GetAsync(url, ct));

HttpClientFactory Integration

builder.Services.AddHttpClient<IMyService, MyService>()
    .AddStandardResilienceHandler(); // Built-in resilience

// Or custom
builder.Services.AddHttpClient<IMyService, MyService>()
    .AddResilienceHandler("custom", builder =>
    {
        builder.AddRetry(retryOptions);
        builder.AddCircuitBreaker(cbOptions);
    });

Circuit Breaker States

CLOSED → (failures exceed threshold) → OPEN
OPEN → (break duration expires) → HALF-OPEN
HALF-OPEN → (test request succeeds) → CLOSED
HALF-OPEN → (test request fails) → OPEN

4. Health Checks

Quick Setup

// Registration
builder.Services.AddHealthChecks()
    .AddCheck("self", () => HealthCheckResult.Healthy())
    .AddSqlServer(connectionString, name: "database")
    .AddRedis(redisConnection, name: "redis")
    .AddUrlGroup(new Uri("https://api.external.com"), name: "external-api");

// Endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // No dependency checks
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

Custom Health Check

public class DatabaseHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, CancellationToken ct = default)
    {
        try
        {
            await _db.Database.CanConnectAsync(ct);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("DB connection failed", ex);
        }
    }
}

Kubernetes Probes Mapping

Probe Endpoint Purpose
livenessProbe /health/live Is process alive?
readinessProbe /health/ready Can accept traffic?
startupProbe /health/startup Has started successfully?

5. Distributed Caching

IDistributedCache Setup

// Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

// Usage
await _cache.SetStringAsync("key", "value", new DistributedCacheEntryOptions
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
    SlidingExpiration = TimeSpan.FromMinutes(1)
});

var value = await _cache.GetStringAsync("key");

Cache-Aside Pattern

public async Task<T?> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
    var cached = await _cache.GetStringAsync(key);
    if (cached != null)
        return JsonSerializer.Deserialize<T>(cached);

    var value = await factory();
    if (value != null)
    {
        await _cache.SetStringAsync(key, JsonSerializer.Serialize(value),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = ttl });
    }
    return value;
}

Cache Stampede Prevention

private readonly SemaphoreSlim _lock = new(1, 1);

public async Task<T?> GetOrSetWithLockAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl)
{
    var cached = await _cache.GetStringAsync(key);
    if (cached != null) return JsonSerializer.Deserialize<T>(cached);

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

        var value = await factory();
        await _cache.SetStringAsync(key, JsonSerializer.Serialize(value),
            new() { AbsoluteExpirationRelativeToNow = ttl });
        return value;
    }
    finally { _lock.Release(); }
}

Multi-Tier Caching (L1 + L2)

public async Task<T?> GetAsync<T>(string key)
{
    // L1: Memory (fast)
    if (_memory.TryGetValue(key, out T? value))
        return value;

    // L2: Redis (distributed)
    var cached = await _redis.GetStringAsync(key);
    if (cached != null)
    {
        value = JsonSerializer.Deserialize<T>(cached);
        _memory.Set(key, value, TimeSpan.FromSeconds(30)); // Short L1 TTL
        return value;
    }

    return default;
}

Quick Decision Trees

Rate Limiter Selection

Need burst support? → Token Bucket
Simple quota per period? → Fixed Window
Smooth distribution needed? → Sliding Window
Limit parallel operations? → Concurrency Limiter

Caching Strategy Selection

Single server? → IMemoryCache
Multiple servers? → IDistributedCache (Redis)
High traffic + multiple servers? → Multi-tier (Memory + Redis)
Complex invalidation? → Consider cache tags/pub-sub

Resilience Strategy Selection

Transient failures? → Retry with exponential backoff
Dependency unreliable? → Circuit Breaker
Need fast failure? → Timeout
All of above? → Combined pipeline

Interview Quick Answers

Q: When would you use CQRS?

When read and write models have different requirements, high read:write ratios, or when scaling reads and writes independently is beneficial.

Q: Difference between rate limiter types?

Fixed Window: simple quota per period (can have burst at boundaries). Sliding Window: smoother distribution. Token Bucket: allows controlled bursts. Concurrency: limits parallel requests.

Q: How to prevent cache stampede?

Use locking (SemaphoreSlim) to ensure only one request fetches data while others wait, then double-check cache after acquiring lock.

Q: Circuit breaker states?

Closed (normal), Open (failing fast), Half-Open (testing recovery). Transitions based on failure ratios and break duration.

Q: Health check types in Kubernetes?

Liveness (restart if fails), Readiness (stop traffic if fails), Startup (allow slow initialization).


Common Gotchas

  1. CQRS: Don’t use for simple CRUD - adds unnecessary complexity
  2. Rate Limiting: Fixed window can allow 2x burst at window boundaries
  3. Circuit Breaker: Set appropriate sampling duration to avoid false positives
  4. Caching: Always handle cache misses gracefully; cache can be cleared anytime
  5. Health Checks: Don’t include slow checks in liveness probes - can cause unnecessary restarts

Last updated: December 2024 | .NET 8

📚 Articole Corelate