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
- CQRS: Donβt use for simple CRUD - adds unnecessary complexity
- Rate Limiting: Fixed window can allow 2x burst at window boundaries
- Circuit Breaker: Set appropriate sampling duration to avoid false positives
- Caching: Always handle cache misses gracefully; cache can be cleared anytime
- Health Checks: Donβt include slow checks in liveness probes - can cause unnecessary restarts
Last updated: December 2024 | .NET 8