Skip to content

tthtun/Cirqus

 
 

Repository files navigation

d60 Cirqus

Simple but powerful event sourcing + CQRS kit.

Provides a model for creating commands, aggregate roots, events, and views, tying it all up in one simple and neat CommandProcessor.

How simple?

You do this:

var processor = CommandProcessor.With()
    .(...)
    .Create();

and let the API guide you through selecting an event store, possibly configuring some more stuff as well, and then you can do this:

processor.ProcessCommand(myCommand);

and then everything will flow from there. The command processor is reentrant and is meant to be kept around for the duration of your application's lifetime. Remember to

processor.Dispose();

when the application shuts down.

More docs

Check out the official documentation wiki.

Is Cirqus for you?

It depends ;) to answer that question, it is necessary to think a little bit about your requirements. Cirqus is not in the extreme high performance camp (if there is such a camp), although it doesn't mean that Cirqus cannot perform extremely well - it's more that on the following scale:

Raw performance                  Polished APIs and general usefulness
================================================(+)==================

Cirqus is positioned around the (+) - it doesn't mean that you cannot achieve great - excellent even - performance with Cirqus, it just means that certain choices have probably been made for your with Cirqus that makes certain things easier, even though it may sacrifice a little bit of raw performance.

And on this scale:

Complex event processing        Domain-model-centric event processing
================================================(+)==================

Cirqus is again positioned around the (+) - it is mostly meant to be used in conjunction with an event-driven domain model, so if you're content with driving everything directly off of events, you will probably not benefit as much from using Cirqus.

Again, it's not that Cirqus doesn't do complex event processing - Cirqus' views can easily be brought to do that - it's just that that is not really the focus (at least not with the existing implementations).

Lastly, on this scale:

.NET-centric                                            Interoperable
=================(+)=================================================

it shows that many things are just made extremely easy for you if you're using Cirqus and .NET. You can of course easily set up something else to read events since all the existing event stores are simply using JSON to represent their contents, but from time to time a .NET type name might show up in the JSON data - which means that it's easier to use from .NET, but not impossible to use from something else (e.g. to have node.js-driven projections or whatever).

Configuration example

This is how you can set up a fully functioning command processor, including a view:

// configure one single view manager
var viewManager = new MsSqlViewManager<CounterView>("sqltestdb");

// let's create & initialize the command processor
var processor = CommandProcessor.With()
    .EventStore(e => e.UseSqlServer("sqltestdb", "Events"))
    .EventDispatcher(e => e.UseViewManagerEventDispatcher(viewManager))
    .Create();

// use the command processor, possibly from multiple threads,
// for the entire lifetime of your application....

// and then, when your application shuts down:
processor.Dispose();

Elaborate configuration example

If you're interested in seeing which moving parts are involved in the command processor, here's the equivalent configuration where all the things are wired together manually. As you can see, it's actually fairly simple (although the configuration API is much more intuitive and concise).

// this is the origin of truth - let's keep it in SQL Server!
var eventStore = new MsSqlEventStore("sqltestdb", "Events", 
                                     automaticallyCreateSchema: true);

// aggregate roots are simply built when needed by replaying events
// for the requested root
var repository = new DefaultAggregateRootRepository(eventStore);

// configure one single view manager in another table in our SQL Server
var viewManager = new MsSqlViewManager<CounterView>("sqltestdb", "CounterView", 
                                                    automaticallyCreateSchema: true);

// Cirqus will deliver emitted events to the event dispatcher when they have
//  been persisted
var eventDispatcher = new ViewManagerEventDispatcher(repository, eventStore, viewManager);

// we can create the processor now
var processor = new CommandProcessor(eventStore, repository, eventDispatcher);

// and then, when your application shuts down:
processor.Dispose();

Code example

This is an example of a command whose purpose it is to instruct the Counter aggregate root to increment itself by some specific value, as indicated by the given delta parameter:

public class IncrementCounter : Command<Counter>
{
    public IncrementCounter(string aggregateRootId, int delta)
        : base(aggregateRootId)
    {
        Delta = delta;
    }

