From fcbc7893cd61a7e4332a8d2395d2147f89c1c138 Mon Sep 17 00:00:00 2001 From: ATCBot Date: Fri, 2 Jun 2023 09:03:00 +0000 Subject: [PATCH 01/19] Set version to '1.9-preview' --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 9aa04f1..34bd0b1 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.8-preview", + "version": "1.9-preview", "assemblyVersion": { "precision": "revision" }, From 7c562b5fec2ccb9471125c451fa5e82cb755171c Mon Sep 17 00:00:00 2001 From: LarsSkovslund Date: Fri, 30 Jun 2023 08:20:35 +0200 Subject: [PATCH 02/19] Register projection options using full namespace. --- CHANGELOG.md | 4 ++++ .../DependencyInjection/Internal/EventStoreCqrsBuilder.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b3eed8..556e4a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fixed issue where adding two or more projections with the same class name would override their configurations resulting in the "dead" projections. + ## [1.8.3] - 2023-06-02 ### Fixed diff --git a/src/Atc.Cosmos.EventStore.Cqrs/DependencyInjection/Internal/EventStoreCqrsBuilder.cs b/src/Atc.Cosmos.EventStore.Cqrs/DependencyInjection/Internal/EventStoreCqrsBuilder.cs index 51830df..334dae7 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/DependencyInjection/Internal/EventStoreCqrsBuilder.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/DependencyInjection/Internal/EventStoreCqrsBuilder.cs @@ -74,7 +74,7 @@ public IEventStoreCqrsBuilder AddProjectionJob( builder .Services .Configure( - typeof(TProjection).Name, + typeof(TProjection).FullName, options => projectionBuilder.Build(options)); return this; From c8de6f364aab93b3d85b7562f8407b8fe92285ce Mon Sep 17 00:00:00 2001 From: LarsSkovslund Date: Fri, 30 Jun 2023 08:42:32 +0200 Subject: [PATCH 03/19] Add missing change for projection options factory --- .../Projections/ProjectionOptionsFactory.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Projections/ProjectionOptionsFactory.cs b/src/Atc.Cosmos.EventStore.Cqrs/Projections/ProjectionOptionsFactory.cs index 3c9726c..1addc42 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Projections/ProjectionOptionsFactory.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Projections/ProjectionOptionsFactory.cs @@ -8,12 +8,10 @@ internal class ProjectionOptionsFactory : IProjectionOptionsFactory public ProjectionOptionsFactory( IOptionsMonitor namedOptions) - { - this.namedOptions = namedOptions; - } + => this.namedOptions = namedOptions; public IProjectionOptions GetOptions() where TProjection : IProjection => namedOptions.Get( - typeof(TProjection).Name); + typeof(TProjection).FullName); } \ No newline at end of file From c9abe7cf9fa4aa0388181893f1c7a9bc01854fac Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:42:25 +0200 Subject: [PATCH 04/19] Ignore JetBrains IDE files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 96e4eb9..06759d2 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +.idea From 9d1d0d50f6b5c34789e8bfeb0190998a76910388 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:42:42 +0200 Subject: [PATCH 05/19] Introduce CosmosDb constants class --- .../Cosmos/CosmosConstants.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs diff --git a/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs new file mode 100644 index 0000000..7a0c943 --- /dev/null +++ b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs @@ -0,0 +1,16 @@ +namespace Atc.Cosmos.EventStore.Cosmos; + +internal static class CosmosConstants +{ + /// + /// Cosmos DB has a limit of 100 operations per batch and a limit of 2MB per batch. + /// See https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits. + /// + internal const int BatchLimit = 100; + + /// + /// A transaction will consist of at least 2 operations, + /// one for the stream metadata and the other for the events. + /// + internal const int EventLimit = BatchLimit - 1; +} From 643b19436ae8799bb00d220e4afb74fa8ed192f4 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:43:11 +0200 Subject: [PATCH 06/19] Introduce ThrowIfEventLimitExceeded() extension method --- .../Events/EventDocumentExtensions.cs | 19 +++++++++ .../Events/EventLimitExceededException.cs | 13 ++++++ .../Events/EventDocumentExtensionsTests.cs | 42 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs create mode 100644 src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs create mode 100644 test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs diff --git a/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs new file mode 100644 index 0000000..016a270 --- /dev/null +++ b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs @@ -0,0 +1,19 @@ +using Atc.Cosmos.EventStore.Cosmos; + +namespace Atc.Cosmos.EventStore.Events; + +internal static class EventDocumentExtensions +{ + public static IReadOnlyCollection ThrowIfEventLimitExceeded( + this IReadOnlyCollection events) + { + if (events.Count > CosmosConstants.EventLimit) + { + throw new EventLimitExceededException( + events.Count, + CosmosConstants.EventLimit); + } + + return events; + } +} \ No newline at end of file diff --git a/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs b/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs new file mode 100644 index 0000000..283c429 --- /dev/null +++ b/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Atc.Cosmos.EventStore.Events; + +[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "By Design")] +[SuppressMessage("Major Code Smell", "S3925:\"ISerializable\" should be implemented correctly", Justification = "By Design")] +public class EventLimitExceededException : EventStoreException +{ + public EventLimitExceededException(int count, int limit) + : base($"The maximum number of events ({limit}) per command has been exceeded: {count}") + { + } +} \ No newline at end of file diff --git a/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs new file mode 100644 index 0000000..7286d14 --- /dev/null +++ b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs @@ -0,0 +1,42 @@ +using Atc.Cosmos.EventStore.Events; +using FluentAssertions; +using Xunit; + +namespace Atc.Cosmos.EventStore.Tests.Events; + +public class EventDocumentExtensionsTests +{ + [Theory] + [InlineData(10)] + [InlineData(50)] + [InlineData(99)] + public void ThrowIfLimitExceeded_ShouldNotThrow(int eventsCount) + { + var events = new List(); + for (int i = 0; i < eventsCount; i++) + { + events.Add(new object()); + } + + events.ThrowIfEventLimitExceeded().Should().BeEquivalentTo(events); + } + + [Theory] + [InlineData(1000)] + [InlineData(1500)] + public void ThrowIfLimitExceeded_ShouldThrow(int eventsCount) + { + var events = new List(); + for (int i = 0; i < eventsCount; i++) + { + events.Add(new object()); + } + + var ex = Assert.Throws(() => + { + events.ThrowIfEventLimitExceeded(); + }); + + ex.Should().NotBeNull(); + } +} \ No newline at end of file From 56fb8d1ca454ea860c366e1009a415b2e633774b Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:51:03 +0200 Subject: [PATCH 07/19] Use ThrowIfEventLimitExceeded() in StreamBatch --- .../Streams/StreamBatch.cs | 1 + .../StreamBatchTests.cs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs diff --git a/src/Atc.Cosmos.EventStore/Streams/StreamBatch.cs b/src/Atc.Cosmos.EventStore/Streams/StreamBatch.cs index 92285cf..8564285 100644 --- a/src/Atc.Cosmos.EventStore/Streams/StreamBatch.cs +++ b/src/Atc.Cosmos.EventStore/Streams/StreamBatch.cs @@ -10,6 +10,7 @@ public StreamBatch( { Metadata = metadata; Documents = events; + Documents.ThrowIfEventLimitExceeded(); } public StreamMetadata Metadata { get; } diff --git a/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs b/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs new file mode 100644 index 0000000..bf48477 --- /dev/null +++ b/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs @@ -0,0 +1,33 @@ +using Atc.Cosmos.EventStore.Events; +using Atc.Test; +using FluentAssertions; +using Xunit; + +namespace Atc.Cosmos.EventStore.Streams.Tests; + +public class StreamBatchTests +{ + [Theory] + [InlineAutoNSubstituteData] + internal void Constructor_WithValidMetadataAndEvents_ReturnsInstance( + StreamMetadata metadata, + List events) + { + var sut = new StreamBatch(metadata, events); + sut.Should().NotBeNull(); + sut.Metadata.Should().BeEquivalentTo(metadata); + sut.Documents.Should().BeEquivalentTo(events); + } + + [Theory] + [InlineAutoNSubstituteData] + internal void Constructor_WithEvents_ThrowsException_WhenEventLimitIsExceeded( + StreamMetadata metadata, + List events) + { + var action = () => new StreamBatch( + metadata, + Enumerable.Repeat(events[0], 100).ToList()); + action.Should().Throw(); + } +} \ No newline at end of file From 7c36cb1eb0b67c01c9a13dfe622611c3f4ebb360 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:51:25 +0200 Subject: [PATCH 08/19] Use ThrowIfEventLimitExceeded() in EventStoreClient --- src/Atc.Cosmos.EventStore/EventStoreClient.cs | 4 +++- .../EventStoreClientTests.cs | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Atc.Cosmos.EventStore/EventStoreClient.cs b/src/Atc.Cosmos.EventStore/EventStoreClient.cs index fda51cd..76b58f0 100644 --- a/src/Atc.Cosmos.EventStore/EventStoreClient.cs +++ b/src/Atc.Cosmos.EventStore/EventStoreClient.cs @@ -1,3 +1,5 @@ +using Atc.Cosmos.EventStore.Cosmos; +using Atc.Cosmos.EventStore.Events; using Atc.Cosmos.EventStore.Streams; namespace Atc.Cosmos.EventStore; @@ -92,7 +94,7 @@ public Task WriteToStreamAsync( => streamWriter .WriteAsync( streamId, - Arguments.EnsureNoNullValues(events, nameof(events)), + Arguments.EnsureNoNullValues(events, nameof(events)).ThrowIfEventLimitExceeded(), version ?? StreamVersion.Any, options, cancellationToken); diff --git a/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs b/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs index 43cabbb..c63e5b7 100644 --- a/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs @@ -1,4 +1,6 @@ using System.Collections.ObjectModel; +using Atc.Cosmos.EventStore.Cosmos; +using Atc.Cosmos.EventStore.Events; using Atc.Cosmos.EventStore.Streams; using Atc.Test; using AutoFixture.AutoNSubstitute; @@ -83,6 +85,24 @@ internal async ValueTask Should_WriteToStream( .BeEquivalentTo(expected); } + [Theory, AutoNSubstituteData] + internal async Task Should_Throw_EventLimitExceededException( + EventStoreClient sut, + StreamId streamId, + IReadOnlyList events, + CancellationToken cancellationToken) + { + await sut + .Invoking( + c => c.WriteToStreamAsync( + streamId, + Enumerable.Repeat(events[0], CosmosConstants.BatchLimit).ToList(), + StreamVersion.StartOfStream, + cancellationToken: cancellationToken)) + .Should() + .ThrowExactlyAsync(); + } + [Theory, AutoNSubstituteData] internal async Task Should_Throw_When_EventsList_Containes_NullObject( EventStoreClient sut, From 17a50835f336847dc3da212900a8d1a1ca5bb79d Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 19:51:38 +0200 Subject: [PATCH 09/19] Use ThrowIfEventLimitExceeded() in EventBatchProducer --- .../Events/EventBatchProducer.cs | 4 +++- .../Events/EventBatchProducerTests.cs | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Atc.Cosmos.EventStore/Events/EventBatchProducer.cs b/src/Atc.Cosmos.EventStore/Events/EventBatchProducer.cs index 6c7e7c2..d4c48e2 100644 --- a/src/Atc.Cosmos.EventStore/Events/EventBatchProducer.cs +++ b/src/Atc.Cosmos.EventStore/Events/EventBatchProducer.cs @@ -1,3 +1,4 @@ +using Atc.Cosmos.EventStore.Cosmos; using Atc.Cosmos.EventStore.Streams; namespace Atc.Cosmos.EventStore.Events; @@ -31,7 +32,8 @@ public StreamBatch FromEvents( options?.CorrelationId, options?.CausationId, timestamp)) - .ToArray(); + .ToArray() + .ThrowIfEventLimitExceeded(); return new StreamBatch( new StreamMetadata( diff --git a/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs b/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs index 9836630..3e440dc 100644 --- a/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs @@ -213,4 +213,19 @@ public void Should_Have_Metadata_State_Active() .State .Should() .Be(StreamState.Active); + + [Theory, AutoNSubstituteData] + internal void Throws_If_Limit_Exceeded( + IReadOnlyCollection events, + EventBatchProducer sut) + { + sut.Invoking( + x => x + .FromEvents( + Enumerable.Repeat(events, 100).ToList(), + metadata, + options)) + .Should() + .ThrowExactly(); + } } \ No newline at end of file From 3a6600c13d323e10322bbf711cff8c4bb2b6addd Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Thu, 22 Jun 2023 21:13:35 +0200 Subject: [PATCH 10/19] Use ThrowIfEventLimitExceeded() in CommandContext --- .../Commands/CommandContext.cs | 6 ++- .../CommandContextTests.cs | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs b/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs index 0a0c186..9495f7e 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs @@ -1,4 +1,5 @@ using Atc.Cosmos.EventStore.Cqrs.Testing; +using Atc.Cosmos.EventStore.Events; namespace Atc.Cosmos.EventStore.Cqrs.Commands; @@ -10,7 +11,10 @@ public IReadOnlyCollection Events => appliedEvents; public void AddEvent(object evt) - => appliedEvents.Add(evt); + { + appliedEvents.Add(evt); + appliedEvents.ThrowIfEventLimitExceeded(); + } public object? ResponseObject { get; set; } } \ No newline at end of file diff --git a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs new file mode 100644 index 0000000..a210a12 --- /dev/null +++ b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs @@ -0,0 +1,41 @@ +using Atc.Cosmos.EventStore.Events; +using Atc.Test; +using FluentAssertions; +using Xunit; + +namespace Atc.Cosmos.EventStore.Cqrs.Commands.Tests; + +public class CommandContextTests +{ + [Theory, AutoNSubstituteData] + internal void EventsShouldReturnEmptyList_WhenNoEventsAdded( + CommandContext sut) + { + var actual = sut.Events; + actual.Should().BeEmpty(); + } + + [Theory, AutoNSubstituteData] + internal void AddEventShouldAddEventToAppliedEventsList( + CommandContext sut, + object eventData) + { + sut.AddEvent(eventData); + sut.Events.Should().Contain(eventData); + } + + [Theory, AutoNSubstituteData] + internal void AddEventShouldThrowException_WhenAppliedEventsCountExceedsLimit( + CommandContext sut, + object eventData) + { + for (int i = 0; i < 99; i++) + { + sut.AddEvent(eventData); + } + + sut.Invoking(c => c.AddEvent(new object())) + .Should() + .Throw(); + } +} From 2b8fdd8d2bc99637dd71172d73422ac91bef4c5d Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 23 Jun 2023 09:13:51 +0200 Subject: [PATCH 11/19] Make CosmosConstants public --- src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs index 7a0c943..d7044f0 100644 --- a/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs +++ b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs @@ -1,16 +1,17 @@ namespace Atc.Cosmos.EventStore.Cosmos; -internal static class CosmosConstants +public static class CosmosConstants { /// /// Cosmos DB has a limit of 100 operations per batch and a limit of 2MB per batch. /// See https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-request-limits. /// - internal const int BatchLimit = 100; + public const int BatchLimit = 100; /// /// A transaction will consist of at least 2 operations, /// one for the stream metadata and the other for the events. + /// Since there is no efficient way to determine the size of the total payload, /// - internal const int EventLimit = BatchLimit - 1; + public const int EventLimit = BatchLimit / 2; } From ea3613b40f1f7830284487ad728855d406202a05 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 23 Jun 2023 09:16:10 +0200 Subject: [PATCH 12/19] Update failing tests --- test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs | 2 +- .../Events/EventDocumentExtensionsTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs index a210a12..24c332d 100644 --- a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs +++ b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs @@ -29,7 +29,7 @@ internal void AddEventShouldThrowException_WhenAppliedEventsCountExceedsLimit( CommandContext sut, object eventData) { - for (int i = 0; i < 99; i++) + for (int i = 0; i < 50; i++) { sut.AddEvent(eventData); } diff --git a/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs index 7286d14..eea6304 100644 --- a/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs @@ -9,7 +9,6 @@ public class EventDocumentExtensionsTests [Theory] [InlineData(10)] [InlineData(50)] - [InlineData(99)] public void ThrowIfLimitExceeded_ShouldNotThrow(int eventsCount) { var events = new List(); From 2c67d17cd7dee00507d919e95bb515a5b4ff5ce2 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 23 Jun 2023 10:29:03 +0200 Subject: [PATCH 13/19] Add limit parameter to ThrowIfEventLimitExceeded() --- .../Events/EventDocumentExtensions.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs index 016a270..a735a72 100644 --- a/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs +++ b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs @@ -1,17 +1,16 @@ -using Atc.Cosmos.EventStore.Cosmos; +using Atc.Cosmos.EventStore.Cosmos; namespace Atc.Cosmos.EventStore.Events; internal static class EventDocumentExtensions { public static IReadOnlyCollection ThrowIfEventLimitExceeded( - this IReadOnlyCollection events) + this IReadOnlyCollection events, + int limit = CosmosConstants.EventLimit) { - if (events.Count > CosmosConstants.EventLimit) + if (events.Count > limit) { - throw new EventLimitExceededException( - events.Count, - CosmosConstants.EventLimit); + throw new EventLimitExceededException(events.Count, limit); } return events; From 602eede32f103be1a36d4ed1f1edcdd702c576f3 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 23 Jun 2023 10:29:31 +0200 Subject: [PATCH 14/19] Set event limit per command to 10 --- src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs | 4 +++- test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs b/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs index 9495f7e..2f39a51 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Commands/CommandContext.cs @@ -5,6 +5,8 @@ namespace Atc.Cosmos.EventStore.Cqrs.Commands; internal class CommandContext : ICommandContext, ICommandContextInspector { + public const int EventLimit = 10; + private readonly List appliedEvents = new(); public IReadOnlyCollection Events @@ -13,7 +15,7 @@ public IReadOnlyCollection Events public void AddEvent(object evt) { appliedEvents.Add(evt); - appliedEvents.ThrowIfEventLimitExceeded(); + appliedEvents.ThrowIfEventLimitExceeded(EventLimit); } public object? ResponseObject { get; set; } diff --git a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs index 24c332d..dd02e63 100644 --- a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs +++ b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs @@ -29,7 +29,7 @@ internal void AddEventShouldThrowException_WhenAppliedEventsCountExceedsLimit( CommandContext sut, object eventData) { - for (int i = 0; i < 50; i++) + for (int i = 0; i < CommandContext.EventLimit; i++) { sut.AddEvent(eventData); } From 072510aaf3722d3546dd13c0d49999a4f1dea087 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Fri, 23 Jun 2023 11:25:47 +0200 Subject: [PATCH 15/19] Resolve analyzer warnings --- src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs index d7044f0..cacef54 100644 --- a/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs +++ b/src/Atc.Cosmos.EventStore/Cosmos/CosmosConstants.cs @@ -11,7 +11,8 @@ public static class CosmosConstants /// /// A transaction will consist of at least 2 operations, /// one for the stream metadata and the other for the events. - /// Since there is no efficient way to determine the size of the total payload, + /// Since there is no efficient way to determine the size of the total payload, + /// we assume the worst case scenario as set the limit to half of the batch limit. /// public const int EventLimit = BatchLimit / 2; } From 875acb282c7e68150a1285066354e1b52134bbd5 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 3 Jul 2023 11:30:16 +0200 Subject: [PATCH 16/19] Remove EventLimitExceededException and use InvalidOperationException instead --- .../Events/EventDocumentExtensions.cs | 3 ++- .../Events/EventLimitExceededException.cs | 13 ------------- .../CommandContextTests.cs | 2 +- .../EventStoreClientTests.cs | 4 ++-- .../Events/EventBatchProducerTests.cs | 2 +- .../Events/EventDocumentExtensionsTests.cs | 2 +- .../Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs | 2 +- 7 files changed, 8 insertions(+), 20 deletions(-) delete mode 100644 src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs diff --git a/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs index a735a72..0ad10bb 100644 --- a/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs +++ b/src/Atc.Cosmos.EventStore/Events/EventDocumentExtensions.cs @@ -10,7 +10,8 @@ public static IReadOnlyCollection ThrowIfEventLimitExceeded( { if (events.Count > limit) { - throw new EventLimitExceededException(events.Count, limit); + throw new InvalidOperationException( + $"The maximum number of events ({limit}) per command has been exceeded: {events.Count}"); } return events; diff --git a/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs b/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs deleted file mode 100644 index 283c429..0000000 --- a/src/Atc.Cosmos.EventStore/Events/EventLimitExceededException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Atc.Cosmos.EventStore.Events; - -[SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "By Design")] -[SuppressMessage("Major Code Smell", "S3925:\"ISerializable\" should be implemented correctly", Justification = "By Design")] -public class EventLimitExceededException : EventStoreException -{ - public EventLimitExceededException(int count, int limit) - : base($"The maximum number of events ({limit}) per command has been exceeded: {count}") - { - } -} \ No newline at end of file diff --git a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs index dd02e63..859ee09 100644 --- a/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs +++ b/test/Atc.Cosmos.EventStore.Cqrs.Tests/CommandContextTests.cs @@ -36,6 +36,6 @@ internal void AddEventShouldThrowException_WhenAppliedEventsCountExceedsLimit( sut.Invoking(c => c.AddEvent(new object())) .Should() - .Throw(); + .Throw(); } } diff --git a/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs b/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs index c63e5b7..bcc3b13 100644 --- a/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/EventStoreClientTests.cs @@ -86,7 +86,7 @@ internal async ValueTask Should_WriteToStream( } [Theory, AutoNSubstituteData] - internal async Task Should_Throw_EventLimitExceededException( + internal async Task Should_Throw_InvalidOperationException( EventStoreClient sut, StreamId streamId, IReadOnlyList events, @@ -100,7 +100,7 @@ await sut StreamVersion.StartOfStream, cancellationToken: cancellationToken)) .Should() - .ThrowExactlyAsync(); + .ThrowExactlyAsync(); } [Theory, AutoNSubstituteData] diff --git a/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs b/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs index 3e440dc..e54f94f 100644 --- a/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/Events/EventBatchProducerTests.cs @@ -226,6 +226,6 @@ internal void Throws_If_Limit_Exceeded( metadata, options)) .Should() - .ThrowExactly(); + .ThrowExactly(); } } \ No newline at end of file diff --git a/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs index eea6304..8817ff0 100644 --- a/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/Events/EventDocumentExtensionsTests.cs @@ -31,7 +31,7 @@ public void ThrowIfLimitExceeded_ShouldThrow(int eventsCount) events.Add(new object()); } - var ex = Assert.Throws(() => + var ex = Assert.Throws(() => { events.ThrowIfEventLimitExceeded(); }); diff --git a/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs b/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs index bf48477..a1bc4ca 100644 --- a/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs +++ b/test/Atc.Cosmos.EventStore.Tests/StreamBatchTests.cs @@ -28,6 +28,6 @@ internal void Constructor_WithEvents_ThrowsException_WhenEventLimitIsExceeded( var action = () => new StreamBatch( metadata, Enumerable.Repeat(events[0], 100).ToList()); - action.Should().Throw(); + action.Should().Throw(); } } \ No newline at end of file From 2a88f627c61885130284decf65ff7c4e67a79c17 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 3 Jul 2023 11:45:08 +0200 Subject: [PATCH 17/19] Update changelog with event store limits --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 556e4a9..29321b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Introduce hard limits to the number of events the system can accept per operation. + - A maximum of 10 events per command context (CQRS) + - A maximum of 50 events per stream batch (Event Store) + ### Fixed - Fixed issue where adding two or more projections with the same class name would override their configurations resulting in the "dead" projections. From f5f7eea55dc33ee440732780dc73008d9e89dd2d Mon Sep 17 00:00:00 2001 From: LarsSkovslund Date: Mon, 3 Jul 2023 12:45:15 +0200 Subject: [PATCH 18/19] Introduce async events factory for given, when, then testing --- CHANGELOG.md | 3 ++- .../Testing/CommandHandlerExtensions.cs | 9 ++++++++- .../Testing/CommandHandlerTester.cs | 16 ++++++++++------ .../Testing/ICommandGiven.cs | 3 ++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29321b4..5faa6c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Introduce hard limits to the number of events the system can accept per operation. +- Introduced options to provide events async when testing commands using `ICommandGiven`, `ICommandWhen` and `ICommandThen`. +- Introduce hard limits to the number of events the system can accept per operation. - A maximum of 10 events per command context (CQRS) - A maximum of 50 events per stream batch (Event Store) diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerExtensions.cs b/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerExtensions.cs index 5173185..b1b6e94 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerExtensions.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerExtensions.cs @@ -7,5 +7,12 @@ public static ICommandWhen GivenStreamContainingEvents( params object[] events) where TCommand : ICommand => new CommandHandlerTester(handler) - .GivenEvents(events); + .GivenEvents(() => Task.FromResult(events)); + + public static ICommandWhen GivenStreamContainingEvents( + this ICommandHandler handler, + Func> eventsFactory) + where TCommand : ICommand + => new CommandHandlerTester(handler) + .GivenEvents(eventsFactory); } \ No newline at end of file diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerTester.cs b/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerTester.cs index ac06503..f9f6c25 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerTester.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Testing/CommandHandlerTester.cs @@ -27,7 +27,7 @@ public ValueTask ApplyEventAsync( private readonly ICommandHandler handler; private readonly TestMetadata handlerMetadata; - private readonly List events; + private Func> eventsProvider = EmptyEventStreamAsync; private TCommand? command; public CommandHandlerTester( @@ -35,17 +35,18 @@ public CommandHandlerTester( { this.handler = handler; this.handlerMetadata = new TestMetadata(handler); - this.events = new List(); } - public ICommandWhen GivenEvents(params object[] events) + public ICommandWhen GivenEvents( + Func> eventsFactory) { - this.events.AddRange(events); + this.eventsProvider = eventsFactory; return this; } - public ICommandThen WhenExecuting(TCommand command) + public ICommandThen WhenExecuting( + TCommand command) { this.command = command; @@ -96,10 +97,13 @@ await ExecuteAsync(cancellationToken) throw new InvalidOperationException($"{typeof(TException).Name} not thrown"); } + private static Task EmptyEventStreamAsync() + => Task.FromResult(Array.Empty()); + private async Task ExecuteAsync(CancellationToken cancellationToken) { var version = 1; - foreach (var evt in events) + foreach (var evt in await eventsProvider().ConfigureAwait(false)) { await handlerMetadata .ApplyEventAsync( diff --git a/src/Atc.Cosmos.EventStore.Cqrs/Testing/ICommandGiven.cs b/src/Atc.Cosmos.EventStore.Cqrs/Testing/ICommandGiven.cs index e39b546..41fc7f4 100644 --- a/src/Atc.Cosmos.EventStore.Cqrs/Testing/ICommandGiven.cs +++ b/src/Atc.Cosmos.EventStore.Cqrs/Testing/ICommandGiven.cs @@ -3,5 +3,6 @@ namespace Atc.Cosmos.EventStore.Cqrs.Testing; public interface ICommandGiven where TCommand : ICommand { - ICommandWhen GivenEvents(params object[] events); + ICommandWhen GivenEvents( + Func> eventsFactory); } \ No newline at end of file From 791850202c2feb50fedc0e8a79508cf49d368938 Mon Sep 17 00:00:00 2001 From: ATCBot Date: Mon, 3 Jul 2023 10:51:12 +0000 Subject: [PATCH 19/19] Set version to '1.9' --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 34bd0b1..33f7f5e 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.9-preview", + "version": "1.9", "assemblyVersion": { "precision": "revision" },