Vertical Slice Architecture

Vertical Slice mimarisini kullanarak örnek bir proje oluşturdum. Proje içerisinde Vertical Slice mimarisi ile beraber MediatR, CQRS, Migration gibi kütüphanelerde kullandım. “Vertical Slice Architecture Nedir?” sorusunun cevabı için “N-Tier Arcihtecture” ile kıyasladığım yazıyı inceleyebilirsiniz.
Bknz : Vertical Slice Architecture Vs. N-Tier Architecture


Örnek projede kullandığımız “Mediator Pattern” yaklaşımını bizler için oldukça basit bir hale getiren “MediatR” paketini kullanıyoruz. Bu pakette “Vertical Slice Architecture” fikrini ortaya atan kişi tarafından oluşturulan bir kütüphane. Jimmy Bogard’ın aynı zamanda “AutoMapper” adında bir çoğumuzun kullandığı bir kütüphanesi daha var. Bu yüzden Jimmy abimizi takip etmenizi öneririm. 🙂

blank
Mediator Pattern

Mediator Pattern’ı anlatan bir çok güzel görsel var buda onlardan bir tanesi. Bir trafik polisinin görevini düşünün trafikte seyir halinde devam eden araçlar kendileri arasında “sen geç abi, dur ben geçeyim” tarzında tartışmalar girdiğinde ortaya çıkan sonucu düşünün. Bir de trafik polisinin araç araç yaptığı yönlendirme sonucunda düzenli geçişlerden çıkan sonucu düşünün.

Bizimde MediatR’a vereceğim görev şu olacak. Biz Query ya da Command feature için belirlediğimiz request cağırıldığında ilgili Handler’ın cağırılmasını bize sağlayacak. Yani method cağırırken aslında sadece request nesnemizi belirleceğiz mediatR bize ilgili handlerı adresliyecek. Bunun detaylı örneğini birazdan konuşacağız.

MediatR’ın sağladığı en büyük avantajlardan biride “PipelineBehavior”. Vertical Slice’da bir request bir feature demiştik.(Örn : GetUserById)
Tüm aksiyonlardan için tek bir yerden validation, loglama, cacheleme yapmak istiyorum.

Bu davranışları tek bir yerden yönetilme imkanını “PipelineBehavior” ile yakalıyoruz. Bir request işlemi gerçekleşirken akışın arasına girip girdi ve çıktıya erişebilmemizi sağlıyor.

Ben örnek projemde sadece “ValidationBehavior” kullandım. Bir örneğini gördüğünüzde diğer davranışları siz zaten rahat bir şekilde yapabileceğinizi göreceksiniz.

using MediatR;
using System.Threading;
using System.Threading.Tasks;
using VerticalSliceArchitectureSample.WebApi.Results;
using VerticalSliceArchitectureSample.WebApi.Validation;

namespace VerticalSliceArchitectureSample.WebApi.Behaviours
{
    public class ValidationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TResponse : ResultModel, new()
    {
        private readonly IValidationHandler _validationHandler;
        public ValidationBehaviour(IValidationHandler validationHandler)
        {
            _validationHandler = validationHandler;
        }
        public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next)
        {
            ResultModel result = await _validationHandler.Validate(request);
            if (!result.IsSuccess)
                return new TResponse
                {
                    IsSuccess = false,
                    Messages = result.Messages
                };
            return await next();
        }
    }
}

IPipelineBehavior ara yüzünü implemente ederek generic TRequest nesnesini validate işlemine dokup olumsuz bir durumda TResponse ile geri dönüş yapabiliyoruz. Middleware gibide düşünebiliriz.

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));

Startup içerisinde tanımlamasını bu şekilde yapacağız. Birden fazla pipelineBehavior olabilir. Aynı şekilde kayıt edebiliriz ancak ekleme yaparken hangi davranışın daha önce çalışmasını istiyorsak ona göre sırayı belirmeliyiz. Startup’da yapacağımız bir çok dependency kaydı var. Bunları tek bir yerde toplamak ve bir extension haline getirmek çok daha iyi olacaktır.

using Corex.Model.Infrastructure;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using VerticalSliceArchitectureSample.WebApi.Behaviours;
using VerticalSliceArchitectureSample.WebApi.Contexts;
using VerticalSliceArchitectureSample.WebApi.Validation;

namespace VerticalSliceArchitectureSample.WebApi.Dependency
{
    public static class DependencyServiceExtension
    {