    public int Delta { get; private set; }

    public override void Execute(Counter aggregateRoot)
    {
        aggregateRoot.Increment(Delta);
    }
}

Note how the command indicates the type and ID of the aggregate root to address, as well as an Execute method that will be invoked by the framework. Let's take a look at Counter - aggregate roots must be based on the AggregateRoot base class and must of course follow the emit/apply pattern for mutating themselves - it looks like this:

public class Counter : AggregateRoot, IEmit<CounterIncremented>
{
    int _currentValue;

    public void Increment(int delta)
    {
        Emit(new CounterIncremented(delta));
    }

    public void Apply(CounterIncremented e)
    {
        _currentValue += e.Delta;
    }

    public int CurrentValue
    {
        get { return _currentValue; }
    }

    public double GetSecretBizValue()
    {
        return CurrentValue%2 == 0
            ? Math.PI
            : CurrentValue;
    }
}

As you can see, the command's Execute method will invoke the Increment(delta) method on the root, which in turn will emit a CounterIncremented event, which simply looks like this:

public class CounterIncremented : DomainEvent<Counter>
{
    public CounterIncremented(int delta)
    {
        Delta = delta;
    }

    public int Delta { get; private set; }
}

The event is immediately applied (via the root's Apply method that comes from implementing IEmit<CounterIncremented>), and this is the place where the root is free to mutate itself - in this case, we increment the private _currentValue variable, which serves to demonstrate that aggregate roots are free to keep their privates private.

Note also how the aggregate root is capable of calculating a secret business value, which happens to alternate between the counter's value and π, depending on whether the counter's value is odd or even.

Lastly, we have set up a MsSqlViewManager that operates on a CounterView that looks like this:

public class CounterView : IViewInstance<InstancePerAggregateRootLocator>,
    ISubscribeTo<CounterIncremented>
{
    public CounterView()
    {
        SomeRecentBizValues = new List<double>();
    }

    public string Id { get; set; }

    public long LastGlobalSequenceNumber { get; set; }

    public int CurrentValue { get; set; }

    public double SecretBizValue { get; set; }

    public List<double> SomeRecentBizValues { get; set; }

    public void Handle(IViewContext context, CounterIncremented domainEvent)
    {
        CurrentValue += domainEvent.Delta;

        var id = domainEvent.GetAggregateRootId();
        var version = domainEvent.GetGlobalSequenceNumber();
        var counter = context.Load<Counter>(id, version);

        SecretBizValue = counter.GetSecretBizValue();

        SomeRecentBizValues.Add(SecretBizValue);

        // trim to 10 most recent biz values
        while(SomeRecentBizValues.Count > 10) 
            SomeRecentBizValues.RemoveAt(0);
    }
}

Views can define how processed events are mapped to view IDs via the ViewLocator implementation that closes the IViewInstance<> interface - in this case, we're using InstancePerAggregateRootLocator which means that the aggregate root ID of the processed event is simply used as the ID of the view, in turn resulting in one instance of the view per aggregate root.

In order to actually get to receive events, the view class must implement one or more ISubscribeTo<> interfaces - in this case, we subscribe to CounterIncremented which requires that we implement the Handle method.

In addition to the two required properties, Id and LastGLobalSequenceNumber, we've added a property for the current value of the counter (CurrentValue), a property for the secret business value (SecretBizValue), and a list that can contain the 10 most recent business values (SomeRecentBizValues).

Note how the IViewContext gives access to a Load method that can be used by the view to load aggregate roots if it needs to invoke domain logic to extract certain values out of the domain (like e.g. our secret business value).

Note also how an aggregate root must be loaded by specifying a global sequence number which will serve as a "roof" for applied event, thus ensuring that the loaded aggregate root has the version that corresponds to the time when the event was emitted, thus allowing for eternally consistent replay of events. It also allows for peeking back and forth in time, but that's a story for another time... ;)

About

d60 event sourcing + CQRS framework

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%