diff --git a/src/Marten.Testing/Events/CustomAggregatorLookupTests.cs b/src/Marten.Testing/Events/CustomAggregatorLookupTests.cs new file mode 100644 index 0000000000..5dd72e252c --- /dev/null +++ b/src/Marten.Testing/Events/CustomAggregatorLookupTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Baseline; +using Marten.Events; +using Marten.Events.Projections; +using Marten.Util; +using Shouldly; +using Xunit; + +namespace Marten.Testing.Events +{ + public class CustomAggregatorLookupTests + { + private readonly EventGraph theGraph = new EventGraph(new StoreOptions()); + + public CustomAggregatorLookupTests() + { + theGraph.UseAggregatorLookup(new AggregatorLookup(type => typeof(AggregatorUsePrivateApply<>).CloseAndBuildAs(type))); + } + + [Fact] + public void can_lookup_private_apply_methods() + { + var aggregator = theGraph.AggregateFor(); + + var stream = new EventStream(Guid.NewGuid(), false) + .Add(new QuestStarted {Name = "Destroy the Ring"}); + + var party = aggregator.Build(stream.Events, null); + + party.Name.ShouldBe("Destroy the Ring"); + } + } + + public class AggregatorUsePrivateApply : IAggregator where T : class, new() + { + public static readonly string ApplyMethod = "Apply"; + + private readonly IDictionary _aggregations = new Dictionary(); + + + public AggregatorUsePrivateApply() + { + typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .Where(x => x.Name == ApplyMethod && x.GetParameters().Length == 1) + .Each(method => + { + var eventType = method.GetParameters().Single().ParameterType; + var step = typeof(AggregationStep<,>) + .CloseAndBuildAs(method, typeof(T), eventType); + + _aggregations.Add(eventType, step); + }); + + Alias = typeof(T).Name.ToTableAlias(); + } + + public Type AggregateType => typeof(T); + + public string Alias { get; } + + public T Build(IEnumerable events, IDocumentSession session) + { + var state = new T(); + + events.Each(x => x.Apply(state, this)); + + return state; + } + + public Type[] EventTypes => _aggregations.Keys.ToArray(); + + public AggregatorUsePrivateApply Add(IAggregation aggregation) + { + if (_aggregations.ContainsKey(typeof(TEvent))) + { + _aggregations[typeof(TEvent)] = aggregation; + } + else + { + _aggregations.Add(typeof(TEvent), aggregation); + } + + return this; + } + + public AggregatorUsePrivateApply Add(Action application) + { + return Add(new AggregationStep(application)); + } + + public IAggregation AggregatorFor() + { + return _aggregations.ContainsKey(typeof(TEvent)) + ? _aggregations[typeof(TEvent)].As>() + : null; + } + + + public bool AppliesTo(EventStream stream) + { + return stream.Events.Any(x => _aggregations.ContainsKey(x.Data.GetType())); + } + } + + public class AggregateWithPrivateEventApply + { + public Guid Id { get; set; } + + private void Apply(QuestStarted started) + { + Name = started.Name; + } + + public string Name { get; private set; } + } +} \ No newline at end of file diff --git a/src/Marten/Events/EventGraph.cs b/src/Marten/Events/EventGraph.cs index b4a3a455a4..cbd499f7ce 100644 --- a/src/Marten/Events/EventGraph.cs +++ b/src/Marten/Events/EventGraph.cs @@ -19,12 +19,13 @@ public class EventGraph private readonly ConcurrentCache _byEventName = new ConcurrentCache(); private readonly ConcurrentCache _events = new ConcurrentCache(); - + private IAggregatorLookup _aggregatorLookup; private string _databaseSchemaName; public EventGraph(StoreOptions options) { Options = options; + _aggregatorLookup = new AggregatorLookup(); _events.OnMissing = eventType => { var mapping = typeof(EventMapping<>).CloseAndBuildAs(this, eventType); @@ -38,7 +39,7 @@ public EventGraph(StoreOptions options) SchemaObjects = new EventStoreDatabaseObjects(this); InlineProjections = new ProjectionCollection(options); - AsyncProjections = new ProjectionCollection(options); + AsyncProjections = new ProjectionCollection(options); } internal StoreOptions Options { get; } @@ -104,7 +105,7 @@ public string DatabaseSchemaName .GetOrAdd(typeof(T), type => { Options.MappingFor(typeof(T)); - return new Aggregator(); + return _aggregatorLookup.Lookup(); }) .As>(); } @@ -124,12 +125,21 @@ public Type AggregateTypeFor(string aggregateTypeName) public string AggregateAliasFor(Type aggregateType) { return _aggregates - .GetOrAdd(aggregateType, type => typeof(Aggregator<>).CloseAndBuildAs(aggregateType)).Alias; + .GetOrAdd(aggregateType, type => _aggregatorLookup.Lookup(type)).Alias; } public IProjection ProjectionFor(Type viewType) { return AsyncProjections.ForView(viewType) ?? InlineProjections.ForView(viewType); } + + /// + /// Set default strategy to lookup IAggregator when no explicit IAggregator registration exists. + /// + /// Unless called, is used + public void UseAggregatorLookup(IAggregatorLookup aggregatorLookup) + { + _aggregatorLookup = aggregatorLookup; + } } } \ No newline at end of file diff --git a/src/Marten/Events/Projections/AggregatorLookup.cs b/src/Marten/Events/Projections/AggregatorLookup.cs new file mode 100644 index 0000000000..57541401fe --- /dev/null +++ b/src/Marten/Events/Projections/AggregatorLookup.cs @@ -0,0 +1,30 @@ +using System; +using Baseline; + +namespace Marten.Events.Projections +{ + /// + /// Default IAggregator lookup strategy. Defaults to + /// + public sealed class AggregatorLookup : IAggregatorLookup + { + private readonly Func factory; + + /// Factory for resolving IAggregator for the supplied type + public AggregatorLookup(Func factory = null) + { + this.factory = factory; + } + + public IAggregator Lookup() where T : class, new() + { + // trade null check for the cost of using default factory with Activator.CreateInstance + return (IAggregator)factory?.Invoke(typeof(T)) ?? new Aggregator(); + } + + public IAggregator Lookup(Type aggregateType) + { + return factory?.Invoke(aggregateType) ?? typeof(Aggregator<>).CloseAndBuildAs(aggregateType); + } + } +} \ No newline at end of file diff --git a/src/Marten/Events/Projections/IAggregatorLookup.cs b/src/Marten/Events/Projections/IAggregatorLookup.cs new file mode 100644 index 0000000000..c36b294b14 --- /dev/null +++ b/src/Marten/Events/Projections/IAggregatorLookup.cs @@ -0,0 +1,19 @@ +using System; + +namespace Marten.Events.Projections +{ + /// + /// Used by to resolve IAggregator when no explicit IAggregator registration exists + /// + public interface IAggregatorLookup + { + /// + /// Resolve aggregator for T + /// + IAggregator Lookup() where T : class, new(); + /// + /// Resolve aggregator for aggregateType + /// + IAggregator Lookup(Type aggregateType); + } +} \ No newline at end of file