        public static void AddDependencies(this IServiceCollection services, IConfigurationRoot configurationRoot)
        {
            RegisterIoCManager(services);
            RegisterAllDependencies(services);
            RegisterMediatR(services);
            RegisterContext(services, configurationRoot);
        }
        #region Private Methods
        private static void RegisterContext(IServiceCollection services, IConfigurationRoot configurationRoot)
        {
            services.AddContexts(configurationRoot);
        }

        private static void RegisterMediatR(IServiceCollection services)
        {
            services.AddMediatR(typeof(Startup).Assembly);
            services.AddValidators();
            //Behaviour sırası önemli. Verilecek sıraya göre davranış gösterecektir.
            services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
        }
        private static void RegisterIoCManager(IServiceCollection services)
        {
            services.AddSingleton(x =>
            ActivatorUtilities.CreateInstance(x, services));
        }
        private static void RegisterAllDependencies(IServiceCollection services)
        {
            services.Scan(scan => scan
                     .FromApplicationDependencies()
                     .AddClasses(classes => classes.AssignableTo()).AsImplementedInterfaces().WithTransientLifetime()
                     .AddClasses(classes => classes.AssignableTo()).AsImplementedInterfaces().WithScopedLifetime()
                     .AddClasses(classes => classes.AssignableTo()).AsImplementedInterfaces().WithSingletonLifetime()
                 );
        }
        #endregion
    }
}

Böylece startup içerisinde sadece “services.AddDependencies” methodu tüm dependencylerimizi kayıt etmiş olacaktır.


blank
CQRS Pattern

CQRS için bir çok yerde gördüğüm bu görevli çok beğeniyorum. Konuyu çok güzel özetliyor. CQRS kullanmanın bir çok avantajı var.

  • İlişkisel veri tabanlarında kayıt ekleme/düzenleme işlemleri sırasında ilgili satır locklanır. Bu süre zarfında gelen select sorgularında deadlock problemi yaşarız. CQRS ile bu sorunu çözmüş oluyoruz.
  • Command işlemi sırasında farklı bir ORM, Query işlemi sırasında farklı bir ORM kullanmakta özgürüz.
  • Command işleminde MsSQL, PostgreSQL kullanıp, Query işleminde MongoDB, Redis v.b noSQL yapıları kullanmakta özgürüz.
  • Yaptığımız gösterimlerin (dashboard, rapor v.b) trafik yoğunluğu ile beraber dar boğaz yaratıp kayıtlarımızda ekleme/düzenleme işlemi yaparken yavaşlık sorunları yaşabiliyoruz. CQRS ile bunuda çözmüş oluyoruz.

En büyük dezavantajı ise command işleminde sonra read yapılacak DB’nin replicasının sağlıklı bir şekilde beslenmesi. Burada yapılacak event patlatma yapısının doğru ve stabil bir şekilde çalıştığından emin olmalıyız.

Yukarıda bahsettiğim dependency kayıtlarında “AddContexts”de var. Command işlemleri için “SampleCommandContext”, Query işlemleri için ise “SampleQueryContext” oluşturdum. Bunun amacına uygun olarak iki farklı DB kullanmak doğru olacaktır tabii ancak örnekte ben tek DB kullandım. Dilerseniz sadece connectionString değiştirerek bu özelliği kullanabilirsiniz.

MediatR ve CQRS hakkında konuştuktan sonra şimdi bunların kullanıldığı Query ve Command featurelarını detaylıca inceleyelim. MediatR ile API seviyesinde nasıl kullanacağız bunları görelim.


Talep : User tablosundan “Id” değerini kontrol ederek varsa kullanıcının geriye “Id” ve “Email” değerlerini döndüren bir API end point isteniyor.

İlk olarak bu talep için bir isim bekliyoruz. Yani feature adı diyebiliriz, bunu da “GetUserById” olarak belirledik. Bu talep veri tabanında kayıt, düzenleme gibi bir aksiyon almayacağı için bunu “Query Feature” olarak “Features/Query” klasörü altında açıyoruz. “Query Feature” özetle “HttpGet” işlemine denk gelir diyebiliriz.

using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VerticalSliceArchitectureSample.WebApi.Contexts;
using VerticalSliceArchitectureSample.WebApi.Domain.Users.User;
using VerticalSliceArchitectureSample.WebApi.ExceptionHandler;
using VerticalSliceArchitectureSample.WebApi.Results;
using VerticalSliceArchitectureSample.WebApi.Validation;

