Chapter 6: .NET Specific Architecture - Production-Ready Code
Series: Low Level Design for .NET Developers | Previous: Chapter 5: Behavioral Design Patterns | Next: Chapter 7: Domain Driven Design Basics
📖 Introduction
Understanding design patterns is essential, but knowing how to implement them in a real-world .NET application is what separates senior developers from intermediate ones. This chapter covers the .NET-specific architectural patterns and best practices that you'll use daily in enterprise development.
We'll explore:
- Dependency Injection (DI) - The heart of modern .NET applications
- Async/Await Best Practices - Writing responsive, scalable code
- Unit Testing & Mocking - Ensuring code quality
- ASP.NET Core Middleware - Building request pipelines
- Entity Framework Patterns - Repository, Unit of Work, Specification
- Configuration & Options Pattern - Managing settings
- Logging & Telemetry - Observability in production
- Exception Handling - Global error handling patterns
1. Dependency Injection (DI) Deep Dive
1.1 Understanding DI in .NET
Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them internally. .NET has built-in support for DI, making it the foundation of modern .NET applications.
1.2 Service Lifetimes
| Lifetime | When Created | When Disposed | Use Case |
|---|---|---|---|
| Transient | Each time requested | End of request/scope | Lightweight, stateless services |
| Scoped | Once per HTTP request / scope | End of request/scope | DbContext, services with request-specific data |
| Singleton | First time requested or at startup | Application shutdown | Caching, configuration, logging |
1.3 DI Configuration and Registration
// Program.cs - .NET 6+ style
var builder = WebApplication.CreateBuilder(args);
// ===== SERVICE REGISTRATION =====
// 1. Simple registration
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// 2. Register with implementation factory
builder.Services.AddScoped<IPaymentService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var apiKey = config["PaymentGateway:ApiKey"];
var baseUrl = config["PaymentGateway:BaseUrl"];
return new StripePaymentService(apiKey, baseUrl);
});
// 3. Register multiple implementations of same interface
builder.Services.AddScoped<INotificationChannel, EmailChannel>();
builder.Services.AddScoped<INotificationChannel, SmsChannel>();
builder.Services.AddScoped<INotificationChannel, PushChannel>();
// 4. Register generic types
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
// 5. Register with different implementations based on condition
builder.Services.AddScoped<IDataService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var useMockData = config.GetValue<bool>("UseMockData");
if (useMockData)
return new MockDataService();
else
return new RealDataService();
});
// 6. Register using reflection (scanning assemblies)
var services = typeof(IService).Assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && t.GetInterfaces().Contains(typeof(IService)))
.Select(t => new { Interface = typeof(IService), Implementation = t });
foreach (var service in services)
{
builder.Services.AddScoped(service.Interface, service.Implementation);
}
// 7. Register HttpClient with named clients
builder.Services.AddHttpClient("GitHub", client =>
{
client.BaseAddress = new Uri("https://api.github.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
});
// 8. Register options pattern
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("Email"));
builder.Services.Configure<DatabaseSettings>(options =>
{
options.ConnectionString = builder.Configuration.GetConnectionString("Default");
options.Timeout = 30;
});
// 9. Register hosted services (background tasks)
builder.Services.AddHostedService<EmailBackgroundService>();
builder.Services.AddHostedService<CacheCleanupService>();
// 10. Add health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddUrlGroup(new Uri("https://api.example.com"), "External API");
var app = builder.Build();
// ===== CONSUMING DEPENDENCIES =====
// Constructor Injection - Preferred approach
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
private readonly ILogger<OrderService> _logger;
private readonly IOptions<EmailSettings> _emailSettings;
public OrderService(
IOrderRepository orderRepository,
IEmailService emailService,
ILogger<OrderService> logger,
IOptions<EmailSettings> emailSettings)
{
_orderRepository = orderRepository;
_emailService = emailService;
_logger = logger;
_emailSettings = emailSettings;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
_logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
var order = new Order
{
CustomerId = request.CustomerId,
Total = request.Items.Sum(i => i.Price * i.Quantity),
Status = OrderStatus.Pending
};
await _orderRepository.AddAsync(order);
// Use settings from options
var fromEmail = _emailSettings.Value.FromEmail;
await _emailService.SendOrderConfirmationAsync(order.CustomerEmail, order.Id);
_logger.LogInformation("Order {OrderId} created successfully", order.Id);
return order;
}
}
// Property Injection - For optional dependencies
public class PaymentService : IPaymentService
{
[Inject] // Some frameworks support attribute injection
public ILogger<PaymentService> Logger { get; set; }
public IEmailService EmailService { get; set; } // Manually set
}
// Method Injection - For runtime dependencies
public class ReportGenerator
{
public void GenerateReport(IPdfGenerator pdfGenerator, ReportData data)
{
// pdfGenerator is provided at method call time
pdfGenerator.CreatePdf(data);
}
}
// FromServices attribute in Minimal APIs
app.MapPost("/api/orders", async (OrderRequest request, IOrderService orderService) =>
{
var order = await orderService.CreateOrderAsync(request);
return Results.Ok(order);
});
// ===== ADVANCED DI PATTERNS =====
// Factory Pattern with DI
public interface IServiceFactory
{
T GetService<T>() where T : class;
object GetService(Type serviceType);
}
public class ServiceFactory : IServiceFactory
{
private readonly IServiceProvider _serviceProvider;
public ServiceFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T GetService<T>() where T : class
{
return _serviceProvider.GetRequiredService<T>();
}
public object GetService(Type serviceType)
{
return _serviceProvider.GetRequiredService(serviceType);
}
}
// Decorator Pattern with DI
public interface ICacheService
{
Task<T> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan expiration);
}
public class RedisCacheService : ICacheService
{
// Implementation
}
public class CachedRepositoryDecorator<T> : IRepository<T> where T : class
{
private readonly IRepository<T> _inner;
private readonly ICacheService _cache;
public CachedRepositoryDecorator(IRepository<T> inner, ICacheService cache)
{
_inner = inner;
_cache = cache;
}
public async Task<T> GetByIdAsync(int id)
{
var cacheKey = $"{typeof(T).Name}_{id}";
var cached = await _cache.GetAsync<T>(cacheKey);
if (cached != null)
return cached;
var entity = await _inner.GetByIdAsync(id);
await _cache.SetAsync(cacheKey, entity, TimeSpan.FromMinutes(5));
return entity;
}
}
// Register decorator
services.AddScoped<IRepository<Product>, ProductRepository>();
services.Decorate<IRepository<Product>, CachedRepositoryDecorator<Product>>();
1.4 Avoiding Common DI Anti-Patterns
// ❌ Service Locator Anti-Pattern
public class BadOrderService
{
private readonly IServiceProvider _serviceProvider;
public BadOrderService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider; // Don't inject IServiceProvider
}
public async Task ProcessOrder(int orderId)
{
// Service Locator pattern - hides dependencies
var repository = _serviceProvider.GetRequiredService<IOrderRepository>();
var emailService = _serviceProvider.GetRequiredService<IEmailService>();
var order = await repository.GetByIdAsync(orderId);
await emailService.SendConfirmationAsync(order);
}
}
// ✅ Proper Constructor Injection
public class GoodOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
public GoodOrderService(IOrderRepository orderRepository, IEmailService emailService)
{
_orderRepository = orderRepository;
_emailService = emailService; // Dependencies are explicit
}
public async Task ProcessOrder(int orderId)
{
var order = await _orderRepository.GetByIdAsync(orderId);
await _emailService.SendConfirmationAsync(order);
}
}
// ❌ Captive Dependency - Singleton depending on Scoped
public class SingletonService
{
private readonly ScopedService _scopedService; // BAD! Singleton holding scoped service
public SingletonService(ScopedService scopedService)
{
_scopedService = scopedService;
}
}
// ✅ Fix - Use factory or register as scoped
public class BetterSingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public BetterSingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task DoWork()
{
using var scope = _scopeFactory.CreateScope();
var scopedService = scope.ServiceProvider.GetRequiredService<ScopedService>();
await scopedService.DoScopedWork();
}
}
💡 DI Best Practices:
- Prefer constructor injection over property injection
- Make dependencies explicit and immutable (readonly fields)
- Avoid injecting IServiceProvider (Service Locator anti-pattern)
- Be careful with singleton-scoped dependencies (captive dependencies)
- Use factory pattern for creating objects with runtime parameters
- Register services with appropriate lifetimes
- Use IOptions<T> for configuration values
2. Async/Await Best Practices
2.1 Understanding Async/Await
Async/await is a powerful feature that allows writing asynchronous code that looks and behaves like synchronous code. Proper usage is critical for scalable applications.
2.2 ❌ Common Anti-Patterns
// ❌ Async Void - Only for event handlers
public async void ProcessData() // BAD! Exception cannot be caught
{
await Task.Delay(1000);
}
// ❌ Blocking on Async Code
public string GetData()
{
return _dataService.GetDataAsync().Result; // BAD! Can cause deadlocks
}
// ❌ Using .Wait() or .Result in ASP.NET
public async Task<IActionResult> Index()
{
var result = _service.GetDataAsync().Result; // BAD! Deadlock risk
return Ok(result);
}
// ❌ Missing ConfigureAwait in library code
public async Task<string> GetDataAsync()
{
var data = await _httpClient.GetStringAsync(url); // BAD for libraries
return data;
}
// ❌ Not using cancellation tokens
public async Task<List<Order>> GetOrdersAsync()
{
return await _context.Orders.ToListAsync(); // Can't cancel if user aborts
}
2.3 ✅ Async/Await Best Practices
// ✅ Async Task - Properly awaitable
public async Task ProcessDataAsync()
{
await Task.Delay(1000);
}
// ✅ Use async all the way
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
var result = await _service.GetDataAsync(cancellationToken);
return Ok(result);
}
// ✅ Use ConfigureAwait in library code
public async Task<string> GetDataAsync()
{
// ConfigureAwait(false) tells the runtime not to capture the context
var data = await _httpClient.GetStringAsync(url).ConfigureAwait(false);
return data;
}
// ✅ Always pass CancellationToken
public async Task<List<Order>> GetOrdersAsync(CancellationToken cancellationToken = default)
{
return await _context.Orders
.Where(o => o.Status == OrderStatus.Active)
.ToListAsync(cancellationToken);
}
// ✅ ValueTask for high-performance scenarios (often cached or synchronous)
public async ValueTask<User> GetUserAsync(int id)
{
var cached = _cache.Get<User>($"user_{id}");
if (cached != null)
return cached;
return await _repository.GetByIdAsync(id);
}
2.4 Advanced Async Patterns
// Parallel execution with Task.WhenAll
public async Task<DashboardData> GetDashboardAsync(int userId, CancellationToken cancellationToken)
{
var ordersTask = _orderService.GetOrdersByUserAsync(userId, cancellationToken);
var paymentsTask = _paymentService.GetPaymentsByUserAsync(userId, cancellationToken);
var reviewsTask = _reviewService.GetReviewsByUserAsync(userId, cancellationToken);
await Task.WhenAll(ordersTask, paymentsTask, reviewsTask);
return new DashboardData
{
Orders = await ordersTask,
Payments = await paymentsTask,
Reviews = await reviewsTask
};
}
// Sequential with Task.WhenAny (race condition pattern)
public async Task<Product> GetProductFromFastestSourceAsync(int productId, CancellationToken cancellationToken)
{
var tasks = new List<Task<Product>>
{
_cacheService.GetProductAsync(productId, cancellationToken),
_databaseService.GetProductAsync(productId, cancellationToken),
_externalApiService.GetProductAsync(productId, cancellationToken)
};
var completedTask = await Task.WhenAny(tasks);
return await completedTask;
}
// Retry pattern with Polly
public class ResilientDataService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger _logger;
public async Task<string> GetDataWithRetryAsync(string url, CancellationToken cancellationToken)
{
var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (exception, timeSpan, retryCount, context) =>
{
_logger.LogWarning(exception,
"Retry {RetryCount} after {Delay}ms", retryCount, timeSpan.TotalMilliseconds);
});
var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(30));
var combinedPolicy = Policy.WrapAsync(retryPolicy, timeoutPolicy);
return await combinedPolicy.ExecuteAsync(async () =>
{
var client = _httpClientFactory.CreateClient();
return await client.GetStringAsync(url, cancellationToken);
});
}
}
// Async lazy initialization
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<Task<T>> taskFactory) : base(() => Task.Run(taskFactory))
{
}
public Task<T> GetValueAsync() => Value;
}
// Usage
public class ExpensiveService
{
private readonly AsyncLazy<ExpensiveData> _lazyData;
public ExpensiveService()
{
_lazyData = new AsyncLazy<ExpensiveData>(async () =>
{
await Task.Delay(5000); // Simulate expensive initialization
return new ExpensiveData();
});
}
public async Task<ExpensiveData> GetDataAsync()
{
return await _lazyData.GetValueAsync();
}
}
// Async Streams (IAsyncEnumerable)
public async IAsyncEnumerable<Order> GetOrdersStreamAsync(int customerId)
{
await foreach (var order in _context.Orders
.Where(o => o.CustomerId == customerId)
.AsAsyncEnumerable())
{
yield return order;
}
}
// Consuming async streams
public async Task ProcessOrdersAsync(int customerId)
{
await foreach (var order in GetOrdersStreamAsync(customerId))
{
Console.WriteLine($"Processing order {order.Id}");
// Process each order as it arrives
}
}
// Async disposable pattern
public class DatabaseConnection : IAsyncDisposable
{
private SqlConnection _connection;
public async Task OpenAsync()
{
_connection = new SqlConnection("connection_string");
await _connection.OpenAsync();
}
public async ValueTask DisposeAsync()
{
if (_connection != null)
{
await _connection.CloseAsync();
await _connection.DisposeAsync();
}
}
}
// Usage with await using
public async Task UseDatabaseAsync()
{
await using var connection = new DatabaseConnection();
await connection.OpenAsync();
// Use connection
} // Automatically disposed asynchronously
3. Unit Testing with xUnit and Moq
3.1 Test Structure - Arrange, Act, Assert
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _mockRepository;
private readonly Mock<IEmailService> _mockEmailService;
private readonly Mock<ILogger<OrderService>> _mockLogger;
private readonly OrderService _service;
public OrderServiceTests()
{
_mockRepository = new Mock<IOrderRepository>();
_mockEmailService = new Mock<IEmailService>();
_mockLogger = new Mock<ILogger<OrderService>>();
_service = new OrderService(
_mockRepository.Object,
_mockEmailService.Object,
_mockLogger.Object);
}
[Fact]
public async Task CreateOrderAsync_ValidRequest_ShouldCreateOrderAndSendEmail()
{
// Arrange
var request = new CreateOrderRequest
{
CustomerId = 1,
Items = new List<OrderItem>
{
new OrderItem { ProductId = 1, Quantity = 2, Price = 50m },
new OrderItem { ProductId = 2, Quantity = 1, Price = 100m }
}
};
Order savedOrder = null;
_mockRepository.Setup(r => r.AddAsync(It.IsAny<Order>()))
.Callback<Order>(order => savedOrder = order)
.Returns(Task.CompletedTask);
// Act
var result = await _service.CreateOrderAsync(request);
// Assert
Assert.NotNull(result);
Assert.Equal(200m, result.Total); // (2*50 + 1*100) = 200
Assert.Equal(OrderStatus.Pending, result.Status);
_mockRepository.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
_mockEmailService.Verify(e => e.SendOrderConfirmationAsync(
It.IsAny<string>(), It.IsAny<int>()), Times.Once);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(null)]
public async Task CreateOrderAsync_InvalidCustomerId_ShouldThrowException(int? customerId)
{
// Arrange
var request = new CreateOrderRequest
{
CustomerId = customerId ?? 0,
Items = new List<OrderItem>()
};
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(
() => _service.CreateOrderAsync(request));
}
[Theory]
[InlineData(100, true)]
[InlineData(1000, true)]
[InlineData(5000, false)]
public async Task CheckDiscountEligibility_ShouldReturnCorrectResult(decimal orderTotal, bool expected)
{
// Arrange
var customer = new Customer { Id = 1, TotalSpent = orderTotal };
_mockRepository.Setup(r => r.GetCustomerAsync(1))
.ReturnsAsync(customer);
// Act
var result = await _service.IsEligibleForDiscountAsync(1);
// Assert
Assert.Equal(expected, result);
}
}
// Using AutoFixture for test data generation
public class OrderServiceWithAutoFixtureTests
{
private readonly Fixture _fixture;
private readonly Mock<IOrderRepository> _mockRepository;
private readonly OrderService _service;
public OrderServiceWithAutoFixtureTests()
{
_fixture = new Fixture();
_fixture.Behaviors.OfType<ThrowingRecursionBehavior>()
.ToList()
.ForEach(b => _fixture.Behaviors.Remove(b));
_fixture.Behaviors.Add(new OmitOnRecursionBehavior());
_mockRepository = new Mock<IOrderRepository>();
_service = new OrderService(_mockRepository.Object,
Mock.Of<IEmailService>(),
Mock.Of<ILogger<OrderService>>());
}
[Fact]
public async Task GetOrderAsync_ExistingOrder_ShouldReturnOrder()
{
// Arrange
var expectedOrder = _fixture.Create<Order>();
_mockRepository.Setup(r => r.GetByIdAsync(expectedOrder.Id))
.ReturnsAsync(expectedOrder);
// Act
var result = await _service.GetOrderAsync(expectedOrder.Id);
// Assert
Assert.Equal(expectedOrder.Id, result.Id);
Assert.Equal(expectedOrder.Total, result.Total);
}
}
// Integration tests with WebApplicationFactory
public class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public OrderApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task CreateOrder_ValidRequest_ReturnsCreatedOrder()
{
// Arrange
var request = new
{
CustomerId = 1,
Items = new[]
{
new { ProductId = 1, Quantity = 2 }
}
};
var content = new StringContent(
JsonSerializer.Serialize(request),
Encoding.UTF8,
"application/json");
// Act
var response = await _client.PostAsync("/api/orders", content);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var order = await response.Content.ReadFromJsonAsync<Order>();
Assert.NotNull(order);
Assert.True(order.Id > 0);
}
}
// Mocking advanced scenarios
public class AdvancedMockingTests
{
[Fact]
public async Task Mock_WithCallback_ShouldTrackCalls()
{
var mock = new Mock<IOrderRepository>();
var savedOrders = new List<Order>();
mock.Setup(r => r.AddAsync(It.IsAny<Order>()))
.Callback<Order>(order => savedOrders.Add(order))
.Returns(Task.CompletedTask);
var service = new OrderService(mock.Object, null, null);
await service.CreateOrderAsync(new CreateOrderRequest
{
CustomerId = 1,
Items = new List<OrderItem>()
});
Assert.Single(savedOrders);
}
[Fact]
public async Task Mock_WithReturns_ShouldReturnDifferentValues()
{
var mock = new Mock<IRandomService>();
mock.SetupSequence(r => r.GetRandomNumber())
.Returns(1)
.Returns(2)
.Returns(3);
var result1 = mock.Object.GetRandomNumber();
var result2 = mock.Object.GetRandomNumber();
var result3 = mock.Object.GetRandomNumber();
Assert.Equal(1, result1);
Assert.Equal(2, result2);
Assert.Equal(3, result3);
}
[Fact]
public void Mock_WithThrows_ShouldThrowException()
{
var mock = new Mock<IDatabaseService>();
mock.Setup(r => r.SaveAsync(It.IsAny<object>()))
.ThrowsAsync(new SqlException("Connection failed"));
var service = new DataService(mock.Object);
Assert.ThrowsAsync<SqlException>(() => service.SaveDataAsync(new object()));
}
[Fact]
public void Verify_Logging_ShouldBeCalled()
{
var mockLogger = new Mock<ILogger<OrderService>>();
var service = new OrderService(null, null, mockLogger.Object);
service.ProcessWithLogging();
mockLogger.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Processing")),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.Once);
}
}
4. ASP.NET Core Middleware Pipeline
4.1 Understanding Middleware
Middleware are components that form the request pipeline in ASP.NET Core. Each middleware can process requests, pass them to the next middleware, or short-circuit the pipeline.
4.2 Custom Middleware Implementation
// ===== SIMPLE MIDDLEWARE =====
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// Before next middleware
_logger.LogInformation("Request: {Method} {Path}",
context.Request.Method, context.Request.Path);
var stopwatch = Stopwatch.StartNew();
// Call next middleware
await _next(context);
// After next middleware
stopwatch.Stop();
_logger.LogInformation("Response: {StatusCode} in {Elapsed}ms",
context.Response.StatusCode, stopwatch.ElapsedMilliseconds);
}
}
// ===== MIDDLEWARE WITH BRANCHING =====
public class MaintenanceModeMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public MaintenanceModeMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var isMaintenanceMode = _configuration.GetValue<bool>("MaintenanceMode");
if (isMaintenanceMode && !context.Request.Path.StartsWithSegments("/api/health"))
{
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync("Service is under maintenance. Please try again later.");
return; // Short-circuit the pipeline
}
await _next(context);
}
}
// ===== FACTORY-BASED MIDDLEWARE =====
public class CorrelationIdMiddleware : IMiddleware
{
private readonly ILogger<CorrelationIdMiddleware> _logger;
public CorrelationIdMiddleware(ILogger<CorrelationIdMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Items["CorrelationId"] = correlationId;
context.Response.Headers["X-Correlation-ID"] = correlationId;
using (_logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = correlationId
}))
{
_logger.LogInformation("Request started");
await next(context);
_logger.LogInformation("Request ended");
}
}
}
// ===== MIDDLEWARE EXTENSION METHODS =====
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
public static IApplicationBuilder UseMaintenanceMode(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MaintenanceModeMiddleware>();
}
public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CorrelationIdMiddleware>();
}
}
// ===== REGISTERING MIDDLEWARE IN PROGRAM.CS =====
var app = builder.Build();
// Order matters! Middleware runs in the order registered
// 1. Error handling middleware (first to catch exceptions)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
// 2. Custom middleware
app.UseMaintenanceMode();
app.UseCorrelationId();
app.UseRequestLogging();
// 3. Authentication & Authorization
app.UseAuthentication();
app.UseAuthorization();
// 4. Static files
app.UseStaticFiles();
// 5. Routing & Endpoints
app.UseRouting();
app.MapControllers();
app.MapHealthChecks("/health");
// ===== TERMINAL MIDDLEWARE =====
app.Map("/api/status", app =>
{
app.Run(async context =>
{
await context.Response.WriteAsync("OK");
});
});
// ===== CONDITIONAL MIDDLEWARE =====
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
// This only runs for API requests
context.Response.Headers.Add("X-API-Version", "1.0");
await next();
});
});
// ===== ENDPOINT MIDDLEWARE WITH MAPGET =====
app.MapGet("/api/time", async context =>
{
await context.Response.WriteAsync(DateTime.Now.ToString());
});
// ===== COMPLETE PIPELINE EXAMPLE =====
public class CustomMiddlewarePipeline
{
public static void ConfigurePipeline(WebApplication app)
{
// Exception handling
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("An error occurred");
});
});
// Rate limiting
app.Use(async (context, next) =>
{
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
if (IsRateLimited(ipAddress))
{
context.Response.StatusCode = 429;
await context.Response.WriteAsync("Too many requests");
return;
}
await next();
});
// Authentication
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("Authorization"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
await next();
});
// Response compression
app.Use(async (context, next) =>
{
context.Response.Headers["Content-Encoding"] = "gzip";
await next();
});
// Terminal middleware
app.Run(async context =>
{
await context.Response.WriteAsync("Hello World");
});
}
private static bool IsRateLimited(string ipAddress)
{
// Rate limiting logic
return false;
}
}
5. Entity Framework Patterns
5.1 Repository Pattern with Unit of Work
// ===== GENERIC REPOSITORY =====
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task<T> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
void Update(T entity);
void Remove(T entity);
Task<bool> AnyAsync(Expression<Func<T, bool>> predicate);
Task<int> CountAsync(Expression<Func<T, bool>> predicate = null);
}
public class EfRepository<T> : IRepository<T> where T : class
{
protected readonly ApplicationDbContext _context;
protected readonly DbSet<T> _dbSet;
public EfRepository(ApplicationDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public async Task<T> SingleOrDefaultAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.SingleOrDefaultAsync(predicate);
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
}
public void Update(T entity)
{
_dbSet.Update(entity);
}
public void Remove(T entity)
{
_dbSet.Remove(entity);
}
public async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.AnyAsync(predicate);
}
public async Task<int> CountAsync(Expression<Func<T, bool>> predicate = null)
{
if (predicate == null)
return await _dbSet.CountAsync();
return await _dbSet.CountAsync(predicate);
}
}
// ===== SPECIFICATION PATTERN FOR QUERIES =====
public abstract class BaseSpecification<T> : ISpecification<T>
{
public Expression<Func<T, bool>> Criteria { get; private set; }
public List<Expression<Func<T, object>>> Includes { get; } = new();
public List<string> IncludeStrings { get; } = new();
public Expression<Func<T, object>> OrderBy { get; private set; }
public Expression<Func<T, object>> OrderByDescending { get; private set; }
public int Take { get; private set; }
public int Skip { get; private set; }
public bool IsPagingEnabled { get; private set; }
protected void AddCriteria(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
protected void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
protected void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
protected void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
protected void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
{
OrderBy = orderByExpression;
}
protected void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
{
OrderByDescending = orderByDescExpression;
}
}
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
List<string> IncludeStrings { get; }
Expression<Func<T, object>> OrderBy { get; }
Expression<Func<T, object>> OrderByDescending { get; }
int Take { get; }
int Skip { get; }
bool IsPagingEnabled { get; }
}
// ===== SPECIFICATION IMPLEMENTATIONS =====
public class ActiveOrdersSpecification : BaseSpecification<Order>
{
public ActiveOrdersSpecification()
{
AddCriteria(o => o.Status == OrderStatus.Pending ||
o.Status == OrderStatus.Confirmed ||
o.Status == OrderStatus.Processing);
AddInclude(o => o.Customer);
AddInclude(o => o.Items);
ApplyOrderByDescending(o => o.CreatedAt);
}
}
public class OrdersByCustomerSpecification : BaseSpecification<Order>
{
public OrdersByCustomerSpecification(int customerId, bool includeCompleted = false)
{
AddCriteria(o => o.CustomerId == customerId);
if (!includeCompleted)
{
AddCriteria(o => o.Status != OrderStatus.Delivered &&
o.Status != OrderStatus.Cancelled);
}
AddInclude(o => o.Items);
ApplyOrderByDescending(o => o.CreatedAt);
}
}
public class HighValueOrdersSpecification : BaseSpecification<Order>
{
public HighValueOrdersSpecification(decimal minAmount, int pageNumber, int pageSize)
{
AddCriteria(o => o.TotalAmount >= minAmount);
AddInclude(o => o.Customer);
ApplyOrderByDescending(o => o.TotalAmount);
ApplyPaging((pageNumber - 1) * pageSize, pageSize);
}
}
// ===== SPECIFICATION EVALUATOR =====
public static class SpecificationEvaluator<T> where T : class
{
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> specification)
{
var query = inputQuery;
if (specification.Criteria != null)
{
query = query.Where(specification.Criteria);
}
query = specification.Includes.Aggregate(query,
(current, include) => current.Include(include));
query = specification.IncludeStrings.Aggregate(query,
(current, include) => current.Include(include));
if (specification.OrderBy != null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification.OrderByDescending != null)
{
query = query.OrderByDescending(specification.OrderByDescending);
}
if (specification.IsPagingEnabled)
{
query = query.Skip(specification.Skip).Take(specification.Take);
}
return query;
}
}
// ===== SPECIFICATION REPOSITORY =====
public interface ISpecificationRepository<T> where T : class
{
Task<T> GetSingleBySpecAsync(ISpecification<T> spec);
Task<IEnumerable<T>> GetBySpecAsync(ISpecification<T> spec);
Task<int> CountBySpecAsync(ISpecification<T> spec);
}
public class EfSpecificationRepository<T> : ISpecificationRepository<T> where T : class
{
private readonly ApplicationDbContext _context;
public EfSpecificationRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<T> GetSingleBySpecAsync(ISpecification<T> spec)
{
return await SpecificationEvaluator<T>.GetQuery(_context.Set<T>(), spec)
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<T>> GetBySpecAsync(ISpecification<T> spec)
{
return await SpecificationEvaluator<T>.GetQuery(_context.Set<T>(), spec)
.ToListAsync();
}
public async Task<int> CountBySpecAsync(ISpecification<T> spec)
{
return await SpecificationEvaluator<T>.GetQuery(_context.Set<T>(), spec)
.CountAsync();
}
}
// ===== UNIT OF WORK PATTERN =====
public interface IUnitOfWork : IDisposable
{
IRepository<Order> Orders { get; }
IRepository<Customer> Customers { get; }
IRepository<Product> Products { get; }
ISpecificationRepository<Order> OrderSpecs { get; }
Task<int> CompleteAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private IRepository<Order> _orders;
private IRepository<Customer> _customers;
private IRepository<Product> _products;
private ISpecificationRepository<Order> _orderSpecs;
private IDbContextTransaction _transaction;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
}
public IRepository<Order> Orders =>
_orders ??= new EfRepository<Order>(_context);
public IRepository<Customer> Customers =>
_customers ??= new EfRepository<Customer>(_context);
public IRepository<Product> Products =>
_products ??= new EfRepository<Product>(_context);
public ISpecificationRepository<Order> OrderSpecs =>
_orderSpecs ??= new EfSpecificationRepository<Order>(_context);
public async Task<int> CompleteAsync()
{
return await _context.SaveChangesAsync();
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
await _transaction?.CommitAsync();
}
public async Task RollbackTransactionAsync()
{
await _transaction?.RollbackAsync();
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}
// ===== USAGE IN SERVICE LAYER =====
public class OrderServiceWithUnitOfWork
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<OrderServiceWithUnitOfWork> _logger;
public OrderServiceWithUnitOfWork(IUnitOfWork unitOfWork, ILogger<OrderServiceWithUnitOfWork> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<Order> CreateOrderWithTransactionAsync(CreateOrderRequest request)
{
await _unitOfWork.BeginTransactionAsync();
try
{
// Create order
var order = new Order
{
CustomerId = request.CustomerId,
TotalAmount = request.Items.Sum(i => i.Price * i.Quantity),
Status = OrderStatus.Pending
};
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.CompleteAsync();
// Update inventory
foreach (var item in request.Items)
{
var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
if (product.StockQuantity < item.Quantity)
{
throw new InvalidOperationException($"Insufficient stock for {product.Name}");
}
product.StockQuantity -= item.Quantity;
_unitOfWork.Products.Update(product);
}
await _unitOfWork.CompleteAsync();
await _unitOfWork.CommitTransactionAsync();
return order;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating order");
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
public async Task<IEnumerable<Order>> GetCustomerOrdersAsync(int customerId)
{
var spec = new OrdersByCustomerSpecification(customerId);
return await _unitOfWork.OrderSpecs.GetBySpecAsync(spec);
}
public async Task<Order> GetActiveOrderAsync(int orderId)
{
var spec = new ActiveOrdersSpecification();
// Add additional criteria
spec.AddCriteria(o => o.Id == orderId);
return await _unitOfWork.OrderSpecs.GetSingleBySpecAsync(spec);
}
}
6. Configuration & Options Pattern
6.1 Options Pattern Implementation
// ===== SETTINGS CLASSES =====
public class EmailSettings
{
public const string SectionName = "Email";
public string SmtpServer { get; set; }
public int SmtpPort { get; set; }
public string FromEmail { get; set; }
public string FromName { get; set; }
public bool UseSsl { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public int RetryCount { get; set; } = 3;
public int TimeoutSeconds { get; set; } = 30;
}
public class DatabaseSettings
{
public const string SectionName = "Database";
public string ConnectionString { get; set; }
public int CommandTimeout { get; set; } = 30;
public int MaxRetryCount { get; set; } = 3;
public bool EnableSensitiveDataLogging { get; set; }
}
public class PaymentSettings
{
public const string SectionName = "Payment";
public string Provider { get; set; }
public string ApiKey { get; set; }
public string ApiSecret { get; set; }
public string WebhookSecret { get; set; }
public Dictionary<string, string> ProviderSettings { get; set; }
}
// ===== CONFIGURATION REGISTRATION =====
var builder = WebApplication.CreateBuilder(args);
// 1. Simple binding
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection(EmailSettings.SectionName));
// 2. Validate on startup
builder.Services.AddOptions<EmailSettings>()
.Bind(builder.Configuration.GetSection(EmailSettings.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
// 3. With validation callback
builder.Services.AddOptions<DatabaseSettings>()
.Bind(builder.Configuration.GetSection(DatabaseSettings.SectionName))
.Validate(settings =>
{
if (string.IsNullOrEmpty(settings.ConnectionString))
return false;
return true;
}, "Connection string is required");
// 4. Using configuration from multiple sources
builder.Services.Configure<PaymentSettings>(options =>
{
options.Provider = builder.Configuration["Payment:Provider"];
options.ApiKey = builder.Configuration["Payment:ApiKey"];
options.ApiSecret = builder.Configuration["Payment:ApiSecret"];
});
// 5. Using IConfigureOptions for complex setup
public class PaymentSettingsConfigurator : IConfigureOptions<PaymentSettings>
{
private readonly IConfiguration _configuration;
public PaymentSettingsConfigurator(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(PaymentSettings options)
{
options.Provider = _configuration["Payment:Provider"];
options.ApiKey = _configuration["Payment:ApiKey"];
// Additional setup
if (options.Provider == "Stripe")
{
options.ProviderSettings = new Dictionary<string, string>
{
["Version"] = "2023-10-16",
["WebhookSecret"] = _configuration["Payment:StripeWebhookSecret"]
};
}
}
}
builder.Services.ConfigureOptions<PaymentSettingsConfigurator>();
// ===== CONSUMING OPTIONS =====
// 1. IOptions - For singleton/scoped services (recommended for most cases)
public class EmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public async Task SendEmailAsync(string to, string subject, string body)
{
using var client = new SmtpClient(_settings.SmtpServer, _settings.SmtpPort);
if (_settings.UseSsl)
client.EnableSsl = true;
if (!string.IsNullOrEmpty(_settings.Username))
client.Credentials = new NetworkCredential(_settings.Username, _settings.Password);
var message = new MailMessage
{
From = new MailAddress(_settings.FromEmail, _settings.FromName),
Subject = subject,
Body = body
};
message.To.Add(to);
await client.SendMailAsync(message);
}
}
// 2. IOptionsSnapshot - For scoped services that need to reload on configuration changes
public class DynamicEmailService
{
private readonly IOptionsSnapshot<EmailSettings> _options;
public DynamicEmailService(IOptionsSnapshot<EmailSettings> options)
{
_options = options;
}
public void ShowCurrentSettings()
{
// Settings can change at runtime if configuration file is updated
var settings = _options.Value;
Console.WriteLine($"Current SMTP: {settings.SmtpServer}:{settings.SmtpPort}");
}
}
// 3. IOptionsMonitor - For watching configuration changes
public class ConfigWatcherService : BackgroundService
{
private readonly IOptionsMonitor<EmailSettings> _emailMonitor;
private readonly ILogger<ConfigWatcherService> _logger;
private IDisposable _onChangeToken;
public ConfigWatcherService(IOptionsMonitor<EmailSettings> emailMonitor, ILogger<ConfigWatcherService> logger)
{
_emailMonitor = emailMonitor;
_logger = logger;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_onChangeToken = _emailMonitor.OnChange(settings =>
{
_logger.LogInformation("Email settings changed: {Server}:{Port}",
settings.SmtpServer, settings.SmtpPort);
});
return Task.CompletedTask;
}
public override void Dispose()
{
_onChangeToken?.Dispose();
base.Dispose();
}
}
// 4. Validation with Data Annotations
public class AppSettings
{
[Required]
[MinLength(5)]
public string ApplicationName { get; set; }
[Range(1, 100)]
public int MaxConcurrentRequests { get; set; } = 10;
[Url]
public string BaseUrl { get; set; }
[EmailAddress]
public string SupportEmail { get; set; }
[RegularExpression(@"^[A-Z]{2}-\d{4}$")]
public string RegionCode { get; set; }
}
// Register with validation
builder.Services.AddOptions<AppSettings>()
.Bind(builder.Configuration.GetSection("App"))
.ValidateDataAnnotations()
.ValidateOnStart();
7. Logging & Telemetry
7.1 Structured Logging with ILogger
// ===== BASIC LOGGING =====
public class OrderProcessor
{
private readonly ILogger<OrderProcessor> _logger;
public OrderProcessor(ILogger<OrderProcessor> logger)
{
_logger = logger;
}
public async Task ProcessOrderAsync(Order order)
{
// Log with structured data
_logger.LogInformation("Processing order {OrderId} for customer {CustomerId}",
order.Id, order.CustomerId);
try
{
// Business logic
await Task.Delay(100);
_logger.LogInformation("Order {OrderId} processed successfully", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing order {OrderId}", order.Id);
throw;
}
}
}
// ===== LOG SCOPES =====
public class ScopedLoggingService
{
private readonly ILogger<ScopedLoggingService> _logger;
public ScopedLoggingService(ILogger<ScopedLoggingService> logger)
{
_logger = logger;
}
public async Task ProcessWithScopeAsync(int orderId, string customerEmail)
{
// Create a scope - all logs within this scope will include these properties
using (_logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = orderId,
["CustomerEmail"] = customerEmail,
["CorrelationId"] = Guid.NewGuid()
}))
{
_logger.LogInformation("Starting order processing");
await Step1Async();
await Step2Async();
await Step3Async();
_logger.LogInformation("Order processing completed");
}
}
private async Task Step1Async()
{
_logger.LogDebug("Executing step 1");
await Task.Delay(10);
}
private async Task Step2Async()
{
_logger.LogDebug("Executing step 2");
await Task.Delay(10);
}
private async Task Step3Async()
{
_logger.LogDebug("Executing step 3");
await Task.Delay(10);
}
}
// ===== CUSTOM LOGGER EXTENSIONS =====
public static class LoggerExtensions
{
private static readonly Action<ILogger, int, decimal, Exception> _orderCreated =
LoggerMessage.Define<int, decimal>(
LogLevel.Information,
new EventId(1001, "OrderCreated"),
"Order {OrderId} created with amount {Amount:C}");
private static readonly Action<ILogger, int, Exception> _orderShipped =
LoggerMessage.Define<int>(
LogLevel.Information,
new EventId(1002, "OrderShipped"),
"Order {OrderId} has been shipped");
private static readonly Action<ILogger, int, Exception> _orderCancelled =
LoggerMessage.Define<int>(
LogLevel.Warning,
new EventId(1003, "OrderCancelled"),
"Order {OrderId} was cancelled");
public static void OrderCreated(this ILogger logger, int orderId, decimal amount)
{
_orderCreated(logger, orderId, amount, null);
}
public static void OrderShipped(this ILogger logger, int orderId)
{
_orderShipped(logger, orderId, null);
}
public static void OrderCancelled(this ILogger logger, int orderId)
{
_orderCancelled(logger, orderId, null);
}
}
// Usage
public class OrderServiceWithExtensions
{
private readonly ILogger<OrderServiceWithExtensions> _logger;
public OrderServiceWithExtensions(ILogger<OrderServiceWithExtensions> logger)
{
_logger = logger;
}
public async Task CreateOrderAsync(Order order)
{
_logger.OrderCreated(order.Id, order.TotalAmount);
// Business logic
}
}
// ===== SERILOG CONFIGURATION =====
/*
// Install packages:
// Serilog.AspNetCore
// Serilog.Sinks.Console
// Serilog.Sinks.File
// Serilog.Sinks.Seq
// Serilog.Enrichers.Environment
// Serilog.Enrichers.Thread
*/
public class SerilogSetup
{
public static void Configure()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.Enrich.WithProperty("Application", "MyApp")
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.Enrich.WithThreadId()
.Enrich.WithProcessId()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File("logs/app-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.Seq("http://localhost:5341")
.CreateLogger();
}
}
// In Program.cs
// builder.Host.UseSerilog();
// ===== APPLICATION INSIGHTS TELEMETRY =====
public class TelemetryService
{
private readonly TelemetryClient _telemetryClient;
private readonly ILogger<TelemetryService> _logger;
public TelemetryService(TelemetryClient telemetryClient, ILogger<TelemetryService> logger)
{
_telemetryClient = telemetryClient;
_logger = logger;
}
public async Task<T> TrackOperationAsync<T>(string operationName, Func<Task<T>> operation, Dictionary<string, string> properties = null)
{
var stopwatch = Stopwatch.StartNew();
var startTime = DateTime.UtcNow;
try
{
var result = await operation();
stopwatch.Stop();
_telemetryClient.TrackDependency(operationName, operationName, startTime, stopwatch.Elapsed, true);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_telemetryClient.TrackException(ex, properties);
_telemetryClient.TrackDependency(operationName, operationName, startTime, stopwatch.Elapsed, false);
throw;
}
}
public void TrackEvent(string eventName, Dictionary<string, string> properties = null, Dictionary<string, double> metrics = null)
{
_telemetryClient.TrackEvent(eventName, properties, metrics);
_logger.LogInformation("Event tracked: {EventName}", eventName);
}
public void TrackMetric(string metricName, double value, Dictionary<string, string> properties = null)
{
_telemetryClient.TrackMetric(metricName, value, properties);
}
}
// Usage
public class CheckoutServiceWithTelemetry
{
private readonly TelemetryService _telemetry;
public CheckoutServiceWithTelemetry(TelemetryService telemetry)
{
_telemetry = telemetry;
}
public async Task<Order> CheckoutAsync(CheckoutRequest request)
{
var properties = new Dictionary<string, string>
{
["CustomerId"] = request.CustomerId.ToString(),
["PaymentMethod"] = request.PaymentMethod
};
_telemetry.TrackEvent("CheckoutStarted", properties);
var result = await _telemetry.TrackOperationAsync("ProcessPayment", async () =>
{
// Payment processing logic
await Task.Delay(500);
return new Order { Id = 12345 };
}, properties);
_telemetry.TrackMetric("OrderAmount", (double)result.TotalAmount, properties);
_telemetry.TrackEvent("CheckoutCompleted", properties);
return result;
}
}
8. Global Exception Handling
8.1 Exception Handling Middleware
// ===== CUSTOM EXCEPTION TYPES =====
public abstract class AppException : Exception
{
public int StatusCode { get; }
public string ErrorCode { get; }
protected AppException(string message, int statusCode, string errorCode) : base(message)
{
StatusCode = statusCode;
ErrorCode = errorCode;
}
}
public class NotFoundException : AppException
{
public NotFoundException(string entityName, object id)
: base($"{entityName} with id '{id}' not found", 404, "NOT_FOUND")
{
}
public NotFoundException(string message) : base(message, 404, "NOT_FOUND")
{
}
}
public class ValidationException : AppException
{
public Dictionary<string, string[]> Errors { get; }
public ValidationException(string message) : base(message, 400, "VALIDATION_ERROR")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(Dictionary<string, string[]> errors)
: base("Validation failed", 400, "VALIDATION_ERROR")
{
Errors = errors;
}
}
public class BusinessException : AppException
{
public BusinessException(string message) : base(message, 400, "BUSINESS_ERROR")
{
}
}
public class UnauthorizedException : AppException
{
public UnauthorizedException(string message) : base(message, 401, "UNAUTHORIZED")
{
}
}
public class ForbiddenException : AppException
{
public ForbiddenException(string message) : base(message, 403, "FORBIDDEN")
{
}
}
// ===== EXCEPTION HANDLING MIDDLEWARE =====
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IWebHostEnvironment _env;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IWebHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "An error occurred while processing request {Path}", context.Request.Path);
var response = new ErrorResponse();
switch (exception)
{
case NotFoundException notFound:
response.StatusCode = notFound.StatusCode;
response.ErrorCode = notFound.ErrorCode;
response.Message = notFound.Message;
break;
case ValidationException validation:
response.StatusCode = validation.StatusCode;
response.ErrorCode = validation.ErrorCode;
response.Message = validation.Message;
response.Errors = validation.Errors;
break;
case BusinessException business:
response.StatusCode = business.StatusCode;
response.ErrorCode = business.ErrorCode;
response.Message = business.Message;
break;
case UnauthorizedException unauthorized:
response.StatusCode = unauthorized.StatusCode;
response.ErrorCode = unauthorized.ErrorCode;
response.Message = unauthorized.Message;
break;
case ForbiddenException forbidden:
response.StatusCode = forbidden.StatusCode;
response.ErrorCode = forbidden.ErrorCode;
response.Message = forbidden.Message;
break;
case DbUpdateConcurrencyException concurrency:
response.StatusCode = 409;
response.ErrorCode = "CONCURRENCY_ERROR";
response.Message = "The record was modified by another user";
break;
case SqlException sqlEx when sqlEx.Number == 2601 || sqlEx.Number == 2627:
response.StatusCode = 409;
response.ErrorCode = "DUPLICATE_KEY";
response.Message = "A record with the same key already exists";
break;
case FluentValidation.ValidationException fluentValidation:
response.StatusCode = 400;
response.ErrorCode = "VALIDATION_ERROR";
response.Message = "Validation failed";
response.Errors = fluentValidation.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToArray()
);
break;
default:
response.StatusCode = 500;
response.ErrorCode = "INTERNAL_SERVER_ERROR";
response.Message = _env.IsDevelopment()
? exception.Message
: "An unexpected error occurred. Please try again later.";
break;
}
context.Response.ContentType = "application/json";
context.Response.StatusCode = response.StatusCode;
var jsonResponse = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
await context.Response.WriteAsync(jsonResponse);
}
}
public class ErrorResponse
{
public int StatusCode { get; set; }
public string ErrorCode { get; set; }
public string Message { get; set; }
public string TraceId { get; set; } = Activity.Current?.Id ?? HttpContextAccessor.HttpContext?.TraceIdentifier;
public Dictionary<string, string[]> Errors { get; set; }
public string Path { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
// ===== EXCEPTION HANDLING FILTER =====
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IWebHostEnvironment _env;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger, IWebHostEnvironment env)
{
_logger = logger;
_env = env;
}
public void OnException(ExceptionContext context)
{
_logger.LogError(context.Exception, "Exception occurred in controller");
var response = new ErrorResponse
{
Path = context.HttpContext.Request.Path
};
switch (context.Exception)
{
case ValidationException validation:
response.StatusCode = validation.StatusCode;
response.ErrorCode = validation.ErrorCode;
response.Message = validation.Message;
response.Errors = validation.Errors;
break;
case NotFoundException notFound:
response.StatusCode = notFound.StatusCode;
response.ErrorCode = notFound.ErrorCode;
response.Message = notFound.Message;
break;
default:
response.StatusCode = 500;
response.ErrorCode = "INTERNAL_SERVER_ERROR";
response.Message = _env.IsDevelopment()
? context.Exception.Message
: "An unexpected error occurred";
break;
}
context.Result = new ObjectResult(response)
{
StatusCode = response.StatusCode
};
context.ExceptionHandled = true;
}
}
// ===== REGISTRATION IN PROGRAM.CS =====
var app = builder.Build();
// Global exception handling middleware
app.UseMiddleware<GlobalExceptionMiddleware>();
// Or use built-in exception handling
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandler = context.RequestServices.GetRequiredService<ILogger<Program>>();
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
exceptionHandler.LogError(exception, "Unhandled exception");
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var response = new { error = "An error occurred" };
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
});
});
}
// Register exception filter globally
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalExceptionFilter>();
});
9. Summary and Key Takeaways
- Dependency Injection: Use constructor injection, register services with appropriate lifetimes, avoid service locator pattern
- Async/Await: Use async all the way, pass cancellation tokens, use ConfigureAwait(false) in libraries
- Unit Testing: Follow Arrange-Act-Assert pattern, use mocks for dependencies, test both success and failure scenarios
- Middleware: Understand pipeline order, create focused middleware components, use extension methods for registration
- EF Patterns: Use Repository pattern for data access abstraction, Specification pattern for complex queries, Unit of Work for transactions
- Configuration: Use Options pattern with validation, consider IOptionsSnapshot for reloadable config
- Logging: Use structured logging, create scopes for correlation, leverage telemetry for production monitoring
- Exception Handling: Use custom exception types, global exception middleware, return consistent error responses
🎯 Production-Ready Checklist:
- ✅ All services registered with appropriate lifetimes
- ✅ Async methods with cancellation tokens
- ✅ Comprehensive unit tests covering edge cases
- ✅ Structured logging with correlation IDs
- ✅ Global exception handling
- ✅ Configuration validation on startup
- ✅ Health checks for dependencies
- ✅ Performance monitoring and telemetry
- ✅ Proper middleware ordering
10. Practice Exercises
Exercise 1: Implement a Retry Mechanism
Create a service that implements retry logic with exponential backoff for external API calls.
Click to see solution
public interface IRetryService
{
Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, CancellationToken cancellationToken = default);
}
public class RetryService : IRetryService
{
private readonly ILogger<RetryService> _logger;
private readonly int _maxRetries;
private readonly TimeSpan _initialDelay;
public RetryService(ILogger<RetryService> logger, IConfiguration configuration)
{
_logger = logger;
_maxRetries = configuration.GetValue<int>("Retry:MaxRetries", 3);
_initialDelay = TimeSpan.FromSeconds(configuration.GetValue<int>("Retry:InitialDelaySeconds", 1));
}
public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, CancellationToken cancellationToken = default)
{
int retryCount = 0;
TimeSpan delay = _initialDelay;
while (true)
{
try
{
return await operation();
}
catch (Exception ex) when (IsRetryableException(ex) && retryCount < _maxRetries)
{
retryCount++;
_logger.LogWarning(ex, "Operation failed (attempt {RetryCount}/{MaxRetries}). Retrying after {Delay}ms",
retryCount, _maxRetries, delay.TotalMilliseconds);
await Task.Delay(delay, cancellationToken);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff
}
}
}
private bool IsRetryableException(Exception ex)
{
return ex is HttpRequestException ||
ex is TimeoutException ||
(ex is SqlException sqlEx && sqlEx.Number is 1205 or -2); // Deadlock or timeout
}
}
Exercise 2: Create a Rate Limiting Middleware
Implement middleware that limits requests per IP address.
Click to see solution
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RateLimitingMiddleware> _logger;
private readonly IMemoryCache _cache;
private readonly int _requestLimit;
private readonly TimeSpan _timeWindow;
public RateLimitingMiddleware(
RequestDelegate next,
ILogger<RateLimitingMiddleware> logger,
IMemoryCache cache,
IConfiguration configuration)
{
_next = next;
_logger = logger;
_cache = cache;
_requestLimit = configuration.GetValue<int>("RateLimiting:RequestLimit", 100);
_timeWindow = TimeSpan.FromMinutes(configuration.GetValue<int>("RateLimiting:TimeWindowMinutes", 1));
}
public async Task InvokeAsync(HttpContext context)
{
var ipAddress = context.Connection.RemoteIpAddress?.ToString();
if (ipAddress == null)
{
await _next(context);
return;
}
var cacheKey = $"rate_limit_{ipAddress}";
var requestCount = _cache.Get<int>(cacheKey);
if (requestCount >= _requestLimit)
{
_logger.LogWarning("Rate limit exceeded for IP {IpAddress}", ipAddress);
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.Response.WriteAsync("Too many requests. Please try again later.");
return;
}
_cache.Set(cacheKey, (requestCount + 1), _timeWindow);
await _next(context);
}
}
11. What's Next?
In Chapter 7, we'll explore Domain Driven Design (DDD) Basics - Entities, Value Objects, Aggregates, Repositories, and Domain Services. These concepts will help you model complex business domains effectively.
Chapter 7 Preview:
- Entities vs Value Objects - Understanding identity and immutability
- Aggregates and Aggregate Roots - Maintaining consistency boundaries
- Repositories - Abstraction for aggregate persistence
- Domain Events - Decoupling business logic
- Domain Services - Encapsulating domain logic that doesn't fit in entities
- Specification Pattern - Business rules as first-class citizens
📝 Practice Assignments:
- Implement a complete DI setup with different lifetimes and factory patterns
- Create an async service with proper cancellation token support and retry logic
- Write comprehensive unit tests for a service with mocked dependencies
- Build a custom middleware pipeline with authentication, logging, and rate limiting
- Implement Repository and Unit of Work patterns for data access
- Configure structured logging with Serilog and Application Insights
- Create a global exception handling system with custom exception types
Happy Coding! 🚀