CQRS + MediatR

1100

23 მარტი, 2021 წელი

CQRS + MediatR

CQRS (Command Query Responsibility Segregation) ტერმინი საზოგადოებას Greg Young-მა 2010 წელს გააცნო და აღწერს პატერნს სადაც მონაცემების წაკითხვა, მონაცემების შეცვლისგან არის განცალკევებული და დაფუძნებულია CQS (Command Query Seperation)-ის თეორიაზე. პატერნის გამოყენება საშუალებას გვაძლევს ლოგიკურად ან/და ფიზიკურად განვაცალკეოთ მონაცემების წაკითხვის ფუნქციონალი, მათი ცვლილების ფუნქციონალისგან. თუმცა ამგვარი დაყოფა ყველა ტიპის პროექტში არ არის გამართლებული და ღირებული, რადგან შესაძლოა ამ მიდგომამ პროექტების უმეტესობაში შემოიტანოს სარისკო კომპლექსურობა.

CQRS პატერნის გამოყენების მოტივაცია მჭიდროდ არის დაკავშირებული Event Sourcing-თან და Event-Driven არქიტექტურასთან მითუმეტეს თუ ჩვენი სისტემა შედგება მიკროსერვისებისგან.

აპლიკაციების უმრავლესობაში გვხვდება მონაცემთა ბაზასთან CRUD (Create, Read, Update, Delete) ოპერაციები. CRUD ოპერაციები CQRS პატერნში ითარგმნება შემდეგნაირად. CQRS პატერნი ყოფს მოვალეობებს 2 ნაწილად, Command -ები და Query-ები. Command-ების დანიშნულება არის მონაცემების მდგომარეობის შეცვლა, ხოლო Query-ების მონაცემების წაკითხვა.

ამგვარი დაყოფა გვაძლევს რამდენიმე გამოსარჩევ სარგებელს. მაგალითად მოვიყვან:

  • Query-ის მხარეს შეგვიძლია გვქონდეს განსხვავებული მონაცემთა საცავი (მაგ. არარელაციური), რომელიც უფრო მეტად იქნება ოპტიმიზირებული მონაცემების წაკითხვისთვის.
  • ფიზიკური განცალკევება Queries-ის და Command-ების უფრო ეფექტურად, რომ გავშალოთ დიდი მაშტაბზე. შეგვიძლია Query-ები ცალკე API -ში გვქონდეს და მაშტაბირება მოვახდინოთ მხოლოდ Query-ის API-ის. აპლიკაციის შრეზე კი გვექნება გაყოფილი Query-ები და Command-ები.
  • მონაცემებთან კავშირის ფრეიმვორკის აგნოსტიურობა. რაც გულისხმობს, რომ CQRS-ის გამოყენება საშუალებას გვაძლევს Query-ის ნაწილში გამოვიყენოთ მაგ. ADO.NET-ი (View, Stored Procedures, Table Valued Functions) და Command-ების მხარეს EntityFramework-ი.

ასეთი დაყოფისას ობიექტებს შორის კავშირებმა და დამოკიდებულებებმა რომ არ მიიღოს ქაოსის სახე, ამ პრობლემის მოგვარებაში გვეხმარება Mediator პატერნი.

Mediator არის ქცევითი დიზაინ პატერნი, რომლის გამოყენებაც ამცირებს ობიექტებს შორის დამოკიდებულებას. პატერნი ზღუდავს პირდაპირ კავშირს ობიექტებს შორის და აიძულებს ერთმანეთთან კომუნიკაციას mediator-ის ობიექტის გამოყენებით. განვიხილოთ CQRS-ის რეალიზაცია .NET-ში MediatR- ბიბლიოთეკის გამოყენებით.
იმისათვის რომ .NET Core-ის პროექტში დავაინიციროთ MediatR ბიბლიოთეკა, საჭიროა startup.cs ფაილში ჩავამატოთ MediatR-ის ინიცირების კოდის ფრაგმენტი.

public void ConfigureServices(IServiceCollection services)
{
    // Add MediatR services into DI
    services.AddMediatR(typeof(CreateAccountCommandHandler).GetTypeInfo().Assembly);

    services.AddControllers();
}

იმისათვის რომ სადემონსტრაციო პროექტში სიმარტივე შევინარჩუნოთ ავიღოთ .NET Core -ზე Web API-ის ტიპის პროექტი და CQRS-ისთვის დამახასიათებელი ლოგიკურად დავყოთ ამავე პროექტში.

594

მაგალითი: პროექტში CQRS-ის პატერნით განაწილებული მოვალეობები

