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.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Notify of
GW
GW

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

Marcin
Marcin

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

kombain
kombain

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

Emi

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

piatkosia

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

mpustelak

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 :)

Scooletz

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

PiotrB

Brakuje przy IHandleEvent: where TEvent :IEvent

Krzysztof Jendrzyca

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 :)

rade
rade

Ś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 …?

sconrads
sconrads

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

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

Krzysiek

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

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… Read more »

devamator
devamator

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 […]

Moja książka

Facebook

Zobacz również