Introduktion til CQRS og event sourcing #4

Nu hvor vi de foregående tre indlæg har arbejdet os hen til at kunne processere commands, så mangler vi bare det sidste trin i raketten for at kunne slutte cirklen: Views.

Views dannes ud fra events efterhånden som de sker, og med Cirqus foregår dette ved at man installerer en passende IEventDispatcher implementation. En IEventDispatcher er en dims, der får mulighed for at gøre ting ved events efter at de er blevet gemt i vores event store, og dermed blevet en del af den evige og uforanderlige verdenshistorie.

Man kunne forestille sig IEventDispatcher implementationer, der gjorde alle mulige spændende ting, f.eks. replikerede events til andre systemer, sendte events i en message queue i skyen osv., men i dette tilfælde vil vi prøve at danne et simpelt view, som holder styr på hvor lang tid det tager at mase hver rødbede, hvilket også er ret spændende.

Med Cirqus er det ret nemt at lave simple views, der abonnerer på events og gemmer deres tilstand efter hver eneste event de processerer – man starter med at lave en klasse, som skal implementere IViewInstance<>-interfacet, som lukkes med en ViewLocator-implementation, som får betydning for hvor mange instanser, der bliver oprettet af vores view.

I dette tilfælde vil vi bare have en view-instans per rødbede, og vi vælger derfor at lave view-klassen således:

public class TimeToBeCrushedView : IViewInstance<InstancePerAggregateRootLocator>
{
    public string Id { get; set; }
    public long LastGlobalSequenceNumber { get; set; }
}

Nu kan vi så implementere alle de ISubscribeTo<>-interfaces, som vi synes er interessante – i dette tilfælde vil vi gerne gemme tidspunktet for modtagelsen af den første BeetrootSquashed-event og så beregne time-to-be-crushed når vi modtager BeetrootCompletelyCrushed – derfor:

public class TimeToBeCrushedView : IViewInstance<InstancePerAggregateRootLocator>,
    ISubscribeTo<BeetrootSquashed>,
    ISubscribeTo<BeetrootCompletelyCrushed>
{
    public string Id { get; set; }
    public long LastGlobalSequenceNumber { get; set; }

    public void Handle(IViewContext context, BeetrootSquashed e)
    {
    }

    public void Handle(IViewContext context, BeetrootCompletelyCrushed e)
    {
    }
}

Nu skal vi så bare ordne det med tidspunkterne… MEN tid i et event sourcet system er et tricky emne, så vi kan IKKE bare fyre den af med DateTime.Now, for så får vi et andet resultat den dag vi sletter viewet og replayer alle events… det generelle princip med event sourcing er at ALT skal genereres ud fra vores events, så det skal vi også overholde når vi skal regne med tidspunkter.

Heldigvis har DomainEvent-klassen en stribe headers, som bliver sat automatisk i forbindelse med at de bliver emitted, og det inkluderer også UTC-tidspunktet for hvornår det skete. Disse headers kan tilgås via Meta-property’en på DomainEvent-klassen, og til de mest almindelige headers (aggregate root-ID, sekvensnumre og tidspunktet) er der lavet nogle fine extension methods, som gør det nemt – den endelige version af vores view kan altså implementeres således:

public class TimeToBeCrushedView : IViewInstance<InstancePerAggregateRootLocator>,
    ISubscribeTo<BeetrootSquashed>,
    ISubscribeTo<BeetrootCompletelyCrushed>
{
    public string Id { get; set; }
    public long LastGlobalSequenceNumber { get; set; }

    public DateTime TimeOfFirstEvent { get; set; }

    public TimeSpan TimeToBeCrushed { get; set; }

    public void Handle(IViewContext context, BeetrootSquashed e)
    {
        // ignorere event hvis tidspunktet allerede er sat
        if (TimeOfFirstEvent > DateTime.MinValue) return;

        TimeOfFirstEvent = e.GetUtcTime();
    }

    public void Handle(IViewContext context, BeetrootCompletelyCrushed e)
    {
        TimeToBeCrushed = e.GetUtcTime() - TimeOfFirstEvent;
    }
}

Nu har vi kodet vores view – så mangler vi bare at give det liv, og det kan p.t. gøres i hukommelsen (vha. InMemoryViewManager, i SQL Server (vha. MsSqlViewManager) og i MongoDB (vha. MongoDbViewManager).

Vi prøver at bruge SQL Server, ligesom vi også gjorde til vores events – lad os vende tilbage til opsætningen af vores command processor, hvor vi havde fat i en ViewManagerEventDispatcher, som er en IEventDispatcher-implementation, der kan administrere et vilkårligt antal views – den skal vi nu sætte op til at administrere vores TimeToBeCrushedView:

var view = new MsSqlViewManager<TimeToBeCrushedView>("mssql", "TimeToBeCrushed",
                                                     automaticallyCreateSchema: true);
var dispatcher = new ViewManagerEventDispatcher(aggregateRootRepository, eventStore, view);

og dermed vil den fulde opsætning med events og view i SQL Server se således ud:

// ved opstart:
var eventStore = new MsSqlEventStore("mssql", "Events", automaticallyCreateSchema: true);
var repository = new DefaultAggregateRootRepository(eventStore);

var view = new MsSqlViewManager<TimeToBeCrushedView>("mssql", "TimeToBeCrushed",
                                                     automaticallyCreateSchema: true);
var dispatcher = new ViewManagerEventDispatcher(aggregateRootRepository, view);

var commandProcessor = new CommandProcessor(eventStore, repository, dispatcher);

// først....
commandProcessor.Initialize();

// og så i resten af applikationens levetid:
commandProcessor.ProcessCommand(...);

og med det har vi sluttet hele cirklen (= “cirkus” på latin, eller “cirqus” hvis det skal lugte lidt af CQRS) og kan nu knuse massevis af rødbeder og holde styr på hvor lang tid det tager at knuse hver enkelt rødbede.

I næste indlæg vil jeg prøve at snakke lige mere om views – f.eks. vil jeg diskutere synkrone/asynkrone views, replay, osv.

1 comment for “Introduktion til CQRS og event sourcing #4

Skriv et svar til  Introduktion til CQRS og event sourcing #5 | Mogens Heller Grabe Annuller svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *