From 4683791e6be13bff99f9cac01c9ad6c56670b24b Mon Sep 17 00:00:00 2001 From: David Guida Date: Sun, 10 Jan 2021 12:13:36 -0500 Subject: [PATCH 1/9] added e2e test initial version, has an issue with cast to dynamic in InMemoryPublisher. https://stackoverflow.com/questions/65649702/generic-resolution-with-dynamic-cast-failing-in-unit-tes --- .../InMemorySagaConfiguratorExtensions.cs | 28 ++++---- .../Messaging/InMemoryPublisher.cs | 3 +- .../OpenSleigh.Core.Tests.csproj | 4 +- .../E2E/SimpleSaga.cs | 33 +++++++++ .../E2E/SimpleSagaScenario.cs | 67 +++++++++++++++++++ ...enSleigh.Persistence.InMemory.Tests.csproj | 15 +++-- .../OpenSleigh.Persistence.Mongo.Tests.csproj | 4 +- ...OpenSleigh.Transport.RabbitMQ.Tests.csproj | 4 +- 8 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs create mode 100644 tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs diff --git a/src/OpenSleigh.Persistence.InMemory/InMemorySagaConfiguratorExtensions.cs b/src/OpenSleigh.Persistence.InMemory/InMemorySagaConfiguratorExtensions.cs index e1e6bc3e..f5b6b272 100644 --- a/src/OpenSleigh.Persistence.InMemory/InMemorySagaConfiguratorExtensions.cs +++ b/src/OpenSleigh.Persistence.InMemory/InMemorySagaConfiguratorExtensions.cs @@ -4,7 +4,9 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Reflection; using System.Threading.Channels; +using OpenSleigh.Core.Messaging; using OpenSleigh.Persistence.InMemory.Messaging; namespace OpenSleigh.Persistence.InMemory @@ -12,6 +14,9 @@ namespace OpenSleigh.Persistence.InMemory [ExcludeFromCodeCoverage] public static class InMemorySagaConfiguratorExtensions { + private static readonly MethodInfo RawRegisterMessageMethod = typeof(InMemorySagaConfiguratorExtensions) + .GetMethod("RegisterMessage", BindingFlags.Static | BindingFlags.NonPublic); + public static ISagaConfigurator UseInMemoryTransport(this ISagaConfigurator sagaConfigurator) where TS : Saga where TD : SagaState @@ -30,21 +35,20 @@ public static ISagaConfigurator UseInMemoryTransport(this ISagaC var messageType = i.GetGenericArguments().First(); - var rawMethod = typeof(Channel).GetMethod(nameof(Channel.CreateUnbounded), Array.Empty()); - var method = rawMethod.MakeGenericMethod(messageType); - dynamic channel = method.Invoke(null, null); - - sagaConfigurator.Services.AddSingleton(typeof(Channel<>).MakeGenericType(messageType), (object)channel); - - sagaConfigurator.Services.AddSingleton(typeof(ChannelWriter<>).MakeGenericType(messageType), (object)channel.Writer); - - sagaConfigurator.Services.AddSingleton(typeof(ChannelReader<>).MakeGenericType(messageType), (object)channel.Reader); - - sagaConfigurator.Services.AddSingleton(typeof(ISubscriber), - typeof(InMemorySubscriber<>).MakeGenericType(messageType)); + //TODO: ensure the message was not already registered + var registerMessageMethod = RawRegisterMessageMethod.MakeGenericMethod(messageType); + registerMessageMethod.Invoke(null, new[] {sagaConfigurator.Services}); } return sagaConfigurator; } + + private static void RegisterMessage(IServiceCollection services) where TM : IMessage + { + var channel = Channel.CreateUnbounded(); + services.AddSingleton>(channel.Reader) + .AddSingleton>(channel.Writer) + .AddSingleton>(); + } } } \ No newline at end of file diff --git a/src/OpenSleigh.Persistence.InMemory/Messaging/InMemoryPublisher.cs b/src/OpenSleigh.Persistence.InMemory/Messaging/InMemoryPublisher.cs index bdc2b7f2..bea23b7c 100644 --- a/src/OpenSleigh.Persistence.InMemory/Messaging/InMemoryPublisher.cs +++ b/src/OpenSleigh.Persistence.InMemory/Messaging/InMemoryPublisher.cs @@ -15,8 +15,7 @@ public InMemoryPublisher(IChannelFactory channelFactory) { _channelFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory)); } - - + public Task PublishAsync(IMessage message, CancellationToken cancellationToken = default) { if (message == null) diff --git a/tests/OpenSleigh.Core.Tests/OpenSleigh.Core.Tests.csproj b/tests/OpenSleigh.Core.Tests/OpenSleigh.Core.Tests.csproj index 82ad62d8..b13442d7 100644 --- a/tests/OpenSleigh.Core.Tests/OpenSleigh.Core.Tests.csproj +++ b/tests/OpenSleigh.Core.Tests/OpenSleigh.Core.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -20,7 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs new file mode 100644 index 00000000..7e1b41bb --- /dev/null +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OpenSleigh.Core; +using OpenSleigh.Core.Messaging; + +namespace OpenSleigh.Persistence.InMemory.Tests.E2E +{ + internal class SimpleSagaState : SagaState + { + public SimpleSagaState(Guid id) : base(id) + { + } + } + + internal record StartSimpleSaga(Guid Id, Guid CorrelationId) : ICommand; + + internal class SimpleSaga : Saga, IStartedBy + { + private readonly Action _onStart; + + public SimpleSaga(Action onStart) + { + _onStart = onStart; + } + + public Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + _onStart?.Invoke(context.Message); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs new file mode 100644 index 00000000..f2dbadc1 --- /dev/null +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenSleigh.Core.DependencyInjection; +using OpenSleigh.Core.Messaging; +using Xunit; + +namespace OpenSleigh.Persistence.InMemory.Tests.E2E +{ + public class SimpleSagaScenario + { + [Fact] + public async Task SimpleSaga_should_handle_start_message() + { + var hostBuilder = CreateHostBuilder(); + + var message = new StartSimpleSaga(Guid.NewGuid(), Guid.NewGuid()); + + var received = false; + Action onMessage = msg => + { + msg.Should().Be(message); + received = true; + }; + + hostBuilder.ConfigureServices((ctx, services) => + { + services.AddSingleton(onMessage); + }); + + var host = hostBuilder.Build(); + + using var scope = host.Services.CreateScope(); + var bus = scope.ServiceProvider.GetRequiredService(); + + + var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + await Task.WhenAll(new[] + { + host.RunAsync(token: tokenSource.Token), + bus.PublishAsync(message, tokenSource.Token) + }); + + received.Should().BeTrue(); + } + + static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddOpenSleigh(cfg => + { + cfg.UseInMemoryTransport() + .UseInMemoryPersistence(); + + cfg.AddSaga() + .UseStateFactory(msg => new SimpleSagaState(msg.CorrelationId)) + .UseInMemoryTransport(); + }); + }); + } + +} diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj b/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj index b1fbf0da..5a50a028 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj @@ -7,10 +7,7 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + @@ -20,7 +17,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -30,4 +31,8 @@ + + + + diff --git a/tests/OpenSleigh.Persistence.Mongo.Tests/OpenSleigh.Persistence.Mongo.Tests.csproj b/tests/OpenSleigh.Persistence.Mongo.Tests/OpenSleigh.Persistence.Mongo.Tests.csproj index 04656ef4..857c704f 100644 --- a/tests/OpenSleigh.Persistence.Mongo.Tests/OpenSleigh.Persistence.Mongo.Tests.csproj +++ b/tests/OpenSleigh.Persistence.Mongo.Tests/OpenSleigh.Persistence.Mongo.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/OpenSleigh.Transport.RabbitMQ.Tests/OpenSleigh.Transport.RabbitMQ.Tests.csproj b/tests/OpenSleigh.Transport.RabbitMQ.Tests/OpenSleigh.Transport.RabbitMQ.Tests.csproj index 05100be1..65c7c89f 100644 --- a/tests/OpenSleigh.Transport.RabbitMQ.Tests/OpenSleigh.Transport.RabbitMQ.Tests.csproj +++ b/tests/OpenSleigh.Transport.RabbitMQ.Tests/OpenSleigh.Transport.RabbitMQ.Tests.csproj @@ -7,7 +7,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From b89e82f79cd8897989c0dd89281c92983655a61a Mon Sep 17 00:00:00 2001 From: David Guida Date: Sun, 10 Jan 2021 12:16:22 -0500 Subject: [PATCH 2/9] updated path --- .../E2E/{ => Sagas}/SimpleSaga.cs | 0 .../OpenSleigh.Persistence.InMemory.Tests.csproj | 4 ---- 2 files changed, 4 deletions(-) rename tests/OpenSleigh.Persistence.InMemory.Tests/E2E/{ => Sagas}/SimpleSaga.cs (100%) diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs similarity index 100% rename from tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSaga.cs rename to tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj b/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj index 5a50a028..72804840 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/OpenSleigh.Persistence.InMemory.Tests.csproj @@ -31,8 +31,4 @@ - - - - From b7a2db18cbc458dc3238f82b616f73deccf8cb1e Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 09:34:20 -0500 Subject: [PATCH 3/9] fixed broken test marking the classes as internal will prevent dynamic typecasting, resulting in instances resolved as object instead --- .../E2E/Sagas/SimpleSaga.cs | 8 ++++---- .../E2E/SimpleSagaScenario.cs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs index 7e1b41bb..15387e96 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs @@ -6,16 +6,16 @@ namespace OpenSleigh.Persistence.InMemory.Tests.E2E { - internal class SimpleSagaState : SagaState + public class SimpleSagaState : SagaState { public SimpleSagaState(Guid id) : base(id) { } } - internal record StartSimpleSaga(Guid Id, Guid CorrelationId) : ICommand; - - internal class SimpleSaga : Saga, IStartedBy + public record StartSimpleSaga(Guid Id, Guid CorrelationId) : ICommand; + + public class SimpleSaga : Saga, IStartedBy { private readonly Action _onStart; diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs index f2dbadc1..158872b1 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs @@ -35,7 +35,6 @@ public async Task SimpleSaga_should_handle_start_message() using var scope = host.Services.CreateScope(); var bus = scope.ServiceProvider.GetRequiredService(); - var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); From fa07d059414c0cecc8f6b195bf45f30bcf3201b4 Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 09:36:29 -0500 Subject: [PATCH 4/9] fixed namespaces --- .../E2E/Sagas/SimpleSaga.cs | 2 +- .../E2E/SimpleSagaScenario.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs index 15387e96..fbebb049 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/SimpleSaga.cs @@ -4,7 +4,7 @@ using OpenSleigh.Core; using OpenSleigh.Core.Messaging; -namespace OpenSleigh.Persistence.InMemory.Tests.E2E +namespace OpenSleigh.Persistence.InMemory.Tests.E2E.Sagas { public class SimpleSagaState : SagaState { diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs index 158872b1..9772ff79 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Hosting; using OpenSleigh.Core.DependencyInjection; using OpenSleigh.Core.Messaging; +using OpenSleigh.Persistence.InMemory.Tests.E2E.Sagas; using Xunit; namespace OpenSleigh.Persistence.InMemory.Tests.E2E From d792f2fe633bfc881ca2777d723201a56475ec2c Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 10:30:13 -0500 Subject: [PATCH 5/9] minor update --- .../Integration/MongoSagaStateRepositoryTests.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/OpenSleigh.Persistence.Mongo.Tests/Integration/MongoSagaStateRepositoryTests.cs b/tests/OpenSleigh.Persistence.Mongo.Tests/Integration/MongoSagaStateRepositoryTests.cs index e9c567f1..fa574a4c 100644 --- a/tests/OpenSleigh.Persistence.Mongo.Tests/Integration/MongoSagaStateRepositoryTests.cs +++ b/tests/OpenSleigh.Persistence.Mongo.Tests/Integration/MongoSagaStateRepositoryTests.cs @@ -60,16 +60,19 @@ public async Task LockAsync_should_allow_different_saga_state_types_to_share_the var options = new MongoSagaStateRepositoryOptions(TimeSpan.FromMinutes(1)); var sut = new MongoSagaStateRepository(_fixture.DbContext, serializer, options); + + var correlationId = Guid.NewGuid(); - var newState = DummyState.New(); - var (state, lockId) = await sut.LockAsync(newState.Id, newState, CancellationToken.None); + var newState = new DummyState(correlationId, "lorem", 42); + + var (state, lockId) = await sut.LockAsync(correlationId, newState, CancellationToken.None); var newState2 = new DummyState2(state.Id); newState2.Id.Should().Be(newState.Id); - var (state2, lockId2) = await sut.LockAsync(newState2.Id, newState2, CancellationToken.None); + var (state2, lockId2) = await sut.LockAsync(correlationId, newState2, CancellationToken.None); state2.Should().NotBeNull(); - state2.Id.Should().Be(newState.Id); + state2.Id.Should().Be(correlationId); } } From ae51bd7f42aed91d14ea58f144c3995d4f391a42 Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 10:30:30 -0500 Subject: [PATCH 6/9] added support for multiple types sharing correlation id --- .../InMemorySagaStateRepository.cs | 32 +++++++++++-------- .../Unit/InMemorySagaStateRepositoryTests.cs | 18 +++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/OpenSleigh.Persistence.InMemory/InMemorySagaStateRepository.cs b/src/OpenSleigh.Persistence.InMemory/InMemorySagaStateRepository.cs index bde4f42b..13e23cf9 100644 --- a/src/OpenSleigh.Persistence.InMemory/InMemorySagaStateRepository.cs +++ b/src/OpenSleigh.Persistence.InMemory/InMemorySagaStateRepository.cs @@ -3,6 +3,7 @@ using OpenSleigh.Core.Persistence; using System; using System.Collections.Concurrent; +using System.Diagnostics.Contracts; using System.Threading; using System.Threading.Tasks; @@ -10,18 +11,15 @@ namespace OpenSleigh.Persistence.InMemory { public class InMemorySagaStateRepository : ISagaStateRepository { - private readonly ConcurrentDictionary _items; - private static readonly SemaphoreSlim _semaphore = new (1, 1); - - public InMemorySagaStateRepository() - { - _items = new ConcurrentDictionary(); - } + private readonly ConcurrentDictionary _items = new (); + private static readonly SemaphoreSlim ReleaseSemaphore = new (1, 1); public Task<(TD state, Guid lockId)> LockAsync(Guid correlationId, TD newState = default, CancellationToken cancellationToken = default) where TD : SagaState { - var (state, lockId) = _items.AddOrUpdate(correlationId, + var key = BuildKey(correlationId); + + var (state, lockId) = _items.AddOrUpdate(key, k => (newState, Guid.NewGuid()), (k, v) => { @@ -45,21 +43,29 @@ public Task ReleaseLockAsync(TD state, Guid lockId, ITransaction transaction private async Task ReleaseLockAsyncCore(TD state, Guid lockId, CancellationToken cancellationToken) where TD : SagaState { - await _semaphore.WaitAsync(cancellationToken); + await ReleaseSemaphore.WaitAsync(cancellationToken); + var key = BuildKey(state.Id); + try { - if (!_items.ContainsKey(state.Id)) + if (!_items.ContainsKey(key)) throw new ArgumentOutOfRangeException(nameof(state), $"invalid state correlationId '{state.Id}'"); - var stored = _items[state.Id]; + var stored = _items[key]; if (stored.lockId != lockId) throw new LockException($"unable to release lock on saga state '{state.Id}'"); - _items[state.Id] = (state, null); + _items[key] = (state, null); } finally { - _semaphore.Release(); + ReleaseSemaphore.Release(); } } + + private static string BuildKey(Guid correlationId) + { + var stateType = typeof(TD); + return $"{correlationId}|{stateType.FullName}"; + } } } \ No newline at end of file diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/InMemorySagaStateRepositoryTests.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/InMemorySagaStateRepositoryTests.cs index bdc7d046..41f21dd4 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/InMemorySagaStateRepositoryTests.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/InMemorySagaStateRepositoryTests.cs @@ -58,6 +58,24 @@ public async Task LockAsync_should_lock_item_if_available() secondLockId.Should().NotBe(lockId); } + [Fact] + public async Task LockAsync_should_allow_different_saga_state_types_to_share_the_correlation_id() + { + var sut = new InMemorySagaStateRepository(); + + var correlationId = Guid.NewGuid(); + + var newState = new DummyState(correlationId, "lorem", 42); + var (state, lockId) = await sut.LockAsync(correlationId, newState, CancellationToken.None); + + var newState2 = new DummyState2(correlationId); + newState2.Id.Should().Be(newState.Id); + + var (state2, lockId2) = await sut.LockAsync(correlationId, newState2, CancellationToken.None); + state2.Should().NotBeNull(); + state2.Id.Should().Be(correlationId); + } + [Fact] public async Task UpdateAsync_should_release_lock() { From e21f77f1ef9ecc9d8ca5ea60806ab554d2bcdf6c Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 10:33:01 -0500 Subject: [PATCH 7/9] added support for multiple types sharing correlation id --- .../Unit/DummyState.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/DummyState.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/DummyState.cs index 8ccb7b74..a917a367 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/DummyState.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/Unit/DummyState.cs @@ -16,4 +16,11 @@ public DummyState(Guid id, string foo, int bar) : base(id) public static DummyState New() => new DummyState(Guid.NewGuid(), "lorem ipsum", 42); } + + public class DummyState2 : SagaState + { + public DummyState2(Guid id) : base(id) { } + + public static DummyState2 New() => new DummyState2(Guid.NewGuid()); + } } \ No newline at end of file From 3e7a52f80e7360914b7b63f957141c874508bbdf Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 10:36:09 -0500 Subject: [PATCH 8/9] updated in-memory e2e tests --- .../E2E/ParentChildScenario.cs | 74 +++++++++++++++++++ .../E2E/Sagas/ChildSaga.cs | 38 ++++++++++ .../E2E/Sagas/ParentSaga .cs | 63 ++++++++++++++++ .../E2E/SimpleSagaScenario.cs | 14 +++- 4 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 tests/OpenSleigh.Persistence.InMemory.Tests/E2E/ParentChildScenario.cs create mode 100644 tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ChildSaga.cs create mode 100644 tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ParentSaga .cs diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/ParentChildScenario.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/ParentChildScenario.cs new file mode 100644 index 00000000..e1b73bd4 --- /dev/null +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/ParentChildScenario.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenSleigh.Core.DependencyInjection; +using OpenSleigh.Core.Messaging; +using OpenSleigh.Persistence.InMemory.Tests.E2E.Sagas; +using Xunit; + +namespace OpenSleigh.Persistence.InMemory.Tests.E2E +{ + [Category("E2E")] + [Trait("Category", "E2E")] + public class ParentChildScenario + { + [Fact] + public async Task run_parent_child_scenario() + { + var hostBuilder = CreateHostBuilder(); + + var message = new StartParentSaga(Guid.NewGuid(), Guid.NewGuid()); + + var received = false; + var tokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + + Action onMessage = msg => + { + received = true; + tokenSource.Cancel(); + }; + + hostBuilder.ConfigureServices((ctx, services) => + { + services.AddSingleton(onMessage); + }); + + var host = hostBuilder.Build(); + + using var scope = host.Services.CreateScope(); + var bus = scope.ServiceProvider.GetRequiredService(); + + await Task.WhenAll(new[] + { + host.RunAsync(token: tokenSource.Token), + bus.PublishAsync(message, tokenSource.Token) + }); + + received.Should().BeTrue(); + } + + static IHostBuilder CreateHostBuilder() => + Host.CreateDefaultBuilder() + .ConfigureServices((hostContext, services) => + { + services.AddOpenSleigh(cfg => + { + cfg.UseInMemoryTransport() + .UseInMemoryPersistence(); + + cfg.AddSaga() + .UseStateFactory(msg => new ParentSagaState(msg.CorrelationId)) + .UseInMemoryTransport(); + + cfg.AddSaga() + .UseStateFactory(msg => new ChildSagaState(msg.CorrelationId)) + .UseInMemoryTransport(); + }); + }); + } + +} diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ChildSaga.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ChildSaga.cs new file mode 100644 index 00000000..cf3b3d78 --- /dev/null +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ChildSaga.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OpenSleigh.Core; +using OpenSleigh.Core.Messaging; + +namespace OpenSleigh.Persistence.InMemory.Tests.E2E.Sagas +{ + public class ChildSagaState : SagaState + { + public ChildSagaState(Guid id) : base(id) { } + } + + public record StartChildSaga(Guid Id, Guid CorrelationId) : ICommand { } + + public record ProcessChildSaga(Guid Id, Guid CorrelationId) : ICommand { } + + public record ChildSagaCompleted(Guid Id, Guid CorrelationId) : IEvent { } + + public class ChildSaga : + Saga, + IStartedBy, + IHandleMessage + { + + public async Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + var message = new ProcessChildSaga(Guid.NewGuid(), context.Message.CorrelationId); + await this.Bus.PublishAsync(message, cancellationToken); + } + + public async Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + var completedEvent = new ChildSagaCompleted(Guid.NewGuid(), context.Message.CorrelationId); + await this.Bus.PublishAsync(completedEvent, cancellationToken); + } + } +} \ No newline at end of file diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ParentSaga .cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ParentSaga .cs new file mode 100644 index 00000000..f0e7e4ca --- /dev/null +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/Sagas/ParentSaga .cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OpenSleigh.Core; +using OpenSleigh.Core.Messaging; + +namespace OpenSleigh.Persistence.InMemory.Tests.E2E.Sagas +{ + public class ParentSagaState : SagaState + { + public ParentSagaState(Guid id) : base(id) { } + } + + public record StartParentSaga(Guid Id, Guid CorrelationId) : ICommand { } + + public record ProcessParentSaga(Guid Id, Guid CorrelationId) : ICommand { } + + public record ParentSagaCompleted(Guid Id, Guid CorrelationId) : IEvent { } + + public class ParentSaga : + Saga, + IStartedBy, + IHandleMessage, + IHandleMessage, + IHandleMessage + { + private readonly Action _onCompleted; + + + public ParentSaga(Action onCompleted) + { + _onCompleted = onCompleted ?? throw new ArgumentNullException(nameof(onCompleted)); + } + + public async Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + var message = new ProcessParentSaga(Guid.NewGuid(), context.Message.CorrelationId); + await this.Bus.PublishAsync(message, cancellationToken); + } + + public async Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + var message = new StartChildSaga(Guid.NewGuid(), context.Message.CorrelationId); + await this.Bus.PublishAsync(message, cancellationToken); + } + + public async Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + var message = new ParentSagaCompleted(Guid.NewGuid(), context.Message.CorrelationId); + await this.Bus.PublishAsync(message, cancellationToken); + } + + public Task HandleAsync(IMessageContext context, CancellationToken cancellationToken = default) + { + this.State.MarkAsCompleted(); + + _onCompleted?.Invoke(context.Message); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs index 9772ff79..4a5ba39e 100644 --- a/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs +++ b/tests/OpenSleigh.Persistence.InMemory.Tests/E2E/SimpleSagaScenario.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using FluentAssertions; @@ -11,20 +12,27 @@ namespace OpenSleigh.Persistence.InMemory.Tests.E2E { + [Category("E2E")] + [Trait("Category", "E2E")] public class SimpleSagaScenario { [Fact] - public async Task SimpleSaga_should_handle_start_message() + public async Task run_single_message_scenario() { var hostBuilder = CreateHostBuilder(); var message = new StartSimpleSaga(Guid.NewGuid(), Guid.NewGuid()); var received = false; + var tokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + Action onMessage = msg => { + received = true; + + tokenSource.Cancel(); + msg.Should().Be(message); - received = true; }; hostBuilder.ConfigureServices((ctx, services) => @@ -36,8 +44,6 @@ public async Task SimpleSaga_should_handle_start_message() using var scope = host.Services.CreateScope(); var bus = scope.ServiceProvider.GetRequiredService(); - - var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); await Task.WhenAll(new[] { From 8c7c4b9cceaafed61bcf0002723d043ca2259082 Mon Sep 17 00:00:00 2001 From: David Guida Date: Mon, 11 Jan 2021 10:50:14 -0500 Subject: [PATCH 9/9] added changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..373f7a24 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [2021-01-11](https://github.com/mizrael/OpenSleigh/pull/14) +### Added +- added in-memory E2E tests + +### Fixed +- fixed in-memory state repository, added support for multiple types sharing correlation id \ No newline at end of file