fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
7 minut

CQRS+DI: implementacja w C# i Autofac


10.11.2016

O CQRS (Command Query Responsibility Segregation) jest w ostatnich latach bardzo głośno. Sam wielokrotnie mówiłem na ten temat prezentację i napisałem artykuł do ProgramistaMag. W tym tekście pominę wstęp teoretyczny i wskoczę prosto w kodzik.

Akcja: BLOGvember! Post nr 8.
W listopadzie w każdy roboczy poranek na devstyle.pl znajdziesz nowy, świeżutki tekst. W sam raz do porannej kawy na dobry początek dnia.
Miłej lektury i do przeczytania jutro! :)

Commands

Podstawowym elementem składowym CQRS są komendy. Zresztą: sama nazwa o tym mówi, prawda? Command pattern, czyli zamknięcie wszystkich informacji potrzebnych do wykonania dowolnej akcji w osobnej, dedykowanej klasie. To tak, jakby… wziąć wszystkie parametry z jakiejś metody i je opakować.

Potrzebujemy w systemie abstrakcji nad pojęciem “komendy”. W .NET doskonale nada się do tego interfejs. Jednak komendy nie potrzebują współdzielić żadnych informacji, nie mają niczego wspólnego. Więc będzie to interfejs pusty, tzw. “marker interface“:

public interface ICommand
{
         
}

Tak, wiem, że można do tego zastosować również atrybuty/adnotacje. Owszem, można. Ale atrybuty to po prostu… więcej zachodu. I nie dają możliwości wykorzystania generics, co bardzo nam się przyda.

Każda komenda zaimplementuje taki interfejs. Ale co on daje, skoro jest pusty? Wbrew pozorom, całkiem sporo: daje nam możliwość łatwego odnalezienia wszystkich komend w systemie. Czy to ręcznie, przez programistę (“go to implementation” w R#), czy też przez runtime, za pomocą refleksji. To drugie zastosowanie wyciśniemy już za chwilę.

Skoro mamy komendę, to potrzebujemy “coś, co ją wykona”. Czyli: command handler. No to siup!

public interface IHandleCommand
{

}

W CQRS każda komenda ma jeden handler. Nie zero, nie wiele. JEDEN. Można o tym myśleć jak o rozbiciu metody na dwie osobne części. Argumenty i ciało metody w dedykowanych klasach.

Ale to jest znowu marker interface: nic nie robi, niczego nie definiuje! To prawda, dlatego potrzebujemy:

public interface IHandleCommand<TCommand> : IHandleCommand
    where TCommand : ICommand
{
    void Handle(TCommand command);
}

Wersja generyczna już przy deklaracji swojego typu mówi, jaką komendę obsługuje. I wreszcie mamy jakąś metodę: Handle().

Brakuje jeszcze jednego elementu spinającego całość: mechanizmu dystrybucji komend w systemie. “Szyny komend”:

public interface ICommandsBus
{
    void Send<TCommand>(TCommand command) where TCommand : ICommand;
}

Implementacja tego interfejsu może być różna w zależności od specyfiki systemu. Jedno z rozwiązań to:

public class CommandsBus : ICommandsBus
{
    private readonly Func<Type, IHandleCommand> _handlersFactory;
    public CommandsBus(Func<Type, IHandleCommand> handlersFactory)

    {
        _handlersFactory = handlersFactory;
    }

    public void Send<TCommand>(TCommand command) where TCommand : ICommand
    {
        var handler = (IHandleCommand<TCommand>)_handlersFactory(typeof(TCommand));
        handler.Handle(command);
    }
}

Co my tu mamy? Dependency Injection! Nasza szyna jest zależna od “fabryki handlerów”: funkcji, która na podstawie typu wiadomości potrafi stworzyć instancję jej handlera. To jest abstrakcja doskonała: zostajemy przy czystym .NET (bo Func<>), nie uzależniamy się na tym etapie od żadnego kontenera DI, a jednocześnie dokładnie modelujemy swoje potrzeby. “Dam temu czemuś komendę, a to coś da mi handler”. Wsio.

Events

Koncept CQRS jest rozszerzalny o zdarzenia, czyli Eventy. Zdarzenia mogą posłużyć wielu celom (event sourcing, any1?), ale ich najbardziej podstawowe zastosowanie to: poinformować resztę systemu, że COŚ się wydarzyŁO.

Struktury potrzebne do ich implementacji są lustrzanym odbiciem tego, co znamy już z komend. Czyli:

public interface IEvent
{

}
public interface IHandleEvent
{

}
public interface IHandleEvent<TEvent> : IHandleEvent
    where TEvent : IEvent
{
    void Handle(TEvent @event);
}

Różnice zaczynamy zauważać przy “szynie zdarzeń”:

public interface IEventsBus
{
    void Publish<TEvent>(TEvent @event) where TEvent : IEvent;
}

Metoda “Send()” z CommandBus została zastąpiona metodą “Publish()”. Koncepcyjnie bowiem zdarzenia mogą mieć dowolną liczbę handlerów. Dowolną, czyli: 0, 1 lub wiele.

Łatwo domyślić się zatem, jak może wyglądać przykładowa implementacja:

public class EventsBus : IEventsBus
{
    private readonly Func<Type, IEnumerable<IHandleEvent>> _handlersFactory;
    public EventsBus(Func<Type, IEnumerable<IHandleEvent>> handlersFactory)
    {
        _handlersFactory = handlersFactory;
    }

    public void Publish<TEvent>(TEvent @event) where TEvent : IEvent
    {
        var handlers = _handlersFactory(typeof(TEvent))
            .Cast<IHandleEvent<TEvent>>();

        foreach (var handler in handlers)
        {
            handler.Handle(@event);
        }
    }
}

Jest to PRAWIE to samo co CommandBus, tylko obsługujemy scenariusz “wielu handlerów”. Uzależniamy się od funkcji zwracającej IEnumerable. Pięknie.

No i dobra, ale jak to wszystko… spiąć?

Wire it up!

O Dependency Injection pisałem już wielokrotnie, i pewnie jeszcze nieraz napiszę. I właśnie DI z sensownym kontenerem pomoże nam całość posklejać. Poukładać, dopasować, jak puzzle. I wreszcie: uruchomić.

Poniższy kod przedstawia konfigurację Autofac, mojego ulubionego kontenera DI w .NET. Autofac poważam, ponieważ bardzo mocno separuje on proces konfiguracji i tworzenia kontenera od procesu jego używania. Służą do tego nawet osobne klasy: ContainerBuilder i Container.

Konfiguracja zachowania Autofaca odbywa się w Modułach. Czyli: nie wrzucamy wszystkich porejestrowanych zależności na hurra w jedno miejsce, a sensownie dzielimy pomiędzy dedykowane klasy. Podobny mechanizm znajdziemy zresztą chociażby w StructureMap (Rejestry) czy Castle Windsor (Instalatory).

Zarejestrowanie WSZYSTKIEGO związanego z komendami można umieścić w CommandsModule:

public class CommandsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.RegisterAssemblyTypes(ThisAssembly)
            .Where(x => x.IsAssignableTo<IHandleCommand>())
            .AsImplementedInterfaces();

        builder.Register<Func<Type, IHandleCommand>>(c =>
        {
            var ctx = c.Resolve<IComponentContext>();

            return t =>
            {
                var handlerType = typeof (IHandleCommand<>).MakeGenericType(t);
                return (IHandleCommand) ctx.Resolve(handlerType);
            };
        });

        builder.RegisterType<CommandsBus>()
            .AsImplementedInterfaces();
    }
}

