diff --git a/Src/LiquidProjections/EventMapBuilder.cs b/Src/LiquidProjections/EventMapBuilder.cs index 7fcaa75..2eb4302 100644 --- a/Src/LiquidProjections/EventMapBuilder.cs +++ b/Src/LiquidProjections/EventMapBuilder.cs @@ -179,7 +179,7 @@ public IEventMap Build(ProjectorMap proje private sealed class CrudAction : ICrudAction { private readonly IAction actionBuilder; - private readonly Func > getProjector; + private readonly Func> getProjector; public CrudAction(EventMapBuilder parent) { @@ -187,6 +187,16 @@ public CrudAction(EventMapBuilder parent) getProjector = () => parent.projector; } + public ICreateAction AsCreateOf(Func getKey) + { + if (getKey == null) + { + throw new ArgumentNullException(nameof(getKey)); + } + + return new CreateAction(actionBuilder, getProjector, getKey); + } + public ICreateAction AsCreateOf(Func getKey) { if (getKey == null) @@ -218,6 +228,16 @@ public IDeleteAction AsDeleteOf(Func getKe return new DeleteAction(actionBuilder, getProjector, getKey); } + public IDeleteAction AsDeleteOf(Func getKey) + { + if (getKey == null) + { + throw new ArgumentNullException(nameof(getKey)); + } + + return new DeleteAction(actionBuilder, getProjector, getKey); + } + public IDeleteAction AsDeleteIfExistsOf(Func getKey) { return AsDeleteOf(getKey).IgnoringMisses(); @@ -230,6 +250,16 @@ public IUpdateAction AsUpdateOf(Func getKey(anEvent)); + } + + public IUpdateAction AsUpdateOf(Func getKey) + { + if (getKey == null) + { + throw new ArgumentNullException(nameof(getKey)); + } + return new UpdateAction(actionBuilder, getProjector, getKey); } @@ -267,15 +297,27 @@ private sealed class CreateAction : ICreateAction private readonly IAction actionBuilder; private readonly Func> projector; - private readonly Func getKey; + private readonly Func getKey; public CreateAction(IAction actionBuilder, - Func> projector, Func getKey) + Func> projector, Func getKey) { this.actionBuilder = actionBuilder; this.projector = projector; this.getKey = getKey; + shouldOverwrite = (existingProjection, @event, context) => + throw new ProjectionException( + $"Projection {typeof(TProjection)} with key {getKey(@event,context)}already exists."); + } + + public CreateAction(IAction actionBuilder, + Func> projector, Func getKey) + { + this.actionBuilder = actionBuilder; + this.projector = projector; + this.getKey = (@event, context) =>getKey(@event); + shouldOverwrite = (existingProjection, @event, context) => throw new ProjectionException( $"Projection {typeof(TProjection)} with key {getKey(@event)}already exists."); @@ -289,7 +331,7 @@ public ICreateAction Using(Func this.projector().Create( - getKey(anEvent), + getKey(anEvent,context), context, projection => projector(projection, anEvent, context), existingProjection => shouldOverwrite(existingProjection, anEvent, context))); @@ -320,11 +362,11 @@ private sealed class UpdateAction : IUpdateAction actionBuilder; private readonly Func> projector; - private readonly Func getKey; + private readonly Func getKey; private Func handleMissesUsing; public UpdateAction(IAction actionBuilder, - Func> projector, Func getKey) + Func> projector, Func getKey) { this.projector = projector; this.actionBuilder = actionBuilder; @@ -347,7 +389,7 @@ public IUpdateAction Using(Func projector, TEvent anEvent, TContext context) { - var key = getKey(anEvent); + var key = getKey(anEvent,context); await this.projector().Update( key, @@ -386,16 +428,24 @@ private class DeleteAction : IDeleteAction private Action handleMissing; public DeleteAction(IAction actionBuilder, - Func> projector, Func getKey) + Func> projector, Func getKey) { actionBuilder.As((anEvent, context) => OnDelete(projector(), getKey, anEvent, context)); ThrowingIfMissing(); } - private async Task OnDelete(ProjectorMap projector, Func getKey, TEvent anEvent, TContext context) + public DeleteAction(IAction actionBuilder, + Func> projector, Func getKey) + { + actionBuilder.As((anEvent, context) => OnDelete(projector(), (anEvent1, context1) => getKey(anEvent1), anEvent, context)); + + ThrowingIfMissing(); + } + + private async Task OnDelete(ProjectorMap projector, Func getKey, TEvent anEvent, TContext context) { - TKey key = getKey(anEvent); + TKey key = getKey(anEvent, context); bool deleted = await projector.Delete(key, context); if (!deleted) { diff --git a/Src/LiquidProjections/MapBuilding/IAction.cs b/Src/LiquidProjections/MapBuilding/IAction.cs index 49260d3..a1923d9 100644 --- a/Src/LiquidProjections/MapBuilding/IAction.cs +++ b/Src/LiquidProjections/MapBuilding/IAction.cs @@ -52,6 +52,17 @@ public interface ICrudAction : IAction ICreateAction AsCreateOf(Func getKey); + /// + /// Continues configuring a handler for events of type . + /// Specifies that a new projection with the specified key will be created for the event handler. + /// An exception will be thrown if a projection with such key already exists. + /// + /// The delegate that determines the projection key for the event. + /// + /// that allows to continue configuring the handler. + /// + ICreateAction AsCreateOf(Func getKey); + /// /// Continues configuring a handler for events of type . /// Specifies that a new projection with the specified key will be created for the event handler. @@ -76,6 +87,17 @@ public interface ICrudAction : IAction IUpdateAction AsUpdateOf(Func getKey); + /// + /// Continues configuring a handler for events of type . + /// Specifies that the projection with the specified key will be updated by the event handler. + /// An exception will be thrown if a projection with such key does not exist. + /// + /// The delegate that determines the projection key for the event. + /// + /// that allows to continue configuring the handler. + /// + IUpdateAction AsUpdateOf(Func getKey); + /// /// Continues configuring a handler for events of type . /// Specifies that the projection with the specified key will be updated by the event handler. @@ -99,7 +121,7 @@ public interface ICrudAction : IAction [Obsolete("Use AsCreateOf().OverwritingDuplicates() instead")] - ICreateAction AsCreateOrUpdateOf(Func getKey); + ICreateAction AsCreateOrUpdateOf(Func getKey); /// /// Finishes configuring a handler for events of type . @@ -109,6 +131,14 @@ public interface ICrudAction : IActionThe delegate that determines the projection key for the event. IDeleteAction AsDeleteOf(Func getKey); + /// + /// Finishes configuring a handler for events of type . + /// Specifies that the projection with the specified key will be deleted when handling the event. + /// An exception will be thrown if a projection with such key does not exist. + /// + /// The delegate that determines the projection key for the event. + IDeleteAction AsDeleteOf(Func getKey); + /// /// Finishes configuring a handler for events of type . /// Specifies that the projection with the specified key will be deleted when handling the event. diff --git a/Tests/LiquidProjections.Specs/EventMapSpecs.cs b/Tests/LiquidProjections.Specs/EventMapSpecs.cs index 8f636d5..e9b3782 100644 --- a/Tests/LiquidProjections.Specs/EventMapSpecs.cs +++ b/Tests/LiquidProjections.Specs/EventMapSpecs.cs @@ -44,10 +44,10 @@ public When_event_should_create_a_new_projection() When(async () => { await map.Handle(new ProductAddedToCatalogEvent - { - Category = "Hybrids", - ProductKey = "c350E" - }, + { + Category = "Hybrids", + ProductKey = "c350E" + }, new ProjectionContext()); }); } @@ -64,6 +64,59 @@ public void It_should_properly_pass_the_mapping_to_the_creating_handler() } } + public class When_event_should_create_a_new_projection_from_context : GivenWhenThen + { + private ProductCatalogEntry projection; + private IEventMap map; + + public When_event_should_create_a_new_projection_from_context() + { + Given(() => + { + var mapBuilder = new EventMapBuilder(); + mapBuilder.Map().AsCreateOf((e, context) => context.EventHeaders["ProductId"] as string).Using((p, e, ctx) => + { + p.Category = e.Category; + + return Task.FromResult(0); + }); + + map = mapBuilder.Build(new ProjectorMap + { + Create = async (key, context, projector, shouldOverwrite) => + { + projection = new ProductCatalogEntry + { + Id = key, + }; + + await projector(projection); + } + }); + }); + + When(async () => + { + await map.Handle(new ProductAddedToCatalogEvent + { + Category = "Hybrids" + }, + new ProjectionContext() { EventHeaders = new Dictionary(1) { { "ProductId", "1234" } } }); + }); + } + + [Fact] + public void It_should_properly_pass_the_mapping_to_the_creating_handler() + { + projection.Should().BeEquivalentTo(new + { + Id = "1234", + Category = "Hybrids", + Deleted = false + }); + } + } + public class When_a_creating_event_must_ignore_an_existing_projection : GivenWhenThen { private ProductCatalogEntry existingProjection; @@ -84,7 +137,7 @@ public When_a_creating_event_must_ignore_an_existing_projection() p.Category = e.Category; return Task.FromResult(0); }); - + existingProjection = new ProductCatalogEntry { Id = "c350E", @@ -235,10 +288,10 @@ public When_a_creating_event_should_allow_manual_handling_of_duplicates() When(async () => { await map.Handle(new ProductAddedToCatalogEvent - { - Category = "NewCategory", - ProductKey = "c350E" - }, + { + Category = "NewCategory", + ProductKey = "c350E" + }, new ProjectionContext()); }); } @@ -314,7 +367,7 @@ public void It_should_throw_without_affecting_the_projection() existingProjection.Category.Should().Be("OldCategory"); } - } + } public class When_an_updating_event_should_ignore_missing_projections : GivenWhenThen { @@ -360,6 +413,7 @@ public void It_should_not_throw() WhenAction.Should().NotThrow(); } } + public class When_an_updating_event_should_create_a_missing_projection : GivenWhenThen { private IEventMap map; @@ -386,7 +440,7 @@ public When_an_updating_event_should_create_a_missing_projection() Update = (key, context, projector, createIfMissing) => { shouldCreate = true; - + return Task.FromResult(0); } }); @@ -411,6 +465,57 @@ public void It_should_not_throw() } } + public class When_an_updating_event_should_create_a_missing_projection_from_context : GivenWhenThen + { + private IEventMap map; + private bool shouldCreate; + + public When_an_updating_event_should_create_a_missing_projection_from_context() + { + Given(() => + { + var mapBuilder = new EventMapBuilder(); + + mapBuilder + .Map() + .AsUpdateOf((e, context) => context.EventHeaders["ProductId"] as string) + .CreatingIfMissing() + .Using((p, e, ctx) => + { + p.Category = e.Category; + return Task.FromResult(0); + }); + + map = mapBuilder.Build(new ProjectorMap + { + Update = (key, context, projector, createIfMissing) => + { + shouldCreate = true; + + return Task.FromResult(0); + } + }); + }); + + When(async () => + { + await map.Handle( + new ProductAddedToCatalogEvent + { + Category = "Hybrids", + ProductKey = "c350E" + }, + new ProjectionContext() { EventHeaders = new Dictionary(1) { { "ProductId", "1234" } } }); + }); + } + + [Fact] + public void It_should_not_throw() + { + 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; @@ -486,7 +591,7 @@ public When_an_event_is_mapped_as_a_delete() { isDeleted = true; return Task.FromResult(true); - } + } }); }); @@ -524,7 +629,7 @@ public When_deleting_a_non_existing_event_should_be_ignored() map = mapBuilder.Build(new ProjectorMap { - Delete = (key, context) => Task.FromResult(false) + Delete = (key, context) => Task.FromResult(false) }); }); @@ -566,7 +671,7 @@ public When_deleting_a_non_existing_event_should_be_handled_manually() map = mapBuilder.Build(new ProjectorMap { - Delete = (key, context) => Task.FromResult(false) + Delete = (key, context) => Task.FromResult(false) }); }); @@ -588,6 +693,48 @@ public void It_should_not_throw() } } + public class When_deleting_a_non_existing_event_should_be_handled_manually_from_context : GivenWhenThen + { + private IEventMap map; + private object missedKey; + + public When_deleting_a_non_existing_event_should_be_handled_manually_from_context() + { + Given(() => + { + var mapBuilder = new EventMapBuilder(); + mapBuilder + .Map() + .AsDeleteOf((e, context) => context.EventHeaders["ProductId"] as string) + .HandlingMissesUsing((key, context) => + { + missedKey = key; + }); + + map = mapBuilder.Build(new ProjectorMap + { + Delete = (key, context) => Task.FromResult(false) + }); + }); + + WhenLater(async () => + { + await map.Handle( + new ProductDiscontinuedEvent + { + ProductKey = "c350E" + }, + new ProjectionContext() { EventHeaders = new Dictionary(1) { { "ProductId", "1234" } } }); + }); + } + + [Fact] + public void It_should_not_throw() + { + WhenAction.Should().NotThrow(); + } + } + public class When_an_event_is_mapped_as_a_custom_action : GivenWhenThen { private IEventMap map; @@ -648,7 +795,7 @@ public When_a_global_filter_is_not_met() return Task.FromResult(true); }); - + mapBuilder .Map() .As((e, ctx) => @@ -660,7 +807,7 @@ public When_a_global_filter_is_not_met() map = mapBuilder.Build(new ProjectorMap { - Custom = (context, projector) => projector() + Custom = (context, projector) => projector() }); }); @@ -705,7 +852,7 @@ public When_a_condition_is_not_met() map = mapBuilder.Build(new ProjectorMap { - Custom = (context, projector) => projector() + Custom = (context, projector) => projector() }); }); @@ -793,7 +940,7 @@ public When_multiple_conditions_are_registered() var map = mapBuilder.Build(new ProjectorMap { - Custom = (context, projector) =>throw new InvalidOperationException("Custom action should not be called.") + Custom = (context, projector) => throw new InvalidOperationException("Custom action should not be called.") }); }; }); @@ -1073,7 +1220,7 @@ public class When_an_event_is_mapped_by_a_base_class_and_an_event_of_child_type_ private IEventMap map; private ProductCatalogEntry projection; - + public When_an_event_is_mapped_by_a_base_class_and_an_event_of_child_type_is_handled() { Given(() => @@ -1102,7 +1249,7 @@ public When_an_event_is_mapped_by_a_base_class_and_an_event_of_child_type_is_han } }); }); - + When(async () => { await map.Handle( @@ -1121,7 +1268,7 @@ public void It_should_invoke_the_handler_configured_on_the_base_class() projection.Category.Should().Be("All Products"); } } - + public class When_mappings_for_the_base_and_concrete_event_types_are_both_configured : GivenWhenThen { private IEventMap map; @@ -1129,7 +1276,7 @@ public class When_mappings_for_the_base_and_concrete_event_types_are_both_config private ProductCatalogEntry projection; private Type lastHandlerInvoked; - + public When_mappings_for_the_base_and_concrete_event_types_are_both_configured() { Given(() => @@ -1147,7 +1294,7 @@ public When_mappings_for_the_base_and_concrete_event_types_are_both_configured() lastHandlerInvoked = typeof(ProductEvent); return Task.FromResult(0); }); - + mapBuilder .Map() .AsUpdateOf(e => e.ProductKey) @@ -1177,7 +1324,7 @@ public When_mappings_for_the_base_and_concrete_event_types_are_both_configured() } }); }); - + When(async () => { await map.Handle(