Ana içeriğe geç

Cross-Cutting Concerns

Cross-cutting concern’ler loglama, validasyon ve exception handling gibi tüm katmanları kesen sorumluluklardır; yanlış yönetim kod tekrarına ve bakım zorluğuna yol açar.


1. Her Katmanda Loglama Tekrarı

Yanlış Kullanım: Her servis metodunda manuel loglama yapmak.

public class OrderService
{
    public async Task<Order> CreateAsync(CreateOrderDto dto)
    {
        _logger.LogInformation("CreateAsync başladı: {@Dto}", dto);
        try
        {
            var order = new Order(dto.CustomerId);
            await _repository.AddAsync(order);
            _logger.LogInformation("Sipariş oluşturuldu: {OrderId}", order.Id);
            return order;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "CreateAsync hatası: {@Dto}", dto);
            throw;
        }
    }
}

İdeal Kullanım: MediatR Pipeline Behavior ile loglama merkezileştirin.

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) => _logger = logger;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation("Handling {Request}: {@RequestData}", requestName, request);

        var response = await next();

        _logger.LogInformation("Handled {Request}", requestName);
        return response;
    }
}

2. Validasyon Mantığını Dağıtmak

Yanlış Kullanım: Validasyonu birden fazla yerde tekrarlamak.

// Controller'da
if (string.IsNullOrEmpty(dto.Name)) return BadRequest();

// Serviste
if (string.IsNullOrEmpty(dto.Name)) throw new ValidationException();

// Repository'de
if (string.IsNullOrEmpty(entity.Name)) throw new ArgumentException();

İdeal Kullanım: Validasyonu tek bir noktada, pipeline’da yapın.

public class CreateProductValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
        => _validators = validators;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count > 0) throw new ValidationException(failures);
        return await next();
    }
}

3. Transaction Yönetimini Her Yerde Tekrarlamak

Yanlış Kullanım: Her handler’da transaction açıp kapatmak.

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    public async Task<int> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        using var transaction = await _context.Database.BeginTransactionAsync(ct);
        try
        {
            // ... iş mantığı
            await _context.SaveChangesAsync(ct);
            await transaction.CommitAsync(ct);
            return order.Id;
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
}

İdeal Kullanım: Transaction behavior ile merkezileştirin.

public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly AppDbContext _context;

    public TransactionBehavior(AppDbContext context) => _context = context;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        var strategy = _context.Database.CreateExecutionStrategy();

        return await strategy.ExecuteAsync(async () =>
        {
            await using var transaction = await _context.Database.BeginTransactionAsync(ct);
            var response = await next();
            await _context.SaveChangesAsync(ct);
            await transaction.CommitAsync(ct);
            return response;
        });
    }
}

4. Performance Monitoring’i Manuel Yapmak

Yanlış Kullanım: Her metodda Stopwatch kullanmak.

public async Task<Order> GetOrderAsync(int id)
{
    var sw = Stopwatch.StartNew();
    var order = await _repository.GetByIdAsync(id);
    sw.Stop();
    _logger.LogInformation("GetOrderAsync süre: {Elapsed}ms", sw.ElapsedMilliseconds);
    return order;
}

İdeal Kullanım: Performance behavior ile süre ölçümünü merkezileştirin.

public class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<PerformanceBehavior<TRequest, TResponse>> _logger;
    private readonly Stopwatch _timer = new();

    public PerformanceBehavior(ILogger<PerformanceBehavior<TRequest, TResponse>> logger) => _logger = logger;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        _timer.Start();
        var response = await next();
        _timer.Stop();

        if (_timer.ElapsedMilliseconds > 500)
        {
            _logger.LogWarning("Yavaş sorgu: {Request} ({Elapsed}ms)",
                typeof(TRequest).Name, _timer.ElapsedMilliseconds);
        }

        return response;
    }
}

5. Caching’i Servislere Gömmek

Yanlış Kullanım: Her serviste cache mantığını tekrarlamak.

public class ProductService
{
    public async Task<Product> GetByIdAsync(int id)
    {
        var cacheKey = $"product:{id}";
        var cached = await _cache.GetStringAsync(cacheKey);
        if (cached != null) return JsonSerializer.Deserialize<Product>(cached);

        var product = await _repository.GetByIdAsync(id);
        await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
        return product;
    }
}

İdeal Kullanım: Caching behavior veya decorator ile merkezileştirin.

public interface ICacheableQuery
{
    string CacheKey { get; }
    TimeSpan CacheDuration { get; }
}

public record GetProductQuery(int Id) : IRequest<ProductDto>, ICacheableQuery
{
    public string CacheKey => $"product:{Id}";
    public TimeSpan CacheDuration => TimeSpan.FromMinutes(5);
}

public class CachingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IDistributedCache _cache;

    public CachingBehavior(IDistributedCache cache) => _cache = cache;

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
    {
        if (request is not ICacheableQuery cacheable) return await next();

        var cached = await _cache.GetStringAsync(cacheable.CacheKey, ct);
        if (cached != null) return JsonSerializer.Deserialize<TResponse>(cached);

        var response = await next();
        await _cache.SetStringAsync(cacheable.CacheKey,
            JsonSerializer.Serialize(response),
            new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheable.CacheDuration }, ct);

        return response;
    }
}