Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегия обработки ошибок в C# Backend
Обработка ошибок в C# Backend — это комплексная стратегия, охватывающая все уровни приложения от бизнес-логики до инфраструктуры. Я строю надежную систему на нескольких ключевых принципах.
Основные принципы и подходы
1. Иерархия исключений и их типизация Я всегда создаю четкую иерархию пользовательских исключений, отражающую домен приложения. Это позволяет точно классифицировать ошибки.
// Базовые исключения домена
public abstract class DomainException : Exception
{
public DomainException(string message) : base(message) {}
public DomainException(string message, Exception inner) : base(message, inner) {}
}
// Конкретные исключения бизнес-логики
public class OrderNotFoundException : DomainException
{
public OrderNotFoundException(int orderId)
: base($"Order with ID {orderId} was not found") {}
}
public class InsufficientFundsException : DomainException
{
public decimal RequiredAmount { get; }
public InsufficientFundsException(decimal requiredAmount)
: base($"Insufficient funds. Required: {requiredAmount}")
{
RequiredAmount = requiredAmount;
}
}
2. Централизованная обработка в middleware (ASP.NET Core) Для веб-API я использую глобальный обработчик исключений через middleware. Это обеспечивает единый формат ответов и логирование.
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (DomainException ex)
{
// Бизнес-ошибки: детальный ответ клиенту
_logger.LogWarning(ex, "Business rule violation");
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new
{
error = ex.Message,
type = ex.GetType().Name,
details = ex.Data // дополнительные данные
});
}
catch (ValidationException ex)
{
// Ошибки валидации
context.Response.StatusCode = 422;
await context.Response.WriteAsJsonAsync(new
{
error = "Validation failed",
errors = ex.Errors.Select(e => new { field = e.PropertyName, message = e.ErrorMessage })
});
}
catch (Exception ex)
{
// Системные/неожиданные ошибки: минимум информации клиенту
_logger.LogError(ex, "Unhandled system error");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
error = "Internal server error",
referenceId = Guid.NewGuid() // ID для отслеживания в логах
});
}
}
}
Практические техники в коде
3. Try-Catch с конкретными исключениями и восстановлением Я избегаю пустых catch блоков и всегда стараюсь восстановить состояние или выполнить альтернативную логику.
public async Task ProcessPaymentAsync(PaymentRequest request)
{
try
{
await _paymentService.Process(request);
}
catch (PaymentGatewayTimeoutException ex)
{
// Специфичная обработка: повтор с задержкой
_logger.LogWarning(ex, "Payment gateway timeout");
await Task.Delay(1000);
await RetryPaymentWithFallbackGateway(request);
}
catch (PaymentDeclinedException ex)
{
// Логирование для анализа и четкий ответ
_logger.LogInformation(ex, $"Payment declined: {ex.ReasonCode}");
throw new InsufficientFundsException(request.Amount); // Преобразование в доменное исключение
}
}
4. Использование Result-объектов для операций без исключений Для некоторых сценариев (например, валидация) я применяю подход с возвратом результата вместо исключений.
public class ValidationResult<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public List<string> Errors { get; }
public static ValidationResult<T> Success(T value) => new(true, value);
public static ValidationResult<T> Failure(List<string> errors) => new(false, default, errors);
}
public ValidationResult<Order> ValidateOrderCreation(OrderDto dto)
{
var errors = new List<string>();
if (dto.Total <= 0) errors.Add("Total must be positive");
if (string.IsNullOrEmpty(dto.CustomerEmail)) errors.Add("Customer email is required");
return errors.Any()
? ValidationResult<Order>.Failure(errors)
: ValidationResult<Order>.Success(new Order(dto));
}
Инфраструктурные компоненты
5. Логирование и мониторинг
- Использование structured logging (Serilog, NLog) с контекстом (UserId, RequestId)
- Интеграция с системами мониторинга (Application Insights, Prometheus) для отслеживания частоты ошибок
- Настройка Health Checks для раннего обнаружения проблем инфраструктуры
6. Resilience patterns через Polly Для внешних вызовов (API, базы данных) я применяю библиотеку Polly для реализации:
- Retry policies с экспоненциальной задержкой
- Circuit breaker для защиты от повторяющихся сбоев
- Timeout policies для предотвращения hanging requests
public class ResilientHttpClient
{
private readonly IAsyncPolicy<HttpResponseMessage> _policy;
public ResilientHttpClient()
{
_policy = Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (result, timeSpan, attempt, context) =>
{
// Логирование каждой попытки
LogRetryAttempt(result.Exception, attempt, timeSpan);
});
}
public async Task<HttpResponseMessage> GetWithResilienceAsync(string url)
{
return await _policy.ExecuteAsync(() => _httpClient.GetAsync(url));
}
}
Ключевые выводы
- Разделение ответственности: Бизнес-ошибки → клиенту с деталями, системные ошибки → в лог с минимальным exposure клиенту.
- Контекстуальность: Все исключения содержат максимально полезную информацию для диагностики.
- Восстановление: Система должна стараться продолжить работу даже при частичных сбоях.
- Мониторинг: Ошибки должны не только обрабатываться, но и анализироваться для улучшения системы.
Эта многоуровневая стратегия превращает обработку ошибок из механической задачи в важную часть архитектуры, повышая устойчивость, удобство поддержки и качество пользовательского опыта.