From 2891ac2db3d7962cad197b003f379611ea5d8953 Mon Sep 17 00:00:00 2001 From: Lucas Andrade de Lima Date: Fri, 23 May 2025 19:29:47 -0300 Subject: [PATCH 01/56] Enhance documenation for Sagas add section for Saga.State --- Documentation/basics/sagas.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Documentation/basics/sagas.md b/Documentation/basics/sagas.md index 5df006444..3a643d79d 100644 --- a/Documentation/basics/sagas.md +++ b/Documentation/basics/sagas.md @@ -127,6 +127,21 @@ public class OrderSaga `AggregateSaga<,,>`). +## Understanding Saga Lifecycle States + + +Each Saga has an internal ```State``` property that defines how it processes events. This property can have the following values: + +- **New** + - Only events defined using ```ISagaIsStartedBy<>``` can be processed. +- **Running** + - Events defined using ```ISagaHandles<>``` will be processed. + - Events defined using ```ISagaIsStartedBy<>``` will also behave the same of ```ISagaHandles<>```. +- **Completed** + - No events will be processed by the Saga anymore. + + + ## Alternative saga store By default, EventFlow is configured to use event sourcing and aggregate From ad012449fd1d5e2b74a28377bdb8130fcaeeddc6 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Thu, 29 May 2025 10:29:41 +0200 Subject: [PATCH 02/56] Update Documentation/basics/sagas.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Documentation/basics/sagas.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/basics/sagas.md b/Documentation/basics/sagas.md index 3a643d79d..fb4e9fb44 100644 --- a/Documentation/basics/sagas.md +++ b/Documentation/basics/sagas.md @@ -136,7 +136,7 @@ Each Saga has an internal ```State``` property that defines how it processes eve - Only events defined using ```ISagaIsStartedBy<>``` can be processed. - **Running** - Events defined using ```ISagaHandles<>``` will be processed. - - Events defined using ```ISagaIsStartedBy<>``` will also behave the same of ```ISagaHandles<>```. + - Events defined using ```ISagaIsStartedBy<>``` will also behave the same as ```ISagaHandles<>```. - **Completed** - No events will be processed by the Saga anymore. From 5f35713af3fa7387807ecc3626c21139c251ea78 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 12:31:25 +0300 Subject: [PATCH 03/56] Add shouldly --- Source/EventFlow.Tests/EventFlow.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 29f1502c4..89e630555 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -10,6 +10,7 @@ + From fd319b8c57dae0955b52685a13c0501497ad4297 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 12:31:35 +0300 Subject: [PATCH 04/56] Replace in LicenseHeaderTests --- Source/EventFlow.Tests/LicenseHeaderTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/EventFlow.Tests/LicenseHeaderTests.cs b/Source/EventFlow.Tests/LicenseHeaderTests.cs index 0175c4f4b..b37536771 100644 --- a/Source/EventFlow.Tests/LicenseHeaderTests.cs +++ b/Source/EventFlow.Tests/LicenseHeaderTests.cs @@ -27,9 +27,10 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; +// ReSharper disable StringLiteralTypo // ReSharper disable StringLiteralTypo namespace EventFlow.Tests @@ -61,7 +62,7 @@ public async Task EveryFileHasCorrectLicenseHeader() var sourceFiles = await Task.WhenAll(sourceFilesPaths.Select(GetSourceFileAsync)); // Sanity asserts - sourceFiles.Should().HaveCountGreaterThan(700); + sourceFiles.Length.ShouldBeGreaterThan(700); // Missing headers var missingHeaders = sourceFiles @@ -79,8 +80,8 @@ public async Task EveryFileHasCorrectLicenseHeader() validationErrors.ForEach(Console.WriteLine); // Asserts - missingHeaders.Should().BeEmpty(); - validationErrors.Should().BeEmpty(); + missingHeaders.ShouldBeEmpty(); + validationErrors.ShouldBeEmpty(); } private static string PathRelativeTo(string root, string fullPath) From 8b3e0c1e6fe3b86a81ab3edf5f4710f158148d14 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 12:53:55 +0300 Subject: [PATCH 05/56] More replaces --- .../CustomAggregateIdExplorationTest.cs | 4 +-- .../EventUpgradeExplorationTest.cs | 4 +-- .../RegisterSubscribersExplorationTests.cs | 4 +-- .../GuidFactoriesDeterministicTests.cs | 4 +-- .../UnitTests/Core/IdentityTests.cs | 27 ++++++++++--------- .../UnitTests/Core/ReflectionHelperTests.cs | 8 +++--- .../UnitTests/Core/RetryDelayTests.cs | 6 ++--- ...OptimisticConcurrencyRetryStrategyTests.cs | 6 ++--- .../Core/TransientFaultHandlerTests.cs | 10 +++---- ...VersionedTypeDefinitionServiceTestSuite.cs | 24 ++++++++--------- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/Source/EventFlow.Tests/Exploration/CustomAggregateIdExplorationTest.cs b/Source/EventFlow.Tests/Exploration/CustomAggregateIdExplorationTest.cs index d3938991f..bca593b27 100644 --- a/Source/EventFlow.Tests/Exploration/CustomAggregateIdExplorationTest.cs +++ b/Source/EventFlow.Tests/Exploration/CustomAggregateIdExplorationTest.cs @@ -25,9 +25,9 @@ using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.Exploration { @@ -48,7 +48,7 @@ public async Task AggregatesCanHaveCustomImplementedIdentity() var customAggregate = await aggregateStore.LoadAsync(customId, CancellationToken.None).ConfigureAwait(false); // Assert - customAggregate.Id.Value.Should().Be(customId.Value); + customAggregate.Id.Value.ShouldBe(customId.Value); } } diff --git a/Source/EventFlow.Tests/Exploration/EventUpgradeExplorationTest.cs b/Source/EventFlow.Tests/Exploration/EventUpgradeExplorationTest.cs index a68a13ff0..6b035fc7c 100644 --- a/Source/EventFlow.Tests/Exploration/EventUpgradeExplorationTest.cs +++ b/Source/EventFlow.Tests/Exploration/EventUpgradeExplorationTest.cs @@ -29,9 +29,9 @@ using EventFlow.EventStores; using EventFlow.Extensions; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.Exploration { @@ -77,7 +77,7 @@ await aggregateStore.UpdateAsync( var aggregate = await aggregateStore.LoadAsync( id, CancellationToken.None); - aggregate.V2Applied.Should().BeTrue(); + aggregate.V2Applied.ShouldBeTrue(); } public class SourceId : ISourceId diff --git a/Source/EventFlow.Tests/Exploration/RegisterSubscribersExplorationTests.cs b/Source/EventFlow.Tests/Exploration/RegisterSubscribersExplorationTests.cs index f89cbf3cf..920f5a856 100644 --- a/Source/EventFlow.Tests/Exploration/RegisterSubscribersExplorationTests.cs +++ b/Source/EventFlow.Tests/Exploration/RegisterSubscribersExplorationTests.cs @@ -34,9 +34,9 @@ using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.Exploration { @@ -68,7 +68,7 @@ await commandBus.PublishAsync( } // Assert - wasHandled.Should().BeTrue(); + wasHandled.ShouldBeTrue(); } public static IEnumerable> TestCases() diff --git a/Source/EventFlow.Tests/UnitTests/Core/GuidFactories/GuidFactoriesDeterministicTests.cs b/Source/EventFlow.Tests/UnitTests/Core/GuidFactories/GuidFactoriesDeterministicTests.cs index 95958286a..caef88f7b 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/GuidFactories/GuidFactoriesDeterministicTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/GuidFactories/GuidFactoriesDeterministicTests.cs @@ -25,8 +25,8 @@ using System.Linq; using AutoFixture; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core.GuidFactories { @@ -52,7 +52,7 @@ public void Create_EmptyNameBytes_ThrowsArgumentNullException() public void Create(Guid namespaceId, byte[] nameBytes, Guid expected) { var result = EventFlow.Core.GuidFactories.Deterministic.Create(namespaceId, nameBytes); - result.Should().Be(expected); + result.ShouldBe(expected); } private static IEnumerable GetTestCases() diff --git a/Source/EventFlow.Tests/UnitTests/Core/IdentityTests.cs b/Source/EventFlow.Tests/UnitTests/Core/IdentityTests.cs index b32ec053f..ee8c02fc8 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/IdentityTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/IdentityTests.cs @@ -24,7 +24,7 @@ using EventFlow.Core; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using FluentAssertions; +using Shouldly; using NUnit.Framework; namespace EventFlow.Tests.UnitTests.Core @@ -43,8 +43,8 @@ public void NewDeterministic_ReturnsKnownResult() var testId = ThingyId.NewDeterministic(namespaceId, name); // Assert - testId.Value.Should().Be("thingy-da7ab6b1-c513-581f-a1a0-7cdf17109deb"); - ThingyId.IsValid(testId.Value).Should().BeTrue(); + testId.Value.ShouldBe("thingy-da7ab6b1-c513-581f-a1a0-7cdf17109deb"); + ThingyId.IsValid(testId.Value).ShouldBeTrue(); } [TestCase("thingy-da7ab6b1-c513-581f-a1a0-7cdf17109deb", "da7ab6b1-c513-581f-a1a0-7cdf17109deb")] @@ -59,9 +59,9 @@ public void WithValidValue(string value, string expectedGuidValue) Assert.DoesNotThrow(() => thingyId = ThingyId.With(value)); // Assert - thingyId.Should().NotBeNull(); - thingyId.Value.Should().Be(value); - thingyId.GetGuid().Should().Be(expectedGuid); + thingyId.ShouldNotBeNull(); + thingyId.Value.ShouldBe(value); + thingyId.GetGuid().ShouldBe(expectedGuid); } [Test] @@ -74,7 +74,7 @@ public void InputOutput() var thingyId = ThingyId.With(guid); // Assert - thingyId.GetGuid().Should().Be(guid); + thingyId.GetGuid().ShouldBe(guid); } [Test] @@ -84,7 +84,7 @@ public void ShouldBeLowerCase() var testId = ThingyId.New; // Assert - testId.Value.Should().Be(testId.Value.ToLowerInvariant()); + testId.Value.ShouldBe(testId.Value.ToLowerInvariant()); } [Test] @@ -94,7 +94,7 @@ public void New_IsValid() var testId = ThingyId.New; // Assert - ThingyId.IsValid(testId.Value).Should().BeTrue(testId.Value); + ThingyId.IsValid(testId.Value).ShouldBeTrue(testId.Value); } [Test] @@ -104,7 +104,7 @@ public void NewComb_IsValid() var testId = ThingyId.NewComb(); // Assert - ThingyId.IsValid(testId.Value).Should().BeTrue(testId.Value); + ThingyId.IsValid(testId.Value).ShouldBeTrue(testId.Value); } [Test] @@ -114,7 +114,7 @@ public void NewDeterministic_IsValid() var testId = ThingyId.NewDeterministic(Guid.NewGuid(), Guid.NewGuid().ToString()); // Assert - ThingyId.IsValid(testId.Value).Should().BeTrue(testId.Value); + ThingyId.IsValid(testId.Value).ShouldBeTrue(testId.Value); } [TestCase("da7ab6b1-c513-581f-a1a0-7cdf17109deb")] @@ -127,7 +127,8 @@ public void NewDeterministic_IsValid() public void CannotCreateBadIds(string badIdValue) { // Act - Assert.Throws(() => ThingyId.With(badIdValue)).Message.Should().Contain("Identity is invalid:"); + var exception = Assert.Throws(() => ThingyId.With(badIdValue)); + exception.Message.ShouldContain("Identity is invalid:"); } public class Id : Identity @@ -148,7 +149,7 @@ public void JustId() var id = Id.With(guid); // Assert - id.Value.Should().Be(expected); + id.Value.ShouldBe(expected); } } } diff --git a/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs b/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs index c9de8a0d0..153300696 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/ReflectionHelperTests.cs @@ -23,8 +23,8 @@ using System; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core { @@ -39,7 +39,7 @@ public void CompileMethodInvocation() var result = caller(new Calculator(), 1, 2); // Assert - result.Should().Be(3); + result.ShouldBe(3); } [Test] @@ -55,7 +55,7 @@ public void CompileMethodInvocation_CanUpcast() // Assert var c = (Number) result; - c.I.Should().Be(3); + c.I.ShouldBe(3); } [Test] @@ -71,7 +71,7 @@ public void CompileMethodInvocation_CanDoBothUpcastAndPass() // Assert var c = (Number)result; - c.I.Should().Be(3); + c.I.ShouldBe(3); } public interface INumber { } diff --git a/Source/EventFlow.Tests/UnitTests/Core/RetryDelayTests.cs b/Source/EventFlow.Tests/UnitTests/Core/RetryDelayTests.cs index 86d6a9950..b5163a549 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/RetryDelayTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/RetryDelayTests.cs @@ -23,8 +23,8 @@ using System; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core { @@ -46,8 +46,8 @@ public void PickDelay_IsWithinBounds() var delay = sut.PickDelay(); // Assert - delay.TotalMilliseconds.Should().BeGreaterOrEqualTo(min); - delay.TotalMilliseconds.Should().BeLessOrEqualTo(max); + delay.TotalMilliseconds.ShouldBeGreaterThanOrEqualTo(min); + delay.TotalMilliseconds.ShouldBeLessThanOrEqualTo(max); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Core/RetryStrategies/OptimisticConcurrencyRetryStrategyTests.cs b/Source/EventFlow.Tests/UnitTests/Core/RetryStrategies/OptimisticConcurrencyRetryStrategyTests.cs index f9694d334..895957c03 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/RetryStrategies/OptimisticConcurrencyRetryStrategyTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/RetryStrategies/OptimisticConcurrencyRetryStrategyTests.cs @@ -25,9 +25,9 @@ using EventFlow.Core.RetryStrategies; using EventFlow.Exceptions; using EventFlow.TestHelpers; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core.RetryStrategies { @@ -63,7 +63,7 @@ public void ShouldThisBeRetried_OptimisticConcurrencyException_ShouldBeRetired(i var shouldThisBeRetried = Sut.ShouldThisBeRetried(optimisticConcurrencyException, A(), currentRetryCount); // Assert - shouldThisBeRetried.ShouldBeRetried.Should().Be(expectedShouldThisBeRetried); + shouldThisBeRetried.ShouldBeRetried.ShouldBe(expectedShouldThisBeRetried); } [TestCase(0)] @@ -80,7 +80,7 @@ public void ShouldThisBeRetried_Exception_ShouldNeverBeRetired(int currentRetryC var shouldThisBeRetried = Sut.ShouldThisBeRetried(exception, A(), currentRetryCount); // Assert - shouldThisBeRetried.ShouldBeRetried.Should().BeFalse(); + shouldThisBeRetried.ShouldBeRetried.ShouldBeFalse(); } } diff --git a/Source/EventFlow.Tests/UnitTests/Core/TransientFaultHandlerTests.cs b/Source/EventFlow.Tests/UnitTests/Core/TransientFaultHandlerTests.cs index 8fd22e814..0ba677595 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/TransientFaultHandlerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/TransientFaultHandlerTests.cs @@ -25,10 +25,10 @@ using System.Threading.Tasks; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core { @@ -73,14 +73,14 @@ public async Task Result() var result = await Sut.TryAsync(c => action.Object(), A diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 89e630555..29f1502c4 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -10,7 +10,6 @@ - diff --git a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs index 5c6f60dc1..5f0dcda65 100644 --- a/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/BackwardCompatibilityTests.cs @@ -33,9 +33,9 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -71,9 +71,9 @@ public async Task ValidateTestAggregate() var testAggregate = await _aggregateStore.LoadAsync(_thingyId, CancellationToken.None); // Assert - testAggregate.Version.Should().Be(2); - testAggregate.PingsReceived.Should().Contain(PingId.With("95433aa0-11f7-4128-bd5f-18e0ecc4d7c1")); - testAggregate.PingsReceived.Should().Contain(PingId.With("2352d09b-4712-48cc-bb4f-5560d7c52558")); + testAggregate.Version.ShouldBe(2); + testAggregate.PingsReceived.ShouldContain(PingId.With("95433aa0-11f7-4128-bd5f-18e0ecc4d7c1")); + testAggregate.PingsReceived.ShouldContain(PingId.With("2352d09b-4712-48cc-bb4f-5560d7c52558")); } [Test, Explicit] diff --git a/Source/EventFlow.Tests/IntegrationTests/BasicTests.cs b/Source/EventFlow.Tests/IntegrationTests/BasicTests.cs index 2a9bfe38c..28bdb467a 100644 --- a/Source/EventFlow.Tests/IntegrationTests/BasicTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/BasicTests.cs @@ -38,9 +38,9 @@ using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.TestHelpers.Aggregates.ValueObjects; using EventFlow.Tests.IntegrationTests.ReadStores.ReadModels; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -126,10 +126,10 @@ public async Task BasicFlow(IEventFlowOptions eventFlowOptions) .ConfigureAwait(false); // Assert - pingReadModels.Should().HaveCount(2); - testAggregate.DomainErrorAfterFirstReceived.Should().BeTrue(); - testReadModelFromQuery1.DomainErrorAfterFirstReceived.Should().BeTrue(); - testReadModelFromQuery2.Should().NotBeNull(); + pingReadModels.Count.ShouldBe(2); + testAggregate.DomainErrorAfterFirstReceived.ShouldBeTrue(); + testReadModelFromQuery1.DomainErrorAfterFirstReceived.ShouldBeTrue(); + testReadModelFromQuery2.ShouldNotBeNull(); } } diff --git a/Source/EventFlow.Tests/IntegrationTests/CancellationTests.cs b/Source/EventFlow.Tests/IntegrationTests/CancellationTests.cs index c1ad2930a..1d166af68 100644 --- a/Source/EventFlow.Tests/IntegrationTests/CancellationTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/CancellationTests.cs @@ -44,9 +44,9 @@ using EventFlow.TestHelpers.Aggregates.ValueObjects; using EventFlow.TestHelpers.Extensions; using EventFlow.Tests.IntegrationTests.ReadStores.ReadModels; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -137,29 +137,29 @@ private List CreateSteps(ThingyId id) CancellationBoundary.BeforeCommittingEvents, _commandHandler.ExecuteCompletionSource, () => Task.FromResult(_commandHandler.HasBeenCalled), - v => v.Should().BeTrue(), - v => v.Should().BeFalse()), + v => v.ShouldBeTrue(), + v => v.ShouldBeFalse()), new Step>( CancellationBoundary.BeforeUpdatingReadStores, _eventPersistence.CommitCompletionSource, () => _eventPersistence.LoadCommittedEventsAsync(id, 0, CancellationToken.None), - v => v.Should().NotBeEmpty(), - v => v.Should().BeEmpty()), + v => v.ShouldNotBeEmpty(), + v => v.ShouldBeEmpty()), new Step>( CancellationBoundary.BeforeNotifyingSubscribers, _readStore.UpdateCompletionSource, () => _readStore.GetAsync(id.ToString(), CancellationToken.None), - v => v.ReadModel.Should().NotBeNull(), - v => v.ReadModel.Should().BeNull()), + v => v.ReadModel.ShouldNotBeNull(), + v => v.ReadModel.ShouldBeNull()), new Step( CancellationBoundary.CancelAlways, _subscriber.HandleCompletionSource, () => Task.FromResult(_subscriber.HasHandled), - v => v.Should().BeTrue(), - v => v.Should().BeFalse()) + v => v.ShouldBeTrue(), + v => v.ShouldBeFalse()) }; return steps; diff --git a/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs b/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs index 605f2caed..79a06cdf0 100644 --- a/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/CommandResultTests.cs @@ -24,15 +24,14 @@ using System.Threading.Tasks; using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; -using EventFlow.Configuration; using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.Tests.UnitTests.Specifications; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -107,14 +106,14 @@ public async Task CommandResult() new TestSuccessResultCommand(ThingyId.New), CancellationToken.None) .ConfigureAwait(false); - success.IsSuccess.Should().BeTrue(); - success.MagicNumber.Should().Be(42); + success.IsSuccess.ShouldBeTrue(); + success.MagicNumber.ShouldBe(42); var failed = await commandBus.PublishAsync( new TestFailedResultCommand(ThingyId.New), CancellationToken.None) .ConfigureAwait(false); - failed.IsSuccess.Should().BeFalse(); + failed.IsSuccess.ShouldBeFalse(); } } } diff --git a/Source/EventFlow.Tests/IntegrationTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/IntegrationTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs index 484211f58..536f553ad 100644 --- a/Source/EventFlow.Tests/IntegrationTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/ReadStores/MultipleAggregateReadStoreManagerTests.cs @@ -31,9 +31,9 @@ using EventFlow.Queries; using EventFlow.ReadStores; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; // ReSharper disable ClassNeverInstantiated.Local @@ -71,9 +71,7 @@ public async Task EventOrdering() new ReadModelByIdQuery(ReadModelId), CancellationToken.None); - readModelAb.Indexes.Should().BeEquivalentTo( - new []{0, 1, 2, 3}, - o => o.WithStrictOrdering()); + readModelAb.Indexes.ShouldBe(new []{0, 1, 2, 3}, ignoreOrder: false); } protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs index ced49a553..f36704984 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/AggregateSagaTests.cs @@ -25,7 +25,6 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Configuration; using EventFlow.Extensions; using EventFlow.Sagas; using EventFlow.Subscribers; @@ -35,10 +34,10 @@ using EventFlow.TestHelpers.Aggregates.Sagas; using EventFlow.TestHelpers.Aggregates.Sagas.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests.Sagas { @@ -54,7 +53,7 @@ public async Task InitialSagaStateIsNew() var thingySaga = await LoadSagaAsync(A()); // Assert - thingySaga.State.Should().Be(SagaState.New); + thingySaga.State.ShouldBe(SagaState.New); } [Test] @@ -68,7 +67,7 @@ public async Task PublishingEventWithoutStartingSagaLeavesItNew() // Assert var thingySaga = await LoadSagaAsync(thingyId); - thingySaga.State.Should().Be(SagaState.New); + thingySaga.State.ShouldBe(SagaState.New); } [Test] @@ -82,7 +81,7 @@ public async Task PublishingEventWithoutStartingDoesntPublishToMainAggregate() // Assert var thingyAggregate = await LoadAggregateAsync(thingyId); - thingyAggregate.Messages.Should().BeEmpty(); + thingyAggregate.Messages.ShouldBeEmpty(); } [Test] @@ -96,7 +95,7 @@ public async Task PublishingCompleteEventWithoutStartingSagaLeavesItNew() // Assert var thingySaga = await LoadSagaAsync(thingyId); - thingySaga.State.Should().Be(SagaState.New); + thingySaga.State.ShouldBe(SagaState.New); } [Test] @@ -110,7 +109,7 @@ public async Task PublishingStartTiggerEventStartsSaga() // Assert var thingySaga = await LoadSagaAsync(thingyId); - thingySaga.State.Should().Be(SagaState.Running); + thingySaga.State.ShouldBe(SagaState.Running); } [Test] @@ -125,7 +124,7 @@ public async Task PublishingStartAndCompleteTiggerEventsCompletesSaga() // Assert var thingySaga = await LoadSagaAsync(thingyId); - thingySaga.State.Should().Be(SagaState.Completed); + thingySaga.State.ShouldBe(SagaState.Completed); } [Test] @@ -158,18 +157,19 @@ public async Task PublishingStartAndCompleteWithPingsResultInCorrectMessages() // Assert - saga var thingySaga = await LoadSagaAsync(thingyId); - thingySaga.State.Should().Be(SagaState.Completed); - thingySaga.PingIdsSinceStarted.Should().BeEquivalentTo(pingsWithRunningSaga); + thingySaga.State.ShouldBe(SagaState.Completed); + thingySaga.PingIdsSinceStarted.ShouldBe(pingsWithRunningSaga, ignoreOrder: true); // Assert - aggregate var thingyAggregate = await LoadAggregateAsync(thingyId); - thingyAggregate.PingsReceived.Should().BeEquivalentTo( - pingsWithNewSaga.Concat(pingsWithRunningSaga).Concat(pingsWithCompletedSaga)); + thingyAggregate.PingsReceived.ShouldBe( + pingsWithNewSaga.Concat(pingsWithRunningSaga).Concat(pingsWithCompletedSaga), + ignoreOrder: true); var receivedSagaPingIds = thingyAggregate.Messages .Select(m => PingId.With(m.Message)) .ToList(); - receivedSagaPingIds.Should().HaveCount(3); - receivedSagaPingIds.Should().BeEquivalentTo(pingsWithRunningSaga); + receivedSagaPingIds.Count.ShouldBe(3); + receivedSagaPingIds.ShouldBe(pingsWithRunningSaga, ignoreOrder: true); } protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs index f3cb9be51..e3947748c 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTestClasses.cs @@ -28,12 +28,11 @@ using EventFlow.Aggregates; using EventFlow.Aggregates.ExecutionResults; using EventFlow.Commands; -using EventFlow.Configuration; using EventFlow.Core; using EventFlow.Sagas; using EventFlow.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; +using Shouldly; namespace EventFlow.Tests.IntegrationTests.Sagas { @@ -61,7 +60,7 @@ public InMemorySagaStore( public void UpdateShouldNotHaveBeenCalled() { - this._hasUpdateBeenCalled.Should().BeFalse(); + this._hasUpdateBeenCalled.ShouldBeFalse(); } public override async Task UpdateAsync( diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTests.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTests.cs index ea6dd833f..f040a2bc2 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/AlternativeSagaStoreTests.cs @@ -27,9 +27,9 @@ using EventFlow.Extensions; using EventFlow.Sagas; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests.Sagas { @@ -87,9 +87,9 @@ await _commandBus.PublishAsync( var testAggregate = await _aggregateStore.LoadAsync( aggregateId, CancellationToken.None); - testAggregate.As.Should().Be(1); - testAggregate.Bs.Should().Be(1); - testAggregate.Cs.Should().Be(1); + testAggregate.As.ShouldBe(1); + testAggregate.Bs.ShouldBe(1); + testAggregate.Cs.ShouldBe(1); } [Test] @@ -107,9 +107,9 @@ await _commandBus.PublishAsync( var testAggregate = await _aggregateStore.LoadAsync( aggregateId, CancellationToken.None); - testAggregate.As.Should().Be(0); - testAggregate.Bs.Should().Be(1); - testAggregate.Cs.Should().Be(0); + testAggregate.As.ShouldBe(0); + testAggregate.Bs.ShouldBe(1); + testAggregate.Cs.ShouldBe(0); } [Test] diff --git a/Source/EventFlow.Tests/IntegrationTests/Sagas/SagaErrorHandlerTests.cs b/Source/EventFlow.Tests/IntegrationTests/Sagas/SagaErrorHandlerTests.cs index dcbf666b9..efcd5281c 100644 --- a/Source/EventFlow.Tests/IntegrationTests/Sagas/SagaErrorHandlerTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/Sagas/SagaErrorHandlerTests.cs @@ -20,19 +20,18 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -using EventFlow.Configuration; using EventFlow.Sagas; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Sagas; -using FluentAssertions; using Moq; using NUnit.Framework; using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Shouldly; namespace EventFlow.Tests.IntegrationTests.Sagas { @@ -57,8 +56,8 @@ await CommandBus.PublishAsync(new ThingyThrowExceptionInSagaCommand(thingyId), C }; // Assert - commandPublishAction.Should().Throw() - .WithMessage("Exception thrown (as requested by ThingySagaExceptionRequestedEvent)"); + var exception = await Should.ThrowAsync(commandPublishAction); + exception.Message.ShouldContain("Exception thrown (as requested by ThingySagaExceptionRequestedEvent)"); } [Test] @@ -83,7 +82,7 @@ await CommandBus.PublishAsync(new ThingyThrowExceptionInSagaCommand(thingyId), C }; // Assert - commandPublishAction.Should().NotThrow(); + await Should.NotThrowAsync(commandPublishAction); } protected override IServiceProvider Configure(IEventFlowOptions eventFlowOptions) diff --git a/Source/EventFlow.Tests/IntegrationTests/SeparationTests.cs b/Source/EventFlow.Tests/IntegrationTests/SeparationTests.cs index e2b379dc4..6d138cb16 100644 --- a/Source/EventFlow.Tests/IntegrationTests/SeparationTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/SeparationTests.cs @@ -31,9 +31,9 @@ using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.TestHelpers.Aggregates.ValueObjects; using EventFlow.TestHelpers.Extensions; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -60,7 +60,7 @@ await resolver1.GetRequiredService().PublishAsync( thingyId, CancellationToken.None) .ConfigureAwait(false); - aggregate.IsNew.Should().BeTrue(); + aggregate.IsNew.ShouldBeTrue(); } } diff --git a/Source/EventFlow.Tests/IntegrationTests/ServiceProviderTests.cs b/Source/EventFlow.Tests/IntegrationTests/ServiceProviderTests.cs index f024fc34c..673b0456e 100644 --- a/Source/EventFlow.Tests/IntegrationTests/ServiceProviderTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/ServiceProviderTests.cs @@ -29,9 +29,9 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Queries; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.IntegrationTests { @@ -64,10 +64,8 @@ public async Task ResolverAggregatesFactoryCanResolve() var serviceDependentAggregate = await aggregateFactory.CreateNewAggregateAsync(ThingyId.New).ConfigureAwait(false); // Assert - serviceDependentAggregate.Service.Should() - .NotBeNull() - .And - .BeOfType(); + serviceDependentAggregate.Service.ShouldNotBeNull(); + serviceDependentAggregate.Service.ShouldBeOfType(); } } diff --git a/Source/EventFlow.Tests/IntegrationTests/UnicodeTests.cs b/Source/EventFlow.Tests/IntegrationTests/UnicodeTests.cs index cdb727310..0e8a420c6 100644 --- a/Source/EventFlow.Tests/IntegrationTests/UnicodeTests.cs +++ b/Source/EventFlow.Tests/IntegrationTests/UnicodeTests.cs @@ -31,10 +31,10 @@ using EventFlow.EventStores; using EventFlow.Extensions; using EventFlow.TestHelpers; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Shouldly; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo @@ -51,7 +51,7 @@ public void UpperCaseIdentityThrows() Action action = () => new Identität1("Identität1-00000000-0000-0000-0000-000000000000"); // Assert - action.Should().Throw(); + action.ShouldThrow(); } [Test] @@ -61,7 +61,7 @@ public void LowerCaseIdentityWorks() var id = new Identität1("identität1-00000000-0000-0000-0000-000000000000"); // Assert - id.GetGuid().Should().BeEmpty(); + id.GetGuid().ShouldBe(Guid.Empty); } [Test] @@ -71,7 +71,7 @@ public void UnicodeIdentities() var identität = Identität1.New.Value; // Assert - identität.Should().StartWith("identität1-"); + identität.ShouldStartWith("identität1-"); } [Test] @@ -86,7 +86,7 @@ public void UnicodeCommands() Action action = () => commandDefinitions.Load(typeof(Cömmand)); // Assert - action.Should().NotThrow(); + action.ShouldNotThrow(); } [Test] @@ -102,7 +102,7 @@ public void UnicodeEvents() Action action = () => eventDefinitionService.Load(typeof(Püng1Event)); // Assert - action.Should().NotThrow(); + action.ShouldNotThrow(); } [Test] diff --git a/Source/EventFlow.Tests/ReadMeExamples.cs b/Source/EventFlow.Tests/ReadMeExamples.cs index 3e9101593..4159a145a 100644 --- a/Source/EventFlow.Tests/ReadMeExamples.cs +++ b/Source/EventFlow.Tests/ReadMeExamples.cs @@ -29,9 +29,9 @@ using EventFlow.Extensions; using EventFlow.Queries; using EventFlow.ReadStores; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests { @@ -69,7 +69,7 @@ await commandBus.PublishAsync( new ReadModelByIdQuery(exampleId), CancellationToken.None); // Verify that the read model has the expected magic number - exampleReadModel.MagicNumber.Should().Be(42); + exampleReadModel.MagicNumber.ShouldBe(42); } } diff --git a/Source/EventFlow.Tests/UnitTests/ValueObjects/SingleValueObjectTests.cs b/Source/EventFlow.Tests/UnitTests/ValueObjects/SingleValueObjectTests.cs index 12d1bf954..7376fc0cc 100644 --- a/Source/EventFlow.Tests/UnitTests/ValueObjects/SingleValueObjectTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ValueObjects/SingleValueObjectTests.cs @@ -24,9 +24,9 @@ using System.Linq; using EventFlow.TestHelpers; using EventFlow.ValueObjects; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.ValueObjects { @@ -57,16 +57,14 @@ public void Ordering() // Arrange var values = Many(10); var orderedValues = values.OrderBy(s => s).ToList(); - values.Should().NotEqual(orderedValues); // Data test + values.ShouldNotBe(orderedValues); // Data test var singleValueObjects = values.Select(s => new StringSingleValue(s)).ToList(); // Act var orderedSingleValueObjects = singleValueObjects.OrderBy(v => v).ToList(); // Assert - orderedSingleValueObjects.Select(v => v.Value).Should().BeEquivalentTo( - orderedValues, - o => o.WithStrictOrdering()); + orderedSingleValueObjects.Select(v => v.Value).ShouldBe(orderedValues, ignoreOrder: false); } [Test] @@ -75,16 +73,14 @@ public void EnumOrdering() // Arrange var values = Many(10); var orderedValues = values.OrderBy(s => s).ToList(); - values.Should().NotEqual(orderedValues); // Data test + values.ShouldNotBe(orderedValues); // Data test var singleValueObjects = values.Select(s => new MagicEnumSingleValue(s)).ToList(); // Act var orderedSingleValueObjects = singleValueObjects.OrderBy(v => v).ToList(); // Assert - orderedSingleValueObjects.Select(v => v.Value).Should().BeEquivalentTo( - orderedValues, - o => o.WithStrictOrdering()); + orderedSingleValueObjects.Select(v => v.Value).ShouldBe(orderedValues, ignoreOrder: false); } [Test] @@ -93,7 +89,7 @@ public void ProtectAgainsInvalidEnumValues() // Act + Assert // ReSharper disable once ObjectCreationAsStatement var exception = Assert.Throws(() => new MagicEnumSingleValue((MagicEnum)42)); - exception.Message.Should().Be("The value '42' isn't defined in enum 'MagicEnum'"); + exception.Message.ShouldBe("The value '42' isn't defined in enum 'MagicEnum'"); } [Test] @@ -115,15 +111,13 @@ public void EnumOrderingManual() .ToList(); // Assert - orderedValues.Should().BeEquivalentTo( - new [] + orderedValues.ShouldBe(new [] { MagicEnum.Zero, MagicEnum.One, MagicEnum.Two, MagicEnum.Three, - }, - o => o.WithStrictOrdering()); + }, ignoreOrder: false); } [Test] @@ -135,7 +129,7 @@ public void NullEquals() // Assert // ReSharper disable once ExpressionIsAlwaysNull - obj.Equals(null_).Should().BeFalse(); + obj.Equals(null_).ShouldBeFalse(); } [Test] @@ -147,8 +141,8 @@ public void EqualsForSameValues() var obj2 = new StringSingleValue(value); // Assert - (obj1 == obj2).Should().BeTrue(); - obj1.Equals(obj2).Should().BeTrue(); + (obj1 == obj2).ShouldBeTrue(); + obj1.Equals(obj2).ShouldBeTrue(); } [Test] @@ -161,8 +155,8 @@ public void EqualsForDifferentValues() var obj2 = new StringSingleValue(value2); // Assert - (obj1 == obj2).Should().BeFalse(); - obj1.Equals(obj2).Should().BeFalse(); + (obj1 == obj2).ShouldBeFalse(); + obj1.Equals(obj2).ShouldBeFalse(); } private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings @@ -198,8 +192,8 @@ public void DeserializeNullableIntWithoutValue() var with = JsonConvert.DeserializeObject(json); // Assert - with.Should().NotBeNull(); - with.I.Should().BeNull(); + with.ShouldNotBeNull(); + with.I.ShouldBeNull(); } [Test] @@ -212,8 +206,8 @@ public void DeserializeNullableIntWithNullValue() var with = JsonConvert.DeserializeObject(json); // Assert - with.Should().NotBeNull(); - with.I.Should().BeNull(); + with.ShouldNotBeNull(); + with.I.ShouldBeNull(); } [Test] @@ -227,8 +221,8 @@ public void DeserializeNullableIntWithValue() var with = JsonConvert.DeserializeObject(json); // Assert - with.Should().NotBeNull(); - with.I.Value.Should().Be(i); + with.ShouldNotBeNull(); + with.I.Value.ShouldBe(i); } [Test] @@ -241,7 +235,7 @@ public void SerializeNullableIntWithoutValue() var json = JsonConvert.SerializeObject(with, Settings); // Assert - json.Should().Be("{}"); + json.ShouldBe("{}"); } [Test] @@ -254,7 +248,7 @@ public void SerializeNullableIntWithValue() var json = JsonConvert.SerializeObject(with, Settings); // Assert - json.Should().Be("{\"I\":42}"); + json.ShouldBe("{\"I\":42}"); } } } From f978c3e0393e10cdda4d4a5c7748d672f1ee9eb4 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 13:44:36 +0300 Subject: [PATCH 09/56] tests --- .../UnitTests/Commands/CommandTests.cs | 8 ++++---- .../Commands/DistinctCommandTests.cs | 4 ++-- .../NamespaceAndClassNameStrategyTest.cs | 4 ++-- .../NamespaceAndNameStrategyTest.cs | 5 ++--- .../EventNamingStrategy/VoidStrategyTest.cs | 4 ++-- .../Serialization/JsonOptionsTests.cs | 12 +++++------ .../UnitTests/Core/CircularBufferTests.cs | 10 +++++----- .../ReadModelDomainEventApplierTests.cs | 20 +++++++++---------- .../ReadStores/ReadModelFactoryTests.cs | 10 +++++----- .../SingleAggregateReadStoreManagerTests.cs | 20 ++++++++++--------- 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs b/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs index 3d23150a8..e93498877 100644 --- a/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Commands/CommandTests.cs @@ -25,9 +25,9 @@ using EventFlow.Commands; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Commands { @@ -55,9 +55,9 @@ public void SerializeDeserialize() var deserialized = JsonConvert.DeserializeObject(json); // Assert - deserialized.CriticalData.Should().Be(criticalCommand.CriticalData); - deserialized.SourceId.Should().Be(criticalCommand.SourceId); - deserialized.AggregateId.Should().Be(criticalCommand.AggregateId); + deserialized.CriticalData.ShouldBe(criticalCommand.CriticalData); + deserialized.SourceId.ShouldBe(criticalCommand.SourceId); + deserialized.AggregateId.ShouldBe(criticalCommand.AggregateId); } } } diff --git a/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs b/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs index 052b73513..879f90584 100644 --- a/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Commands/DistinctCommandTests.cs @@ -27,8 +27,8 @@ using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Commands { @@ -66,7 +66,7 @@ public void Arguments(string aggregateId, int magicNumber, string expectedSouceI var sourceId = command.SourceId; // Assert - sourceId.Value.Should().Be(expectedSouceId); + sourceId.Value.ShouldBe(expectedSouceId); } } } diff --git a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndClassNameStrategyTest.cs b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndClassNameStrategyTest.cs index 52119fb32..49ba644b7 100644 --- a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndClassNameStrategyTest.cs +++ b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndClassNameStrategyTest.cs @@ -22,8 +22,8 @@ using EventFlow.Configuration.EventNamingStrategy; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Configuration.EventNamingStrategy { @@ -42,7 +42,7 @@ public void EventNameShouldBeNamespaceAndClassName() var name = strategy.CreateEventName(1, typeof(Any), "OriginalName"); // Assert - name.Should().Be(GetType().Namespace + ".Any"); + name.ShouldBe(GetType().Namespace + ".Any"); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndNameStrategyTest.cs b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndNameStrategyTest.cs index b144cb51c..8bcb5b3ab 100644 --- a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndNameStrategyTest.cs +++ b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/NamespaceAndNameStrategyTest.cs @@ -21,9 +21,8 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. using EventFlow.Configuration.EventNamingStrategy; -using EventFlow.EventStores; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Configuration.EventNamingStrategy { @@ -41,7 +40,7 @@ public void EventNameShouldBeNamespaceAndClassName() var name = strategy.CreateEventName(1, typeof(Any), "NameFromAttribute"); // Assert - name.Should().Be(GetType().Namespace + ".NameFromAttribute"); + name.ShouldBe(GetType().Namespace + ".NameFromAttribute"); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/VoidStrategyTest.cs b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/VoidStrategyTest.cs index 3fee98f5a..2a74e678e 100644 --- a/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/VoidStrategyTest.cs +++ b/Source/EventFlow.Tests/UnitTests/Configuration/EventNamingStrategy/VoidStrategyTest.cs @@ -22,8 +22,8 @@ using EventFlow.Configuration.EventNamingStrategy; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Configuration.EventNamingStrategy { @@ -42,7 +42,7 @@ public void EventNameShouldBeUnchanged() var name = strategy.CreateEventName(1, typeof(Any), "OriginalName"); // Assert - name.Should().Be("OriginalName"); + name.ShouldBe("OriginalName"); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Configuration/Serialization/JsonOptionsTests.cs b/Source/EventFlow.Tests/UnitTests/Configuration/Serialization/JsonOptionsTests.cs index acf6948fa..38d447024 100644 --- a/Source/EventFlow.Tests/UnitTests/Configuration/Serialization/JsonOptionsTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Configuration/Serialization/JsonOptionsTests.cs @@ -24,10 +24,10 @@ using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.ValueObjects; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; using System; +using Shouldly; namespace EventFlow.Tests.UnitTests.Configuration.Serialization { @@ -77,11 +77,11 @@ public void JsonOptionsCanBeUsedToConstructJsonSerializerSettings() var svoDeserialized = JsonConvert.DeserializeObject(svoSerialized); // Assert - myClassSerialized.Should().Be("1000000"); - myClassDeserialized.DateTime.Ticks.Should().Be(1000000); - myClassDeserialized.DateTime.Ticks.Should().NotBe(10); - svoDeserialized.Should().Be(new MySingleValueObject(new DateTime(1970, 1, 1))); - svoDeserialized.Should().NotBe(new MySingleValueObject(new DateTime(2001, 1, 1))); + myClassSerialized.ShouldBe("1000000"); + myClassDeserialized.DateTime.Ticks.ShouldBe(1000000); + myClassDeserialized.DateTime.Ticks.ShouldNotBe(10); + svoDeserialized.ShouldBe(new MySingleValueObject(new DateTime(1970, 1, 1))); + svoDeserialized.ShouldNotBe(new MySingleValueObject(new DateTime(2001, 1, 1))); } } } diff --git a/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs b/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs index 3c318c2b6..8d52f5a5a 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs @@ -23,8 +23,8 @@ using System.Linq; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Core { @@ -50,7 +50,7 @@ public void Put(params int[] numbers) // Assert var shouldContain = numbers.Reverse().Take(capacity).ToList(); - sut.Should().Contain(shouldContain); + sut.ShouldBe(shouldContain); } [Test] @@ -67,7 +67,7 @@ public void OrderAboveCapacity() var numbers = sut.ToArray(); // Assert - numbers.Should().ContainInOrder(2, 3, 4); + numbers.ShouldBe(new[] {2, 3, 4}, ignoreOrder: false); } [Test] @@ -83,7 +83,7 @@ public void OrderAtCapacity() var numbers = sut.ToArray(); // Assert - numbers.Should().ContainInOrder(1, 2, 3); + numbers.ShouldBe( new[] {1, 2, 3}, ignoreOrder: false); } [Test] @@ -98,7 +98,7 @@ public void OrderBelowCapacity() var numbers = sut.ToArray(); // Assert - numbers.Should().ContainInOrder(1, 2); + numbers.ShouldBe(new[] {1, 2}, ignoreOrder: false); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs index 760026d17..13a777519 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelDomainEventApplierTests.cs @@ -27,8 +27,8 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.ReadStores { @@ -109,7 +109,7 @@ public async Task ReadModelDoesNotReceiveOtherEvents() await Sut.UpdateReadModelAsync(readModel, events, A(), CancellationToken.None).ConfigureAwait(false); // Assert - readModel.PingEventsReceived.Should().BeFalse(); + readModel.PingEventsReceived.ShouldBeFalse(); } [Test] @@ -138,8 +138,8 @@ await Sut.UpdateReadModelAsync( .ConfigureAwait(false); // Assert - pingReadModel.PingEventsReceived.Should().BeTrue(); - theOtherPingReadModel.PingEventsReceived.Should().BeTrue(); + pingReadModel.PingEventsReceived.ShouldBeTrue(); + theOtherPingReadModel.PingEventsReceived.ShouldBeTrue(); } [Test] @@ -169,8 +169,8 @@ await Sut.UpdateReadModelAsync( .ConfigureAwait(false); // Assert - pingReadModel.PingEventsReceived.Should().BeTrue(); - domainErrorAfterFirstReadModel.DomainErrorAfterFirstEventsReceived.Should().BeTrue(); + pingReadModel.PingEventsReceived.ShouldBeTrue(); + domainErrorAfterFirstReadModel.DomainErrorAfterFirstEventsReceived.ShouldBeTrue(); } [Test] @@ -190,7 +190,7 @@ public async Task UpdateReturnsFalseIfNoEventsWasApplied() CancellationToken.None); // Assert - appliedAny.Should().BeFalse(); + appliedAny.ShouldBeFalse(); } [Test] @@ -210,7 +210,7 @@ public async Task UpdateReturnsTrueIfEventsWereApplied() CancellationToken.None); // Assert - appliedAny.Should().BeTrue(); + appliedAny.ShouldBeTrue(); } [Test] @@ -232,7 +232,7 @@ await Sut.UpdateReadModelAsync( .ConfigureAwait(false); // Assert - readModel.PingEventsReceived.Should().BeTrue(); + readModel.PingEventsReceived.ShouldBeTrue(); } [Test] @@ -254,7 +254,7 @@ await Sut.UpdateReadModelAsync( .ConfigureAwait(false); // Assert - readModel.PingEventsReceived.Should().BeTrue(); + readModel.PingEventsReceived.ShouldBeTrue(); } } } diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTests.cs index 8d91cbee6..8614186a8 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelFactoryTests.cs @@ -25,10 +25,10 @@ using System.Threading.Tasks; using EventFlow.ReadStores; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; using EventFlow.Extensions; using Microsoft.Extensions.DependencyInjection; +using Shouldly; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable ClassNeverInstantiated.Global @@ -61,9 +61,9 @@ public async Task ReadModelFactoryCanBeConfigured() var readModelC = await resolver.GetRequiredService>().CreateAsync(A(), CancellationToken.None); // Assert - readModelA.MagicNumber.Should().Be(expectedMagicNumberForReadModelA); - readModelB.MagicNumber.Should().Be(expectedMagicNumberForReadModelB); - readModelC.MagicNumber.Should().Be(expectedMagicNumberForReadModelC); + readModelA.MagicNumber.ShouldBe(expectedMagicNumberForReadModelA); + readModelB.MagicNumber.ShouldBe(expectedMagicNumberForReadModelB); + readModelC.MagicNumber.ShouldBe(expectedMagicNumberForReadModelC); } } @@ -76,7 +76,7 @@ public void ThrowsExceptionForNoEmptyConstructors() // Assert // ReSharper disable once PossibleNullReferenceException - exception.InnerException.Message.Should().Contain("doesn't have an empty constructor"); + exception.InnerException.Message.ShouldContain("doesn't have an empty constructor"); } public class ReadModelWithConstructorArguments : IReadModel diff --git a/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs b/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs index 08ba5408b..a6f3926bf 100644 --- a/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/ReadStores/SingleAggregateReadStoreManagerTests.cs @@ -32,9 +32,9 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Extensions; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.ReadStores { @@ -71,8 +71,8 @@ public async Task EventsAreApplied() await Sut.UpdateReadStoresAsync(emittedEvents, CancellationToken.None).ConfigureAwait(false); // Assert - AppliedDomainEvents.Should().HaveCount(emittedEvents.Length); - AppliedDomainEvents.Should().BeEquivalentTo(emittedEvents); + AppliedDomainEvents.Count.ShouldBe(emittedEvents.Length); + AppliedDomainEvents.ShouldBe(emittedEvents); } [Test] @@ -93,8 +93,8 @@ public async Task AlreadyAppliedEventsAreNotApplied() await Sut.UpdateReadStoresAsync(emittedEvents, CancellationToken.None).ConfigureAwait(false); // Assert - AppliedDomainEvents.Should().BeEmpty(); - resultingReadModelUpdates.Single().IsModified.Should().BeFalse(); + AppliedDomainEvents.ShouldBeEmpty(); + resultingReadModelUpdates.Single().IsModified.ShouldBeFalse(); } [Test] @@ -115,7 +115,7 @@ public async Task OutdatedEventsAreNotApplied() await Sut.UpdateReadStoresAsync(emittedEvents, CancellationToken.None); // Assert - AppliedDomainEvents.Should().BeEmpty(); + AppliedDomainEvents.ShouldBeEmpty(); } [Test] @@ -146,8 +146,8 @@ public async Task StoredEventsAreAppliedIfThereAreMissingEvents() await Sut.UpdateReadStoresAsync(emittedEvents, CancellationToken.None).ConfigureAwait(false); // Assert - AppliedDomainEvents.Should().HaveCount(storedEvents.Length); - AppliedDomainEvents.Should().BeEquivalentTo(storedEvents); + AppliedDomainEvents.Count.ShouldBe(storedEvents.Length); + AppliedDomainEvents.ShouldBe(storedEvents); } [Test] @@ -159,7 +159,9 @@ public void ThrowsIfReadModelSubscribesNoEvents() ReadModelWithoutEvents>(null, null, null, null, null, null); }; - a.Should().Throw().WithInnerException().WithMessage("*does not implement any*"); + var exception = Should.Throw(a); + exception.InnerException.ShouldNotBeNull(); + exception.InnerException.Message.ShouldContain("does not implement any"); } private class ReadModelWithoutEvents : IReadModel From 58ec279aa321a8363361d2f10fe351b5305608bb Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 13:57:50 +0300 Subject: [PATCH 10/56] tests --- .../Aggregates/AggregateFactoryTests.cs | 12 +-- .../UnitTests/Aggregates/AggregateIdTests.cs | 14 +-- .../AggregateRootApplyEventTests.cs | 4 +- .../Aggregates/AggregateRootNameTests.cs | 4 +- .../Aggregates/AggregateRootTests.cs | 41 +++++---- .../Aggregates/AggregateStateTests.cs | 4 +- .../UnitTests/Aggregates/MetadataTests.cs | 32 +++---- .../ConcurrentFilesEventPersistanceTests.cs | 8 +- ...ConcurrentInMemoryEventPersistanceTests.cs | 4 +- .../EventDefinitionServiceTests.cs | 15 ++-- .../EventStores/EventUpgradeManagerTests.cs | 12 +-- .../AggregateSagas/AggregateSagaTests.cs | 88 +++++++++---------- .../UnitTests/Sagas/DispatchToSagasTests.cs | 5 +- .../Sagas/SagaDefinitionServiceTests.cs | 10 +-- .../Snapshots/SnapshotAggregateRootTests.cs | 12 +-- .../Snapshots/SnapshotMetadataTests.cs | 28 +++--- .../Snapshots/SnapshotSerializerTests.cs | 18 ++-- .../Snapshots/SnapshotSerilizerTests.cs | 18 ++-- .../Snapshots/SnapshotUpgradeServiceTests.cs | 8 +- .../SnapshotEveryFewVersionsStrategyTests.cs | 6 +- 20 files changed, 169 insertions(+), 174 deletions(-) diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs index 370b6a9dd..77fdf18ae 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateFactoryTests.cs @@ -25,9 +25,9 @@ using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -52,8 +52,8 @@ public async Task CanCreateIdOnlyAggregateRootAsync() var idOnlyAggregateRoot = await Sut.CreateNewAggregateAsync(aggregateId).ConfigureAwait(false); // Assert - idOnlyAggregateRoot.Should().NotBeNull(); - idOnlyAggregateRoot.Id.Should().Be(aggregateId); + idOnlyAggregateRoot.ShouldNotBeNull(); + idOnlyAggregateRoot.Id.ShouldBe(aggregateId); } [Test] @@ -68,9 +68,9 @@ public async Task CanCreateAggregateWithServices() var aggregateWithServices = await Sut.CreateNewAggregateAsync(aggregateId).ConfigureAwait(false); // Assert - aggregateWithServices.Should().NotBeNull(); - aggregateWithServices.Id.Should().Be(aggregateId); - aggregateWithServices.Service.Should().BeSameAs(serviceMock.Object); + aggregateWithServices.ShouldNotBeNull(); + aggregateWithServices.Id.ShouldBe(aggregateId); + aggregateWithServices.Service.ShouldBeSameAs(serviceMock.Object); } diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateIdTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateIdTests.cs index 6bd395ed1..1902bbee7 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateIdTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateIdTests.cs @@ -22,8 +22,8 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -40,7 +40,7 @@ public void ManuallyCreatedIsOk() var testId = ThingyId.With(value); // Test - testId.Value.Should().Be(value); + testId.Value.ShouldBe(value); } [Test] @@ -51,7 +51,7 @@ public void CreatedIsDifferent() var id2 = ThingyId.New; // Assert - id1.Value.Should().NotBe(id2.Value); + id1.Value.ShouldNotBe(id2.Value); } [Test] @@ -63,8 +63,8 @@ public void SameIdsAreEqual() var id2 = ThingyId.With(value); // Assert - id1.Equals(id2).Should().BeTrue(); - (id1 == id2).Should().BeTrue(); + id1.Equals(id2).ShouldBeTrue(); + (id1 == id2).ShouldBeTrue(); } [Test] @@ -75,8 +75,8 @@ public void DifferentAreNotEqual() var id2 = ThingyId.With("thingy-d15b1562-11f2-4645-8b1a-f8b946b566d3"); // Assert - id1.Equals(id2).Should().BeFalse(); - (id1 == id2).Should().BeFalse(); + id1.Equals(id2).ShouldBeFalse(); + (id1 == id2).ShouldBeFalse(); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootApplyEventTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootApplyEventTests.cs index 2505551bc..214f8b673 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootApplyEventTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootApplyEventTests.cs @@ -23,8 +23,8 @@ using EventFlow.Aggregates; using EventFlow.Core; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -41,7 +41,7 @@ public void EventApplier() myAggregate.Count(42); // Assert - myAggregate.State.Count.Should().Be(42); + myAggregate.State.Count.ShouldBe(42); } diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootNameTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootNameTests.cs index 73f6861ee..1bb90e212 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootNameTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootNameTests.cs @@ -24,8 +24,8 @@ using EventFlow.Aggregates; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -57,7 +57,7 @@ public void AggregateName(Type aggregateType, string expectedName) var aggregate = (IAggregateRoot) Activator.CreateInstance(aggregateType, ThingyId.New); // Assert - aggregate.Name.Value.Should().Be(expectedName); + aggregate.Name.Value.ShouldBe(expectedName); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs index 7786637c2..eac930c40 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateRootTests.cs @@ -27,8 +27,8 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -40,9 +40,9 @@ public class AggregateRootTests : TestsFor public void InitialVersionIsZero() { // Assert - Sut.Version.Should().Be(0); - Sut.IsNew.Should().BeTrue(); - Sut.UncommittedEvents.Count().Should().Be(0); + Sut.Version.ShouldBe(0); + Sut.IsNew.ShouldBeTrue(); + Sut.UncommittedEvents.Count().ShouldBe(0); } [Test] @@ -52,10 +52,10 @@ public void ApplyingEventIncrementsVersion() Sut.Ping(PingId.New); // Assert - Sut.Version.Should().Be(1); - Sut.IsNew.Should().BeFalse(); - Sut.UncommittedEvents.Count().Should().Be(1); - Sut.PingsReceived.Count.Should().Be(1); + Sut.Version.ShouldBe(1); + Sut.IsNew.ShouldBeFalse(); + Sut.UncommittedEvents.Count().ShouldBe(1); + Sut.PingsReceived.Count.ShouldBe(1); } [Test] @@ -73,10 +73,10 @@ public void EventsCanBeApplied() Sut.ApplyEvents(domainEvents); // Assert - Sut.IsNew.Should().BeFalse(); - Sut.Version.Should().Be(2); - Sut.PingsReceived.Count.Should().Be(2); - Sut.UncommittedEvents.Count().Should().Be(0); + Sut.IsNew.ShouldBeFalse(); + Sut.Version.ShouldBe(2); + Sut.PingsReceived.Count.ShouldBe(2); + Sut.UncommittedEvents.Count().ShouldBe(0); } [Test] @@ -86,7 +86,7 @@ public void EmptyListCanBeApplied() Sut.ApplyEvents(new IDomainEvent[]{}); // Assert - Sut.Version.Should().Be(0); + Sut.Version.ShouldBe(0); } [Test] @@ -96,7 +96,7 @@ public void ApplyIsInvoked() Sut.DomainErrorAfterFirst(); // Assert - Sut.DomainErrorAfterFirstReceived.Should().BeTrue(); + Sut.DomainErrorAfterFirstReceived.ShouldBeTrue(); } [Test] @@ -106,7 +106,7 @@ public void ApplyIsInvokedForExplicitImplementations() Sut.Delete(); // Assert - Sut.IsDeleted.Should().BeTrue(); + Sut.IsDeleted.ShouldBeTrue(); } [Test] @@ -119,7 +119,8 @@ public void UncommittedEventIdsShouldBeDistinct() // Assert Sut.UncommittedEvents .Select(e => e.Metadata.EventId).Distinct() - .Should().HaveCount(2); + .Count() + .ShouldBe(2); } [Test] @@ -139,12 +140,10 @@ public void UncommittedEventIdsShouldBeDeterministic() // GuidFactories.Deterministic.Namespaces.Events, $"{thingyId.Value}-v1" eventIdGuids[0] - .Should() - .Be("event-3dde5ccb-b594-59b4-ad0a-4d432ffce026"); + .ShouldBe("event-3dde5ccb-b594-59b4-ad0a-4d432ffce026"); // GuidFactories.Deterministic.Namespaces.Events, $"{thingyId.Value}-v2" eventIdGuids[1] - .Should() - .Be("event-2e79868f-6ef7-5c88-a941-12ae7ae801c7"); + .ShouldBe("event-2e79868f-6ef7-5c88-a941-12ae7ae801c7"); } [Test] @@ -158,7 +157,7 @@ public void ApplyEventWithOutOfOrderSequenceNumberShouldThrow() Action applyingEvents = () => Sut.ApplyEvents(new []{ domainEvent }); // Assert - applyingEvents.Should().Throw(); + applyingEvents.ShouldThrow(); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStateTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStateTests.cs index 6c96afbc2..591f0bd86 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStateTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/AggregateStateTests.cs @@ -26,8 +26,8 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -44,7 +44,7 @@ public void ApplyIsInvoked() Sut.Apply(null, new ThingyPingEvent(pingId)); // Assert - Sut.PingIds.Should().Contain(pingId); + Sut.PingIds.ShouldContain(pingId); } public class TestAggregateState : AggregateState, diff --git a/Source/EventFlow.Tests/UnitTests/Aggregates/MetadataTests.cs b/Source/EventFlow.Tests/UnitTests/Aggregates/MetadataTests.cs index ed74f4eaf..5166353a6 100644 --- a/Source/EventFlow.Tests/UnitTests/Aggregates/MetadataTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Aggregates/MetadataTests.cs @@ -24,9 +24,9 @@ using System.Collections.Generic; using EventFlow.Aggregates; using EventFlow.TestHelpers; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Aggregates { @@ -46,7 +46,7 @@ public void TimestampIsSerializedCorrectly() }; // Assert - sut.Timestamp.Should().Be(timestamp); + sut.Timestamp.ShouldBe(timestamp); } [Test] @@ -62,7 +62,7 @@ public void EventNameIsSerializedCorrectly() }; // Assert - sut.EventName.Should().Be(eventName); + sut.EventName.ShouldBe(eventName); } [Test] @@ -78,7 +78,7 @@ public void EventVersionIsSerializedCorrectly() }; // Assert - sut.EventVersion.Should().Be(eventVersion); + sut.EventVersion.ShouldBe(eventVersion); } [Test] @@ -94,7 +94,7 @@ public void AggregateSequenceNumberIsSerializedCorrectly() }; // Assert - sut.AggregateSequenceNumber.Should().Be(aggregateSequenceNumber); + sut.AggregateSequenceNumber.ShouldBe(aggregateSequenceNumber); } [Test] @@ -111,12 +111,12 @@ public void CloneWithCanMerge() var metadata2 = metadata1.CloneWith(new KeyValuePair(key2, value2)); // Assert - metadata1.ContainsKey(key2).Should().BeFalse(); + metadata1.ContainsKey(key2).ShouldBeFalse(); - metadata2.ContainsKey(key1).Should().BeTrue(); - metadata2.ContainsKey(key2).Should().BeTrue(); - metadata2[key1].Should().Be(value1); - metadata2[key2].Should().Be(value2); + metadata2.ContainsKey(key1).ShouldBeTrue(); + metadata2.ContainsKey(key2).ShouldBeTrue(); + metadata2[key1].ShouldBe(value1); + metadata2[key2].ShouldBe(value2); } [Test] @@ -138,10 +138,10 @@ public void SerializeDeserializeWithValues() var metadata = JsonConvert.DeserializeObject(json); // Assert - metadata.Count.Should().Be(3); - metadata.AggregateName.Should().Be(aggregateName); - metadata.AggregateSequenceNumber.Should().Be(aggregateSequenceNumber); - metadata.Timestamp.Should().Be(timestamp); + metadata.Count.ShouldBe(3); + metadata.AggregateName.ShouldBe(aggregateName); + metadata.AggregateSequenceNumber.ShouldBe(aggregateSequenceNumber); + metadata.Timestamp.ShouldBe(timestamp); } [Test] @@ -155,8 +155,8 @@ public void SerializeDeserializeEmpty() var metadata = JsonConvert.DeserializeObject(json); // Assert - json.Should().Be("{}"); - metadata.Count.Should().Be(0); + json.ShouldBe("{}"); + metadata.Count.ShouldBe(0); } } } diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentFilesEventPersistanceTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentFilesEventPersistanceTests.cs index 6c2a2e84b..685aa11a6 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentFilesEventPersistanceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentFilesEventPersistanceTests.cs @@ -38,9 +38,9 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.EventStores { @@ -103,7 +103,7 @@ public void MultipleInstancesWithSamePathFail() Action action = () => Task.WaitAll(tasks.ToArray()); // Assert - action.Should().Throw("because of concurrent access to the same files."); + action.ShouldThrow("because of concurrent access to the same files."); } [Test] @@ -120,7 +120,7 @@ public void MultipleInstancesWithDifferentPathsWork() Action action = () => Task.WaitAll(tasks.ToArray()); // Assert - action.Should().NotThrow(); + action.ShouldNotThrow(); } [Test] @@ -137,7 +137,7 @@ public void SingleInstanceWorks() Action action = () => Task.WaitAll(tasks.ToArray()); // Assert - action.Should().NotThrow(); + action.ShouldNotThrow(); } private IFilesEventStoreConfiguration ConfigurePath(string storePath) diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentInMemoryEventPersistanceTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentInMemoryEventPersistanceTests.cs index db64289a0..03a654718 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentInMemoryEventPersistanceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/ConcurrentInMemoryEventPersistanceTests.cs @@ -38,8 +38,8 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.EventStores { @@ -75,7 +75,7 @@ public async Task MultipleInstances() int.MaxValue, new EventUpgradeContext(), CancellationToken.None); - allEvents.DomainEvents.Count.Should().Be(NumberOfEvents * DegreeOfParallelism); + allEvents.DomainEvents.Count.ShouldBe(NumberOfEvents * DegreeOfParallelism); } private EventStoreBase CreateStore() diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs index 471ed1366..ecfc1c5a8 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs @@ -24,14 +24,13 @@ using System.Collections.Generic; using System.Linq; using EventFlow.Aggregates; -using EventFlow.Configuration; using EventFlow.Configuration.EventNamingStrategy; using EventFlow.Core; using EventFlow.EventStores; using EventFlow.TestHelpers; using EventFlow.Tests.UnitTests.Core.VersionedTypes; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.EventStores { @@ -65,10 +64,10 @@ public void GetDefinitions_OnEventWithMultipleDefinitions_ReturnsThemAll() var eventDefinitions = Sut.GetDefinitions(typeof(MultiNamesEvent)); // Assert - eventDefinitions.Should().HaveCount(3); + eventDefinitions.Count.ShouldBe(3); eventDefinitions .Select(d => $"{d.Name}-V{d.Version}") - .Should().BeEquivalentTo(new []{"multi-names-event-V1", "MultiNamesEvent-V1", "MultiNamesEvent-V2"}); + .ShouldBeEquivalentTo(new []{"multi-names-event-V1", "MultiNamesEvent-V1", "MultiNamesEvent-V2"}); } [Test] @@ -82,13 +81,15 @@ public void GetDefinitions_OnEventWithMultipleDefinitionsAndNonDefaultNamingStra var eventDefinitions = Sut.GetDefinitions(typeof(MultiNamesEvent)); // Assert - eventDefinitions.Should().HaveCount(3); + eventDefinitions.Count.ShouldBe(3); eventDefinitions .Select(d => $"{d.Name}-V{d.Version}") - .Should().BeEquivalentTo( + .ShouldBe(new[] + { "EventFlow.Tests.UnitTests.EventStores.MultiNamesEvent-V1", "EventFlow.Tests.UnitTests.EventStores.MultiNamesEvent-V1", - "EventFlow.Tests.UnitTests.EventStores.MultiNamesEvent-V2"); + "EventFlow.Tests.UnitTests.EventStores.MultiNamesEvent-V2" + }, ignoreOrder: true); } [EventVersion("Fancy", 42)] diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventUpgradeManagerTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventUpgradeManagerTests.cs index 8d3a4a939..53a705c22 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventUpgradeManagerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventUpgradeManagerTests.cs @@ -31,9 +31,9 @@ using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.EventStores { @@ -64,7 +64,7 @@ public async Task EmptyListReturnsEmptyList() var upgradedEvents = await Sut.UpgradeAsync(events.ToAsyncEnumerable(), CancellationToken.None).ToArrayAsync(); // Assert - upgradedEvents.Should().BeEmpty(); + upgradedEvents.ShouldBeEmpty(); } [Test] @@ -81,8 +81,8 @@ public async Task EventWithNoUpgradersIsReturned() var upgradedEvents = await Sut.UpgradeAsync(events.ToAsyncEnumerable(), CancellationToken.None).ToArrayAsync(); // Assert - upgradedEvents.Length.Should().Be(2); - upgradedEvents.Should().Contain(events); + upgradedEvents.Length.ShouldBe(2); + upgradedEvents.ShouldBe(events, ignoreOrder: true); } [Test] @@ -101,10 +101,10 @@ public async Task EventsAreUpgradedToLatestVersion() var upgradedEvents = await Sut.UpgradeAsync(events.ToAsyncEnumerable(), CancellationToken.None).ToArrayAsync(); // Assert - upgradedEvents.Length.Should().Be(3); + upgradedEvents.Length.ShouldBe(3); foreach (var upgradedEvent in upgradedEvents) { - upgradedEvent.Should().BeAssignableTo>(); + upgradedEvent.ShouldBeAssignableTo>(); } } diff --git a/Source/EventFlow.Tests/UnitTests/Sagas/AggregateSagas/AggregateSagaTests.cs b/Source/EventFlow.Tests/UnitTests/Sagas/AggregateSagas/AggregateSagaTests.cs index ef2e1aced..296418651 100644 --- a/Source/EventFlow.Tests/UnitTests/Sagas/AggregateSagas/AggregateSagaTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Sagas/AggregateSagas/AggregateSagaTests.cs @@ -30,13 +30,13 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Sagas; -using FluentAssertions; using Moq; using NUnit.Framework; using System; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Shouldly; namespace EventFlow.Tests.UnitTests.Sagas.AggregateSagas { @@ -88,7 +88,7 @@ await Sut.PublishAsync( } // Assert - exception.Should().BeNull(); + exception.ShouldBeNull(); } [Test] @@ -119,7 +119,7 @@ await Sut.PublishAsync( } // Assert - exception.Should().BeNull(); + exception.ShouldBeNull(); } [Test] @@ -154,7 +154,7 @@ await Sut.PublishAsync( } // Assert - exception.Should().BeNull(); + exception.ShouldBeNull(); } [Test] @@ -186,19 +186,19 @@ await Sut.PublishAsync( } // Assert - exception.Should().NotBeNull(); - exception.Should().BeAssignableTo(); + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); var sagaPublishException = exception as SagaPublishException; - sagaPublishException.InnerExceptions.Count.Should().Be(1); + sagaPublishException.InnerExceptions.Count.ShouldBe(1); var innerException = sagaPublishException.InnerExceptions[0]; - innerException.Should().BeAssignableTo(); + innerException.ShouldBeAssignableTo(); var commandException = innerException as CommandException; - commandException.Message.Should().Contain(message); - commandException.Message.Should().Contain(Sut.Id.ToString()); - commandException.CommandType.Should().Be(typeof(ThingyAddMessageCommand)); - commandException.ExecutionResult.Should().NotBeNull(); - commandException.ExecutionResult.IsSuccess.Should().BeFalse(); - commandException.ExecutionResult.ToString().Should().Contain(message); + commandException.Message.ShouldContain(message); + commandException.Message.ShouldContain(Sut.Id.ToString()); + commandException.CommandType.ShouldBe(typeof(ThingyAddMessageCommand)); + commandException.ExecutionResult.ShouldNotBeNull(); + commandException.ExecutionResult.IsSuccess.ShouldBeFalse(); + commandException.ExecutionResult.ToString().ShouldContain(message); } [Test] @@ -234,20 +234,20 @@ await Sut.PublishAsync( } // Assert - exception.Should().NotBeNull(); - exception.Should().BeAssignableTo(); + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); var sagaPublishException = exception as SagaPublishException; - sagaPublishException.InnerExceptions.Count.Should().Be(2); + sagaPublishException.InnerExceptions.Count.ShouldBe(2); foreach (var innerException in sagaPublishException.InnerExceptions) { - innerException.Should().BeAssignableTo(); + innerException.ShouldBeAssignableTo(); var commandException = innerException as CommandException; - commandException.Message.Should().Contain(message); - commandException.Message.Should().Contain(Sut.Id.ToString()); - commandException.CommandType.Should().Be(typeof(ThingyAddMessageCommand)); - commandException.ExecutionResult.Should().NotBeNull(); - commandException.ExecutionResult.IsSuccess.Should().BeFalse(); - commandException.ExecutionResult.ToString().Should().Contain(message); + commandException.Message.ShouldContain(message); + commandException.Message.ShouldContain(Sut.Id.ToString()); + commandException.CommandType.ShouldBe(typeof(ThingyAddMessageCommand)); + commandException.ExecutionResult.ShouldNotBeNull(); + commandException.ExecutionResult.IsSuccess.ShouldBeFalse(); + commandException.ExecutionResult.ToString().ShouldContain(message); } } @@ -280,19 +280,19 @@ await Sut.PublishAsync( } // Assert - exception.Should().NotBeNull(); - exception.Should().BeAssignableTo(); + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); var sagaPublishException = exception as SagaPublishException; - sagaPublishException.InnerExceptions.Count.Should().Be(1); + sagaPublishException.InnerExceptions.Count.ShouldBe(1); var innerException = sagaPublishException.InnerExceptions[0]; - innerException.Should().BeAssignableTo(); + innerException.ShouldBeAssignableTo(); var commandException = innerException as CommandException; - commandException.Message.Should().Contain(message); - commandException.Message.Should().Contain(Sut.Id.ToString()); - commandException.CommandType.Should().Be(typeof(ThingyAddMessageCommand)); - commandException.InnerException.Should().NotBeNull(); - commandException.InnerException.Should().BeAssignableTo(); - commandException.InnerException.Message.Should().Be(message); + commandException.Message.ShouldContain(message); + commandException.Message.ShouldContain(Sut.Id.ToString()); + commandException.CommandType.ShouldBe(typeof(ThingyAddMessageCommand)); + commandException.InnerException.ShouldNotBeNull(); + commandException.InnerException.ShouldBeAssignableTo(); + commandException.InnerException.Message.ShouldBe(message); } [Test] @@ -328,20 +328,20 @@ await Sut.PublishAsync( } // Assert - exception.Should().NotBeNull(); - exception.Should().BeAssignableTo(); + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); var sagaPublishException = exception as SagaPublishException; - sagaPublishException.InnerExceptions.Count.Should().Be(2); + sagaPublishException.InnerExceptions.Count.ShouldBe(2); foreach (var innerException in sagaPublishException.InnerExceptions) { - innerException.Should().BeAssignableTo(); + innerException.ShouldBeAssignableTo(); var commandException = innerException as CommandException; - commandException.Message.Should().Contain(message); - commandException.Message.Should().Contain(Sut.Id.ToString()); - commandException.CommandType.Should().Be(typeof(ThingyAddMessageCommand)); - commandException.InnerException.Should().NotBeNull(); - commandException.InnerException.Should().BeAssignableTo(); - commandException.InnerException.Message.Should().Be(message); + commandException.Message.ShouldContain(message); + commandException.Message.ShouldContain(Sut.Id.ToString()); + commandException.CommandType.ShouldBe(typeof(ThingyAddMessageCommand)); + commandException.InnerException.ShouldNotBeNull(); + commandException.InnerException.ShouldBeAssignableTo(); + commandException.InnerException.Message.ShouldBe(message); } } } diff --git a/Source/EventFlow.Tests/UnitTests/Sagas/DispatchToSagasTests.cs b/Source/EventFlow.Tests/UnitTests/Sagas/DispatchToSagasTests.cs index 6f9b1a470..4204a107c 100644 --- a/Source/EventFlow.Tests/UnitTests/Sagas/DispatchToSagasTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Sagas/DispatchToSagasTests.cs @@ -24,15 +24,14 @@ using System.Threading; using System.Threading.Tasks; using EventFlow.Aggregates; -using EventFlow.Configuration; using EventFlow.Core; using EventFlow.Sagas; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Sagas; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Sagas { @@ -143,7 +142,7 @@ public void UnhandledSagaErrorIsThrown() async () => await Sut.ProcessAsync(domainEvents, CancellationToken.None).ConfigureAwait(false)); // Assert - thrownException.Should().BeSameAs(expectedException); + thrownException.ShouldBeSameAs(expectedException); } private Exception Arrange_Faulty_SagaStore() diff --git a/Source/EventFlow.Tests/UnitTests/Sagas/SagaDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/Sagas/SagaDefinitionServiceTests.cs index 22fc0eadf..365ec39f3 100644 --- a/Source/EventFlow.Tests/UnitTests/Sagas/SagaDefinitionServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Sagas/SagaDefinitionServiceTests.cs @@ -26,8 +26,8 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Sagas; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Sagas { @@ -45,8 +45,8 @@ public void GetSagaTypeDetails_WithSubscribedAggregateEvents(Type aggregateEvent var sagaTypeDetails = Sut.GetSagaDetails(aggregateEventType).ToList(); // Assert - sagaTypeDetails.Should().HaveCount(1); - sagaTypeDetails.Single().SagaType.Should().Be(typeof(ThingySaga)); + sagaTypeDetails.Count.ShouldBe(1); + sagaTypeDetails.Single().SagaType.ShouldBe(typeof(ThingySaga)); } [Test] @@ -58,7 +58,7 @@ public void LoadSagas_IgnoresMultipleLoads() // Assert var sagaDetails = Sut.GetSagaDetails(typeof(ThingySagaStartRequestedEvent)).ToList(); - sagaDetails.Should().HaveCount(1); + sagaDetails.Count.ShouldBe(1); } [TestCase(typeof(ThingyDomainErrorAfterFirstEvent))] @@ -72,7 +72,7 @@ public void GetSagaTypeDetails_WithUnknownAggregateEvents(Type aggregateEventTyp var sagaTypeDetails = Sut.GetSagaDetails(aggregateEventType).ToList(); // Assert - sagaTypeDetails.Should().BeEmpty(); + sagaTypeDetails.ShouldBeEmpty(); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotAggregateRootTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotAggregateRootTests.cs index 7989b8f65..38a39c012 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotAggregateRootTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotAggregateRootTests.cs @@ -35,9 +35,9 @@ using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Snapshots; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots { @@ -84,9 +84,9 @@ public async Task SnapshotIsLoaded(int eventsInStore) await Sut.LoadAsync(_eventStoreMock.Object, _snapshotStore.Object, CancellationToken.None); // Assert - Sut.Version.Should().Be(eventsInStore); - Sut.SnapshotVersion.GetValueOrDefault().Should().Be(ThingyAggregate.SnapshotEveryVersion); - Sut.PreviousSourceIds.Should().NotBeEmpty(); + Sut.Version.ShouldBe(eventsInStore); + Sut.SnapshotVersion.GetValueOrDefault().ShouldBe(ThingyAggregate.SnapshotEveryVersion); + Sut.PreviousSourceIds.ShouldNotBeEmpty(); } [SetUp] @@ -112,7 +112,7 @@ public async Task Test_Arrange_EventStore(int eventInStore, int fromEventSequenc CancellationToken.None); // Assert - domainEvents.Should().HaveCount(expectedNumberOfEvents); + domainEvents.Count.ShouldBe(expectedNumberOfEvents); } [Description("Mock test")] @@ -133,7 +133,7 @@ public async Task Test_Arrange_EventStore_SequenceRange(int eventInStore, int fr CancellationToken.None); // Assert - domainEvents.Should().HaveCount(expectedNumberOfEvents); + domainEvents.Count.ShouldBe(expectedNumberOfEvents); } private void Arrange_EventStore(IEnumerable> domainEvents) diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs index 2efe4b8e7..82f7eafe5 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs @@ -25,9 +25,9 @@ using EventFlow.Snapshots; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Extensions; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots { @@ -52,12 +52,12 @@ public void DeserializesCorrectly() var snapshotMetadata = JsonConvert.DeserializeObject(json); // Assert - snapshotMetadata.AggregateId.Should().Be("thingy-42"); - snapshotMetadata.AggregateName.Should().Be("thingy"); - snapshotMetadata.AggregateSequenceNumber.Should().Be(42); - snapshotMetadata.SnapshotName.Should().Be("thingy"); - snapshotMetadata.SnapshotVersion.Should().Be(84); - snapshotMetadata.PreviousSourceIds.Select(s => s.Value).Should().BeEquivalentTo("cool", "magic"); + snapshotMetadata.AggregateId.ShouldBe("thingy-42"); + snapshotMetadata.AggregateName.ShouldBe("thingy"); + snapshotMetadata.AggregateSequenceNumber.ShouldBe(42); + snapshotMetadata.SnapshotName.ShouldBe("thingy"); + snapshotMetadata.SnapshotVersion.ShouldBe(84); + snapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBeEquivalentTo("cool", "magic"); } [Test] @@ -83,12 +83,12 @@ public void GettersAndSettersWork() var deserializedSnapshotMetadata = JsonConvert.DeserializeObject(json); // Assert - deserializedSnapshotMetadata.AggregateId.Should().Be("thingy-42"); - deserializedSnapshotMetadata.AggregateName.Should().Be("thingy"); - deserializedSnapshotMetadata.AggregateSequenceNumber.Should().Be(42); - deserializedSnapshotMetadata.SnapshotName.Should().Be("thingy"); - deserializedSnapshotMetadata.SnapshotVersion.Should().Be(84); - deserializedSnapshotMetadata.PreviousSourceIds.Select(s => s.Value).Should().BeEquivalentTo("cool", "magic"); + deserializedSnapshotMetadata.AggregateId.ShouldBe("thingy-42"); + deserializedSnapshotMetadata.AggregateName.ShouldBe("thingy"); + deserializedSnapshotMetadata.AggregateSequenceNumber.ShouldBe(42); + deserializedSnapshotMetadata.SnapshotName.ShouldBe("thingy"); + deserializedSnapshotMetadata.SnapshotVersion.ShouldBe(84); + deserializedSnapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBeEquivalentTo("cool", "magic"); } [Test] @@ -101,7 +101,7 @@ public void PreviousSourceIdsIsAllowedToBeEmpty() var previousSourceIds = snapshotMetadata.PreviousSourceIds; // Assert - previousSourceIds.Should().BeEmpty(); + previousSourceIds.ShouldBeEmpty(); } } } \ No newline at end of file diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerializerTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerializerTests.cs index 8cbac5db3..79bb15390 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerializerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerializerTests.cs @@ -29,10 +29,10 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Snapshots; -using FluentAssertions; using Moq; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots { @@ -57,9 +57,9 @@ public async Task SerilizeAsync_ReturnsSomething() .ConfigureAwait(false); // Assert - serializedSnapshot.Should().NotBeNull(); - serializedSnapshot.SerializedData.Should().NotBeNullOrEmpty(); - serializedSnapshot.SerializedMetadata.Should().NotBeNullOrEmpty(); + serializedSnapshot.ShouldNotBeNull(); + serializedSnapshot.SerializedData.ShouldNotBeNullOrEmpty(); + serializedSnapshot.SerializedMetadata.ShouldNotBeNullOrEmpty(); } [Test] @@ -77,12 +77,10 @@ public async Task DeserializeAsync_ReturnsSomething() .ConfigureAwait(false); // Assert - snapshotContainer.Should().NotBeNull(); - snapshotContainer.Metadata.Should().NotBeNull(); - snapshotContainer.Snapshot.Should() - .NotBeNull() - .And - .BeOfType(); + snapshotContainer.ShouldNotBeNull(); + snapshotContainer.Metadata.ShouldNotBeNull(); + snapshotContainer.Snapshot.ShouldNotBeNull(); + snapshotContainer.Snapshot.ShouldBeOfType(); } [SetUp] diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerilizerTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerilizerTests.cs index df4389062..0ca74d964 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerilizerTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotSerilizerTests.cs @@ -29,10 +29,10 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates; using EventFlow.TestHelpers.Aggregates.Snapshots; -using FluentAssertions; using Moq; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots { @@ -58,9 +58,9 @@ public async Task SerilizeAsync_ReturnsSomething() .ConfigureAwait(false); // Assert - serializedSnapshot.Should().NotBeNull(); - serializedSnapshot.SerializedData.Should().NotBeNullOrEmpty(); - serializedSnapshot.SerializedMetadata.Should().NotBeNullOrEmpty(); + serializedSnapshot.ShouldNotBeNull(); + serializedSnapshot.SerializedData.ShouldNotBeNullOrEmpty(); + serializedSnapshot.SerializedMetadata.ShouldNotBeNullOrEmpty(); } [Test] @@ -78,12 +78,10 @@ public async Task DeserializeAsync_ReturnsSomething() .ConfigureAwait(false); // Assert - snapshotContainer.Should().NotBeNull(); - snapshotContainer.Metadata.Should().NotBeNull(); - snapshotContainer.Snapshot.Should() - .NotBeNull() - .And - .BeOfType(); + snapshotContainer.ShouldNotBeNull(); + snapshotContainer.Metadata.ShouldNotBeNull(); + snapshotContainer.Snapshot.ShouldNotBeNull(); + snapshotContainer.Snapshot.ShouldBeOfType(); } [SetUp] diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs index c6fcaf3c0..f13f102dc 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs @@ -30,10 +30,10 @@ using EventFlow.TestHelpers.Aggregates.Snapshots; using EventFlow.TestHelpers.Aggregates.Snapshots.Upgraders; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots { @@ -64,10 +64,10 @@ public async Task UpgradeAsync_UpgradesSnapshot() var snapshot = await Sut.UpgradeAsync(new ThingySnapshotV1(pingIds), CancellationToken.None); // Assert - snapshot.Should().BeOfType(); + snapshot.ShouldBeOfType(); var thingySnapshot = (ThingySnapshot) snapshot; - thingySnapshot.PingsReceived.Should().BeEquivalentTo(pingIds); - thingySnapshot.PreviousVersions.Should().BeEquivalentTo(new[] {ThingySnapshotVersion.Version1, ThingySnapshotVersion.Version2}); + thingySnapshot.PingsReceived.ShouldBeEquivalentTo(pingIds); + thingySnapshot.PreviousVersions.ShouldBeEquivalentTo(new[] {ThingySnapshotVersion.Version1, ThingySnapshotVersion.Version2}); } private void Arrange_All_Upgraders() diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/Strategies/SnapshotEveryFewVersionsStrategyTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/Strategies/SnapshotEveryFewVersionsStrategyTests.cs index 6eefead09..ca32453b8 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/Strategies/SnapshotEveryFewVersionsStrategyTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/Strategies/SnapshotEveryFewVersionsStrategyTests.cs @@ -25,9 +25,9 @@ using EventFlow.Snapshots; using EventFlow.Snapshots.Strategies; using EventFlow.TestHelpers; -using FluentAssertions; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.Tests.UnitTests.Snapshots.Strategies { @@ -47,7 +47,7 @@ public class SnapshotEveryFewVersionsStrategyTests public async Task ShouldCreateSnapshotAsync_ReturnsCorrect(int aggregateRootVersion, int? snapshotVersion, bool expectedShouldCreateSnapshot) { // Assumptions - SnapshotEveryFewVersionsStrategy.DefautSnapshotAfterVersions.Should().Be(100); + SnapshotEveryFewVersionsStrategy.DefautSnapshotAfterVersions.ShouldBe(100); // Arrange var sut = SnapshotEveryFewVersionsStrategy.Default; @@ -62,7 +62,7 @@ public async Task ShouldCreateSnapshotAsync_ReturnsCorrect(int aggregateRootVers .ConfigureAwait(false); // Assert - shouldCreateSnapshot.Should().Be(expectedShouldCreateSnapshot); + shouldCreateSnapshot.ShouldBe(expectedShouldCreateSnapshot); } } } \ No newline at end of file From 0de9b91557b8715ba6dc4bff60d07d4110e8e4ee Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 14:27:49 +0300 Subject: [PATCH 11/56] more tests --- .../MsSql/EfMsSqlReadStoreIncludeTests.cs | 28 +++-- .../Integration/HangfireJobSchedulerTests.cs | 11 +- .../ReadStores/MongoDbReadModelStoreTests.cs | 5 +- .../EventFlowEventStoresMsSqlTests.cs | 8 +- .../IdentityIndexFragmentationTests.cs | 16 +-- .../ReadModels/MultipleMsSqlDatabasesTests.cs | 8 +- .../EventFlowSnapshotStoresMsSqlTests.cs | 6 +- .../EventFlowEventStoresPostgresSqlTests.cs | 8 +- ...EventFlowSnapshotStoresPostgresSqlTests.cs | 6 +- .../Integration/RabbitMqTests.cs | 12 +-- Source/EventFlow.TestHelpers/LoggerMock.cs | 13 ++- .../Suites/TestSuiteForEventStore.cs | 102 +++++++++--------- .../Suites/TestSuiteForReadModelStore.cs | 60 +++++------ .../Suites/TestSuiteForScheduler.cs | 5 +- .../Suites/TestSuiteForSnapshotStore.cs | 41 +++---- Source/EventFlow.Tests/LicenseHeaderTests.cs | 1 - 16 files changed, 173 insertions(+), 157 deletions(-) diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs index a73f2d312..01ffeb90d 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs @@ -31,9 +31,9 @@ using EventFlow.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.EntityFramework.Tests.MsSql { @@ -80,9 +80,9 @@ await CommandBus .ConfigureAwait(false); // Assert - readModel.Should().NotBeNull(); - readModel.Name.Should().Be("Bob"); - readModel.Addresses.Should().BeNullOrEmpty(); + readModel.ShouldNotBeNull(); + readModel.Name.ShouldBe("Bob"); + readModel.Addresses.ShouldBeNull(); } [Test] @@ -114,11 +114,21 @@ await CommandBus .ConfigureAwait(false); // Assert - readModel.Should().NotBeNull(); - readModel.NumberOfAddresses.Should().Be(2); - readModel.Addresses.Should().HaveCount(2); - readModel.Addresses.Should().ContainEquivalentOf(address1); - readModel.Addresses.Should().ContainEquivalentOf(address2); + readModel.ShouldNotBeNull(); + readModel.NumberOfAddresses.ShouldBe(2); + readModel.Addresses.Count.ShouldBe(2); + + readModel.Addresses.ShouldContain(a => + a.Street == address1.Street && + a.PostalCode == address1.PostalCode && + a.City == address1.City && + a.Country == address1.Country); + + readModel.Addresses.ShouldContain(a => + a.Street == address2.Street && + a.PostalCode == address2.PostalCode && + a.City == address2.City && + a.Country == address2.Country); } } } \ No newline at end of file diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs index 277db2180..abbbf60b7 100644 --- a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs @@ -36,13 +36,12 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; -using FluentAssertions.Common; using Hangfire; using Hangfire.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NUnit.Framework; +using Shouldly; namespace EventFlow.Hangfire.Tests.Integration { @@ -113,7 +112,7 @@ public async Task AsynchronousSubscribesGetInvoked() // Assert var receivedPingId = await Task.Run(() => _testAsynchronousSubscriber.PingIds.Take(), cts.Token).ConfigureAwait(false); - receivedPingId.Should().IsSameOrEqualTo(pingId); + receivedPingId.ShouldBeSameAs(pingId); } [Test] @@ -165,10 +164,10 @@ private async Task ValidateScheduleHappens(Func s.Name, s => s); // Assert - sqlScripts.Should().HaveCount(2); - sqlScripts.Should().ContainKey("EventStores.Scripts.0001 - Create table EventFlow.sql"); - sqlScripts.Should().ContainKey("EventStores.Scripts.0002 - Create eventdatamodel_list_type.sql"); + sqlScripts.Count.ShouldBe(2); + sqlScripts.ShouldContainKey("EventStores.Scripts.0001 - Create table EventFlow.sql"); + sqlScripts.ShouldContainKey("EventStores.Scripts.0002 - Create eventdatamodel_list_type.sql"); } } } \ No newline at end of file diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/IdentityIndexFragmentationTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/IdentityIndexFragmentationTests.cs index 28b2d170e..d755e3f87 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/IdentityIndexFragmentationTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/IdentityIndexFragmentationTests.cs @@ -28,8 +28,8 @@ using EventFlow.MsSql.Tests.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.MsSql; -using FluentAssertions; using NUnit.Framework; +using Shouldly; // ReSharper disable StringLiteralTypo @@ -56,7 +56,7 @@ public void VerifyIdentityHasThereLittleFragmentationUsingString() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationString"); - fragmentation.Should().BeLessThan(10); + fragmentation.ShouldBeLessThan(10); } @@ -77,7 +77,7 @@ public void SanityIntLowFragmentationStoredInGuid() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationString"); - fragmentation.Should().BeLessThan(10); + fragmentation.ShouldBeLessThan(10); } [Test] @@ -97,7 +97,7 @@ public void SanityIntAsHexLowFragmentationStoredInGuid() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationString"); - fragmentation.Should().BeLessThan(10); + fragmentation.ShouldBeLessThan(10); } @@ -109,7 +109,7 @@ public void SanityCombYieldsLowFragmentationStoredInGuid() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationGuid"); - fragmentation.Should().BeLessThan(10); + fragmentation.ShouldBeLessThan(10); } [Test] @@ -120,7 +120,7 @@ public void SanityCombYieldsHighFragmentationStoredInString() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationString"); - fragmentation.Should().BeGreaterThan(90); + fragmentation.ShouldBeGreaterThan(90); } [Test] @@ -131,7 +131,7 @@ public void SanityGuidIdentityYieldsHighFragmentationStoredInString() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationString"); - fragmentation.Should().BeGreaterThan(30); // closer to 100 in reality + fragmentation.ShouldBeGreaterThan(30); // closer to 100 in reality } [Test] @@ -142,7 +142,7 @@ public void SanityGuidIdentityYieldsHighFragmentationStoredInGuid() // Assert var fragmentation = GetIndexFragmentation("IndexFragmentationGuid"); - fragmentation.Should().BeGreaterThan(30); // closer to 100 in reality + fragmentation.ShouldBeGreaterThan(30); // closer to 100 in reality } public void InsertRows(Func generator, int count, string table) diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/ReadModels/MultipleMsSqlDatabasesTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/ReadModels/MultipleMsSqlDatabasesTests.cs index b7ac561f5..1f0879522 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/ReadModels/MultipleMsSqlDatabasesTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/ReadStores/ReadModels/MultipleMsSqlDatabasesTests.cs @@ -35,9 +35,9 @@ using EventFlow.TestHelpers; using EventFlow.TestHelpers.Extensions; using EventFlow.TestHelpers.MsSql; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.MsSql.Tests.IntegrationTests.ReadStores.ReadModels { @@ -120,10 +120,10 @@ public async Task MultipleDatabases() var fetchedMagicReadModels = _readModelDatabase.Query( "SELECT * FROM [ReadModel-Magic] WHERE [MagicId] = @Id", new { Id = magicId.Value }); - fetchedMagicReadModels.Should().HaveCount(1); + fetchedMagicReadModels.Count.ShouldBe(1); var fetchedMagicReadModel = fetchedMagicReadModels.Single(); - fetchedMagicReadModel.Message.Should().Be(expectedMessage); - fetchedMagicReadModel.Version.Should().Be(2); + fetchedMagicReadModel.Message.ShouldBe(expectedMessage); + fetchedMagicReadModel.Version.ShouldBe(2); } public class MagicId : Identity diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresMsSqlTests.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresMsSqlTests.cs index a1cd853ae..44bc43224 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresMsSqlTests.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresMsSqlTests.cs @@ -23,8 +23,8 @@ using System.Linq; using EventFlow.MsSql.SnapshotStores; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.MsSql.Tests.IntegrationTests.SnapshotStores { @@ -38,8 +38,8 @@ public void GetSqlScripts() var sqlScripts = EventFlowSnapshotStoresMsSql.GetSqlScripts().ToDictionary(s => s.Name, s => s); // Assert - sqlScripts.Should().HaveCount(1); - sqlScripts.Should().ContainKey("SnapshotStores.Scripts.0001 - Create EventFlowSnapshots.sql"); + sqlScripts.Count.ShouldBe(1); + sqlScripts.ShouldContainKey("SnapshotStores.Scripts.0001 - Create EventFlowSnapshots.sql"); } } } \ No newline at end of file diff --git a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/EventStores/EventFlowEventStoresPostgresSqlTests.cs b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/EventStores/EventFlowEventStoresPostgresSqlTests.cs index 4ebf0e448..50fe07979 100644 --- a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/EventStores/EventFlowEventStoresPostgresSqlTests.cs +++ b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/EventStores/EventFlowEventStoresPostgresSqlTests.cs @@ -23,8 +23,8 @@ using System.Linq; using EventFlow.PostgreSql.EventStores; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.PostgreSql.Tests.IntegrationTests.EventStores { @@ -38,9 +38,9 @@ public void GetSqlScripts() var sqlScripts = EventFlowEventStoresPostgreSql.GetSqlScripts().ToDictionary(s => s.Name, s => s); // Assert - sqlScripts.Should().HaveCount(2); - sqlScripts.Should().ContainKey("EventStores.Scripts.0001 - Create table EventFlow.sql"); - sqlScripts.Should().ContainKey("EventStores.Scripts.0002 - Create eventdatamodel_list_type.sql"); + sqlScripts.Count.ShouldBe(2); + sqlScripts.ShouldContainKey("EventStores.Scripts.0001 - Create table EventFlow.sql"); + sqlScripts.ShouldContainKey("EventStores.Scripts.0002 - Create eventdatamodel_list_type.sql"); } } } \ No newline at end of file diff --git a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresPostgresSqlTests.cs b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresPostgresSqlTests.cs index b6abfaa6f..9ef62d52a 100644 --- a/Source/EventFlow.PostgreSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresPostgresSqlTests.cs +++ b/Source/EventFlow.PostgreSql.Tests/IntegrationTests/SnapshotStores/EventFlowSnapshotStoresPostgresSqlTests.cs @@ -23,8 +23,8 @@ using System.Linq; using EventFlow.PostgreSql.SnapshotStores; using EventFlow.TestHelpers; -using FluentAssertions; using NUnit.Framework; +using Shouldly; namespace EventFlow.PostgreSql.Tests.IntegrationTests.SnapshotStores { @@ -38,8 +38,8 @@ public void GetSqlScripts() var sqlScripts = EventFlowSnapshotStoresPostgreSql.GetSqlScripts().ToDictionary(s => s.Name, s => s); // Assert - sqlScripts.Should().HaveCount(1); - sqlScripts.Should().ContainKey("SnapshotStores.Scripts.0001 - Create EventFlowSnapshots.sql"); + sqlScripts.Count.ShouldBe(1); + sqlScripts.ShouldContainKey("SnapshotStores.Scripts.0001 - Create EventFlowSnapshots.sql"); } } } \ No newline at end of file diff --git a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs index 09759f67f..aef439d1c 100644 --- a/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs +++ b/Source/EventFlow.RabbitMQ.Tests/Integration/RabbitMqTests.cs @@ -36,11 +36,11 @@ using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.Queries; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Shouldly; namespace EventFlow.RabbitMQ.Tests.Integration { @@ -83,14 +83,14 @@ public async Task Scenario() await commandBus.PublishAsync(new ThingyPingCommand(ThingyId.New, pingId), _timeout.Token).ConfigureAwait(false); var rabbitMqMessage = consumer.GetMessages(TimeSpan.FromMinutes(1)).Single(); - rabbitMqMessage.Exchange.Value.Should().Be(exchange.Value); - rabbitMqMessage.RoutingKey.Value.Should().Be("eventflow.domainevent.thingy.thingy-ping.1"); + rabbitMqMessage.Exchange.Value.ShouldBe(exchange.Value); + rabbitMqMessage.RoutingKey.Value.ShouldBe("eventflow.domainevent.thingy.thingy-ping.1"); var pingEvent = (IDomainEvent)eventJsonSerializer.Deserialize( rabbitMqMessage.Message, new Metadata(rabbitMqMessage.Headers)); - pingEvent.AggregateEvent.PingId.Should().Be(pingId); + pingEvent.AggregateEvent.PingId.ShouldBe(pingId); } } @@ -116,8 +116,8 @@ public async Task PublisherPerformance() await Task.WhenAll(tasks).ConfigureAwait(false); var rabbitMqMessages = consumer.GetMessages(TimeSpan.FromMinutes(1), totalMessageCount); - rabbitMqMessages.Should().HaveCount(totalMessageCount); - exceptions.Should().BeEmpty(); + rabbitMqMessages.Count.ShouldBe(totalMessageCount); + exceptions.ShouldBeEmpty(); } } diff --git a/Source/EventFlow.TestHelpers/LoggerMock.cs b/Source/EventFlow.TestHelpers/LoggerMock.cs index bd99c161d..2bbc873a1 100644 --- a/Source/EventFlow.TestHelpers/LoggerMock.cs +++ b/Source/EventFlow.TestHelpers/LoggerMock.cs @@ -25,8 +25,8 @@ using System.Collections.Generic; using System.Linq; using EventFlow.Core; -using FluentAssertions; using Microsoft.Extensions.Logging; +using Shouldly; namespace EventFlow.TestHelpers { @@ -79,7 +79,7 @@ public void VerifyNoProblems() var messages = Logs(LogLevel.Critical, LogLevel.Error) .Select(m => m.Message) .ToList(); - messages.Should().BeEmpty(string.Join(", ", messages)); + messages.ShouldBeEmpty(string.Join(", ", messages)); } public void VerifyProblemLogged(params Exception[] expectedExceptions) @@ -87,7 +87,14 @@ public void VerifyProblemLogged(params Exception[] expectedExceptions) var exceptions = Logs(LogLevel.Error, LogLevel.Critical) .Select(m => m.Exception) .ToList(); - exceptions.Should().AllBeEquivalentTo(expectedExceptions); + + exceptions.Count.ShouldBe(expectedExceptions.Length); + + foreach (var exception in exceptions) + { + exception.ShouldNotBeNull(); + expectedExceptions.ShouldContain(ex => ex.GetType() == exception.GetType() && ex.Message == exception.Message); + } } public IReadOnlyCollection Logs(params LogLevel[] logLevels) diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs index 73e7d43fe..ba271e134 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs @@ -37,10 +37,10 @@ using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; using EventFlow.TestHelpers.Extensions; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Moq; using NUnit.Framework; +using Shouldly; namespace EventFlow.TestHelpers.Suites { @@ -56,8 +56,8 @@ public async Task NewAggregateCanBeLoaded() var testAggregate = await LoadAggregateAsync(ThingyId.New); // Assert - testAggregate.Should().NotBeNull(); - testAggregate.IsNew.Should().BeTrue(); + testAggregate.ShouldNotBeNull(); + testAggregate.IsNew.ShouldBeTrue(); } [Test] @@ -72,16 +72,16 @@ public async Task EventsCanBeStored() var domainEvents = await testAggregate.CommitAsync(EventStore, SnapshotStore, SourceId.New, CancellationToken.None); // Assert - domainEvents.Count.Should().Be(1); + domainEvents.Count.ShouldBe(1); var pingEvent = domainEvents.Single() as IDomainEvent; - pingEvent.Should().NotBeNull(); - pingEvent.AggregateIdentity.Should().Be(id); - pingEvent.AggregateSequenceNumber.Should().Be(1); - pingEvent.AggregateType.Should().Be(typeof(ThingyAggregate)); - pingEvent.EventType.Should().Be(typeof(ThingyPingEvent)); - pingEvent.Timestamp.Should().NotBe(default); - pingEvent.Metadata.Count.Should().BeGreaterThan(0); - pingEvent.Metadata.SourceId.IsNone().Should().BeFalse(); + pingEvent.ShouldNotBeNull(); + pingEvent.AggregateIdentity.ShouldBe(id); + pingEvent.AggregateSequenceNumber.ShouldBe(1); + pingEvent.AggregateType.ShouldBe(typeof(ThingyAggregate)); + pingEvent.EventType.ShouldBe(typeof(ThingyPingEvent)); + pingEvent.Timestamp.ShouldNotBe(default); + pingEvent.Metadata.Count.ShouldBeGreaterThan(0); + pingEvent.Metadata.SourceId.IsNone().ShouldBeFalse(); } [Test] @@ -97,10 +97,10 @@ public async Task AggregatesCanBeLoaded() var loadedTestAggregate = await LoadAggregateAsync(id); // Assert - loadedTestAggregate.Should().NotBeNull(); - loadedTestAggregate.IsNew.Should().BeFalse(); - loadedTestAggregate.Version.Should().Be(1); - loadedTestAggregate.PingsReceived.Count.Should().Be(1); + loadedTestAggregate.ShouldNotBeNull(); + loadedTestAggregate.IsNew.ShouldBeFalse(); + loadedTestAggregate.Version.ShouldBe(1); + loadedTestAggregate.PingsReceived.Count.ShouldBe(1); } [Test] @@ -118,7 +118,7 @@ public async Task EventsCanContainUnicodeCharacters() var loadedTestAggregate = await LoadAggregateAsync(id); // Assert - loadedTestAggregate.Messages.Single().Message.Should().Be("😉"); + loadedTestAggregate.Messages.Single().Message.ShouldBe("😉"); } [Test] @@ -140,8 +140,8 @@ public async Task AggregateEventStreamsAreSeperate() aggregate2 = await LoadAggregateAsync(id2); // Assert - aggregate1.Version.Should().Be(1); - aggregate2.Version.Should().Be(2); + aggregate1.Version.ShouldBe(1); + aggregate2.Version.ShouldBe(2); } [Test] @@ -167,7 +167,7 @@ public async Task DomainEventCanBeLoaded() CancellationToken.None); // Assert - domainEvents.DomainEvents.Count.Should().BeGreaterOrEqualTo(2); + domainEvents.DomainEvents.Count.ShouldBeGreaterThanOrEqualTo(2); } [Test] @@ -181,10 +181,10 @@ public async Task LoadingOfEventsCanStartLater() var domainEvents = await EventStore.LoadEventsAsync(id, 3, CancellationToken.None); // Assert - domainEvents.Should().HaveCount(3); - domainEvents.ElementAt(0).AggregateSequenceNumber.Should().Be(3); - domainEvents.ElementAt(1).AggregateSequenceNumber.Should().Be(4); - domainEvents.ElementAt(2).AggregateSequenceNumber.Should().Be(5); + domainEvents.Count.ShouldBe(3); + domainEvents.ElementAt(0).AggregateSequenceNumber.ShouldBe(3); + domainEvents.ElementAt(1).AggregateSequenceNumber.ShouldBe(4); + domainEvents.ElementAt(2).AggregateSequenceNumber.ShouldBe(5); } [Test] @@ -198,9 +198,9 @@ public async Task LoadingOfEventsCanStartLaterAndStopEarlier() var domainEvents = await EventStore.LoadEventsAsync(id, 3, 4, CancellationToken.None); // Assert - domainEvents.Should().HaveCount(2); - domainEvents.ElementAt(0).AggregateSequenceNumber.Should().Be(3); - domainEvents.ElementAt(1).AggregateSequenceNumber.Should().Be(4); + domainEvents.Count.ShouldBe(2); + domainEvents.ElementAt(0).AggregateSequenceNumber.ShouldBe(3); + domainEvents.ElementAt(1).AggregateSequenceNumber.ShouldBe(4); } [Test] @@ -219,7 +219,7 @@ public async Task AggregateCanHaveMultipleCommits() aggregate = await LoadAggregateAsync(id); // Assert - aggregate.PingsReceived.Count.Should().Be(2); + aggregate.PingsReceived.Count.ShouldBe(2); } [Test] @@ -237,9 +237,9 @@ await CommandBus.PublishAsync( // Assert var aggregate = await LoadAggregateAsync(id); - aggregate.UpgradableEventV1Received.Should().Be(0); - aggregate.UpgradableEventV2Received.Should().Be(0); - aggregate.UpgradableEventV3Received.Should().Be(version1 + version2 + version3); + aggregate.UpgradableEventV1Received.ShouldBe(0); + aggregate.UpgradableEventV2Received.ShouldBe(0); + aggregate.UpgradableEventV3Received.ShouldBe(version1 + version2 + version3); } [Test] @@ -262,8 +262,8 @@ public async Task AggregateEventStreamsCanBeDeleted() // Assert aggregate1 = await LoadAggregateAsync(id1); aggregate2 = await LoadAggregateAsync(id2); - aggregate1.Version.Should().Be(1); - aggregate2.Version.Should().Be(0); + aggregate1.Version.ShouldBe(1); + aggregate2.Version.ShouldBe(0); } [Test] @@ -294,7 +294,7 @@ public async Task NextPositionIsIdOfNextEvent() CancellationToken.None); // Assert - domainEvents.NextGlobalPosition.Value.Should().NotBe(string.Empty); + domainEvents.NextGlobalPosition.Value.ShouldNotBe(string.Empty); } [Test] @@ -317,8 +317,8 @@ public async Task LoadingFirstPageShouldLoadCorrectEvents() CancellationToken.None); // Assert - domainEvents.DomainEvents.OfType>().Should().Contain(e => e.AggregateEvent.PingId == pingIds[0]); - domainEvents.DomainEvents.OfType>().Should().Contain(e => e.AggregateEvent.PingId == pingIds[1]); + domainEvents.DomainEvents.OfType>().ShouldContain(e => e.AggregateEvent.PingId == pingIds[0]); + domainEvents.DomainEvents.OfType>().ShouldContain(e => e.AggregateEvent.PingId == pingIds[1]); } [Test] @@ -353,13 +353,13 @@ public async Task AggregatesCanUpdatedAfterOptimisticConcurrency() // Act aggregate1 = await LoadAggregateAsync(id); - aggregate1.PingsReceived.Single().Should().Be(pingId1); + aggregate1.PingsReceived.Single().ShouldBe(pingId1); aggregate1.Ping(pingId2); await aggregate1.CommitAsync(EventStore, SnapshotStore, SourceId.New, CancellationToken.None); // Assert aggregate1 = await LoadAggregateAsync(id); - aggregate1.PingsReceived.Should().BeEquivalentTo(new[] {pingId1, pingId2}); + aggregate1.PingsReceived.ShouldBeEquivalentTo(new[] {pingId1, pingId2}); } [Test] @@ -388,7 +388,7 @@ await commandBus.PublishAsync( // Assert var aggregate = await LoadAggregateAsync(id); - aggregate.PingsReceived.Should().BeEquivalentTo(new []{pingId1, pingId2}); + aggregate.PingsReceived.ShouldBeEquivalentTo(new []{pingId1, pingId2}); } [Test] @@ -404,8 +404,8 @@ await CommandBus.PublishAsync( ; // Assert - PublishedDomainEvents.Count.Should().Be(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).Should().BeEquivalentTo(Enumerable.Range(1, 10)); + PublishedDomainEvents.Count.ShouldBe(10); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBeEquivalentTo(Enumerable.Range(1, 10)); } [Test] @@ -425,8 +425,8 @@ await CommandBus.PublishAsync( ; // Assert - PublishedDomainEvents.Count.Should().Be(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).Should().BeEquivalentTo(Enumerable.Range(11, 10)); + PublishedDomainEvents.Count.ShouldBe(10); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBeEquivalentTo(Enumerable.Range(11, 10)); } [Test] @@ -447,18 +447,20 @@ public virtual async Task LoadAllEventsAsyncFindsEventsAfterLargeGaps() var idsWithGap = ids.Where(i => !removedIds.Contains(i)); foreach (var id in removedIds) { - await EventPersistence.DeleteEventsAsync(id, CancellationToken.None) - ; + await EventPersistence.DeleteEventsAsync(id, CancellationToken.None); } // Act var result = await EventStore - .LoadAllEventsAsync(GlobalPosition.Start, 5, new EventUpgradeContext(), CancellationToken.None) - ; + .LoadAllEventsAsync(GlobalPosition.Start, 5, new EventUpgradeContext(), CancellationToken.None); // Assert - var domainEventIds = result.DomainEvents.Select(d => d.GetIdentity()); - domainEventIds.Should().Contain(idsWithGap); + var domainEventIds = result.DomainEvents.Select(d => d.GetIdentity()).ToList(); + + foreach (var id in idsWithGap) + { + domainEventIds.ShouldContain(id); + } } [SetUp] @@ -498,7 +500,7 @@ private static async Task ThrowsExceptionAsync(Func action) } } - wasCorrectException.Should().BeTrue("Action was expected to throw exception {0}", typeof(TException).PrettyPrint()); + wasCorrectException.ShouldBeTrue($"Action was expected to throw exception {typeof(TException).PrettyPrint()}"); } } } diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs index 87844eeca..9d0d8c813 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs @@ -36,10 +36,10 @@ using AutoFixture; using EventFlow.Extensions; using EventFlow.TestHelpers.Aggregates.Events; -using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NUnit.Framework; +using Shouldly; using EventId = EventFlow.Aggregates.EventId; namespace EventFlow.TestHelpers.Suites @@ -56,7 +56,7 @@ public async Task NonExistingReadModelReturnsNull() var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id)).ConfigureAwait(false); // Assert - readModel.Should().BeNull(); + readModel.ShouldBeNull(); } [Test] @@ -70,8 +70,8 @@ public async Task ReadModelReceivesEvent() var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id)).ConfigureAwait(false); // Assert - readModel.Should().NotBeNull(); - readModel.PingsReceived.Should().Be(5); + readModel.ShouldNotBeNull(); + readModel.PingsReceived.ShouldBe(5); } [Test] @@ -84,7 +84,7 @@ public async Task InitialReadModelVersionIsNull() var version = await QueryProcessor.ProcessAsync(new ThingyGetVersionQuery(thingyId)).ConfigureAwait(false); // Assert - version.Should().NotHaveValue(); + version.ShouldBeNull(); } [Test] @@ -99,7 +99,7 @@ public async Task ReadModelVersionShouldMatchAggregate() var version = await QueryProcessor.ProcessAsync(new ThingyGetVersionQuery(thingyId)).ConfigureAwait(false); // Assert - version.Should().Be((long)version); + version.ShouldBe((long)version); } [Test] @@ -115,8 +115,8 @@ public async Task CanStoreMultipleMessages() var returnedThingyMessages = await QueryProcessor.ProcessAsync(new ThingyGetMessagesQuery(thingyId)).ConfigureAwait(false); // Assert - returnedThingyMessages.Should().HaveCount(thingyMessages.Count); - returnedThingyMessages.Should().BeEquivalentTo(thingyMessages); + returnedThingyMessages.Count.ShouldBe(thingyMessages.Count); + returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); } [Test] @@ -137,8 +137,8 @@ await CommandBus.PublishAsync(new ThingyImportCommand( var thingy = await QueryProcessor.ProcessAsync(new ThingyGetQuery(thingyId)).ConfigureAwait(false); // Assert - thingy.PingsReceived.Should().Be(pingIds.Count); - returnedThingyMessages.Should().BeEquivalentTo(thingyMessages); + thingy.PingsReceived.ShouldBe(pingIds.Count); + returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); } [Test] @@ -153,7 +153,7 @@ public async Task PurgeRemovesReadModels() var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id)).ConfigureAwait(false); // Assert - readModel.Should().BeNull(); + readModel.ShouldBeNull(); } [Test] @@ -166,8 +166,8 @@ public async Task DeleteRemovesSpecificReadModel() await PublishPingCommandAsync(id2).ConfigureAwait(false); var readModel1 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id1)).ConfigureAwait(false); var readModel2 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel1.Should().NotBeNull(); - readModel2.Should().NotBeNull(); + readModel1.ShouldNotBeNull(); + readModel2.ShouldNotBeNull(); // Act await ReadModelPopulator.DeleteAsync( @@ -179,8 +179,8 @@ await ReadModelPopulator.DeleteAsync( // Assert readModel1 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id1)).ConfigureAwait(false); readModel2 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel1.Should().BeNull(); - readModel2.Should().NotBeNull(); + readModel1.ShouldBeNull(); + readModel2.ShouldNotBeNull(); } [Test] @@ -200,8 +200,8 @@ public async Task RePopulateHandlesManyAggregates() var readModel1 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id1)).ConfigureAwait(false); var readModel2 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel1.PingsReceived.Should().Be(3); - readModel2.PingsReceived.Should().Be(5); + readModel1.PingsReceived.ShouldBe(3); + readModel2.PingsReceived.ShouldBe(5); } [Test] @@ -220,7 +220,7 @@ public async Task RePopulateHandlesDeletedAggregate() // Assert var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel.PingsReceived.Should().Be(5); + readModel.PingsReceived.ShouldBe(5); } [Test] @@ -236,8 +236,8 @@ public async Task PopulateCreatesReadModels() var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id)).ConfigureAwait(false); // Assert - readModel.Should().NotBeNull(); - readModel.PingsReceived.Should().Be(2); + readModel.ShouldNotBeNull(); + readModel.PingsReceived.ShouldBe(2); } [Test] @@ -257,7 +257,7 @@ await PublishPingCommandAsync(id).ConfigureAwait(false) // Assert var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id)).ConfigureAwait(false); - readModel.PingsReceived.Should().Be(pingIds.Count); + readModel.PingsReceived.ShouldBe(pingIds.Count); } } @@ -290,7 +290,7 @@ public virtual async Task OptimisticConcurrencyCheck() // Assert var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id), cts.Token).ConfigureAwait(false); - readModel.PingsReceived.Should().Be(3); + readModel.PingsReceived.ShouldBe(3); } } @@ -304,8 +304,8 @@ public async Task MarkingForDeletionRemovesSpecificReadModel() await PublishPingCommandAsync(id2).ConfigureAwait(false); var readModel1 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id1)).ConfigureAwait(false); var readModel2 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel1.Should().NotBeNull(); - readModel2.Should().NotBeNull(); + readModel1.ShouldNotBeNull(); + readModel2.ShouldNotBeNull(); // Act await CommandBus.PublishAsync(new ThingyDeleteCommand(id1), CancellationToken.None); @@ -313,8 +313,8 @@ public async Task MarkingForDeletionRemovesSpecificReadModel() // Assert readModel1 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id1)).ConfigureAwait(false); readModel2 = await QueryProcessor.ProcessAsync(new ThingyGetQuery(id2)).ConfigureAwait(false); - readModel1.Should().BeNull(); - readModel2.Should().NotBeNull(); + readModel1.ShouldBeNull(); + readModel2.ShouldNotBeNull(); } [Test] @@ -330,8 +330,8 @@ public async Task CanStoreMessageHistory() var returnedThingyMessages = await QueryProcessor.ProcessAsync(new ThingyGetMessagesQuery(thingyId)).ConfigureAwait(false); // Assert - returnedThingyMessages.Should().HaveCount(thingyMessages.Count); - returnedThingyMessages.Should().BeEquivalentTo(thingyMessages); + returnedThingyMessages.Count.ShouldBe(thingyMessages.Count); + returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); } [TestCase(true, true)] @@ -372,9 +372,9 @@ await readStoreManager.UpdateReadStoresAsync( // Assert var returnedThingyMessages = await QueryProcessor.ProcessAsync(new ThingyGetMessagesQuery(thingyId)).ConfigureAwait(false); - returnedThingyMessages.Should().HaveCount(1); + returnedThingyMessages.Count.ShouldBe(1); var readModel = await QueryProcessor.ProcessAsync(new ThingyGetQuery(thingyId)).ConfigureAwait(false); - readModel.PingsReceived.Should().Be(1); + readModel.PingsReceived.ShouldBe(1); } private class WaitState diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs index 1b988ef71..f58998abc 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs @@ -33,10 +33,9 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Events; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; -using FluentAssertions.Common; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +using Shouldly; namespace EventFlow.TestHelpers.Suites { @@ -81,7 +80,7 @@ public async Task AsynchronousSubscribesGetInvoked() // Assert var receivedPingId = await Task.Run(() => _testAsynchronousSubscriber.PingIds.Take(), cts.Token).ConfigureAwait(false); - receivedPingId.Should().IsSameOrEqualTo(pingId); + receivedPingId.ShouldBeSameAs(pingId); } } diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForSnapshotStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForSnapshotStore.cs index 91e1d94fb..195560f04 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForSnapshotStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForSnapshotStore.cs @@ -32,9 +32,9 @@ using EventFlow.TestHelpers.Aggregates.Commands; using EventFlow.TestHelpers.Aggregates.Snapshots; using EventFlow.TestHelpers.Aggregates.ValueObjects; -using FluentAssertions; using Newtonsoft.Json; using NUnit.Framework; +using Shouldly; namespace EventFlow.TestHelpers.Suites { @@ -51,7 +51,7 @@ public async Task GetSnapshotAsync_NoneExistingSnapshotReturnsNull() .ConfigureAwait(false); // Assert - committedSnapshot.Should().BeNull(); + committedSnapshot.ShouldBeNull(); } [Test] @@ -89,7 +89,7 @@ public async Task NoSnapshotsAreCreatedWhenCommittingFewEvents() var thingySnapshot = await LoadSnapshotAsync(thingyId).ConfigureAwait(false); // Assert - thingySnapshot.Should().BeNull(); + thingySnapshot.ShouldBeNull(); } [Test] @@ -104,8 +104,8 @@ public async Task SnapshotIsCreatedWhenCommittingManyEvents() var thingySnapshot = await LoadSnapshotAsync(thingyId).ConfigureAwait(false); // Assert - thingySnapshot.Should().NotBeNull(); - thingySnapshot.PingsReceived.Count.Should().Be(ThingyAggregate.SnapshotEveryVersion); + thingySnapshot.ShouldNotBeNull(); + thingySnapshot.PingsReceived.Count.ShouldBe(ThingyAggregate.SnapshotEveryVersion); } [TestCase(1)] @@ -121,7 +121,7 @@ public async Task DuplicateOperationExceptionIsThrown(int index) // Validate var thingySnapshot = await LoadSnapshotAsync(thingyId).ConfigureAwait(false); - thingySnapshot.PingsReceived.Should().HaveCount(ThingyAggregate.SnapshotEveryVersion); + thingySnapshot.PingsReceived.Count.ShouldBe(ThingyAggregate.SnapshotEveryVersion); // Act var command = new ThingyPingCommand(thingyId, sourceIds[index], PingId.New); @@ -140,8 +140,8 @@ public async Task LoadedAggregateHasCorrectVersionsWhenSnapshotIsApplied() var thingyAggregate = await LoadAggregateAsync(thingyId).ConfigureAwait(false); // Assert - thingyAggregate.Version.Should().Be(pingsSent); - thingyAggregate.SnapshotVersion.GetValueOrDefault().Should().Be(ThingyAggregate.SnapshotEveryVersion); + thingyAggregate.Version.ShouldBe(pingsSent); + thingyAggregate.SnapshotVersion.GetValueOrDefault().ShouldBe(ThingyAggregate.SnapshotEveryVersion); } [Test] @@ -151,9 +151,9 @@ public async Task LoadingNoneExistingSnapshottedAggregateReturnsVersionZeroAndNu var thingyAggregate = await LoadAggregateAsync(A()).ConfigureAwait(false); // Assert - thingyAggregate.Should().NotBeNull(); - thingyAggregate.Version.Should().Be(0); - thingyAggregate.SnapshotVersion.Should().NotHaveValue(); + thingyAggregate.ShouldNotBeNull(); + thingyAggregate.Version.ShouldBe(0); + thingyAggregate.SnapshotVersion.ShouldBeNull(); } [Test] @@ -173,12 +173,12 @@ public async Task OldSnapshotsAreUpgradedToLatestVersionAndHaveCorrectMetadata() .ConfigureAwait(false); // Assert - snapshotContainer.Snapshot.Should().BeOfType(); - snapshotContainer.Metadata.AggregateId.Should().Be(thingyId.Value); - snapshotContainer.Metadata.AggregateName.Should().Be("ThingyAggregate"); - snapshotContainer.Metadata.AggregateSequenceNumber.Should().Be(expectedVersion); - snapshotContainer.Metadata.SnapshotName.Should().Be("thingy"); - snapshotContainer.Metadata.SnapshotVersion.Should().Be(1); + snapshotContainer.Snapshot.ShouldBeOfType(); + snapshotContainer.Metadata.AggregateId.ShouldBe(thingyId.Value); + snapshotContainer.Metadata.AggregateName.ShouldBe("ThingyAggregate"); + snapshotContainer.Metadata.AggregateSequenceNumber.ShouldBe(expectedVersion); + snapshotContainer.Metadata.SnapshotName.ShouldBe("thingy"); + snapshotContainer.Metadata.SnapshotVersion.ShouldBe(1); } [Test] @@ -221,9 +221,10 @@ public async Task OldSnapshotsAreUpgradedToLatestVersionAndAppliedToAggregate() var thingyAggregate = await LoadAggregateAsync(thingyId).ConfigureAwait(false); // Assert - thingyAggregate.Version.Should().Be(expectedVersion); - thingyAggregate.PingsReceived.Should().BeEquivalentTo(pingIds); - thingyAggregate.SnapshotVersions.Should().Contain(new[] {ThingySnapshotVersion.Version1, ThingySnapshotVersion.Version2}); + thingyAggregate.Version.ShouldBe(expectedVersion); + thingyAggregate.PingsReceived.ShouldBe(pingIds, ignoreOrder: true); + thingyAggregate.SnapshotVersions.ShouldContain(ThingySnapshotVersion.Version1); + thingyAggregate.SnapshotVersions.ShouldContain(ThingySnapshotVersion.Version2); } protected override IEventFlowOptions Options(IEventFlowOptions eventFlowOptions) diff --git a/Source/EventFlow.Tests/LicenseHeaderTests.cs b/Source/EventFlow.Tests/LicenseHeaderTests.cs index b37536771..b1dc8a7ad 100644 --- a/Source/EventFlow.Tests/LicenseHeaderTests.cs +++ b/Source/EventFlow.Tests/LicenseHeaderTests.cs @@ -30,7 +30,6 @@ using NUnit.Framework; using Shouldly; -// ReSharper disable StringLiteralTypo // ReSharper disable StringLiteralTypo namespace EventFlow.Tests From 84556b246888fac552585b884790c5f8ec3b062e Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 14:51:04 +0300 Subject: [PATCH 12/56] Fixed EventFlow.Tests --- Source/EventFlow.TestHelpers/LoggerMock.cs | 10 ++-------- .../Suites/TestSuiteForEventStore.cs | 8 ++++---- .../Suites/TestSuiteForScheduler.cs | 2 +- .../UnitTests/Core/CircularBufferTests.cs | 6 +++++- .../VersionedTypeDefinitionServiceTestSuite.cs | 10 ++++++++-- .../EventStores/EventDefinitionServiceTests.cs | 16 ++++++++++++++-- .../UnitTests/Snapshots/SnapshotMetadataTests.cs | 4 ++-- .../Snapshots/SnapshotUpgradeServiceTests.cs | 4 ++-- 8 files changed, 38 insertions(+), 22 deletions(-) diff --git a/Source/EventFlow.TestHelpers/LoggerMock.cs b/Source/EventFlow.TestHelpers/LoggerMock.cs index 2bbc873a1..0f66b3c5d 100644 --- a/Source/EventFlow.TestHelpers/LoggerMock.cs +++ b/Source/EventFlow.TestHelpers/LoggerMock.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.Linq; using EventFlow.Core; +using FluentAssertions; using Microsoft.Extensions.Logging; using Shouldly; @@ -87,14 +88,7 @@ public void VerifyProblemLogged(params Exception[] expectedExceptions) var exceptions = Logs(LogLevel.Error, LogLevel.Critical) .Select(m => m.Exception) .ToList(); - - exceptions.Count.ShouldBe(expectedExceptions.Length); - - foreach (var exception in exceptions) - { - exception.ShouldNotBeNull(); - expectedExceptions.ShouldContain(ex => ex.GetType() == exception.GetType() && ex.Message == exception.Message); - } + exceptions.Should().AllBeEquivalentTo(expectedExceptions); } public IReadOnlyCollection Logs(params LogLevel[] logLevels) diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs index ba271e134..037449d2e 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs @@ -359,7 +359,7 @@ public async Task AggregatesCanUpdatedAfterOptimisticConcurrency() // Assert aggregate1 = await LoadAggregateAsync(id); - aggregate1.PingsReceived.ShouldBeEquivalentTo(new[] {pingId1, pingId2}); + aggregate1.PingsReceived.ShouldBe(new[] { pingId1, pingId2 }); } [Test] @@ -388,7 +388,7 @@ await commandBus.PublishAsync( // Assert var aggregate = await LoadAggregateAsync(id); - aggregate.PingsReceived.ShouldBeEquivalentTo(new []{pingId1, pingId2}); + aggregate.PingsReceived.ShouldBe(new[] { pingId1, pingId2 }); } [Test] @@ -405,7 +405,7 @@ await CommandBus.PublishAsync( // Assert PublishedDomainEvents.Count.ShouldBe(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBeEquivalentTo(Enumerable.Range(1, 10)); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBe(Enumerable.Range(1, 10)); } [Test] @@ -426,7 +426,7 @@ await CommandBus.PublishAsync( // Assert PublishedDomainEvents.Count.ShouldBe(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBeEquivalentTo(Enumerable.Range(11, 10)); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBe(Enumerable.Range(11, 10)); } [Test] diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs index f58998abc..55dee81df 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForScheduler.cs @@ -80,7 +80,7 @@ public async Task AsynchronousSubscribesGetInvoked() // Assert var receivedPingId = await Task.Run(() => _testAsynchronousSubscriber.PingIds.Take(), cts.Token).ConfigureAwait(false); - receivedPingId.ShouldBeSameAs(pingId); + receivedPingId.ShouldBe(pingId); } } diff --git a/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs b/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs index 8d52f5a5a..6527d1384 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/CircularBufferTests.cs @@ -50,7 +50,11 @@ public void Put(params int[] numbers) // Assert var shouldContain = numbers.Reverse().Take(capacity).ToList(); - sut.ShouldBe(shouldContain); + + foreach (var sc in shouldContain) + { + sut.ShouldContain(sc); + } } [Test] diff --git a/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs b/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs index 884d5291e..752f7ff66 100644 --- a/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs +++ b/Source/EventFlow.Tests/UnitTests/Core/VersionedTypes/VersionedTypeDefinitionServiceTestSuite.cs @@ -200,12 +200,18 @@ public void GetAllDefinitions_WhenAllLoaded_ReturnsAll() var expectedTypes = Arrange_LoadAllTestTypes(); // Act - var result = Sut.GetAllDefinitions().Select(d => d.Type) + var result = Sut.GetAllDefinitions() + .Select(d => d.Type) .Distinct() + .OrderBy(t => t.FullName) .ToList(); // Assert - result.ShouldBeEquivalentTo(expectedTypes); + var expected = expectedTypes + .OrderBy(t => t.FullName) + .ToList(); + + result.ShouldBe(expected); } [Test] diff --git a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs index ecfc1c5a8..a8e8b0e2d 100644 --- a/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/EventStores/EventDefinitionServiceTests.cs @@ -65,9 +65,21 @@ public void GetDefinitions_OnEventWithMultipleDefinitions_ReturnsThemAll() // Assert eventDefinitions.Count.ShouldBe(3); - eventDefinitions + + var actualStrings = eventDefinitions .Select(d => $"{d.Name}-V{d.Version}") - .ShouldBeEquivalentTo(new []{"multi-names-event-V1", "MultiNamesEvent-V1", "MultiNamesEvent-V2"}); + .OrderBy(s => s) + .ToList(); + + var expectedStrings = new[] + { + "multi-names-event-V1", + "MultiNamesEvent-V1", + "MultiNamesEvent-V2" + }.OrderBy(s => s) + .ToList(); + + actualStrings.ShouldBe(expectedStrings); } [Test] diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs index 82f7eafe5..55f652c00 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotMetadataTests.cs @@ -57,7 +57,7 @@ public void DeserializesCorrectly() snapshotMetadata.AggregateSequenceNumber.ShouldBe(42); snapshotMetadata.SnapshotName.ShouldBe("thingy"); snapshotMetadata.SnapshotVersion.ShouldBe(84); - snapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBeEquivalentTo("cool", "magic"); + snapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBe(new [] { "cool", "magic" }); } [Test] @@ -88,7 +88,7 @@ public void GettersAndSettersWork() deserializedSnapshotMetadata.AggregateSequenceNumber.ShouldBe(42); deserializedSnapshotMetadata.SnapshotName.ShouldBe("thingy"); deserializedSnapshotMetadata.SnapshotVersion.ShouldBe(84); - deserializedSnapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBeEquivalentTo("cool", "magic"); + deserializedSnapshotMetadata.PreviousSourceIds.Select(s => s.Value).ShouldBe(new [] { "cool", "magic" }); } [Test] diff --git a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs index f13f102dc..713100eee 100644 --- a/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs +++ b/Source/EventFlow.Tests/UnitTests/Snapshots/SnapshotUpgradeServiceTests.cs @@ -66,8 +66,8 @@ public async Task UpgradeAsync_UpgradesSnapshot() // Assert snapshot.ShouldBeOfType(); var thingySnapshot = (ThingySnapshot) snapshot; - thingySnapshot.PingsReceived.ShouldBeEquivalentTo(pingIds); - thingySnapshot.PreviousVersions.ShouldBeEquivalentTo(new[] {ThingySnapshotVersion.Version1, ThingySnapshotVersion.Version2}); + thingySnapshot.PingsReceived.ShouldBe(pingIds, ignoreOrder: true); + thingySnapshot.PreviousVersions.ShouldBe(new[] { ThingySnapshotVersion.Version1, ThingySnapshotVersion.Version2 }, ignoreOrder: true); } private void Arrange_All_Upgraders() From 247ff28a6804038eb1b771ae1199bf79eda631e4 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 15:00:39 +0300 Subject: [PATCH 13/56] Fixed Extensions Tests --- .../Integration/HangfireJobSchedulerTests.cs | 2 +- .../Suites/TestSuiteForEventStore.cs | 8 ++++---- .../Suites/TestSuiteForReadModelStore.cs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs index abbbf60b7..3f3a1ee45 100644 --- a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs @@ -112,7 +112,7 @@ public async Task AsynchronousSubscribesGetInvoked() // Assert var receivedPingId = await Task.Run(() => _testAsynchronousSubscriber.PingIds.Take(), cts.Token).ConfigureAwait(false); - receivedPingId.ShouldBeSameAs(pingId); + receivedPingId.ShouldBe(pingId); } [Test] diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs index 037449d2e..db04442de 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForEventStore.cs @@ -359,7 +359,7 @@ public async Task AggregatesCanUpdatedAfterOptimisticConcurrency() // Assert aggregate1 = await LoadAggregateAsync(id); - aggregate1.PingsReceived.ShouldBe(new[] { pingId1, pingId2 }); + aggregate1.PingsReceived.SequenceEqual(new[] { pingId1, pingId2 }).ShouldBeTrue(); } [Test] @@ -388,7 +388,7 @@ await commandBus.PublishAsync( // Assert var aggregate = await LoadAggregateAsync(id); - aggregate.PingsReceived.ShouldBe(new[] { pingId1, pingId2 }); + aggregate.PingsReceived.SequenceEqual(new[] { pingId1, pingId2 }).ShouldBeTrue(); } [Test] @@ -405,7 +405,7 @@ await CommandBus.PublishAsync( // Assert PublishedDomainEvents.Count.ShouldBe(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBe(Enumerable.Range(1, 10)); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).SequenceEqual(Enumerable.Range(1, 10)).ShouldBeTrue(); } [Test] @@ -426,7 +426,7 @@ await CommandBus.PublishAsync( // Assert PublishedDomainEvents.Count.ShouldBe(10); - PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).ShouldBe(Enumerable.Range(11, 10)); + PublishedDomainEvents.Select(d => d.AggregateSequenceNumber).SequenceEqual(Enumerable.Range(11, 10)).ShouldBeTrue(); } [Test] diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs index 9d0d8c813..6f0fe40a7 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs @@ -116,7 +116,7 @@ public async Task CanStoreMultipleMessages() // Assert returnedThingyMessages.Count.ShouldBe(thingyMessages.Count); - returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); + returnedThingyMessages.ShouldBe(thingyMessages, ignoreOrder: true); } [Test] @@ -138,7 +138,7 @@ await CommandBus.PublishAsync(new ThingyImportCommand( // Assert thingy.PingsReceived.ShouldBe(pingIds.Count); - returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); + returnedThingyMessages.SequenceEqual(thingyMessages).ShouldBeTrue(); } [Test] @@ -331,7 +331,7 @@ public async Task CanStoreMessageHistory() // Assert returnedThingyMessages.Count.ShouldBe(thingyMessages.Count); - returnedThingyMessages.ShouldBeEquivalentTo(thingyMessages); + returnedThingyMessages.SequenceEqual(thingyMessages).ShouldBeTrue(); } [TestCase(true, true)] From 9247ce084b1cccf5de4f8efb6863f4ff373f7875 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 15:42:13 +0300 Subject: [PATCH 14/56] Fix 2 --- .../MsSql/EfMsSqlReadStoreIncludeTests.cs | 2 +- .../Suites/TestSuiteForReadModelStore.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs index 01ffeb90d..9f7062eeb 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs @@ -82,7 +82,7 @@ await CommandBus // Assert readModel.ShouldNotBeNull(); readModel.Name.ShouldBe("Bob"); - readModel.Addresses.ShouldBeNull(); + readModel.Addresses.ShouldBeEmpty(); } [Test] diff --git a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs index 6f0fe40a7..e7fee737a 100644 --- a/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs +++ b/Source/EventFlow.TestHelpers/Suites/TestSuiteForReadModelStore.cs @@ -138,7 +138,7 @@ await CommandBus.PublishAsync(new ThingyImportCommand( // Assert thingy.PingsReceived.ShouldBe(pingIds.Count); - returnedThingyMessages.SequenceEqual(thingyMessages).ShouldBeTrue(); + returnedThingyMessages.ShouldBe(thingyMessages, ignoreOrder: true); } [Test] @@ -331,7 +331,7 @@ public async Task CanStoreMessageHistory() // Assert returnedThingyMessages.Count.ShouldBe(thingyMessages.Count); - returnedThingyMessages.SequenceEqual(thingyMessages).ShouldBeTrue(); + returnedThingyMessages.ShouldBe(thingyMessages, ignoreOrder: true); } [TestCase(true, true)] From 7eaad77000342783b0e347b82710acb3cc2d6985 Mon Sep 17 00:00:00 2001 From: Focus Date: Sat, 14 Jun 2025 16:17:01 +0300 Subject: [PATCH 15/56] Remove FluentAssertions, replace in LoggerMock, Examples --- .../DateTimeExtensions.cs | 12 ++++++++++++ .../IntegrationTests/Scenarios.cs | 1 - ...ransportLegsAreConnectedSpecificationTests.cs | 16 ++++++++-------- .../Routing/RoutingServiceTests.cs | 5 ++--- .../EventFlow.Examples.Shipping.Tests/Voyages.cs | 1 - .../EventFlow.TestHelpers.csproj | 1 - Source/EventFlow.TestHelpers/LoggerMock.cs | 7 +++++-- 7 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs diff --git a/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs b/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs new file mode 100644 index 000000000..d15d8fdf9 --- /dev/null +++ b/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace EventFlow.Examples.Shipping.Tests; + +public static class DateTimeExtensions +{ + public static DateTime October(this int day, int year) => new(year, 10, day); + public static DateTime November(this int day, int year) => new(year, 11, day); + public static DateTime January(this int day, int year) => new(year, 1, day); + + public static DateTime At(this DateTime date, int hours, int minutes) => new(date.Year, date.Month, date.Day, hours, minutes, 0); +} \ No newline at end of file diff --git a/Source/EventFlow.Examples.Shipping.Tests/IntegrationTests/Scenarios.cs b/Source/EventFlow.Examples.Shipping.Tests/IntegrationTests/Scenarios.cs index a0c6eaef5..b363c18f1 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/IntegrationTests/Scenarios.cs +++ b/Source/EventFlow.Examples.Shipping.Tests/IntegrationTests/Scenarios.cs @@ -33,7 +33,6 @@ using EventFlow.Examples.Shipping.Domain.Model.VoyageModel.Commands; using EventFlow.Examples.Shipping.Queries.InMemory; using EventFlow.TestHelpers; -using FluentAssertions.Extensions; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; diff --git a/Source/EventFlow.Examples.Shipping.Tests/UnitTests/Domain/Model/CargoModel/Speficications/TransportLegsAreConnectedSpecificationTests.cs b/Source/EventFlow.Examples.Shipping.Tests/UnitTests/Domain/Model/CargoModel/Speficications/TransportLegsAreConnectedSpecificationTests.cs index 28e1b4fcf..b6f6d3b06 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/UnitTests/Domain/Model/CargoModel/Speficications/TransportLegsAreConnectedSpecificationTests.cs +++ b/Source/EventFlow.Examples.Shipping.Tests/UnitTests/Domain/Model/CargoModel/Speficications/TransportLegsAreConnectedSpecificationTests.cs @@ -20,14 +20,14 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System.Linq; using EventFlow.Examples.Shipping.Domain.Model.CargoModel.Entities; using EventFlow.Examples.Shipping.Domain.Model.CargoModel.Specifications; using EventFlow.Examples.Shipping.Domain.Model.VoyageModel; using EventFlow.Examples.Shipping.Domain.Model.VoyageModel.Entities; using EventFlow.TestHelpers; -using FluentAssertions; -using FluentAssertions.Extensions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Examples.Shipping.Tests.UnitTests.Domain.Model.CargoModel.Speficications { @@ -50,8 +50,8 @@ public void Valid() var why = sut.WhyIsNotSatisfiedBy(transportLegs); // Assert - isSatisfiedBy.Should().BeTrue(); - why.Should().HaveCount(0); + isSatisfiedBy.ShouldBeTrue(); + why.Count().ShouldBe(0); } [Test] @@ -70,8 +70,8 @@ public void UnloadIsAfterLoad() var why = sut.WhyIsNotSatisfiedBy(transportLegs); // Assert - isSatisfiedBy.Should().BeFalse(); - why.Should().HaveCount(1); + isSatisfiedBy.ShouldBeFalse(); + why.Count().ShouldBe(1); } [Test] @@ -90,8 +90,8 @@ public void UnloadAndLoadLocationsAreDifferent() var why = sut.WhyIsNotSatisfiedBy(transportLegs); // Assert - isSatisfiedBy.Should().BeFalse(); - why.Should().HaveCount(1); + isSatisfiedBy.ShouldBeFalse(); + why.Count().ShouldBe(1); } } } \ No newline at end of file diff --git a/Source/EventFlow.Examples.Shipping.Tests/UnitTests/ExternalServices/Routing/RoutingServiceTests.cs b/Source/EventFlow.Examples.Shipping.Tests/UnitTests/ExternalServices/Routing/RoutingServiceTests.cs index df879ec46..1abf4ca8c 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/UnitTests/ExternalServices/Routing/RoutingServiceTests.cs +++ b/Source/EventFlow.Examples.Shipping.Tests/UnitTests/ExternalServices/Routing/RoutingServiceTests.cs @@ -24,9 +24,8 @@ using EventFlow.Examples.Shipping.Domain.Model.VoyageModel; using EventFlow.Examples.Shipping.ExternalServices.Routing; using EventFlow.TestHelpers; -using FluentAssertions; -using FluentAssertions.Extensions; using NUnit.Framework; +using Shouldly; namespace EventFlow.Examples.Shipping.Tests.UnitTests.ExternalServices.Routing { @@ -51,7 +50,7 @@ public void Itinerary() // Assert // TODO: Assert list of legs - itineraries.Should().HaveCount(1); + itineraries.Count.ShouldBe(1); } } } \ No newline at end of file diff --git a/Source/EventFlow.Examples.Shipping.Tests/Voyages.cs b/Source/EventFlow.Examples.Shipping.Tests/Voyages.cs index 8055bed0e..10b04e802 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/Voyages.cs +++ b/Source/EventFlow.Examples.Shipping.Tests/Voyages.cs @@ -23,7 +23,6 @@ using System.Collections.Generic; using EventFlow.Examples.Shipping.Domain.Model.VoyageModel; using EventFlow.Examples.Shipping.Domain.Model.VoyageModel.ValueObjects; -using FluentAssertions.Extensions; namespace EventFlow.Examples.Shipping.Tests { diff --git a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj index d730be3d8..6bcd904fb 100644 --- a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj +++ b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj @@ -18,7 +18,6 @@ - diff --git a/Source/EventFlow.TestHelpers/LoggerMock.cs b/Source/EventFlow.TestHelpers/LoggerMock.cs index 0f66b3c5d..9741e5997 100644 --- a/Source/EventFlow.TestHelpers/LoggerMock.cs +++ b/Source/EventFlow.TestHelpers/LoggerMock.cs @@ -25,7 +25,6 @@ using System.Collections.Generic; using System.Linq; using EventFlow.Core; -using FluentAssertions; using Microsoft.Extensions.Logging; using Shouldly; @@ -88,7 +87,11 @@ public void VerifyProblemLogged(params Exception[] expectedExceptions) var exceptions = Logs(LogLevel.Error, LogLevel.Critical) .Select(m => m.Exception) .ToList(); - exceptions.Should().AllBeEquivalentTo(expectedExceptions); + + foreach (var exception in exceptions) + { + exception.ShouldBeOneOf(expectedExceptions); + } } public IReadOnlyCollection Logs(params LogLevel[] logLevels) From 4f1d58e59716621f7e3136489417acdfbd90d48a Mon Sep 17 00:00:00 2001 From: James Thompson Date: Thu, 19 Jun 2025 19:41:42 +1000 Subject: [PATCH 16/56] Cleanup dependendencies #1107 Signed-off-by: James Thompson --- .../EventFlow.EntityFramework.csproj | 4 +--- Source/EventFlow.Sql/EventFlow.Sql.csproj | 3 +++ Source/EventFlow/EventFlow.csproj | 6 ++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj index bce4b23e7..55788830a 100644 --- a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj +++ b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj @@ -20,9 +20,7 @@ - - 8.0.11 - + diff --git a/Source/EventFlow.Sql/EventFlow.Sql.csproj b/Source/EventFlow.Sql/EventFlow.Sql.csproj index 1f6164a38..1ff1c8cb9 100644 --- a/Source/EventFlow.Sql/EventFlow.Sql.csproj +++ b/Source/EventFlow.Sql/EventFlow.Sql.csproj @@ -15,6 +15,9 @@ + + + diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index c21c9733d..239c2e66a 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -10,16 +10,14 @@ - - - All - + + From 19af51f7433c8f6ed5f25de8f4912fda296a4509 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Thu, 19 Jun 2025 19:20:58 +1000 Subject: [PATCH 17/56] #1106 use asp net framework reference Signed-off-by: James Thompson --- .../EventFlow.AspNetCore.csproj | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj b/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj index cd610eeb6..d9f73b679 100644 --- a/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj +++ b/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj @@ -9,15 +9,19 @@ - - - - + + + + + + + + @@ -29,5 +33,5 @@ - + From fa401fa9873f2f09c1b5eb717e7e33b6a863485f Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 10:36:08 +0200 Subject: [PATCH 18/56] Ping --- Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj b/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj index d9f73b679..8bd236e7f 100644 --- a/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj +++ b/Source/EventFlow.AspNetCore/EventFlow.AspNetCore.csproj @@ -35,3 +35,4 @@ + From 02ad88f5ef31fa9e28f4bb314ba9d443233556fe Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 10:41:58 +0200 Subject: [PATCH 19/56] Ping --- .../MsSql/EfMsSqlReadStoreIncludeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs index 9f7062eeb..9003fc415 100644 --- a/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs +++ b/Source/EventFlow.EntityFramework.Tests/MsSql/EfMsSqlReadStoreIncludeTests.cs @@ -131,4 +131,4 @@ await CommandBus a.Country == address2.Country); } } -} \ No newline at end of file +} From 648f7d24c6d3895165036d591036716a92c9ec49 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 10:44:24 +0200 Subject: [PATCH 20/56] GPT-5-Codex generated instructions --- .github/copilot-instructions.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..592fea3fe --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +# Copilot Instructions for EventFlow + +## Architecture snapshot +- EventFlow is a CQRS+ES framework; the core runtime lives in `Source/EventFlow` and exposes aggregates, commands, queries, read stores, sagas, jobs, and snapshots. +- Command flow: clients call `CommandBus` (`Source/EventFlow/CommandBus.cs`) which resolves handlers, invokes aggregates deriving from `AggregateRoot`, and emits events that pipe through subscribers and read-store dispatchers. +- Aggregates load and persist via `IAggregateStore`/`IEventStore`; defaults use the in-memory persistence registered in `EventFlowOptions`, while integration packages under `Source/EventFlow.*` swap in specific stores. +- Read models implement `IReadModel` plus `IAmReadModelFor<...>`; dispatch logic sits in `ReadStores` and uses metadata to map events to view updates. +- Sagas and jobs live under `Source/EventFlow/Sagas` and `Source/EventFlow/Jobs`, coordinating cross-aggregate workflows and deferred execution. +- Documentation that explains the concepts is checked in under `Documentation/`; updates should travel with code changes. + +## Extension & configuration guide +- Dependency injection starts with `services.AddEventFlow(o => { ... })` (`Source/EventFlow/Extensions/ServiceCollectionExtensions.cs`); chain option methods to register events, commands, read models, snapshots, sagas, and custom services. +- Use the fluent helpers in `EventFlowOptions` (`Source/EventFlow/EventFlowOptions.cs`) such as `.AddEvents`, `.AddCommands`, `.UseInMemoryReadStoreFor()`, `.ConfigureOptimisticConcurrencyRetry(...)`, or `.UseEventPersistence()` to pivot storage/backends. +- Strongly typed IDs must derive from `Identity` (`Source/EventFlow/Core/Identity.cs`); create new IDs via `ExampleId.New` or `Identity.With(Guid)` to honor prefix validation. +- When adding domain objects, follow the naming pattern `ThingyAggregate` + `ThingyId` + `ThingyEvent`; see `EventFlow.TestHelpers/Aggregates/Thingy*` for canonical examples including event emit/apply patterns. +- Integration modules (MongoDB, MsSql, PostgreSql, Redis, SQLite, etc.) expose option extensions in their `Extensions/` folder; replicate those patterns when introducing new infrastructure. +- Prefer using `EventFlow.TestHelpers` base classes and fixtures when authoring tests so categories, logging, and deterministic IDs behave consistently. + +## Build, test, and verification +- The solution is organized under `EventFlow.sln`; build with `dotnet build EventFlow.sln` (warnings are treated as errors via `Source/Directory.Build.props`). +- Unit tests target `netcoreapp3.1`, `net6.0`, and `net8.0`; run fast feedback with `dotnet test EventFlow.sln --filter "Category!=integration"` and rely on `EventFlow.TestHelpers.Categories` constants when tagging new suites. +- Integration tests span external services (MongoDB, PostgreSQL, SQL Server, RabbitMQ, Elasticsearch, EventStore); start containers with `docker-compose up` before executing the corresponding `*.Tests` projects or include the `integration` category filter. +- Source generators and analyzers live in `Source/EventFlow.SourceGenerators` and `Source/EventFlow.CodeStyle`; ensure the .NET SDK version supports C# 12 and keep analyzer warnings clean. +- Documentation builds use MkDocs (`requirements.txt`); run `pip install -r requirements.txt` followed by `mkdocs serve` when verifying doc updates. + +## Coding conventions & review tips +- Favor async APIs and accept `CancellationToken` parameters throughout—the core dispatchers expect cooperative cancellation (see `CommandBus.PublishAsync` and `AggregateStore` methods). +- New events should inherit `AggregateEvent`, carry immutable data, and rely on aggregate `Apply` methods to mutate state; never mutate state directly inside command handlers. +- Subscribers and read stores should request dependencies via constructor injection and avoid static singletons; look at `Source/EventFlow/Subscribers` for the expected interface contracts. +- When wiring new persistence, register required DI services before calling `.UseEventPersistence()` to avoid the `RemoveAll()` guard removing your registration. +- Keep public APIs binary compatible where possible; breaking changes require updates in `Documentation/` and `RELEASE_NOTES.md`. +- Mirror existing namespace layout (`EventFlow.{Feature}`) and group files into folders matching their conceptual role to keep source generators and discovery heuristics effective. From 21869d5107c3fed88121a3c1d545d36cffd5796c Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 11:05:44 +0200 Subject: [PATCH 21/56] Started on release notes --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 17e72dd26..2e44624f1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ ### New in 1.2.2 (working version, not released yet) -* *Nothing yet...* +* Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) ### New in 1.2.1 (released 2025-05-29) From 0d1a2a5c04668928d4bc0a2e5b926b39291b447c Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 11:07:18 +0200 Subject: [PATCH 22/56] Add missing header --- .../DateTimeExtensions.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs b/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs index d15d8fdf9..ff294da7d 100644 --- a/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs +++ b/Source/EventFlow.Examples.Shipping.Tests/DateTimeExtensions.cs @@ -1,4 +1,26 @@ -using System; +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; namespace EventFlow.Examples.Shipping.Tests; @@ -9,4 +31,4 @@ public static class DateTimeExtensions public static DateTime January(this int day, int year) => new(year, 1, day); public static DateTime At(this DateTime date, int hours, int minutes) => new(date.Year, date.Month, date.Day, hours, minutes, 0); -} \ No newline at end of file +} From 1e98687a5ae27c5f5cd44fa94ed75798d7051c4e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 11:19:15 +0200 Subject: [PATCH 23/56] MCP and additonal instructions --- .github/copilot-instructions.md | 7 +++++++ .gitignore | 4 ---- .vscode/mcp.json | 9 +++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .vscode/mcp.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 592fea3fe..b2eb09474 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,5 +1,7 @@ # Copilot Instructions for EventFlow +These guidelines govern contributions within the EventFlow code base hosted at https://github.com/eventflow/EventFlow/. Follow them whenever collaborating in this repository to stay aligned with the project’s expectations. + ## Architecture snapshot - EventFlow is a CQRS+ES framework; the core runtime lives in `Source/EventFlow` and exposes aggregates, commands, queries, read stores, sagas, jobs, and snapshots. - Command flow: clients call `CommandBus` (`Source/EventFlow/CommandBus.cs`) which resolves handlers, invokes aggregates deriving from `AggregateRoot`, and emits events that pipe through subscribers and read-store dispatchers. @@ -30,3 +32,8 @@ - When wiring new persistence, register required DI services before calling `.UseEventPersistence()` to avoid the `RemoveAll()` guard removing your registration. - Keep public APIs binary compatible where possible; breaking changes require updates in `Documentation/` and `RELEASE_NOTES.md`. - Mirror existing namespace layout (`EventFlow.{Feature}`) and group files into folders matching their conceptual role to keep source generators and discovery heuristics effective. + +## Operational safeguards +- Avoid invoking GitHub management tools that mutate remote state (issues, pull requests, repositories, projects, workflows, labels, security alerts, notifications, etc.) unless the user has granted explicit permission in the current conversation. +- Never run mutating `git` commands (commit, push, merge, rebase, reset, clean, etc.) without explicit user authorization; limit `git` usage to read-only inspection by default. +- If permission is unclear, pause and ask the user before attempting any action that could alter repository or GitHub state. diff --git a/.gitignore b/.gitignore index 97a000728..e411278b3 100644 --- a/.gitignore +++ b/.gitignore @@ -201,10 +201,6 @@ FakesAssemblies/ # Git commit history /-la -# Visual Studio Code -/.ionide -/.vscode - ##################################################### # Project specific files diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 000000000..6d014d446 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + "servers": { + "github-official": { + "url": "https://api.githubcopilot.com/mcp/", + "type": "http" + } + }, + "inputs": [] +} \ No newline at end of file From 5bb40cb44f8f140c450c871fc4ea2c89f89dcde4 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 11:24:15 +0200 Subject: [PATCH 24/56] Update release notes --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2e44624f1..10d999f18 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,7 @@ ### New in 1.2.2 (working version, not released yet) * Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) +* Maintenance: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) ### New in 1.2.1 (released 2025-05-29) From aeb07f4f8e02ae897fb7f8502df96a69c62a0ffd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 11:57:27 +0200 Subject: [PATCH 25/56] Update target framework from netstandard2.0 to netstandard2.1 --- Source/EventFlow.Sql/EventFlow.Sql.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/EventFlow.Sql/EventFlow.Sql.csproj b/Source/EventFlow.Sql/EventFlow.Sql.csproj index 1ff1c8cb9..ef9160bd1 100644 --- a/Source/EventFlow.Sql/EventFlow.Sql.csproj +++ b/Source/EventFlow.Sql/EventFlow.Sql.csproj @@ -17,7 +17,7 @@ - + From 84f166bb67091b4338174ca644c58fdf26ab981f Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:14:21 +0200 Subject: [PATCH 26/56] Update release notes --- RELEASE_NOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 10d999f18..3b602590d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,7 +1,8 @@ ### New in 1.2.2 (working version, not released yet) * Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) -* Maintenance: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) +* Fix: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) +* Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (thanks @thompson-tomo) ### New in 1.2.1 (released 2025-05-29) From ae9bcbd3df0c2359a630d215d0ffba7f08dc0cb7 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:28:58 +0200 Subject: [PATCH 27/56] Improve documentation for MongoDB --- Documentation/integration/mongodb.md | 133 +++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 10 deletions(-) diff --git a/Documentation/integration/mongodb.md b/Documentation/integration/mongodb.md index 9c89ac614..1b5576139 100644 --- a/Documentation/integration/mongodb.md +++ b/Documentation/integration/mongodb.md @@ -2,21 +2,134 @@ layout: default title: MongoDB parent: Integration -nav_order: 2 +nav_order: 3 --- -Mongo DB -======== +# MongoDB -To setup EventFlow Mongo DB, install the NuGet package `EventFlow.MongoDB` and add this to your EventFlow setup. +Use the `EventFlow.MongoDB` integration when you want EventFlow to persist events, +read models, or snapshots in MongoDB. This guide walks through the recommended +package, configuration patterns, collection preparation, and a few +troubleshooting tips. + +## Prerequisites + +- A MongoDB server (Replica Set recommended for production). EventFlow works with + MongoDB 5.0 or newer; the integration tests run against Mongo2Go, which ships + with MongoDB 6.x. +- A .NET application already wired with `EventFlow`. +- Network access and credentials that allow reads and writes to the target database. + +## Install the NuGet package + +Add the MongoDB integration to every project that configures EventFlow. + +```bash +dotnet add package EventFlow.MongoDB +``` + +## Configure EventFlow + +The `ConfigureMongoDb` helpers make sure a single `IMongoDatabase` instance is +registered with DI. You can pass a connection string, a custom `MongoClient`, or +an `IMongoDatabase` factory. + +```csharp +// Program.cs / Startup.cs +var mongoUrl = new MongoUrl(Configuration.GetConnectionString("eventflow-mongo")); +var mongoClient = new MongoClient(mongoUrl); + +services.AddEventFlow(ef => ef + .ConfigureMongoDb(mongoClient, mongoUrl.DatabaseName) + .UseMongoDbEventStore() // Events + .UseMongoDbSnapshotStore() // Snapshots (optional) + .UseMongoDbReadModel() // Read models + .UseMongoDbReadModel()); +``` + +### Read models must implement `IMongoDbReadModel` + +Mongo-backed read models use optimistic concurrency on a `Version` field and +store documents in a single collection per read model type. Implement the +interface and optionally override the collection name. + +```csharp +[MongoDbCollectionName("users")] +public class UserReadModel : IMongoDbReadModel, + IAmReadModelFor +{ + public string Id { get; set; } = default!; // MongoDB document _id + public long? Version { get; set; } + public string Username { get; set; } = default!; + + public Task ApplyAsync( + IReadModelContext context, + IDomainEvent domainEvent, + CancellationToken cancellationToken) + { + Id = domainEvent.AggregateIdentity.Value; + Username = domainEvent.AggregateEvent.Username.Value; + return Task.CompletedTask; + } +} +``` + +If you omit `MongoDbCollectionNameAttribute`, EventFlow defaults to +`ReadModel-[TypeName]` for the collection name. + +### Snapshots + +Calling `UseMongoDbSnapshotStore()` stores aggregate snapshots in the same +database. Each snapshot is kept in a shared `eventflow-snapshots` collection, +including the version number and metadata required for upgrades. + +## Prepare collections and indexes + +EventFlow registers an `IMongoDbEventPersistenceInitializer` that sets up the +unique index on `(AggregateId, AggregateSequenceNumber)` in the events +collection. Run it once during application startup or as a migration step. ```csharp -// ... -.ConfigureMongoDb(client, "database-name") -// ... +using (var scope = services.BuildServiceProvider().CreateScope()) +{ + scope.ServiceProvider + .GetRequiredService() + .Initialize(); +} +``` + +Read model collections are created lazily. When running in production, pre-create +them with the appropriate indexes for your query workload (for example, on +`Username` or `TenantId` fields) and size any capped collections ahead of time. + +## Local development quickstart + +Spin up a disposable MongoDB container and point your connection string at +`mongodb://localhost:27017/eventflow`. + +```bash +docker run --rm -p 27017:27017 --name eventflow-mongo mongo:7 ``` -After setting up Mongo DB support in EventFlow, you can continue to configure it. +Integration tests live in `Source/EventFlow.MongoDB.Tests` if you need sample +fixtures for seeding data or running smoke tests. + +## Troubleshooting + +- **Duplicate key errors on event writes** – ensure the initializer created the + index or rerun `Initialize()`. Unique index collisions usually indicate a + concurrency issue in the aggregate. +- **Read model updates never land** – confirm your read models implement + `IMongoDbReadModel` and expose a writable `Version` property. Without it, the + optimistic concurrency check fails silently. +- **Connection spikes on cold start** – reuse a singleton `MongoClient` instead + of recreating it per request so the driver can manage pooling. +- **Changing collection names** – rename carefully and migrate existing data; + EventFlow does not perform collection migrations automatically. + +## See also + +- [Event stores](event-stores.md#mongo-db) +- [Read model stores](read-stores.md#mongo-db) +- [Snapshots](../additional/snapshots.md) -- [Event store](event-stores.md#mongo-db) -- [Read model store](read-stores.md#mongo-db) From 3408fde48a9f7a13833ea3d0203166dac62199d9 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:30:34 +0200 Subject: [PATCH 28/56] Improve documentation for MSSQL --- Documentation/integration/mssql.md | 148 ++++++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 12 deletions(-) diff --git a/Documentation/integration/mssql.md b/Documentation/integration/mssql.md index 0c7a407a9..1c6f2b5b4 100644 --- a/Documentation/integration/mssql.md +++ b/Documentation/integration/mssql.md @@ -5,26 +5,150 @@ parent: Integration nav_order: 2 --- -Microsoft SQL Server -==================== +# Microsoft SQL Server -To setup EventFlow Microsoft SQL Server integration, install the NuGet -package `EventFlow.MsSql` and add this to your EventFlow setup. +EventFlow ships with first-class integration for Microsoft SQL Server (MSSQL) across the event store, snapshot store, and read models. This page walks through the required packages, configuration, and operational processes you need to run EventFlow on MSSQL in production. + +## Prerequisites + +- .NET 8.0 (or the version used by your application) with access to NuGet feeds. +- SQL Server 2017 or later (on-premises or Azure SQL Database) with permissions to create schemas, tables, indexes, and table types. +- An understanding of EventFlow event sourcing concepts such as [aggregates](../basics/aggregates.md) and [read stores](read-stores.md). + +## Install the NuGet packages + +Add the MSSQL integration package to every project that configures EventFlow. + +```powershell +dotnet add package EventFlow.MsSql +``` + +If you also leverage the generic SQL helpers, ensure `EventFlow.Sql` is referenced; it is already a dependency of the MSSQL package when installed via NuGet. + +## Configure EventFlow + +All MSSQL components share the same connection configuration. Call `ConfigureMsSql` once before registering the specific stores you need. ```csharp public void ConfigureServices(IServiceCollection services) { - services.AddEventFlow(ef => + services.AddEventFlow(options => { - ef.ConfigureMsSql(MsSqlConfiguration.New - .SetConnectionString(@"Server=.\SQLEXPRESS;Database=MyApp;User Id=sa;Password=???")) - .UseMssqlEventStore(); + options + .ConfigureMsSql(MsSqlConfiguration + .New + .SetConnectionString(@"Server=.\SQLEXPRESS;Database=MyApp;User Id=sa;Password=Pa55w0rd!")) + .UseMssqlEventStore() + .UseMssqlSnapshotStore() + .UseMssqlReadModel() + .UseMssqlReadModel(); }); } ``` -After setting up Microsoft SQL Server support in EventFlow, you can -continue to configure it. +`ConfigureMsSql` registers the `IMsSqlConfiguration` and the database migrator that is reused by the event, snapshot, and read model stores. You can fine-tune the configuration (timeouts, retry counts, schema names) via the fluent helpers on `MsSqlConfiguration`. + +## Event store + +### Enable the MSSQL event store + +The event store replaces the in-memory default by calling `UseMssqlEventStore()`. All aggregates share a single table that stores the full stream history. + +```csharp +services.AddEventFlow(o => + o.ConfigureMsSql(config) + .UseMssqlEventStore()); +``` + +### Provision the schema + +Before the first aggregate is persisted, run the embedded SQL scripts shipped with EventFlow. This creates the `EventFlow` table, supporting indexes, and the `eventdatamodel_list_type` table type used for batch inserts. + +```csharp +using var serviceProvider = services.BuildServiceProvider(); +var migrator = serviceProvider.GetRequiredService(); +await EventFlowEventStoresMsSql.MigrateDatabaseAsync(migrator, cancellationToken); +``` + +Run this during deployment or application startup. The migrator is idempotent, so reruns simply ensure the schema is present. If your SQL login does not have `CREATE TYPE` rights, grant them explicitly; otherwise batch appends will fail at runtime. + +### Recommended database settings + +- Enable [READ_COMMITTED_SNAPSHOT](https://learn.microsoft.com/sql/t-sql/statements/alter-database-transact-sql-set-options) to minimize locking contention under load. +- Monitor transaction log growth—the event store writes append-only batches with explicit transactions. +- Retain the default clustered index unless you have a measured need; the included scripts already optimize the append path. + +## Snapshot store + +Snapshot persistence reduces load time for long-running aggregates. Enable it with `.UseMssqlSnapshotStore()` after calling `ConfigureMsSql`. + +```csharp +services.AddEventFlow(o => + o.ConfigureMsSql(config) + .UseMssqlSnapshotStore()); +``` + +Provision the schema using the bundled scripts. + +```csharp +var migrator = serviceProvider.GetRequiredService(); +await EventFlowSnapshotStoresMsSql.MigrateDatabaseAsync(migrator, cancellationToken); +``` + +This creates the `EventFlowSnapshots` table and supporting indexes. Snapshots are optional, so call this migrator only when the snapshot store is configured. + +## Read model store + +MSSQL read models use the generic SQL read store implementation while relying on user-supplied schema scripts. Register each read model with either `.UseMssqlReadModel()` or the locator overload when IDs are derived from event data. + +```csharp +services.AddEventFlow(o => + o.ConfigureMsSql(config) + .UseMssqlReadModel() + .UseMssqlReadModel()); +``` + +### Shape your tables + +EventFlow does not automatically create read model tables. Instead, generate the DDL once (using `ReadModelSqlGenerator` if you prefer) and deploy it alongside your migrations. The minimal schema requires: + +- A table—by convention named `ReadModel-[ClassName]`, or override via `[Table("CustomName")]`. +- A primary key column marked with `[SqlReadModelIdentityColumn]` (type `nvarchar(255)` is typical). +- An integer column decorated with `[SqlReadModelVersionColumn]` to track the sequence number. + +Example T-SQL: + +```sql +CREATE TABLE [dbo].[ReadModel-UserReadModel] +( + [Id] NVARCHAR(255) NOT NULL, + [Version] INT NOT NULL, + [UserId] NVARCHAR(255) NOT NULL, + [Username] NVARCHAR(255) NOT NULL, + CONSTRAINT [PK_ReadModel-UserReadModel] PRIMARY KEY CLUSTERED ([Id]) +); +``` + +Deploy custom scripts with the database migrator. You can embed them in your assembly and run them at startup: + +```csharp +await migrator.MigrateDatabaseUsingEmbeddedScriptsAsync( + typeof(Program).Assembly, + scriptNamespace: "MyCompany.MyApp.SqlScripts", + cancellationToken); +``` + +### Tips for production + +- Add covering indexes to match your query patterns; EventFlow only enforces the identity index. +- When read models include JSON or large payloads, use `NVARCHAR(MAX)` and keep the row count lean by projecting separate tables per query. +- The read store honours optimistic concurrency; transient conflicts surface as `ReadModelTemporaryException`. Wrap updates in retry logic where necessary. + +## Deployment checklist + +- [ ] Run `EventFlowEventStoresMsSql.MigrateDatabaseAsync` in every environment that uses the MSSQL event store. +- [ ] Run `EventFlowSnapshotStoresMsSql.MigrateDatabaseAsync` when snapshots are enabled. +- [ ] Deploy read model DDL scripts alongside your application binaries. +- [ ] Verify connection strings and credentials for background workers that publish commands or process read models. -- [Event store](event-stores.md#mongo-db) -- [Read model store](read-stores.md#mongo-db) +With these steps in place, your EventFlow application can confidently use Microsoft SQL Server for reliable event sourcing, snapshots, and query projections. From 54d2ef46b9c1664ce3125b8e9ec42ab8e58914d1 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:45:49 +0200 Subject: [PATCH 29/56] Improve documentation for RabbitMQ --- Documentation/integration/rabbitmq.md | 162 ++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 10 deletions(-) diff --git a/Documentation/integration/rabbitmq.md b/Documentation/integration/rabbitmq.md index 21750f6bb..7be9dba7d 100644 --- a/Documentation/integration/rabbitmq.md +++ b/Documentation/integration/rabbitmq.md @@ -5,19 +5,161 @@ parent: Integration nav_order: 2 --- -RabbitMQ -======== +# RabbitMQ -To setup EventFlow's [RabbitMQ](https://www.rabbitmq.com/) integration, install the NuGet package -`EventFlow.RabbitMQ` and add this to your EventFlow setup. +EventFlow ships with a RabbitMQ integration that fans every persisted domain event out to an exchange. This is +useful when downstream systems (read models, legacy services, analytics pipelines, and so on) must react to +aggregate changes without being tightly coupled to the write model. + +The integration focuses on **publishing**. It does not create queues or start consumers for you—topology remains +an infrastructure concern so you can keep the messaging contract explicit. + +## Prerequisites + +- RabbitMQ 3.8 or newer (older versions work, but automatic recovery and federation are more reliable on ≥3.8). +- The [`EventFlow.RabbitMQ`](https://www.nuget.org/packages/EventFlow.RabbitMQ) package alongside the core EventFlow packages. +- A pre-provisioned exchange (typically a durable topic exchange) plus the queues/bindings you want to consume from. + EventFlow does **not** declare exchanges or queues automatically. + +## Install and register the publisher + +```bash +dotnet add package EventFlow.RabbitMQ +``` + +Enable the publisher when you build your `EventFlowOptions`. ```csharp -var uri = new Uri("amqp://localhost"); -// ... -.PublishToRabbitMq(RabbitMqConfiguration.With(uri)) -// ... +using EventFlow.RabbitMQ; +using EventFlow.RabbitMQ.Extensions; + +var rabbitUri = new Uri("amqp://guest:guest@localhost:5672/"); + +services.AddEventFlow(options => options + // ... register aggregates, commands, read models, etc. + .PublishToRabbitMq( + RabbitMqConfiguration.With( + rabbitUri, + persistent: true, // mark messages as durable + modelsPrConnection: 5, // pooled channels per connection + exchange: "eventflow"))); // topic exchange to publish to ``` -After setting up RabbitMQ support in EventFlow, you can continue to configure it. +`RabbitMqConfiguration.With` exposes the following knobs: + +- `uri` – The AMQP URI, including credentials and vhost. Use `amqps://` when TLS is required. +- `persistent` – Whether RabbitMQ should persist messages to disk (`true` by default). Set this to `false` for + transient data. +- `modelsPrConnection` – How many channels (models) the integration pools per connection. Increase the value if you + have a high write rate and observe channel contention. +- `exchange` – Name of the exchange EventFlow publishes to. The exchange must already exist. + +Once configured, EventFlow registers an `ISubscribeSynchronousToAll` subscriber that ships each domain event to +RabbitMQ right after the event is committed to the event store. The command is considered complete only after the +publish succeeds (or ultimately fails), so RabbitMQ errors surface to the caller. + +## Exchange and routing conventions + +By default messages are published with: + +- **Exchange** – The value supplied via `RabbitMqConfiguration.With` (defaults to `eventflow`). +- **Routing key** – `eventflow.domainevent.{aggregate-name}.{event-name}.{event-version}` where each segment is + slugified (lowercase, dashes for PascalCase). + +For example, an event named `UserRegistered` version `1` from `CustomerAggregate` produces: + +``` +eventflow.domainevent.customer.user-registered.1 +``` + +### Creating queues and bindings + +EventFlow does not create queues. Bind your own queues to the configured exchange using the routing keys that matter +to a consumer. With the default topic exchange, you can subscribe to an entire aggregate or event family: + +- `eventflow.domainevent.customer.*` – All events from `CustomerAggregate`. +- `eventflow.domainevent.*.user-registered.*` – Any `UserRegistered` event regardless of aggregate. + +```csharp +using var connection = factory.CreateConnection(); +using var channel = connection.CreateModel(); + +channel.ExchangeDeclare("eventflow", ExchangeType.Topic, durable: true); +channel.QueueDeclare("customer-updates", durable: true, exclusive: false, autoDelete: false); +channel.QueueBind("customer-updates", "eventflow", "eventflow.domainevent.customer.#"); +``` + +Run similar provisioning code (or infrastructure as code) before your service starts or during deployment. + +## Message payload and headers + +The integration serializes the aggregate event using EventFlow’s regular JSON serializer. Metadata is sent alongside +the message in two places: + +- **Body** – JSON payload with the actual event data. This is identical to what the event store persists. +- **Headers** – A `Dictionary` containing EventFlow metadata such as: + - `event_name`, `event_version` + - `aggregate_id`, `aggregate_name`, `aggregate_sequence_number` + - `event_id`, `batch_id`, `timestamp`, `timestamp_epoch` + - `source_id` when available + +Example body: + +```json +{ + "UserId": "dcd3f2e1-6f9b-4fcb-8901-9a5f6f2f4c0a", + "Email": "customer@example.com", + "RegisteredAt": "2025-09-20T17:53:41.197842Z" +} +``` + +Example headers: + +| Header | Example value | +| --- | --- | +| `event_name` | `user-registered` | +| `event_version` | `1` | +| `aggregate_name` | `customer` | +| `aggregate_id` | `customer-5b0d9af0` | +| `aggregate_sequence_number` | `42` | +| `event_id` | `01JF2ZNKX1QZS5CJ1V6AQ13RPT` | +| `timestamp` | `2025-09-20T17:53:41.2012129Z` | + +Leverage these headers to enrich logs, implement idempotency, or drive filtering logic in consumers. + +## Reliability characteristics + +- **Persistent messages** – Enabled by default via `basicProperties.Persistent = true` when configured. +- **Connection pooling** – A connection is opened per URI and keeps a pool of AMQP channels (models) to avoid throttling. + Tune `modelsPrConnection` for your throughput profile. +- **Automatic recovery** – The RabbitMQ client enables topology and automatic connection recovery so brief network blips + self-heal. +- **Retry strategy** – Transient `BrokerUnreachableException`, `OperationInterruptedException`, and `EndOfStreamException` + are retried up to three times with a 25 ms backoff. Replace `IRabbitMqRetryStrategy` in the container if you need custom + retry logic. + +Failures that propagate after retries cause the current command to fail; the publish will be retried the next time the +command is executed or when the aggregate emits subsequent events. + +## Customizing the integration + +- **Alternate exchange or routing key** – Replace the registered `IRabbitMqMessageFactory` with your own implementation + to target different exchanges, enrich headers, or transform the payload. +- **Custom publish mechanics** – Override `IRabbitMqPublisher` if you need publisher confirms, tracing, or batch semantics. +- **Asynchronous publishing** – If you prefer to publish outside the command execution pipeline, register your own + `ISubscribeAsynchronousToAll` implementation and publish from there instead of relying on the built-in synchronous publisher. + +```csharp +services.TryAddSingleton(); +``` + +## Troubleshooting + +- `NOT_FOUND - no exchange` – The exchange name does not exist. Create it manually or update the configuration. +- `NO_ROUTE` warnings – Nothing is bound to the routing key. Check your queue bindings. +- **Channel busy or blocked** – Increase `modelsPrConnection` or scale out publishers. +- **Silent drops** – Inspect consumer acknowledgements and dead-letter queues; EventFlow only publishes and cannot observe + downstream failures. -- [Publish all domain events](../basics/subscribers.md) +For general guidance on subscribers and out-of-order delivery considerations, review the +[subscribers documentation](../basics/subscribers.md). From 19148f4630df60f5e58949fe7530a573675ab6f5 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:58:15 +0200 Subject: [PATCH 30/56] Improve documentation for configuration --- Documentation/additional/configuration.md | 149 ++++++++++++++++++++-- 1 file changed, 137 insertions(+), 12 deletions(-) diff --git a/Documentation/additional/configuration.md b/Documentation/additional/configuration.md index 051ae448d..55f812980 100644 --- a/Documentation/additional/configuration.md +++ b/Documentation/additional/configuration.md @@ -1,21 +1,146 @@ ---- title: Configuration --- -# Configuration +# EventFlow runtime configuration + +EventFlow ships with sensible defaults, but most production workloads need to tune how the pipeline reacts to retries, subscriber failures, and replay throughput. All of those switches are exposed via `EventFlowOptions` when you wire the framework into your dependency injection container. + +## How configuration is applied + +Calling `AddEventFlow` registers the core services and gives you an `IEventFlowOptions` hook. Every configuration tweak happens inside that callback. + +```csharp +using System; +using Microsoft.Extensions.DependencyInjection; +using EventFlow; + +var services = new ServiceCollection(); + +services.AddEventFlow(options => +{ + options + .ConfigureOptimisticConcurrencyRetry(retries: 6, delayBeforeRetry: TimeSpan.FromMilliseconds(250)) + .Configure(cfg => + { + cfg.ThrowSubscriberExceptions = true; + cfg.IsAsynchronousSubscribersEnabled = true; + }); +}); + +using var provider = services.BuildServiceProvider(); +``` + +Under the covers `AddEventFlow` calls `EventFlowOptions.New(serviceCollection)` and stores a single `EventFlowConfiguration` instance in the container as both `IEventFlowConfiguration` and `ICancellationConfiguration`. Additional fluent helpers (e.g., `.AddEvents`, `.AddCommands`, `.UsePostgreSqlEventStore`) can be chained in the same callback. + +!!! tip + You can invoke `.Configure(...)` multiple times. Each delegate receives the same `EventFlowConfiguration` instance, so later calls simply overwrite earlier values. -EventFlow configuration can be done via the `.Configure(o => {})` method, which is available on the `EventFlowOptions` object. +## `EventFlowConfiguration` reference + +`EventFlowConfiguration` is defined in `Source/EventFlow/Configuration/EventFlowConfiguration.cs`. All properties are mutable so that they can be adjusted during startup. + +| Setting | Default | Used by | Effect | +| --- | --- | --- | --- | +| `LoadReadModelEventPageSize` | `200` | `ReadModelPopulator.LoadEventsAsync` | Controls how many events are fetched per call when bulk-populating read models via `IReadModelPopulator`. Increase this if your event store can stream large pages efficiently; reduce it when replaying against constrained backends. +| `PopulateReadModelEventPageSize` | `10000` | `ReadModelPopulator.ProcessEventQueueAsync` | Sets the batch size used when dispatching replayed events to read-store managers. Lower values trade throughput for lower memory pressure during large replays. +| `NumberOfRetriesOnOptimisticConcurrencyExceptions` | `4` | `OptimisticConcurrencyRetryStrategy` | Upper bound on how many times the aggregate store retries commits when the persistence layer reports `OptimisticConcurrencyException`. +| `DelayBeforeRetryOnOptimisticConcurrencyExceptions` | `00:00:00.100` | `OptimisticConcurrencyRetryStrategy` | Delay inserted between those retries. Combine with the retry count to soften hot spots in high-contention aggregates. +| `ThrowSubscriberExceptions` | `false` | `DispatchToEventSubscribers` | When `false`, synchronous subscriber exceptions are logged and wrapped in a resilience strategy; when `true`, they are rethrown so the calling command handler observes the failure immediately. +| `IsAsynchronousSubscribersEnabled` | `false` | `DomainEventPublisher.PublishToAsynchronousSubscribersAsync` | When enabled, every asynchronous subscriber invocation is scheduled through `IJobScheduler` (`InstantJobScheduler` by default). Pair this with a durable scheduler such as `EventFlow.Hangfire` to honor delayed execution. +| `CancellationBoundary` | `CancellationBoundary.BeforeCommittingEvents` | `ICancellationConfiguration.Limit` | Decides how far cancellation tokens propagate through the command pipeline. Choose a later boundary if downstream infrastructure (read stores, subscribers) should respect cancellation requests. +| `ForwardOptimisticConcurrencyExceptions` | `false` | `AggregateStore` | When `true`, optimistic concurrency exceptions are forwarded to `IAggregateStoreResilienceStrategy.HandleCommitFailedAsync` before bubbling out. Use this if you implement a custom resilience strategy that can translate conflicts into domain-specific outcomes. + +!!! note + The enum values for `CancellationBoundary` are defined in `Configuration/Cancellation/CancellationBoundary.cs` and progress in chronological order through the command pipeline (`BeforeUpdatingAggregate` → `BeforeCommittingEvents` → `BeforeUpdatingReadStores` → `BeforeNotifyingSubscribers` → `CancelAlways`). + +## Practical configuration scenarios + +### Enable durable asynchronous subscribers ```csharp -using var serviceCollection = new ServiceCollection() - // ... - .AddEventFlow(e => e.Configure(o => +using Hangfire; +using EventFlow.Hangfire.Extensions; + +services.AddHangfire(config => config.UseInMemoryStorage()); +services.AddHangfireServer(); + +services.AddEventFlow(options => +{ + options.Configure(cfg => { - o.IsAsynchronousSubscribersEnabled = true; - o.ThrowSubscriberExceptions = true; - })) - // ... - .BuildServiceProvider(); + cfg.IsAsynchronousSubscribersEnabled = true; + }); + + options.UseHangfireJobScheduler(); +}); ``` -In this example, we enable asynchronous subscribers and configure EventFlow to throw exceptions for subscriber errors. You can customize the configuration options to suit your needs. +Setting `IsAsynchronousSubscribersEnabled` causes `DomainEventPublisher` to enqueue a `DispatchToAsynchronousEventSubscribersJob` for every emitted domain event. Without a scheduler such as Hangfire, the bundled `InstantJobScheduler` executes jobs immediately in-process, effectively making asynchronous subscribers synchronous. + +### Harden aggregates against hot-spot contention + +```csharp +services.AddEventFlow(options => +{ + options + .ConfigureOptimisticConcurrencyRetry(retries: 8, delayBeforeRetry: TimeSpan.FromMilliseconds(500)) + .Configure(cfg => cfg.ForwardOptimisticConcurrencyExceptions = true); +}); +``` + +The retry helper only adjusts the built-in retry strategy. Setting `ForwardOptimisticConcurrencyExceptions` allows a custom `IAggregateStoreResilienceStrategy` to inspect the conflict and, for example, emit a domain-specific execution result instead of throwing. + +### Tune read model replay throughput + +```csharp +services.AddEventFlow(options => +{ + options.Configure(cfg => + { + cfg.LoadReadModelEventPageSize = 1000; // event store paging + cfg.PopulateReadModelEventPageSize = 2000; // read model batch size + }); +}); +``` + +These knobs directly influence `IReadModelPopulator.PopulateAsync`. Smaller batches reduce memory footprint and can help when replaying to remote databases; larger batches maximize throughput when the event store and read store are co-located. + +### Adjust cancellation semantics + +```csharp +services.AddEventFlow(options => +{ + options.Configure(cfg => + { + cfg.CancellationBoundary = CancellationBoundary.BeforeNotifyingSubscribers; + }); +}); +``` + +Raising the boundary ensures cancellation tokens are honoured while rebuilding read stores, but once the boundary is crossed EventFlow will run to completion to keep the event store and read models consistent. + +## Consuming configuration at runtime + +Every component registered with the container can request `IEventFlowConfiguration` or `ICancellationConfiguration` to observe these values. + +```csharp +using EventFlow.Configuration; + +public class ProjectionWorker(IEventFlowConfiguration configuration) +{ + public Task HandleAsync(CancellationToken cancellationToken) + { + var maxBatchSize = configuration.PopulateReadModelEventPageSize; + // ... use the configured value + return Task.CompletedTask; + } +} +``` + +This is useful when custom infrastructure (for example, an outbox publisher) needs to stay in lockstep with the same retry and cancellation semantics as the built-in components. + +## See also + +- [Subscribers](../basics/subscribers.md) — explains synchronous vs. asynchronous subscribers in detail. +- [Queries and read stores](../basics/queries.md) and [Read store integrations](../integration/read-stores.md) — pair naturally with the read model replay settings. +- [Commands](../basics/commands.md) — outlines how command handlers surface execution results that may be impacted by retry and exception settings. From b0d3a4771e0e1daa0ff327eb9f092a8b967323dd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 12:59:55 +0200 Subject: [PATCH 31/56] Fix spelling --- Documentation/additional/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/additional/configuration.md b/Documentation/additional/configuration.md index 55f812980..5efb56d47 100644 --- a/Documentation/additional/configuration.md +++ b/Documentation/additional/configuration.md @@ -117,7 +117,7 @@ services.AddEventFlow(options => }); ``` -Raising the boundary ensures cancellation tokens are honoured while rebuilding read stores, but once the boundary is crossed EventFlow will run to completion to keep the event store and read models consistent. +Raising the boundary ensures cancellation tokens are honored while rebuilding read stores, but once the boundary is crossed EventFlow will run to completion to keep the event store and read models consistent. ## Consuming configuration at runtime From 72d9eafe5bdb6ea92204859126eafae759b4722d Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 13:04:29 +0200 Subject: [PATCH 32/56] Improve documentation for FAQs --- Documentation/additional/faq.md | 89 +++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/Documentation/additional/faq.md b/Documentation/additional/faq.md index 61f5891da..349eca914 100644 --- a/Documentation/additional/faq.md +++ b/Documentation/additional/faq.md @@ -6,43 +6,78 @@ title: FAQ ## How can I ensure that only specific users can execute commands? -You can either replace the implementation of `ICommandBus` with your own implementation, or add a decorator that adds the authentication logic. +EventFlow deliberately keeps the command pipeline thin. The default `CommandBus.PublishAsync` resolves the single `ICommandHandler<,,,>` that matches a command and forwards the call to `IAggregateStore.UpdateAsync`. No authorization hooks are executed for you. + +You therefore have to enforce authorization either close to the domain or by decorating the command bus: + +- Inject any ambient context (for example an `ICurrentUser`) into your command handlers and return a failed `IExecutionResult` or throw a `DomainError` if the caller is not allowed to proceed. +- Replace the `ICommandBus` registration with a decorator that checks permissions before delegating to the inner bus. EventFlow registers the bus with `TryAddTransient()`, so a subsequent `services.Replace(...)` takes over cleanly. One possible decorator looks like this: + +```csharp +public sealed class SecuredCommandBus : ICommandBus +{ + private readonly ICommandBus _inner; + private readonly ICommandAuthorizer _authorizer; + private readonly ICurrentUser _user; + + public SecuredCommandBus(ICommandBus inner, ICommandAuthorizer authorizer, ICurrentUser user) + { + _inner = inner; + _authorizer = authorizer; + _user = user; + } + + public Task PublishAsync( + ICommand command, + CancellationToken cancellationToken) + where TAggregate : IAggregateRoot + where TIdentity : IIdentity + where TExecutionResult : IExecutionResult + { + if (!_authorizer.CanExecute(command, _user)) + { + throw DomainError.With( + "Command {0} is not allowed for {1}", + command.GetType().Name, + _user.Id); + } + + return _inner.PublishAsync(command, cancellationToken); + } +} +``` + +```csharp +services.AddEventFlow(options => { /* configure aggregates, commands, ... */ }); + +services.Replace(ServiceDescriptor.Transient(sp => + new SecuredCommandBus( + ActivatorUtilities.CreateInstance(sp), + sp.GetRequiredService(), + sp.GetRequiredService()))); +``` + +This keeps sensitive logic centralized while still letting the built-in `CommandBus` discover handlers and persist aggregates. ## Why isn't there a "global sequence number" on domain events? -While this is easy to support in some event stores like MSSQL, it -doesn't really make sense from a domain perspective. Greg Young also has -this to say on the subject: +Every `IDomainEvent` emitted by an aggregate exposes the `AggregateSequenceNumber` from `DomainEvent` and repeats the value in metadata under `MetadataKeys.AggregateSequenceNumber`. EventFlow guarantees ordering inside a single aggregate root and stops there, because cross-aggregate ordering is a projection concern rather than a domain invariant. -!!! quote - Order is only assured per a handler within an aggregate root - boundary. There is no assurance of order between handlers or between - aggregates. Trying to provide those things leads to the dark side. > - - [Greg Young](https://groups.yahoo.com/neo/groups/domaindrivendesign/conversations/topics/18453) +Most event store integrations (e.g., `MsSqlEventPersistence`, `PostgresSqlEventPersistence`, `SQLiteEventPersistence`) maintain their own `GlobalSequenceNumber` internally so they can serve `IEventStore.LoadAllEventsAsync(...)`. That API returns an `AllEventsPage` with a `GlobalPosition` token you can persist if you are building a log-reading process. Once the events are materialized into `IDomainEvent` instances and dispatched to handlers, the global number is intentionally not exposed—reactive code should not rely on cross-aggregate ordering promises. +If you need an application-level notion of global ordering, capture the `GlobalPosition` when you read from `LoadAllEventsAsync` or store it alongside your read model state. ## Why doesn't EventFlow have a unit of work concept? -Short answer, you shouldn't need it. But Mike has a way better answer: - -!!! quote - In the Domain, everything flows in one direction: forward. When - something bad happens, a correction is applied. The Domain doesn't - care about the database and UoW is very coupled to the db. In my - opinion, it's a pattern which is usable only with data access - objects, and in probably 99% of the cases you won't be needing it. - As with the Singleton, there are better ways but everything depends - on proper domain design. > `Mike +The aggregate itself is the unit of consistency in EventFlow. A command published through the `CommandBus` flows into `IAggregateStore.UpdateAsync`, which (1) rehydrates the aggregate by replaying its event stream, (2) executes your domain logic delegate, (3) commits the newly emitted events in a single call to `IEventPersistence.CommitEventsAsync`, and (4) publishes the resulting domain events to read stores, subscribers, and sagas. Because aggregate state comes entirely from events, there is no ambient change tracker to flush and a classic unit-of-work abstraction would not add any extra safety. - [Mogosanu](http://blog.sapiensworks.com/post/2014/06/04/Unit-Of-Work-is-the-new-Singleton.aspx) +When you really need to coordinate with another resource (e.g., enlist additional SQL statements), plug into `IAggregateStoreResilienceStrategy` or move the extra work into a subscriber that runs after the events have been durably written. -If your case falls within the 1% case, write a decorator for the -`ICommandBus` that starts a transaction, use MSSQL as event store and -make sure your read models are stored in MSSQL as well. +## Why are subscribers receiving events out of order? +EventFlow publishes events in stages through `DomainEventPublisher`: -## Why are subscribers receiving events out of order? +- Read stores and synchronous subscribers run one event at a time and in order. `DispatchToEventSubscribers.DispatchToSynchronousSubscribersAsync` awaits each handler before moving on. +- Asynchronous subscribers are intentionally different. Each `IDomainEvent` is wrapped in a `DispatchToAsynchronousEventSubscribersJob` and scheduled via `IJobScheduler`. The default `InstantJobScheduler` executes the jobs immediately, but `DomainEventPublisher` starts them with `Task.WhenAll(...)`, so completion order is not guaranteed and alternative schedulers (such as Hangfire) may execute them on other workers entirely. -It might be that your aggregates are emitting multiple events. Read about -[subscribers and out of order events](../basics/subscribers.md#out-of-order-events). +If your projection relies on strict ordering, keep it synchronous or persist enough information—such as the `AggregateSequenceNumber`—to detect and discard late arrivals. From 896016db76072449bad0fc4458f36fc9787a15b4 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 13:16:29 +0200 Subject: [PATCH 33/56] Improve documentation for jobs --- Documentation/basics/jobs.md | 89 ++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/Documentation/basics/jobs.md b/Documentation/basics/jobs.md index 1ea9f275a..f9c02c251 100644 --- a/Documentation/basics/jobs.md +++ b/Documentation/basics/jobs.md @@ -7,44 +7,46 @@ nav_order: 2 # Jobs -A job is basically a task that you want to execute outside of the -current context, on another server or at a later time. EventFlow -provides basic functionality for jobs. +Jobs let you execute work outside of the current request or process. They are +ideal when something should happen later, needs retries, or has to run on a +different machine. EventFlow ships with the primitives required to define, +register, and schedule jobs. -There are areas where you might find jobs very useful, here are some -examples +Typical use cases include: -- Publish a command at a specific time in the future -- Transient error handling +- Publishing a command at a specific time in the future +- Retrying transient operations without blocking the caller +- Deferring background work to a dedicated processor ```csharp var jobScheduler = resolver.Resolve(); var job = PublishCommandJob.Create(new SendEmailCommand(id), resolver); + await jobScheduler.ScheduleAsync( - job, - TimeSpan.FromDays(7), - CancellationToken.None) - .ConfigureAwait(false); + job, + TimeSpan.FromDays(7), + CancellationToken.None); ``` -In the above example the `SendEmailCommand` command will be published -in seven days. +The code above schedules the `SendEmailCommand` to run seven days from now. -!!! attention - When working with jobs, you should be aware of the following +!!! warning + The default `IJobScheduler` implementation in EventFlow is the + `InstantJobScheduler`. It executes jobs **immediately in the current + process**, ignoring `runAt` and `delay` arguments. To perform actual delayed + or distributed execution you must register another scheduler, for example + the Hangfire integration shown later on this page. - - The default implementation does executes the job *now* (completely ignoring `runAt`/`delay` parameters) and in the - current context. To get support for scheduled jobs, inject another implementation of `IJobScheduler`, - e.g. by installing `EventFlow.Hangfire` (Read below for details). - - Your jobs should serialize to JSON properly, see the section on - [value objects](../additional/value-objects.md) for more information - - If you use the provided `PublishCommandJob`, make sure that your - commands serialize properly as well +!!! note + Jobs must serialize to JSON cleanly, because schedulers typically persist + the job payload. Review the guidance on [value + objects](../additional/value-objects.md) and ensure any commands emitted via + `PublishCommandJob` serialize correctly as well. ## Create your own jobs -To create your own jobs, your job merely needs to implement the `IJob` -interface and be registered in EventFlow. +Implement the `IJob` interface for each job type you want to schedule and +register it with EventFlow. Here's an example of a job implementing `IJob` @@ -70,12 +72,10 @@ public class LogMessageJob : IJob } ``` -Note that the `JobVersion` attribute specifies the job name and -version to EventFlow and this is how EventFlow distinguishes between the -different job types. This makes it possible for you to reorder your -code, even rename the job type. As long as you keep the same attribute -values it is considered the same job in EventFlow. If the attribute is -omitted, the name will be the type name and version will be `1`. +The `JobVersion` attribute sets the logical job name and version used during +serialization. This allows you to move or rename the CLR type without breaking +existing scheduled jobs. If you omit the attribute, EventFlow falls back to the +type name and version `1`. Here's how the job is registered in EventFlow. @@ -89,7 +89,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -Then to schedule the job +Then schedule the job through `IJobScheduler`: ```csharp var jobScheduler = serviceProvider.GetRequiredService(); @@ -97,28 +97,29 @@ var job = new LogMessageJob("Great log message"); await jobScheduler.ScheduleAsync( job, TimeSpan.FromDays(7), - CancellationToken.None) - .ConfigureAwait(false); + CancellationToken.None); ``` ## Hangfire -To use [Hangfire](http://hangfire.io/) as the job scheduler, install -the NuGet package `EventFlow.Hangfire` and configure EventFlow to use -the scheduler like this. - -hangfire supports several different storage solutions including Microsoft SQL Server and MongoDB. Use only inMemoryStorage for testing and development. +For production-grade scheduling scenarios we recommend +[Hangfire](http://hangfire.io/). Install the `EventFlow.Hangfire` package and +configure EventFlow to use the Hangfire-backed scheduler. Hangfire supports +multiple storage providers (SQL Server, PostgreSQL, MongoDB, Redis, etc.). Use +the in-memory storage only during development. ```csharp private void RegisterHangfire(IEventFlowOptions eventFlowOptions) { - eventFlowOptions.ServiceCollection - .AddHangfire(c => c.UseInMemoryStorage()) - .AddHangfireServer(); - eventFlowOptions.UseHangfireJobScheduler(); + eventFlowOptions.ServiceCollection + .AddHangfire(configuration => configuration.UseSqlServerStorage(connectionString)) + .AddHangfireServer(); + + eventFlowOptions.UseHangfireJobScheduler(); } ``` !!! note - The `UseHangfireJobScheduler()` doesn't do any Hangfire - configuration, but merely registers the proper scheduler in EventFlow. + `UseHangfireJobScheduler()` simply swaps the scheduler implementation in + EventFlow. You are still responsible for configuring Hangfire storage, + servers, and dashboards according to your environment. From 27a4d353a92fca05b71979d3dfd811c72a162d59 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 13:17:54 +0200 Subject: [PATCH 34/56] Improve documentation for PostgreSQL --- Documentation/integration/postgresql.md | 250 ++++++++++++++++++++++-- 1 file changed, 230 insertions(+), 20 deletions(-) diff --git a/Documentation/integration/postgresql.md b/Documentation/integration/postgresql.md index 88dac37c0..cf4c1d4a3 100644 --- a/Documentation/integration/postgresql.md +++ b/Documentation/integration/postgresql.md @@ -2,28 +2,238 @@ layout: default title: PostgreSQL parent: Integration -nav_order: 2 +nav_order: 4 --- -## PostgreSQL +# PostgreSQL -To setup EventFlow PostgreSQL integration, install the NuGet -package [EventFlow.PostgreSql](https://www.nuget.org/packages/EventFlow.PostgreSql) and add this to your EventFlow setup. +Use the `EventFlow.PostgreSql` integration when you want EventFlow to persist +events, snapshots, and read models in PostgreSQL. The package wraps the Npgsql +driver and DbUp migrations, giving you consistent configuration, retries, and +schema provisioning across the stack. + +## Prerequisites + +- A .NET application already wired with `EventFlow`. +- PostgreSQL 12 or later. The bundled scripts rely on `GENERATED ... AS IDENTITY` + columns and user-defined types. +- Credentials that can execute `CREATE TABLE`, `CREATE TYPE`, and `CREATE INDEX` + statements in the target database. +- Network access for every service that emits commands or processes read + models. + +## Install the NuGet package + +Add the PostgreSQL integration to every project that configures EventFlow. + +```bash +dotnet add package EventFlow.PostgreSql +``` + +## Configure EventFlow + +Call `ConfigurePostgreSql` once to register the shared connection, migrator, and +transient retry strategy, then opt into the specific stores you need. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + var postgres = PostgreSqlConfiguration.New + .SetConnectionString(Configuration.GetConnectionString("eventflow-postgres")) + .SetTransientRetryCount(3); + + services.AddEventFlow(o => o + .ConfigurePostgreSql(postgres) + .UsePostgreSqlEventStore() // Events + .UsePostgreSqlSnapshotStore() // Snapshots (optional) + .UsePostgreSqlReadModel() // Read models + .UsePostgreSqlReadModel()); +} +``` + +`ConfigurePostgreSql` wires up `IPostgreSqlConnection`, the DbUp-based +`IPostgreSqlDatabaseMigrator`, and the `PostgreSqlErrorRetryStrategy` used by +the event store and read models. + +### Optional tuning + +- Call `SetConnectionString("read-models", ...)` when you want read models to + connect to a different database or replica. +- Adjust `SetTransientRetryCount` / `SetTransientRetryDelay` to tune retries + for deadlocks (`SqlState 40P01`) and active-transaction conflicts (`SqlState 25001`). +- Increase `SetUpgradeExecutionTimeout` when migration batches take longer than + five minutes. + +## Event store + +### Enable the PostgreSQL event store + +Replace the in-memory default by calling `UsePostgreSqlEventStore()` after +`ConfigurePostgreSql`. + +```csharp +services.AddEventFlow(o => + o.ConfigurePostgreSql(postgres) + .UsePostgreSqlEventStore()); +``` + +### Provision the schema + +Run the embedded scripts once per environment to create the `EventFlow` table, +the `(AggregateId, AggregateSequenceNumber)` unique index, and the +`eventdatamodel_list_type` composite type used for batch inserts. + +```csharp +await using var scope = services.BuildServiceProvider().CreateAsyncScope(); +var migrator = scope.ServiceProvider.GetRequiredService(); +await EventFlowEventStoresPostgreSql.MigrateDatabaseAsync(migrator, cancellationToken); +``` + +The migrator is idempotent—rerunning it simply ensures the schema is present. +Lack of `CREATE TYPE` or `CREATE TABLE` permissions causes install-time failures. + +### Operational notes + +- `PostgreSqlEventPersistence` surfaces duplicate key violations (`SqlState 23505`) + as `OptimisticConcurrencyException`; investigate aggregate concurrency if you + see these at runtime. +- Event batches are appended inside a transaction. Monitor WAL growth and plan + for appropriate autovacuum settings. +- The built-in retry strategy only retries deadlocks and active-transaction + errors; unexpected exceptions bubble immediately. + +## Snapshot store + +Enable PostgreSQL snapshots with `.UsePostgreSqlSnapshotStore()` and run the +companion migration to create the `EventFlowSnapshots` table. + +```csharp +services.AddEventFlow(o => + o.ConfigurePostgreSql(postgres) + .UsePostgreSqlSnapshotStore()); + +await EventFlowSnapshotStoresPostgreSql.MigrateDatabaseAsync(migrator, cancellationToken); +``` + +Snapshots share a single table keyed by `(AggregateName, AggregateId)` and store +the serialized data plus metadata needed for upgrades. Duplicate writes are +ignored when a snapshot with the same sequence number already exists. + +## Read model store + +### Register the store + +`UsePostgreSqlReadModel` (or the locator overload) plugs the SQL read-store +implementation into EventFlow. + +```csharp +services.AddEventFlow(o => + o.ConfigurePostgreSql(postgres) + .UsePostgreSqlReadModel() + .UsePostgreSqlReadModel()); +``` + +### Implement the read model + +PostgreSQL read models should implement `IReadModel` and either derive from +`PostgreSqlReadModel` or decorate key properties with the provided attributes. + +```csharp +[Table("ReadModel-User")] +public class UserReadModel : PostgreSqlReadModel, + IAmReadModelFor +{ + public string DisplayName { get; set; } = default!; + + public Task ApplyAsync( + IReadModelContext context, + IDomainEvent @event, + CancellationToken cancellationToken) + { + AggregateId = @event.AggregateIdentity.Value; + DisplayName = @event.AggregateEvent.DisplayName; + UpdatedTime = DateTimeOffset.UtcNow; + if (CreateTime == default) + { + CreateTime = UpdatedTime; + } + return Task.CompletedTask; + } +} +``` + +The base class marks `AggregateId` with `[PostgreSqlReadModelIdentityColumn]` and +`LastAggregateSequenceNumber` with `[PostgreSqlReadModelVersionColumn]`. Use +`[PostgreSqlReadModelIgnoreColumn]` to skip properties that are not persisted. + +### Create the table + +EventFlow does not auto-create read model tables. Deploy DDL that matches your +read model shape—by convention the table name is `ReadModel-[TypeName]`. + +```sql +CREATE TABLE IF NOT EXISTS "ReadModel-User" ( + Id BIGINT GENERATED BY DEFAULT AS IDENTITY, + AggregateId VARCHAR(64) NOT NULL, + CreateTime TIMESTAMPTZ NOT NULL, + UpdatedTime TIMESTAMPTZ NOT NULL, + LastAggregateSequenceNumber INT NOT NULL, + DisplayName TEXT NOT NULL, + CONSTRAINT "PK_ReadModel-User" PRIMARY KEY (Id) +); + +CREATE INDEX IF NOT EXISTS "IX_ReadModel-User_AggregateId" + ON "ReadModel-User" (AggregateId); +``` + +At a minimum, keep the identity column, the optimistic concurrency column, and +the fields mined by your query handlers. Add additional indexes to match your +query patterns. + +### Run read model migrations + +Package the DDL alongside your application and execute it with the shared +`IPostgreSqlDatabaseMigrator`. ```csharp -// ... -.ConfigurePostgreSql(PostgreSqlConfiguration.New - .SetConnectionString(@"User ID=me;Password=???;Host=localhost;Port=5432;Database=MyApp")) -.UsePostgreSqlEventStore() -.UsePostgreSqlSnapshotStore() -.UsePostgreSqlReadModel() -.UsePostgreSqlReadModel() -// ... -``` - -This code block configures EventFlow to store events, snapshots and read models in PostgreSQL. It's not mandatory, you -can mix and match, i.e. storing events in PostgreSQL, read models in Elastic search and don't using snapshots at all. - -- Event store. One big table `EventFlow` for all events for all aggregates. -- Read model store. Table `ReadModel-[ClassName]` per read model type. -- Snapshot store. One big table `EventFlowSnapshots` for all aggregates. +var migrator = scope.ServiceProvider.GetRequiredService(); +await migrator.MigrateDatabaseUsingEmbeddedScriptsAsync( + typeof(Program).Assembly, + scriptNamespace: "MyCompany.MyApp.SqlScripts", + cancellationToken); +``` + +The tests in `Source/EventFlow.PostgreSql.Tests` demonstrate this pattern: embed +versioned SQL files and invoke the migrator during startup or deployment. + +## Local development quickstart + +Run a disposable PostgreSQL container and point `ConfigurePostgreSql` to it. + +```bash +docker run --rm -p 5432:5432 --name eventflow-postgres \ + -e POSTGRES_PASSWORD=eventflow \ + -e POSTGRES_DB=eventflow \ + postgres:16 +``` + +## Troubleshooting + +- **`SqlState 23505` (duplicate key)** – the unique index on + `(AggregateId, AggregateSequenceNumber)` rejected a reinsert. Inspect aggregate + concurrency or idempotency guards. +- **`eventdatamodel_list_type` does not exist** – rerun + `EventFlowEventStoresPostgreSql.MigrateDatabaseAsync`; the composite type is + required for batch inserts. +- **Missing read model rows** – confirm the table exists, the identity column is + marked with `[PostgreSqlReadModelIdentityColumn]`, and the process has write + access; otherwise updates are ignored. +- **Permission errors during migration** – grant `CREATE TABLE`, `CREATE TYPE`, + and `CREATE INDEX` to the login executing the migrator. + +## See also + +- [Event stores](event-stores.md#postgresql-event-store) +- [Read model stores](read-stores.md) +- [Snapshots](../additional/snapshots.md) + From 8dbf312cc0dcce694ad3576719b4c6d631080eed Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 13:21:19 +0200 Subject: [PATCH 35/56] Update RELEASE_NOTES.md with documentation cleanup Cleaned up major parts of the documentation hosted on https://geteventflow.net. --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3b602590d..70b8fa601 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,7 @@ * Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) * Fix: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) * Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (thanks @thompson-tomo) +* Fix: Cleaned up major parts of the documentation hosted on https://geteventflow.net/ ### New in 1.2.1 (released 2025-05-29) From 3844b7b49ac68ecd620a056a98e446325fe821b7 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 13:42:45 +0200 Subject: [PATCH 36/56] Fix: Scheduling bug in Hangfire --- .../Integration/HangfireJobSchedulerTests.cs | 24 ++++++++++++++----- .../Integration/HangfireJobScheduler.cs | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs index 3f3a1ee45..3f2729339 100644 --- a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs @@ -124,16 +124,23 @@ public async Task ScheduleNow() [Test] public async Task ScheduleAsyncWithDateTime() { - await ValidateScheduleHappens((j, s) => s.ScheduleAsync(j, DateTimeOffset.Now.AddSeconds(1), CancellationToken.None)).ConfigureAwait(false); + var minimumDelay = TimeSpan.FromSeconds(0.75); + var runAt = DateTimeOffset.UtcNow.AddSeconds(1); + await ValidateScheduleHappens( + (j, s) => s.ScheduleAsync(j, runAt, CancellationToken.None), + minimumDelay).ConfigureAwait(false); } [Test] public async Task ScheduleAsyncWithTimeSpan() { - await ValidateScheduleHappens((j, s) => s.ScheduleAsync(j, TimeSpan.FromSeconds(1), CancellationToken.None)).ConfigureAwait(false); + var minimumDelay = TimeSpan.FromSeconds(0.75); + await ValidateScheduleHappens( + (j, s) => s.ScheduleAsync(j, TimeSpan.FromSeconds(1), CancellationToken.None), + minimumDelay).ConfigureAwait(false); } - private async Task ValidateScheduleHappens(Func> schedule) + private async Task ValidateScheduleHappens(Func> schedule, TimeSpan? minimumElapsed = null) { // Arrange var testId = ThingyId.New; @@ -141,18 +148,23 @@ private async Task ValidateScheduleHappens(Func(testId, CancellationToken.None).ConfigureAwait(false); if (!testAggregate.IsNew) { + var elapsed = DateTimeOffset.UtcNow - start; await AssertJobIsSuccessfullyAsync(jobId).ConfigureAwait(false); + if (minimumElapsed.HasValue) + { + elapsed.ShouldBeGreaterThanOrEqualTo(minimumElapsed.Value); + } Assert.Contains(pingId, testAggregate.PingsReceived.ToList()); - Assert.Pass(); + return; } await Task.Delay(TimeSpan.FromSeconds(0.2)).ConfigureAwait(false); diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs index 2bd5f88e2..c1a1c93f6 100644 --- a/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs @@ -71,7 +71,7 @@ public Task ScheduleAsync(IJob job, DateTimeOffset runAt, CancellationTo cancellationToken, (jobDefinition, json) => _queueName == null - ? _backgroundJobClient.Enqueue(ExecuteMethodCallExpression(jobDefinition, json)) + ? _backgroundJobClient.Schedule(ExecuteMethodCallExpression(jobDefinition, json), runAt) : _backgroundJobClient.Schedule(_queueName, ExecuteMethodCallExpression(jobDefinition, json), runAt)); } @@ -82,7 +82,7 @@ public Task ScheduleAsync(IJob job, TimeSpan delay, CancellationToken ca cancellationToken, (jobDefinition, json) => _queueName == null - ? _backgroundJobClient.Enqueue(ExecuteMethodCallExpression(jobDefinition, json)) + ? _backgroundJobClient.Schedule(ExecuteMethodCallExpression(jobDefinition, json), delay) : _backgroundJobClient.Schedule(_queueName, ExecuteMethodCallExpression(jobDefinition, json), delay)); } From f18fcc83e25c1d5963ba2c2de72af69bd65b64a2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:16:40 +0200 Subject: [PATCH 37/56] Update release notes --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 70b8fa601..9d1d1dd7a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,7 @@ * Fix: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) * Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (thanks @thompson-tomo) * Fix: Cleaned up major parts of the documentation hosted on https://geteventflow.net/ +* Fix: Resolved Hangfire delayed job scheduling bug by switching to the correct `Schedule` API ### New in 1.2.1 (released 2025-05-29) From 616fd73ea8ae793d726098ae11c2ff383f87bdb9 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:30:18 +0200 Subject: [PATCH 38/56] Fix backward compatibility with 0.x --- ...fireJobRunnerBackwardCompatibilityTests.cs | 99 +++++++++++++++++++ .../Integration/HangfireJobRunner.cs | 13 +++ .../Integration/IHangfireJobRunner.cs | 9 ++ 3 files changed, 121 insertions(+) create mode 100644 Source/EventFlow.Hangfire.Tests/Integration/HangfireJobRunnerBackwardCompatibilityTests.cs diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobRunnerBackwardCompatibilityTests.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobRunnerBackwardCompatibilityTests.cs new file mode 100644 index 000000000..965416e00 --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobRunnerBackwardCompatibilityTests.cs @@ -0,0 +1,99 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2025 Rasmus Mikkelsen +// https://github.com/eventflow/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Threading; +using System.Threading.Tasks; +using EventFlow.Hangfire.Integration; +using EventFlow.Jobs; +using NUnit.Framework; +using Shouldly; + +namespace EventFlow.Hangfire.Tests.Integration +{ + /// + /// Exercises the legacy Hangfire job runner signatures removed in EventFlow 1.x to ensure + /// that jobs enqueued by EventFlow.Hangfire 0.x still deserialize and execute via the + /// forwarding overloads introduced to address issue #1109. + /// + [TestFixture] + public class HangfireJobRunnerBackwardCompatibilityTests + { + private sealed class RecordingJobRunner : IJobRunner + { + public (string JobName, int Version, string Job, CancellationToken CancellationToken)? LastCall { get; private set; } + + public Task ExecuteAsync(string jobName, int version, string json, CancellationToken cancellationToken) + { + LastCall = (jobName, version, json, cancellationToken); + return Task.CompletedTask; + } + + public void Reset() + { + LastCall = null; + } + } + + [Test] + public void OldJobRunnerSignatureIsStillExposed() + { + var methodInfo = typeof(IHangfireJobRunner).GetMethod( + "ExecuteAsync", + new[] + { + typeof(string), + typeof(string), + typeof(int), + typeof(string), + typeof(string), + }); + + methodInfo.ShouldNotBeNull(); + } + + [Test] + public async Task OldSignatureDelegatesToModernImplementation() + { + var recordingJobRunner = new RecordingJobRunner(); + var hangfireJobRunner = new HangfireJobRunner(recordingJobRunner); + + #pragma warning disable CS0618 + await hangfireJobRunner.ExecuteAsync("display", "job", 7, "payload").ConfigureAwait(false); + + recordingJobRunner.LastCall.ShouldNotBeNull(); + recordingJobRunner.LastCall.Value.JobName.ShouldBe("job"); + recordingJobRunner.LastCall.Value.Version.ShouldBe(7); + recordingJobRunner.LastCall.Value.Job.ShouldBe("payload"); + recordingJobRunner.LastCall.Value.CancellationToken.ShouldBe(CancellationToken.None); + + recordingJobRunner.Reset(); + + await hangfireJobRunner.ExecuteAsync("display", "job", 7, "payload", "queue").ConfigureAwait(false); + #pragma warning restore CS0618 + + recordingJobRunner.LastCall.ShouldNotBeNull(); + recordingJobRunner.LastCall.Value.JobName.ShouldBe("job"); + recordingJobRunner.LastCall.Value.Version.ShouldBe(7); + recordingJobRunner.LastCall.Value.Job.ShouldBe("payload"); + } + } +} diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs index dc4ac5ae1..3f7e9cbaf 100644 --- a/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.Threading; using System.Threading.Tasks; using EventFlow.Jobs; @@ -36,6 +37,18 @@ public HangfireJobRunner( _jobRunner = jobRunner; } + [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] + public Task ExecuteAsync(string displayName, string jobName, int version, string job) + { + return ExecuteAsync(jobName, version, job); + } + + [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] + public Task ExecuteAsync(string displayName, string jobName, int version, string job, string queueName) + { + return ExecuteAsync(jobName, version, job); + } + public Task ExecuteAsync(string jobName, int version, string job) { return _jobRunner.ExecuteAsync(jobName, version, job, CancellationToken.None); diff --git a/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs b/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs index e2eaaa6cc..d91b2b1b3 100644 --- a/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs +++ b/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs @@ -20,6 +20,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +using System; using System.ComponentModel; using System.Threading.Tasks; @@ -27,6 +28,14 @@ namespace EventFlow.Hangfire.Integration { public interface IHangfireJobRunner { + [Obsolete("Use ExecuteAsync(string jobName, int version, string job).")] + [DisplayName("{0}")] + Task ExecuteAsync(string displayName, string jobName, int version, string job); + + [Obsolete("Use ExecuteAsync(string jobName, int version, string job).")] + [DisplayName("{0}")] + Task ExecuteAsync(string displayName, string jobName, int version, string job, string queueName); + [DisplayName("{0}")] Task ExecuteAsync(string jobName, int version, string job); } From d6aedb83ba0b058eb4d71afa6c6bd722756e808e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:36:35 +0200 Subject: [PATCH 39/56] Update RELEASE_NOTES.md for version 1.2.2 Updated release notes for version 1.2.2 to include issue fixes and acknowledgments. --- RELEASE_NOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9d1d1dd7a..2566d51bb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,9 +2,9 @@ * Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) * Fix: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) -* Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (thanks @thompson-tomo) +* Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (fixes #1107, thanks @thompson-tomo) * Fix: Cleaned up major parts of the documentation hosted on https://geteventflow.net/ -* Fix: Resolved Hangfire delayed job scheduling bug by switching to the correct `Schedule` API +* Fix: Resolved Hangfire delayed job scheduling bug by switching to the correct `Schedule` API (fixes #1104) ### New in 1.2.1 (released 2025-05-29) From 487dc65b15f613a772ca485beb245401363549c2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:38:55 +0200 Subject: [PATCH 40/56] Update Obsolete attribute messages for clarity --- Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs b/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs index d91b2b1b3..f2ff14882 100644 --- a/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs +++ b/Source/EventFlow.Hangfire/Integration/IHangfireJobRunner.cs @@ -28,15 +28,15 @@ namespace EventFlow.Hangfire.Integration { public interface IHangfireJobRunner { - [Obsolete("Use ExecuteAsync(string jobName, int version, string job).")] + [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] [DisplayName("{0}")] Task ExecuteAsync(string displayName, string jobName, int version, string job); - [Obsolete("Use ExecuteAsync(string jobName, int version, string job).")] + [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] [DisplayName("{0}")] Task ExecuteAsync(string displayName, string jobName, int version, string job, string queueName); [DisplayName("{0}")] Task ExecuteAsync(string jobName, int version, string job); } -} \ No newline at end of file +} From e665739b5c0cd2d3a2b668565674c22c1a031497 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:39:51 +0200 Subject: [PATCH 41/56] Suppress warnings for obsolete ExecuteAsync parameters Suppress unused parameter warnings for displayName and queueName in obsolete methods. --- Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs index 3f7e9cbaf..8463b6c4c 100644 --- a/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobRunner.cs @@ -40,12 +40,15 @@ public HangfireJobRunner( [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] public Task ExecuteAsync(string displayName, string jobName, int version, string job) { + _ = displayName; return ExecuteAsync(jobName, version, job); } [Obsolete("For backwards compatibility with jobs enqueued before EventFlow 1.x. Use ExecuteAsync(string jobName, int version, string job).")] public Task ExecuteAsync(string displayName, string jobName, int version, string job, string queueName) { + _ = displayName; + _ = queueName; return ExecuteAsync(jobName, version, job); } @@ -54,4 +57,4 @@ public Task ExecuteAsync(string jobName, int version, string job) return _jobRunner.ExecuteAsync(jobName, version, job, CancellationToken.None); } } -} \ No newline at end of file +} From 6392e04370906b2a0ab464982721a757badbeac1 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 16:40:46 +0200 Subject: [PATCH 42/56] Test stability --- .../Integration/HangfireJobSchedulerTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs index 3f2729339..def069a4d 100644 --- a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobSchedulerTests.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -148,16 +149,18 @@ private async Task ValidateScheduleHappens(Func(testId, CancellationToken.None).ConfigureAwait(false); if (!testAggregate.IsNew) { - var elapsed = DateTimeOffset.UtcNow - start; + var elapsed = stopwatch.Elapsed; + stopwatch.Stop(); await AssertJobIsSuccessfullyAsync(jobId).ConfigureAwait(false); if (minimumElapsed.HasValue) { @@ -170,6 +173,7 @@ private async Task ValidateScheduleHappens(Func Date: Sat, 11 Oct 2025 16:50:19 +0200 Subject: [PATCH 43/56] Update release notes --- RELEASE_NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2566d51bb..7b3f2d8c7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,7 @@ * Fix: Lean on framework-provided `Microsoft.CSharp` where available to trim redundant package references (fixes #1107, thanks @thompson-tomo) * Fix: Cleaned up major parts of the documentation hosted on https://geteventflow.net/ * Fix: Resolved Hangfire delayed job scheduling bug by switching to the correct `Schedule` API (fixes #1104) +* Fix: Restore Hangfire job runner backward compatibility with EventFlow 0.x by reintroducing legacy overloads (fixes #1109) ### New in 1.2.1 (released 2025-05-29) From f8f8a8a40879a20e99728a88f1f22d43865b12d6 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 17:30:20 +0200 Subject: [PATCH 44/56] Ping --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e74c5b47e..9ea9e2dea 100644 --- a/README.md +++ b/README.md @@ -505,3 +505,4 @@ SOFTWARE. ``` + From 0b65bbff030987da3eb97fec994fe6c2330de0dd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 17:45:43 +0200 Subject: [PATCH 45/56] Version is now 1.2.3 :) --- .github/workflows/pull-requests.yaml | 2 +- .github/workflows/release.yaml | 2 +- RELEASE_NOTES.md | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull-requests.yaml b/.github/workflows/pull-requests.yaml index 99e20d8a6..ffd59d97e 100644 --- a/.github/workflows/pull-requests.yaml +++ b/.github/workflows/pull-requests.yaml @@ -13,4 +13,4 @@ jobs: pipeline: uses: ./.github/workflows/pipeline.yaml with: - version: "1.2.2-pr${{ github.event.number }}-b${{ github.run_number }}" + version: "1.2.3-pr${{ github.event.number }}-b${{ github.run_number }}" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 263d85242..f61d7f3da 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,6 +14,6 @@ jobs: with: bake-convention: 'Release' environment: 'release' - version: "1.2.2" + version: "1.2.3" secrets: nuget-api-key: ${{ secrets.NUGET_APIKEY }} diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7b3f2d8c7..a6a4aff09 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,8 @@ -### New in 1.2.2 (working version, not released yet) +### New in 1.2.3 (released 2025-10-11) + +- *Nothing yet...* + +### New in 1.2.2 (released 2025-10-11) * Fix: Use the ASP.NET Core shared framework reference for non-`netstandard` targets in `EventFlow.AspNetCore` to avoid redundant package references (thanks @thompson-tomo) * Fix: Replace FluentAssertions with Shouldly across the solution to simplify assertion usage (thanks @Focus1337) From 2cbf7d29f274b2c7c4ee7e726326917fa6fd4a3e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 17:46:23 +0200 Subject: [PATCH 46/56] Fix release note heading --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a6a4aff09..0a9e4d017 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,4 @@ -### New in 1.2.3 (released 2025-10-11) +### New in 1.2.3 (not released yet) - *Nothing yet...* From dcdcd7f86a28883c3c256de35e32602bada4873f Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 11 Oct 2025 20:15:03 +0200 Subject: [PATCH 47/56] Change RabbitMQ status from inactive to active --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ea9e2dea..46558dc44 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The following list key characteristics of each version as well as its related br - 💀 `EventFlow.Owin` - 🟢 `EventFlow.PostgreSql` - 🟠 `EventFlow.Redis` - - 🟠 `EventFlow.RabbitMQ` + - 🟢 `EventFlow.RabbitMQ` - 🟢 `EventFlow.Sql` - 🟢 `EventFlow.SQLite` - 🟢 `EventFlow.TestHelpers` From 9a1625587441f464006638a33289e50095eb5c60 Mon Sep 17 00:00:00 2001 From: Meikel <11669846+MeikelLP@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:33:58 +0200 Subject: [PATCH 48/56] docs: update warning on main page --- Documentation/index.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Documentation/index.md b/Documentation/index.md index fd4cd6c06..cd6675f78 100644 --- a/Documentation/index.md +++ b/Documentation/index.md @@ -17,12 +17,8 @@ Have a look at our [getting started guide](getting-started.md), the [do’s and * **No use of threads or background workers** * **MIT licensed:** Easy to understand and use license for enterprise -!!! example +!!! Warning **Documentation is still in progress for v1** If you have any suggestions for the documentation, even if it's just a typo, please create an issue or a pull request. Improvements to the documentation are always welcome. - - Useful links for updating the documentation: - - - [https://squidfunk.github.io/mkdocs-material/reference/](https://squidfunk.github.io/mkdocs-material/reference/) From 0826c74c5b18ca9b7bc94ac124367b4114fa1a99 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 25 Oct 2025 10:05:33 +0200 Subject: [PATCH 49/56] Testing out GPT-5-Codex for reviews --- .github/workflows/review.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/review.yaml diff --git a/.github/workflows/review.yaml b/.github/workflows/review.yaml new file mode 100644 index 000000000..160d71100 --- /dev/null +++ b/.github/workflows/review.yaml @@ -0,0 +1,22 @@ +name: Perform a code review + +on: + issue_comment: + types: [created] + +permissions: + pull-requests: write + issues: write + checks: read + contents: read + +jobs: + pipeline: + uses: rasmus/workflow-review/.github/workflows/pipeline.yaml@v1 + with: + pull_request_number: ${{ github.event.issue.number }} + allow_drafts: "true" + allowlist: "rasmus" + skip_ci: "true" + secrets: + openai_api_key: ${{ secrets.OPENAI_API_KEY }} From 432a9c245e99a51254551aab551f155d34896528 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sun, 26 Oct 2025 14:22:47 +0100 Subject: [PATCH 50/56] Add issue analysis as well --- .github/workflows/analyze.yaml | 19 +++++++++++++++++++ .github/workflows/review.yaml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/analyze.yaml diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml new file mode 100644 index 000000000..05f7f2c1c --- /dev/null +++ b/.github/workflows/analyze.yaml @@ -0,0 +1,19 @@ +name: Perform an issue analysis + +on: + issue_comment: + types: [created] + +permissions: + issues: write + checks: read + contents: read + +jobs: + pipeline: + uses: rasmus/workflow-review/.github/workflows/analyze.yaml@v2 + with: + issue_number: ${{ github.event.issue.number }} + allowlist: "rasmus" + secrets: + openai_api_key: ${{ secrets.OPENAI_API_KEY }} diff --git a/.github/workflows/review.yaml b/.github/workflows/review.yaml index 160d71100..b756490b2 100644 --- a/.github/workflows/review.yaml +++ b/.github/workflows/review.yaml @@ -12,7 +12,7 @@ permissions: jobs: pipeline: - uses: rasmus/workflow-review/.github/workflows/pipeline.yaml@v1 + uses: rasmus/workflow-review/.github/workflows/review.yaml@v2 with: pull_request_number: ${{ github.event.issue.number }} allow_drafts: "true" From 588e4671e1682aa31e92f3a70cab689d3f9713bd Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 12:40:15 +0100 Subject: [PATCH 51/56] Add .NET 10 --- RELEASE_NOTES.md | 4 ++-- .../EventFlow.EntityFramework.Tests.csproj | 14 +++++++++++--- .../EventFlow.EntityFramework.csproj | 8 ++++++-- .../EventFlow.Hangfire.Tests.csproj | 2 +- .../EventFlow.Hangfire/EventFlow.Hangfire.csproj | 2 +- .../EventFlow.MongoDB.Tests.csproj | 2 +- Source/EventFlow.MongoDB/EventFlow.MongoDB.csproj | 2 +- Source/EventFlow.MsSql/EventFlow.MsSql.csproj | 2 +- .../EventFlow.RabbitMQ.Tests.csproj | 2 +- .../EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj | 2 +- .../EventFlow.SQLite.Tests.csproj | 2 +- Source/EventFlow.SQLite/EventFlow.SQLite.csproj | 2 +- .../EventFlow.Sql.Tests/EventFlow.Sql.Tests.csproj | 2 +- Source/EventFlow.Sql/EventFlow.Sql.csproj | 2 +- .../EventFlow.TestHelpers.csproj | 2 +- Source/EventFlow.Tests/EventFlow.Tests.csproj | 2 +- Source/EventFlow/EventFlow.csproj | 9 ++++++++- global.json | 6 ++++++ 18 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 global.json diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0a9e4d017..28cdb6843 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ -### New in 1.2.3 (not released yet) +### New in 1.3.0 (not released yet) -- *Nothing yet...* +- New: Compiled and packaged for .NET 10 ### New in 1.2.2 (released 2025-10-11) diff --git a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj index efaad38f5..5e5870ec1 100644 --- a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj +++ b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net10.0 False @@ -10,12 +10,20 @@ - - + + + + + + + + + + diff --git a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj index 55788830a..dc1564994 100644 --- a/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj +++ b/Source/EventFlow.EntityFramework/EventFlow.EntityFramework.csproj @@ -1,6 +1,6 @@ - net8.0 + net8.0;net10.0 True EventFlow.EntityFramework Frank Ebersoll @@ -19,10 +19,14 @@ - + + + + + diff --git a/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj index fd228330a..95838e918 100644 --- a/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj +++ b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net10.0 False diff --git a/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj b/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj index d051b07f7..446e01679 100644 --- a/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj +++ b/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.Hangfire EventFlow.Hangfire Rasmus Mikkelsen diff --git a/Source/EventFlow.MongoDB.Tests/EventFlow.MongoDB.Tests.csproj b/Source/EventFlow.MongoDB.Tests/EventFlow.MongoDB.Tests.csproj index d55130097..2ba987c7e 100644 --- a/Source/EventFlow.MongoDB.Tests/EventFlow.MongoDB.Tests.csproj +++ b/Source/EventFlow.MongoDB.Tests/EventFlow.MongoDB.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False diff --git a/Source/EventFlow.MongoDB/EventFlow.MongoDB.csproj b/Source/EventFlow.MongoDB/EventFlow.MongoDB.csproj index 8d9b0b49a..ed5506a9f 100644 --- a/Source/EventFlow.MongoDB/EventFlow.MongoDB.csproj +++ b/Source/EventFlow.MongoDB/EventFlow.MongoDB.csproj @@ -1,6 +1,6 @@ - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.MongoDB EventFlow.MongoDB Jan Feyen, Warren Pieterse diff --git a/Source/EventFlow.MsSql/EventFlow.MsSql.csproj b/Source/EventFlow.MsSql/EventFlow.MsSql.csproj index ff686d64c..d6ef77d12 100644 --- a/Source/EventFlow.MsSql/EventFlow.MsSql.csproj +++ b/Source/EventFlow.MsSql/EventFlow.MsSql.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.MsSql Rasmus Mikkelsen Rasmus Mikkelsen diff --git a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj index 6fcfcb625..3a8e5dd91 100644 --- a/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj +++ b/Source/EventFlow.RabbitMQ.Tests/EventFlow.RabbitMQ.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False diff --git a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj index e9e675940..dfc626fb9 100644 --- a/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj +++ b/Source/EventFlow.RabbitMQ/EventFlow.RabbitMQ.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.RabbitMQ EventFlow.RabbitMQ Rasmus Mikkelsen diff --git a/Source/EventFlow.SQLite.Tests/EventFlow.SQLite.Tests.csproj b/Source/EventFlow.SQLite.Tests/EventFlow.SQLite.Tests.csproj index 66a337527..f788cea16 100644 --- a/Source/EventFlow.SQLite.Tests/EventFlow.SQLite.Tests.csproj +++ b/Source/EventFlow.SQLite.Tests/EventFlow.SQLite.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0;net8.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False diff --git a/Source/EventFlow.SQLite/EventFlow.SQLite.csproj b/Source/EventFlow.SQLite/EventFlow.SQLite.csproj index ed9480299..0a8d70635 100644 --- a/Source/EventFlow.SQLite/EventFlow.SQLite.csproj +++ b/Source/EventFlow.SQLite/EventFlow.SQLite.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.SQLite SQLite event store for EventFlow CQRS ES event sourcing SQLite diff --git a/Source/EventFlow.Sql.Tests/EventFlow.Sql.Tests.csproj b/Source/EventFlow.Sql.Tests/EventFlow.Sql.Tests.csproj index bce9cbffd..e5e12244b 100644 --- a/Source/EventFlow.Sql.Tests/EventFlow.Sql.Tests.csproj +++ b/Source/EventFlow.Sql.Tests/EventFlow.Sql.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0;net8.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False diff --git a/Source/EventFlow.Sql/EventFlow.Sql.csproj b/Source/EventFlow.Sql/EventFlow.Sql.csproj index ef9160bd1..8ee2ede7e 100644 --- a/Source/EventFlow.Sql/EventFlow.Sql.csproj +++ b/Source/EventFlow.Sql/EventFlow.Sql.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.Sql Rasmus Mikkelsen Rasmus Mikkelsen diff --git a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj index 6bcd904fb..0de96c53d 100644 --- a/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj +++ b/Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow.TestHelpers Rasmus Mikkelsen Rasmus Mikkelsen diff --git a/Source/EventFlow.Tests/EventFlow.Tests.csproj b/Source/EventFlow.Tests/EventFlow.Tests.csproj index 29f1502c4..05a5f0f88 100644 --- a/Source/EventFlow.Tests/EventFlow.Tests.csproj +++ b/Source/EventFlow.Tests/EventFlow.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0;net8.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 239c2e66a..2ebbb0abf 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -1,6 +1,6 @@  - netstandard2.1;netcoreapp3.1;net6.0;net8.0 + netstandard2.1;netcoreapp3.1;net6.0;net8.0;net10.0 EventFlow Async/await first CQRS+ES and DDD framework for .NET - https://docs.geteventflow.net/ CQRS ES event sourcing @@ -45,4 +45,11 @@ + + + + + + + diff --git a/global.json b/global.json new file mode 100644 index 000000000..512142d2b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} From 722fcb06f17d979aca0d264834972fd60c3d12fa Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 12:44:56 +0100 Subject: [PATCH 52/56] Fix compile error --- RELEASE_NOTES.md | 2 +- Source/EventFlow/EventFlow.csproj | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 28cdb6843..53e84b5e7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ ### New in 1.3.0 (not released yet) -- New: Compiled and packaged for .NET 10 +- New: Compiled and packaged for .NET 10, which has the dependency on `System.Linq.Async` removed ### New in 1.2.2 (released 2025-10-11) diff --git a/Source/EventFlow/EventFlow.csproj b/Source/EventFlow/EventFlow.csproj index 2ebbb0abf..22e862349 100644 --- a/Source/EventFlow/EventFlow.csproj +++ b/Source/EventFlow/EventFlow.csproj @@ -12,11 +12,11 @@ - + @@ -25,6 +25,7 @@ + @@ -32,6 +33,7 @@ + @@ -39,6 +41,7 @@ + From 63546242a0d0f7992219b7ef78ad032793fe8ec2 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 12:51:46 +0100 Subject: [PATCH 53/56] Add missing .NET 8 in tests --- Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj index 95838e918..2124e5385 100644 --- a/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj +++ b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6.0;net10.0 + netcoreapp3.1;net6.0;net8.0;net10.0 False From 86ad14e4c982708c54c95a3cee4dbe0999492c53 Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 12:53:03 +0100 Subject: [PATCH 54/56] Enable forks in workflow review configuration --- .github/workflows/review.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/review.yaml b/.github/workflows/review.yaml index b756490b2..c17ebba5a 100644 --- a/.github/workflows/review.yaml +++ b/.github/workflows/review.yaml @@ -16,6 +16,7 @@ jobs: with: pull_request_number: ${{ github.event.issue.number }} allow_drafts: "true" + allow_forks: "true" allowlist: "rasmus" skip_ci: "true" secrets: From 6f530f376a1ac038c292938870a1525f8814215e Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 12:56:51 +0100 Subject: [PATCH 55/56] Correct dependencies for .NET 10 --- .../EventFlow.EntityFramework.Tests.csproj | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj index 5e5870ec1..02e965084 100644 --- a/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj +++ b/Source/EventFlow.EntityFramework.Tests/EventFlow.EntityFramework.Tests.csproj @@ -6,20 +6,23 @@ - - - + + + + + + From 4c070a03846ef8ecda4166a12d1165e157d766da Mon Sep 17 00:00:00 2001 From: Rasmus Mikkelsen Date: Sat, 6 Dec 2025 14:06:53 +0100 Subject: [PATCH 56/56] Update release notes for versioning changes --- RELEASE_NOTES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 53e84b5e7..e0a517f73 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,6 @@ -### New in 1.3.0 (not released yet) +### New in 1.2.4 (not released yet) + +### New in 1.2.3 (released 2025-12-06) - New: Compiled and packaged for .NET 10, which has the dependency on `System.Linq.Async` removed