Simple Event Store

Właśnie umieściłem na githubie zarys projektu SimpleEventStore (w skrócie SES). W założeniach ma to być biblioteczka (library - not framework) wspomagająca tworzenie aplikacji opartych o eventsouring (ES), gdzie dane są przechowywane w relacyjnej bazie danych. Na początek w podstawowej implementacji MS SQL Server.

Główne założenia do projektu:

Dodatkową zaletą ma być odczyt zdarzeń przy zachowaniu porządku zapisu. Przedstawię to na następującym przykładzie. Załóżmy, że klient rejestruje się w naszej aplikacji. Z punktu widzenia logiki biznesowej ważne jest, aby wiedzieć, że klient się zarejestrował (jedno zdarzenie) oraz dodatkową informacją jest to, że zaakceptował możliwość odbierania informacji handlowych w postaci newslettera (drugie zdarzenie).

Nasz agregat (User) wyemituje zatem dwa zdarzenia:

SimpleEventStore w podstawowej implementacji (czyli dla MS SQL Server) będzie gwarantował, że zapisane zdarzenia będą mogły być przetwarzane przez subskrybentów zawsze w tej samej kolejności. Nie będzie możliwa sytuacja, że system będzie chciał oznaczać możliwość wysyłania newslettera do niezarejestrowanego jeszcze użytkownika.

Przetwarzanie zdarzeń bez zachowania kolejności budzi wiele problemów. Ten temat powraca jak bumerang na łamach grupy DDD/CQRS/ES.

Wszystko to co powyżej zostało określone w założeniach da się wykonać dzięki temu, że relacyjne bazy danych wspierają transakcje. To bardzo ułatwia. :)

Poniżej przykładowy (oczekiwany) kod obrazujący api SES:

var options = new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted};
using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    var id = SequentalGuid.NewGuid();

    var aggregate = new ShoppingCart();
    aggregate.AddItem(SequentalGuid.NewGuid(), name: "Product 1", quantity: 3);


    var stream = new EventStream(id)

    // Appending events
    stream.Append(aggregate.TakeUncommittedEvents());

    // Adding metadata item (key, value)
    stream.Advanced.AddMetadata("RequestIP", "0.0.0.0");
    stream.Advanced.AddMetadata("User", "John Doe");

    await store.SaveChanges(stream);

    await scope.Complete();
}

A teraz z użyciem Repository, który skrzętnie ukrywa niepotrzebne szczegóły z punktu widzenia obsługi logiki samej aplikacji:

using (var scope = new TransactionScope(TransactionScopeOption.Required, options))
{
    using (var repo = new SourcedRepo<ShoppingCart>(store))
    {
        var aggregate = await repo.Load(id);

        aggregate.AddItem(Guid.NewGuid(), "Product 1", 3);

        await repo.SaveChanges(aggregate);
    }
    scope.Complete();
}

Na potrzeby jednego z projektów popełniłem już podobne rozwiązanie. Niestety wcześniej zbyt mocno wzorowałem się na NEventStore. Nie wpłynęło to dobrze na prawidłową implementację wcześniejszego rozwiązania. Zresztą jeden z commiterów NES także sam zauważył, że NES poszedł w złym kierunku.

Na pierwszy rzut oka można sobie pomyśleć 'to nie będzie działać wydajnie'. Nic bardziej mylnego. W poprzednim rozwiązaniu na moim i5 (dysk SSD) spokojnie dało się wyciągnąć ponad 6000 msg/sec. Jak na mój gust całkiem nieźle.

Komentarze