Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Onion Architecture’da CQRS + MediatR Pattern + AutoMapper + Fluent Validation

Merhaba,

Önceki yazılarımızdan Nedir Bu Onion Architecture? Tam Teferruatlı İnceleyelim başlıklı makalemizde incelediğimiz Onion Architecture üzerinde yaşanabilecek bir takım problemleri ele alacak ve bunlara CQRS Pattern ile çözüm getirmeye çalışacağız.

Problemleri Tanıyalım

İlgili makaleye göz atarsanız eğer ‘Presentation’ katmanındaki API uygulamasından dış dünyanın taleplerine karşılık üretilen datalar ‘Domain’ katmanındaki entity’ler türünde sunulmaktadır. Bu tarz bir sunum biz geliştiriciler tarafından pek tercih edilebilir bir durum değildir. Nihayetinde ‘Application’ katmanında DTO nesnelerimiz mevcuttur ve controller’lar bu DTO’ları aşıp direkt ‘Domain’de ki entity’lere ulaşıyorsa eğer burada tasarımsal bir sakınca var demektir. Böyle bir durumda controller, istek neticesinde verileri entity türünde elde etmekte ve bu elde edilen verileri client’a sunabilmek için DTO dönüşümünden(conversion) sorumlu olmaktadır. Halbuki bu conversion işleminden sorumlu olan controller değil, ‘Application’ katmanıdır. Veri sorgulanırken ‘Application’ katmanından öyle ya da böyle DTO türünde gelmeli ve controller herhangi bir dönüşümle ya da tür ile ilgili işlemlerle ilgilenmeksizin gelen datayı dış dünyaya göndermelidir.

Çözüm

Controller’lar da ki conversion sorumluluğunun arındırılıp ‘Application’ katmanına verilebilmesi ve böylece ‘Presentation’ katmanı açısından direkt olarak talep edilen datanın entity ve DTO türleriyle, ne şekilde hangi şartlarda dönüşümün ve eşleştirmenin gerçekleştirildiğiyle ilgilenmeksizin ‘Application’dan talep edilmesi ve mümkün mertebe Action metotlar da gerçekleştirilen operasyonların sadeleştirilebilmesi için MediatR kütüphanesi eşliğinde CQRS pattern’i kullanacağız.

Pratik Uygulama

Onion Architecture'da CQRS + MediatR PatternÖncelikle ‘Application’ katmanına MediatR ve MediatR.Extensions.Microsoft .DependencyInjection isimli kütüphaneleri kuralım. Ardından ilgili katmanda ‘Features’ isimli klasör içerisinde ‘Commands’ ve ‘Queries’ isimli iki klasör oluşturalım ve CQRS pattern yapılanmasını burada kuralım.

Esasında buradaki tüm ‘..Request’ ve ‘..Response’ sınıfları sistemdeki DTO sınıflarına karşılık gelmektedir. Amma velakin bizler CQRS pattern’ı daha düzenli oluşturabilmek için sistemi bu şekilde inşa etmeyi tercih edeceğiz. Sizlere de tavsiyem bu yöndedir. Aksi taktirde sayıları arttıkça hangi DTO hangi command ya da query’nin olduğu haddinden fazla kompleksleşecektir.

Burada yapılanmanın kaynağına dair herhangi bir örnek göstermenin gereği yok kanaatindeyim. Nihayetinde yukarıda da adresini referans ettiğim ve önceden klavyeye almış olduğum CQRS Pattern makalemizde bu yapılanmanın ne olduğunu ve hangi mantıkla inşa edildiğini tam teferruatlı ele almış bulunmaktayız.

AutoMapper Konfigürasyonu

