Fork me on GitHub

Immutable projections as read model Edit on GitHub


This use case demonstrates how to create immutable projections from event streams.

Scenario

To make projections immutable, the event application methods invoked by aggregators need to be made private, as well as any property setters.


public class AggregateWithPrivateEventApply
{
    public Guid Id { get; private set; }

    private void Apply(QuestStarted started)
    {
        Name = started.Name;
    }

    public void Apply(QuestEnded ended)
    {
        Name = ended.Name;
    }

    public string Name { get; private set; }
}


To run aggregators against such projections, aggregator lookup strategy is configured to use aggregators that look for private Apply([Event Type]) methods. Furthermore, document deserialization is configured to look for private property setters, allowing hydration of the projected objects from the database.

This can be done in the store configuration as follows:


var serializer = new JsonNetSerializer();
serializer.Customize(c => c.ContractResolver = new ResolvePrivateSetters());
options.Serializer(serializer);
options.Events.UseAggregatorLookup(AggregationLookupStrategy.UsePrivateApply);
options.Events.InlineProjections.AggregateStreamsWith<AggregateWithPrivateEventApply>();

The serializer contract applied customises the default behaviour of the Json.NET serializer:


internal class ResolvePrivateSetters: DefaultContractResolver
{
    protected override JsonProperty CreateProperty(
        MemberInfo member,
        MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);

        if (!prop.Writable)
        {
            var property = member as PropertyInfo;
            if (property != null)
            {
                var hasPrivateSetter = property.GetSetMethod(true) != null;
                prop.Writable = hasPrivateSetter;
            }
        }

        return prop;
    }
}

Given the setup, a stream can now be projected using `AggregateWithPrivateEventApply` shown above. Furthermore, the created projection can be hydrated from the document store:

var quest = new QuestStarted { Name = "Destroy the Ring" };
var questId = Guid.NewGuid();
theSession.Events.StartStream<QuestParty>(questId, quest);
theSession.SaveChanges();

var projection = theSession.Load<AggregateWithPrivateEventApply>(questId);
projection.Name.ShouldBe("Destroy the Ring");