namespace VerticalSliceArchitectureSample.WebApi.Queries.Users.User
{
    public static class GetUserById
    {
        public record Query(Guid Id) : IRequest;

        public class Response : ResultModel
        {
            public Guid Id { get; set; }
            public string Email { get; set; }
        }

        public class ValidatorHandler : IValidationHandler
        {
            public Task Validate(Query request)
            {
                //GetById için validation gerekirse buraya yazabiliriz.
                return Task.FromResult(ResultModel.Ok());
            }
        }
        public class Handler : IRequestHandler<Query, Response>
        {
            private readonly SampleQueryContext _dbContext;
            public Handler(SampleQueryContext context)
            {
                _dbContext = context;
            }
            public async Task Handle(Query request, CancellationToken cancellationToken)
            {
                using IDbContextTransaction transaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
                try
                {

                    UserEntity userEntity = _dbContext.Set().Where(s => s.Id == request.Id).FirstOrDefault();
                    return await Task.FromResult(new Response
                    {
                        IsSuccess = true,
                        Id = userEntity.Id,
                        Email = userEntity.Email
                    });
                }
                catch (System.Exception ex)
                {
                    await transaction.RollbackAsync(cancellationToken);
                    ExceptionManager exceptionManager = new ExceptionManager(ex);
                    return await Task.FromResult((Response)ResultModel.Error(exceptionManager.GetMessages()));
                }
            }
        }
    }
}

Feature içerisinde talep edilen request ve response classları özgürce belirliyorum.
Farklı bir katmanda Dto ya da ViewModel objesi açmak için vakit kaybetmiyorum.
Parametrelerin çok büyüdüğü durumlarda yine feature içerisinde ayrı classlar tanımlayabilirim. Bunda da özgürüm.
MediatR kütüphanesinin IRequest, IRequestHandler gibi interfacelerini kullanarak feature geliştirmelerimi tamamlıyorum.
API üzerinden nasıl cağıracağımıza geçmeden önce bir diğer feature için neler yapacağız ona göz atalım.


Talep: User tablosuna kayıt için “Email, Password” bilgileri ile kayıt işlemi yapılması istenilen bir API end point talep ediliyor. Email sistemde kayıtlı ise tekrar kayıt olmasın, işlem başarılı olursada geriye yeni kayıt olan kullanıcının ID bilgisinin dönülmesi gerekiyor.

Talep için belirlediğimiz feature ismi “RegisterUser” bir http post işlemi olduğu için “Command Feature” olarak değerlendiriyoruz. “Features/Commands” altında “RegisterUser” adında feature açarak kodlarımızı yazıyoruz.

using Corex.Model.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using VerticalSliceArchitectureSample.WebApi.Contexts;
using VerticalSliceArchitectureSample.WebApi.Controllers.Users.RequestValidations;
using VerticalSliceArchitectureSample.WebApi.Domain.Users.User;
using VerticalSliceArchitectureSample.WebApi.ExceptionHandler;
using VerticalSliceArchitectureSample.WebApi.Results;
using VerticalSliceArchitectureSample.WebApi.Validation;

namespace VerticalSliceArchitectureSample.WebApi.Commands.Users.User
{
    public static class RegisterUser
    {
        public record Command(string Email, string Password) : IRequest;
        public class Response : ResultModel
        {
            public Guid Id { get; set; }
        }

