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...

12 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ı)

  3. Kaan Acar dedi ki:

    Merhabalar benim hep merak ettiğim Application katmanında Parameters kısmı diye bir klasör görünmekte. Buradaki olay he request de kullanıcından istenecek parametreler sanırım. Bunun uygulaması ile ilgili hiç bir yerde örnek bir makale, uygulama vs göremedim. Konuya biraz daha açıklama getirebilirseniz çok memnun olurum.

    • Gençay dedi ki:

      Merhaba,

      İlgili klasör genellikle bu konuda verilen örnek materyallerden birisidir ve düşündüğünüz gibi request’te gelecek bazı parametreleri object olarak karşılayabilmemizi sağlayacak bazı class’ları barındırmaktadır.
      Sevgiler…

  4. Soner dedi ki:

    Merhabalar,

    Bazı örneklerde services katmanı oluşturulmakta, bu katmandan UnitOfWork’a oradan da Repository’lerimize ulaştığımız yapılar var. Bu farklı bir mimariye mi ait yoksa mimarilerde kullanılan bir tasarım deseni midir? Onion Architacture baktığımda bahsettiğim bu durum uymuyor gibi geldi bana. Eğer kullanabiliyor isek avantaj sağlar mıyız. Bahsettiğim örneklerde de Core katmanı görünmekte. Sanki n-tier gibi geldi bana bu bahsettiğim durum :))

    Aydınlatırsanız çok memnun olurum.

    • Gençay dedi ki:

      Merhaba Soner Bey,

      Onion Architecture’da services katmanına Infrastructure(Persistence – Infrastructure) katmanı karşılık gelmektedir. Persistence‘da veritabanına dair servis operasyonları yürütülürken Infrastructure‘da ise diğer servis operasyonları yürütülür.

      Bunun dışında türlü katmanlı yaklaşımlar mevcuttur. Evet, geleneksel mimarinin bir başka türevinde services katmanı olduğunu hatırlıyor gibiyim…

      Umarım sorunuza cevap verebilmişimdir.
      İyi çalışmalar dilerim…

      • Soner dedi ki:

        Teşekkürler Gençay Bey,

        Yanlış anlaşılmama adına bir örnek aradım ve sonunda buldum. Açıkçası kavramları da öğrenmeye çalışıyorum. Bahsettiğim şey bulduğum kaynağa göre Repository-Service Pattern olarak geçmekte. En azından kaynağım bu şekilde söylemekte. Bir de örnekle anlatmakta. Kaynak: https://exceptionnotfound.net/the-repository-service-pattern-with-dependency-injection-and-asp-net-core/

        Bu örnek üzerinden gider isek benim anlatmaya çalıştığım soru üzerinden yanıtladığınız cevap aynen geçerli midir ? :)) Geçerli değil ise nasıl barındırmamız gerekecek.

        Bu tasarım desenini kullanmaz isek Onion Architacture mimarisinde aynı örnekte ifade edilen, Yemek ve Bilet üzerinden oluşturulacak bir istatistik nasıl ve ne şekilde barındırmamız gerekecek?

        Yorduğum için kusura bakmayın. Şimdiden teşekkürler.

        • Gençay dedi ki:

          Soner Bey merhaba,

          İlgili pattern hakkında şuanda bir bilgim bulunmamaktadır. Konuya dair benimde meramım oluştuğu için ilk fırsatta referans ettiğiniz makale dahil geniş bir araştırma yapıp, gerektiği taktirle bir makale eşliğinde sizlere geri dönüş yapacağım…

          Şimdilik görüşmek üzere…
          Kolaylıklar dilerim…

          • Soner dedi ki:

            Merhaba tekrardan,

            Youtube üzerinden yayınlarınızı görmekteyim ve .net core 6 ile e-ticaret sitesini Onion Architecture ile yazacağınızı gördüm. Açıkçası çok memnun oldum. Sanırsam buradaki makaleleriniz bu örnek projeye temel teşkil edecek. Konuyu başlatma nedenim olan “Repository-Service Pattern” inceleme fırsatınız oldu mu? Gerçekten böyle bir kavram var mı ondan da emin değilim. Eğer var ise bu pattern’i de ilgili projede örnekleyebilirseniz çok memnun olurum.

            Tüm iyi dileklerimle.

  5. Cihan dedi ki:

    Merhaba,

    Öncelikle makale için teşekkürler. Onion Architecture ve CQRS monolith projelerde kullanılıyor mu ? Genelde mikroservis mimarili projelerde görüyorum. Eğer olmuyorsa neden olmuyor bilgilendirebilir misiniz ?

  6. batu dedi ki:

    Öncelikle emeğinize sağlık,

    Birden fazla Validator sınıfı kullanmak istersek aşağıdaki kodu nasıl düzenlemeliyiz?

    services.AddControllers(options => options.Filters.Add())
                .AddFluentValidation(configuration => configuration
                    .RegisterValidatorsFromAssemblyContaining())
                .ConfigureApiBehaviorOptions(o => o.SuppressModelStateInvalidFilter = true);
    

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.