Как бы вы реализовали cross-cutting concern (например, логирование, валидация, транзакции)?csharp-118

Что такое Cross-Cutting Concerns?

Cross-Cutting Concerns (сквозная функциональность) - это аспекты системы, которые затрагивают несколько компонентов:

  • Логирование
  • Валидация
  • Транзакции
  • Кэширование
  • Авторизация
  • Обработка ошибок

Основные подходы реализации

1. Декораторы

public interface IOrderService
{
    void PlaceOrder(Order order);
}

public class OrderService : IOrderService { /* реализация */ }

public class LoggingOrderService : IOrderService
{
    private readonly IOrderService _decorated;
    private readonly ILogger _logger;

    public LoggingOrderService(IOrderService decorated, ILogger logger)
    {
        _decorated = decorated;
        _logger = logger;
    }

    public void PlaceOrder(Order order)
    {
        _logger.Log("Начало PlaceOrder");
        try
        {
            _decorated.PlaceOrder(order);
            _logger.Log("Успешно PlaceOrder");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка PlaceOrder");
            throw;
        }
    }
}

2. Атрибуты

public class LogActionAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // Логирование перед выполнением
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // Логирование после выполнения
    }
}

[LogAction]
public IActionResult GetOrders() { /* ... */ }

3. Интерцепторы

public class LoggingInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"Вызов {invocation.Method.Name}");
        invocation.Proceed();
        Console.WriteLine($"Завершение {invocation.Method.Name}");
    }
}

// Использование с Castle DynamicProxy
var generator = new ProxyGenerator();
var service = generator.CreateInterfaceProxyWithTarget<IOrderService>(
    new OrderService(),
    new LoggingInterceptor());

4. Middleware

public class TransactionMiddleware
{
    private readonly RequestDelegate _next;

    public TransactionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, DbContext db)
    {
        await using var transaction = await db.Database.BeginTransactionAsync();
        try
        {
            await _next(context);
            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

5. Aspect-Oriented Programming Frameworks

Пример с PostSharp:

[Serializable]
public class LogAttribute : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        // Логирование входа
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        // Логирование выхода
    }
}

[Log]
public class OrderService { /* ... */ }

Сравнение подходов

Подход Плюсы Минусы
Декораторы Простота, явность Ручное создание оберток
Атрибуты Простота использования Ограниченная гибкость
Интерцепторы Мощность, гибкость Сложность отладки
Middleware Централизованное управление Только для HTTP конвейера
AOP фреймворки Максимальная автоматизация Дополнительные зависимости

Практические примеры

Комплексный пример с валидацией и транзакциями

public class ValidateAndTransactionDecorator<T> : ICommandHandler<T>
{
    private readonly ICommandHandler<T> _inner;
    private readonly IValidator<T> _validator;
    private readonly DbContext _db;

    public ValidateAndTransactionDecorator(
        ICommandHandler<T> inner,
        IValidator<T> validator,
        DbContext db)
    {
        _inner = inner;
        _validator = validator;
        _db = db;
    }

    public async Task Handle(T command)
    {
        var result = await _validator.ValidateAsync(command);
        if (!result.IsValid) throw new ValidationException(result.Errors);

        await using var transaction = await _db.Database.BeginTransactionAsync();
        try
        {
            await _inner.Handle(command);
            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

Резюмируем

  1. Выбор подхода зависит от:

    • Масштаба проекта
    • Требований к гибкости
    • Командных предпочтений
  2. Лучшие практики:

    • Разделяйте concerns на отдельные компоненты
    • Избегайте "волшебных строк" в логах и ошибках
    • Документируйте поведение cross-cutting компонентов
  3. Оптимальный путь:

    • Для веб-приложений: Middleware + Filters
    • Для сервисного слоя: Декораторы + Интерцепторы
    • Для сложных систем: AOP фреймворки

Главное правило: "Cross-cutting код должен быть максимально предсказуемым и ненавязчивым"