        public class Validator : IValidationHandler
        {
            private readonly SampleQueryContext _dbContext;
            public Validator(SampleQueryContext dbContext) => _dbContext = dbContext;
            public Task Validate(Command request)
            {
                //Request Validation..
                UserRegisterRequestValidation validationRules = new UserRegisterRequestValidation();
                FluentValidation.Results.ValidationResult validateResult = validationRules.Validate(request);
                if (!validateResult.IsValid)
                {
                    return Task.FromResult(ResultModel.Error(validateResult.Errors.Select(v =>
                    new MessageItem
                    {
                        Code = v.ErrorCode,
                        Message = v.ErrorMessage
                    }).ToList()));
                }

                //Business Validation..
                if (_dbContext.Set().Any(v => v.Email == request.Email))
                {
                    return Task.FromResult(ResultModel.Error(new MessageItem
                    {
                        Code = "Duplicate_Email",
                        Message = "There is a registered mail."
                    }));

                }
                return Task.FromResult(ResultModel.Ok());
            }
        }
        public class Handler : IRequestHandler<Command, Response>
        {
            private readonly SampleCommandContext _dbContext;
            public Handler(SampleCommandContext sampleCommandContext)
            {
                _dbContext = sampleCommandContext;
            }
            public async Task Handle(Command request, CancellationToken cancellationToken)
            {
                using IDbContextTransaction transaction = await _dbContext.Database.BeginTransactionAsync(cancellationToken);
                try
                {
                    IResultObjectModel createUserResult = UserEntity.Create(request.Email, request.Password);
                    if (!createUserResult.IsSuccess)
                    {
                        return await Task.FromResult((Response)ResultModel.Error(createUserResult.Messages));
                    }

                    EntityEntry insertItem = _dbContext.Set().Add(createUserResult.Data);
                    await _dbContext.SaveChangesAsync(cancellationToken);
                    await transaction.CommitAsync(cancellationToken);
                    return await Task.FromResult(new Response
                    {
                        IsSuccess = true,
                        Id = insertItem.Entity.Id
                    });
                }
                catch (System.Exception ex)
                {
                    await transaction.RollbackAsync(cancellationToken);
                    ExceptionManager exceptionManager = new ExceptionManager(ex);
                    return await Task.FromResult((Response)ResultModel.Error(exceptionManager.GetMessages()));
                }
            }
        }
    }
}

Command için Query’de yaptığımız gibi IRequest objesinden inputModelimizi oluşturuyoruz. Gelen isteği db seviyesinde sorgu atmadan request validation yaparken FluentValidation kütüphanesini kullandım.
Böylece kayıt isteği geldiğinde geçersiz bir email talebi varsa db seviyesinde her hangi bir sorgu atmadan isteği sonlandırabiliyoruz.
Handle içerisinde kayıt işlemlerimizi tamamlıyoruz.
Kayıt sırasında olası bir hata durumunda ExceptionManager’ı Corex içerisinde kullandım. Corex’de buna benzer bir çok yardımcı paket bulabilirsiniz.

Query ve Command için nasıl feature oluşturacağımızı gördük şimdi işin en basit bölümü kaldı API tarafında kullanmak bunun içinde dependency injection ile MediatR kütüphanesinin bize sağladığı “IMediator” interfaceini kullanarak featurelarımızı cağıracağız.


using MediatR;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
using VerticalSliceArchitectureSample.WebApi.Commands.Users.User;
using VerticalSliceArchitectureSample.WebApi.Queries.Users.User;

namespace VerticalSliceArchitectureSample.WebApi.Controllers
{
    public class UserController : BaseApiController
    {
        private readonly IMediator _mediator;
        public UserController(IMediator mediator)
        {
            _mediator = mediator;
        }
        [HttpPost]
        public Task Register(RegisterUser.Command registerCommand)
        {
            return _mediator.Send(registerCommand);
        }
        [HttpGet]
        public Task GetById(Guid id)
        {
            GetUserById.Query request = new GetUserById.Query(id);
            return _mediator.Send(request);
        }
    }
} 

Register örneğinde Command nesnesini direkt olarak bir Dto, ViewModel ya da InputModel gibi kullanabildiğimizi göstermek istedim. Query içinde dışarıdan Guid parametresi olarak instance alarak mediator.send yapmayı göstermek istedim.
Mediator’a query ve command nesnelerini göndermemiz yeterli oluyor.
Direkt olarak ilgili handler cağırmamıza gerek kalmadığı için olası bir handler değişiminde özgür olmuş oluyoruz bu da çok büyük bir avantaj sağlıyor bizlere.


Vertical Slice Architecture’ı anlatırken bir yandan CQRS ve MediatR’da kullanmış olduk. Best Practice budur diyemem ancak güzel bir örnek olduğunu düşünüyorum. Şuan Vertical Slice ile edindiğim tecrübelere dayanarak şunu söyleyebilirim.
Mikro bir proje(mikro servis) yapacaksam Vertical Slice kullanmayı tercih ederim. Monolit bir proje yapmak zorundaysam ve büyük bir proje ise n-tier architecture tercih ederim diye düşünüyorum.

İleride elde edeceğim tecrübelerime göre bu fikirler tabii değişebilir. Sizlerinde örnek projeyi inceleyip, olumlu/olumsuz eleştrilerinizle projeye katkıda bulursanız eğer çok sevinirim.

Github repo url : https://github.com/gsmtcnr/VerticalSliceArchitectureSample

Leave a Reply

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