Ana içeriğe geç

Application Layer

Application katmanı, use case’leri orkestre eder ve domain ile altyapı arasında köprü kurar; yanlış kullanımlar iş mantığının yanlış katmana yerleşmesine neden olur.


1. Application Katmanında İş Mantığı Yazmak

Yanlış Kullanım: Domain kurallarını application servisinde uygulamak.

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    public async Task<int> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = new Order();
        decimal total = 0;

        foreach (var item in request.Items)
        {
            if (item.Quantity <= 0) throw new ValidationException("Miktar pozitif olmalı");
            total += item.Price * item.Quantity;
        }

        if (total > 50000) throw new BusinessException("Sipariş limiti aşıldı");

        order.TotalPrice = total;
        order.Status = OrderStatus.Pending;
        // ...
    }
}

İdeal Kullanım: İş mantığını domain’de tutun, application sadece orkestre etsin.

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = Order.Create(request.CustomerId);

        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Price, item.Quantity); // Domain kuralları burada uygulanır
        }

        await _repository.AddAsync(order);
        await _unitOfWork.SaveChangesAsync(ct);
        return order.Id;
    }
}

2. DTO ve Domain Model Karışımı

Yanlış Kullanım: Domain entity’sini doğrudan API response olarak döndürmek.

[HttpGet("{id}")]
public async Task<Order> GetOrder(int id)
{
    return await _context.Orders
        .Include(o => o.Items)
        .Include(o => o.Customer)
        .FirstOrDefaultAsync(o => o.Id == id);
    // Circular reference, gereksiz veri, domain detayları sızar
}

İdeal Kullanım: DTO ile domain model’i ayırın.

public record OrderDto(int Id, string CustomerName, decimal TotalPrice, List<OrderItemDto> Items);
public record OrderItemDto(string ProductName, int Quantity, decimal Price);

public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)
    {
        var order = await _repository.GetByIdAsync(request.OrderId);
        if (order == null) throw new NotFoundException("Sipariş bulunamadı.");

        return new OrderDto(
            order.Id,
            order.Customer.Name,
            order.TotalPrice,
            order.Items.Select(i => new OrderItemDto(i.ProductName, i.Quantity, i.Price)).ToList()
        );
    }
}

3. Cross-Cutting Concern’leri Handler İçine Gömmek

Yanlış Kullanım: Her handler’da loglama, validasyon, transaction tekrarlamak.

public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
    {
        _logger.LogInformation("CreateProduct başlatıldı");

        var validationResult = await _validator.ValidateAsync(request);
        if (!validationResult.IsValid) throw new ValidationException(validationResult.Errors);

        using var transaction = await _context.Database.BeginTransactionAsync();
        // ... iş mantığı
        await transaction.CommitAsync();

        _logger.LogInformation("CreateProduct tamamlandı");
        return product.Id;
    }
}

İdeal Kullanım: Pipeline Behavior ile cross-cutting concern’leri merkezileştirin.

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 failures = _validators
            .Select(v => v.Validate(request))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count > 0)
            throw new ValidationException(failures);

        return await next();
    }
}

// Handler temiz kalır
public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    public async Task<int> Handle(CreateProductCommand request, CancellationToken ct)
    {
        var product = new Product(request.Name, request.Price);
        await _repository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync(ct);
        return product.Id;
    }
}

4. Application Katmanında Altyapı Bağımlılığı

Yanlış Kullanım: Application katmanında concrete altyapı sınıflarını kullanmak.

public class SendNotificationHandler : IRequestHandler<SendNotificationCommand>
{
    private readonly SmtpClient _smtp;
    private readonly TwilioClient _twilio;

    public async Task Handle(SendNotificationCommand request, CancellationToken ct)
    {
        await _smtp.SendMailAsync(new MailMessage(/* ... */));
        await _twilio.SendSmsAsync(/* ... */);
    }
}

İdeal Kullanım: Application katmanında interface tanımlayın, implementasyonu Infrastructure’a bırakın.

// Application Layer
public interface INotificationService
{
    Task SendEmailAsync(string to, string subject, string body);
    Task SendSmsAsync(string to, string message);
}

public class SendNotificationHandler : IRequestHandler<SendNotificationCommand>
{
    private readonly INotificationService _notificationService;

    public async Task Handle(SendNotificationCommand request, CancellationToken ct)
    {
        await _notificationService.SendEmailAsync(request.Email, request.Subject, request.Body);
    }
}

// Infrastructure Layer
public class NotificationService : INotificationService
{
    public async Task SendEmailAsync(string to, string subject, string body) { /* SMTP */ }
    public async Task SendSmsAsync(string to, string message) { /* Twilio */ }
}

5. Mapping Mantığını Dağıtmak

Yanlış Kullanım: Her yerde manuel mapping yapmak.

public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
{
    public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken ct)
    {
        var product = await _repository.GetByIdAsync(request.Id);
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            CategoryName = product.Category.Name,
            // 20 satır daha mapping...
        };
    }
}

İdeal Kullanım: Mapping profillerini merkezi olarak tanımlayın.

public class ProductMappingProfile : Profile
{
    public ProductMappingProfile()
    {
        CreateMap<Product, ProductDto>()
            .ForMember(d => d.CategoryName, opt => opt.MapFrom(s => s.Category.Name));
    }
}

public class GetProductHandler : IRequestHandler<GetProductQuery, ProductDto>
{
    private readonly IMapper _mapper;
    private readonly IProductRepository _repository;

    public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken ct)
    {
        var product = await _repository.GetByIdAsync(request.Id);
        return _mapper.Map<ProductDto>(product);
    }
}