
Table of Contents
The Philosophy of Error Handling
Understanding Exception Types in .NET
Basic Try-Catch Patterns
Global Error Handling with Middleware
Custom Exception Middleware
Exception Filters in MVC
Structured Logging with Serilog
Error Monitoring & Alerting
API Error Responses & Standards
Validation Error Handling
Database & ORM Error Handling
Async/Await Error Patterns
Custom Exception Classes
Error Handling in Background Services
Health Checks & Diagnostics
Performance Considerations
Security Aspects of Error Handling
Testing Error Scenarios
Real-World Implementation Example
Conclusion & Best Practices
1. The Philosophy of Error Handling
Error handling is not just about catching exceptions—it's about creating resilient, maintainable applications that provide meaningful feedback to users and developers. In modern web applications, proper error handling is crucial for:
User Experience: Providing clear, helpful error messages
Debugging: Capturing sufficient context for issue resolution
Monitoring: Tracking application health and error trends
Security: Preventing information leakage through error messages
Real-World Analogy: Air Traffic Control
Think of error handling like air traffic control:
Try-Catch: Pilot's immediate reaction to turbulence
Middleware: Air traffic control's monitoring system
Logging: Flight data recorder (black box)
Global Handling: Emergency procedures and protocols
2. Understanding Exception Types in .NET
Common Exception Hierarchy
// Base exception types
public void DemonstrateExceptionTypes()
{
try
{
// System.Exception - The root of all exceptions
throw new Exception("This is a generic exception");
// System.SystemException - Base class for system exceptions
throw new SystemException("System-level exception occurred");
// System.ApplicationException - Base class for application exceptions
throw new ApplicationException("Application-specific error");
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception: {ex.GetType().Name}");
}
}
Specific Exception Types with Real-World Context
public class BankAccountService
{
private readonly Dictionary<string, decimal> _accounts = new();
public void TransferMoney(string fromAccount, string toAccount, decimal amount)
{
try
{
// ArgumentException - Invalid input parameters
if (string.IsNullOrEmpty(fromAccount))
throw new ArgumentException("Source account cannot be empty", nameof(fromAccount));
if (string.IsNullOrEmpty(toAccount))
throw new ArgumentException("Destination account cannot be empty", nameof(toAccount));
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount), "Transfer amount must be positive");
// InvalidOperationException - Object in invalid state
if (!_accounts.ContainsKey(fromAccount))
throw new InvalidOperationException($"Source account {fromAccount} does not exist");
if (!_accounts.ContainsKey(toAccount))
throw new InvalidOperationException($"Destination account {toAccount} does not exist");
// Custom business logic exception
if (_accounts[fromAccount] < amount)
throw new InsufficientFundsException(fromAccount, amount, _accounts[fromAccount]);
// Perform transfer
_accounts[fromAccount] -= amount;
_accounts[toAccount] += amount;
}
catch (ArgumentException ex)
{
// Log and rethrow with additional context
throw new BankingOperationException(
"Invalid parameters provided for money transfer", ex);
}
}
}
// Custom business exception
public class InsufficientFundsException : Exception
{
public string AccountNumber { get; }
public decimal RequestedAmount { get; }
public decimal CurrentBalance { get; }
public InsufficientFundsException(string accountNumber, decimal requestedAmount, decimal currentBalance)
: base($"Insufficient funds in account {accountNumber}. " +
$"Requested: {requestedAmount:C}, Available: {currentBalance:C}")
{
AccountNumber = accountNumber;
RequestedAmount = requestedAmount;
CurrentBalance = currentBalance;
}
}
3. Basic Try-Catch Patterns <a name="try-catch-patterns"></a>
Fundamental Try-Catch-Finally
public class FileProcessor
{
public string ReadFileContent(string filePath)
{
FileStream fileStream = null;
StreamReader reader = null;
try
{
// Attempt to open and read file
fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
reader = new StreamReader(fileStream);
return reader.ReadToEnd();
}
catch (FileNotFoundException ex)
{
// Specific handling for file not found
throw new ProcessingException($"File not found: {filePath}", ex);
}
catch (DirectoryNotFoundException ex)
{
// Specific handling for directory not found
throw new ProcessingException($"Directory not found for file: {filePath}", ex);
}
catch (IOException ex) when (ex.Message.Contains("sharing violation"))
{
// Filtered catch for file locking issues
throw new ProcessingException($"File is locked by another process: {filePath}", ex);
}
catch (Exception ex)
{
// General exception handling
throw new ProcessingException($"Error reading file: {filePath}", ex);
}
finally
{
// Always execute - cleanup resources
reader?.Dispose();
fileStream?.Dispose();
}
}
}
Modern Using Statements with Error Handling
public async Task<string> ReadFileAsync(string filePath)
{
try
{
// Using declaration automatically disposes the resource
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var reader = new StreamReader(fileStream);
return await reader.ReadToEndAsync();
}
catch (Exception ex) when (IsFileAccessError(ex))
{
// Filter based on custom condition
throw new ProcessingException($"File access error: {filePath}", ex);
}
}
private bool IsFileAccessError(Exception ex)
{
return ex is FileNotFoundException ||
ex is DirectoryNotFoundException ||
ex is UnauthorizedAccessException;
}
Exception Filters in Action
public class PaymentProcessor
{
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
try
{
// Payment processing logic
var result = await _paymentGateway.ProcessAsync(request);
return result;
}
catch (HttpRequestException ex) when (ex.Message.Contains("timeout", StringComparison.OrdinalIgnoreCase))
{
// Handle timeout specifically
throw new PaymentTimeoutException("Payment gateway timeout", ex);
}
catch (HttpRequestException ex) when ((int?)ex.StatusCode >= 500)
{
// Handle server errors from payment gateway
throw new PaymentGatewayException("Payment gateway server error", ex);
}
catch (HttpRequestException ex) when ((int?)ex.StatusCode == 401)
{
// Handle authentication errors
throw new PaymentAuthenticationException("Invalid payment gateway credentials", ex);
}
}
}
4. Global Error Handling with Middleware <a name="global-error-handling"></a>
Built-in Exception Handling Middleware
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Development environment error handling
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage(); // For Entity Framework errors
}
else
{
// Production environment error handling
app.UseExceptionHandler("/error");
app.UseStatusCodePagesWithReExecute("/error/{0}");
// HSTS for security
app.UseHsts();
}
// Custom error handling middleware
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Custom Error Handling Endpoint
[ApiController]
public class ErrorController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
public ErrorController(IWebHostEnvironment environment)
{
_environment = environment;
}
[Route("/error")]
public IActionResult HandleError()
{
var exceptionHandlerPathFeature =
HttpContext.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
var path = exceptionHandlerPathFeature?.Path;
// Log the error here
_logger.LogError(exception, "Unhandled exception occurred for path: {Path}", path);
if (_environment.IsDevelopment())
{
return Problem(
detail: exception?.ToString(),
instance: path,
title: exception?.Message);
}
// Production response
return Problem(
detail: "An error occurred while processing your request.",
instance: path,
title: "Internal Server Error");
}
[Route("/error/{statusCode}")]
public IActionResult HandleError(int statusCode)
{
var statusCodeReExecuteFeature =
HttpContext.Features.Get<IStatusCodeReExecuteFeature>();
var path = statusCodeReExecuteFeature?.OriginalPath;
_logger.LogWarning("HTTP {StatusCode} occurred for path: {Path}", statusCode, path);
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetStatusCodeTitle(statusCode),
Instance = path,
Detail = GetStatusCodeDetail(statusCode)
};
return StatusCode(statusCode, problemDetails);
}
private static string GetStatusCodeTitle(int statusCode) => statusCode switch
{
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Error"
};
private static string GetStatusCodeDetail(int statusCode) => statusCode switch
{
400 => "The request was malformed or contained invalid parameters.",
401 => "Authentication is required to access this resource.",
403 => "You don't have permission to access this resource.",
404 => "The requested resource was not found.",
500 => "An internal server error occurred.",
_ => "An error occurred."
};
}
5. Custom Exception Middleware <a name="custom-middleware"></a>
Comprehensive Global Exception Middleware
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
private readonly IWebHostEnvironment _environment;
public GlobalExceptionMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger,
IWebHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
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)
{
// Log the exception with structured logging
LogException(exception, context);
// Determine the response based on exception type
var (statusCode, problemDetails) = CreateProblemDetails(exception, context);
// Set response properties
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/problem+json";
// Serialize and return the problem details
var json = JsonSerializer.Serialize(problemDetails, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = _environment.IsDevelopment()
});
await context.Response.WriteAsync(json);
}
private void LogException(Exception exception, HttpContext context)
{
var logLevel = GetLogLevel(exception);
var eventId = new EventId(exception.HResult);
var request = context.Request;
_logger.Log(
logLevel,
eventId,
exception,
"Unhandled exception occurred. " +
"Method: {Method}, Path: {Path}, QueryString: {QueryString}",
request.Method,
request.Path,
request.QueryString);
}
private static LogLevel GetLogLevel(Exception exception) => exception switch
{
ValidationException => LogLevel.Warning,
NotFoundException => LogLevel.Warning,
UnauthorizedAccessException => LogLevel.Warning,
_ => LogLevel.Error
};
private (int StatusCode, ProblemDetails ProblemDetails) CreateProblemDetails(
Exception exception, HttpContext context)
{
var statusCode = exception switch
{
ValidationException => StatusCodes.Status400BadRequest,
NotFoundException => StatusCodes.Status404NotFound,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
BusinessRuleException => StatusCodes.Status422UnprocessableEntity,
_ => StatusCodes.Status500InternalServerError
};
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetExceptionTitle(exception),
Detail = GetExceptionDetail(exception),
Instance = context.Request.Path,
Type = GetExceptionType(exception)
};
// Add additional information for specific exception types
if (exception is ValidationException validationException)
{
problemDetails.Extensions["errors"] = validationException.Errors;
}
if (exception is BusinessRuleException businessException)
{
problemDetails.Extensions["rule"] = businessException.RuleName;
problemDetails.Extensions["businessContext"] = businessException.BusinessContext;
}
// Include stack trace in development
if (_environment.IsDevelopment())
{
problemDetails.Extensions["trace"] = exception.StackTrace;
problemDetails.Extensions["innerException"] = exception.InnerException?.Message;
}
return (statusCode, problemDetails);
}
private static string GetExceptionTitle(Exception exception) => exception switch
{
ValidationException => "Validation Error",
NotFoundException => "Resource Not Found",
UnauthorizedAccessException => "Access Denied",
BusinessRuleException => "Business Rule Violation",
_ => "Internal Server Error"
};
private string GetExceptionDetail(Exception exception) => exception switch
{
ValidationException validationEx => validationEx.Message,
NotFoundException notFoundEx => notFoundEx.Message,
UnauthorizedAccessException => "You don't have permission to access this resource.",
BusinessRuleException businessEx => businessEx.Message,
_ => _environment.IsDevelopment()
? exception.Message
: "An error occurred while processing your request."
};
private static string GetExceptionType(Exception exception) => exception switch
{
ValidationException => "https://tools.ietf.org/html/rfc7231#section-6.5.1",
NotFoundException => "https://tools.ietf.org/html/rfc7231#section-6.5.4",
UnauthorizedAccessException => "https://tools.ietf.org/html/rfc7235#section-3.1",
BusinessRuleException => "https://tools.ietf.org/html/rfc4918#section-11.2",
_ => "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
}
Advanced Exception Middleware with Correlation IDs
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationIdHeader = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var correlationId = GetOrCreateCorrelationId(context);
context.Items["CorrelationId"] = correlationId;
context.Response.Headers[CorrelationIdHeader] = correlationId;
// Add correlation ID to logger scope
using (_logger.BeginScope("{CorrelationId}", correlationId))
{
await _next(context);
}
}
private static string GetOrCreateCorrelationId(HttpContext context)
{
if (context.Request.Headers.TryGetValue(CorrelationIdHeader, out var correlationId))
{
return correlationId.ToString();
}
return Guid.NewGuid().ToString();
}
}
// Enhanced exception middleware with correlation ID
public class EnhancedExceptionMiddleware
{
// ... previous code ...
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var correlationId = context.Items["CorrelationId"] as string ?? "unknown";
// Log with correlation ID
_logger.LogError(
exception,
"Unhandled exception with correlation ID: {CorrelationId}",
correlationId);
var problemDetails = CreateProblemDetails(exception, context, correlationId);
context.Response.StatusCode = problemDetails.Status ?? 500;
context.Response.ContentType = "application/problem+json";
var json = JsonSerializer.Serialize(problemDetails);
await context.Response.WriteAsync(json);
}
private ProblemDetails CreateProblemDetails(Exception exception, HttpContext context, string correlationId)
{
var problemDetails = new ProblemDetails
{
Status = GetStatusCode(exception),
Title = GetExceptionTitle(exception),
Detail = GetExceptionDetail(exception),
Instance = context.Request.Path,
Extensions = { ["correlationId"] = correlationId }
};
// Add machine name for debugging
problemDetails.Extensions["machineName"] = Environment.MachineName;
// Add timestamp
problemDetails.Extensions["timestamp"] = DateTime.UtcNow.ToString("O");
return problemDetails;
}
}
6. Exception Filters in MVC <a name="exception-filters"></a>
Global Exception Filter
public class GlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<GlobalExceptionFilter> _logger;
private readonly IWebHostEnvironment _environment;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger, IWebHostEnvironment environment)
{
_logger = logger;
_environment = environment;
}
public void OnException(ExceptionContext context)
{
// Only handle if exception isn't already handled
if (context.ExceptionHandled) return;
var exception = context.Exception;
var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
LogException(exception, actionDescriptor);
var result = CreateErrorResult(exception, actionDescriptor);
context.Result = result;
context.ExceptionHandled = true;
}
private void LogException(Exception exception, ControllerActionDescriptor actionDescriptor)
{
var logLevel = GetLogLevel(exception);
_logger.Log(
logLevel,
exception,
"Exception in {Controller}.{Action}",
actionDescriptor?.ControllerName,
actionDescriptor?.ActionName);
}
private IActionResult CreateErrorResult(Exception exception, ControllerActionDescriptor actionDescriptor)
{
// API controllers return ProblemDetails
if (IsApiController(actionDescriptor))
{
return CreateProblemDetailsResult(exception);
}
// MVC controllers return error views
return CreateViewResult(exception);
}
private bool IsApiController(ControllerActionDescriptor actionDescriptor)
{
return actionDescriptor?.ControllerTypeInfo
.GetCustomAttributes<ApiControllerAttribute>()
.Any() == true;
}
private IActionResult CreateProblemDetailsResult(Exception exception)
{
var statusCode = GetStatusCode(exception);
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetExceptionTitle(exception),
Detail = _environment.IsDevelopment() ? exception.Message : null,
Type = GetExceptionType(exception)
};
if (exception is ValidationException validationException)
{
problemDetails.Extensions["errors"] = validationException.Errors;
}
return new ObjectResult(problemDetails)
{
StatusCode = statusCode
};
}
private IActionResult CreateViewResult(Exception exception)
{
var statusCode = GetStatusCode(exception);
var viewName = statusCode switch
{
404 => "NotFound",
403 => "AccessDenied",
_ => "Error"
};
var model = new ErrorViewModel
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier,
Message = _environment.IsDevelopment() ? exception.Message : "An error occurred.",
StackTrace = _environment.IsDevelopment() ? exception.StackTrace : null
};
return new ViewResult
{
ViewName = viewName,
StatusCode = statusCode,
ViewData = new ViewDataDictionary<ErrorViewModel>(ViewData, model)
};
}
}
Action-Specific Exception Filter
public class HandleBusinessExceptionAttribute : ExceptionFilterAttribute
{
private readonly string _errorView;
private readonly string _logMessage;
public HandleBusinessExceptionAttribute(string errorView = "BusinessError", string logMessage = null)
{
_errorView = errorView;
_logMessage = logMessage;
}
public override void OnException(ExceptionContext context)
{
if (context.Exception is BusinessRuleException businessException)
{
// Log the business exception
if (!string.IsNullOrEmpty(_logMessage))
{
context.HttpContext.RequestServices
.GetRequiredService<ILogger<HandleBusinessExceptionAttribute>>()
.LogWarning(businessException, _logMessage);
}
// Handle the exception
context.ExceptionHandled = true;
if (IsApiController(context))
{
context.Result = new ObjectResult(new
{
error = businessException.Message,
rule = businessException.RuleName,
context = businessException.BusinessContext
})
{
StatusCode = 422 // Unprocessable Entity
};
}
else
{
context.Result = new ViewResult
{
ViewName = _errorView,
ViewData = new ViewDataDictionary(new Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary())
{
Model = new BusinessErrorViewModel
{
Message = businessException.Message,
RuleName = businessException.RuleName,
BusinessContext = businessException.BusinessContext
}
}
};
}
}
}
private static bool IsApiController(ExceptionContext context)
{
return context.ActionDescriptor is ControllerActionDescriptor descriptor &&
descriptor.ControllerTypeInfo.GetCustomAttributes<ApiControllerAttribute>().Any();
}
}
// Usage in controllers
[ApiController]
[Route("api/[controller]")]
[HandleBusinessException]
public class OrdersController : ControllerBase
{
[HttpPost]
[HandleBusinessException("OrderError", "Business rule violation in order creation")]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
// Business logic that might throw BusinessRuleException
var order = await _orderService.CreateOrderAsync(request);
return Ok(order);
}
}
7. Structured Logging with Serilog <a name="structured-logging"></a>
Comprehensive Serilog Configuration
public static class LoggingConfiguration
{
public static IHostBuilder ConfigureSerilog(this IHostBuilder hostBuilder)
{
return hostBuilder.UseSerilog((context, services, configuration) =>
{
var env = context.HostingEnvironment;
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentName()
.Enrich.WithProperty("Application", context.HostingEnvironment.ApplicationName)
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
.Enrich.WithExceptionDetails()
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} " +
"{Properties:j}{NewLine}{Exception}")
.WriteTo.Debug()
.WriteTo.File(
path: "Logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7,
shared: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.Seq(serverUrl: context.Configuration.GetValue<string>("Seq:ServerUrl"))
.WriteTo.ApplicationInsights(
services.GetRequiredService<TelemetryConfiguration>(),
TelemetryConverter.Traces);
// Add additional sinks based on environment
if (env.IsProduction())
{
configuration.WriteTo.AzureAnalytics(
workspaceId: context.Configuration["AzureLogAnalytics:WorkspaceId"],
authenticationId: context.Configuration["AzureLogAnalytics:AuthenticationId"]);
}
});
}
}
Structured Logging in Action
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task<Order> ProcessOrderAsync(OrderRequest request)
{
using var activity = _logger.BeginScope(new Dictionary<string, object>
{
["OrderId"] = request.OrderId,
["CustomerId"] = request.CustomerId,
["TotalAmount"] = request.TotalAmount
});
try
{
_logger.LogInformation(
"Starting order processing for order {OrderId} from customer {CustomerId}",
request.OrderId,
request.CustomerId);
// Validate order
if (!await ValidateOrderAsync(request))
{
_logger.LogWarning(
"Order validation failed for order {OrderId}. Customer: {CustomerId}",
request.OrderId,
request.CustomerId);
throw new ValidationException("Order validation failed");
}
// Process payment
var paymentResult = await ProcessPaymentAsync(request);
_logger.LogInformation(
"Payment processed successfully for order {OrderId}. " +
"PaymentId: {PaymentId}, Amount: {Amount}",
request.OrderId,
paymentResult.PaymentId,
paymentResult.Amount);
// Create order
var order = await CreateOrderAsync(request, paymentResult);
_logger.LogInformation(
"Order created successfully. OrderId: {OrderId}, Status: {Status}",
order.Id,
order.Status);
return order;
}
catch (PaymentException ex)
{
_logger.LogError(
ex,
"Payment processing failed for order {OrderId}. " +
"Error: {ErrorMessage}, PaymentGateway: {Gateway}",
request.OrderId,
ex.Message,
ex.GatewayName);
throw;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Unexpected error processing order {OrderId}. " +
"Customer: {CustomerId}, Amount: {Amount}",
request.OrderId,
request.CustomerId,
request.TotalAmount);
throw new OrderProcessingException("Failed to process order", ex);
}
}
public async Task RetryOrderProcessingAsync(Order order)
{
var stopwatch = Stopwatch.StartNew();
try
{
_logger.LogInformation("Retrying order processing for order {OrderId}", order.Id);
await _orderProcessor.RetryAsync(order);
_logger.LogInformation(
"Order retry completed successfully. OrderId: {OrderId}, Duration: {ElapsedMs}ms",
order.Id,
stopwatch.ElapsedMilliseconds);
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Order retry failed. OrderId: {OrderId}, Attempt: {Attempt}, Duration: {ElapsedMs}ms",
order.Id,
order.RetryCount,
stopwatch.ElapsedMilliseconds);
throw;
}
finally
{
stopwatch.Stop();
}
}
}
Advanced Logging Patterns
public static class LoggerExtensions
{
// Extension methods for specific log scenarios
public static void LogSecurityEvent(this ILogger logger,
string eventType, string userId, string resource, string details)
{
logger.LogWarning(
"Security event: {EventType}. User: {UserId}, Resource: {Resource}, Details: {Details}",
eventType, userId, resource, details);
}
public static void LogPerformanceMetric(this ILogger logger,
string operation, long durationMs, string context = null)
{
var logLevel = durationMs > 1000 ? LogLevel.Warning : LogLevel.Information;
logger.Log(
logLevel,
"Performance metric: {Operation} took {DurationMs}ms. Context: {Context}",
operation, durationMs, context);
}
public static void LogBusinessEvent(this ILogger logger,
string eventType, string entityType, string entityId, object additionalData = null)
{
logger.LogInformation(
"Business event: {EventType}. Entity: {EntityType}, ID: {EntityId}, Data: {@AdditionalData}",
eventType, entityType, entityId, additionalData);
}
// High-performance logging with conditional compilation
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LogDebugIfEnabled(this ILogger logger, string message, params object[] args)
{
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug(message, args);
}
}
}
// Usage
_logger.LogSecurityEvent("LoginFailed", user.Id, "Authentication", "Invalid credentials");
_logger.LogPerformanceMetric("DatabaseQuery", 150, "GetUserOrders");
_logger.LogBusinessEvent("OrderCreated", "Order", order.Id, new { order.TotalAmount, order.Currency });
8. Error Monitoring & Alerting <a name="error-monitoring"></a>
Application Insights Integration
public static class ApplicationInsightsConfig
{
public static void ConfigureApplicationInsights(IServiceCollection services, IConfiguration configuration)
{
services.AddApplicationInsightsTelemetry(options =>
{
options.ConnectionString = configuration["ApplicationInsights:ConnectionString"];
options.EnableAdaptiveSampling = false; // Get all telemetry for errors
options.EnableDependencyTrackingTelemetryModule = true;
options.EnablePerformanceCounterCollectionModule = true;
options.EnableRequestTrackingTelemetryModule = true;
options.EnableExceptionTrackingTelemetryModule = true;
});
// Custom telemetry initializer for additional context
services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();
// Telemetry processor for filtering
services.AddApplicationInsightsTelemetryProcessor<FilterHealthCheckTelemetryProcessor>();
}
}
public class CustomTelemetryInitializer : ITelemetryInitializer
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
var context = _httpContextAccessor.HttpContext;
if (context == null) return;
// Add correlation ID
if (context.Items.TryGetValue("CorrelationId", out var correlationId))
{
telemetry.Context.Operation.Id = correlationId.ToString();
}
// Add user information
if (context.User.Identity.IsAuthenticated)
{
telemetry.Context.User.Id = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
telemetry.Context.User.AuthenticatedUserId = context.User.Identity.Name;
}
// Add custom properties
telemetry.Context.Cloud.RoleName = "OrderProcessingService";
telemetry.Context.Component.Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString();
// Add business context for request telemetry
if (telemetry is RequestTelemetry requestTelemetry)
{
if (context.Request.RouteValues.TryGetValue("controller", out var controller))
{
requestTelemetry.Properties["Controller"] = controller.ToString();
}
if (context.Request.RouteValues.TryGetValue("action", out var action))
{
requestTelemetry.Properties["Action"] = action.ToString();
}
}
}
}
public class FilterHealthCheckTelemetryProcessor : ITelemetryProcessor
{
private readonly ITelemetryProcessor _next;
public FilterHealthCheckTelemetryProcessor(ITelemetryProcessor next)
{
_next = next;
}
public void Process(ITelemetry item)
{
// Filter out health check requests to reduce noise
if (item is RequestTelemetry request &&
request.Url != null &&
request.Url.AbsolutePath.Contains("/health"))
{
return;
}
_next.Process(item);
}
}
Custom Error Monitoring Service
public interface IErrorMonitoringService
{
Task TrackExceptionAsync(Exception exception, IDictionary<string, object> context = null);
Task TrackErrorAsync(string errorCode, string message, IDictionary<string, object> context = null);
Task<bool> ShouldAlertAsync(Exception exception);
}
public class ErrorMonitoringService : IErrorMonitoringService
{
private readonly TelemetryClient _telemetryClient;
private readonly ILogger<ErrorMonitoringService> _logger;
private readonly IConfiguration _configuration;
public ErrorMonitoringService(
TelemetryClient telemetryClient,
ILogger<ErrorMonitoringService> logger,
IConfiguration configuration)
{
_telemetryClient = telemetryClient;
_logger = logger;
_configuration = configuration;
}
public async Task TrackExceptionAsync(Exception exception, IDictionary<string, object> context = null)
{
try
{
var telemetry = new ExceptionTelemetry(exception);
// Add context to telemetry
if (context != null)
{
foreach (var kvp in context)
{
telemetry.Properties[kvp.Key] = kvp.Value?.ToString();
}
}
// Track the exception
_telemetryClient.TrackException(telemetry);
// Flush to ensure telemetry is sent
_telemetryClient.Flush();
// Check if we should send an alert
if (await ShouldAlertAsync(exception))
{
await SendAlertAsync(exception, context);
}
_logger.LogInformation("Exception tracked: {ExceptionType}", exception.GetType().Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to track exception in monitoring service");
}
}
public async Task TrackErrorAsync(string errorCode, string message, IDictionary<string, object> context = null)
{
var telemetry = new TraceTelemetry(message, SeverityLevel.Error);
telemetry.Properties["ErrorCode"] = errorCode;
if (context != null)
{
foreach (var kvp in context)
{
telemetry.Properties[kvp.Key] = kvp.Value?.ToString();
}
}
_telemetryClient.TrackTrace(telemetry);
_telemetryClient.Flush();
_logger.LogWarning("Error tracked: {ErrorCode} - {Message}", errorCode, message);
}
public Task<bool> ShouldAlertAsync(Exception exception)
{
// Don't alert for client errors
if (exception is ValidationException ||
exception is NotFoundException ||
exception is UnauthorizedAccessException)
{
return Task.FromResult(false);
}
// Alert for critical infrastructure errors
if (exception is DatabaseConnectionException ||
exception is ExternalServiceException ||
exception is OutOfMemoryException)
{
return Task.FromResult(true);
}
// Rate limiting: don't alert for the same error repeatedly
return Task.FromResult(true);
}
private async Task SendAlertAsync(Exception exception, IDictionary<string, object> context)
{
try
{
var alertSettings = _configuration.GetSection("ErrorAlerting");
var enabled = alertSettings.GetValue<bool>("Enabled");
if (!enabled) return;
var alertMessage = CreateAlertMessage(exception, context);
// Send alert via configured channels (email, Slack, etc.)
await SendAlertToChannelsAsync(alertMessage);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send error alert");
}
}
private string CreateAlertMessage(Exception exception, IDictionary<string, object> context)
{
var sb = new StringBuilder();
sb.AppendLine($"🚨 APPLICATION ALERT: {exception.GetType().Name}");
sb.AppendLine($"Message: {exception.Message}");
sb.AppendLine($"Time: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
if (context != null && context.Any())
{
sb.AppendLine("Context:");
foreach (var kvp in context)
{
sb.AppendLine($" {kvp.Key}: {kvp.Value}");
}
}
if (exception.StackTrace != null)
{
sb.AppendLine($"Stack Trace: {exception.StackTrace}");
}
return sb.ToString();
}
private async Task SendAlertToChannelsAsync(string alertMessage)
{
var alertChannels = _configuration.GetSection("ErrorAlerting:Channels").Get<string[]>();
foreach (var channel in alertChannels ?? Array.Empty<string>())
{
try
{
switch (channel.ToLowerInvariant())
{
case "email":
await SendEmailAlertAsync(alertMessage);
break;
case "slack":
await SendSlackAlertAsync(alertMessage);
break;
case "teams":
await SendTeamsAlertAsync(alertMessage);
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send alert to channel: {Channel}", channel);
}
}
}
private Task SendEmailAlertAsync(string alertMessage)
{
// Implementation for email alerts
return Task.CompletedTask;
}
private Task SendSlackAlertAsync(string alertMessage)
{
// Implementation for Slack alerts
return Task.CompletedTask;
}
private Task SendTeamsAlertAsync(string alertMessage)
{
// Implementation for Microsoft Teams alerts
return Task.CompletedTask;
}
}
9. API Error Responses & Standards <a name="api-error-responses"></a>
RFC 7807 Problem Details Implementation
public class ProblemDetailsService : IProblemDetailsService
{
private readonly IWebHostEnvironment _environment;
private readonly ILogger<ProblemDetailsService> _logger;
public ProblemDetailsService(IWebHostEnvironment environment, ILogger<ProblemDetailsService> logger)
{
_environment = environment;
_logger = logger;
}
public async ValueTask WriteAsync(ProblemDetailsContext context)
{
var httpContext = context.HttpContext;
var problemDetails = context.ProblemDetails;
// Ensure status code is set
problemDetails.Status ??= httpContext.Response.StatusCode;
// Set instance if not provided
problemDetails.Instance ??= httpContext.Request.Path;
// Add correlation ID
if (httpContext.Items.TryGetValue("CorrelationId", out var correlationId))
{
problemDetails.Extensions["correlationId"] = correlationId;
}
// Add trace ID for debugging
problemDetails.Extensions["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier;
// Include additional details in development
if (_environment.IsDevelopment())
{
AddDevelopmentDetails(problemDetails, context.Exception);
}
// Log the problem
LogProblemDetails(problemDetails, context.Exception, httpContext);
// Write the response
httpContext.Response.StatusCode = problemDetails.Status.Value;
httpContext.Response.ContentType = "application/problem+json";
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
await httpContext.Response.WriteAsJsonAsync(problemDetails, options, context.HttpContext.RequestAborted);
}
private void AddDevelopmentDetails(ProblemDetails problemDetails, Exception exception)
{
if (exception != null)
{
problemDetails.Extensions["exception"] = new
{
type = exception.GetType().FullName,
message = exception.Message,
stackTrace = exception.StackTrace,
innerException = exception.InnerException?.Message
};
}
problemDetails.Extensions["machineName"] = Environment.MachineName;
problemDetails.Extensions["timestamp"] = DateTime.UtcNow.ToString("O");
}
private void LogProblemDetails(ProblemDetails problemDetails, Exception exception, HttpContext httpContext)
{
var logLevel = GetLogLevel(problemDetails.Status.Value, exception);
_logger.Log(
logLevel,
exception,
"Problem details response. Status: {StatusCode}, Type: {ProblemType}, " +
"Title: {Title}, Instance: {Instance}, CorrelationId: {CorrelationId}",
problemDetails.Status,
problemDetails.Type,
problemDetails.Title,
problemDetails.Instance,
problemDetails.Extensions.GetValueOrDefault("correlationId"));
}
private static LogLevel GetLogLevel(int statusCode, Exception exception)
{
if (exception != null) return LogLevel.Error;
return statusCode switch
{
>= 500 => LogLevel.Error,
>= 400 => LogLevel.Warning,
_ => LogLevel.Information
};
}
}
// Custom problem details for specific scenarios
public class ValidationProblemDetails : ProblemDetails
{
public IDictionary<string, string[]> Errors { get; set; } = new Dictionary<string, string[]>();
public ValidationProblemDetails()
{
Title = "Validation Error";
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1";
}
public ValidationProblemDetails(ModelStateDictionary modelState) : this()
{
if (modelState == null)
throw new ArgumentNullException(nameof(modelState));
foreach (var keyModelStatePair in modelState)
{
var key = keyModelStatePair.Key;
var errors = keyModelStatePair.Value.Errors;
if (errors != null && errors.Count > 0)
{
if (errors.Count == 1)
{
var errorMessage = GetErrorMessage(errors[0]);
Errors.Add(key, new[] { errorMessage });
}
else
{
var errorMessages = new string[errors.Count];
for (var i = 0; i < errors.Count; i++)
{
errorMessages[i] = GetErrorMessage(errors[i]);
}
Errors.Add(key, errorMessages);
}
}
}
}
private static string GetErrorMessage(ModelError error)
{
return string.IsNullOrEmpty(error.ErrorMessage)
? "The input was not valid."
: error.ErrorMessage;
}
}
public class BusinessRuleProblemDetails : ProblemDetails
{
public string RuleName { get; set; }
public string BusinessContext { get; set; }
public string SuggestedAction { get; set; }
public BusinessRuleProblemDetails(BusinessRuleException exception)
{
Title = "Business Rule Violation";
Status = StatusCodes.Status422UnprocessableEntity;
Type = "https://tools.ietf.org/html/rfc4918#section-11.2";
Detail = exception.Message;
RuleName = exception.RuleName;
BusinessContext = exception.BusinessContext;
SuggestedAction = GetSuggestedAction(exception.RuleName);
}
private static string GetSuggestedAction(string ruleName)
{
return ruleName switch
{
"InsufficientInventory" => "Reduce the quantity or check back later",
"PriceMismatch" => "Refresh the product and try again",
"ShippingRestriction" => "Choose a different shipping method or address",
_ => "Please review your request and try again"
};
}
}
API-Specific Exception Handling
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
[Consumes("application/json")]
public class ApiControllerBase : ControllerBase
{
protected readonly ILogger Logger;
protected ApiControllerBase(ILogger logger)
{
Logger = logger;
}
protected IActionResult OkOrNotFound<T>(T result, string resourceName = null)
{
if (result == null)
{
var message = resourceName != null
? $"{resourceName} not found"
: "Resource not found";
return NotFound(new ProblemDetails
{
Title = "Resource Not Found",
Status = StatusCodes.Status404NotFound,
Detail = message,
Instance = HttpContext.Request.Path
});
}
return Ok(result);
}
protected IActionResult CreatedAt<T>(string actionName, object routeValues, T value)
{
return CreatedAtAction(actionName, routeValues, value);
}
protected async Task<IActionResult> ExecuteAndHandleAsync<T>(
Func<Task<T>> operation,
string successLogMessage = null)
{
try
{
var result = await operation();
if (!string.IsNullOrEmpty(successLogMessage))
{
Logger.LogInformation(successLogMessage);
}
return Ok(result);
}
catch (ValidationException ex)
{
Logger.LogWarning(ex, "Validation error in API operation");
return BadRequest(new ValidationProblemDetails
{
Title = "Validation Error",
Detail = ex.Message,
Instance = HttpContext.Request.Path,
Errors = ex.Errors
});
}
catch (NotFoundException ex)
{
Logger.LogWarning(ex, "Resource not found in API operation");
return NotFound(new ProblemDetails
{
Title = "Resource Not Found",
Status = StatusCodes.Status404NotFound,
Detail = ex.Message,
Instance = HttpContext.Request.Path
});
}
catch (BusinessRuleException ex)
{
Logger.LogWarning(ex, "Business rule violation in API operation");
return UnprocessableEntity(new BusinessRuleProblemDetails(ex));
}
catch (UnauthorizedAccessException ex)
{
Logger.LogWarning(ex, "Unauthorized access in API operation");
return Unauthorized(new ProblemDetails
{
Title = "Unauthorized",
Status = StatusCodes.Status401Unauthorized,
Detail = "Authentication is required to access this resource",
Instance = HttpContext.Request.Path
});
}
catch (Exception ex)
{
Logger.LogError(ex, "Unexpected error in API operation");
return StatusCode(500, new ProblemDetails
{
Title = "Internal Server Error",
Status = StatusCodes.Status500InternalServerError,
Detail = "An unexpected error occurred",
Instance = HttpContext.Request.Path
});
}
}
}
10. Validation Error Handling <a name="validation-errors"></a>
Comprehensive Validation Framework
public class CustomValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status400BadRequest,
Instance = context.HttpContext.Request.Path,
Extensions = { ["traceId"] = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier }
};
context.Result = new BadRequestObjectResult(problemDetails);
}
base.OnActionExecuting(context);
}
}
// Custom validation exception
public class ValidationException : Exception
{
public IDictionary<string, string[]> Errors { get; }
public ValidationException() : base("One or more validation errors occurred.")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IDictionary<string, string[]> errors) : this()
{
Errors = errors;
}
public ValidationException(string message) : base(message)
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(string message, IDictionary<string, string[]> errors) : base(message)
{
Errors = errors;
}
public ValidationException(string message, Exception innerException) : base(message, innerException)
{
Errors = new Dictionary<string, string[]>();
}
}
// FluentValidation integration
public class FluentValidationFilter : IEndpointFilter
{
private readonly IServiceProvider _serviceProvider;
public FluentValidationFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
foreach (var argument in context.Arguments)
{
if (argument == null) continue;
var argumentType = argument.GetType();
var validatorType = typeof(IValidator<>).MakeGenericType(argumentType);
var validator = _serviceProvider.GetService(validatorType) as IValidator;
if (validator != null)
{
var validationContext = new ValidationContext<object>(argument);
var validationResult = await validator.ValidateAsync(validationContext);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.GroupBy(x => x.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(x => x.ErrorMessage).ToArray());
throw new ValidationException("Validation failed", errors);
}
}
}
return await next(context);
}
}
// Custom validators
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty().WithMessage("Customer ID is required")
.MaximumLength(50).WithMessage("Customer ID cannot exceed 50 characters");
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Order must contain at least one item")
.Must(items => items.Sum(i => i.Quantity) > 0).WithMessage("Total quantity must be greater than 0");
RuleForEach(x => x.Items).SetValidator(new OrderItemValidator());
RuleFor(x => x.ShippingAddress)
.NotNull().WithMessage("Shipping address is required")
.SetValidator(new AddressValidator());
RuleFor(x => x.TotalAmount)
.GreaterThan(0).WithMessage("Total amount must be positive")
.ScalePrecision(2, 18).WithMessage("Total amount must have valid precision");
RuleFor(x => x.Currency)
.NotEmpty().WithMessage("Currency is required")
.Length(3).WithMessage("Currency must be a 3-letter code")
.Must(BeValidCurrency).WithMessage("Invalid currency code");
}
private bool BeValidCurrency(string currency)
{
return new[] { "USD", "EUR", "GBP", "JPY" }.Contains(currency.ToUpperInvariant());
}
}
public class OrderItemValidator : AbstractValidator<OrderItemRequest>
{
public OrderItemValidator()
{
RuleFor(x => x.ProductId)
.NotEmpty().WithMessage("Product ID is required");
RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("Quantity must be positive")
.LessThanOrEqualTo(1000).WithMessage("Quantity cannot exceed 1000");
RuleFor(x => x.UnitPrice)
.GreaterThanOrEqualTo(0).WithMessage("Unit price cannot be negative")
.ScalePrecision(2, 18).WithMessage("Unit price must have valid precision");
}
}
11. Database & ORM Error Handling <a name="database-errors"></a>
Entity Framework Core Error Handling
public class RepositoryBase<T> where T : class
{
private readonly ApplicationDbContext _context;
private readonly ILogger<RepositoryBase<T>> _logger;
public RepositoryBase(ApplicationDbContext context, ILogger<RepositoryBase<T>> logger)
{
_context = context;
_logger = logger;
}
public async Task<T> GetByIdAsync(int id)
{
try
{
return await _context.Set<T>().FindAsync(id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving {EntityType} with ID {Id}", typeof(T).Name, id);
throw new DataAccessException($"Error retrieving {typeof(T).Name}", ex);
}
}
public async Task AddAsync(T entity)
{
try
{
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
}
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
{
_logger.LogWarning(ex, "Unique constraint violation while adding {EntityType}", typeof(T).Name);
throw new UniqueConstraintException($"Duplicate {typeof(T).Name} found", ex);
}
catch (DbUpdateException ex) when (IsForeignKeyViolation(ex))
{
_logger.LogWarning(ex, "Foreign key violation while adding {EntityType}", typeof(T).Name);
throw new ForeignKeyViolationException("Related entity not found", ex);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database update error while adding {EntityType}", typeof(T).Name);
throw new DataAccessException($"Error adding {typeof(T).Name}", ex);
}
}
public async Task UpdateAsync(T entity)
{
try
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
_logger.LogWarning(ex, "Concurrency conflict while updating {EntityType}", typeof(T).Name);
throw new ConcurrencyException("Data was modified by another user", ex);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database update error while updating {EntityType}", typeof(T).Name);
throw new DataAccessException($"Error updating {typeof(T).Name}", ex);
}
}
public async Task ExecuteInTransactionAsync(Func<Task> operation)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
await operation();
await transaction.CommitAsync();
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Transaction failed for {EntityType} operation", typeof(T).Name);
throw;
}
}
private static bool IsUniqueConstraintViolation(DbUpdateException ex)
{
return ex.InnerException is SqlException sqlEx &&
(sqlEx.Number == 2627 || sqlEx.Number == 2601); // Unique constraint violation
}
private static bool IsForeignKeyViolation(DbUpdateException ex)
{
return ex.InnerException is SqlException sqlEx && sqlEx.Number == 547; // Foreign key violation
}
}
// Custom database exceptions
public class DataAccessException : Exception
{
public DataAccessException(string message) : base(message) { }
public DataAccessException(string message, Exception innerException) : base(message, innerException) { }
}
public class UniqueConstraintException : DataAccessException
{
public UniqueConstraintException(string message) : base(message) { }
public UniqueConstraintException(string message, Exception innerException) : base(message, innerException) { }
}
public class ForeignKeyViolationException : DataAccessException
{
public ForeignKeyViolationException(string message) : base(message) { }
public ForeignKeyViolationException(string message, Exception innerException) : base(message, innerException) { }
}
public class ConcurrencyException : DataAccessException
{
public ConcurrencyException(string message) : base(message) { }
public ConcurrencyException(string message, Exception innerException) : base(message, innerException) { }
}
Resilient Database Operations
public class ResilientDatabaseExecutor
{
private readonly ILogger<ResilientDatabaseExecutor> _logger;
private readonly IPolicy[] _policies;
public ResilientDatabaseExecutor(ILogger<ResilientDatabaseExecutor> logger)
{
_logger = logger;
_policies = CreateResiliencePolicies();
}
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation, string operationName)
{
var policyWrap = Policy.WrapAsync(_policies);
return await policyWrap.ExecuteAsync(async () =>
{
using var activity = _logger.BeginScope(new Dictionary<string, object>
{
["DatabaseOperation"] = operationName,
["RetryCount"] = 0 // This would be updated by retry policy
});
_logger.LogInformation("Executing database operation: {Operation}", operationName);
return await operation();
});
}
public async Task ExecuteAsync(Func<Task> operation, string operationName)
{
var policyWrap = Policy.WrapAsync(_policies);
await policyWrap.ExecuteAsync(async () =>
{
using var activity = _logger.BeginScope(new Dictionary<string, object>
{
["DatabaseOperation"] = operationName
});
_logger.LogInformation("Executing database operation: {Operation}", operationName);
await operation();
});
}
private IAsyncPolicy[] CreateResiliencePolicies()
{
// Retry policy for transient errors
var retryPolicy = Policy
.Handle<SqlException>(IsTransientError)
.Or<TimeoutException>()
.OrInner<SocketException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
_logger.LogWarning(
"Retry {RetryCount} for {Operation} after {Delay}ms. Error: {Error}",
retryCount,
context.OperationKey,
timespan.TotalMilliseconds,
outcome.Exception?.Message);
});
// Circuit breaker policy
var circuitBreakerPolicy = Policy
.Handle<SqlException>(IsTransientError)
.Or<TimeoutException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromMinutes(1),
onBreak: (exception, timespan) =>
{
_logger.LogError(
"Circuit breaker opened for {Timespan} due to: {Error}",
timespan,
exception.Message);
},
onReset: () =>
{
_logger.LogInformation("Circuit breaker reset");
},
onHalfOpen: () =>
{
_logger.LogInformation("Circuit breaker half-open");
});
// Timeout policy
var timeoutPolicy = Policy
.TimeoutAsync(TimeSpan.FromSeconds(30), onTimeoutAsync: (context, timespan, task) =>
{
_logger.LogWarning("Operation {Operation} timed out after {Timeout}s",
context.OperationKey, timespan.TotalSeconds);
return Task.CompletedTask;
});
return new IAsyncPolicy[] { retryPolicy, circuitBreakerPolicy, timeoutPolicy };
}
private static bool IsTransientError(SqlException sqlException)
{
// SQL Server transient error codes
var transientErrorNumbers = new[] { -2, 20, 64, 233, 10053, 10054, 10060, 40143, 40197, 40501, 40613 };
return transientErrorNumbers.Contains(sqlException.Number);
}
}

Join the conversation! Your thoughts help the community grow.