I pozamiatane. Rejestrujemy wszystkie handlery, ich fabrykę, a na koniec samą szynę. Na uwagę zasługuje głównie proces rejestracji fabryki handlerów. Pamiętajcie, że rejestrując własną fabrykę w Autofac nie można używać bezpośrednio przekazanego parametru “c”: trzeba z niego pobrać IComponentContext i dopiero ten kontekst wykorzystać w zwracanej lambdzie. Inaczej: w końcu coś się wywali.

Dzięki tym rejestracjom jesteśmy w stanie dorzucić do kontrolera (czy gdziekolwiek indziej) zależność na ICommandBus i cieszyć się dystrybucją komend w systemie.

Podobnie wygląda sprawa ze zdarzeniami, tylko ze względu na konieczność użycia IEnumerable, rejestracja fabryki handlerów jest nieco bardziej skomplikowana:

public class EventsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.RegisterAssemblyTypes(ThisAssembly)
            .Where(x => x.IsAssignableTo<IHandleEvent>())
            .AsImplementedInterfaces();

        builder.RegisterGeneric(typeof (AllEventsHandler<>))
            .As(typeof (IHandleEvent<>));

        builder.Register<Func<Type, IEnumerable<IHandleEvent>>>(c =>
        {
            var ctx = c.Resolve<IComponentContext>();
            return t =>
            {
                var handlerType = typeof(IHandleEvent<>).MakeGenericType(t);
                var handlersCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType);
                return (IEnumerable<IHandleEvent>)ctx.Resolve(handlersCollectionType);
            };
        });

        builder.RegisterType<EventsBus>()
            .AsImplementedInterfaces();
    }
}

Dwa razy “MakeGenericType()”, jedno po drugim, GENERICS AAALLLEEEEERT!!!!