Tabi CQRS pattern’ı kullanırken bir yandan da Handle sınıflarındaki conversion/mapping operasyonlarının AutoMapper kütüphanesiyle ne şekilde daha pratik hale getirildiğini gözlemleyebilmek için herhangi bir handler sınıfının içeriğini aşağıya alalım.

    public class GetAllProductQueryHandler : IRequestHandler<GetAllProductQueryRequest, List<GetAllProductQueryResponse>>
    {
        readonly IProductRepository _productRepository;
        IMapper _mapper;
        public GetAllProductQueryHandler(IProductRepository productRepository, IMapper mapper)
        {
            _productRepository = productRepository;
            _mapper = mapper;
        }
        public async Task<List<GetAllProductQueryResponse>> Handle(GetAllProductQueryRequest request, CancellationToken cancellationToken)
        {
            var products = await _productRepository.GetAsync();
            return _mapper.Map<List<GetAllProductQueryResponse>>(products);
        }
    }

Görüldüğü üzere handler sınıflarında repository üzerinden entity türünde elde edilen datalar response nesnelerine ‘IMapper’ üzerinden hızlıca dönüştürülmektedir. Bunun için ‘Application’ katmanındaki ‘Mapping’ klasörü içerisinde;

    public class GeneralMapping : Profile
    {
        public GeneralMapping()
        {
            CreateMap<Product, GetAllProductQueryResponse>()
                .ReverseMap();
            CreateMap<Product, GetByIdProductQueryResponse>()
                .ReverseMap();
            CreateMap<Product, GetWhereProductQueryResponse>()
                .ReverseMap();
        }
    }

içeriğine sahip bir ‘Profile’ sınıfının tanımlanması yeterli olacaktır.

Tüm bu işlemlerden sonra ‘Application’ katmanının ‘ServiceRegistration’ sınıfında aşağıdaki konfigürasyonların yapılması gerekmektedir;

    static public class ServiceRegistration
    {
        public static void AddApplicationServices(this IServiceCollection serviceCollection)
        {
            serviceCollection.AddMediatR(Assembly.GetExecutingAssembly());
            serviceCollection.AddAutoMapper(Assembly.GetExecutingAssembly());
        }
    }

Geriye ‘Presentation’ katmanındaki Web API uygulamasında bu servisleri kullanan aşağıdaki controller’ın tasarlanması kalmaktadır;

    [Route("api/[controller]")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        IMediator _mediator;
        public ProductsController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        public async Task<List<GetAllProductQueryResponse>> Get()
        {
            return await _mediator.Send(new GetAllProductQueryRequest());
        }

        [HttpGet("{Id}")]
        public async Task<GetByIdProductQueryResponse> Get([FromQuery] GetByIdProductQueryRequest request)
        {
            return await _mediator.Send(request);
        }

        [HttpGet("search/{Name}")]
        public async Task<List<GetWhereProductQueryResponse>> Get([FromQuery] GetWhereProductQueryRequest request)
        {
            return await _mediator.Send(request);
        }

        [HttpPost]
        public async Task<CreateProductCommandResponse> Post([FromBody] CreateProductCommandRequest request)
        {
            return await _mediator.Send(request);
        }
    }

İşte bu kadar… Görüldüğü üzere bu tarz bir tasarım sayesinde controller’lar client’a iletilecek verinin türüyle yahut conversion işlemleriyle ilgilenmemekte, direkt olarak iş kuralını uygulayıp geriye sonuç dönmektedirler.

Fluent Validation Konfigürasyonu

Handle operasyonlarında client’lardan gelecek olan verileri doğrulamamız gerekebilmektedir. Bunun için en etkili kütüphanelerden biri olan FluentValidation‘ı kullanabiliriz.

Onion Architecture'da CQRS + MediatR Pattern + AutoMapper + Fluent Validation

Tüm bu kütüphaneleri Application katmanına yükleyiniz…

Onion Architecture'da CQRS + MediatR Pattern + AutoMapper + Fluent ValidationFluent Validation kütüphanesi Validator sınıfları üzerinden doğrulama gerçekleştirmektedir. Onion Architecture’da Validator sınıflarını ‘Features’ altında ilgili Command yahut Query altında oluşturmamız yeterli olacaktır.

    public class CreateProductCommandValidator : AbstractValidator<CreateProductCommandRequest>
    {
        public CreateProductCommandValidator()
        {
            RuleFor(p => p.Name)
                .NotNull()
                .WithMessage("Lütfen 'Name'i boş geçmeyiniz.")
                .MaximumLength(20)
                .MinimumLength(3)
                .WithMessage("'Name' değeri 3 ile 20 karakter arasında olmalıdır.");
            RuleFor(p => p.Price)
                .Must(p => p > 0)
                .WithMessage("Lütfen 'Price' değerini doğru giriniz.");
        }
    }

