Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion documentation/documentation/events/projections/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ At this point, you would be able to query against `QuestParty` as just another d


`EventGraph.UseAggregatorLookup(IAggregatorLookup aggregatorLookup)` can be used to register an `IAggregatorLookup` that is used to look up `IAggregator<T>` for aggregations. This allows e.g. for generic aggregation strategy to be used, rathen than registering aggregators
case-by-case through `EventGraphAddAggregator<T>(IAggregator<T> aggregator)`.
case-by-case through `EventGraphAddAggregator<T>(IAggregator<T> aggregator)`.

A shorthand extension method `EventGraph.UseAggregatorLookup(this EventGraph eventGraph, AggregationLookupStrategy strategy)` can be used to set default aggregation lookup, whereby

- `AggregationLookupStrategy.UsePublicApply` resolves aggregators that use public Apply
- `AggregationLookupStrategy.UsePrivateApply` resolves aggregators that use private Apply

<[sample:register-custom-aggregator-lookup]>
2 changes: 1 addition & 1 deletion documentation/documentation/order.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ cli
documents
events
precompiling

scenarios
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--Title: Immutable projections as read model-->

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.

<[sample:scenarios-immutableprojections-projection]>

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:

<[sample:scenarios-immutableprojections-storesetup]>

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

<[sample:scenarios-immutableprojections-serializer]>

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

<[sample:scenarios-immutableprojections-projectstream]>
8 changes: 8 additions & 0 deletions documentation/documentation/scenarios/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!--Title:Scenarios-->
<!--Url:scenarios-->

This page documents various use cases with a sample implementation using Marten.

## Scenarios

<[TableOfContents]>
147 changes: 82 additions & 65 deletions src/Marten.Testing/Events/CustomAggregatorLookupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,39 @@
using Baseline;
using Marten.Events;
using Marten.Events.Projections;
using Marten.Services;
using Marten.Services.Events;
using Marten.Testing.Events.Projections;
using Marten.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Shouldly;
using Xunit;

namespace Marten.Testing.Events
{
public class CustomAggregatorLookupTests
{
private readonly EventGraph theGraph = new EventGraph(new StoreOptions());

public class CustomAggregatorLookupTests : DocumentSessionFixture<NulloIdentityMap>
{
public CustomAggregatorLookupTests()
{
// SAMPLE: register-custom-aggregator-lookup
// Registering an aggregator lookup that provides aggregator supporting private Apply([Event Type]) methods
theGraph.UseAggregatorLookup(new AggregatorLookup(type => typeof(AggregatorUsePrivateApply<>).CloseAndBuildAs<IAggregator>(type)));
// ENDSAMPLE
{
StoreOptions(options =>
{
// SAMPLE: scenarios-immutableprojections-storesetup
var serializer = new JsonNetSerializer();
serializer.Customize(c => c.ContractResolver = new ResolvePrivateSetters());
options.Serializer(serializer);
options.Events.UseAggregatorLookup(AggregationLookupStrategy.UsePrivateApply);
options.Events.InlineProjections.AggregateStreamsWith<AggregateWithPrivateEventApply>();
// ENDSAMPLE
});
}

[Fact]
public void can_lookup_private_apply_methods()
{
var theGraph = new EventGraph(new StoreOptions());
theGraph.UseAggregatorLookup(new AggregatorLookup(type => typeof(AggregatorApplyPrivate<>).CloseAndBuildAs<IAggregator>(type)));

var aggregator = theGraph.AggregateFor<AggregateWithPrivateEventApply>();

var stream = new EventStream(Guid.NewGuid(), false)
Expand All @@ -34,83 +46,63 @@ public void can_lookup_private_apply_methods()
var party = aggregator.Build(stream.Events, null);

party.Name.ShouldBe("Destroy the Ring");
}
}

public class AggregatorUsePrivateApply<T> : IAggregator<T> where T : class, new()
{
public static readonly string ApplyMethod = "Apply";

private readonly IDictionary<Type, object> _aggregations = new Dictionary<Type, object>();
}


public AggregatorUsePrivateApply()
[Fact]
public void can_set_private_apply_aggregator_through_extension_methods_and_strategy()
{
typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(x => x.Name == ApplyMethod && x.GetParameters().Length == 1)
.Each(method =>
{
var eventType = method.GetParameters().Single<ParameterInfo>().ParameterType;
var step = typeof(AggregationStep<,>)
.CloseAndBuildAs<object>(method, typeof(T), eventType);

_aggregations.Add(eventType, step);
});

Alias = typeof(T).Name.ToTableAlias();
}

public Type AggregateType => typeof(T);
var theGraph = new EventGraph(new StoreOptions());
// SAMPLE: register-custom-aggregator-lookup
// Registering an aggregator lookup that provides aggregator supporting private Apply([Event Type]) methods
theGraph.UseAggregatorLookup(AggregationLookupStrategy.UsePrivateApply);
// ENDSAMPLE

public string Alias { get; }
var aggregator = theGraph.AggregateFor<AggregateWithPrivateEventApply>();

public T Build(IEnumerable<IEvent> events, IDocumentSession session)
{
var state = new T();
var stream = new EventStream(Guid.NewGuid(), false)
.Add(new QuestStarted { Name = "Destroy the Ring" });

events.Each(x => x.Apply(state, this));
var party = aggregator.Build(stream.Events, null);

return state;
party.Name.ShouldBe("Destroy the Ring");
}

public Type[] EventTypes => _aggregations.Keys.ToArray();

public AggregatorUsePrivateApply<T> Add<TEvent>(IAggregation<T, TEvent> aggregation)
[Fact]
public void can_set_aggregator_through_extension_methods_and_strategy()
{
if (_aggregations.ContainsKey(typeof(TEvent)))
{
_aggregations[typeof(TEvent)] = aggregation;
}
else
{
_aggregations.Add(typeof(TEvent), aggregation);
}
var theGraph = new EventGraph(new StoreOptions());
theGraph.UseAggregatorLookup(AggregationLookupStrategy.UsePublicApply);

return this;
}
var aggregator = theGraph.AggregateFor<QuestParty>();

public AggregatorUsePrivateApply<T> Add<TEvent>(Action<T, TEvent> application)
{
return Add(new AggregationStep<T, TEvent>(application));
}
var stream = new EventStream(Guid.NewGuid(), false)
.Add(new QuestStarted { Name = "Destroy the Ring" });

public IAggregation<T, TEvent> AggregatorFor<TEvent>()
{
return _aggregations.ContainsKey(typeof(TEvent))
? _aggregations[typeof(TEvent)].As<IAggregation<T, TEvent>>()
: null;
}
var party = aggregator.Build(stream.Events, null);

party.Name.ShouldBe("Destroy the Ring");
}

public bool AppliesTo(EventStream stream)
[Fact]
public void can_use_custom_aggregator_with_inline_projection()
{
return stream.Events.Any(x => _aggregations.ContainsKey(x.Data.GetType()));
// SAMPLE: scenarios-immutableprojections-projectstream
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");
// ENDSAMPLE
}
}

// SAMPLE: scenarios-immutableprojections-projection
public class AggregateWithPrivateEventApply
{
public Guid Id { get; set; }
public Guid Id { get; private set; }

private void Apply(QuestStarted started)
{
Expand All @@ -119,4 +111,29 @@ private void Apply(QuestStarted started)

public string Name { get; private set; }
}
// ENDSAMPLE

// SAMPLE: scenarios-immutableprojections-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;
}
}
// ENDSAMPLE
}
23 changes: 23 additions & 0 deletions src/Marten/Events/EventGraphExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Marten.Events.Projections;
using Marten.Services.Events;
using Baseline;