To tak naprawdę nic szczególnie skomplikowanego. Po prostu potrzebujemy zarejestrować w kontenerze fabryki generyczne bez wykorzystania notacji generycznej.

RUN!

Hyc, koniec.

Zostało dorzucić do swojego systemu, zarejestrować oba moduły w kontenerze i używać. Poimplementować komendy i handlery i cieszyć się, jak się fajnie wywołują. Dorzucić zdarzenia i koncepcyjnie oddzielać AKCJE od REAKCJI.

Have fun.

0 0 votes
Article Rating
35 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
GW
GW
7 years ago

O jaaa… Takiego posta to się tu nie spodziewałem :D Nieźle wyszło!

GW
GW
7 years ago

Racja, racja.
Przyzwyczaiłeś, a potem uderzyłeś z zaskoczenia po prostu :)

Co do treści to przykład idealny do wykorzystania jako “starter CQRS”, bez żadnego zbędnego narzutu.

Marcin
Marcin
7 years ago

O kurka.. I życie staje się prostsze. Thx :)

kombain
kombain
7 years ago

Dzięki, czytałem kilka artykułów o CQRS, ale dopiero Twój uderza w sedno. Boom! Headshot!

Emi
Emi
7 years ago

Kurcze, ale fajnie jest tak zaczynać każdy dzień od Aniserowicza :D

piatkosia
piatkosia
7 years ago

Ooo wracają posty techniczne. Fajno. Plus dla Ciebie.

mpustelak
7 years ago

Wreszcie doczekalem sie CQRS u Ciebie na blogu. Oby tak dalej :)
Jedno do czego moge sie przyczepic to zgubienie ‘Q’ w w CQRS. ICommand odpowiada za komendy czyli typowe ‘zapisz’ zmiany, jednakze moglbys zaimplementowac tez IQuery, ktore po przekazaniu pewnych parametrow zwrociloby TResult.

Skarbnica wiedzy w tym temacie sa posty Jmmiego Bogard’a (nie wiem jak to powinno sie odmieniac) na blogu LosTechies (http://lostechies.com/). On zaproponowal paczke MediatR, ktora sama w sobie obsluguje te wszystkie elementy i posiada takze wiele rozszerzen.

PS: Sorry za brak polskich znakow :)

mpustelak
7 years ago

W pelni sie z Toba zgadzam i czekam na rozwiniecie tematu “read” :)

Scooletz
7 years ago

No! W końcu mięso! A dzień bez `MakeGenericType` to dzień stracony ;-)

PiotrB
7 years ago

Brakuje przy IHandleEvent: where TEvent :IEvent

Krzysztof Jendrzyca
7 years ago

Bardzo przydatny wpis. CQRS to jeden z tych wzorców, który pomaga nie tylko w organizacji kodu i architektury, ale także w organizacji pracy. Jedna osoba może implementować zapis, a druga odczyt. Są to osobne pliki więc praktycznie unikamy merge-conflictów. Stworzyłem kiedyś prosty starter do CQRSa ze skonfigurowanym Simple Injectorem, może komuś się przyda https://github.com/kjendrzyca/SimpleCQRS
Czekam na kolejne wpisy :)

Krzysztof Jendrzyca
7 years ago

Z pewnością byłoby czyściej zastosować fabrykę handlerów tak jak to pokazałeś i jest to świetny pomysł (będę stosował). W praktyce jednak i tak unikasz wszystkich smrodków związanych z service locatorem, bo jest on używany w 2 klasach w całej aplikacji i służy do tego samego do czego służyłaby fabryka :) A kontener i tak rejestruje się w DI sam, więc to kwestia dyscypliny teamu i CR-ek żeby go nie używali bezpośrednio.

rade
rade
7 years ago

Świetny wpis jak zwykle ;)
Mam jednak jedno pytanie, które nasuwa mi się po przeczytaniu jakiegokolwiek artykułu o CQRS.
Mamy Command, mamy Handler, mamy Event , pierwszy wniosek, który widzę to : teoretycznie asynchroniczne to to jak w mordę strzelił. Rzucam Command na CommandBus, a niech się wykona, a gdzieś tam zostanie podniesiony event jak się wykonało. Logiczne.
A jak to zrealizować w przypadku usług ? WCFa np. ? Przy założeniu, że klient usługi oczekuje na odpowiedź aby pójść dalej ze swoją logiką. Rzucam Command na CommandBus i …?

rade
rade
7 years ago

Właśnie asynchroniczność w moim przypadku jest kompletnie zbędna. Mam usługę, która wykonuje sobie jakieś tam akcje i ma to być synchroniczne.
Teraz chcąc zastosować CQRS muszę jakoś zwracać rezultaty wykonania tych akcji. I tu stwierdzam za każdym podejściem, że tak to chyba się nie da, albo raczej da ale to nie będzie CQRS tylko po prostu request-response.
Co Ty na to ?

