← Назад к вопросам

Как обрабатываешь ошибки?

2.3 Middle🔥 171 комментариев
#Основы C# и .NET

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стратегия обработки ошибок в 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 клиенту.
  • Контекстуальность: Все исключения содержат максимально полезную информацию для диагностики.
  • Восстановление: Система должна стараться продолжить работу даже при частичных сбоях.
  • Мониторинг: Ошибки должны не только обрабатываться, но и анализироваться для улучшения системы.

Эта многоуровневая стратегия превращает обработку ошибок из механической задачи в важную часть архитектуры, повышая устойчивость, удобство поддержки и качество пользовательского опыта.