Przejdź do treści

DevStyle - Strona Główna
CQRS+DI: implementacja w C# i Autofac

CQRS+DI: implementacja w C# i Autofac

Maciej Aniserowicz

10 listopada 2016

Backend

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.

[blogvember2016 no=”8″]

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.

Zobacz również