sconrads
sconrads
7 years ago

Jak na moje oko to to nie jest artykuł dotyczący CQRS tylko takiego poor man’s messaging frameworka (udostepniajacego troszku tego co daje np NServiceBus). Tytuł jest więc mylący. Chyba, że chodziło Ci tylko o Command. Ale duzy plus za kod, bo czasem juz za dużo lałeś wodę w tekstach ;).
Osobiście uważam za fajne podejście ze Query strzela z serwisu aplikacyjnego bezpośrednio do repo readonly, a Command z serwisu aplikacyjnego używa DomainModel (z artefaktami z DDD) i repo z commitem.

UKS Niedźwiadek
7 years ago

Super post! dzięki wielkie za niego :) piątka

Krzysiek
7 years ago

A ja chciałbym żebyś przy którymś z kolejnych wpisów rozwinął jak dobre korzystać z Modułów w Contenerze DI :)

Chodzi mi o sytuację czy masz moduł per projekt, i czy zdarza Ci się reużywać tych modułów. W kilku miejscach można przeczytać że cała konfiguracja DI powinna być zawsze tylko w jednym miejscu (najczęściej projekt WebApp czy podobny). Jak więc ma się to do modułu per projekt i składania takiej konfiguracji w WebApp z modułów.

PaSkol
7 years ago

Przy okazji tego wpisu postanowiłem przyjrzeć się bliżej CQRS. Obejrzałem prezentację, przeczytałem artykuł, obejrzałem jeszcze inną prezentację. I mam problem. Lubię kiedy nazywa się rzeczy po imieniu, a nie mruga okiem i umawia, że będzie się je traktować inaczej (“choćbyście nie wiem jak się starali, to nie zrobicie z Tico Ferrari” – jak śpiewał pewien kabaret). I tu w CQRS mówi się o poleceniu, a owo polecenie jest de facto zestawem danych (nic nie robi), właściwym poleceniem okazuje się ów handler (zastanawiam się jak to przetłumaczyć, bo dobrze byłoby znaleźć bardziej adekwatne pojęcie niż “uchwyt”). Dlaczego zatem nie nazywać owych bytów dokładnie tak jak wskazuje ich przeznaczenie? Ów handler niech będzie poleceniem, a owo “polecenie” jest danymi.
Paradoksalnie to pytanie (IQuery) jest bliższe poleceniu (bo się wykonuje) niż owo ICommand.

Przypomnę tylko, że Meyer jako polecenia rozumiał procedury (czyli metody, które coś robią i nie zwracają wyniku), a za zapytania funkcje (które też coś robią, ale ma to na celu wyłącznie uzyskanie zwracanego wyniku).

PaSkol
7 years ago

Ale właśnie o szczegóły chodzi. Bo w nich kryje się diabeł (DevDevil ;) ). No i niestety owe szczegóły stały się de facto wzorem (nie wzorcem, ale wzorem), szablonem, który raczej bezrefleksyjnie się powiela (pachnie kopypasteryzmem).

Oczywiście poczekam na wpis na blogu.

P.S. We wspomnianej prezentacji bardzo podobał mi się koncept odejścia od ORM-a. Wydawało mi się, że ja z racji wieku jestem w kwestii ORM malkontentem (choć mam konkretne zarzuty, ale ORM to podobno takie nowoczesne) no i tutaj widzę, że kolejne pokolenia dojrzewają do uświadomienia sobie przerostu formy nad treścią dla ORM. Twój patent z widokiem SQL ja zrealizowałem już w 2009 (wtedy jeszcze w innym języku) za pomocą elastycznego systemu procedur składowanych (zrobiłem z tego podyplomówkę). Obecnie w C# używam go we współpracy z Dapperem. To podejście bardzo dobrze oddaję ideę podziału odpowiedzialności. A odpowiedzialność za utrwalanie danych oraz ich pozyskiwanie spoczywa na bazie danych.

devamator
devamator
7 years ago

Bardzo prosty i czytelny przykład.
Nie tak dawno temu przygotowałem sobie podobnego gotowca z uwzględnieniem query (https://github.com/devamator/CQRS). Moje ego skoczyło co najmniej o dwa poziomy, bo z tego co widzę jest dość podobnie.
Po przeczytaniu twojego posta uzupełniłem swoje repo o konfigurację autofaca, bo wcześniej robiłem to trochę bardziej naokoło, a dodatkowo będę miał na przyszłość żeby nie klepać jej za każdym razem od nowa :)

trackback

[…] CQRS+DI: implementacja w C# i Autofac […]

trackback

[…] CQRS+DI: implementacja w C# i Autofac […]

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również