namespace Marten.Events
{
public static class EventGraphExtensions
{
public static EventGraph UseAggregatorLookup(this EventGraph eventGraph, AggregationLookupStrategy strategy)
{
if (strategy == AggregationLookupStrategy.UsePublicApply)
{
eventGraph.UseAggregatorLookup(new AggregatorLookup(type => typeof(Aggregator<>).CloseAndBuildAs<IAggregator>(type)));
}
else if (strategy == AggregationLookupStrategy.UsePrivateApply)
{
eventGraph.UseAggregatorLookup(new AggregatorLookup(type => typeof(AggregatorApplyPrivate<>).CloseAndBuildAs<IAggregator>(type)));
}

return eventGraph;
}
}
}
23 changes: 13 additions & 10 deletions src/Marten/Events/Projections/Aggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ namespace Marten.Events.Projections
private readonly IDictionary<Type, object> _aggregations = new Dictionary<Type, object>();


public Aggregator()
public Aggregator() : this(typeof(T).GetMethods()
.Where(x => x.Name == ApplyMethod && x.GetParameters().Length == 1))
{
Alias = typeof(T).Name.ToTableAlias();
}

protected Aggregator(IEnumerable<MethodInfo> overrideMethodLookup)
{
typeof (T).GetMethods()
.Where(x => x.Name == ApplyMethod && x.GetParameters().Length == 1)
overrideMethodLookup
.Each(method =>
{
object step = null;
Expand All @@ -36,11 +41,9 @@ public Aggregator()

_aggregations.Add(eventType, step);
});

Alias = typeof (T).Name.ToTableAlias();
}

public Type AggregateType => typeof (T);
public Type AggregateType => typeof(T);

public string Alias { get; }

Expand All @@ -57,9 +60,9 @@ public T Build(IEnumerable<IEvent> events, IDocumentSession session)

public Aggregator<T> Add<TEvent>(IAggregation<T, TEvent> aggregation)
{
if (_aggregations.ContainsKey(typeof (TEvent)))
if (_aggregations.ContainsKey(typeof(TEvent)))
{
_aggregations[typeof (TEvent)] = aggregation;
_aggregations[typeof(TEvent)] = aggregation;
}
else
{
Expand All @@ -76,8 +79,8 @@ public Aggregator<T> Add<TEvent>(Action<T, TEvent> application)

public IAggregation<T, TEvent> AggregatorFor<TEvent>()
{
return _aggregations.ContainsKey(typeof (TEvent))
? _aggregations[typeof (TEvent)].As<IAggregation<T, TEvent>>()
return _aggregations.ContainsKey(typeof(TEvent))
? _aggregations[typeof(TEvent)].As<IAggregation<T, TEvent>>()
: null;
}

Expand Down
16 changes: 16 additions & 0 deletions src/Marten/Events/Projections/AggregatorApplyPrivate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Linq;
using System.Reflection;

namespace Marten.Events.Projections
{
/// <summary>
/// Customize behaviour of <see cref="Aggregator{T}" /> by using private Apply methods in aggregation.
/// </summary>
public class AggregatorApplyPrivate<T> : Aggregator<T> where T : class, new()
{
public AggregatorApplyPrivate() : base(typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(x => x.Name == ApplyMethod && x.GetParameters().Length == 1))
{
}
}
}
4 changes: 2 additions & 2 deletions src/Marten/Events/Projections/ProjectionCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ IEnumerator IEnumerable.GetEnumerator()
}

public AggregationProjection<T> AggregateStreamsWith<T>() where T : class, new()
{
var aggregator = new Aggregator<T>();
{
var aggregator = _options.Events.AggregateFor<T>();
var finder = new AggregateFinder<T>();
var projection = new AggregationProjection<T>(finder, aggregator);

Expand Down
Loading