From a928b93f1ff0381d750ab78a97c55630aa202f20 Mon Sep 17 00:00:00 2001 From: Tomohisa Takaoka Date: Fri, 27 Dec 2024 22:19:25 -0800 Subject: [PATCH] Add Publish Only --- src/Sekiban.Pure/Class1.cs | 195 +++++++++++++++++++++++----- tests/Pure.Domain.Test/UnitTest1.cs | 98 ++++++++++++++ 2 files changed, 258 insertions(+), 35 deletions(-) diff --git a/src/Sekiban.Pure/Class1.cs b/src/Sekiban.Pure/Class1.cs index 20c92cb2..f02b58de 100644 --- a/src/Sekiban.Pure/Class1.cs +++ b/src/Sekiban.Pure/Class1.cs @@ -46,7 +46,7 @@ public ResultBox Project(IEvent ev, IAggregateProjector projector) => { Payload = projector.Project(Payload, ev), LastSortableUniqueId = ev.SortableUniqueId, - Version = ev.Version + Version = Version + 1 }; public ResultBox Project(List events, IAggregateProjector projector) => ResultBox .FromValue(events) @@ -75,17 +75,30 @@ public interface IAggregateProjector public IAggregatePayload Project(IAggregatePayload payload, IEvent ev); public virtual string GetVersion() => "initial"; } -public interface ICommandContext where TAggregatePayload : IAggregatePayload +public class NoneAggregateProjector : IAggregateProjector { - public List Events { get; } - internal IAggregate GetAggregateCommon(); - public EventOrNone AppendEvent(IEventPayload eventPayload); - internal CommandExecuted GetCommandExecuted(List producedEvents) => new( + public static NoneAggregateProjector Empty => new(); + public IAggregatePayload Project(IAggregatePayload payload, IEvent ev) => payload; +} +public interface ICommandContext : ICommandContextWithoutState + where TAggregatePayload : IAggregatePayload +{ + CommandExecuted ICommandContextWithoutState.GetCommandExecuted(List producedEvents) => new( GetAggregateCommon().PartitionKeys, GetAggregateCommon().LastSortableUniqueId, producedEvents); + internal IAggregate GetAggregateCommon(); public ResultBox> GetAggregate(); } +public interface ICommandContextWithoutState +{ + public List Events { get; } + public PartitionKeys GetPartitionKeys(); + public int GetNextVersion(); + public int GetCurrentVersion(); + internal CommandExecuted GetCommandExecuted(List producedEvents); + public ResultBox AppendEvent(IEventPayload eventPayload); +} public interface ICommand; public interface ICommandWithAggregateRestriction : ICommand where TAggregatePayload : IAggregatePayload; @@ -327,23 +340,54 @@ public Task> ExecuteGeneral specifyPartitionKeys(command) .ToResultBox() - .Combine(keys => Repository.Load(keys, projector)) - .Verify((keys, aggregate) => VerifyAggregateType(command, aggregate)) .Combine( - (partitionKeys, aggregate) => ResultBox.FromValue( - new CommandContext(aggregate, projector, EventTypes))) + keys => CreateCommandContextWithoutState( + command, + keys, + projector, + handler)) .Combine( - (partitionKeys, aggregate, context) => RunHandler(command, context, inject, handler) - .Conveyor(eventOrNone => EventToCommandExecuted(context, eventOrNone))) - .Conveyor(values => Repository.Save(values.Value4.ProducedEvents).Remap(_ => values)) + (partitionKeys, context) => + RunHandler(command, context, inject, handler) + .Conveyor(eventOrNone => EventToCommandExecuted(context, eventOrNone))) + .Conveyor(values => Repository.Save(values.Value3.ProducedEvents).Remap(_ => values)) .Conveyor( - (partitionKeys, aggregate, context, executed) => ResultBox.FromValue( + (partitionKeys, context, executed) => ResultBox.FromValue( new CommandResponse( partitionKeys, executed.ProducedEvents, executed.ProducedEvents.Count > 0 ? executed.ProducedEvents.Last().Version - : aggregate.Version))); + : context.GetCurrentVersion()))); + private ResultBox + CreateCommandContextWithoutState( + TCommand command, + PartitionKeys partitionKeys, + IAggregateProjector projector, + Delegate handler) where TCommand : ICommand where TAggregatePayload : IAggregatePayload => + handler switch + { + Func> handler1 => + ResultBox.FromValue( + new CommandContextWithoutState(partitionKeys, EventTypes)), + Func>> handler1 => + ResultBox.FromValue( + new CommandContextWithoutState(partitionKeys, EventTypes)), + Func> handler1 => + ResultBox.FromValue( + new CommandContextWithoutState(partitionKeys, EventTypes)), + Func>> handler1 => + ResultBox.FromValue( + new CommandContextWithoutState(partitionKeys, EventTypes)), + _ => ResultBox + .Start + .Conveyor(keys => Repository.Load(partitionKeys, projector)) + .Verify(aggregate => VerifyAggregateType(command, aggregate)) + .Conveyor( + aggregate => ResultBox.FromValue( + new CommandContext(aggregate, projector, EventTypes))) + }; + private ExceptionOrNone VerifyAggregateType(TCommand command, Aggregate aggregate) where TCommand : ICommand where TAggregatePayload : IAggregatePayload => aggregate.GetPayload() is not TAggregatePayload @@ -356,38 +400,44 @@ private ExceptionOrNone VerifyAggregateType(TComman private Task> RunHandler( TCommand command, - ICommandContext context, + ICommandContextWithoutState context, OptionalValue inject, Delegate handler) where TCommand : ICommand where TAggregatePayload : IAggregatePayload => - (handler, inject.HasValue) switch + (handler, inject.HasValue, context) switch { - (Func, ResultBox> handler1, _) => handler1( - command, - context) - .ToTask(), - (Func, ResultBox> handler1, true) => - handler1(command, inject.GetValue(), context).ToTask(), - (Func, Task>> handler1, _) => handler1( - command, - context), - (Func, Task>> handler1, true) - => handler1(command, inject.GetValue(), context), + (Func> handler1, _, { } contextWithout) => + Task.FromResult(handler1(command, contextWithout)), + (Func>> handler1, _, { } contextWithout) + => handler1(command, contextWithout), + (Func> handler1, true, { + } contextWithout) => Task.FromResult(handler1(command, inject.GetValue(), contextWithout)), + (Func>> handler1, true, { + } contextWithout) => handler1(command, inject.GetValue(), contextWithout), + (Func, ResultBox> handler1, _, + ICommandContext stateContext) => handler1(command, stateContext).ToTask(), + (Func, ResultBox> handler1, true, + ICommandContext stateContext) => handler1(command, inject.GetValue(), stateContext) + .ToTask(), + (Func, Task>> handler1, _, + ICommandContext stateContext) => handler1(command, stateContext), + (Func, Task>> handler1, true, + ICommandContext stateContext) => handler1(command, inject.GetValue(), stateContext), _ => ResultBox .FromException( new SekibanCommandHandlerNotMatchException( $"{handler.GetType().Name} does not match as command handler")) .ToTask() }; - private ResultBox EventToCommandExecuted( - ICommandContext commandContext, - EventOrNone eventOrNone) where TAggregatePayload : IAggregatePayload => + private ResultBox EventToCommandExecuted( + ICommandContextWithoutState commandContext, + EventOrNone eventOrNone) => (eventOrNone.HasEvent ? EventTypes .GenerateTypedEvent( eventOrNone.GetValue(), - commandContext.GetAggregateCommon().PartitionKeys, + commandContext.GetPartitionKeys(), SortableUniqueIdValue.Generate(SekibanDateProducer.GetRegistered().UtcNow, Guid.NewGuid()), - commandContext.GetAggregateCommon().Version + 1) + commandContext.GetNextVersion()) .Remap(ev => commandContext.Events.Append(ev).ToList()) : ResultBox.FromValue(commandContext.Events)).Remap(commandContext.GetCommandExecuted); #endregion @@ -402,8 +452,12 @@ public class CommandContext( public IAggregateProjector Projector { get; } = projector; public IEventTypes EventTypes { get; } = eventTypes; public List Events { get; } = new(); + public PartitionKeys GetPartitionKeys() => Aggregate.PartitionKeys; + public int GetNextVersion() => Aggregate.Version + 1; + public int GetCurrentVersion() => Aggregate.Version; public IAggregate GetAggregateCommon() => Aggregate; - public EventOrNone AppendEvent(IEventPayload eventPayload) + public ResultBox> GetAggregate() => Aggregate.ToTypedPayload(); + public ResultBox AppendEvent(IEventPayload eventPayload) { var toAdd = EventTypes.GenerateTypedEvent( eventPayload, @@ -418,7 +472,26 @@ public EventOrNone AppendEvent(IEventPayload eventPayload) Events.Add(ev); return EventOrNone.Empty; } - public ResultBox> GetAggregate() => Aggregate.ToTypedPayload(); +} +public record CommandContextWithoutState(PartitionKeys PartitionKeys, IEventTypes EventTypes) + : ICommandContextWithoutState +{ + public List Events { get; } = new(); + public PartitionKeys GetPartitionKeys() => PartitionKeys; + public int GetNextVersion() => 0; + public int GetCurrentVersion() => 0; + public CommandExecuted GetCommandExecuted(List producedEvents) => new( + PartitionKeys, + SortableUniqueIdValue.Generate(SekibanDateProducer.GetRegistered().UtcNow, Guid.NewGuid()), + producedEvents); + public ResultBox AppendEvent(IEventPayload eventPayload) => EventTypes + .GenerateTypedEvent( + eventPayload, + PartitionKeys, + SortableUniqueIdValue.Generate(SekibanDateProducer.GetRegistered().UtcNow, Guid.NewGuid()), + 0) + .Do(ev => Events.Add(ev)) + .Remap(_ => EventOrNone.Empty); } public class Repository { @@ -572,3 +645,55 @@ public record CommandResourceWithInjectTask Handler; public OptionalValue GetAggregatePayloadType() => typeof(TAggregatePayload); } +public record CommandResourcePublishOnly( + Func SpecifyPartitionKeys, + Func> Handler) : ICommandResource + where TCommand : ICommand, IEquatable +{ + + public Func GetSpecifyPartitionKeysFunc() => SpecifyPartitionKeys; + public OptionalValue GetAggregatePayloadType() => OptionalValue.Empty; + public Type GetCommandType() => typeof(TCommand); + public IAggregateProjector GetProjector() => NoneAggregateProjector.Empty; + public object? GetInjection() => null; + public Delegate GetHandler() => Handler; +} +public record CommandResourcePublishOnlyWithInject( + Func SpecifyPartitionKeys, + TInject? Injection, + Func> Handler) : ICommandResource + where TCommand : ICommand, IEquatable +{ + + public Func GetSpecifyPartitionKeysFunc() => SpecifyPartitionKeys; + public OptionalValue GetAggregatePayloadType() => OptionalValue.Empty; + public Type GetCommandType() => typeof(TCommand); + public IAggregateProjector GetProjector() => NoneAggregateProjector.Empty; + public object? GetInjection() => Injection; + public Delegate GetHandler() => Handler; +} +public record CommandResourcePublishOnlyTask( + Func SpecifyPartitionKeys, + Func>> Handler) : ICommandResource + where TCommand : ICommand, IEquatable +{ + public Func GetSpecifyPartitionKeysFunc() => SpecifyPartitionKeys; + public OptionalValue GetAggregatePayloadType() => OptionalValue.Empty; + public Type GetCommandType() => typeof(TCommand); + public IAggregateProjector GetProjector() => NoneAggregateProjector.Empty; + public object? GetInjection() => null; + public Delegate GetHandler() => Handler; +} +public record CommandResourcePublishOnlyWithInjectTask( + Func SpecifyPartitionKeys, + TInject? Injection, + Func>> Handler) + : ICommandResource where TCommand : ICommand, IEquatable +{ + public Func GetSpecifyPartitionKeysFunc() => SpecifyPartitionKeys; + public OptionalValue GetAggregatePayloadType() => OptionalValue.Empty; + public Type GetCommandType() => typeof(TCommand); + public IAggregateProjector GetProjector() => NoneAggregateProjector.Empty; + public object? GetInjection() => Injection; + public Delegate GetHandler() => Handler; +} diff --git a/tests/Pure.Domain.Test/UnitTest1.cs b/tests/Pure.Domain.Test/UnitTest1.cs index 6c61897d..7536adbc 100644 --- a/tests/Pure.Domain.Test/UnitTest1.cs +++ b/tests/Pure.Domain.Test/UnitTest1.cs @@ -415,4 +415,102 @@ public async Task ExecuteWithoutGeneric() var result = await executor.Execute(createCommand); Assert.True(result.IsSuccess); } + [Fact] + public async Task ExecuteWithResource1() + { + var executor = new CommandExecutor { EventTypes = new PureDomainEventTypes() }; + var createCommand = new RegisterBranch("a"); + var partitionKeys = PartitionKeys.Generate(); + var result = await executor.ExecuteWithResource( + createCommand, + new CommandResource( + command => partitionKeys, + (command, _) => EventOrNone.Event(new BranchCreated(command.Name)))); + Assert.True(result.IsSuccess); + var aggregate = Repository.Load(partitionKeys); + Assert.NotNull(aggregate); + Assert.IsType(aggregate.GetValue().GetPayload()); + var branch = aggregate.GetValue().GetPayload() as Branch ?? throw new ApplicationException(); + Assert.Equal("a", branch.Name); + Assert.Equal(1, aggregate.GetValue().Version); + } + + [Fact] + public async Task ExecuteWithResourcePublishOnly() + { + var executor = new CommandExecutor { EventTypes = new PureDomainEventTypes() }; + var createCommand = new RegisterBranch("a"); + var partitionKeys = PartitionKeys.Generate(); + var result = await executor.ExecuteWithResource( + createCommand, + new CommandResourcePublishOnly( + command => partitionKeys, + (command, _) => EventOrNone.Event(new BranchCreated(command.Name)))); + Assert.True(result.IsSuccess); + var aggregate = Repository.Load(partitionKeys); + Assert.NotNull(aggregate); + Assert.IsType(aggregate.GetValue().GetPayload()); + var branch = aggregate.GetValue().GetPayload() as Branch ?? throw new ApplicationException(); + Assert.Equal("a", branch.Name); + Assert.Equal(1, aggregate.GetValue().Version); + } + [Fact] + public async Task ExecuteWithResourcePublishOnlyTask() + { + var executor = new CommandExecutor { EventTypes = new PureDomainEventTypes() }; + var createCommand = new RegisterBranch("a"); + var partitionKeys = PartitionKeys.Generate(); + var result = await executor.ExecuteWithResource( + createCommand, + new CommandResourcePublishOnlyTask( + command => partitionKeys, + (command, _) => Task.FromResult(EventOrNone.Event(new BranchCreated(command.Name))))); + Assert.True(result.IsSuccess); + var aggregate = Repository.Load(partitionKeys); + Assert.NotNull(aggregate); + Assert.IsType(aggregate.GetValue().GetPayload()); + var branch = aggregate.GetValue().GetPayload() as Branch ?? throw new ApplicationException(); + Assert.Equal("a", branch.Name); + Assert.Equal(1, aggregate.GetValue().Version); + } + [Fact] + public async Task ExecuteWithResourcePublishOnlyInject() + { + var executor = new CommandExecutor { EventTypes = new PureDomainEventTypes() }; + var createCommand = new RegisterBranch("a"); + var partitionKeys = PartitionKeys.Generate(); + var result = await executor.ExecuteWithResource( + createCommand, + new CommandResourcePublishOnlyWithInject>( + command => partitionKeys, + _ => false, + (command, _, _) => EventOrNone.Event(new BranchCreated(command.Name)))); + Assert.True(result.IsSuccess); + var aggregate = Repository.Load(partitionKeys); + Assert.NotNull(aggregate); + Assert.IsType(aggregate.GetValue().GetPayload()); + var branch = aggregate.GetValue().GetPayload() as Branch ?? throw new ApplicationException(); + Assert.Equal("a", branch.Name); + Assert.Equal(1, aggregate.GetValue().Version); + } + [Fact] + public async Task ExecuteWithResourcePublishOnlyWithInjectTask() + { + var executor = new CommandExecutor { EventTypes = new PureDomainEventTypes() }; + var createCommand = new RegisterBranch("a"); + var partitionKeys = PartitionKeys.Generate(); + var result = await executor.ExecuteWithResource( + createCommand, + new CommandResourcePublishOnlyWithInjectTask>( + command => partitionKeys, + _ => false, + (command, _, _) => Task.FromResult(EventOrNone.Event(new BranchCreated(command.Name))))); + Assert.True(result.IsSuccess); + var aggregate = Repository.Load(partitionKeys); + Assert.NotNull(aggregate); + Assert.IsType(aggregate.GetValue().GetPayload()); + var branch = aggregate.GetValue().GetPayload() as Branch ?? throw new ApplicationException(); + Assert.Equal("a", branch.Name); + Assert.Equal(1, aggregate.GetValue().Version); + } }