diff --git a/.vs/LiquidProjections/v15/sqlite3/storage.ide b/.vs/LiquidProjections/v15/sqlite3/storage.ide index 8a135bb..4cd3f7b 100644 Binary files a/.vs/LiquidProjections/v15/sqlite3/storage.ide and b/.vs/LiquidProjections/v15/sqlite3/storage.ide differ diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..f77567e --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,6 @@ +branches: + (pull|pull\-requests|pr)[/-]: + mode: ContinuousDeployment + tag: pr +ignore: + sha: [] diff --git a/Samples/ExampleHost/ExampleProjector.cs b/Samples/ExampleHost/ExampleProjector.cs index 7b4855f..4806da2 100644 --- a/Samples/ExampleHost/ExampleProjector.cs +++ b/Samples/ExampleHost/ExampleProjector.cs @@ -27,32 +27,34 @@ public ExampleProjector(IEventMapBuilder private IEventMap BuildMapFrom(IEventMapBuilder mapBuilder) { - mapBuilder.HandleProjectionModificationsAs((key, context, projector, options) => + return mapBuilder.Build(new ProjectorMap { - TProjection projection = store.GetRepository().Find(key); - if (projection == null) + Create = async (key, context, projector, shouldOverride) => { - projection = new TProjection() + var projection = new TProjection() { Id = key }; - store.Add(projection); - } + await projector(projection); - return projector(projection); - }); + store.Add(projection); + }, + Update = async (key, context, projector, createIfMissing) => + { + TProjection projection = store.GetRepository().Find(key); + await projector(projection); - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - store.GetRepository().RemoveByKey(key); + store.Add(projection); + }, + Delete = (key, context) => + { + store.GetRepository().RemoveByKey(key); - return Task.FromResult(0); + return Task.FromResult(true); + }, + Custom = (context, projector) => projector() }); - - mapBuilder.HandleCustomActionsAs((context, projector) => projector()); - - return mapBuilder.Build(); } public async Task Handle(IReadOnlyList transactions) diff --git a/Src/LiquidProjections/EventMap.cs b/Src/LiquidProjections/EventMap.cs index a387248..5a205a8 100644 --- a/Src/LiquidProjections/EventMap.cs +++ b/Src/LiquidProjections/EventMap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace LiquidProjections @@ -10,8 +11,8 @@ namespace LiquidProjections public class EventMap : IEventMap { private readonly Dictionary> mappings = new Dictionary>(); - - internal CustomHandler Do { get; set; } + private readonly List>> filters = + new List>>(); internal void Add(Func action) { @@ -23,10 +24,12 @@ internal void Add(Func action) mappings[typeof(TEvent)].Add((@event, context) => action((TEvent)@event, context)); } - /// - /// Handles asynchronously using context . - /// - public async Task Handle(object anEvent, TContext context) + internal void AddFilter(Func> filter) + { + filters.Add(filter); + } + + public async Task Handle(object anEvent, TContext context) { if (anEvent == null) { @@ -38,17 +41,36 @@ public async Task Handle(object anEvent, TContext context) throw new ArgumentNullException(nameof(context)); } - Type key = anEvent.GetType(); - - List handlers; - - if (mappings.TryGetValue(key, out handlers)) + if (await PassesFilter(anEvent, context)) { - foreach (Handler handler in handlers) + Type key = anEvent.GetType(); + + if (mappings.TryGetValue(key, out var handlers)) { - await handler(anEvent, context); + foreach (Handler handler in handlers) + { + await handler(anEvent, context); + } + + return true; } } + + return false; + } + + private async Task PassesFilter(object anEvent, TContext context) + { + if (filters.Count > 0) + { + bool[] results = await Task.WhenAll(filters.Select(filter => filter(anEvent, context))); + + return results.All(x => x); + } + else + { + return true; + } } private delegate Task Handler(object @event, TContext context); diff --git a/Src/LiquidProjections/EventMapBuilder.cs b/Src/LiquidProjections/EventMapBuilder.cs index 8890bae..7fcaa75 100644 --- a/Src/LiquidProjections/EventMapBuilder.cs +++ b/Src/LiquidProjections/EventMapBuilder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; - using LiquidProjections.MapBuilding; namespace LiquidProjections @@ -12,98 +11,83 @@ namespace LiquidProjections public sealed class EventMapBuilder : IEventMapBuilder { private readonly EventMap eventMap = new EventMap(); - private bool isBuilt; - private CustomHandler customHandler; + private ProjectorMap projector; /// - /// Starts configuring a new handler for events of type . + /// Ensures that only events matching the predicate are processed. /// - /// - /// that allows to continue configuring the handler. - /// - public IEventMappingBuilder Map() + public EventMapBuilder Where(Func> filter) { - AssertNotBuilt(); - - return new EventMappingBuilder(this); + eventMap.AddFilter(filter); + return this; } /// - /// Builds the resulting event map. Can only be called once. - /// No changes can be made after the event map has been built. + /// Starts configuring a new handler for events of type . /// - public IEventMap Build() + /// + /// that allows to continue configuring the handler. + /// + public IAction Map() { AssertNotBuilt(); - AssertComplete(); - isBuilt = true; - return eventMap; + return new Action(this, () => projector); } /// - /// Configures the event map to handle custom actions via the provided delegate . + /// Builds the resulting event map. /// - public void HandleCustomActionsAs(CustomHandler handler) + /// + /// Can only be called once. + /// No changes can be made after the event map has been built. + /// + /// + /// Contains the handler that a projector needs to support to handle events from this map. + /// + public IEventMap Build(ProjectorMap projector) { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + AssertNotBuilt(); - if (customHandler != null) + if (projector == null) { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleCustomActionsAs)} was already called."); + throw new ArgumentNullException(nameof(projector)); } - AssertNotBuilt(); - - customHandler = handler; - } - - internal void Add(Func action) - { - if (action == null) + if (projector.Custom == null) { - throw new ArgumentNullException(nameof(action)); + throw new ArgumentException( + $"Expected the Custom property to point to a valid instance of {nameof(CustomHandler)}", nameof(projector)); } - AssertNotBuilt(); + this.projector = projector; - eventMap.Add(action); + return eventMap; } - internal void AssertNotBuilt() + private void AssertNotBuilt() { - if (isBuilt) + if (projector != null) { throw new InvalidOperationException("The event map has already been built."); } } - private void AssertComplete() - { - if (customHandler == null) - { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleCustomActionsAs)} was not called."); - } - } - - internal sealed class EventMappingBuilder : IEventMappingBuilder + private sealed class Action : IAction { - private readonly EventMapBuilder eventMapBuilder; + private readonly EventMapBuilder parent; + private readonly Func> getProjector; private readonly List>> predicates = new List>>(); - public EventMappingBuilder(EventMapBuilder eventMapBuilder) + public Action(EventMapBuilder parent, Func> getProjector) { - this.eventMapBuilder = eventMapBuilder; + this.parent = parent; + this.getProjector = getProjector; } - public IEventMappingBuilder When(Func> predicate) + public IAction When(Func> predicate) { if (predicate == null) { @@ -121,12 +105,12 @@ public void As(Func action) throw new ArgumentNullException(nameof(action)); } - Add((anEvent, context) => eventMapBuilder.customHandler(context, async () => await action(anEvent, context))); + Add((anEvent, context) => getProjector().Custom(context, async () => await action(anEvent, context))); } - internal void Add(Func action) + private void Add(Func action) { - eventMapBuilder.Add(async (anEvent, context) => + parent.eventMap.Add(async (anEvent, context) => { foreach (Func> predicate in predicates) { @@ -150,204 +134,122 @@ internal void Add(Func action) public sealed class EventMapBuilder : IEventMapBuilder { private readonly EventMapBuilder innerBuilder = new EventMapBuilder(); - private ProjectionModificationHandler projectionModificationHandler; - private ProjectionDeletionHandler projectionDeletionHandler; - - /// - /// Starts configuring a new handler for events of type . - /// - /// - /// that allows to continue configuring the handler. - /// - public IEventMappingBuilder Map() - { - innerBuilder.AssertNotBuilt(); - - return new ProjectionEventMappingBuilder(this); - } + private ProjectorMap projector; + /// - /// Builds the resulting event map. Can only be called once. - /// No changes can be made after the event map has been built. - /// - public IEventMap Build() - { - innerBuilder.AssertNotBuilt(); - AssertComplete(); - - return innerBuilder.Build(); - } - - /// - /// Configures the event map to handle custom actions via the provided delegate . + /// Ensures that only events matching the predicate are processed. /// - public void HandleCustomActionsAs(CustomHandler handler) + public EventMapBuilder Where(Func> predicate) { - innerBuilder.HandleCustomActionsAs(handler); + innerBuilder.Where(predicate); + return this; } - + /// - /// Configures the event map to handle projection creation and updating - /// via the provided delegate . + /// Starts configuring a new handler for events of type . /// - public void HandleProjectionModificationsAs(ProjectionModificationHandler handler) + /// + /// that allows to continue configuring the handler. + /// + public ICrudAction Map() { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } - - if (projectionModificationHandler != null) - { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleProjectionModificationsAs)} " + - "was already called."); - } - - innerBuilder.AssertNotBuilt(); - - projectionModificationHandler = handler; + return new CrudAction(this); } /// - /// Configures the event map to handle projection deletion - /// via the provided delegate . + /// Builds the resulting event map. /// - public void HandleProjectionDeletionsAs(ProjectionDeletionHandler handler) - { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } - - if (projectionDeletionHandler != null) - { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleProjectionDeletionsAs)} was already called."); - } - - innerBuilder.AssertNotBuilt(); - - projectionDeletionHandler = handler; - } - - private void AssertComplete() + /// + /// Can only be called once. + /// No changes can be made after the event map has been built. + /// + /// + /// Contains the create, update, delete and custom handlers that a projector needs to support to handle events from this map. + /// + public IEventMap Build(ProjectorMap projector) { - if (projectionModificationHandler == null) - { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleProjectionModificationsAs)} was not called."); - } - - if (projectionDeletionHandler == null) + this.projector = projector; + return innerBuilder.Build(new ProjectorMap { - throw new InvalidOperationException( - $"{nameof(IEventMapBuilder.HandleProjectionDeletionsAs)} was not called."); - } + Custom = (context, projectEvent) => projectEvent() + }); } - private sealed class ProjectionEventMappingBuilder : IEventMappingBuilder + private sealed class CrudAction : ICrudAction { - private static readonly ProjectionDeletionOptions optionsForDelete = - new ProjectionDeletionOptions(MissingProjectionDeletionBehavior.Throw); + private readonly IAction actionBuilder; + private readonly Func > getProjector; - private static readonly ProjectionDeletionOptions optionsForDeleteIfExists = - new ProjectionDeletionOptions(MissingProjectionDeletionBehavior.Ignore); - - private readonly EventMapBuilder.EventMappingBuilder innerBuilder; - private readonly EventMapBuilder eventMapBuilder; - - public ProjectionEventMappingBuilder(EventMapBuilder eventMapBuilder) + public CrudAction(EventMapBuilder parent) { - innerBuilder = new EventMapBuilder.EventMappingBuilder(eventMapBuilder.innerBuilder); - this.eventMapBuilder = eventMapBuilder; + actionBuilder = parent.innerBuilder.Map(); + getProjector = () => parent.projector; } - public ICreateEventActionBuilder AsCreateOf(Func getKey) + public ICreateAction AsCreateOf(Func getKey) { if (getKey == null) { throw new ArgumentNullException(nameof(getKey)); } - return new CreateEventActionBuilder(this, getKey); + return new CreateAction(actionBuilder, getProjector, getKey); } - public ICreateIfDoesNotExistEventActionBuilder AsCreateIfDoesNotExistOf( + public ICreateAction AsCreateIfDoesNotExistOf( Func getKey) { - if (getKey == null) - { - throw new ArgumentNullException(nameof(getKey)); - } - - return new CreateIfDoesNotExistEventActionBuilder(this, getKey); + return AsCreateOf(getKey).IgnoringDuplicates(); } - public ICreateOrUpdateEventActionBuilder AsCreateOrUpdateOf(Func getKey) + public ICreateAction AsCreateOrUpdateOf(Func getKey) { - if (getKey == null) - { - throw new ArgumentNullException(nameof(getKey)); - } - - return new CreateOrUpdateEventActionBuilder(this, getKey); + return AsCreateOf(getKey).OverwritingDuplicates(); } - public void AsDeleteOf(Func getKey) + public IDeleteAction AsDeleteOf(Func getKey) { if (getKey == null) { throw new ArgumentNullException(nameof(getKey)); } - innerBuilder.Add((anEvent, context) => - eventMapBuilder.projectionDeletionHandler(getKey(anEvent), context, optionsForDelete)); + return new DeleteAction(actionBuilder, getProjector, getKey); } - public void AsDeleteIfExistsOf(Func getKey) + public IDeleteAction AsDeleteIfExistsOf(Func getKey) { - if (getKey == null) - { - throw new ArgumentNullException(nameof(getKey)); - } - - innerBuilder.Add((anEvent, context) => - eventMapBuilder.projectionDeletionHandler(getKey(anEvent), context, optionsForDeleteIfExists)); + return AsDeleteOf(getKey).IgnoringMisses(); } - public IUpdateEventActionBuilder AsUpdateOf(Func getKey) + public IUpdateAction AsUpdateOf(Func getKey) { if (getKey == null) { throw new ArgumentNullException(nameof(getKey)); } - return new UpdateEventActionBuilder(this, getKey); + return new UpdateAction(actionBuilder, getProjector, getKey); } - public IUpdateIfExistsEventActionBuilder AsUpdateIfExistsOf(Func getKey) + public IUpdateAction AsUpdateIfExistsOf(Func getKey) { - if (getKey == null) - { - throw new ArgumentNullException(nameof(getKey)); - } - - return new UpdateIfExistsEventActionBuilder(this, getKey); + return AsUpdateOf(getKey).IgnoringMisses(); } public void As(Func action) { - innerBuilder.As(action); + actionBuilder.As((anEvent, context) => getProjector().Custom(context, () => action(anEvent, context))); } - IEventMappingBuilder IEventMappingBuilder.When( + IAction IAction.When( Func> predicate) { return When(predicate); } - public IEventMappingBuilder When( + public ICrudAction When( Func> predicate) { if (predicate == null) @@ -355,920 +257,170 @@ public IEventMappingBuilder When( throw new ArgumentNullException(nameof(predicate)); } - innerBuilder.When(predicate); + actionBuilder.When(predicate); return this; } - private sealed class CreateEventActionBuilder : - ICreateEventActionBuilder + private sealed class CreateAction : ICreateAction { - private static readonly ProjectionModificationOptions options = new ProjectionModificationOptions( - MissingProjectionModificationBehavior.Create, - ExistingProjectionModificationBehavior.Throw); + private Func shouldOverwrite; - private readonly ProjectionEventMappingBuilder eventMappingBuilder; + private readonly IAction actionBuilder; + private readonly Func> projector; private readonly Func getKey; - public CreateEventActionBuilder( - ProjectionEventMappingBuilder eventMappingBuilder, - Func getKey) + public CreateAction(IAction actionBuilder, + Func> projector, Func getKey) { - this.eventMappingBuilder = eventMappingBuilder; + this.actionBuilder = actionBuilder; + this.projector = projector; this.getKey = getKey; + + shouldOverwrite = (existingProjection, @event, context) => + throw new ProjectionException( + $"Projection {typeof(TProjection)} with key {getKey(@event)}already exists."); } - public void Using(Func projector) + public ICreateAction Using(Func projector) { if (projector == null) { throw new ArgumentNullException(nameof(projector)); } - eventMappingBuilder.innerBuilder.Add((anEvent, context) => - eventMappingBuilder.eventMapBuilder.projectionModificationHandler( + actionBuilder.As((anEvent, context) => this.projector().Create( getKey(anEvent), context, projection => projector(projection, anEvent, context), - options)); - } - } - - private sealed class CreateIfDoesNotExistEventActionBuilder : - ICreateIfDoesNotExistEventActionBuilder - { - private static readonly ProjectionModificationOptions options = new ProjectionModificationOptions( - MissingProjectionModificationBehavior.Create, - ExistingProjectionModificationBehavior.Ignore); + existingProjection => shouldOverwrite(existingProjection, anEvent, context))); - private readonly ProjectionEventMappingBuilder eventMappingBuilder; - private readonly Func getKey; + return this; + } - public CreateIfDoesNotExistEventActionBuilder( - ProjectionEventMappingBuilder eventMappingBuilder, - Func getKey) + public ICreateAction IgnoringDuplicates() { - this.eventMappingBuilder = eventMappingBuilder; - this.getKey = getKey; + shouldOverwrite = (duplicate, @event,context) => false; + return this; } - public void Using(Func projector) + public ICreateAction OverwritingDuplicates() { - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } + shouldOverwrite = (duplicate, @event,context) => true; + return this; + } - eventMappingBuilder.innerBuilder.Add((anEvent, context) => - eventMappingBuilder.eventMapBuilder.projectionModificationHandler( - getKey(anEvent), - context, - projection => projector(projection, anEvent, context), - options)); + public ICreateAction HandlingDuplicatesUsing(Func shouldOverwrite) + { + this.shouldOverwrite = shouldOverwrite; + return this; } } - private sealed class UpdateEventActionBuilder : - IUpdateEventActionBuilder + private sealed class UpdateAction : IUpdateAction { - private static readonly ProjectionModificationOptions options = new ProjectionModificationOptions( - MissingProjectionModificationBehavior.Throw, - ExistingProjectionModificationBehavior.Update); - - private readonly ProjectionEventMappingBuilder eventMappingBuilder; + private readonly IAction actionBuilder; + private readonly Func> projector; private readonly Func getKey; + private Func handleMissesUsing; - public UpdateEventActionBuilder( - ProjectionEventMappingBuilder eventMappingBuilder, - Func getKey) + public UpdateAction(IAction actionBuilder, + Func> projector, Func getKey) { - this.eventMappingBuilder = eventMappingBuilder; + this.projector = projector; + this.actionBuilder = actionBuilder; this.getKey = getKey; + + ThrowingIfMissing(); } - public void Using(Func projector) + public IUpdateAction Using(Func updateAction) { - if (projector == null) + if (updateAction == null) { - throw new ArgumentNullException(nameof(projector)); + throw new ArgumentNullException(nameof(updateAction)); } - eventMappingBuilder.innerBuilder.Add((anEvent, context) => - eventMappingBuilder.eventMapBuilder.projectionModificationHandler( - getKey(anEvent), - context, - projection => projector(projection, anEvent, context), - options)); + actionBuilder.As((anEvent, context) => OnUpdate(updateAction, anEvent, context)); + + return this; } - } - private sealed class UpdateIfExistsEventActionBuilder : - IUpdateIfExistsEventActionBuilder - { - private static readonly ProjectionModificationOptions options = new ProjectionModificationOptions( - MissingProjectionModificationBehavior.Ignore, - ExistingProjectionModificationBehavior.Update); + private async Task OnUpdate(Func projector, TEvent anEvent, TContext context) + { + var key = getKey(anEvent); + + await this.projector().Update( + key, + context, + projection => projector(projection, anEvent, context), + () => handleMissesUsing(key, context)); + } - private readonly ProjectionEventMappingBuilder eventMappingBuilder; - private readonly Func getKey; + public IUpdateAction ThrowingIfMissing() + { + handleMissesUsing = (key, ctx) => throw new ProjectionException($"Failed to find {typeof(TProjection).Name} with key {key}"); + return this; + } - public UpdateIfExistsEventActionBuilder( - ProjectionEventMappingBuilder eventMappingBuilder, - Func getKey) + public IUpdateAction IgnoringMisses() { - this.eventMappingBuilder = eventMappingBuilder; - this.getKey = getKey; + handleMissesUsing = (_, __) => false; + return this; } - public void Using(Func projector) + public IUpdateAction CreatingIfMissing() { - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } + handleMissesUsing = (_, __) => true; + return this; + } - eventMappingBuilder.innerBuilder.Add((anEvent, context) => - eventMappingBuilder.eventMapBuilder.projectionModificationHandler( - getKey(anEvent), - context, - projection => projector(projection, anEvent, context), - options)); + public IUpdateAction HandlingMissesUsing(Func action) + { + handleMissesUsing = action; + return this; } } - private sealed class CreateOrUpdateEventActionBuilder : - ICreateOrUpdateEventActionBuilder + private class DeleteAction : IDeleteAction { - private static readonly ProjectionModificationOptions options = new ProjectionModificationOptions( - MissingProjectionModificationBehavior.Create, - ExistingProjectionModificationBehavior.Update); + private Action handleMissing; - private readonly ProjectionEventMappingBuilder eventMappingBuilder; - private readonly Func getKey; - - public CreateOrUpdateEventActionBuilder( - ProjectionEventMappingBuilder eventMappingBuilder, - Func getKey) + public DeleteAction(IAction actionBuilder, + Func> projector, Func getKey) { - this.eventMappingBuilder = eventMappingBuilder; - this.getKey = getKey; + actionBuilder.As((anEvent, context) => OnDelete(projector(), getKey, anEvent, context)); + + ThrowingIfMissing(); } - public void Using(Func projector) + private async Task OnDelete(ProjectorMap projector, Func getKey, TEvent anEvent, TContext context) { - if (projector == null) + TKey key = getKey(anEvent); + bool deleted = await projector.Delete(key, context); + if (!deleted) { - throw new ArgumentNullException(nameof(projector)); + handleMissing(key, context); } - - eventMappingBuilder.innerBuilder.Add((anEvent, context) => - eventMappingBuilder.eventMapBuilder.projectionModificationHandler( - getKey(anEvent), - context, - projection => projector(projection, anEvent, context), - options)); } - } - } - } - - /// - /// Contains extension methods to map events to handlers in a fluent fashion. - /// - public static class EventMapBuilderExtensions - { - /// - /// Finishes configuring a custom handler for events of type - /// using context of type . - /// - /// The . - /// - /// The synchronous delegate that handles the event. - /// Takes the event and the context as the parameters. - /// - public static void As( - this IEventMappingBuilder eventMappingBuilder, - Action action) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - eventMappingBuilder.As((anEvent, context) => - { - action(anEvent, context); - return SpecializedTasks.ZeroTask; - }); - } - - /// - /// Finishes configuring a custom handler for events of type - /// using context of type . - /// - /// The . - /// - /// The synchronous delegate that handles the event. - /// Takes the event as the parameter. - /// - public static void As( - this IEventMappingBuilder eventMappingBuilder, - Action action) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - eventMappingBuilder.As((anEvent, context) => - { - action(anEvent); - return SpecializedTasks.ZeroTask; - }); - } - - /// - /// Finishes configuring a custom handler for events of type - /// using context of type . - /// - /// The . - /// - /// The asynchronous delegate that handles the event. - /// Takes the event as the parameter. - /// - public static void As( - this IEventMappingBuilder eventMappingBuilder, - Func action) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - eventMappingBuilder.As((anEvent, context) => action(anEvent)); - } - - /// - /// Continues configuring a handler for events of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The synchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event and the context as the parameters. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - return eventMappingBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent, context))); - } - - /// - /// Continues configuring a handler for events of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The synchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event as the parameter. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - return eventMappingBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent))); - } - /// - /// Continues configuring a handler for events of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The asynchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event as the parameter. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func> predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - return eventMappingBuilder.When((anEvent, context) => predicate(anEvent)); - } + public IDeleteAction ThrowingIfMissing() + { + handleMissing = (key, ctx) => throw new ProjectionException($"Could not delete {typeof(TProjection).Name} with key {key} because it does not exist");; + return this; + } - /// - /// Continues configuring a handler for events of type - /// for projections of type with key of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The synchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event and the context as the parameters. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } + public IDeleteAction IgnoringMisses() + { + handleMissing = (_, __) => {}; + return this; + } - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); + public IDeleteAction HandlingMissesUsing(Action action) + { + handleMissing = action; + return this; + } } - - return eventMappingBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent, context))); - } - - /// - /// Continues configuring a handler for events of type - /// for projections of type with key of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The synchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event as the parameter. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - return eventMappingBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent))); - } - - /// - /// Continues configuring a handler for events of type - /// for projections of type with key of type - /// using context of type . - /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. - /// - /// The . - /// - /// The asynchronous delegate that filters the events and - /// should return true for events that will be handled by the handler. - /// Takes the event as the parameter. - /// - /// - /// that allows to continue configuring the handler. - /// - public static IEventMappingBuilder When( - this IEventMappingBuilder eventMappingBuilder, - Func> predicate) - { - if (eventMappingBuilder == null) - { - throw new ArgumentNullException(nameof(eventMappingBuilder)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - return eventMappingBuilder.When((anEvent, context) => predicate(anEvent)); - } - - /// - /// Finishes configuring a projection creation handler for events of type - /// for projections of type using context of type . - /// - /// The . - /// - /// The synchronous delegate that initializes the created projection. - /// Takes the projection, the event and the context as the parameters. - /// - public static void Using( - this ICreateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent, context); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation handler for events of type - /// for projections of type using context of type . - /// - /// The . - /// - /// The synchronous delegate that initializes the created projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation handler for events of type - /// for projections of type using context of type . - /// - /// The . - /// - /// The asynchronous delegate that initializes the created projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateEventActionBuilder eventActionBuilder, - Func projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); - } - - /// - /// Finishes configuring a projection creation handler for projections which do not exist yet - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that initializes the created projection. - /// Takes the projection, the event and the context as the parameters. - /// - public static void Using( - this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent, context); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation handler for projections which do not exist yet - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that initializes the created projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation handler for projections which do not exist yet - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The asynchronous delegate that initializes the created projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, - Func projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); - } - - /// - /// Finishes configuring a projection updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that updates the projection. - /// Takes the projection, the event and the context as the parameters. - /// - public static void Using( - this IUpdateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent, context); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that updates the projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this IUpdateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The asynchronous delegate that updates the projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this IUpdateEventActionBuilder eventActionBuilder, - Func projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); - } - - /// - /// Finishes configuring a projection updating handler for projections which do already exist - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that updates the projection. - /// Takes the projection, the event and the context as the parameters. - /// - public static void Using( - this IUpdateIfExistsEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent, context); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection updating handler for projections which do already exist - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that updates the projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this IUpdateIfExistsEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection updating handler for projections which do already exist - /// for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The asynchronous delegate that updates the projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this IUpdateIfExistsEventActionBuilder eventActionBuilder, - Func projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); - } - - /// - /// Finishes configuring a projection creation or updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that initializes the created projection or updates the existing projection. - /// Takes the projection, the event and the context as the parameters. - /// - public static void Using( - this ICreateOrUpdateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent, context); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation or updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The synchronous delegate that initializes the created projection or updates the existing projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateOrUpdateEventActionBuilder eventActionBuilder, - Action projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => - { - projector(projection, anEvent); - return SpecializedTasks.FalseTask; - }); - } - - /// - /// Finishes configuring a projection creation or updating handler for events of type - /// for projections of type using context of type . - /// - /// - /// The . - /// - /// - /// The asynchronous delegate that initializes the created projection or updates the existing projection. - /// Takes the projection and the event as the parameters. - /// - public static void Using( - this ICreateOrUpdateEventActionBuilder eventActionBuilder, - Func projector) - { - if (eventActionBuilder == null) - { - throw new ArgumentNullException(nameof(eventActionBuilder)); - } - - if (projector == null) - { - throw new ArgumentNullException(nameof(projector)); - } - - eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); } } } \ No newline at end of file diff --git a/Src/LiquidProjections/EventMapBuilderExtensions.cs b/Src/LiquidProjections/EventMapBuilderExtensions.cs new file mode 100644 index 0000000..0858934 --- /dev/null +++ b/Src/LiquidProjections/EventMapBuilderExtensions.cs @@ -0,0 +1,747 @@ +using System; +using System.Threading.Tasks; +using LiquidProjections.MapBuilding; + +namespace LiquidProjections +{ + /// + /// Contains extension methods to map events to handlers in a fluent fashion. + /// + public static class EventMapBuilderExtensions + { + /// + /// Finishes configuring a custom handler for events of type + /// using context of type . + /// + /// The . + /// + /// The synchronous delegate that handles the event. + /// Takes the event and the context as the parameters. + /// + public static void As( + this IAction actionBuilder, + Action action) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + actionBuilder.As((anEvent, context) => + { + action(anEvent, context); + return SpecializedTasks.ZeroTask; + }); + } + + /// + /// Finishes configuring a custom handler for events of type + /// using context of type . + /// + /// The . + /// + /// The synchronous delegate that handles the event. + /// Takes the event as the parameter. + /// + public static void As( + this IAction actionBuilder, + Action action) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + actionBuilder.As((anEvent, context) => + { + action(anEvent); + return SpecializedTasks.ZeroTask; + }); + } + + /// + /// Finishes configuring a custom handler for events of type + /// using context of type . + /// + /// The . + /// + /// The asynchronous delegate that handles the event. + /// Takes the event as the parameter. + /// + public static void As( + this IAction actionBuilder, + Func action) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + actionBuilder.As((anEvent, context) => action(anEvent)); + } + + /// + /// Continues configuring a handler for events of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The synchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event and the context as the parameters. + /// + /// + /// that allows to continue configuring the handler. + /// + public static IAction When( + this IAction actionBuilder, + Func predicate) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return actionBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent, context))); + } + + /// + /// Continues configuring a handler for events of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The synchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event as the parameter. + /// + /// + /// that allows to continue configuring the handler. + /// + public static IAction When( + this IAction actionBuilder, + Func predicate) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return actionBuilder.When((anEvent, context) => Task.FromResult(predicate(anEvent))); + } + + /// + /// Continues configuring a handler for events of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The asynchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event as the parameter. + /// + /// + /// that allows to continue configuring the handler. + /// + public static IAction When( + this IAction actionBuilder, + Func> predicate) + { + if (actionBuilder == null) + { + throw new ArgumentNullException(nameof(actionBuilder)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return actionBuilder.When((anEvent, context) => predicate(anEvent)); + } + + /// + /// Continues configuring a handler for events of type + /// for projections of type with key of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The synchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event and the context as the parameters. + /// + /// + /// that allows to continue configuring the handler. + /// + public static ICrudAction When( + this ICrudAction crudAction, + Func predicate) + { + if (crudAction == null) + { + throw new ArgumentNullException(nameof(crudAction)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return crudAction.When((anEvent, context) => Task.FromResult(predicate(anEvent, context))); + } + + /// + /// Continues configuring a handler for events of type + /// for projections of type with key of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The synchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event as the parameter. + /// + /// + /// that allows to continue configuring the handler. + /// + public static ICrudAction When( + this ICrudAction crudAction, + Func predicate) + { + if (crudAction == null) + { + throw new ArgumentNullException(nameof(crudAction)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return crudAction.When((anEvent, context) => Task.FromResult(predicate(anEvent))); + } + + /// + /// Continues configuring a handler for events of type + /// for projections of type with key of type + /// using context of type . + /// Provides an additional condition that needs to be satisfied in order for the event to be handled by the handler. + /// + /// The . + /// + /// The asynchronous delegate that filters the events and + /// should return true for events that will be handled by the handler. + /// Takes the event as the parameter. + /// + /// + /// that allows to continue configuring the handler. + /// + public static ICrudAction When( + this ICrudAction crudAction, + Func> predicate) + { + if (crudAction == null) + { + throw new ArgumentNullException(nameof(crudAction)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + return crudAction.When((anEvent, context) => predicate(anEvent)); + } + + /// + /// Finishes configuring a projection creation handler for events of type + /// for projections of type using context of type . + /// + /// The . + /// + /// The synchronous delegate that initializes the created projection. + /// Takes the projection, the event and the context as the parameters. + /// + public static void Using( + this ICreateAction action, + Action projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => + { + projector(projection, anEvent, context); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation handler for events of type + /// for projections of type using context of type . + /// + /// The . + /// + /// The synchronous delegate that initializes the created projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateAction action, + Action projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => + { + projector(projection, anEvent); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation handler for events of type + /// for projections of type using context of type . + /// + /// The . + /// + /// The asynchronous delegate that initializes the created projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateAction action, + Func projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => projector(projection, anEvent)); + } + + /// + /// Finishes configuring a projection creation handler for projections which do not exist yet + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that initializes the created projection. + /// Takes the projection, the event and the context as the parameters. + /// + public static void Using( + this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent, context); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation handler for projections which do not exist yet + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that initializes the created projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation handler for projections which do not exist yet + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The asynchronous delegate that initializes the created projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateIfDoesNotExistEventActionBuilder eventActionBuilder, + Func projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); + } + + /// + /// Finishes configuring a projection updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that updates the projection. + /// Takes the projection, the event and the context as the parameters. + /// + public static void Using( + this IUpdateAction action, + Action projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => + { + projector(projection, anEvent, context); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that updates the projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this IUpdateAction action, + Action projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => + { + projector(projection, anEvent); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The asynchronous delegate that updates the projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this IUpdateAction action, + Func projector) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + action.Using((projection, anEvent, context) => projector(projection, anEvent)); + } + + /// + /// Finishes configuring a projection updating handler for projections which do already exist + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that updates the projection. + /// Takes the projection, the event and the context as the parameters. + /// + public static void Using( + this IUpdateIfExistsEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent, context); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection updating handler for projections which do already exist + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that updates the projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this IUpdateIfExistsEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection updating handler for projections which do already exist + /// for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The asynchronous delegate that updates the projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this IUpdateIfExistsEventActionBuilder eventActionBuilder, + Func projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); + } + + /// + /// Finishes configuring a projection creation or updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that initializes the created projection or updates the existing projection. + /// Takes the projection, the event and the context as the parameters. + /// + public static void Using( + this ICreateOrUpdateEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent, context); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation or updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The synchronous delegate that initializes the created projection or updates the existing projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateOrUpdateEventActionBuilder eventActionBuilder, + Action projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => + { + projector(projection, anEvent); + return SpecializedTasks.FalseTask; + }); + } + + /// + /// Finishes configuring a projection creation or updating handler for events of type + /// for projections of type using context of type . + /// + /// + /// The . + /// + /// + /// The asynchronous delegate that initializes the created projection or updates the existing projection. + /// Takes the projection and the event as the parameters. + /// + public static void Using( + this ICreateOrUpdateEventActionBuilder eventActionBuilder, + Func projector) + { + if (eventActionBuilder == null) + { + throw new ArgumentNullException(nameof(eventActionBuilder)); + } + + if (projector == null) + { + throw new ArgumentNullException(nameof(projector)); + } + + eventActionBuilder.Using((projection, anEvent, context) => projector(projection, anEvent)); + } + } +} \ No newline at end of file diff --git a/Src/LiquidProjections/IEventMap.cs b/Src/LiquidProjections/IEventMap.cs index ca57131..e7e9a54 100644 --- a/Src/LiquidProjections/IEventMap.cs +++ b/Src/LiquidProjections/IEventMap.cs @@ -13,6 +13,10 @@ public interface IEventMap /// /// Handles asynchronously. /// - Task Handle(object anEvent, TContext context); + /// + /// Returns a value indicating whether the map was configured to handle the event, + /// taking into account any local or global filters. + /// + Task Handle(object anEvent, TContext context); } } \ No newline at end of file diff --git a/Src/LiquidProjections/IEventMapBuilder.cs b/Src/LiquidProjections/IEventMapBuilder.cs index 0406c93..364865d 100644 --- a/Src/LiquidProjections/IEventMapBuilder.cs +++ b/Src/LiquidProjections/IEventMapBuilder.cs @@ -11,12 +11,7 @@ public interface IEventMapBuilder /// /// Builds the resulting event map. Can only be called once. /// - IEventMap Build(); - - /// - /// Configures the event map to handle custom actions via the provided delegate . - /// - void HandleCustomActionsAs(CustomHandler handler); + IEventMap Build(ProjectorMap projector); } /// @@ -33,154 +28,102 @@ public interface IEventMapBuilder /// Type of the projections. /// Type of the projection keys. /// Type of the context. - public interface IEventMapBuilder : IEventMapBuilder + public interface IEventMapBuilder { /// - /// Configures the event map to handle projection creation and updating - /// via the provided delegate . - /// - void HandleProjectionModificationsAs(ProjectionModificationHandler handler); - - /// - /// Configures the event map to handle projection deletion - /// via the provided delegate . + /// Builds the resulting event map. Can only be called once. /// - void HandleProjectionDeletionsAs(ProjectionDeletionHandler handler); + IEventMap Build(ProjectorMap projector); } /// - /// Handles projection creation and updating asynchronously using context . - /// - /// Type of the projections. - /// Type of the projection keys. - /// Type of the context. - /// Key of the projection. - /// The context. - /// - /// The delegate that must be invoked to handle the event for the provided projection - /// and modify the projection accordingly. - /// - /// Additional options . - public delegate Task ProjectionModificationHandler( - TKey key, - TContext context, - Func projector, - ProjectionModificationOptions options); - - /// - /// Provides additional options for . + /// Defines the contract for a projector that can handle the CRUD operations needed to handle + /// events as mapped through the . /// - public class ProjectionModificationOptions + public class ProjectorMap { - /// Behavior when the projection does not exists. - /// Behavior when the projection already exists. - public ProjectionModificationOptions( - MissingProjectionModificationBehavior missingProjectionBehavior, - ExistingProjectionModificationBehavior existingProjectionBehavior) - { - MissingProjectionBehavior = missingProjectionBehavior; - ExistingProjectionBehavior = existingProjectionBehavior; - } - - /// - /// Behavior when the projection does not exists. - /// - public MissingProjectionModificationBehavior MissingProjectionBehavior { get; } - - /// - /// Behavior when the projection already exists. - /// - public ExistingProjectionModificationBehavior ExistingProjectionBehavior { get; } + public CustomHandler Custom { get; set; } = (context, projector) + => throw new NotSupportedException("No handler has been set-up for custom actions."); } /// - /// Specifies behavior for - /// when the projection does not exists. + /// Defines the contract for a projector that can handle the CRUD operations needed to handle + /// events as mapped through the /// - public enum MissingProjectionModificationBehavior + public class ProjectorMap : ProjectorMap { - /// - /// Creates a new projection when the projection does not exists. - /// - Create, + public CreationHandler Create { get; set; } = (key, context, projector, shouldOverwrite) => + throw new NotSupportedException("No handler has been set-up for creations."); - /// - /// Does nothing when the projection does not exists. - /// - Ignore, + public UpdateHandler Update { get; set; } = (key, context, projector, createIfMissing) => + throw new NotSupportedException("No handler has been set-up for updates."); - /// - /// Throws an exception when the projection does not exists. - /// - Throw + public DeletionHandler Delete { get; set; } = (key, context) => + throw new NotSupportedException("No handler has been set-up for deletions."); } /// - /// Specifies behavior for - /// when the projection already exists. + /// Defines a handler for creating projections based on an event. /// - public enum ExistingProjectionModificationBehavior - { - /// - /// Updates the projection when it already exists. - /// - Update, - - /// - /// Does nothing when the projection already exists. - /// - Ignore, - - /// - /// Throws an exception when the projection already exists. - /// - Throw - } - - /// - /// Handles projection deletion asynchronously using context . - /// - /// The type of the projection keys. - /// The type of the context. - /// The key of the projection. - /// The context. - /// Additional options . - public delegate Task ProjectionDeletionHandler( + /// + /// The key of projection as extracted from the event during its mapping configuration. + /// + /// + /// An object providing information about the current event and any projector-specific metadata. + /// + /// + /// The delegate that must be invoked to handle the event for the provided projection + /// and modify the projection accordingly. + /// + /// + /// Should be called by the handler to determine how to handle existing projections by the same key. + /// If it returns true then the handler should use the to update the + /// state of the existing projection, or false to ignore the call. Can throw an exception if that was + /// requested through the event map. + /// + public delegate Task CreationHandler( TKey key, TContext context, - ProjectionDeletionOptions options); + Func projector, + Func shouldOverwite); /// - /// Provides additional options for . + /// Defines a handler for updating projections based on an event. /// - public class ProjectionDeletionOptions - { - /// Behavior when the projection does not exists. - public ProjectionDeletionOptions(MissingProjectionDeletionBehavior missingProjectionBehavior) - { - MissingProjectionBehavior = missingProjectionBehavior; - } - - /// - /// Behavior when the projection does not exists. - /// - public MissingProjectionDeletionBehavior MissingProjectionBehavior { get; } - } + /// + /// The key of projection as extracted from the event during its mapping configuration. + /// + /// + /// An object providing information about the current event and any projector-specific metadata. + /// + /// + /// The delegate that must be invoked to handle the event for the provided projection + /// and modify the projection accordingly. + /// + /// + /// Should be called by the handler to determine whether it should create a missing projection. Depending on + /// how the event was mapped, it can throw an exception that should not be caught by the projector. + /// + public delegate Task UpdateHandler( + TKey key, + TContext context, + Func projector, + Func createIfMissing); - /// - /// Specifies behavior for - /// when the projection does not exists. + /// + /// Defines a handler for deleting projections based on an event. /// - public enum MissingProjectionDeletionBehavior - { - /// - /// Does nothing when the projection does not exists. - /// - Ignore, - - /// - /// Throws an exception when the projection does not exists. - /// - Throw - } -} \ No newline at end of file + /// + /// The key of projection as extracted from the event during its mapping configuration. + /// + /// + /// An object providing information about the current event and any projector-specific metadata. + /// + /// + /// Returns a value indicating if deleting the projection succeeded. Should return false if the projection did not exist, + /// + public delegate Task DeletionHandler( + TKey key, + TContext context); +} + \ No newline at end of file diff --git a/Src/LiquidProjections/MapBuilding/IEventMappingBuilder.cs b/Src/LiquidProjections/MapBuilding/IAction.cs similarity index 74% rename from Src/LiquidProjections/MapBuilding/IEventMappingBuilder.cs rename to Src/LiquidProjections/MapBuilding/IAction.cs index dc15576..49260d3 100644 --- a/Src/LiquidProjections/MapBuilding/IEventMappingBuilder.cs +++ b/Src/LiquidProjections/MapBuilding/IAction.cs @@ -7,7 +7,7 @@ namespace LiquidProjections.MapBuilding /// Allows to configure event map how to handle custom actions for events of type /// using context . /// - public interface IEventMappingBuilder + public interface IAction { /// /// Finishes configuring a custom handler for events of type . @@ -28,9 +28,9 @@ public interface IEventMappingBuilder /// Takes the event and the context as the parameters. /// /// - /// that allows to continue configuring the handler. + /// that allows to continue configuring the handler. /// - IEventMappingBuilder When(Func> predicate); + IAction When(Func> predicate); } /// @@ -39,7 +39,7 @@ public interface IEventMappingBuilder /// with key of type /// using context . /// - public interface IEventMappingBuilder : IEventMappingBuilder + public interface ICrudAction : IAction { /// /// Continues configuring a handler for events of type . @@ -48,9 +48,9 @@ public interface IEventMappingBuilder : /// /// The delegate that determines the projection key for the event. /// - /// that allows to continue configuring the handler. + /// that allows to continue configuring the handler. /// - ICreateEventActionBuilder AsCreateOf(Func getKey); + ICreateAction AsCreateOf(Func getKey); /// /// Continues configuring a handler for events of type . @@ -62,8 +62,8 @@ public interface IEventMappingBuilder : /// /// that allows to continue configuring the handler. /// - ICreateIfDoesNotExistEventActionBuilder AsCreateIfDoesNotExistOf( - Func getKey); + [Obsolete("Use AsCreateOf().IgnoringDuplicates() instead")] + ICreateAction AsCreateIfDoesNotExistOf(Func getKey); /// /// Continues configuring a handler for events of type . @@ -72,9 +72,9 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// /// The delegate that determines the projection key for the event. /// - /// that allows to continue configuring the handler. + /// that allows to continue configuring the handler. /// - IUpdateEventActionBuilder AsUpdateOf(Func getKey); + IUpdateAction AsUpdateOf(Func getKey); /// /// Continues configuring a handler for events of type . @@ -83,9 +83,10 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// /// The delegate that determines the projection key for the event. /// - /// that allows to continue configuring the handler. + /// that allows to continue configuring the handler. /// - IUpdateIfExistsEventActionBuilder AsUpdateIfExistsOf(Func getKey); + [Obsolete("Use AsUpdateOf().IgnoringMissing() instead")] + IUpdateAction AsUpdateIfExistsOf(Func getKey); /// /// Continues configuring a handler for events of type . @@ -97,7 +98,8 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// /// that allows to continue configuring the handler. /// - ICreateOrUpdateEventActionBuilder AsCreateOrUpdateOf(Func getKey); + [Obsolete("Use AsCreateOf().OverwritingDuplicates() instead")] + ICreateAction AsCreateOrUpdateOf(Func getKey); /// /// Finishes configuring a handler for events of type . @@ -105,7 +107,7 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// An exception will be thrown if a projection with such key does not exist. /// /// The delegate that determines the projection key for the event. - void AsDeleteOf(Func getKey); + IDeleteAction AsDeleteOf(Func getKey); /// /// Finishes configuring a handler for events of type . @@ -113,7 +115,8 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// The event will not be handled if the projection with such key does not exist. /// /// The delegate that determines the projection key for the event. - void AsDeleteIfExistsOf(Func getKey); + [Obsolete("Use AsDeleteOf().IgnoringMissing() instead")] + IDeleteAction AsDeleteIfExistsOf(Func getKey); /// /// Continues configuring a handler for events of type . @@ -125,8 +128,8 @@ ICreateIfDoesNotExistEventActionBuilder AsCreateI /// Takes the event and the context as the parameters. /// /// - /// that allows to continue configuring the handler. + /// that allows to continue configuring the handler. /// - new IEventMappingBuilder When(Func> predicate); + new ICrudAction When(Func> predicate); } } diff --git a/Src/LiquidProjections/MapBuilding/ICreateAction.cs b/Src/LiquidProjections/MapBuilding/ICreateAction.cs new file mode 100644 index 0000000..76389cd --- /dev/null +++ b/Src/LiquidProjections/MapBuilding/ICreateAction.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; + +namespace LiquidProjections.MapBuilding +{ + /// + /// Allows to configure event map how to handle projection creation + /// for events of type and projections of type + /// using context . + /// + public interface ICreateAction + { + /// + /// Finishes configuring a projection creation handler. + /// + /// + /// The asynchronous delegate that initializes the created projection. + /// Takes the projection, the event and the context as the parameters. + /// + ICreateAction Using(Func projector); + + /// + /// Tells the implementing projector that duplicates should be ignored. + /// + ICreateAction IgnoringDuplicates(); + + /// + /// Configures the action to overwrite any duplicates. + /// + ICreateAction OverwritingDuplicates(); + + /// + /// Allows custom handling of duplicates found while trying to create a new projection. + /// + /// + /// A predicate that allows the handler to decide whether or not to overwrite the duplicate projection. + /// + ICreateAction HandlingDuplicatesUsing(Func shouldOverwrite); + } +} \ No newline at end of file diff --git a/Src/LiquidProjections/MapBuilding/ICreateEventActionBuilder.cs b/Src/LiquidProjections/MapBuilding/ICreateEventActionBuilder.cs deleted file mode 100644 index a9bc064..0000000 --- a/Src/LiquidProjections/MapBuilding/ICreateEventActionBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace LiquidProjections.MapBuilding -{ - /// - /// Allows to configure event map how to handle projection creation - /// for events of type and projections of type - /// using context . - /// - public interface ICreateEventActionBuilder - { - /// - /// Finishes configuring a projection creation handler. - /// - /// - /// The asynchronous delegate that initializes the created projection. - /// Takes the projection, the event and the context as the parameters. - /// - void Using(Func projector); - } -} \ No newline at end of file diff --git a/Src/LiquidProjections/MapBuilding/IDeleteAction.cs b/Src/LiquidProjections/MapBuilding/IDeleteAction.cs new file mode 100644 index 0000000..cd5a1ee --- /dev/null +++ b/Src/LiquidProjections/MapBuilding/IDeleteAction.cs @@ -0,0 +1,22 @@ +using System; + +namespace LiquidProjections.MapBuilding +{ + public interface IDeleteAction + { + /// + /// Causes the mapping to throw if deleting the projection failed or the projection did not exist anymore. + /// + IDeleteAction ThrowingIfMissing(); + + /// + /// Configures the mapping to ignore any failed attempts to delete a projection. + /// + IDeleteAction IgnoringMisses(); + + /// + /// Allows the consumer to handle missing projections in a custom way. + /// + IDeleteAction HandlingMissesUsing(Action action); + } +} \ No newline at end of file diff --git a/Src/LiquidProjections/MapBuilding/IUpdateAction.cs b/Src/LiquidProjections/MapBuilding/IUpdateAction.cs new file mode 100644 index 0000000..c3ab240 --- /dev/null +++ b/Src/LiquidProjections/MapBuilding/IUpdateAction.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading.Tasks; + +namespace LiquidProjections.MapBuilding +{ + /// + /// Allows to configure event map how to handle projection updating + /// for events of type and projections of type + /// using context . + /// + public interface IUpdateAction + { + /// + /// Finishes configuring a projection updating handler. + /// + /// + /// The asynchronous delegate that updates the projection. + /// Takes the projection, the event and the context as the parameters. + /// + IUpdateAction Using(Func updateAction); + + /// + /// Configures the update action to throw a if the projection was missing. + /// + IUpdateAction ThrowingIfMissing(); + + /// + /// Configures the update action to ignore a missing projection and continue with the next event. + /// + IUpdateAction IgnoringMisses(); + + /// + /// Configures the update action to create the projection if it is missing. + /// + IUpdateAction CreatingIfMissing(); + + /// + /// Allows some custom code to be executed when the a projection was missing and to optionally tell the projector + /// to create it. + /// + IUpdateAction HandlingMissesUsing(Func action); + } +} \ No newline at end of file diff --git a/Src/LiquidProjections/MapBuilding/IUpdateEventActionBuilder.cs b/Src/LiquidProjections/MapBuilding/IUpdateEventActionBuilder.cs deleted file mode 100644 index 602f625..0000000 --- a/Src/LiquidProjections/MapBuilding/IUpdateEventActionBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace LiquidProjections.MapBuilding -{ - /// - /// Allows to configure event map how to handle projection updating - /// for events of type and projections of type - /// using context . - /// - public interface IUpdateEventActionBuilder - { - /// - /// Finishes configuring a projection updating handler. - /// - /// - /// The asynchronous delegate that updates the projection. - /// Takes the projection, the event and the context as the parameters. - /// - void Using(Func projector); - } -} \ No newline at end of file diff --git a/Src/LiquidProjections/Projector.cs b/Src/LiquidProjections/Projector.cs index 6bb58a6..becde49 100644 --- a/Src/LiquidProjections/Projector.cs +++ b/Src/LiquidProjections/Projector.cs @@ -24,8 +24,10 @@ private static IEventMap BuildMap(IEventMapBuilder + { + Custom = (context, projector) => projector() + }); } public Projector(IEventMap map, IEnumerable children = null) @@ -52,11 +54,6 @@ public ShouldRetry ShouldRetry } } - private static void SetupHandlers(IEventMapBuilder eventMapBuilder) - { - eventMapBuilder.HandleCustomActionsAs((context, projector) => projector()); - } - /// /// Instructs the projector to handle a collection of ordered transactions. /// diff --git a/Tests/LiquidProjections.Specs/EventMapSpecs.cs b/Tests/LiquidProjections.Specs/EventMapSpecs.cs index 5bf2b92..915e787 100644 --- a/Tests/LiquidProjections.Specs/EventMapSpecs.cs +++ b/Tests/LiquidProjections.Specs/EventMapSpecs.cs @@ -8,13 +8,12 @@ namespace LiquidProjections.Specs { namespace EventMapSpecs { - public class When_an_event_is_mapped_as_a_create : GivenWhenThen + public class When_event_should_create_a_new_projection : GivenWhenThen { private ProductCatalogEntry projection; private IEventMap map; - private ProjectionModificationOptions options; - public When_an_event_is_mapped_as_a_create() + public When_event_should_create_a_new_projection() { Given(() => { @@ -26,34 +25,23 @@ public When_an_event_is_mapped_as_a_create() return Task.FromResult(0); }); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => + map = mapBuilder.Build(new ProjectorMap { - projection = new ProductCatalogEntry + Create = async (key, context, projector, shouldOverwrite) => { - Id = key, - }; + projection = new ProductCatalogEntry + { + Id = key, + }; - this.options = options; - await projector(projection); + await projector(projection); + } }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - throw new InvalidOperationException("Deletion should not be called."); - }); - - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); - }); - - map = mapBuilder.Build(); }); When(async () => { - await map.Handle( - new ProductAddedToCatalogEvent + await map.Handle(new ProductAddedToCatalogEvent { Category = "Hybrids", ProductKey = "c350E" @@ -72,27 +60,15 @@ public void It_should_properly_pass_the_mapping_to_the_creating_handler() Deleted = false }); } - - [Fact] - public void It_should_create_projection_if_it_does_not_exist() - { - options.MissingProjectionBehavior.Should().Be(MissingProjectionModificationBehavior.Create); - } - - [Fact] - public void It_should_throw_if_the_projection_already_exists() - { - options.ExistingProjectionBehavior.Should().Be(ExistingProjectionModificationBehavior.Throw); - } } - public class When_an_event_is_mapped_as_a_create_if_does_not_exist : GivenWhenThen + public class When_a_creating_event_must_ignore_an_existing_projection : GivenWhenThen { - private ProductCatalogEntry projection; + private ProductCatalogEntry existingProjection; + private IEventMap map; - private ProjectionModificationOptions options; - public When_an_event_is_mapped_as_a_create_if_does_not_exist() + public When_a_creating_event_must_ignore_an_existing_projection() { Given(() => { @@ -100,35 +76,29 @@ public When_an_event_is_mapped_as_a_create_if_does_not_exist() mapBuilder .Map() - .AsCreateIfDoesNotExistOf(e => e.ProductKey) + .AsCreateOf(e => e.ProductKey).IgnoringDuplicates() .Using((p, e, ctx) => { p.Category = e.Category; return Task.FromResult(0); }); - - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => + + existingProjection = new ProductCatalogEntry { - projection = new ProductCatalogEntry - { - Id = key, - }; - - this.options = options; - await projector(projection); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - throw new InvalidOperationException("Deletion should not be called."); - }); + Id = "c350E", + Category = "Fosile", + }; - mapBuilder.HandleCustomActionsAs((context, projector) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Create = async (key, context, projector, shouldOverwrite) => + { + if (shouldOverwrite(existingProjection)) + { + await projector(existingProjection); + } + } }); - - map = mapBuilder.Build(); }); When(async () => @@ -144,36 +114,85 @@ await map.Handle( } [Fact] - public void It_should_properly_pass_the_mapping_to_the_creating_handler() + public void It_should_leave_the_existing_projection_untouched() { - projection.Should().BeEquivalentTo(new + existingProjection.Should().BeEquivalentTo(new { Id = "c350E", - Category = "Hybrids", - Deleted = false + Category = "Fosile" }); } + } - [Fact] - public void It_should_create_projection_if_it_does_not_exist() + public class When_a_creating_event_should_overwrite_an_existing_projection : GivenWhenThen + { + private ProductCatalogEntry existingProjection; + private IEventMap map; + + public When_a_creating_event_should_overwrite_an_existing_projection() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionModificationBehavior.Create); + Given(() => + { + var mapBuilder = new EventMapBuilder(); + + mapBuilder + .Map() + .AsCreateOf(e => e.ProductKey) + .OverwritingDuplicates() + .Using((p, e, ctx) => + { + p.Category = e.Category; + return Task.FromResult(0); + }); + + existingProjection = new ProductCatalogEntry + { + Id = "c350E", + Category = "OldCategory", + }; + + map = mapBuilder.Build(new ProjectorMap + { + Create = async (key, context, projector, shouldOverwrite) => + { + if (shouldOverwrite(existingProjection)) + { + await projector(existingProjection); + } + } + }); + }); + + When(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "NewCategory", + ProductKey = "c350E" + }, + new ProjectionContext()); + }); } [Fact] - public void It_should_do_nothing_if_the_projection_already_exists() + public void It_should_tell_the_creating_handler_to_overwrite_the_existing_proejction() { - options.ExistingProjectionBehavior.Should().Be(ExistingProjectionModificationBehavior.Ignore); + existingProjection.Should().BeEquivalentTo(new + { + Id = "c350E", + Category = "NewCategory", + }); } } - public class When_an_event_is_mapped_as_a_create_or_update : GivenWhenThen + public class When_a_creating_event_should_allow_manual_handling_of_duplicates : GivenWhenThen { - private ProductCatalogEntry projection; + private ProductCatalogEntry existingProjection; private IEventMap map; - private ProjectionModificationOptions options; + private ProductCatalogEntry duplicateProjection; - public When_an_event_is_mapped_as_a_create_or_update() + public When_a_creating_event_should_allow_manual_handling_of_duplicates() { Given(() => { @@ -181,43 +200,41 @@ public When_an_event_is_mapped_as_a_create_or_update() mapBuilder .Map() - .AsCreateOrUpdateOf(e => e.ProductKey) + .AsCreateOf(e => e.ProductKey) + .HandlingDuplicatesUsing((duplicate, @event, context) => + { + duplicateProjection = existingProjection; + return true; + }) .Using((p, e, ctx) => { p.Category = e.Category; return Task.FromResult(0); }); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => - { - projection = new ProductCatalogEntry - { - Id = key, - }; - - this.options = options; - await projector(projection); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => + existingProjection = new ProductCatalogEntry { - throw new InvalidOperationException("Deletion should not be called."); - }); + Id = "c350E", + Category = "OldCategory", + }; - mapBuilder.HandleCustomActionsAs((context, projector) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Create = async (key, context, projector, shouldOverwrite) => + { + if (shouldOverwrite(existingProjection)) + { + await projector(existingProjection); + } + } }); - - map = mapBuilder.Build(); }); When(async () => { - await map.Handle( - new ProductAddedToCatalogEvent + await map.Handle(new ProductAddedToCatalogEvent { - Category = "Hybrids", + Category = "NewCategory", ProductKey = "c350E" }, new ProjectionContext()); @@ -225,36 +242,28 @@ await map.Handle( } [Fact] - public void It_should_properly_pass_the_mapping_to_the_creating_handler() + public void It_should_honor_the_custom_handlings_wish_to_overwrite() { - projection.Should().BeEquivalentTo(new + existingProjection.Should().BeEquivalentTo(new { Id = "c350E", - Category = "Hybrids", - Deleted = false + Category = "NewCategory", }); } [Fact] - public void It_should_create_projection_if_it_does_not_exist() + public void It_should_pass_the_duplicate_to_the_handler() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionModificationBehavior.Create); - } - - [Fact] - public void It_should_update_the_projection_if_it_already_exists() - { - options.ExistingProjectionBehavior.Should().Be(ExistingProjectionModificationBehavior.Update); + duplicateProjection.Should().BeSameAs(existingProjection); } } - public class When_an_event_is_mapped_as_an_update : GivenWhenThen + public class When_an_updating_event_should_throw_on_misses : GivenWhenThen { - private ProductCatalogEntry projection; + private ProductCatalogEntry existingProjection; private IEventMap map; - private ProjectionModificationOptions options; - public When_an_event_is_mapped_as_an_update() + public When_an_updating_event_should_throw_on_misses() { Given(() => { @@ -266,74 +275,95 @@ public When_an_event_is_mapped_as_an_update() return Task.FromResult(0); }); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => - { - projection = new ProductCatalogEntry - { - Id = key, - }; - - this.options = options; - await projector(projection); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => + existingProjection = new ProductCatalogEntry { - throw new InvalidOperationException("Deletion should not be called."); - }); + Id = "c350E", + Category = "OldCategory", + }; - mapBuilder.HandleCustomActionsAs((context, projector) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Update = async (key, context, projector, createIfMissing) => + { + if (createIfMissing()) + { + await projector(existingProjection); + } + } }); - - map = mapBuilder.Build(); }); - When(async () => + WhenLater(async () => { await map.Handle( new ProductAddedToCatalogEvent { - Category = "Hybrids", - ProductKey = "c350E" + ProductKey = "c350E", + Category = "NewCategory" }, new ProjectionContext()); }); } [Fact] - public void It_should_properly_pass_the_mapping_to_the_updating_handler() + public void It_should_throw_without_affecting_the_projection() { - projection.Should().BeEquivalentTo(new - { - Id = "c350E", - Category = "Hybrids", - Deleted = false - }); + WhenAction.Should().Throw(); + + existingProjection.Category.Should().Be("OldCategory"); } + } + public class When_an_updating_event_should_ignore_missing_projections : GivenWhenThen + { + private IEventMap map; - [Fact] - public void It_should_throw_if_the_projection_does_not_exist() + public When_an_updating_event_should_ignore_missing_projections() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionModificationBehavior.Throw); + Given(() => + { + var mapBuilder = new EventMapBuilder(); + + mapBuilder + .Map() + .AsUpdateOf(e => e.ProductKey) + .IgnoringMisses() + .Using((p, e, ctx) => + { + p.Category = e.Category; + return Task.FromResult(0); + }); + + map = mapBuilder.Build(new ProjectorMap + { + Update = (key, context, projector, createIfMissing) => Task.FromResult(false) + }); + }); + + WhenLater(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "Hybrids", + ProductKey = "c350E" + }, + new ProjectionContext()); + }); } [Fact] - public void It_should_update_the_projection_if_it_exists() + public void It_should_not_throw() { - options.ExistingProjectionBehavior.Should().Be(ExistingProjectionModificationBehavior.Update); + WhenAction.Should().NotThrow(); } } - - public class When_an_event_is_mapped_as_an_update_if_exists : GivenWhenThen + public class When_an_updating_event_should_create_a_missing_projection : GivenWhenThen { - private ProductCatalogEntry projection; private IEventMap map; - private ProjectionModificationOptions options; + private bool shouldCreate; - public When_an_event_is_mapped_as_an_update_if_exists() + public When_an_updating_event_should_create_a_missing_projection() { Given(() => { @@ -341,35 +371,23 @@ public When_an_event_is_mapped_as_an_update_if_exists() mapBuilder .Map() - .AsUpdateIfExistsOf(e => e.ProductKey) + .AsUpdateOf(e => e.ProductKey) + .CreatingIfMissing() .Using((p, e, ctx) => { p.Category = e.Category; return Task.FromResult(0); }); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => + map = mapBuilder.Build(new ProjectorMap { - projection = new ProductCatalogEntry + Update = (key, context, projector, createIfMissing) => { - Id = key, - }; - - this.options = options; - await projector(projection); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - throw new InvalidOperationException("Deletion should not be called."); - }); - - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); + shouldCreate = true; + + return Task.FromResult(0); + } }); - - map = mapBuilder.Build(); }); When(async () => @@ -385,66 +403,89 @@ await map.Handle( } [Fact] - public void It_should_properly_pass_the_mapping_to_the_updating_handler() + public void It_should_not_throw() { - projection.Should().BeEquivalentTo(new - { - Id = "c350E", - Category = "Hybrids", - Deleted = false - }); + shouldCreate.Should().BeTrue("because that's how the map was configured"); } + } + public class When_an_updating_event_should_handle_misses_manually : GivenWhenThen + { + private IEventMap map; + private string missedKey; - [Fact] - public void It_should_do_nothing_if_the_projection_does_not_exist() + public When_an_updating_event_should_handle_misses_manually() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionModificationBehavior.Ignore); + Given(() => + { + var mapBuilder = new EventMapBuilder(); + + mapBuilder + .Map() + .AsUpdateOf(e => e.ProductKey) + .HandlingMissesUsing((key, context) => + { + missedKey = key; + return true; + }) + .Using((p, e, ctx) => + { + p.Category = e.Category; + return Task.FromResult(0); + }); + + map = mapBuilder.Build(new ProjectorMap + { + Update = (key, context, projector, createIfMissing) => + { + createIfMissing(); + return Task.FromResult(0); + } + }); + }); + + When(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "Hybrids", + ProductKey = "c350E" + }, + new ProjectionContext()); + }); } [Fact] - public void It_should_update_the_projection_if_it_exists() + public void It_should_give_the_custom_handler_a_chance_to_handle_it() { - options.ExistingProjectionBehavior.Should().Be(ExistingProjectionModificationBehavior.Update); + missedKey.Should().Be("c350E"); } } public class When_an_event_is_mapped_as_a_delete : GivenWhenThen { - private ProductCatalogEntry projection; private IEventMap map; - private ProjectionDeletionOptions options; + private bool isDeleted; public When_an_event_is_mapped_as_a_delete() { Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.Map().AsDeleteOf(e => e.ProductKey); + mapBuilder + .Map() + .AsDeleteOf(e => e.ProductKey) + .ThrowingIfMissing(); - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => + map = mapBuilder.Build(new ProjectorMap { - projection = new ProductCatalogEntry + Delete = (key, context) => { - Id = key, - Deleted = true - }; - - this.options = options; - return Task.FromResult(0); - }); - - mapBuilder.HandleProjectionModificationsAs((key, context, projector, options) => - { - throw new InvalidOperationException("Modification should not be called."); - }); - - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); + isDeleted = true; + return Task.FromResult(true); + } }); - - map = mapBuilder.Build(); }); When(async () => @@ -461,60 +502,73 @@ await map.Handle( [Fact] public void It_should_properly_pass_the_mapping_to_the_deleting_handler() { - projection.Should().BeEquivalentTo(new + isDeleted.Should().BeTrue(); + } + } + + public class When_deleting_a_non_existing_event_should_be_ignored : GivenWhenThen + { + private IEventMap map; + + public When_deleting_a_non_existing_event_should_be_ignored() + { + Given(() => { - Id = "c350E", - Category = (string) null, - Deleted = true + var mapBuilder = new EventMapBuilder(); + mapBuilder + .Map() + .AsDeleteOf(e => e.ProductKey) + .IgnoringMisses(); + + map = mapBuilder.Build(new ProjectorMap + { + Delete = (key, context) => Task.FromResult(false) + }); + }); + + WhenLater(async () => + { + await map.Handle( + new ProductDiscontinuedEvent + { + ProductKey = "c350E" + }, + new ProjectionContext()); }); } [Fact] - public void It_should_throw_if_the_projection_does_not_exist() + public void It_should_not_throw() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionDeletionBehavior.Throw); + WhenAction.Should().NotThrow(); } } - public class When_an_event_is_mapped_as_a_delete_if_exists : GivenWhenThen + public class When_deleting_a_non_existing_event_should_be_handled_manually : GivenWhenThen { - private ProductCatalogEntry projection; private IEventMap map; - private ProjectionDeletionOptions options; + private object missedKey; - public When_an_event_is_mapped_as_a_delete_if_exists() + public When_deleting_a_non_existing_event_should_be_handled_manually() { Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.Map().AsDeleteIfExistsOf(e => e.ProductKey); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - projection = new ProductCatalogEntry + mapBuilder + .Map() + .AsDeleteOf(e => e.ProductKey) + .HandlingMissesUsing((key, context) => { - Id = key, - Deleted = true - }; - - this.options = options; - return Task.FromResult(0); - }); - - mapBuilder.HandleProjectionModificationsAs((key, context, projector, options) => - { - throw new InvalidOperationException("Modification should not be called."); - }); + missedKey = key; + }); - mapBuilder.HandleCustomActionsAs((context, projector) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Delete = (key, context) => Task.FromResult(false) }); - - map = mapBuilder.Build(); }); - When(async () => + WhenLater(async () => { await map.Handle( new ProductDiscontinuedEvent @@ -526,35 +580,22 @@ await map.Handle( } [Fact] - public void It_should_properly_pass_the_mapping_to_the_deleting_handler() - { - projection.Should().BeEquivalentTo(new - { - Id = "c350E", - Category = (string)null, - Deleted = true - }); - } - - [Fact] - public void It_should_do_nothing_if_the_projection_does_not_exist() + public void It_should_not_throw() { - options.MissingProjectionBehavior.Should().Be(MissingProjectionDeletionBehavior.Ignore); + WhenAction.Should().NotThrow(); } } public class When_an_event_is_mapped_as_a_custom_action : GivenWhenThen { - private string involvedKey; private IEventMap map; + private string involvedKey; public When_an_event_is_mapped_as_a_custom_action() { Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleCustomActionsAs((context, projector) => projector()); - mapBuilder.Map().As((@event, context) => { involvedKey = @event.ProductKey; @@ -562,7 +603,10 @@ public When_an_event_is_mapped_as_a_custom_action() return Task.FromResult(0); }); - map = mapBuilder.Build(); + map = mapBuilder.Build(new ProjectorMap + { + Custom = (context, projector) => projector() + }); }); When(async () => @@ -583,13 +627,63 @@ public void It_should_properly_pass_the_mapping_to_the_custom_handler() } } - public class When_a_condition_is_not_met : GivenWhenThen + public class When_a_global_filter_is_not_met : GivenWhenThen { - private readonly ProductCatalogEntry projection = new ProductCatalogEntry + private string involvedKey = null; + private IEventMap map; + + public When_a_global_filter_is_not_met() + { + Given(() => + { + var mapBuilder = new EventMapBuilder() + .Where((@event, context) => + { + if (@event is ProductAddedToCatalogEvent addedEvent) + { + return Task.FromResult(addedEvent.Category == "Electric"); + } + + return Task.FromResult(true); + }); + + mapBuilder + .Map() + .As((e, ctx) => + { + involvedKey = e.ProductKey; + + return Task.FromResult(0); + }); + + map = mapBuilder.Build(new ProjectorMap + { + Custom = (context, projector) => projector() + }); + }); + + When(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "Hybrids", + ProductKey = "c350E" + }, + new object()); + }); + } + + [Fact] + public void It_should_not_invoke_any_handler() { - Id = "c350E", - Category = "Electrics" - }; + involvedKey.Should().BeNull(); + } + } + + public class When_a_condition_is_not_met : GivenWhenThen + { + private string involvedKey = null; private IEventMap map; public When_a_condition_is_not_met() @@ -597,21 +691,20 @@ public When_a_condition_is_not_met() Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.Map() + mapBuilder + .Map() .When(e => e.Category == "Electric") .As((e, ctx) => { - projection.Category = e.Category; + involvedKey = e.ProductKey; return Task.FromResult(0); }); - mapBuilder.HandleCustomActionsAs((context, projector) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Custom = (context, projector) => projector() }); - - map = mapBuilder.Build(); }); When(async () => @@ -629,21 +722,13 @@ await map.Handle( [Fact] public void It_should_not_invoke_any_handler() { - projection.Should().BeEquivalentTo(new - { - Id = "c350E", - Category = "Electrics", - Deleted = false - }); + involvedKey.Should().BeNull(); } } public class When_a_condition_is_met : GivenWhenThen { - private readonly ProductCatalogEntry projection = new ProductCatalogEntry - { - Id = "c350E" - }; + private string involvedKey; private IEventMap map; public When_a_condition_is_met() @@ -651,18 +736,19 @@ public When_a_condition_is_met() Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleCustomActionsAs((context, projector) => projector()); - - mapBuilder.Map() + mapBuilder + .Map() .When(e => e.Category == "Hybrids") .As((e, ctx) => { - projection.Category = e.Category; - + involvedKey = e.ProductKey; return Task.FromResult(0); }); - map = mapBuilder.Build(); + map = mapBuilder.Build(new ProjectorMap + { + Custom = (context, projector) => projector() + }); }); When(async () => @@ -680,12 +766,7 @@ await map.Handle( [Fact] public void It_should_invoke_the_right_handler() { - projection.Should().BeEquivalentTo(new - { - Id = "c350E", - Category = "Hybrids", - Deleted = false - }); + involvedKey.Should().Be("c350E"); } } @@ -704,14 +785,14 @@ public When_multiple_conditions_are_registered() mapBuilder.Map() .When(e => e.Category != "Hybrids") .When(e => e.Category != "Electrics") - .As((e, ctx) => {}); + .As((e, ctx) => + { + }); - mapBuilder.HandleCustomActionsAs((context, projector) => + var map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Custom action should not be called."); + Custom = (context, projector) =>throw new InvalidOperationException("Custom action should not be called.") }); - - var map = mapBuilder.Build(); }; }); } @@ -735,22 +816,6 @@ public When_an_event_is_mapped_as_a_custom_action_on_a_projection() { var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleCustomActionsAs((context, projector) => - { - customActionDecoratorExecuted = true; - return projector(); - }); - - mapBuilder.HandleProjectionModificationsAs((key, context, projector, options) => - { - throw new InvalidOperationException("Modification should not be called."); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - throw new InvalidOperationException("Deletion should not be called."); - }); - mapBuilder.Map().As((@event, context) => { involvedKey = @event.ProductKey; @@ -758,7 +823,14 @@ public When_an_event_is_mapped_as_a_custom_action_on_a_projection() return Task.FromResult(0); }); - map = mapBuilder.Build(); + map = mapBuilder.Build(new ProjectorMap + { + Custom = (context, projector) => + { + customActionDecoratorExecuted = true; + return projector(); + } + }); }); When(async () => @@ -785,29 +857,28 @@ public void It_should_allow_decorating_the_custom_handler() } } - public class When_a_condition_is_not_met_on_a_projection : GivenWhenThen + public class When_a_global_filter_is_not_met_on_a_projection : GivenWhenThen { private ProductCatalogEntry projection; private IEventMap map; - public When_a_condition_is_not_met_on_a_projection() + public When_a_global_filter_is_not_met_on_a_projection() { Given(() => { - var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => - { - projection = new ProductCatalogEntry + var mapBuilder = new EventMapBuilder() + .Where((@event, context) => { - Id = key, - }; + if (@event is ProductAddedToCatalogEvent addedEvent) + { + return Task.FromResult(addedEvent.Category == "Electric"); + } - await projector(projection); - }); + return Task.FromResult(true); + }); mapBuilder .Map() - .When(e => e.Category == "Electric") .AsUpdateOf(e => e.ProductKey) .Using((p, e, ctx) => { @@ -816,17 +887,18 @@ public When_a_condition_is_not_met_on_a_projection() return Task.FromResult(0); }); - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Deletion should not be called."); - }); + Update = async (key, context, projector, createIfMissing) => + { + projection = new ProductCatalogEntry + { + Id = key, + }; - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); + await projector(projection); + } }); - - map = mapBuilder.Build(); }); When(async () => @@ -848,36 +920,71 @@ public void It_should_not_invoke_any_handler() } } - public class When_a_condition_is_met_on_a_projection : GivenWhenThen + public class When_a_condition_is_not_met_on_a_projection : GivenWhenThen { private ProductCatalogEntry projection; private IEventMap map; - public When_a_condition_is_met_on_a_projection() + public When_a_condition_is_not_met_on_a_projection() { Given(() => { var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleProjectionModificationsAs(async (key, context, projector, options) => - { - projection = new ProductCatalogEntry + mapBuilder + .Map() + .When(e => e.Category == "Electric") + .AsUpdateOf(e => e.ProductKey) + .Using((p, e, ctx) => { - Id = key, - }; + p.Category = e.Category; - await projector(projection); - }); + return Task.FromResult(0); + }); - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => + map = mapBuilder.Build(new ProjectorMap { - throw new InvalidOperationException("Deletion should not be called."); - }); + Update = async (key, context, projector, createIfMissing) => + { + projection = new ProductCatalogEntry + { + Id = key, + }; - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); + await projector(projection); + } }); + }); + + When(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "Hybrids", + ProductKey = "c350E" + }, + new ProjectionContext()); + }); + } + + [Fact] + public void It_should_not_invoke_any_handler() + { + projection.Should().BeNull(); + } + } + + public class When_a_condition_is_met_on_a_projection : GivenWhenThen + { + private ProductCatalogEntry projection; + private IEventMap map; + + public When_a_condition_is_met_on_a_projection() + { + Given(() => + { + var mapBuilder = new EventMapBuilder(); mapBuilder .Map() @@ -889,7 +996,18 @@ public When_a_condition_is_met_on_a_projection() return Task.FromResult(0); }); - map = mapBuilder.Build(); + map = mapBuilder.Build(new ProjectorMap + { + Update = async (key, context, projector, createIfMissing) => + { + projection = new ProductCatalogEntry + { + Id = key, + }; + + await projector(projection); + } + }); }); When(async () => @@ -928,21 +1046,6 @@ public When_multiple_conditions_are_registered_on_a_projection() { var mapBuilder = new EventMapBuilder(); - mapBuilder.HandleProjectionModificationsAs((key, context, projector, options) => - { - throw new InvalidOperationException("Modification should not be called."); - }); - - mapBuilder.HandleProjectionDeletionsAs((key, context, options) => - { - throw new InvalidOperationException("Deletion should not be called."); - }); - - mapBuilder.HandleCustomActionsAs((context, projector) => - { - throw new InvalidOperationException("Custom action should not be called."); - }); - mapBuilder.Map() .When(e => e.Category == "Hybrids") .AsUpdateOf(e => e.ProductKey).Using((p, e, ctx) => p.Category = e.Category); @@ -951,7 +1054,7 @@ public When_multiple_conditions_are_registered_on_a_projection() .When(e => e.Category == "Electrics") .AsDeleteOf(e => e.ProductKey); - var map = mapBuilder.Build(); + var map = mapBuilder.Build(new ProjectorMap()); }; }); }