Command-ი გამოიყენება მონაცემების ცვლილებებისას. MediatR ბიბლიოთეკას გააჩნია ინტერფეისი IRequest სადაც T-ს ნაცვლად ეთითება იმ ობიექტის ტიპი, რაც პასუხად უნდა დააბრუნოს Query-იმ ან Command-მა. Command-ის და შესაბამისი CommandHandler-ის ობიექტი გამოიყურება შემდეგნაირად:

using CQRS.Models;
using MediatR;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace CQRS.Account.Commands.CreateAccount
{
    public class CreateAccountCommand : IRequest<long>
    {
        public string Type { get; set; }
        public string Iban { get; set; }
    }

    public class CreateAccountCommandHandler : IRequestHandler<CreateAccountCommand, long>
    {
        private readonly AppDbContext _context;

        public CreateAccountCommandHandler(AppDbContext context)
        {
            _context = context;
        }

        public async Task<long> Handle(CreateAccountCommand request, CancellationToken cancellationToken)
        {
            var createdAccount = _context.Accounts.Add(new CQRS.Models.Account { Iban = request.Iban, Type = request.Type });
            await _context.SaveChangesAsync();

            return createdAccount.Entity.Id;
        }
    }
}

Handler-ის კონსტრუქტორში ხელმისაწვდომია DI Container-ში რეგისტრირებული ყველა სერვისი და აქვს ასინქრონული დამუშავების მხარდაჭერა. მოთხოვნის და Handler-ის ობიექტი Command-ებისთვის და Query-ებისთვის გამოიყურება ერთნაირად, განსხვავდება მხოლოდ დასახელებები. განსხვავებული დასახელებების და ფოლდერებში ლოგიკური დაჯგუფებით ვაღწევთ Command ების და Query-ების განცალკევებას.

Query-ი გამოიყენება მონაცემების წაკითხვისთვის.

using CQRS.Account.Models;
using CQRS.Models;
using MediatR;
using System.Threading;
using System.Threading.Tasks;

namespace CQRS.Account.Queries.GetAccountById
{
    public class GetAccountByIdQuery : IRequest<AccountItemModel>
    {
        public long Id { get; set; }
    }

    public class GetAccountByIdQueryHandler : IRequestHandler<GetAccountByIdQuery, AccountItemModel>
    {
        private readonly AppDbContext _context;

        public GetAccountByIdQueryHandler(AppDbContext context)
        {
            _context = context;
        }

        public async Task<AccountItemModel> Handle(GetAccountByIdQuery request, CancellationToken cancellationToken)
        {
            var account = await _context.Accounts.FindAsync(request.Id);

            return new AccountItemModel
            {
                Iban = account.Iban,
                Description = $"Requested account is type of {account.Type} and IBAN is {account.Iban}"
            };
        }
    }
}

ახლა კი, ვნახოთ როგორ გამოიყურება AccountsController-ი. ვინაიდან ძირითადი ლოგიკა გატანილი გვაქვს შესაბამის მხარეს (Query, Command)-ის Controller-ში რჩება მხოლოდ მედიატორის (შუამავლის) ობიექტის გამოძახება.

using CQRS.Account.Commands.CreateAccount;
using CQRS.Account.Models;
using CQRS.Account.Queries.GetAccountById;
using CQRS.Models;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;

namespace CQRS.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AccountsController : ControllerBase
    {
        private readonly IMediator _mediator;

        public AccountsController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetAccount(long id)
        {
            var account = await _mediator.Send(new GetAccountByIdQuery() { Id = id });

            if (account == null)
                return NotFound();

            return Ok(account);
        }

        [HttpPost]
        public async Task<IActionResult> CreateAccount([FromBody] CreateAccountModel request)
        {
            var createdAccountId = await _mediator.Send(new CreateAccountCommand { Iban = request.Iban, Type = request.Type });

            return Created("/", new { createdAccountId });
        }
    }
}

მაგალითისთვის გამოყენებული პროექტის სრული სორს კოდი შეგიძლიათ იხილოთ შემდეგ მისამართზე:

CQRS-ის პატერნთან ერთად, თიბისის გუნდში მუდმივად ხდება სხვადასხვა არქიტექტურული მიდგომების თუ კოდის დიზაინ პატერნების განხილვა, გაცნობა და პროექტებში გამოყენება. რაც საშუალებას გვაძლევს უფრო მეტი ეფექტურობით შევუსაბამოთ ტექნიკური გადაწყვეტა, პროექტში არსებულ მოთხოვნებს.

პოსტის მეორე ნაწილში განვიხილავ, როგორ შეგვიძლია MediatR-ის ბიბლიოთეკის გამოყენებით შევიმუშაოთ Publish/Subscribe მიდგომა. და ასევე განვიხილავთ MediatR-ის Behavior Pipeline-ებს. რა დამატებით საშუალებებს გვაძლევს აღნიშნული ფუნქციონალის რეალიზება და რა სახის მიდგომები არსებობს.