Ardından bu validator’ı türlü yöntemlerle işleyebilir ve kullanabiliriz. Misal ben burada ‘ValidationFilter’ isminde bir filter oluşturacağım ve ilgili validasyonları burada kontrol edeceğim. Tabi bu filter’ı ‘Infrastructure’ katmanında oluşturmamız daha doğru olacaktır. Haliyle aşağıdaki gibi bir tasarım neticesinde işlemimizi gerçekleştiriyoruz;
Onion Architecture'da CQRS + MediatR Pattern + AutoMapper + Fluent Validation

    public class ValidationFilter : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            if (!context.ModelState.IsValid)
            {
                var errors = context.ModelState
                    .Where(x => x.Value.Errors.Count > 0)
                    .ToDictionary(e => e.Key, e => e.Value.Errors.Select(e => new
                    {
                        e.ErrorMessage
                    })).ToArray();

                context.Result = new BadRequestObjectResult(errors);
                return;
            }
            await next();
        }
    }

İlgili filter’ı oluşturduktan sonra ‘Presentation’ katmanındaki ‘WebAPI’ projesinin ‘Startup.cs’ dosyasında aşağıdaki konfigürasyonu gerçekleştirelim.

    public class Startup
    {
        .
        .
        .
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplicationServices();
            services.AddPersistenceServices(Configuration);
            services.AddInfrastructureServices();

            services.AddControllers(options => options.Filters.Add<ValidationFilter>())
                .AddFluentValidation(configuration => configuration
                    .RegisterValidatorsFromAssemblyContaining<Application.Features.Commands.CreateProduct.CreateProductCommandValidator>())
                .ConfigureApiBehaviorOptions(o => o.SuppressModelStateInvalidFilter = true);
            .
            .
            .
        }
        .
        .
        .
    }

Burada 12 ile 15. satırlar arasına göz atarsanız eğer ilgili entegrasyon sağlanmaktadır. 12. satırda AddControllers(options => options.Filters.Add<ValidationFilter>()) komutu ile geliştirilen ValidationFilter isimli filter’ı uygulamaya dahil ediyoruz. 13. satırda ise Fluent Validation kütüphanesini uygulamaya entegre ediyor ve 14. satırda ise validator sınıflarının hangi assembly’den okunacağını bildiriyoruz. Son olarak da 15. satırda model state’lerinin otomatik kontrol edilmesini pasifleştiriyoruz. Böylece artık her bir istek geldiğinde ilgili filter tetiklenecek ve validation kontrolü sağlamış olacaktır. Uygun olmayan bir model post edildiği zaman aşağıdakine benzer bir hata alınacaktır.

Onion Architecture'da CQRS + MediatR Pattern + AutoMapper + Fluent Validation

Böylece Onion Architecture üzerinde CQRS + MediatR pattern’ın nasıl uygulandığını ve akabinde AutoMapper ile conversion işlemlerinin nasıl otomatize edildiğini ve Fluent Validation sayesinde de validation kontrolünün nasıl yapıldığını kombine olarak incelemiş olduk. Umarım verimli olmuştur…

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek projeyi indirmek için buraya tıklayınız.

Bunlar da hoşunuza gidebilir...

3 Cevaplar

  1. Tarık dedi ki:

    Çok güzel

  2. Murat dedi ki:

    Merhabalar,
    Bu yapıda iş katmanını sizce nerede yapmalıyız? Sizin örneğinizde GetAllProductResponse iş katmanı için uygun yer mi? (Bu kısımda kafam karıştı açıkçası)

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*