From 58b05ff327420b2c0abf839d7fc488ce525b0ad8 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Thu, 20 Jun 2024 17:30:08 +0200 Subject: [PATCH] Simplify command services (#350) * Simplified command services * Removed stateless aggregate * New sample with custom result for HTTP API (controller) * Added a read model query to one sample --- Directory.Packages.props | 202 ++++++++------- .../esdb/Bookings.Domain/Bookings/Booking.cs | 51 +--- .../Bookings.Domain/Bookings/BookingEvents.cs | 38 +-- .../Bookings.Domain/Bookings/BookingState.cs | 11 +- samples/esdb/Bookings.Domain/Money.cs | 2 +- .../Application/CommandApi.cs | 4 +- .../esdb/Bookings.Payments/Domain/Money.cs | 2 +- .../esdb/Bookings.Payments/Domain/Payment.cs | 7 +- samples/esdb/Bookings.Payments/Program.cs | 3 +- .../esdb/Bookings.Payments/Registrations.cs | 6 +- .../Application/BookingsQueryService.cs | 9 + .../Application/Queries/BookingDocument.cs | 12 +- .../Queries/BookingStateProjection.cs | 17 +- .../Application/Queries/MyBookings.cs | 10 +- samples/esdb/Bookings/Bookings.csproj | 1 + .../Bookings/HttpApi/Bookings/CommandApi.cs | 11 +- .../Bookings/CommandApiWithCustomResult.cs | 67 +++++ .../Bookings/HttpApi/Bookings/QueryApi.cs | 8 +- samples/esdb/Bookings/Infrastructure/Mongo.cs | 6 +- samples/esdb/Bookings/Integration/Payments.cs | 17 +- samples/esdb/Bookings/Program.cs | 27 +- samples/esdb/Bookings/Registrations.cs | 16 +- .../Bookings.Domain/Bookings.Domain.csproj | 4 + .../Bookings.Domain/Bookings/Booking.cs | 76 ------ .../Bookings.Domain/Bookings/BookingEvents.cs | 37 --- .../Bookings.Domain/Bookings/BookingId.cs | 5 - .../Bookings.Domain/Bookings/BookingState.cs | 46 ---- .../postgres/Bookings.Domain/DomainModule.cs | 11 - samples/postgres/Bookings.Domain/Money.cs | 29 --- samples/postgres/Bookings.Domain/RoomId.cs | 5 - samples/postgres/Bookings.Domain/Services.cs | 7 - .../postgres/Bookings.Domain/StayPeriod.cs | 17 -- .../Application/CommandApi.cs | 14 - .../Application/CommandService.cs | 40 --- .../Bookings.Payments.csproj | 80 +++--- .../Bookings.Payments/Domain/Money.cs | 30 --- .../Bookings.Payments/Domain/Payment.cs | 27 -- .../Bookings.Payments/Domain/PaymentEvents.cs | 10 - .../Bookings.Payments/Integration/Payments.cs | 10 +- samples/postgres/Bookings.Payments/Program.cs | 2 +- .../Bookings.Payments/Registrations.cs | 7 +- .../Application/BookingsCommandService.cs | 10 +- .../Bookings/HttpApi/Bookings/CommandApi.cs | 10 +- .../Bookings/HttpApi/Bookings/QueryApi.cs | 8 +- .../postgres/Bookings/Integration/Payments.cs | 4 +- samples/postgres/Bookings/Registrations.cs | 2 +- .../AggregateService/CommandHandlerBuilder.cs | 23 +- .../AggregateService/CommandHandlersMap.cs | 24 +- .../CommandHandlingDelegateExtensions.cs | 6 +- .../AggregateService/CommandService.Async.cs | 34 +-- .../AggregateService/CommandService.Sync.cs | 22 +- .../AggregateService/CommandService.cs | 20 +- .../CommandServiceDelegates.cs | 10 +- .../Diagnostics/CommandServiceActivity.cs | 31 +-- .../Diagnostics/TracedCommandService.cs | 83 +++--- .../Diagnostics/TracedFunctionalService.cs | 26 +- .../FunctionalService/FuncHandlersMap.cs | 4 +- .../FunctionalCommandService.cs | 14 +- .../Eventuous.Application/ICommandService.cs | 14 +- .../ThrowingCommandService.cs | 22 +- src/Core/src/Eventuous.Domain/Aggregate.cs | 31 +-- .../Eventuous.Persistence/AggregateFactory.cs | 17 +- .../AggregateStore/AggregateStore.cs | 24 +- .../AggregateStoreExceptions.cs | 16 +- .../AggregateStoreExtensions.cs | 4 +- .../AggregateStoreWithArchive.cs | 24 +- .../AggregateStore/IAggregateStore.cs | 27 +- .../Diagnostics/PersistenceEventSource.cs | 4 +- .../EventStore/StoreFunctions.cs | 13 +- .../StreamNameFactory.cs | 3 - .../Eventuous.Persistence/StreamNameMap.cs | 10 +- .../src/Eventuous.Shared/Store/StreamName.cs | 18 +- src/Core/src/Eventuous.Shared/Tools/Ensure.cs | 24 +- .../Channels/ChannelWorkers.cs | 30 +-- .../Checkpoints/CheckpointCommitHandler.cs | 2 +- .../Checkpoints/MeasuredCheckpointStore.cs | 9 +- .../Context/AsyncConsumeContext.cs | 3 +- .../Context/ContextResultExtensions.cs | 28 +- .../Diagnostics/CheckpointCommitMetrics.cs | 8 +- .../Diagnostics/SubscriptionActivity.cs | 8 +- .../Diagnostics/SubscriptionHealth.cs | 6 +- .../Diagnostics/SubscriptionMetrics.cs | 15 +- .../Diagnostics/SubscriptionsEventSource.cs | 52 ++-- .../EventSubscription.cs | 23 +- .../src/Eventuous.Subscriptions/Exceptions.cs | 15 +- .../Filters/AsyncHandlingFilter.cs | 7 +- .../Filters/ConsumePipe.cs | 3 +- .../Filters/ConsumerFilter.cs | 3 +- .../Filters/IConsumeFilter.cs | 14 +- .../Filters/PartitioningFilter.cs | 13 +- .../Handlers/BaseEventHandler.cs | 3 +- .../Handlers/EventHandler.cs | 2 + .../Handlers/EventHandlingResult.cs | 12 +- .../Handlers/TracedEventHandler.cs | 8 +- .../Logging/CheckpointLogging.cs | 3 + .../Logging/InternalLogger.cs | 6 +- .../Eventuous.Subscriptions/Logging/Logger.cs | 3 +- .../Logging/SubscriptionLogging.cs | 36 ++- .../Registrations/NamedRegistrations.cs | 29 +-- .../Registrations/ParameterMap.cs | 23 -- .../Registrations/SubscriptionBuilder.cs | 12 +- .../SubscriptionBuilderExtensions.cs | 2 +- .../SubscriptionHostedService.cs | 10 +- .../BookingFuncService.cs | 17 +- .../CommandServiceTests.cs | 13 +- .../FunctionalServiceTests.cs | 11 +- .../StateWithIdTests.cs | 5 +- .../Eventuous.Tests.Persistence.Base.csproj | 3 +- .../Store/Append.cs | 2 +- .../Store/Read.cs | 11 +- .../Traits/CategoryAttribute.cs | 5 + .../Traits/CategoryDiscoverer.cs | 7 +- .../Fixtures/TestEventHandler.cs | 1 + .../Fixtures/TracedHandler.cs | 12 +- .../SubscribeToAll.cs | 2 +- .../SubscribeToStream.cs | 13 +- .../AutofixtureExtensions.cs | 6 +- .../ConsumePipeTests.cs | 18 +- .../RegistrationTests.cs | 11 +- .../SequenceTests.cs | 43 ++- .../OperateOnAggregateWithId.cs | 27 ++ .../Aggregates/OperateOnExistingSpec.cs | 28 +- .../Aggregates/TwoAggregateOpsSpec.cs | 44 ++-- .../Eventuous.Tests/Eventuous.Tests.csproj | 1 + .../test/Eventuous.Tests/ForgotToSetId.cs | 8 +- .../test/Eventuous.Tests/StoringEvents.cs | 21 +- .../StoringEventsWithCustomStream.cs | 36 +-- .../Eventuous.Tests/TypeRegistrationTests.cs | 7 +- .../LoggingEventListener.cs | 22 +- .../TracerProviderBuilderExtensions.cs | 11 +- .../Fakes/TestExporter.cs | 9 +- .../MetricsSubscriptionFixtureBase.cs | 13 +- .../MetricsTests.cs | 4 +- .../Producers/EventStoreProduceOptions.cs | 3 + .../Producers/EventStoreProducer.cs | 5 +- .../StreamRevisionExtensions.cs | 9 +- .../AllPersistentSubscription.cs | 9 +- .../AllStreamSubscriptionMeasure.cs | 15 +- .../Diagnostics/StreamSubscriptionMeasure.cs | 3 + .../Subscriptions/EsdbMappings.cs | 3 + .../Subscriptions/EventStoreExtensions.cs | 6 +- .../AllPersistentSubscriptionOptions.cs | 3 + .../Options/AllStreamSubscriptionOptions.cs | 3 + .../Options/CatchUpSubscriptionOptions.cs | 3 + .../Options/EventStoreSubscriptionOptions.cs | 3 + .../Options/PersistentSubscriptionOptions.cs | 5 +- .../StreamPersistentSubscriptionOptions.cs | 3 + .../Options/StreamSubscriptionOptions.cs | 3 + .../StreamPersistentSubscription.cs | 45 ++-- .../Subscriptions/StreamSubscription.cs | 10 +- .../SubscriptionBuilderExtensions.cs | 3 + .../Fixtures/EsdbContainer.cs | 4 +- .../Fixtures/Serializer.cs | 10 - .../Fixtures/StoreFixture.cs | 3 +- .../Fixtures/TestCheckpointStore.cs | 6 +- .../ProducerTracesTests.cs | 4 +- .../Store/AggregateStoreTests.cs | 30 +-- .../LegacySubscriptionFixture.cs | 21 +- .../PersistentPublishAndSubscribeManyTests.cs | 2 +- .../PersistentSubscriptionFixture.cs | 17 +- ...PublishAndSubscribeManyPartitionedTests.cs | 2 +- .../PublishAndSubscribeManyTests.cs | 7 +- .../PublishAndSubscribeOneTests.cs | 2 +- .../StreamSubscriptionDeletedEventsTests.cs | 61 +---- .../StreamSubscriptionWithLinksTests.cs | 5 +- .../Subscriptions/SubscriptionFixture.cs | 3 +- .../src/Eventuous.Spyglass/InsidePeek.cs | 4 +- .../Diagnostics/ExtensionsEventSource.cs | 10 +- .../Http/CommandHttpApiBase.cs | 41 +-- .../Http/CommandHttpApiBaseFunc.cs | 55 ---- .../Http/CommandServiceRouteBuilder.cs | 27 +- .../Http/HttpCommandAttribute.cs | 49 +--- .../Http/HttpCommandMapping.cs | 245 ++++++------------ .../Http/HttpCommandMappingExt.cs | 56 +--- .../Http/ResultExtensions.cs | 14 +- .../Http/RouteHandlerBuilderExt.cs | 9 +- .../Registrations/AggregateFactory.cs | 9 +- .../Registrations/Services.cs | 90 +------ .../Eventuous.Sut.AspNetCore/BookingApi.cs | 11 +- .../Eventuous.Sut.AspNetCore/BookingResult.cs | 10 - .../BookingService.cs | 2 +- .../test/Eventuous.Sut.AspNetCore/Program.cs | 2 +- ...teContractToCommandExplicitly.verified.txt | 28 ++ ...CommandExplicitlyWithoutRoute.verified.txt | 1 + ...lyWithoutRouteWithGenericAttr.verified.txt | 1 + ...mandsTests.MapEnrichedCommand.verified.txt | 1 + .../AggregateCommandsTests.cs | 48 +--- .../ControllerTests.cs | 8 +- ...ts.CallDiscoveredCommandRoute.verified.txt | 1 + .../DiscoveredCommandsTests.cs | 2 +- .../Fixture/Commands.cs | 4 +- .../Fixture/ServerFixture.cs | 9 +- .../Fixture/TestAggregate.cs | 6 +- .../AggregateFactoryRegistrationTests.cs | 12 +- .../MongoCheckpointStore.cs | 69 ++--- .../Producers/RabbitMqProduceOptions.cs | 3 +- .../Producers/RabbitMqProducer.cs | 2 +- .../Shared/RabbitMqExchangeOptions.cs | 1 - .../Subscriptions/RabbitMqSubscription.cs | 42 +-- .../RabbitMqSubscriptionOptions.cs | 2 +- .../Subscriptions/Timestamp.cs | 3 +- .../RabbitMqFixture.cs | 10 +- .../SubscriptionSpec.cs | 13 +- .../AggregateFactoryExtensions.cs | 4 +- .../src/Eventuous.Testing/AggregateSpec.cs | 45 +++- .../Eventuous.Testing/AggregateWithIdSpec.cs | 24 ++ .../Eventuous.Testing.csproj | 3 + .../Eventuous.Testing/InMemoryEventStore.cs | 18 -- test/Eventuous.Sut.App/BookingService.cs | 3 +- test/Eventuous.Sut.Domain/Booking.cs | 6 +- test/Eventuous.Sut.Domain/BookingEvents.cs | 23 +- test/Eventuous.Sut.Domain/BookingState.cs | 28 +- test/Eventuous.TestHelpers/Logging.cs | 8 +- test/Eventuous.TestHelpers/RecordedTrace.cs | 8 +- test/Eventuous.TestHelpers/TestHelper.cs | 10 +- 215 files changed, 1447 insertions(+), 2309 deletions(-) create mode 100644 samples/esdb/Bookings/Application/BookingsQueryService.cs create mode 100644 samples/esdb/Bookings/HttpApi/Bookings/CommandApiWithCustomResult.cs delete mode 100644 samples/postgres/Bookings.Domain/Bookings/Booking.cs delete mode 100644 samples/postgres/Bookings.Domain/Bookings/BookingEvents.cs delete mode 100644 samples/postgres/Bookings.Domain/Bookings/BookingId.cs delete mode 100644 samples/postgres/Bookings.Domain/Bookings/BookingState.cs delete mode 100644 samples/postgres/Bookings.Domain/DomainModule.cs delete mode 100644 samples/postgres/Bookings.Domain/Money.cs delete mode 100644 samples/postgres/Bookings.Domain/RoomId.cs delete mode 100644 samples/postgres/Bookings.Domain/Services.cs delete mode 100644 samples/postgres/Bookings.Domain/StayPeriod.cs delete mode 100644 samples/postgres/Bookings.Payments/Application/CommandApi.cs delete mode 100644 samples/postgres/Bookings.Payments/Application/CommandService.cs delete mode 100644 samples/postgres/Bookings.Payments/Domain/Money.cs delete mode 100644 samples/postgres/Bookings.Payments/Domain/Payment.cs delete mode 100644 samples/postgres/Bookings.Payments/Domain/PaymentEvents.cs delete mode 100644 src/Core/src/Eventuous.Subscriptions/Registrations/ParameterMap.cs create mode 100644 src/Core/test/Eventuous.Tests/AggregateWithId/OperateOnAggregateWithId.cs delete mode 100644 src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/Serializer.cs delete mode 100644 src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBaseFunc.cs delete mode 100644 src/Extensions/test/Eventuous.Sut.AspNetCore/BookingResult.cs create mode 100644 src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitly.verified.txt create mode 100644 src/Testing/src/Eventuous.Testing/AggregateWithIdSpec.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 4eeeb1bc..af2b1b4d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,102 +1,104 @@ - - true - - - 8.0 - 8.0.6 - - - [6.0.5,7) - 2.3.0 - - - 8.0.6 - 3.0.0 - - - 3.9.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + 8.0 + 8.0.6 + + + [6.0.5,7) + 2.3.0 + + + 8.0.6 + 3.0.0 + + + 3.9.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/esdb/Bookings.Domain/Bookings/Booking.cs b/samples/esdb/Bookings.Domain/Bookings/Booking.cs index 2b437950..a3554689 100644 --- a/samples/esdb/Bookings.Domain/Bookings/Booking.cs +++ b/samples/esdb/Bookings.Domain/Bookings/Booking.cs @@ -6,58 +6,32 @@ namespace Bookings.Domain.Bookings; public class Booking : Aggregate { public async Task BookRoom( - string guestId, - RoomId roomId, - StayPeriod period, - Money price, - Money prepaid, - DateTimeOffset bookedAt, - IsRoomAvailable isRoomAvailable - ) { + string guestId, + RoomId roomId, + StayPeriod period, + Money price, + Money prepaid, + DateTimeOffset bookedAt, + IsRoomAvailable isRoomAvailable + ) { EnsureDoesntExist(); await EnsureRoomAvailable(roomId, period, isRoomAvailable); var outstanding = price - prepaid; - Apply( - new V1.RoomBooked( - guestId, - roomId, - period.CheckIn, - period.CheckOut, - price.Amount, - prepaid.Amount, - outstanding.Amount, - price.Currency, - bookedAt - ) - ); + Apply(new V1.RoomBooked(guestId, roomId, period.CheckIn, period.CheckOut, price.Amount, prepaid.Amount, outstanding.Amount, price.Currency, bookedAt)); MarkFullyPaidIfNecessary(bookedAt); } - public void RecordPayment( - Money paid, - string paymentId, - string paidBy, - DateTimeOffset paidAt - ) { + public void RecordPayment(Money paid, string paymentId, string paidBy, DateTimeOffset paidAt) { EnsureExists(); if (State.HasPaymentBeenRegistered(paymentId)) return; var outstanding = State.Outstanding - paid; - Apply( - new V1.PaymentRecorded( - paid.Amount, - outstanding.Amount, - paid.Currency, - paymentId, - paidBy, - paidAt - ) - ); + Apply(new V1.PaymentRecorded(paid.Amount, outstanding.Amount, paid.Currency, paymentId, paidBy, paidAt)); MarkFullyPaidIfNecessary(paidAt); MarkOverpaid(paidAt); @@ -75,6 +49,7 @@ void MarkOverpaid(DateTimeOffset when) { static async Task EnsureRoomAvailable(RoomId roomId, StayPeriod period, IsRoomAvailable isRoomAvailable) { var roomAvailable = await isRoomAvailable(roomId, period); + if (!roomAvailable) throw new DomainException("Room not available"); } -} \ No newline at end of file +} diff --git a/samples/esdb/Bookings.Domain/Bookings/BookingEvents.cs b/samples/esdb/Bookings.Domain/Bookings/BookingEvents.cs index c4e34caf..8a82e98f 100644 --- a/samples/esdb/Bookings.Domain/Bookings/BookingEvents.cs +++ b/samples/esdb/Bookings.Domain/Bookings/BookingEvents.cs @@ -1,32 +1,34 @@ using Eventuous; using NodaTime; +// ReSharper disable NotAccessedPositionalProperty.Global + namespace Bookings.Domain.Bookings; public static class BookingEvents { public static class V1 { [EventType("V1.RoomBooked")] public record RoomBooked( - string GuestId, - string RoomId, - LocalDate CheckInDate, - LocalDate CheckOutDate, - float BookingPrice, - float PrepaidAmount, - float OutstandingAmount, - string Currency, - DateTimeOffset BookingDate - ); + string GuestId, + string RoomId, + LocalDate CheckInDate, + LocalDate CheckOutDate, + float BookingPrice, + float PrepaidAmount, + float OutstandingAmount, + string Currency, + DateTimeOffset BookingDate + ); [EventType("V1.PaymentRecorded")] public record PaymentRecorded( - float PaidAmount, - float Outstanding, - string Currency, - string PaymentId, - string PaidBy, - DateTimeOffset PaidAt - ); + float PaidAmount, + float Outstanding, + string Currency, + string PaymentId, + string PaidBy, + DateTimeOffset PaidAt + ); [EventType("V1.FullyPaid")] public record BookingFullyPaid(DateTimeOffset FullyPaidAt); @@ -37,4 +39,4 @@ public record BookingOverpaid(DateTimeOffset OverpaidAt); [EventType("V1.BookingCancelled")] public record BookingCancelled(string CancelledBy, DateTimeOffset CancelledAt); } -} \ No newline at end of file +} diff --git a/samples/esdb/Bookings.Domain/Bookings/BookingState.cs b/samples/esdb/Bookings.Domain/Bookings/BookingState.cs index 6870a0bd..d5b1602c 100644 --- a/samples/esdb/Bookings.Domain/Bookings/BookingState.cs +++ b/samples/esdb/Bookings.Domain/Bookings/BookingState.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using Eventuous; using static Bookings.Domain.Bookings.BookingEvents; + // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable NotAccessedPositionalProperty.Global @@ -14,11 +15,11 @@ public record BookingState : State { public Money Price { get; init; } = null!; public Money Outstanding { get; init; } = null!; public bool Paid { get; init; } - + public ImmutableArray Payments { get; init; } = ImmutableArray.Empty; - + internal bool HasPaymentBeenRegistered(string paymentId) => Payments.Any(x => x.PaymentId == paymentId); - + public BookingState() { On(HandleBooked); On(HandlePayment); @@ -36,11 +37,11 @@ static BookingState HandleBooked(BookingState state, V1.RoomBooked booked) RoomId = new RoomId(booked.RoomId), Period = new StayPeriod(booked.CheckInDate, booked.CheckOutDate), GuestId = booked.GuestId, - Price = new Money { Amount = booked.BookingPrice, Currency = booked.Currency }, + Price = new Money { Amount = booked.BookingPrice, Currency = booked.Currency }, Outstanding = new Money { Amount = booked.OutstandingAmount, Currency = booked.Currency } }; } public record PaymentRecord(string PaymentId, Money PaidAmount); -public record DiscountRecord(Money Discount, string Reason); \ No newline at end of file +public record DiscountRecord(Money Discount, string Reason); diff --git a/samples/esdb/Bookings.Domain/Money.cs b/samples/esdb/Bookings.Domain/Money.cs index d23a12ca..631ba460 100644 --- a/samples/esdb/Bookings.Domain/Money.cs +++ b/samples/esdb/Bookings.Domain/Money.cs @@ -6,7 +6,7 @@ public record Money { public float Amount { get; internal init; } public string Currency { get; internal init; } = null!; - static readonly string[] SupportedCurrencies = {"USD", "GPB", "EUR"}; + static readonly string[] SupportedCurrencies = ["USD", "GPB", "EUR"]; internal Money() { } diff --git a/samples/esdb/Bookings.Payments/Application/CommandApi.cs b/samples/esdb/Bookings.Payments/Application/CommandApi.cs index 20e7a9cb..9f179ae8 100644 --- a/samples/esdb/Bookings.Payments/Application/CommandApi.cs +++ b/samples/esdb/Bookings.Payments/Application/CommandApi.cs @@ -7,8 +7,8 @@ namespace Bookings.Payments.Application; [Route("payment")] -public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { +public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { [HttpPost] - public Task> RegisterPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) + public Task>> RegisterPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); } \ No newline at end of file diff --git a/samples/esdb/Bookings.Payments/Domain/Money.cs b/samples/esdb/Bookings.Payments/Domain/Money.cs index 6779deaf..d651f7fb 100644 --- a/samples/esdb/Bookings.Payments/Domain/Money.cs +++ b/samples/esdb/Bookings.Payments/Domain/Money.cs @@ -6,7 +6,7 @@ public record Money { public float Amount { get; internal init; } public string Currency { get; internal init; } = null!; - static readonly string[] SupportedCurrencies = { "USD", "GPB", "EUR" }; + static readonly string[] SupportedCurrencies = ["USD", "GPB", "EUR"]; // ReSharper disable once UnusedMember.Global internal Money() { } diff --git a/samples/esdb/Bookings.Payments/Domain/Payment.cs b/samples/esdb/Bookings.Payments/Domain/Payment.cs index 7afc2e6b..8dfa2ba0 100644 --- a/samples/esdb/Bookings.Payments/Domain/Payment.cs +++ b/samples/esdb/Bookings.Payments/Domain/Payment.cs @@ -13,12 +13,7 @@ public record PaymentState : State { public float Amount { get; init; } public PaymentState() { - On( - (state, recorded) => state with { - BookingId = recorded.BookingId, - Amount = recorded.Amount - } - ); + On((state, recorded) => state with { BookingId = recorded.BookingId, Amount = recorded.Amount }); } } diff --git a/samples/esdb/Bookings.Payments/Program.cs b/samples/esdb/Bookings.Payments/Program.cs index 4e68e06e..e8075f99 100644 --- a/samples/esdb/Bookings.Payments/Program.cs +++ b/samples/esdb/Bookings.Payments/Program.cs @@ -25,8 +25,7 @@ app.UseOpenTelemetryPrometheusScrapingEndpoint(); // Here we discover commands by their annotations -// app.MapDiscoveredCommands(); -app.MapDiscoveredCommands(); +app.MapDiscoveredCommands(); app.UseSwaggerUI(); diff --git a/samples/esdb/Bookings.Payments/Registrations.cs b/samples/esdb/Bookings.Payments/Registrations.cs index e22bc1f8..d1793abd 100644 --- a/samples/esdb/Bookings.Payments/Registrations.cs +++ b/samples/esdb/Bookings.Payments/Registrations.cs @@ -17,15 +17,15 @@ public static class Registrations { public static void AddServices(this IServiceCollection services, IConfiguration configuration) { services.AddEventStoreClient(configuration["EventStore:ConnectionString"]!); services.AddAggregateStore(); - services.AddCommandService(); + services.AddCommandService(); services.AddSingleton(Mongo.ConfigureMongo(configuration)); services.AddCheckpointStore(); services.AddProducer(); services .AddGateway( - subscriptionId: "IntegrationSubscription", - routeAndTransform: PaymentsGateway.Transform + "IntegrationSubscription", + PaymentsGateway.Transform ); } diff --git a/samples/esdb/Bookings/Application/BookingsQueryService.cs b/samples/esdb/Bookings/Application/BookingsQueryService.cs new file mode 100644 index 00000000..f3c86be7 --- /dev/null +++ b/samples/esdb/Bookings/Application/BookingsQueryService.cs @@ -0,0 +1,9 @@ +using Bookings.Application.Queries; +using Eventuous.Projections.MongoDB.Tools; +using MongoDB.Driver; + +namespace Bookings.Application; + +public class BookingsQueryService(IMongoDatabase database) { + public async Task GetUserBookings(string userId) => await database.LoadDocument(userId); +} diff --git a/samples/esdb/Bookings/Application/Queries/BookingDocument.cs b/samples/esdb/Bookings/Application/Queries/BookingDocument.cs index 42d37e9b..06bd80b7 100644 --- a/samples/esdb/Bookings/Application/Queries/BookingDocument.cs +++ b/samples/esdb/Bookings/Application/Queries/BookingDocument.cs @@ -1,18 +1,16 @@ using Eventuous.Projections.MongoDB.Tools; using NodaTime; -// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global namespace Bookings.Application.Queries; -public record BookingDocument : ProjectedDocument { - public BookingDocument(string id) : base(id) { } - - public string GuestId { get; init; } = null!; - public string RoomId { get; init; } = null!; +public record BookingDocument(string Id) : ProjectedDocument(Id) { + public string GuestId { get; init; } = null!; + public string RoomId { get; init; } = null!; public LocalDate CheckInDate { get; init; } public LocalDate CheckOutDate { get; init; } public float BookingPrice { get; init; } public float PaidAmount { get; init; } public float Outstanding { get; init; } public bool Paid { get; init; } -} \ No newline at end of file +} diff --git a/samples/esdb/Bookings/Application/Queries/BookingStateProjection.cs b/samples/esdb/Bookings/Application/Queries/BookingStateProjection.cs index 6ec0ca86..e6e0581e 100644 --- a/samples/esdb/Bookings/Application/Queries/BookingStateProjection.cs +++ b/samples/esdb/Bookings/Application/Queries/BookingStateProjection.cs @@ -15,21 +15,18 @@ public BookingStateProjection(IMongoDatabase database) : base(database) { b => b .UpdateOne .DefaultId() - .Update((evt, update) => - update.Set(x => x.Outstanding, evt.Outstanding) - ) + .Update((evt, update) => update.Set(x => x.Outstanding, evt.Outstanding)) ); - On(b => b - .UpdateOne - .DefaultId() - .Update((_, update) => update.Set(x => x.Paid, true)) + On( + b => b + .UpdateOne + .DefaultId() + .Update((_, update) => update.Set(x => x.Paid, true)) ); } - static UpdateDefinition HandleRoomBooked( - IMessageConsumeContext ctx, UpdateDefinitionBuilder update - ) { + static UpdateDefinition HandleRoomBooked(IMessageConsumeContext ctx, UpdateDefinitionBuilder update) { var evt = ctx.Message; return update.SetOnInsert(x => x.Id, ctx.Stream.GetId()) diff --git a/samples/esdb/Bookings/Application/Queries/MyBookings.cs b/samples/esdb/Bookings/Application/Queries/MyBookings.cs index 18180720..53b81598 100644 --- a/samples/esdb/Bookings/Application/Queries/MyBookings.cs +++ b/samples/esdb/Bookings/Application/Queries/MyBookings.cs @@ -1,12 +1,12 @@ using Eventuous.Projections.MongoDB.Tools; using NodaTime; -namespace Bookings.Application.Queries; +// ReSharper disable CollectionNeverUpdated.Global -public record MyBookings : ProjectedDocument { - public MyBookings(string id) : base(id) { } +namespace Bookings.Application.Queries; - public List Bookings { get; init; } = new(); +public record MyBookings(string Id) : ProjectedDocument(Id) { + public List Bookings { get; init; } = []; public record Booking(string BookingId, LocalDate CheckInDate, LocalDate CheckOutDate, float Price); -} \ No newline at end of file +} diff --git a/samples/esdb/Bookings/Bookings.csproj b/samples/esdb/Bookings/Bookings.csproj index 7c6abf82..5e4037e4 100644 --- a/samples/esdb/Bookings/Bookings.csproj +++ b/samples/esdb/Bookings/Bookings.csproj @@ -5,6 +5,7 @@ AnyCPU + diff --git a/samples/esdb/Bookings/HttpApi/Bookings/CommandApi.cs b/samples/esdb/Bookings/HttpApi/Bookings/CommandApi.cs index c0df258b..8b4718eb 100644 --- a/samples/esdb/Bookings/HttpApi/Bookings/CommandApi.cs +++ b/samples/esdb/Bookings/HttpApi/Bookings/CommandApi.cs @@ -6,11 +6,16 @@ namespace Bookings.HttpApi.Bookings; +/// +/// This controller exposes a Web API to execute HTTP POST requests matching application commands using the +/// command service registered in the DI container. +/// +/// [Route("/booking")] -public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { +public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { [HttpPost] [Route("book")] - public Task> BookRoom([FromBody] BookRoom cmd, CancellationToken cancellationToken) + public Task>> BookRoom([FromBody] BookRoom cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); /// @@ -23,6 +28,6 @@ public Task> BookRoom([FromBody] BookRoom cmd, Cancellation /// [HttpPost] [Route("recordPayment")] - public Task> RecordPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) + public Task>> RecordPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); } diff --git a/samples/esdb/Bookings/HttpApi/Bookings/CommandApiWithCustomResult.cs b/samples/esdb/Bookings/HttpApi/Bookings/CommandApiWithCustomResult.cs new file mode 100644 index 00000000..bc6bff22 --- /dev/null +++ b/samples/esdb/Bookings/HttpApi/Bookings/CommandApiWithCustomResult.cs @@ -0,0 +1,67 @@ +using Bookings.Domain; +using Bookings.Domain.Bookings; +using Eventuous; +using Eventuous.AspNetCore.Web; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using static Bookings.Application.BookingCommands; + +namespace Bookings.HttpApi.Bookings; + +/// +/// This command API is for demo purposes only. It's the same as the CommandApi, but with a different route and a custom result type. +/// Check the swagger UI for the API documentation and see the custom result type in action. +/// +[Route("/custom/booking")] +public class CommandApiWithCustomResult(ICommandService service) : CommandHttpApiBase(service) { + [HttpPost] + [Route("book")] + public Task> BookRoom([FromBody] BookRoom cmd, CancellationToken cancellationToken) + => Handle(cmd, cancellationToken); + + protected override ActionResult AsActionResult(Result result) + => result switch { + ErrorResult error => error.Exception switch { + ValidationException => MapValidationExceptionAsValidationProblemDetails(error), + _ => base.AsActionResult(result) + }, + OkResult { State: not null } okResult => new OkObjectResult( + new CustomBookingResult { + GuestId = okResult.State.GuestId, + RoomId = okResult.State.RoomId, + CustomPropertyA = "Some custom property", + CustomPropertyB = "Another custom property", + CustomPropertyC = "Yet another custom property" + } + ), + _ => base.AsActionResult(result) + }; + + static BadRequestObjectResult MapValidationExceptionAsValidationProblemDetails(ErrorResult error) { + if (error?.Exception is not ValidationException exception) { + throw new ArgumentNullException(nameof(error), "Exception in result is not of the type `ValidationException`. Unable to map validation result."); + } + + var problemDetails = new ValidationProblemDetails() { + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + var groupFailures = exception.Errors.GroupBy(v => v.PropertyName); + + foreach (var groupFailure in groupFailures) { + problemDetails.Errors.Add(groupFailure.Key, groupFailure.Select(s => s.ErrorMessage).ToArray()); + } + + return new(problemDetails); + } +} + +public record CustomBookingResult { + public string GuestId { get; init; } = null!; + public RoomId RoomId { get; init; } = null!; + + public string? CustomPropertyA { get; init; } + public string? CustomPropertyB { get; init; } + public string? CustomPropertyC { get; init; } +} diff --git a/samples/esdb/Bookings/HttpApi/Bookings/QueryApi.cs b/samples/esdb/Bookings/HttpApi/Bookings/QueryApi.cs index 7f086364..ad88cb7b 100644 --- a/samples/esdb/Bookings/HttpApi/Bookings/QueryApi.cs +++ b/samples/esdb/Bookings/HttpApi/Bookings/QueryApi.cs @@ -5,15 +5,11 @@ namespace Bookings.HttpApi.Bookings; [Route("/bookings")] -public class QueryApi : ControllerBase { - readonly IAggregateStore _store; - - public QueryApi(IAggregateStore store) => _store = store; - +public class QueryApi(IAggregateStore store) : ControllerBase { [HttpGet] [Route("{id}")] public async Task GetBooking(string id, CancellationToken cancellationToken) { - var booking = await _store.Load(StreamName.For(id), cancellationToken); + var booking = await store.Load(StreamName.For(id), cancellationToken); return booking.State; } } \ No newline at end of file diff --git a/samples/esdb/Bookings/Infrastructure/Mongo.cs b/samples/esdb/Bookings/Infrastructure/Mongo.cs index 6df344b0..e54e029a 100644 --- a/samples/esdb/Bookings/Infrastructure/Mongo.cs +++ b/samples/esdb/Bookings/Infrastructure/Mongo.cs @@ -7,11 +7,10 @@ namespace Bookings.Infrastructure; public static class Mongo { public static IMongoDatabase ConfigureMongo(IConfiguration configuration) { NodaTimeSerializers.Register(); - var config = configuration.GetSection("Mongo").Get(); - + var config = configuration.GetSection("Mongo").Get(); var settings = MongoClientSettings.FromConnectionString(config!.ConnectionString); - if (config.User != null && config.Password != null) { + if (config is { User: not null, Password: not null }) { settings.Credential = new MongoCredential( null, new MongoInternalIdentity("admin", config.User), @@ -20,6 +19,7 @@ public static IMongoDatabase ConfigureMongo(IConfiguration configuration) { } settings.ClusterConfigurator = cb => cb.Subscribe(new DiagnosticsActivityEventSubscriber()); + return new MongoClient(settings).GetDatabase(config.Database); } diff --git a/samples/esdb/Bookings/Integration/Payments.cs b/samples/esdb/Bookings/Integration/Payments.cs index 1776bf08..f126a0f2 100644 --- a/samples/esdb/Bookings/Integration/Payments.cs +++ b/samples/esdb/Bookings/Integration/Payments.cs @@ -9,27 +9,18 @@ namespace Bookings.Integration; public class PaymentsIntegrationHandler : EventHandler { public static readonly StreamName Stream = new("PaymentsIntegration"); - readonly ICommandService _applicationService; + readonly ICommandService _applicationService; - public PaymentsIntegrationHandler(ICommandService applicationService) { + public PaymentsIntegrationHandler(ICommandService applicationService) { _applicationService = applicationService; On(async ctx => await HandlePayment(ctx.Message, ctx.CancellationToken)); } Task HandlePayment(BookingPaymentRecorded evt, CancellationToken cancellationToken) - => _applicationService.Handle( - new RecordPayment( - evt.BookingId, - evt.Amount, - evt.Currency, - evt.PaymentId, - "" - ), - cancellationToken - ); + => _applicationService.Handle(new RecordPayment(evt.BookingId, evt.Amount, evt.Currency, evt.PaymentId, ""), cancellationToken); } static class IntegrationEvents { [EventType("BookingPaymentRecorded")] public record BookingPaymentRecorded(string PaymentId, string BookingId, float Amount, string Currency); -} \ No newline at end of file +} diff --git a/samples/esdb/Bookings/Program.cs b/samples/esdb/Bookings/Program.cs index 7a41d10e..414f9229 100644 --- a/samples/esdb/Bookings/Program.cs +++ b/samples/esdb/Bookings/Program.cs @@ -1,4 +1,5 @@ using Bookings; +using Bookings.Application; using Bookings.Domain.Bookings; using Eventuous; using Eventuous.Diagnostics.Logging; @@ -25,18 +26,13 @@ var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog(); -builder.Services - .AddControllers() - .AddJsonOptions(cfg => cfg.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)); +builder.Services.AddControllers().AddJsonOptions(cfg => cfg.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddTelemetry(); builder.Services.AddEventuous(builder.Configuration); builder.Services.AddEventuousSpyglass(); - -builder.Services.Configure(options - => options.SerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) -); +builder.Services.Configure(options => options.SerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)); var app = builder.Build(); @@ -46,18 +42,27 @@ app.UseOpenTelemetryPrometheusScrapingEndpoint(); app.MapEventuousSpyglass(); +app.MapGet( + "/bookings/my/{userId}", + async (string userId, BookingsQueryService queryService) => { + var userBookings = await queryService.GetUserBookings(userId); + + return userBookings == null ? Results.NotFound() : Results.Ok(userBookings); + } +); + var factory = app.Services.GetRequiredService(); var listener = new LoggingEventListener(factory, "OpenTelemetry"); try { app.Run("http://*:5051"); + return 0; -} -catch (Exception e) { +} catch (Exception e) { Log.Fatal(e, "Host terminated unexpectedly"); + return 1; -} -finally { +} finally { Log.CloseAndFlush(); listener.Dispose(); } diff --git a/samples/esdb/Bookings/Registrations.cs b/samples/esdb/Bookings/Registrations.cs index 083beeac..13502346 100644 --- a/samples/esdb/Bookings/Registrations.cs +++ b/samples/esdb/Bookings/Registrations.cs @@ -22,22 +22,15 @@ namespace Bookings; public static class Registrations { public static void AddEventuous(this IServiceCollection services, IConfiguration configuration) { DefaultEventSerializer.SetDefaultSerializer( - new DefaultEventSerializer( - new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime( - DateTimeZoneProviders.Tzdb - ) - ) + new DefaultEventSerializer(new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)) ); services.AddEventStoreClient(configuration["EventStore:ConnectionString"]!); services.AddAggregateStore(); - services.AddCommandService(); + services.AddCommandService(); - services.AddSingleton((id, period) => new ValueTask(true)); - - services.AddSingleton((from, currency) - => new Money(from.Amount * 2, currency) - ); + services.AddSingleton((_, _) => new(true)); + services.AddSingleton((from, currency) => new Money(from.Amount * 2, currency)); services.AddSingleton(Mongo.ConfigureMongo(configuration)); services.AddCheckpointStore(); @@ -50,6 +43,7 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration .AddEventHandler() .WithPartitioningByStream(2) ); + services.AddSingleton(); services.AddSubscription( "PaymentIntegration", diff --git a/samples/postgres/Bookings.Domain/Bookings.Domain.csproj b/samples/postgres/Bookings.Domain/Bookings.Domain.csproj index 228fe762..d222eeda 100644 --- a/samples/postgres/Bookings.Domain/Bookings.Domain.csproj +++ b/samples/postgres/Bookings.Domain/Bookings.Domain.csproj @@ -9,4 +9,8 @@ + + + + diff --git a/samples/postgres/Bookings.Domain/Bookings/Booking.cs b/samples/postgres/Bookings.Domain/Bookings/Booking.cs deleted file mode 100644 index 4ae0b768..00000000 --- a/samples/postgres/Bookings.Domain/Bookings/Booking.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Eventuous; -using static Bookings.Domain.Bookings.BookingEvents; -using static Bookings.Domain.Services; - -namespace Bookings.Domain.Bookings; - -public class Booking : Aggregate { - public async Task BookRoom( - BookingId bookingId, - string guestId, - RoomId roomId, - StayPeriod period, - Money price, - Money prepaid, - DateTimeOffset bookedAt, - IsRoomAvailable isRoomAvailable - ) { - EnsureDoesntExist(); - await EnsureRoomAvailable(roomId, period, isRoomAvailable); - - var outstanding = price - prepaid; - - Apply( - new V1.RoomBooked( - guestId, - roomId, - period.CheckIn, - period.CheckOut, - price.Amount, - prepaid.Amount, - outstanding.Amount, - price.Currency, - bookedAt - ) - ); - - MarkFullyPaidIfNecessary(bookedAt); - } - - public void RecordPayment( - Money paid, - string paymentId, - string paidBy, - DateTimeOffset paidAt - ) { - EnsureExists(); - - if (State.HasPaymentBeenRecorded(paymentId)) return; - - var outstanding = State.Outstanding - paid; - - Apply( - new V1.PaymentRecorded( - paid.Amount, - outstanding.Amount, - paid.Currency, - paymentId, - paidBy, - paidAt - ) - ); - - MarkFullyPaidIfNecessary(paidAt); - } - - void MarkFullyPaidIfNecessary(DateTimeOffset when) { - if (State.Outstanding.Amount != 0) return; - - Apply(new V1.BookingFullyPaid(when)); - } - - static async Task EnsureRoomAvailable(RoomId roomId, StayPeriod period, IsRoomAvailable isRoomAvailable) { - var roomAvailable = await isRoomAvailable(roomId, period); - if (!roomAvailable) throw new DomainException("Room not available"); - } -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/Bookings/BookingEvents.cs b/samples/postgres/Bookings.Domain/Bookings/BookingEvents.cs deleted file mode 100644 index a160bfab..00000000 --- a/samples/postgres/Bookings.Domain/Bookings/BookingEvents.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Eventuous; -using NodaTime; - -namespace Bookings.Domain.Bookings; - -public static class BookingEvents { - public static class V1 { - [EventType("V1.RoomBooked")] - public record RoomBooked( - string GuestId, - string RoomId, - LocalDate CheckInDate, - LocalDate CheckOutDate, - float BookingPrice, - float PrepaidAmount, - float OutstandingAmount, - string Currency, - DateTimeOffset BookingDate - ); - - [EventType("V1.PaymentRecorded")] - public record PaymentRecorded( - float PaidAmount, - float Outstanding, - string Currency, - string PaymentId, - string PaidBy, - DateTimeOffset PaidAt - ); - - [EventType("V1.FullyPaid")] - public record BookingFullyPaid(DateTimeOffset FullyPaidAt); - - [EventType("V1.BookingCancelled")] - public record BookingCancelled(string CancelledBy, DateTimeOffset CancelledAt); - } -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/Bookings/BookingId.cs b/samples/postgres/Bookings.Domain/Bookings/BookingId.cs deleted file mode 100644 index 59818d8a..00000000 --- a/samples/postgres/Bookings.Domain/Bookings/BookingId.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Eventuous; - -namespace Bookings.Domain.Bookings; - -public record BookingId(string Value) : AggregateId(Value); \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/Bookings/BookingState.cs b/samples/postgres/Bookings.Domain/Bookings/BookingState.cs deleted file mode 100644 index 2a9b4282..00000000 --- a/samples/postgres/Bookings.Domain/Bookings/BookingState.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Immutable; -using Eventuous; -using static Bookings.Domain.Bookings.BookingEvents; - -namespace Bookings.Domain.Bookings; - -public record BookingState : AggregateState { - public string GuestId { get; init; } - public RoomId RoomId { get; init; } - public StayPeriod Period { get; init; } - public Money Price { get; init; } - public Money Outstanding { get; init; } - public bool Paid { get; init; } - - public ImmutableList PaymentRecords { get; init; } = ImmutableList.Empty; - - internal bool HasPaymentBeenRecorded(string paymentId) - => PaymentRecords.Any(x => x.PaymentId == paymentId); - - public BookingState() { - On(HandleBooked); - On(HandlePayment); - On((state, paid) => state with { Paid = true }); - } - - static BookingState HandlePayment(BookingState state, V1.PaymentRecorded e) - => state with { - Outstanding = new Money { Amount = e.Outstanding, Currency = e.Currency }, - PaymentRecords = state.PaymentRecords.Add( - new PaymentRecord(e.PaymentId, new Money { Amount = e.PaidAmount, Currency = e.Currency }) - ) - }; - - static BookingState HandleBooked(BookingState state, V1.RoomBooked booked) - => state with { - RoomId = new RoomId(booked.RoomId), - Period = new StayPeriod(booked.CheckInDate, booked.CheckOutDate), - GuestId = booked.GuestId, - Price = new Money { Amount = booked.BookingPrice, Currency = booked.Currency }, - Outstanding = new Money { Amount = booked.OutstandingAmount, Currency = booked.Currency } - }; -} - -public record PaymentRecord(string PaymentId, Money PaidAmount); - -public record DiscountRecord(Money Discount, string Reason); diff --git a/samples/postgres/Bookings.Domain/DomainModule.cs b/samples/postgres/Bookings.Domain/DomainModule.cs deleted file mode 100644 index e0f1fb13..00000000 --- a/samples/postgres/Bookings.Domain/DomainModule.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Eventuous; - -namespace Bookings.Domain; - -static class DomainModule { - [ModuleInitializer] - [SuppressMessage("Usage", "CA2255", MessageId = "The \'ModuleInitializer\' attribute should not be used in libraries")] - internal static void InitializeDomainModule() => TypeMap.RegisterKnownEventTypes(); -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/Money.cs b/samples/postgres/Bookings.Domain/Money.cs deleted file mode 100644 index 00097718..00000000 --- a/samples/postgres/Bookings.Domain/Money.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Eventuous; - -namespace Bookings.Domain; - -public record Money { - public float Amount { get; internal init; } - public string Currency { get; internal init; } - - static readonly string[] SupportedCurrencies = {"USD", "GPB", "EUR"}; - - internal Money() { } - - public Money(float amount, string currency) { - if (!SupportedCurrencies.Contains(currency)) throw new DomainException($"Unsupported currency: {currency}"); - - Amount = amount; - Currency = currency; - } - - public bool IsSameCurrency(Money another) => Currency == another.Currency; - - public static Money operator -(Money one, Money another) { - if (!one.IsSameCurrency(another)) throw new DomainException("Cannot operate on different currencies"); - - return new Money(one.Amount - another.Amount, one.Currency); - } - - public static implicit operator double(Money money) => money.Amount; -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/RoomId.cs b/samples/postgres/Bookings.Domain/RoomId.cs deleted file mode 100644 index 3a1df23c..00000000 --- a/samples/postgres/Bookings.Domain/RoomId.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Eventuous; - -namespace Bookings.Domain; - -public record RoomId(string Value) : AggregateId(Value); \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/Services.cs b/samples/postgres/Bookings.Domain/Services.cs deleted file mode 100644 index 7d496bf9..00000000 --- a/samples/postgres/Bookings.Domain/Services.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bookings.Domain; - -public static class Services { - public delegate ValueTask IsRoomAvailable(RoomId roomId, StayPeriod period); - - public delegate Money ConvertCurrency(Money from, string targetCurrency); -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Domain/StayPeriod.cs b/samples/postgres/Bookings.Domain/StayPeriod.cs deleted file mode 100644 index 21ca7e4a..00000000 --- a/samples/postgres/Bookings.Domain/StayPeriod.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Eventuous; -using NodaTime; - -namespace Bookings.Domain; - -public record StayPeriod { - public LocalDate CheckIn { get; } - public LocalDate CheckOut { get; } - - internal StayPeriod() { } - - public StayPeriod(LocalDate checkIn, LocalDate checkOut) { - if (checkIn > checkOut) throw new DomainException("Check in date must be before check out date"); - - (CheckIn, CheckOut) = (checkIn, checkOut); - } -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Payments/Application/CommandApi.cs b/samples/postgres/Bookings.Payments/Application/CommandApi.cs deleted file mode 100644 index 28aa80da..00000000 --- a/samples/postgres/Bookings.Payments/Application/CommandApi.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Bookings.Payments.Domain; -using Eventuous; -using Eventuous.AspNetCore.Web; -using Microsoft.AspNetCore.Mvc; -using static Bookings.Payments.Application.PaymentCommands; - -namespace Bookings.Payments.Application; - -[Route("payment")] -public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { - [HttpPost] - public Task> RegisterPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) - => Handle(cmd, cancellationToken); -} diff --git a/samples/postgres/Bookings.Payments/Application/CommandService.cs b/samples/postgres/Bookings.Payments/Application/CommandService.cs deleted file mode 100644 index 8b9e3e94..00000000 --- a/samples/postgres/Bookings.Payments/Application/CommandService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json.Serialization; -using Bookings.Payments.Domain; -using Eventuous; -using Eventuous.AspNetCore.Web; - -namespace Bookings.Payments.Application; - -public class CommandService : CommandService { - public CommandService(IAggregateStore store) : base(store) { - On() - .InState(ExpectedState.New) - .GetId(cmd => new PaymentId(cmd.PaymentId)) - .Act(ProcessPayment); - - return; - - void ProcessPayment(Payment payment, PaymentCommands.RecordPayment cmd) - => payment.ProcessPayment( - new PaymentId(cmd.PaymentId), - cmd.BookingId, - new Money(cmd.Amount, cmd.Currency), - cmd.Method, - cmd.Provider - ); - } -} - -// [AggregateCommands(typeof(Payment))] -public static class PaymentCommands { - [HttpCommand] - public record RecordPayment( - string PaymentId, - string BookingId, - float Amount, - string Currency, - string Method, - string Provider, - [property: JsonIgnore] string PaidBy - ); -} diff --git a/samples/postgres/Bookings.Payments/Bookings.Payments.csproj b/samples/postgres/Bookings.Payments/Bookings.Payments.csproj index d67901e8..16e66f34 100644 --- a/samples/postgres/Bookings.Payments/Bookings.Payments.csproj +++ b/samples/postgres/Bookings.Payments/Bookings.Payments.csproj @@ -1,41 +1,43 @@ - - Debug;Release - - - - - - - - - - - - - - - - - - - - Infrastructure\Logging.cs - - - Infrastructure\Mongo.cs - - - Infrastructure\Telemetry.cs - - - - - - - - - - - + + Debug;Release + + + + + + + + + + + + + + + + + + + + Infrastructure\Logging.cs + + + Infrastructure\Mongo.cs + + + Infrastructure\Telemetry.cs + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/postgres/Bookings.Payments/Domain/Money.cs b/samples/postgres/Bookings.Payments/Domain/Money.cs deleted file mode 100644 index 6779deaf..00000000 --- a/samples/postgres/Bookings.Payments/Domain/Money.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Eventuous; - -namespace Bookings.Payments.Domain; - -public record Money { - public float Amount { get; internal init; } - public string Currency { get; internal init; } = null!; - - static readonly string[] SupportedCurrencies = { "USD", "GPB", "EUR" }; - - // ReSharper disable once UnusedMember.Global - internal Money() { } - - public Money(float amount, string currency) { - if (!SupportedCurrencies.Contains(currency)) throw new DomainException($"Unsupported currency: {currency}"); - - Amount = amount; - Currency = currency; - } - - public bool IsSameCurrency(Money another) => Currency == another.Currency; - - public static Money operator -(Money one, Money another) { - if (!one.IsSameCurrency(another)) throw new DomainException("Cannot operate on different currencies"); - - return new Money(one.Amount - another.Amount, one.Currency); - } - - public static implicit operator double(Money money) => money.Amount; -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Payments/Domain/Payment.cs b/samples/postgres/Bookings.Payments/Domain/Payment.cs deleted file mode 100644 index 6d905b3e..00000000 --- a/samples/postgres/Bookings.Payments/Domain/Payment.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Eventuous; -using static Bookings.Payments.Domain.PaymentEvents; - -namespace Bookings.Payments.Domain; - -public class Payment : Aggregate { - public void ProcessPayment( - PaymentId paymentId, string bookingId, Money amount, string method, string provider - ) - => Apply(new PaymentRecorded(paymentId, bookingId, amount.Amount, amount.Currency, method, provider)); -} - -public record PaymentState : State { - public string BookingId { get; init; } = null!; - public float Amount { get; init; } - - public PaymentState() { - On( - (state, recorded) => state with { - BookingId = recorded.BookingId, - Amount = recorded.Amount - } - ); - } -} - -public record PaymentId(string Value) : Id(Value); \ No newline at end of file diff --git a/samples/postgres/Bookings.Payments/Domain/PaymentEvents.cs b/samples/postgres/Bookings.Payments/Domain/PaymentEvents.cs deleted file mode 100644 index 8ecefdf5..00000000 --- a/samples/postgres/Bookings.Payments/Domain/PaymentEvents.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Eventuous; - -namespace Bookings.Payments.Domain; - -public static class PaymentEvents { - [EventType("PaymentRecorded")] - public record PaymentRecorded( - string PaymentId, string BookingId, float Amount, string Currency, string Method, string Provider - ); -} \ No newline at end of file diff --git a/samples/postgres/Bookings.Payments/Integration/Payments.cs b/samples/postgres/Bookings.Payments/Integration/Payments.cs index afaf89af..41a903e3 100644 --- a/samples/postgres/Bookings.Payments/Integration/Payments.cs +++ b/samples/postgres/Bookings.Payments/Integration/Payments.cs @@ -11,21 +11,17 @@ public static class PaymentsGateway { static readonly StreamName Stream = new("PaymentsIntegration"); static readonly RabbitMqProduceOptions ProduceOptions = new(); - public static ValueTask[]> Transform( - IMessageConsumeContext original - ) { + public static ValueTask[]> Transform(IMessageConsumeContext original) { var result = original.Message is PaymentEvents.PaymentRecorded evt ? new GatewayMessage( Stream, - new BookingPaymentRecorded(evt.PaymentId, evt.BookingId, evt.Amount, evt.Currency), + new BookingPaymentRecorded(original.Stream.GetId(), evt.BookingId, evt.Amount, evt.Currency), new Metadata(), ProduceOptions ) : null; - return ValueTask.FromResult( - result != null ? new[] { result } : Array.Empty>() - ); + return ValueTask.FromResult(result != null ? [result] : Array.Empty>()); } } diff --git a/samples/postgres/Bookings.Payments/Program.cs b/samples/postgres/Bookings.Payments/Program.cs index 32feebe8..4b0e1626 100644 --- a/samples/postgres/Bookings.Payments/Program.cs +++ b/samples/postgres/Bookings.Payments/Program.cs @@ -25,7 +25,7 @@ app.UseOpenTelemetryPrometheusScrapingEndpoint(); // Here we discover commands by their annotations -app.MapDiscoveredCommands(); +app.MapDiscoveredCommands(); if (app.Configuration.GetValue("Postgres:InitializeDatabase")) { await InitialiseSchema(app); diff --git a/samples/postgres/Bookings.Payments/Registrations.cs b/samples/postgres/Bookings.Payments/Registrations.cs index f408c6df..cd3efcb6 100644 --- a/samples/postgres/Bookings.Payments/Registrations.cs +++ b/samples/postgres/Bookings.Payments/Registrations.cs @@ -19,16 +19,15 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration services.AddSingleton(connectionFactory); services.AddEventuousPostgres(configuration.GetSection("Postgres")); services.AddAggregateStore(); - services.AddCommandService(); + services.AddCommandService(); services.AddSingleton(Mongo.ConfigureMongo(configuration)); services.AddCheckpointStore(); services.AddProducer(); services - .AddGateway( + .AddGateway( "IntegrationSubscription", PaymentsGateway.Transform ); } -} \ No newline at end of file +} diff --git a/samples/postgres/Bookings/Application/BookingsCommandService.cs b/samples/postgres/Bookings/Application/BookingsCommandService.cs index 37ed6d20..9560308f 100644 --- a/samples/postgres/Bookings/Application/BookingsCommandService.cs +++ b/samples/postgres/Bookings/Application/BookingsCommandService.cs @@ -13,7 +13,6 @@ public BookingsCommandService(IAggregateStore store, Services.IsRoomAvailable is .GetId(cmd => new BookingId(cmd.BookingId)) .ActAsync( (booking, cmd, _) => booking.BookRoom( - new BookingId(cmd.BookingId), cmd.GuestId, new RoomId(cmd.RoomId), new StayPeriod(LocalDate.FromDateTime(cmd.CheckInDate), LocalDate.FromDateTime(cmd.CheckOutDate)), @@ -27,13 +26,6 @@ public BookingsCommandService(IAggregateStore store, Services.IsRoomAvailable is On() .InState(ExpectedState.Existing) .GetId(cmd => new BookingId(cmd.BookingId)) - .Act( - (booking, cmd) => booking.RecordPayment( - new Money(cmd.PaidAmount, cmd.Currency), - cmd.PaymentId, - cmd.PaidBy, - DateTimeOffset.Now - ) - ); + .Act((booking, cmd) => booking.RecordPayment(new Money(cmd.PaidAmount, cmd.Currency), cmd.PaymentId, cmd.PaidBy, DateTimeOffset.Now)); } } diff --git a/samples/postgres/Bookings/HttpApi/Bookings/CommandApi.cs b/samples/postgres/Bookings/HttpApi/Bookings/CommandApi.cs index 92e67df0..11232ec5 100644 --- a/samples/postgres/Bookings/HttpApi/Bookings/CommandApi.cs +++ b/samples/postgres/Bookings/HttpApi/Bookings/CommandApi.cs @@ -7,12 +7,10 @@ namespace Bookings.HttpApi.Bookings; [Route("/booking")] -public class CommandApi : CommandHttpApiBase { - public CommandApi(ICommandService service) : base(service) { } - +public class CommandApi(ICommandService service) : CommandHttpApiBase(service) { [HttpPost] [Route("book")] - public Task> BookRoom([FromBody] BookRoom cmd, CancellationToken cancellationToken) + public Task>> BookRoom([FromBody] BookRoom cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); /// @@ -25,8 +23,6 @@ public Task> BookRoom([FromBody] BookRoom cmd, Cancellation /// [HttpPost] [Route("recordPayment")] - public Task> RecordPayment( - [FromBody] RecordPayment cmd, CancellationToken cancellationToken - ) + public Task>> RecordPayment([FromBody] RecordPayment cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); } diff --git a/samples/postgres/Bookings/HttpApi/Bookings/QueryApi.cs b/samples/postgres/Bookings/HttpApi/Bookings/QueryApi.cs index 7f086364..ad88cb7b 100644 --- a/samples/postgres/Bookings/HttpApi/Bookings/QueryApi.cs +++ b/samples/postgres/Bookings/HttpApi/Bookings/QueryApi.cs @@ -5,15 +5,11 @@ namespace Bookings.HttpApi.Bookings; [Route("/bookings")] -public class QueryApi : ControllerBase { - readonly IAggregateStore _store; - - public QueryApi(IAggregateStore store) => _store = store; - +public class QueryApi(IAggregateStore store) : ControllerBase { [HttpGet] [Route("{id}")] public async Task GetBooking(string id, CancellationToken cancellationToken) { - var booking = await _store.Load(StreamName.For(id), cancellationToken); + var booking = await store.Load(StreamName.For(id), cancellationToken); return booking.State; } } \ No newline at end of file diff --git a/samples/postgres/Bookings/Integration/Payments.cs b/samples/postgres/Bookings/Integration/Payments.cs index a836e0bb..953662b9 100644 --- a/samples/postgres/Bookings/Integration/Payments.cs +++ b/samples/postgres/Bookings/Integration/Payments.cs @@ -9,9 +9,9 @@ namespace Bookings.Integration; public class PaymentsIntegrationHandler : EventHandler { public const string Stream = "PaymentsIntegration"; - readonly ICommandService _applicationService; + readonly ICommandService _applicationService; - public PaymentsIntegrationHandler(ICommandService applicationService) { + public PaymentsIntegrationHandler(ICommandService applicationService) { _applicationService = applicationService; On(async ctx => await HandlePayment(ctx.Message, ctx.CancellationToken)); } diff --git a/samples/postgres/Bookings/Registrations.cs b/samples/postgres/Bookings/Registrations.cs index 51c0eecb..11b7aa55 100644 --- a/samples/postgres/Bookings/Registrations.cs +++ b/samples/postgres/Bookings/Registrations.cs @@ -36,7 +36,7 @@ public static void AddEventuous(this IServiceCollection services, IConfiguration services.AddEventuousPostgres(configuration.GetSection("Postgres")); services.AddAggregateStore(); - services.AddCommandService(); + services.AddCommandService(); services.AddSingleton((id, period) => new ValueTask(true)); diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs index 55d669c0..c21f986a 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlerBuilder.cs @@ -5,9 +5,8 @@ namespace Eventuous; -public abstract class CommandHandlerBuilder - where TAggregate : Aggregate where TId : Id where TState : State, new() { - internal abstract RegisteredHandler Build(); +public abstract class CommandHandlerBuilder where TAggregate : Aggregate where TState : State, new() where TId : Id { + internal abstract RegisteredHandler Build(); } /// @@ -24,14 +23,14 @@ public class CommandHandlerBuilder(IAggregate where TAggregate : Aggregate, new() where TState : State, new() where TId : Id { - GetIdFromUntypedCommand? _getId; - HandleUntypedCommand? _action; - ResolveStore? _resolveStore; - ExpectedState _expectedState = ExpectedState.Any; + GetIdFromUntypedCommand? _getId; + HandleUntypedCommand? _action; + ResolveStore? _resolveStore; + ExpectedState _expectedState = ExpectedState.Any; /// /// Set the expected aggregate state for the command handler. - /// If the aggregate won't be in the expected state, the command handler will return an error. + /// If the aggregate isn't in the expected state, the command handler will return an error. /// The default is . /// /// Expected aggregate state @@ -69,7 +68,7 @@ public CommandHandlerBuilder GetIdAsync(GetId /// /// A function that executes an operation on an aggregate /// - public CommandHandlerBuilder Act(ActOnAggregate action) { + public CommandHandlerBuilder Act(ActOnAggregate action) { _action = action.AsAct(); return this; @@ -80,7 +79,7 @@ public CommandHandlerBuilder Act(ActOnAggrega /// /// A function that executes an asynchronous operation on an aggregate /// - public CommandHandlerBuilder ActAsync(ActOnAggregateAsync action) { + public CommandHandlerBuilder ActAsync(ActOnAggregateAsync action) { _action = action.AsAct(); return this; @@ -98,8 +97,8 @@ public CommandHandlerBuilder ResolveStore(Res return this; } - internal override RegisteredHandler Build() { - return new RegisteredHandler( + internal override RegisteredHandler Build() { + return new( _expectedState, Ensure.NotNull(_getId, $"Function to get the aggregate id from {typeof(TCommand).Name} is not defined"), Ensure.NotNull(_action, $"Function to act on the aggregate for command {typeof(TCommand).Name} is not defined"), diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs index d1d5ff5d..9e3917c2 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlersMap.cs @@ -8,20 +8,20 @@ namespace Eventuous; using static Diagnostics.ApplicationEventSource; -record RegisteredHandler( - ExpectedState ExpectedState, - GetIdFromUntypedCommand GetId, - HandleUntypedCommand Handler, - ResolveStoreFromCommand ResolveStore - ) where T : Aggregate where TId : Id; +record RegisteredHandler( + ExpectedState ExpectedState, + GetIdFromUntypedCommand GetId, + HandleUntypedCommand Handler, + ResolveStoreFromCommand ResolveStore + ) where T : Aggregate where TId : Id where TState : State, new(); -class HandlersMap where TAggregate : Aggregate where TId : Id { - readonly TypeMap> _typeMap = new(); +class HandlersMap where TAggregate : Aggregate where TId : Id where TState : State, new() { + readonly TypeMap> _typeMap = new(); static readonly MethodInfo AddHandlerInternalMethod = - typeof(HandlersMap).GetMethod(nameof(AddHandlerInternal), BindingFlags.NonPublic | BindingFlags.Instance)!; + typeof(HandlersMap).GetMethod(nameof(AddHandlerInternal), BindingFlags.NonPublic | BindingFlags.Instance)!; - internal void AddHandlerInternal(RegisteredHandler handler) { + internal void AddHandlerInternal(RegisteredHandler handler) { try { _typeMap.Add(handler); Log.CommandHandlerRegistered(); @@ -32,8 +32,8 @@ internal void AddHandlerInternal(RegisteredHandler ha } } - internal void AddHandlerUntyped(Type command, RegisteredHandler handler) + internal void AddHandlerUntyped(Type command, RegisteredHandler handler) => AddHandlerInternalMethod.MakeGenericMethod(command).Invoke(this, [handler]); - public bool TryGet([NotNullWhen(true)] out RegisteredHandler? handler) => _typeMap.TryGetValue(out handler); + public bool TryGet([NotNullWhen(true)] out RegisteredHandler? handler) => _typeMap.TryGetValue(out handler); } diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs index 8eccc064..4dc0933b 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandHandlingDelegateExtensions.cs @@ -12,14 +12,16 @@ public static GetIdFromUntypedCommand AsGetId(this GetIdFrom public static GetIdFromUntypedCommand AsGetId(this GetIdFromCommand getId) where TId : Id where TCommand : class => (cmd, _) => ValueTask.FromResult(getId((TCommand)cmd)); - public static HandleUntypedCommand AsAct(this ActOnAggregateAsync act) where TAggregate : Aggregate + public static HandleUntypedCommand AsAct(this ActOnAggregateAsync act) + where TAggregate : Aggregate where TState : State, new() => async (aggregate, cmd, ct) => { await act(aggregate, (TCommand)cmd, ct).NoContext(); return aggregate; }; - public static HandleUntypedCommand AsAct(this ActOnAggregate act) where TAggregate : Aggregate + public static HandleUntypedCommand AsAct(this ActOnAggregate act) + where TAggregate : Aggregate where TState : State, new() => (aggregate, cmd, _) => { act(aggregate, (TCommand)cmd); diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs index 01ebb449..38132e44 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Async.cs @@ -15,9 +15,9 @@ public abstract partial class CommandService { /// Command type [Obsolete("Use On().InState(ExpectedState.New).GetId(...).ActAsync(...).ResolveStore(...) instead")] protected void OnNewAsync( - GetIdFromCommand getId, - ActOnAggregateAsync action, - ResolveStore? resolveStore = null + GetIdFromCommand getId, + ActOnAggregateAsync action, + ResolveStore? resolveStore = null ) where TCommand : class => On().InState(ExpectedState.New).GetId(getId).ActAsync(action).ResolveStore(resolveStore); @@ -31,9 +31,9 @@ protected void OnNewAsync( [Obsolete("Use On().InState(ExpectedState.Existing).GetId(...).ActAsync(...).ResolveStore(...) instead")] [PublicAPI] protected void OnExistingAsync( - GetIdFromCommand getId, - ActOnAggregateAsync action, - ResolveStore? resolveStore = null + GetIdFromCommand getId, + ActOnAggregateAsync action, + ResolveStore? resolveStore = null ) where TCommand : class => On().InState(ExpectedState.Existing).GetId(getId).ActAsync(action).ResolveStore(resolveStore); @@ -47,9 +47,9 @@ protected void OnExistingAsync( [Obsolete("Use On().InState(ExpectedState.Existing).GetIdAsync(...).ActAsync(...).ResolveStore(...) instead")] [PublicAPI] protected void OnExistingAsync( - GetIdFromCommandAsync getId, - ActOnAggregateAsync action, - ResolveStore? resolveStore = null + GetIdFromCommandAsync getId, + ActOnAggregateAsync action, + ResolveStore? resolveStore = null ) where TCommand : class // => _handlers.AddHandler(ExpectedState.Existing, getId, action, resolveStore ?? DefaultResolve()); => On().InState(ExpectedState.Existing).GetIdAsync(getId).ActAsync(action).ResolveStore(resolveStore); @@ -64,11 +64,11 @@ protected void OnExistingAsync( [Obsolete("Use On().InState(ExpectedState.Any).GetId(...).ActAsync(...).ResolveStore(...) instead")] [PublicAPI] protected void OnAnyAsync( - GetIdFromCommand getId, - ActOnAggregateAsync action, - ResolveStore? resolveStore = null + GetIdFromCommand getId, + ActOnAggregateAsync action, + ResolveStore? resolveStore = null ) where TCommand : class - // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve()); + // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve()); => On().InState(ExpectedState.Any).GetId(getId).ActAsync(action).ResolveStore(resolveStore); /// @@ -81,10 +81,10 @@ protected void OnAnyAsync( [Obsolete("Use On().InState(ExpectedState.Any).GetIdAsync(...).ActAsync(...).ResolveStore(...) instead")] [PublicAPI] protected void OnAnyAsync( - GetIdFromCommandAsync getId, - ActOnAggregateAsync action, - ResolveStore? resolveStore = null + GetIdFromCommandAsync getId, + ActOnAggregateAsync action, + ResolveStore? resolveStore = null ) where TCommand : class - // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve()); + // => _handlers.AddHandler(ExpectedState.Any, getId, action, resolveStore ?? DefaultResolve()); => On().InState(ExpectedState.Any).GetIdAsync(getId).ActAsync(action).ResolveStore(resolveStore); } diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs index bae884d4..69b86da1 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.Sync.cs @@ -15,10 +15,10 @@ public abstract partial class CommandService { /// Command type [Obsolete("Use On().InState(ExpectedState.New).GetId(...).Act(...).ResolveStore(...) instead")] protected void OnNew( - GetIdFromCommand getId, - ActOnAggregate action, - ResolveStore? resolveStore = null - ) where TCommand : class + GetIdFromCommand getId, + ActOnAggregate action, + ResolveStore? resolveStore = null + ) where TCommand : class => On().InState(ExpectedState.New).GetId(getId).Act(action).ResolveStore(resolveStore); /// @@ -30,10 +30,10 @@ protected void OnNew( /// Command type [Obsolete("Use On().InState(ExpectedState.Existing).GetId(...).Act(...).ResolveStore(...) instead")] protected void OnExisting( - GetIdFromCommand getId, - ActOnAggregate action, - ResolveStore? resolveStore = null - ) where TCommand : class + GetIdFromCommand getId, + ActOnAggregate action, + ResolveStore? resolveStore = null + ) where TCommand : class => On().InState(ExpectedState.Existing).GetId(getId).Act(action).ResolveStore(resolveStore); /// @@ -45,9 +45,9 @@ protected void OnExisting( /// Command type [Obsolete("Use On().InState(ExpectedState.Any).GetId(...).Act(...).ResolveStore(...) instead")] protected void OnAny( - GetIdFromCommand getId, - ActOnAggregate action, - ResolveStore? resolveStore = null + GetIdFromCommand getId, + ActOnAggregate action, + ResolveStore? resolveStore = null ) where TCommand : class => On().InState(ExpectedState.Any).GetId(getId).Act(action).ResolveStore(resolveStore); } diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs index 0ea4a8cc..1c409021 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandService.cs @@ -20,14 +20,14 @@ public abstract partial class CommandService( StreamNameMap? streamNameMap = null, TypeMapper? typeMap = null ) - : ICommandService, ICommandService + : ICommandService//, ICommandService where TAggregate : Aggregate, new() where TState : State, new() where TId : Id { [PublicAPI] protected IAggregateStore? Store { get; } = store; - readonly HandlersMap _handlers = new(); + readonly HandlersMap _handlers = new(); readonly AggregateFactoryRegistry _factoryRegistry = factoryRegistry ?? AggregateFactoryRegistry.Instance; readonly StreamNameMap _streamNameMap = streamNameMap ?? new StreamNameMap(); readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance; @@ -82,7 +82,7 @@ public async Task> Handle(TCommand command, Cancellatio // Zero in the global position would mean nothing, so the receiver need to check the Changes.Length if (result.Changes.Count == 0) return new OkResult(result.State, Array.Empty(), 0); - var storeResult = await store.Store(GetAggregateStreamName(), result, cancellationToken).NoContext(); + var storeResult = await store.Store(GetAggregateStreamName(), result, cancellationToken).NoContext(); var changes = result.Changes.Select(x => new Change(x, _typeMap.GetTypeName(x))); Log.CommandHandled(); @@ -93,19 +93,9 @@ public async Task> Handle(TCommand command, Cancellatio return new ErrorResult($"Error handling command {typeof(TCommand).Name}", e); } - TAggregate Create(TId id) => _factoryRegistry.CreateInstance().WithId(id); + TAggregate Create(TId id) => _factoryRegistry.CreateInstance().WithId(id); - StreamName GetAggregateStreamName() => _streamNameMap.GetStreamName(aggregateId); - } - - async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class { - var result = await Handle(command, cancellationToken).NoContext(); - - return result switch { - OkResult(var state, var enumerable, _) => new OkResult(state, enumerable), - ErrorResult error => new ErrorResult(error.Message, error.Exception), - _ => throw new ApplicationException("Unknown result type") - }; + StreamName GetAggregateStreamName() => _streamNameMap.GetStreamName(aggregateId); } readonly Dictionary> _builders = new(); diff --git a/src/Core/src/Eventuous.Application/AggregateService/CommandServiceDelegates.cs b/src/Core/src/Eventuous.Application/AggregateService/CommandServiceDelegates.cs index 9385f2fb..c3e9c259 100644 --- a/src/Core/src/Eventuous.Application/AggregateService/CommandServiceDelegates.cs +++ b/src/Core/src/Eventuous.Application/AggregateService/CommandServiceDelegates.cs @@ -4,12 +4,14 @@ namespace Eventuous; public static class CommandServiceDelegates { - public delegate Task ActOnAggregateAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken) - where TAggregate : Aggregate; + public delegate Task ActOnAggregateAsync(TAggregate aggregate, TCommand command, CancellationToken cancellationToken) + where TAggregate : Aggregate where TState : State, new(); - public delegate void ActOnAggregate(TAggregate aggregate, TCommand command) where TAggregate : Aggregate; + public delegate void ActOnAggregate(TAggregate aggregate, TCommand command) + where TAggregate : Aggregate where TState : State, new(); - internal delegate ValueTask HandleUntypedCommand(T aggregate, object command, CancellationToken cancellationToken) where T : Aggregate; + internal delegate ValueTask HandleUntypedCommand(T aggregate, object command, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new(); public delegate Task GetIdFromCommandAsync(TCommand command, CancellationToken cancellationToken) where TId : Id where TCommand : class; diff --git a/src/Core/src/Eventuous.Application/Diagnostics/CommandServiceActivity.cs b/src/Core/src/Eventuous.Application/Diagnostics/CommandServiceActivity.cs index ed831f12..84d959b9 100644 --- a/src/Core/src/Eventuous.Application/Diagnostics/CommandServiceActivity.cs +++ b/src/Core/src/Eventuous.Application/Diagnostics/CommandServiceActivity.cs @@ -9,37 +9,32 @@ namespace Eventuous.Diagnostics; using Tracing; static class CommandServiceActivity { - public static async Task TryExecute( - string appServiceTypeName, - TCommand command, - DiagnosticSource diagnosticSource, - HandleCommand handleCommand, - GetError getError, - CancellationToken cancellationToken - ) where TCommand : class { + public static async Task> TryExecute( + string appServiceTypeName, + TCommand command, + DiagnosticSource diagnosticSource, + HandleCommand handleCommand, + CancellationToken cancellationToken + ) where TCommand : class where T : State, new() { var cmdName = command.GetType().Name; using var activity = StartActivity(appServiceTypeName, cmdName); - - using var measure = Measure.Start( - diagnosticSource, - new CommandServiceMetricsContext(appServiceTypeName, cmdName) - ); + using var measure = Measure.Start(diagnosticSource, new CommandServiceMetricsContext(appServiceTypeName, cmdName)); try { - var result = await handleCommand(command, cancellationToken).NoContext(); + var result = await handleCommand(command, cancellationToken).NoContext(); activity?.SetActivityStatus( - getError(result, out var exception) - ? ActivityStatus.Error(exception) + result is ErrorResult err + ? ActivityStatus.Error(err.Exception) : ActivityStatus.Ok() ); return result; - } - catch (Exception e) { + } catch (Exception e) { activity?.SetActivityStatus(ActivityStatus.Error(e)); measure.SetError(); + throw; } } diff --git a/src/Core/src/Eventuous.Application/Diagnostics/TracedCommandService.cs b/src/Core/src/Eventuous.Application/Diagnostics/TracedCommandService.cs index e1db28bc..1164f114 100644 --- a/src/Core/src/Eventuous.Application/Diagnostics/TracedCommandService.cs +++ b/src/Core/src/Eventuous.Application/Diagnostics/TracedCommandService.cs @@ -5,38 +5,38 @@ namespace Eventuous.Diagnostics; -public class TracedCommandService(ICommandService appService) : ICommandService where T : Aggregate { - public static ICommandService Trace(ICommandService appService) - => new TracedCommandService(appService); - - ICommandService InnerService { get; } = appService; - - readonly string _appServiceTypeName = appService.GetType().Name; - readonly DiagnosticSource _metricsSource = new DiagnosticListener(CommandServiceMetrics.ListenerName); - - static bool GetError(Result result, out Exception? exception) { - if (result is ErrorResult err) { - exception = err.Exception; - - return true; - } - - exception = null; - - return false; - } - - public Task Handle(TCommand command, CancellationToken cancellationToken) - where TCommand : class - => CommandServiceActivity.TryExecute( - _appServiceTypeName, - command, - _metricsSource, - InnerService.Handle, - GetError, - cancellationToken - ); -} +// public class TracedCommandService(ICommandService appService) : ICommandService where T : Aggregate { +// public static ICommandService Trace(ICommandService appService) +// => new TracedCommandService(appService); +// +// ICommandService InnerService { get; } = appService; +// +// readonly string _appServiceTypeName = appService.GetType().Name; +// readonly DiagnosticSource _metricsSource = new DiagnosticListener(CommandServiceMetrics.ListenerName); +// +// static bool GetError(Result result, out Exception? exception) { +// if (result is ErrorResult err) { +// exception = err.Exception; +// +// return true; +// } +// +// exception = null; +// +// return false; +// } +// +// public Task Handle(TCommand command, CancellationToken cancellationToken) +// where TCommand : class +// => CommandServiceActivity.TryExecute( +// _appServiceTypeName, +// command, +// _metricsSource, +// InnerService.Handle, +// GetError, +// cancellationToken +// ); +// } public class TracedCommandService(ICommandService appService) : ICommandService where TState : State, new() @@ -50,18 +50,6 @@ public static ICommandService Trace(ICommandService result, out Exception? exception) { - if (result is ErrorResult err) { - exception = err.Exception; - - return true; - } - - exception = null; - - return false; - } - public Task> Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class => CommandServiceActivity.TryExecute( @@ -69,11 +57,10 @@ public Task> Handle(TCommand command, CancellationToken command, _metricsSource, InnerService.Handle, - GetError, cancellationToken ); } -delegate Task HandleCommand(TCommand command, CancellationToken cancellationToken) where TCommand : class; - -delegate bool GetError(T result, out Exception? exception); +delegate Task> HandleCommand(TCommand command, CancellationToken cancellationToken) + where TCommand : class + where T : State, new(); \ No newline at end of file diff --git a/src/Core/src/Eventuous.Application/Diagnostics/TracedFunctionalService.cs b/src/Core/src/Eventuous.Application/Diagnostics/TracedFunctionalService.cs index 78e4f02f..2dc4bb1d 100644 --- a/src/Core/src/Eventuous.Application/Diagnostics/TracedFunctionalService.cs +++ b/src/Core/src/Eventuous.Application/Diagnostics/TracedFunctionalService.cs @@ -5,41 +5,27 @@ namespace Eventuous.Diagnostics; -public class TracedFunctionalService : IFuncCommandService where T : State, new() { - public static IFuncCommandService Trace(IFuncCommandService appService) - => new TracedFunctionalService(appService); +public class TracedFunctionalService : ICommandService where TState : State, new() { + public static ICommandService Trace(ICommandService appService) + => new TracedFunctionalService(appService); - IFuncCommandService InnerService { get; } + ICommandService InnerService { get; } readonly string _appServiceTypeName; - readonly GetError _getError; readonly DiagnosticSource _metricsSource = new DiagnosticListener(CommandServiceMetrics.ListenerName); - TracedFunctionalService(IFuncCommandService appService) { + TracedFunctionalService(ICommandService appService) { _appServiceTypeName = appService.GetType().Name; InnerService = appService; - - bool GetError(Result result, out Exception? exception) { - if (result is ErrorResult err) { - exception = err.Exception; - return true; - } - - exception = null; - return false; - } - - _getError = GetError; } - public Task Handle(TCommand command, CancellationToken cancellationToken) + public Task> Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class => CommandServiceActivity.TryExecute( _appServiceTypeName, command, _metricsSource, InnerService.Handle, - _getError, cancellationToken ); } diff --git a/src/Core/src/Eventuous.Application/FunctionalService/FuncHandlersMap.cs b/src/Core/src/Eventuous.Application/FunctionalService/FuncHandlersMap.cs index b8b4ccb6..086877e2 100644 --- a/src/Core/src/Eventuous.Application/FunctionalService/FuncHandlersMap.cs +++ b/src/Core/src/Eventuous.Application/FunctionalService/FuncHandlersMap.cs @@ -21,8 +21,8 @@ class FuncHandlersMap where TState : State { static readonly MethodInfo AddHandlerInternalMethod = typeof(FuncHandlersMap).GetMethod(nameof(AddHandlerInternal), BindingFlags.NonPublic | BindingFlags.Instance)!; - internal void AddHandlerUntyped(Type command, RegisteredFuncHandler handler) - => AddHandlerInternalMethod.MakeGenericMethod(command).Invoke(this, [handler]); + internal void AddHandlerUntyped(Type commandType, RegisteredFuncHandler handler) + => AddHandlerInternalMethod.MakeGenericMethod(commandType).Invoke(this, [handler]); void AddHandlerInternal(RegisteredFuncHandler handler) where TCommand : class { try { diff --git a/src/Core/src/Eventuous.Application/FunctionalService/FunctionalCommandService.cs b/src/Core/src/Eventuous.Application/FunctionalService/FunctionalCommandService.cs index 83343ac2..2b4227ab 100644 --- a/src/Core/src/Eventuous.Application/FunctionalService/FunctionalCommandService.cs +++ b/src/Core/src/Eventuous.Application/FunctionalService/FunctionalCommandService.cs @@ -17,7 +17,7 @@ namespace Eventuous; /// Optional function to add extra information to the event before it gets stored /// State object type public abstract class FunctionalCommandService(IEventReader reader, IEventWriter writer, TypeMapper? typeMap = null, AmendEvent? amendEvent = null) - : IFuncCommandService, IStateCommandService where TState : State, new() { + : ICommandService where TState : State, new() { readonly TypeMapper _typeMap = typeMap ?? TypeMap.Instance; readonly FuncHandlersMap _handlers = new(); @@ -108,16 +108,8 @@ public async Task> Handle(TCommand command, Cancellatio return new ErrorResult($"Error handling command {typeof(TCommand).Name}", e); } } - - async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken) { - var result = await Handle(command, cancellationToken).NoContext(); - - return result switch { - OkResult(var state, var enumerable, _) => new OkResult(state, enumerable), - ErrorResult error => new ErrorResult(error.Message, error.Exception), - _ => throw new ApplicationException("Unknown result type") - }; - } + + protected static StreamName GetStream(string id) => StreamName.ForState(id); readonly Dictionary> _builders = new(); readonly object _handlersLock = new(); diff --git a/src/Core/src/Eventuous.Application/ICommandService.cs b/src/Core/src/Eventuous.Application/ICommandService.cs index 8acc67ba..ed3553d4 100644 --- a/src/Core/src/Eventuous.Application/ICommandService.cs +++ b/src/Core/src/Eventuous.Application/ICommandService.cs @@ -5,20 +5,14 @@ namespace Eventuous; -public interface ICommandService { - Task Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class; -} - -public interface ICommandService : ICommandService where TAggregate : Aggregate; - -public interface IFuncCommandService : ICommandService where TState : State; +[Obsolete("Use ICommandService instead")] +public interface IFuncCommandService : ICommandService where TState : State, new(); -public interface IStateCommandService - where TState : State, new() { +public interface ICommandService where TState : State, new() { Task> Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class; } -public interface ICommandService : IStateCommandService +public interface ICommandService : ICommandService where T : Aggregate where TState : State, new() where TId : Id; \ No newline at end of file diff --git a/src/Core/src/Eventuous.Application/ThrowingCommandService.cs b/src/Core/src/Eventuous.Application/ThrowingCommandService.cs index f015490a..14b8ac42 100644 --- a/src/Core/src/Eventuous.Application/ThrowingCommandService.cs +++ b/src/Core/src/Eventuous.Application/ThrowingCommandService.cs @@ -4,7 +4,7 @@ using System.Runtime.ExceptionServices; namespace Eventuous; -public class ThrowingCommandService(ICommandService inner) : ICommandService, ICommandService +public class ThrowingCommandService(ICommandService inner) : ICommandService//, ICommandService where T : Aggregate where TState : State, new() where TId : Id { @@ -22,14 +22,14 @@ public async Task> Handle(TCommand command, Cancellatio return result; } - async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken) { - var result = await Handle(command, cancellationToken).NoContext(); - - return result switch { - OkResult(var aggregateState, var enumerable, _) => new OkResult(aggregateState, enumerable), - ErrorResult error => throw error.Exception - ?? new ApplicationException($"Error handling command {command}"), - _ => throw new ApplicationException("Unknown result type") - }; - } + // async Task ICommandService.Handle(TCommand command, CancellationToken cancellationToken) { + // var result = await Handle(command, cancellationToken).NoContext(); + // + // return result switch { + // OkResult(var aggregateState, var enumerable, _) => new OkResult(aggregateState, enumerable), + // ErrorResult error => throw error.Exception + // ?? new ApplicationException($"Error handling command {command}"), + // _ => throw new ApplicationException("Unknown result type") + // }; + // } } diff --git a/src/Core/src/Eventuous.Domain/Aggregate.cs b/src/Core/src/Eventuous.Domain/Aggregate.cs index 8d0aaa41..2c074bb6 100644 --- a/src/Core/src/Eventuous.Domain/Aggregate.cs +++ b/src/Core/src/Eventuous.Domain/Aggregate.cs @@ -4,7 +4,7 @@ namespace Eventuous; [PublicAPI] -public abstract class Aggregate { +public abstract class Aggregate where T : State, new() { /// /// The collection of previously persisted events /// @@ -21,14 +21,13 @@ public abstract class Aggregate { public IEnumerable Current => Original.Concat(_changes); /// - /// Clears all the pending changes. Normally not used. Can be used for testing purposes. + /// Clears all the pending changes. Normally not used. It Can be used for testing purposes. /// - public void ClearChanges() - => _changes.Clear(); + public void ClearChanges() => _changes.Clear(); /// /// The original version is the aggregate version we got from the store. - /// It is used for optimistic concurrency, to check if there were no changes made to the + /// It is used for optimistic concurrency to check if there were no changes made to the /// aggregate state between load and save for the current operation. /// public int OriginalVersion => Original.Length - 1; @@ -41,18 +40,11 @@ public void ClearChanges() readonly List _changes = []; - /// - /// Restores the aggregate state from a collection of events, previously stored in the AggregateStore/> - /// - /// Domain events from the aggregate stream - public abstract void Load(IEnumerable events); - /// /// Adds an event to the list of pending changes. /// /// New domain event - protected void AddChange(object evt) - => _changes.Add(evt); + protected void AddChange(object evt) => _changes.Add(evt); /// /// Use this method to ensure you are operating on a new aggregate. @@ -71,15 +63,13 @@ protected void EnsureExists(Func? getException = null) { if (CurrentVersion < 0) throw getException?.Invoke() ?? new DomainException($"{GetType().Name} doesn't exist"); } -} -public abstract class Aggregate : Aggregate where T : State, new() { /// /// Applies a new event to the state, adds the event to the list of pending changes, /// and increases the current version. /// /// New domain event to be applied - /// The previous and the new aggregate states + /// Previous and the new aggregate states protected (T PreviousState, T CurrentState) Apply(TEvent evt) where TEvent : class { AddChange(evt); var previous = State; @@ -88,15 +78,12 @@ protected void EnsureExists(Func? getException = null) { return (previous, State); } - /// - public override void Load(IEnumerable events) { + public void Load(IEnumerable events) { Original = events.Where(x => x != null).ToArray()!; - // ReSharper disable once ConvertClosureToMethodGroup - State = Original.Aggregate(new T(), (state, o) => Fold(state, o)); + State = Original.Aggregate(State, Fold); } - static T Fold(T state, object evt) - => state.When(evt); + static T Fold(T state, object evt) => state.When(evt); /// /// Returns the current aggregate state. Cannot be mutated from the outside. diff --git a/src/Core/src/Eventuous.Persistence/AggregateFactory.cs b/src/Core/src/Eventuous.Persistence/AggregateFactory.cs index 8082b9f7..04ddfab1 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateFactory.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateFactory.cs @@ -13,33 +13,30 @@ public class AggregateFactoryRegistry { /// public static readonly AggregateFactoryRegistry Instance = new(); - internal readonly Dictionary> Registry = new(); + internal readonly Dictionary> Registry = new(); /// /// Adds a custom aggregate factory to the registry /// /// Function to create a given aggregate type instance /// Aggregate type + /// Aggregate state type /// - public AggregateFactoryRegistry CreateAggregateUsing(AggregateFactory factory) where T : Aggregate { + public AggregateFactoryRegistry CreateAggregateUsing(AggregateFactory factory) + where T : Aggregate where TState : State, new() { Registry.TryAdd(typeof(T), () => factory()); return this; } - public void UnsafeCreateAggregateUsing(Type type, Func factory) where T : Aggregate => Registry.TryAdd(type, factory); + public void UnsafeCreateAggregateUsing(Type type, Func factory) + => Registry.TryAdd(type, factory); public T CreateInstance() where T : Aggregate where TState : State, new() { - var instance = CreateInstance(); - - return instance; - } - - public T CreateInstance() where T : Aggregate { var instance = Registry.TryGetValue(typeof(T), out var factory) ? (T)factory() : Activator.CreateInstance(); return instance; } } -public delegate T AggregateFactory() where T : Aggregate; +public delegate T AggregateFactory() where T : Aggregate where TState : State, new(); diff --git a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStore.cs b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStore.cs index 6e86f7f0..9818e515 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStore.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStore.cs @@ -39,17 +39,21 @@ public AggregateStore( public AggregateStore(IEventStore eventStore, AmendEvent? amendEvent = null, AggregateFactoryRegistry? factoryRegistry = null) : this(eventStore, eventStore, amendEvent, factoryRegistry) { } - public Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) where T : Aggregate - => _eventWriter.Store(streamName, aggregate, _amendEvent, cancellationToken); + /// + public Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() => _eventWriter.Store(streamName, aggregate, _amendEvent, cancellationToken); - public Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate - => LoadInternal(streamName, true, cancellationToken); + /// + public Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate where TState : State, new() + => LoadInternal(streamName, true, cancellationToken); - public Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate - => LoadInternal(streamName, false, cancellationToken); + /// + public Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() => LoadInternal(streamName, false, cancellationToken); - async Task LoadInternal(StreamName streamName, bool failIfNotFound, CancellationToken cancellationToken) where T : Aggregate { - var aggregate = _factoryRegistry.CreateInstance(); + async Task LoadInternal(StreamName streamName, bool failIfNotFound, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() { + var aggregate = _factoryRegistry.CreateInstance(); try { var events = await _eventReader.ReadStream(streamName, StreamReadPosition.Start, failIfNotFound, cancellationToken); @@ -57,9 +61,9 @@ async Task LoadInternal(StreamName streamName, bool failIfNotFound, Cancel } catch (StreamNotFound) when (!failIfNotFound) { return aggregate; } catch (Exception e) { - Log.UnableToLoadAggregate(streamName, e); + Log.UnableToLoadAggregate(streamName, e); - throw e is StreamNotFound ? new AggregateNotFoundException(streamName, e) : e; + throw e is StreamNotFound ? new AggregateNotFoundException(streamName, e) : e; } return aggregate; diff --git a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExceptions.cs b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExceptions.cs index c0b564e1..b0496439 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExceptions.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExceptions.cs @@ -11,14 +11,12 @@ public OptimisticConcurrencyException(StreamName streamName, Exception? inner) : base($"Update failed due to the wrong version in stream {streamName}", inner) { } } -public class OptimisticConcurrencyException(StreamName streamName, Exception? inner) : OptimisticConcurrencyException(typeof(T), streamName, inner) - where T : Aggregate; +public class OptimisticConcurrencyException(StreamName streamName, Exception? inner) : OptimisticConcurrencyException(typeof(T), streamName, inner) + where T : Aggregate where TState : State, new(); -public class AggregateNotFoundException : Exception { - public AggregateNotFoundException(Type aggregateType, StreamName streamName, Exception? inner) - : base($"Aggregate {aggregateType.Name} with not found in stream {streamName}", inner) { } -} +public class AggregateNotFoundException(Type aggregateType, StreamName streamName, Exception? inner) + : Exception($"Aggregate {aggregateType.Name} with not found in stream {streamName}", inner); -public class AggregateNotFoundException : AggregateNotFoundException where T : Aggregate { - public AggregateNotFoundException(StreamName streamName, Exception? inner) : base(typeof(T), streamName, inner) { } -} +public class AggregateNotFoundException(StreamName streamName, Exception? inner) : AggregateNotFoundException(typeof(T), streamName, inner) + where T : Aggregate + where TState : State, new(); diff --git a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExtensions.cs b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExtensions.cs index 7edf58b1..98532258 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExtensions.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreExtensions.cs @@ -17,7 +17,7 @@ public static class AggregateStoreExtensions { /// public static async Task Load(this IAggregateStore store, StreamNameMap streamNameMap, TId id, CancellationToken cancellationToken) where T : Aggregate where TId : Id where TState : State, new() { - var aggregate = await store.Load(streamNameMap.GetStreamName(id), cancellationToken).NoContext(); + var aggregate = await store.Load(streamNameMap.GetStreamName(id), cancellationToken).NoContext(); return aggregate.WithId(id); } @@ -35,7 +35,7 @@ public static async Task Load(this IAggregateStore store, Str /// public static async Task LoadOrNew(this IAggregateStore store, StreamNameMap streamNameMap, TId id, CancellationToken cancellationToken) where T : Aggregate where TId : Id where TState : State, new() { - var aggregate = await store.LoadOrNew(streamNameMap.GetStreamName(id), cancellationToken).NoContext(); + var aggregate = await store.LoadOrNew(streamNameMap.GetStreamName(id), cancellationToken).NoContext(); return aggregate.WithId(id); } diff --git a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreWithArchive.cs b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreWithArchive.cs index 4046146b..db01a53c 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreWithArchive.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateStore/AggregateStoreWithArchive.cs @@ -13,17 +13,21 @@ public class AggregateStore( ) : IAggregateStore where TReader : class, IEventReader { readonly AggregateFactoryRegistry _factoryRegistry = factoryRegistry ?? AggregateFactoryRegistry.Instance; - public Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) where T : Aggregate - => eventStore.Store(streamName, aggregate, amendEvent, cancellationToken); + /// + public Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() => eventStore.Store(streamName, aggregate, amendEvent, cancellationToken); - public Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate - => LoadInternal(streamName, true, cancellationToken); + /// + public Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate where TState : State, new() + => LoadInternal(streamName, true, cancellationToken); - public Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate - => LoadInternal(streamName, false, cancellationToken); + /// + public Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() => LoadInternal(streamName, false, cancellationToken); - async Task LoadInternal(StreamName streamName, bool failIfNotFound, CancellationToken cancellationToken) where T : Aggregate { - var aggregate = _factoryRegistry.CreateInstance(); + async Task LoadInternal(StreamName streamName, bool failIfNotFound, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new() { + var aggregate = _factoryRegistry.CreateInstance(); var hotEvents = await LoadStreamEvents(eventStore, StreamReadPosition.Start).NoContext(); @@ -35,7 +39,7 @@ async Task LoadInternal(StreamName streamName, bool failIfNotFound, Cancel var streamEvents = hotEvents.Concat(archivedEvents).Distinct(Comparer).ToArray(); if (streamEvents.Length == 0 && failIfNotFound) { - throw new AggregateNotFoundException(streamName, new StreamNotFound(streamName)); + throw new AggregateNotFoundException(streamName, new StreamNotFound(streamName)); } aggregate.Load(streamEvents.Select(x => x.Payload)); @@ -48,7 +52,7 @@ async Task LoadStreamEvents(IEventReader reader, StreamReadPositi } catch (StreamNotFound) { return []; } catch (Exception e) { - Log.UnableToLoadAggregate(streamName, e); + Log.UnableToLoadAggregate(streamName, e); throw; } diff --git a/src/Core/src/Eventuous.Persistence/AggregateStore/IAggregateStore.cs b/src/Core/src/Eventuous.Persistence/AggregateStore/IAggregateStore.cs index b98b43d9..ee08d81c 100644 --- a/src/Core/src/Eventuous.Persistence/AggregateStore/IAggregateStore.cs +++ b/src/Core/src/Eventuous.Persistence/AggregateStore/IAggregateStore.cs @@ -16,9 +16,11 @@ public interface IAggregateStore { /// Cancellation token /// Aggregate type /// Aggregate identity type + /// Aggregate state type /// - public Task Store(T aggregate, TId id, CancellationToken cancellationToken) where T : Aggregate where TId : Id - => Store(StreamNameFactory.For(id), aggregate, cancellationToken); + public Task Store(T aggregate, TId id, CancellationToken cancellationToken) + where T : Aggregate where TId : Id where TState : State, new() + => Store(StreamNameFactory.For(id), aggregate, cancellationToken); /// /// Store the new or updated aggregate state @@ -27,8 +29,10 @@ public Task Store(T aggregate, TId id, CancellationT /// Aggregate instance, which needs to be persisted /// Cancellation token /// Aggregate type + /// Aggregate state type /// - Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) where T : Aggregate; + Task Store(StreamName streamName, T aggregate, CancellationToken cancellationToken) + where T : Aggregate where TState : State, new(); /// /// Load the aggregate from the store for a given id @@ -36,10 +40,11 @@ public Task Store(T aggregate, TId id, CancellationT /// Aggregate id /// Cancellation token /// Aggregate type + /// Aggregate state type /// Aggregate identity type /// - public Task Load(TId id, CancellationToken cancellationToken) where T : Aggregate where TId : Id - => Load(StreamNameFactory.For(id), cancellationToken); + public Task Load(TId id, CancellationToken cancellationToken) + where T : Aggregate where TId : Id where TState : State, new() => Load(StreamNameFactory.For(id), cancellationToken); /// /// Load the aggregate from the store for a given id @@ -47,8 +52,9 @@ public Task Load(TId id, CancellationToken cancellationToken) where T /// /// Cancellation token /// Aggregate type + /// Aggregate state type /// - Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate; + Task Load(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate where TState : State, new(); /// /// Attempts to load the aggregate from the store for a given id. If the aggregate is not found, @@ -57,10 +63,12 @@ public Task Load(TId id, CancellationToken cancellationToken) where T /// Aggregate id as string /// Cancellation token /// Aggregate type + /// Aggregate state type /// Aggregate identity type /// - public Task LoadOrNew(TId id, CancellationToken cancellationToken) where T : Aggregate where TId : Id - => LoadOrNew(StreamNameFactory.For(id), cancellationToken); + public Task LoadOrNew(TId id, CancellationToken cancellationToken) + where T : Aggregate where TId : Id where TState : State, new() + => LoadOrNew(StreamNameFactory.For(id), cancellationToken); /// /// Attempts to load the aggregate from the store for a given id. If the aggregate is not found, @@ -69,6 +77,7 @@ public Task LoadOrNew(TId id, CancellationToken cancellationToken) wh /// Name of the aggregate stream /// Cancellation token /// Aggregate type + /// Aggregate state type /// - Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate; + Task LoadOrNew(StreamName streamName, CancellationToken cancellationToken) where T : Aggregate where TState : State, new(); } diff --git a/src/Core/src/Eventuous.Persistence/Diagnostics/PersistenceEventSource.cs b/src/Core/src/Eventuous.Persistence/Diagnostics/PersistenceEventSource.cs index 79b67a76..9315cb2c 100644 --- a/src/Core/src/Eventuous.Persistence/Diagnostics/PersistenceEventSource.cs +++ b/src/Core/src/Eventuous.Persistence/Diagnostics/PersistenceEventSource.cs @@ -17,12 +17,12 @@ public class PersistenceEventSource : EventSource { const int UnableToAppendEventsId = 7; [NonEvent] - public void UnableToLoadAggregate(StreamName streamName, Exception exception) where T : Aggregate { + public void UnableToLoadAggregate(StreamName streamName, Exception exception) where T : Aggregate where TState : State, new() { if (IsEnabled(EventLevel.Warning, EventKeywords.All)) UnableToLoadAggregate(typeof(T).Name, streamName, exception.ToString()); } [NonEvent] - public void UnableToStoreAggregate(StreamName streamName, Exception exception) where T : Aggregate { + public void UnableToStoreAggregate(StreamName streamName, Exception exception) where T : Aggregate where TState : State, new() { if (IsEnabled(EventLevel.Warning, EventKeywords.All)) UnableToStoreAggregate(typeof(T).Name, streamName, exception.ToString()); } diff --git a/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs b/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs index c44da285..50110bce 100644 --- a/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs +++ b/src/Core/src/Eventuous.Persistence/EventStore/StoreFunctions.cs @@ -64,25 +64,26 @@ StreamEvent ToStreamEvent(object evt, int position) { /// Optional: function to add extra information to the event before it gets stored /// Cancellation token /// Aggregate type + /// Aggregate state type /// Append event result - /// Gets thrown if the expected stream version mismatches with the given original stream version - public static async Task Store( + /// Gets thrown if the expected stream version mismatches with the given original stream version + public static async Task Store( this IEventWriter eventWriter, StreamName streamName, T aggregate, AmendEvent? amendEvent, CancellationToken cancellationToken - ) where T : Aggregate { + ) where T : Aggregate where TState : State, new() { Ensure.NotNull(aggregate); try { return await eventWriter.Store(streamName, aggregate.OriginalVersion, aggregate.Changes, amendEvent, cancellationToken).NoContext(); } catch (OptimisticConcurrencyException e) { - Log.UnableToStoreAggregate(streamName, e); + Log.UnableToStoreAggregate(streamName, e); throw e.InnerException is null - ? new OptimisticConcurrencyException(streamName, e) - : new OptimisticConcurrencyException(streamName, e.InnerException); + ? new OptimisticConcurrencyException(streamName, e) + : new OptimisticConcurrencyException(streamName, e.InnerException); } } diff --git a/src/Core/src/Eventuous.Persistence/StreamNameFactory.cs b/src/Core/src/Eventuous.Persistence/StreamNameFactory.cs index d1fbc630..e8e25083 100644 --- a/src/Core/src/Eventuous.Persistence/StreamNameFactory.cs +++ b/src/Core/src/Eventuous.Persistence/StreamNameFactory.cs @@ -4,9 +4,6 @@ namespace Eventuous; public static class StreamNameFactory { - public static StreamName For(TId id) where T : Aggregate where TId : Id - => new($"{typeof(T).Name}-{Ensure.NotEmptyString(id.ToString())}"); - public static StreamName For(TId id) where T : Aggregate where TState : State, new() where TId : Id => new($"{typeof(T).Name}-{Ensure.NotEmptyString(id.ToString())}"); } diff --git a/src/Core/src/Eventuous.Persistence/StreamNameMap.cs b/src/Core/src/Eventuous.Persistence/StreamNameMap.cs index 609254be..f81ec71c 100644 --- a/src/Core/src/Eventuous.Persistence/StreamNameMap.cs +++ b/src/Core/src/Eventuous.Persistence/StreamNameMap.cs @@ -8,14 +8,13 @@ namespace Eventuous; public class StreamNameMap { readonly TypeMap> _typeMap = new(); - public void Register(Func map) where TId : Id - => _typeMap.Add(id => map((TId)id)); + public void Register(Func map) where TId : Id => _typeMap.Add(id => map((TId)id)); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public StreamName GetStreamName(TId aggregateId) where TId : Id where T : Aggregate + public StreamName GetStreamName(TId aggregateId) where TId : Id where T : Aggregate where TState : State, new() => _typeMap.TryGetValue(out var map) ? map(aggregateId) - : StreamNameFactory.For(aggregateId); + : StreamNameFactory.For(aggregateId); [MethodImpl(MethodImplOptions.AggressiveInlining)] public StreamName GetStreamName(TId id) where TId : Id @@ -24,5 +23,4 @@ public StreamName GetStreamName(TId id) where TId : Id : throw new StreamNameMapNotFound(id); } -public class StreamNameMapNotFound(TId id) : Exception($"No stream name map found for {typeof(TId).Name} with value {id}") - where TId : Id; +public class StreamNameMapNotFound(TId id) : Exception($"No stream name map found for {typeof(TId).Name} with value {id}") where TId : Id; diff --git a/src/Core/src/Eventuous.Shared/Store/StreamName.cs b/src/Core/src/Eventuous.Shared/Store/StreamName.cs index d2ce5581..a4f0ab48 100644 --- a/src/Core/src/Eventuous.Shared/Store/StreamName.cs +++ b/src/Core/src/Eventuous.Shared/Store/StreamName.cs @@ -15,11 +15,25 @@ public StreamName(string value) { public static StreamName For(string entityId) => new($"{typeof(T).Name}-{Ensure.NotEmptyString(entityId)}"); - public string GetId() => Value[(Value.IndexOf("-", StringComparison.InvariantCulture) + 1)..]; - + public static StreamName ForState(string entityId) { + var stateName = typeof(TState).Name; + + if (stateName.EndsWith("State")) { + stateName = stateName[..^SuffixLength]; + } + + stateName = stateName.Length > 0 ? stateName : typeof(TState).Name; + + return new StreamName($"{stateName}-{Ensure.NotEmptyString(entityId)}"); + } + + public string GetId() => Value[(Value.IndexOf('-') + 1)..]; + public static implicit operator string(StreamName streamName) => streamName.Value; public override string ToString() => Value; + + static readonly int SuffixLength = "State".Length; } public class InvalidStreamName(string? streamName) : Exception($"Stream name is {(string.IsNullOrWhiteSpace(streamName) ? "empty" : "invalid")}"); diff --git a/src/Core/src/Eventuous.Shared/Tools/Ensure.cs b/src/Core/src/Eventuous.Shared/Tools/Ensure.cs index 98b53be7..8680fdc7 100644 --- a/src/Core/src/Eventuous.Shared/Tools/Ensure.cs +++ b/src/Core/src/Eventuous.Shared/Tools/Ensure.cs @@ -17,13 +17,19 @@ static class Ensure { /// [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T NotNull(T? value, [CallerArgumentExpression("value")] string? name = default) where T : class - => value ?? throw new ArgumentNullException(name); + public static T NotNull(T? value, [CallerArgumentExpression("value")] string? name = default) where T : class { + ArgumentNullException.ThrowIfNull(value, name); + + return value; + } [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T NotNull(T? value, [CallerArgumentExpression("value")] string? name = default) where T : struct - => value ?? throw new ArgumentNullException(name); + public static T NotNull(T? value, [CallerArgumentExpression("value")] string? name = default) where T : struct { + ArgumentNullException.ThrowIfNull(value, name); + + return value.Value; + } /// /// Checks if the string is not null or empty, otherwise throws @@ -34,8 +40,14 @@ public static T NotNull(T? value, [CallerArgumentExpression("value")] string? /// [DebuggerHidden] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string NotEmptyString(string? value, [CallerArgumentExpression("value")] string? name = default) - => !string.IsNullOrWhiteSpace(value) ? value : throw new ArgumentNullException(name); + public static string NotEmptyString(string? value, [CallerArgumentExpression("value")] string? name = default) { +#if NET8_0_OR_GREATER + ArgumentNullException.ThrowIfNullOrWhiteSpace(value, name); + return value; +#else + return !string.IsNullOrWhiteSpace(value) ? value : throw new ArgumentNullException(name); +#endif + } /// /// Throws a custom exception if the condition is not met diff --git a/src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkers.cs b/src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkers.cs index 13be6810..c4a22236 100644 --- a/src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkers.cs +++ b/src/Core/src/Eventuous.Subscriptions/Channels/ChannelWorkers.cs @@ -5,27 +5,17 @@ namespace Eventuous.Subscriptions.Channels; -class ChannelWorker : ChannelWorkerBase { - /// - /// Creates a new instance of the channel worker, starts a task for background reads - /// - /// Channel to use for writes and reads - /// Function to process each element the worker reads from the channel - /// Throw if the channel is full to prevent partition blocks - public ChannelWorker(Channel channel, ProcessElement process, bool throwOnFull = false) - : base(channel, token => channel.Read(process, token), 1, throwOnFull) { } -} +class ChannelWorker(Channel channel, ProcessElement process, bool throwOnFull = false) + : ChannelWorkerBase(channel, token => channel.Read(process, token), 1, throwOnFull); -sealed class ConcurrentChannelWorker : ChannelWorkerBase { - /// - /// Creates a new instance of the channel worker, starts a task for background reads - /// - /// Channel to use for writes and reads - /// Function to process each element the worker reads from the channel - /// - public ConcurrentChannelWorker(Channel channel, ProcessElement process, int concurrencyLevel) - : base(channel, token => channel.Read(process, token), concurrencyLevel) { } -} +/// +/// Creates a new instance of the channel worker, starts a task for background reads +/// +/// Channel to use for writes and reads +/// Function to process each element the worker reads from the channel +/// +sealed class ConcurrentChannelWorker(Channel channel, ProcessElement process, int concurrencyLevel) + : ChannelWorkerBase(channel, token => channel.Read(process, token), concurrencyLevel); class BatchedChannelWorker(Channel channel, ProcessElement processor, int maxCount, TimeSpan maxTime, bool throwOnFull = false) : ChannelWorkerBase(channel, token => channel.ReadBatches(processor, maxCount, maxTime, token), 1, throwOnFull); diff --git a/src/Core/src/Eventuous.Subscriptions/Checkpoints/CheckpointCommitHandler.cs b/src/Core/src/Eventuous.Subscriptions/Checkpoints/CheckpointCommitHandler.cs index da0de43b..77d5a4df 100644 --- a/src/Core/src/Eventuous.Subscriptions/Checkpoints/CheckpointCommitHandler.cs +++ b/src/Core/src/Eventuous.Subscriptions/Checkpoints/CheckpointCommitHandler.cs @@ -70,7 +70,7 @@ async ValueTask Process(CommitPosition[] list, CancellationToken cancellationTok } /// - /// Commit a position to be stored, the store action can be delayed + /// Commit a position to be stored; the store action can be delayed /// /// Position to commit /// Cancellation token diff --git a/src/Core/src/Eventuous.Subscriptions/Checkpoints/MeasuredCheckpointStore.cs b/src/Core/src/Eventuous.Subscriptions/Checkpoints/MeasuredCheckpointStore.cs index acfc2d16..c031d47b 100644 --- a/src/Core/src/Eventuous.Subscriptions/Checkpoints/MeasuredCheckpointStore.cs +++ b/src/Core/src/Eventuous.Subscriptions/Checkpoints/MeasuredCheckpointStore.cs @@ -14,10 +14,7 @@ public class MeasuredCheckpointStore(ICheckpointStore checkpointStore) : ICheckp public const string SubscriptionIdTag = "subscriptionId"; public const string CheckpointBaggage = "checkpoint"; - public async ValueTask GetLastCheckpoint( - string checkpointId, - CancellationToken cancellationToken - ) { + public async ValueTask GetLastCheckpoint(string checkpointId, CancellationToken cancellationToken) { using var activity = EventuousDiagnostics.ActivitySource.CreateActivity( ReadOperationName, ActivityKind.Internal, @@ -51,7 +48,5 @@ public async ValueTask StoreCheckpoint(Checkpoint checkpoint, bool f } static KeyValuePair[] GetTags(string checkpointId) - => EventuousDiagnostics.CombineWithDefaultTags( - new KeyValuePair(SubscriptionIdTag, checkpointId) - ); + => EventuousDiagnostics.CombineWithDefaultTags(new KeyValuePair(SubscriptionIdTag, checkpointId)); } diff --git a/src/Core/src/Eventuous.Subscriptions/Context/AsyncConsumeContext.cs b/src/Core/src/Eventuous.Subscriptions/Context/AsyncConsumeContext.cs index 1c88cf20..ff91863d 100644 --- a/src/Core/src/Eventuous.Subscriptions/Context/AsyncConsumeContext.cs +++ b/src/Core/src/Eventuous.Subscriptions/Context/AsyncConsumeContext.cs @@ -28,8 +28,7 @@ public class AsyncConsumeContext : WrappedConsumeContext { /// The original message context /// Function to ACK the message /// Function to NACK the message in case of failure - public AsyncConsumeContext(IMessageConsumeContext inner, Acknowledge acknowledge, Fail fail) - : base(inner) { + public AsyncConsumeContext(IMessageConsumeContext inner, Acknowledge acknowledge, Fail fail) : base(inner) { // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract inner.LogContext ??= Logger.Current; _acknowledge = acknowledge; diff --git a/src/Core/src/Eventuous.Subscriptions/Context/ContextResultExtensions.cs b/src/Core/src/Eventuous.Subscriptions/Context/ContextResultExtensions.cs index 1acab48d..2f00d00f 100644 --- a/src/Core/src/Eventuous.Subscriptions/Context/ContextResultExtensions.cs +++ b/src/Core/src/Eventuous.Subscriptions/Context/ContextResultExtensions.cs @@ -11,21 +11,23 @@ namespace Eventuous.Subscriptions.Context; public static class ContextResultExtensions { /// - /// Allows to acknowledge the message by a specific handler, identified by a string + /// Allows acknowledging the message by a specific handler, identified by a string /// /// Consume context /// Handler type identifier + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Ack(this IBaseConsumeContext context, string handlerType) { context.HandlingResults.Add(EventHandlingResult.Succeeded(handlerType)); context.LogContext.MessageHandled(handlerType, context); } /// - /// Allows to convey the message handling failure that occurred in a specific handler + /// Allows conveying the message handling failure that occurred in a specific handler /// /// Message context /// Handler type identifier /// Optional: handler exception + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Nack(this IBaseConsumeContext context, string handlerType, Exception? exception) { context.HandlingResults.Add(EventHandlingResult.Failed(handlerType, exception)); @@ -39,39 +41,40 @@ public static void Nack(this IBaseConsumeContext context, string handlerType, Ex } /// - /// Allows to convey the fact that the message was ignored by the handler + /// Allows conveying the fact that the message was ignored by the handler /// /// Consume context /// Handler type identifier + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Ignore(this IBaseConsumeContext context, string handlerType) { context.HandlingResults.Add(EventHandlingResult.Ignored(handlerType)); context.LogContext.MessageIgnored(handlerType, context); } /// - /// Allows to acknowledge the message by a specific handler, identified by a string + /// Allows acknowledging the message by a specific handler, identified by a string /// /// Consume context /// Handler type - public static void Ack(this IBaseConsumeContext context) - => context.Ack(typeof(T).Name); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Ack(this IBaseConsumeContext context) => context.Ack(typeof(T).Name); /// - /// Allows to convey the fact that the message was ignored by the handler + /// Allows conveying the fact that the message was ignored by the handler /// /// Consume context /// Handler type - public static void Ignore(this IBaseConsumeContext context) - => context.Ignore(typeof(T).Name); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Ignore(this IBaseConsumeContext context) => context.Ignore(typeof(T).Name); /// - /// Allows to convey the message handling failure that occurred in a specific handler + /// Allows conveying the message handling failure that occurred in a specific handler /// /// Consume context /// Optional: handler exception /// Handler type - public static void Nack(this IBaseConsumeContext context, Exception? exception) - => context.Nack(typeof(T).Name, exception); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Nack(this IBaseConsumeContext context, Exception? exception) => context.Nack(typeof(T).Name, exception); /// /// Returns true if the message was ignored by all handlers @@ -91,6 +94,7 @@ public static bool WasIgnored(this IBaseConsumeContext context) { /// /// Consume context /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasFailed(this IBaseConsumeContext context) => context.HandlingResults.GetFailureStatus() == EventHandlingStatus.Failure; } diff --git a/src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs b/src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs index c755686a..a28a8b49 100644 --- a/src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs +++ b/src/Core/src/Eventuous.Subscriptions/Diagnostics/CheckpointCommitMetrics.cs @@ -21,14 +21,10 @@ protected override void OnEvent(KeyValuePair evt) { } public DateTime GetLastTimestamp(string subscriptionId) - => _commitEvents.TryGetValue(subscriptionId, out var commitEvent) - ? commitEvent.CommitPosition.Timestamp - : DateTime.MinValue; + => _commitEvents.TryGetValue(subscriptionId, out var commitEvent) ? commitEvent.CommitPosition.Timestamp : DateTime.MinValue; public ulong GetLastCommitPosition(string subscriptionId) - => _commitEvents.TryGetValue(subscriptionId, out var commitEvent) - ? commitEvent.CommitPosition.Position - : 0; + => _commitEvents.TryGetValue(subscriptionId, out var commitEvent) ? commitEvent.CommitPosition.Position : 0; public IEnumerable> Record() => _commitEvents diff --git a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionActivity.cs b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionActivity.cs index a5c3538a..240bdd4a 100644 --- a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionActivity.cs +++ b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionActivity.cs @@ -60,11 +60,5 @@ static class SubscriptionActivity { ActivityContext? parentContext = null, IEnumerable>? tags = null ) - => EventuousDiagnostics.ActivitySource.CreateActivity( - name, - activityKind, - parentContext ?? default, - tags, - idFormat: ActivityIdFormat.W3C - ); + => EventuousDiagnostics.ActivitySource.CreateActivity(name, activityKind, parentContext ?? default, tags, idFormat: ActivityIdFormat.W3C); } diff --git a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionHealth.cs b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionHealth.cs index ad802a25..6d2745c8 100644 --- a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionHealth.cs +++ b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionHealth.cs @@ -37,9 +37,7 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc return Task.FromResult(result); } - public void ReportHealthy(string subscriptionId) - => _healthReports[subscriptionId] = HealthReport.Healthy(); + public void ReportHealthy(string subscriptionId) => _healthReports[subscriptionId] = HealthReport.Healthy(); - public void ReportUnhealthy(string subscriptionId, Exception? exception) - => _healthReports[subscriptionId] = HealthReport.Unhealthy(exception); + public void ReportUnhealthy(string subscriptionId, Exception? exception) => _healthReports[subscriptionId] = HealthReport.Unhealthy(exception); } diff --git a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs index fcafd0bd..1cd34ac5 100644 --- a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs +++ b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionMetrics.cs @@ -67,6 +67,8 @@ public SubscriptionMetrics(IEnumerable measures) { _listener = new MetricsListener(ListenerName, duration, errorCount, GetTags); + return; + IEnumerable> ObserveTimeValues() => streams.Values.Select( x => Measure( @@ -87,6 +89,7 @@ Measurement Measure(T value, string subscriptionId) where T : struct { } var tags = new List>(_customTags) { SubTag(subscriptionId) }; + return new Measurement(value, tags); } @@ -115,10 +118,11 @@ TagList GetTags(SubscriptionMetricsContext ctx) { var endOfStream = t.IsCompletedSuccessfully ? t.Result : t.NoContext().GetAwaiter().GetResult(); streams[endOfStream.SubscriptionId] = endOfStream; var lastProcessed = _checkpointMetrics.GetLastCommitPosition(endOfStream.SubscriptionId); + return (endOfStream, lastProcessed); - } - catch (Exception e) { + } catch (Exception e) { Log.MetricCollectionFailed("Subscription Gap", e); + return (EndOfStream.Invalid, 0); } } @@ -128,16 +132,15 @@ static IEnumerable> TryObserving(string metric, ObserveMetric< where T : struct { try { return observe(); - } - catch (Exception e) { + } catch (Exception e) { Log.MetricCollectionFailed(metric, e); Activity.Current?.SetStatus(ActivityStatusCode.Error, e.Message); + return Array.Empty>(); } } - static KeyValuePair SubTag(object? id) - => new(SubscriptionIdTag, id); + static KeyValuePair SubTag(object? id) => new(SubscriptionIdTag, id); readonly Meter _meter = EventuousDiagnostics.GetMeter(MeterName); diff --git a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionsEventSource.cs b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionsEventSource.cs index d5ad69d5..5e0a18de 100644 --- a/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionsEventSource.cs +++ b/src/Core/src/Eventuous.Subscriptions/Diagnostics/SubscriptionsEventSource.cs @@ -23,12 +23,10 @@ public class SubscriptionsEventSource : EventSource { const int CheckpointLastCommitDuplicateId = 105; [NonEvent] - public void MetricCollectionFailed(string metric, Exception exception) - => MetricCollectionFailed(metric, exception.ToString()); + public void MetricCollectionFailed(string metric, Exception exception) => MetricCollectionFailed(metric, exception.ToString()); [NonEvent] - public void MessageTypeNotRegistered() - => MessageTypeNotRegistered(typeof(T).Name); + public void MessageTypeNotRegistered() => MessageTypeNotRegistered(typeof(T).Name); [NonEvent] public void CheckpointAlreadyCommitted(string id, CommitPosition checkpoint) { @@ -38,12 +36,7 @@ public void CheckpointAlreadyCommitted(string id, CommitPosition checkpoint) { [NonEvent] public void CheckpointLastCommitGap(CommitPosition lastCommitPosition, CommitPosition latestPosition) { if (IsEnabled(EventLevel.Verbose, Keywords.Checkpoints)) - CheckpointLastCommitGap( - lastCommitPosition.Sequence, - lastCommitPosition.Position, - latestPosition.Sequence, - latestPosition.Position - ); + CheckpointLastCommitGap(lastCommitPosition.Sequence, lastCommitPosition.Position, latestPosition.Sequence, latestPosition.Position); } [NonEvent] @@ -62,30 +55,47 @@ public void CheckpointGapDetected(CommitPosition before, CommitPosition after) { } [Event(MetricCollectionFailedId, Message = "Failed to collect metric {0}: {1}", Level = EventLevel.Warning)] - void MetricCollectionFailed(string metric, string exception) - => WriteEvent(MetricCollectionFailedId, metric, exception); + void MetricCollectionFailed(string metric, string exception) => WriteEvent(MetricCollectionFailedId, metric, exception); [Event(MessageTypeNotRegisteredId, Message = "Message type {0} is not registered", Level = EventLevel.Warning)] - void MessageTypeNotRegistered(string messageType) - => WriteEvent(MessageTypeNotRegisteredId, messageType); + void MessageTypeNotRegistered(string messageType) => WriteEvent(MessageTypeNotRegisteredId, messageType); [Event(CheckpointAlreadyCommittedId, Message = "Checkpoint already committed {0}:{1}", Level = EventLevel.Verbose, Keywords = Keywords.Checkpoints)] - void CheckpointAlreadyCommitted(string id, ulong position) - => WriteEvent(CheckpointAlreadyCommittedId, id, position); - - [Event(CheckpointLastCommitGapId, Message = "Last commit position {0}:{1} is behind latest {2}:{3}", Level = EventLevel.Verbose, Keywords = Keywords.Checkpoints)] + void CheckpointAlreadyCommitted(string id, ulong position) => WriteEvent(CheckpointAlreadyCommittedId, id, position); + + [Event( + CheckpointLastCommitGapId, + Message = "Last commit position {0}:{1} is behind latest {2}:{3}", + Level = EventLevel.Verbose, + Keywords = Keywords.Checkpoints + )] void CheckpointLastCommitGap(ulong lastCommitSequence, ulong lastCommitPosition, ulong latestSequence, ulong latestPosition) => WriteEvent(CheckpointLastCommitGapId, lastCommitSequence, lastCommitPosition, latestSequence, latestPosition); - [Event(CheckpointLastCommitDuplicateId, Message = "Last commit position {0}:{1} equals the latest", Level = EventLevel.Warning, Keywords = Keywords.Checkpoints)] + [Event( + CheckpointLastCommitDuplicateId, + Message = "Last commit position {0}:{1} equals the latest", + Level = EventLevel.Warning, + Keywords = Keywords.Checkpoints + )] void CheckpointLastCommitDuplicate(ulong latestSequence, ulong latestPosition) => WriteEvent(CheckpointLastCommitDuplicateId, latestSequence, latestPosition); - [Event(CheckpointSequenceInvalidHeadId, Message = "Last commit position {0}:{1} sequence is not zero", Level = EventLevel.Verbose, Keywords = Keywords.Checkpoints)] + [Event( + CheckpointSequenceInvalidHeadId, + Message = "Last commit position {0}:{1} sequence is not zero", + Level = EventLevel.Verbose, + Keywords = Keywords.Checkpoints + )] void CheckpointSequenceInvalidHead(ulong sequence, ulong position) => WriteEvent(CheckpointSequenceInvalidHeadId, sequence, position); - [Event(CheckpointGapDetectedId, Message = "Gap detected in checkpoint between {0}:{1} and {2}:{3}", Level = EventLevel.Verbose, Keywords = Keywords.Checkpoints)] + [Event( + CheckpointGapDetectedId, + Message = "Gap detected in checkpoint between {0}:{1} and {2}:{3}", + Level = EventLevel.Verbose, + Keywords = Keywords.Checkpoints + )] void CheckpointGapDetected(ulong beforeSequence, ulong beforePosition, ulong afterSequence, ulong afterPosition) => WriteEvent(CheckpointGapDetectedId, beforeSequence, beforePosition, afterSequence, afterPosition); diff --git a/src/Core/src/Eventuous.Subscriptions/EventSubscription.cs b/src/Core/src/Eventuous.Subscriptions/EventSubscription.cs index 0209d7a4..cec6503a 100644 --- a/src/Core/src/Eventuous.Subscriptions/EventSubscription.cs +++ b/src/Core/src/Eventuous.Subscriptions/EventSubscription.cs @@ -53,15 +53,14 @@ public async ValueTask Subscribe(OnSubscribed onSubscribed, OnDropped onDropped, _onDropped = onDropped; await Subscribe(cts.Token).NoContext(); IsRunning = true; - Log.InfoLog?.Log("Started"); - + Log.SubscriptionStarted(); onSubscribed(Options.SubscriptionId); } public async ValueTask Unsubscribe(OnUnsubscribed onUnsubscribed, CancellationToken cancellationToken) { IsRunning = false; await Unsubscribe(cancellationToken).NoContext(); - Log.InfoLog?.Log("Unsubscribed"); + Log.SubscriptionStopped(); onUnsubscribed(Options.SubscriptionId); await Finalize(cancellationToken); Sequence = 0; @@ -117,7 +116,7 @@ protected async ValueTask Handler(IMessageConsumeContext context) { if (context.WasIgnored() && activity != null) activity.ActivityTraceFlags = ActivityTraceFlags.None; } catch (OperationCanceledException e) when (Stopping.IsCancellationRequested) { - Log.DebugLog?.Log("Message ignored because subscription is stopping: {Message}", e.Message); + Log.MessageIgnoredWhenStopping(e); } catch (Exception e) { context.Nack(SubscriptionId, e); } if (context.HasFailed()) { @@ -151,7 +150,6 @@ protected async ValueTask Handler(IMessageConsumeContext context) { }; } catch (Exception e) { var exception = new DeserializationException(stream, eventType, position, e); - Log.PayloadDeserializationFailed(stream, position, eventType, exception); if (Options.ThrowOnError) throw; @@ -176,16 +174,16 @@ protected virtual async Task Resubscribe(TimeSpan delay, CancellationToken cance while (IsRunning && IsDropped && !cancellationToken.IsCancellationRequested) { try { - Log.WarnLog?.Log("Resubscribing"); + Log.SubscriptionResubscribing(); await Subscribe(cancellationToken).NoContext(); IsDropped = false; _onSubscribed?.Invoke(Options.SubscriptionId); - Log.InfoLog?.Log("Resubscribed"); + Log.SubscriptionResubscribed(); } catch (OperationCanceledException) { } catch (Exception e) { - Log.ErrorLog?.Log(e, "Failed to resubscribe"); + Log.SubscriptionResubscribeFailed(e); await Task.Delay(1000, cancellationToken).NoContext(); } } @@ -194,7 +192,7 @@ protected virtual async Task Resubscribe(TimeSpan delay, CancellationToken cance protected void Dropped(DropReason reason, Exception? exception) { if (!IsRunning) return; - Log.WarnLog?.Log(exception, "Dropped: {Reason}", reason); + Log.SubscriptionDropped(reason, exception); IsDropped = true; _onDropped?.Invoke(Options.SubscriptionId, reason, exception); @@ -202,10 +200,9 @@ protected void Dropped(DropReason reason, Exception? exception) { Task.Run( async () => { var delay = reason == DropReason.Stopped ? TimeSpan.FromSeconds(10) : TimeSpan.FromSeconds(2); + Log.SubscriptionWillResubscribe(delay); - Log.WarnLog?.Log($"Will resubscribe after {delay}"); - - try { await Resubscribe(delay, Stopping.Token); } catch (Exception e) { + try { await Resubscribe(delay, Stopping.Token).NoContext(); } catch (Exception e) { Log.WarnLog?.Log(e.Message); throw; @@ -219,7 +216,7 @@ protected void Dropped(DropReason reason, Exception? exception) { public async ValueTask DisposeAsync() { if (_disposed) return; - await Pipe.DisposeAsync(); + await Pipe.DisposeAsync().NoContext(); // Stopping.Dispose(); _disposed = true; diff --git a/src/Core/src/Eventuous.Subscriptions/Exceptions.cs b/src/Core/src/Eventuous.Subscriptions/Exceptions.cs index f40056fd..2a81990a 100644 --- a/src/Core/src/Eventuous.Subscriptions/Exceptions.cs +++ b/src/Core/src/Eventuous.Subscriptions/Exceptions.cs @@ -3,22 +3,19 @@ using System.Collections; -namespace Eventuous.Subscriptions; +namespace Eventuous.Subscriptions; public class DeserializationException : Exception { public DeserializationException(string stream, string eventType, ulong position, Exception e) - : base($"Error deserializing event {stream} {position} {eventType}", e) { - } - + : base($"Error deserializing event {stream} {position} {eventType}", e) { } + public DeserializationException(string stream, string eventType, ulong position, string message) - : base($"Error deserializing event {stream} {position} {eventType}: {message}") { - } + : base($"Error deserializing event {stream} {position} {eventType}: {message}") { } } public class SubscriptionException : Exception { - public SubscriptionException(string stream, string eventType, object? evt, Exception e) - : base($"Error processing event {stream} {eventType}", e) + public SubscriptionException(string stream, string eventType, object? evt, Exception e) : base($"Error processing event {stream} {eventType}", e) => Data.Add("Event", evt); public sealed override IDictionary Data { get; } = new Dictionary(); -} \ No newline at end of file +} diff --git a/src/Core/src/Eventuous.Subscriptions/Filters/AsyncHandlingFilter.cs b/src/Core/src/Eventuous.Subscriptions/Filters/AsyncHandlingFilter.cs index cd81c37a..80632066 100644 --- a/src/Core/src/Eventuous.Subscriptions/Filters/AsyncHandlingFilter.cs +++ b/src/Core/src/Eventuous.Subscriptions/Filters/AsyncHandlingFilter.cs @@ -21,12 +21,7 @@ public AsyncHandlingFilter(uint concurrencyLimit, uint bufferSize = 10) { SingleReader = concurrencyLimit == 1, SingleWriter = true }; - _worker = new ConcurrentChannelWorker( - Channel.CreateBounded(options), - // ReSharper disable once ConvertClosureToMethodGroup - (task, token) => DelayedConsume(task, token), - (int)concurrencyLimit - ); + _worker = new ConcurrentChannelWorker(Channel.CreateBounded(options), DelayedConsume, (int)concurrencyLimit); } // ReSharper disable once CognitiveComplexity diff --git a/src/Core/src/Eventuous.Subscriptions/Filters/ConsumePipe.cs b/src/Core/src/Eventuous.Subscriptions/Filters/ConsumePipe.cs index b4db81ee..3d1893e1 100644 --- a/src/Core/src/Eventuous.Subscriptions/Filters/ConsumePipe.cs +++ b/src/Core/src/Eventuous.Subscriptions/Filters/ConsumePipe.cs @@ -46,8 +46,7 @@ public ConsumePipe AddFilterLast(IConsumeFilter filter) return this; } - public ValueTask Send(IBaseConsumeContext context) - => Move(_filters.First, context); + public ValueTask Send(IBaseConsumeContext context) => Move(_filters.First, context); static ValueTask Move(LinkedListNode? node, IBaseConsumeContext context) => node == null ? default : node.Value.Send(context, node.Next); diff --git a/src/Core/src/Eventuous.Subscriptions/Filters/ConsumerFilter.cs b/src/Core/src/Eventuous.Subscriptions/Filters/ConsumerFilter.cs index 22ad566c..f5ec59e7 100644 --- a/src/Core/src/Eventuous.Subscriptions/Filters/ConsumerFilter.cs +++ b/src/Core/src/Eventuous.Subscriptions/Filters/ConsumerFilter.cs @@ -10,6 +10,5 @@ namespace Eventuous.Subscriptions.Filters; public class ConsumerFilter(IMessageConsumer consumer) : ConsumeFilter { [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override ValueTask Send(IMessageConsumeContext context, LinkedListNode? next) - => consumer.Consume(context); + protected override ValueTask Send(IMessageConsumeContext context, LinkedListNode? next) => consumer.Consume(context); } diff --git a/src/Core/src/Eventuous.Subscriptions/Filters/IConsumeFilter.cs b/src/Core/src/Eventuous.Subscriptions/Filters/IConsumeFilter.cs index 754d4231..d451b4d8 100644 --- a/src/Core/src/Eventuous.Subscriptions/Filters/IConsumeFilter.cs +++ b/src/Core/src/Eventuous.Subscriptions/Filters/IConsumeFilter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. // ReSharper disable UnusedTypeParameter + namespace Eventuous.Subscriptions.Filters; using Context; @@ -24,16 +25,10 @@ public abstract class ConsumeFilter : IConsumeFilter public ValueTask Send(IBaseConsumeContext context, LinkedListNode? next) { if (context is not TIn ctx) - throw new ArgumentException( - $"Context type expected to be {typeof(TIn)} but it is {context.GetType().Name}", - nameof(context) - ); + throw new ArgumentException($"Context type expected to be {typeof(TIn)} but it is {context.GetType().Name}", nameof(context)); if (next != null && !next.Value.Consumes.IsAssignableFrom(typeof(TOut))) - throw new ArgumentException( - $"Next filter type expected to consume {typeof(TIn)} but it consumes {next.Value.Consumes}", - nameof(next) - ); + throw new ArgumentException($"Next filter type expected to consume {typeof(TIn)} but it consumes {next.Value.Consumes}", nameof(next)); return Send(ctx, next); } @@ -43,5 +38,4 @@ public ValueTask Send(IBaseConsumeContext context, LinkedListNode typeof(TOut); } -public abstract class ConsumeFilter : ConsumeFilter - where TContext : class, IBaseConsumeContext; +public abstract class ConsumeFilter : ConsumeFilter where TContext : class, IBaseConsumeContext; diff --git a/src/Core/src/Eventuous.Subscriptions/Filters/PartitioningFilter.cs b/src/Core/src/Eventuous.Subscriptions/Filters/PartitioningFilter.cs index b35507a6..6a66009f 100644 --- a/src/Core/src/Eventuous.Subscriptions/Filters/PartitioningFilter.cs +++ b/src/Core/src/Eventuous.Subscriptions/Filters/PartitioningFilter.cs @@ -14,19 +14,17 @@ public sealed class PartitioningFilter : ConsumeFilter, IAs readonly int _partitionCount; public PartitioningFilter( - int partitionCount, - GetPartitionKey? partitioner = null, - GetPartitionHash? getHash = null - ) { + int partitionCount, + GetPartitionKey? partitioner = null, + GetPartitionHash? getHash = null + ) { if (partitionCount <= 0) throw new ArgumentOutOfRangeException(nameof(partitionCount), "Partition count must be greater than zero"); _getHash = getHash ?? MurmurHash3.Hash; _partitionCount = partitionCount; _partitioner = partitioner ?? (ctx => ctx.Stream); - _filters = Enumerable.Range(0, _partitionCount) - .Select(_ => new AsyncHandlingFilter(1)) - .ToArray(); + _filters = Enumerable.Range(0, _partitionCount).Select(_ => new AsyncHandlingFilter(1)).ToArray(); } protected override ValueTask Send(AsyncConsumeContext context, LinkedListNode? next) { @@ -35,6 +33,7 @@ protected override ValueTask Send(AsyncConsumeContext context, LinkedListNode DiagnosticName = GetType().Name; + protected BaseEventHandler() => DiagnosticName = GetType().Name; public string DiagnosticName { get; } diff --git a/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs b/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs index 13457f36..162c4980 100644 --- a/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs +++ b/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandler.cs @@ -36,6 +36,8 @@ protected void On(HandleTypedEvent handler) where T : class { SubscriptionsEventSource.Log.MessageTypeNotRegistered(); } + return; + [MethodImpl(MethodImplOptions.AggressiveInlining)] ValueTask Handle(IMessageConsumeContext context) { return context.Message is not T ? NoHandler() : HandleTypedEvent(); diff --git a/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandlingResult.cs b/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandlingResult.cs index dc0a40d1..a57dde12 100644 --- a/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandlingResult.cs +++ b/src/Core/src/Eventuous.Subscriptions/Handlers/EventHandlingResult.cs @@ -6,14 +6,11 @@ namespace Eventuous.Subscriptions; public readonly record struct EventHandlingResult(EventHandlingStatus Status, string HandlerType, Exception? Exception = null) { - public static EventHandlingResult Succeeded(string handlerType) - => new(EventHandlingStatus.Success, handlerType); + public static EventHandlingResult Succeeded(string handlerType) => new(EventHandlingStatus.Success, handlerType); - public static EventHandlingResult Ignored(string handlerType) - => new(EventHandlingStatus.Ignored, handlerType); + public static EventHandlingResult Ignored(string handlerType) => new(EventHandlingStatus.Ignored, handlerType); - public static EventHandlingResult Failed(string handlerType, Exception? e) - => new(EventHandlingStatus.Failure, handlerType, e); + public static EventHandlingResult Failed(string handlerType, Exception? e) => new(EventHandlingStatus.Failure, handlerType, e); public EventHandlingStatus Status { get; } = Status; public Exception? Exception { get; } = Exception; @@ -32,8 +29,7 @@ public void Add(EventHandlingResult result) { _results.Add(result); } - public IEnumerable GetResultsOf(EventHandlingStatus status) - => _results.Where(x => x.Status == status); + public IEnumerable GetResultsOf(EventHandlingStatus status) => _results.Where(x => x.Status == status); public bool ReportedBy(string handlerType) => _results.Any(x => x.HandlerType == handlerType); diff --git a/src/Core/src/Eventuous.Subscriptions/Handlers/TracedEventHandler.cs b/src/Core/src/Eventuous.Subscriptions/Handlers/TracedEventHandler.cs index 6a01ec94..2d13ef4b 100644 --- a/src/Core/src/Eventuous.Subscriptions/Handlers/TracedEventHandler.cs +++ b/src/Core/src/Eventuous.Subscriptions/Handlers/TracedEventHandler.cs @@ -14,8 +14,7 @@ namespace Eventuous.Subscriptions; public class TracedEventHandler(IEventHandler eventHandler) : IEventHandler { readonly DiagnosticSource _metricsSource = new DiagnosticListener(SubscriptionMetrics.ListenerName); - readonly KeyValuePair[] _defaultTags = - [new KeyValuePair(TelemetryTags.Eventuous.EventHandler, eventHandler.GetType().Name)]; + readonly KeyValuePair[] _defaultTags = [new (TelemetryTags.Eventuous.EventHandler, eventHandler.GetType().Name)]; public string DiagnosticName { get; } = eventHandler.DiagnosticName; @@ -25,10 +24,7 @@ public async ValueTask HandleEvent(IMessageConsumeContext c ?.SetContextTags(context) ?.Start(); - using var measure = Measure.Start( - _metricsSource, - new SubscriptionMetrics.SubscriptionMetricsContext(DiagnosticName, context) - ); + using var measure = Measure.Start(_metricsSource, new SubscriptionMetrics.SubscriptionMetricsContext(DiagnosticName, context)); try { var status = await eventHandler.HandleEvent(context).NoContext(); diff --git a/src/Core/src/Eventuous.Subscriptions/Logging/CheckpointLogging.cs b/src/Core/src/Eventuous.Subscriptions/Logging/CheckpointLogging.cs index ffbe69af..05c61f39 100644 --- a/src/Core/src/Eventuous.Subscriptions/Logging/CheckpointLogging.cs +++ b/src/Core/src/Eventuous.Subscriptions/Logging/CheckpointLogging.cs @@ -8,6 +8,9 @@ namespace Eventuous.Subscriptions.Logging; using Checkpoints; public static class CheckpointLogging { + const int BaseEventId = 15000; + const int PositionReceivedId = BaseEventId + 1; + public static void PositionReceived(this LogContext log, CommitPosition checkpoint) => log.TraceLog?.Log("Received checkpoint: {Position}", checkpoint); diff --git a/src/Core/src/Eventuous.Subscriptions/Logging/InternalLogger.cs b/src/Core/src/Eventuous.Subscriptions/Logging/InternalLogger.cs index 01d663e4..9b1daa92 100644 --- a/src/Core/src/Eventuous.Subscriptions/Logging/InternalLogger.cs +++ b/src/Core/src/Eventuous.Subscriptions/Logging/InternalLogger.cs @@ -9,11 +9,9 @@ namespace Eventuous.Subscriptions.Logging; public class InternalLogger(ILogger logger, LogLevel logLevel, string subscriptionId) { #pragma warning disable CA2254 - public void Log(string message, params object[] args) => - logger.Log(logLevel, GetMessage(message), args); + public void Log(string message, params object[] args) => logger.Log(logLevel, GetMessage(message), args); - public void Log(Exception? exception, string message, params object[] args) => - logger.Log(logLevel, exception, GetMessage(message), args); + public void Log(Exception? exception, string message, params object[] args) => logger.Log(logLevel, exception, GetMessage(message), args); #pragma warning restore CA2254 string GetMessage(string message) => $"[{subscriptionId}] {message}"; diff --git a/src/Core/src/Eventuous.Subscriptions/Logging/Logger.cs b/src/Core/src/Eventuous.Subscriptions/Logging/Logger.cs index 16c044b9..c561fa4a 100644 --- a/src/Core/src/Eventuous.Subscriptions/Logging/Logger.cs +++ b/src/Core/src/Eventuous.Subscriptions/Logging/Logger.cs @@ -52,7 +52,6 @@ public LogContext(string subscriptionId, ILoggerFactory loggerFactory) { return; - InternalLogger? GetLogger(LogLevel logLevel) - => Logger.IsEnabled(logLevel) ? new InternalLogger(Logger, logLevel, SubscriptionId) : null; + InternalLogger? GetLogger(LogLevel logLevel) => Logger.IsEnabled(logLevel) ? new InternalLogger(Logger, logLevel, SubscriptionId) : null; } } diff --git a/src/Core/src/Eventuous.Subscriptions/Logging/SubscriptionLogging.cs b/src/Core/src/Eventuous.Subscriptions/Logging/SubscriptionLogging.cs index ddad83ab..61f22a0b 100644 --- a/src/Core/src/Eventuous.Subscriptions/Logging/SubscriptionLogging.cs +++ b/src/Core/src/Eventuous.Subscriptions/Logging/SubscriptionLogging.cs @@ -1,6 +1,7 @@ // Copyright (C) Ubiquitous AS. All rights reserved // Licensed under the Apache License, Version 2.0. +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; namespace Eventuous.Subscriptions.Logging; @@ -8,6 +9,7 @@ namespace Eventuous.Subscriptions.Logging; using Context; public static class LoggingExtensions { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MessageReceived(this LogContext log, IMessageConsumeContext context) => log.TraceLog?.Log( "Received {MessageType} from {Stream}:{Position} seq {Sequence}", @@ -17,6 +19,7 @@ public static void MessageReceived(this LogContext log, IMessageConsumeContext c context.Sequence ); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MessageHandled(this LogContext log, string handlerType, IBaseConsumeContext context) => log.TraceLog?.Log( "{Handler} handled {MessageType} {Stream}:{Position} seq {Sequence}", @@ -27,6 +30,7 @@ public static void MessageHandled(this LogContext log, string handlerType, IBase context.Sequence ); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void MessageIgnored(this LogContext log, string handlerType, IBaseConsumeContext context) => log.TraceLog?.Log( "{Handler} ignored {MessageType} {Stream}:{Position} seq {Sequence}", @@ -37,6 +41,9 @@ public static void MessageIgnored(this LogContext log, string handlerType, IBase context.Sequence ); + public static void MessageIgnoredWhenStopping(this LogContext log, Exception e) => + log.DebugLog?.Log("Message ignored because subscription is stopping: {Message}", e.Message); + public static void MessageTypeNotFound(this ILogger? log) => log?.LogWarning("Message type {MessageType} not registered in the type map", typeof(T).Name); @@ -59,10 +66,33 @@ public static void ThrowOnErrorIncompatible(this LogContext log) => log.WarnLog?.Log("Failure handler is set, but ThrowOnError is disabled, so the failure handler will never be called"); public static void FailedToHandleMessageWithRetry(this LogContext log, string handlerType, string messageType, int retryCount, Exception exception) - => log.ErrorLog?.Log(exception, "Failed to handle message {MessageType} with {HandlerType} after {RetryCount} retries", messageType, handlerType, retryCount); + => log.ErrorLog?.Log( + exception, + "Failed to handle message {MessageType} with {HandlerType} after {RetryCount} retries", + messageType, + handlerType, + retryCount + ); - public static void MessageAcked(this LogContext log, string messageType, ulong position) => log.TraceLog?.Log("Message {Type} acknowledged at {Position}", messageType, position); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void MessageAcked(this LogContext log, string messageType, ulong position) + => log.TraceLog?.Log("Message {Type} acknowledged at {Position}", messageType, position); - public static void MessageNacked(this LogContext log, string messageType, ulong position, Exception exception) + public static void MessageNacked(this LogContext log, string messageType, ulong position, Exception exception) => log.WarnLog?.Log(exception, "Message {Type} not acknowledged at {Position}", messageType, position); + + public static void SubscriptionStarted(this LogContext log) => log.InfoLog?.Log("Started"); + + public static void SubscriptionStopped(this LogContext log) => log.InfoLog?.Log("Stopped"); + + public static void SubscriptionDropped(this LogContext log, DropReason reason, Exception? exception) + => log.WarnLog?.Log(exception, "Dropped: {Reason}", reason); + + public static void SubscriptionWillResubscribe(this LogContext log, TimeSpan delay) => log.WarnLog?.Log($"Will resubscribe after {delay}"); + + public static void SubscriptionResubscribing(this LogContext log) => log.WarnLog?.Log("Resubscribing"); + + public static void SubscriptionResubscribed(this LogContext log) => log.InfoLog?.Log("Resubscribed"); + + public static void SubscriptionResubscribeFailed(this LogContext log, Exception e) => log.ErrorLog?.Log(e, "Failed to resubscribe"); } diff --git a/src/Core/src/Eventuous.Subscriptions/Registrations/NamedRegistrations.cs b/src/Core/src/Eventuous.Subscriptions/Registrations/NamedRegistrations.cs index e8e004a7..b2546ff4 100644 --- a/src/Core/src/Eventuous.Subscriptions/Registrations/NamedRegistrations.cs +++ b/src/Core/src/Eventuous.Subscriptions/Registrations/NamedRegistrations.cs @@ -8,34 +8,17 @@ namespace Microsoft.Extensions.DependencyInjection; static class NamedRegistrationExtensions { - public static IServiceCollection AddSubscriptionBuilder( - this IServiceCollection services, - SubscriptionBuilder builder - ) where T : EventSubscription where TOptions : SubscriptionOptions { - // if (services.Any(x => x is NamedDescriptor named && named.Name == builder.SubscriptionId)) { - // throw new InvalidOperationException( - // $"Existing subscription builder with id {builder.SubscriptionId} already registered" - // ); - // } - - // var descriptor = new NamedDescriptor(builder.SubscriptionId, typeof(SubscriptionBuilder), builder); - + public static IServiceCollection AddSubscriptionBuilder(this IServiceCollection services, SubscriptionBuilder builder) + where T : EventSubscription where TOptions : SubscriptionOptions { services.AddKeyedSingleton(builder.SubscriptionId, builder); // services.Add(descriptor); services.Configure(builder.SubscriptionId, builder.ConfigureOptions); + return services; } - public static SubscriptionBuilder GetSubscriptionBuilder( - this IServiceProvider provider, - string subscriptionId - ) where T : EventSubscription where TOptions : SubscriptionOptions { + public static SubscriptionBuilder GetSubscriptionBuilder(this IServiceProvider provider, string subscriptionId) + where T : EventSubscription where TOptions : SubscriptionOptions { return provider.GetRequiredKeyedService>(subscriptionId); - // var services = provider.GetServices>(); - // return services.Single(x => x.SubscriptionId == subscriptionId); } -} - -// class NamedDescriptor(string name, Type serviceType, object instance) : ServiceDescriptor(serviceType, instance) { - // public string Name { get; } = name; -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/Core/src/Eventuous.Subscriptions/Registrations/ParameterMap.cs b/src/Core/src/Eventuous.Subscriptions/Registrations/ParameterMap.cs deleted file mode 100644 index d11edcbd..00000000 --- a/src/Core/src/Eventuous.Subscriptions/Registrations/ParameterMap.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) Ubiquitous AS. All rights reserved -// Licensed under the Apache License, Version 2.0. - -namespace Eventuous.Subscriptions.Registrations; - -class ParameterMap { - readonly Dictionary> _resolvers = new(); - - public void Add() where TImplementation : class { - _resolvers.Add(typeof(TService), Resolver); - - return; - - dynamic? Resolver(IServiceProvider provider) => provider.GetService(typeof(TImplementation)); - } - - public void Add(Func resolver) - where TImplementation : class - => _resolvers.Add(typeof(TService), resolver); - - public bool TryGetResolver(Type parameterType, out Func? resolver) - => _resolvers.TryGetValue(parameterType, out resolver); -} diff --git a/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs b/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs index fabb6995..abbefff5 100644 --- a/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs +++ b/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilder.cs @@ -30,8 +30,7 @@ public abstract class SubscriptionBuilder(IServiceCollection services, string su /// /// Event handler type /// - public SubscriptionBuilder AddEventHandler() - where THandler : class, IEventHandler { + public SubscriptionBuilder AddEventHandler() where THandler : class, IEventHandler { Services.TryAddKeyedSingleton(SubscriptionId); AddHandlerResolve(sp => sp.GetRequiredKeyedService(SubscriptionId)); @@ -44,8 +43,7 @@ public SubscriptionBuilder AddEventHandler() /// A function to resolve event handler using the service provider /// Event handler type /// - public SubscriptionBuilder AddEventHandler(Func getHandler) - where THandler : class, IEventHandler { + public SubscriptionBuilder AddEventHandler(Func getHandler) where THandler : class, IEventHandler { Services.TryAddKeyedSingleton(SubscriptionId, (sp, _) => getHandler(sp)); AddHandlerResolve(sp => sp.GetRequiredKeyedService(SubscriptionId)); @@ -58,8 +56,7 @@ public SubscriptionBuilder AddEventHandler(FuncEvent handler instance /// Event handler type /// - public SubscriptionBuilder AddEventHandler(THandler handler) - where THandler : class, IEventHandler { + public SubscriptionBuilder AddEventHandler(THandler handler) where THandler : class, IEventHandler { AddHandlerResolve(_ => handler); return this; @@ -140,8 +137,7 @@ void AddHandlerResolve(ResolveHandler resolveHandler) public class SubscriptionBuilder : SubscriptionBuilder where T : EventSubscription where TOptions : SubscriptionOptions { - public SubscriptionBuilder(IServiceCollection services, string subscriptionId) - : base(services, subscriptionId) { + public SubscriptionBuilder(IServiceCollection services, string subscriptionId) : base(services, subscriptionId) { ResolveConsumer = ResolveDefaultConsumer; ConfigureOptions = options => options.SubscriptionId = subscriptionId; } diff --git a/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilderExtensions.cs b/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilderExtensions.cs index 5af4c709..a9e346d2 100644 --- a/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilderExtensions.cs +++ b/src/Core/src/Eventuous.Subscriptions/Registrations/SubscriptionBuilderExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using Eventuous.Diagnostics; -using Eventuous.Subscriptions.Checkpoints; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -10,6 +9,7 @@ namespace Eventuous.Subscriptions.Registrations; using Filters; using Filters.Partitioning; +using Checkpoints; public static class SubscriptionBuilderExtensions { /// diff --git a/src/Core/src/Eventuous.Subscriptions/SubscriptionHostedService.cs b/src/Core/src/Eventuous.Subscriptions/SubscriptionHostedService.cs index 0bda49db..e2eb0dda 100644 --- a/src/Core/src/Eventuous.Subscriptions/SubscriptionHostedService.cs +++ b/src/Core/src/Eventuous.Subscriptions/SubscriptionHostedService.cs @@ -22,10 +22,7 @@ public class SubscriptionHostedService( public virtual async Task StartAsync(CancellationToken cancellationToken) { Log?.LogDebug("Starting subscription {SubscriptionId}", subscription.SubscriptionId); - var cts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - _subscriptionCts.Token - ); + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _subscriptionCts.Token); await subscription.Subscribe( id => subscriptionHealth?.ReportHealthy(id), @@ -38,10 +35,7 @@ await subscription.Subscribe( } public virtual async Task StopAsync(CancellationToken cancellationToken) { - var cts = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - _subscriptionCts.Token - ); + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _subscriptionCts.Token); await subscription.Unsubscribe(_ => { }, cts.Token).NoContext(); Log?.LogInformation("Stopped subscription {SubscriptionId}", subscription.SubscriptionId); diff --git a/src/Core/test/Eventuous.Tests.Application/BookingFuncService.cs b/src/Core/test/Eventuous.Tests.Application/BookingFuncService.cs index f8a51493..7ec9eb84 100644 --- a/src/Core/test/Eventuous.Tests.Application/BookingFuncService.cs +++ b/src/Core/test/Eventuous.Tests.Application/BookingFuncService.cs @@ -10,24 +10,19 @@ namespace Eventuous.Tests.Application; public class BookingFuncService : FunctionalCommandService { public BookingFuncService(IEventStore store, TypeMapper? typeMap = null, AmendEvent? amendEvent = null) : base(store, typeMap, amendEvent) { -#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS0618 + // Keep it for tests until the old API is gone OnNew(cmd => GetStream(cmd.BookingId), BookRoom); -#pragma warning restore CS0618 // Type or member is obsolete +#pragma warning restore CS0618 On().InState(ExpectedState.Existing).GetStream(cmd => GetStream(cmd.BookingId)).Act(RecordPayment); On().InState(ExpectedState.Any).GetStream(cmd => GetStream(cmd.BookingId)).Act(ImportBooking); return; - static StreamName GetStream(string id) - => StreamName.For(id); + static IEnumerable BookRoom(BookRoom cmd) => [new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price)]; - static IEnumerable BookRoom(BookRoom cmd) { - yield return new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price); - } - - static IEnumerable ImportBooking(BookingState state, object[] events, ImportBooking cmd) { - yield return new BookingImported(cmd.RoomId, cmd.Price, cmd.CheckIn, cmd.CheckOut); - } + static IEnumerable ImportBooking(BookingState state, object[] events, ImportBooking cmd) + => [new BookingImported(cmd.RoomId, cmd.Price, cmd.CheckIn, cmd.CheckOut)]; static IEnumerable RecordPayment(BookingState state, object[] originalEvents, RecordPayment cmd) { if (state.HasPayment(cmd.PaymentId)) yield break; diff --git a/src/Core/test/Eventuous.Tests.Application/CommandServiceTests.cs b/src/Core/test/Eventuous.Tests.Application/CommandServiceTests.cs index 0a65c115..88fddb40 100644 --- a/src/Core/test/Eventuous.Tests.Application/CommandServiceTests.cs +++ b/src/Core/test/Eventuous.Tests.Application/CommandServiceTests.cs @@ -6,16 +6,9 @@ namespace Eventuous.Tests.Application; public class CommandServiceTests { - readonly AggregateStore _aggregateStore; + readonly AggregateStore _aggregateStore = new(new InMemoryEventStore()); - static CommandServiceTests() { - TypeMap.RegisterKnownEventTypes(typeof(BookingEvents.RoomBooked).Assembly); - } - - public CommandServiceTests() { - var store = new InMemoryEventStore(); - _aggregateStore = new AggregateStore(store); - } + static CommandServiceTests() => TypeMap.RegisterKnownEventTypes(typeof(BookingEvents.RoomBooked).Assembly); [Fact] public async Task HandleFirstCommandThreadSafe() { @@ -41,6 +34,6 @@ static Commands.BookRoom GetBookRoom(string bookingId = "123") { var checkIn = LocalDate.FromDateTime(DateTime.Today); var checkOut = checkIn.PlusDays(1); - return new Commands.BookRoom(bookingId, "234", checkIn, checkOut, 100); + return new(bookingId, "234", checkIn, checkOut, 100); } } \ No newline at end of file diff --git a/src/Core/test/Eventuous.Tests.Application/FunctionalServiceTests.cs b/src/Core/test/Eventuous.Tests.Application/FunctionalServiceTests.cs index 02b2c749..a1cde904 100644 --- a/src/Core/test/Eventuous.Tests.Application/FunctionalServiceTests.cs +++ b/src/Core/test/Eventuous.Tests.Application/FunctionalServiceTests.cs @@ -17,9 +17,9 @@ static FunctionalServiceTests() { } public FunctionalServiceTests(ITestOutputHelper output) { - _store = new InMemoryEventStore(); - _service = new BookingFuncService(_store); - _listener = new TestEventListener(output); + _store = new(); + _service = new(_store); + _listener = new(output); } [Fact] @@ -88,7 +88,7 @@ static Commands.BookRoom GetBookRoom() { var checkIn = LocalDate.FromDateTime(DateTime.Today); var checkOut = checkIn.PlusDays(1); - return new Commands.BookRoom("123", "234", checkIn, checkOut, 100); + return new("123", "234", checkIn, checkOut, 100); } async Task Seed() { @@ -98,6 +98,5 @@ static Commands.BookRoom GetBookRoom() { return cmd; } - public void Dispose() - => _listener.Dispose(); + public void Dispose() => _listener.Dispose(); } diff --git a/src/Core/test/Eventuous.Tests.Application/StateWithIdTests.cs b/src/Core/test/Eventuous.Tests.Application/StateWithIdTests.cs index fc4de5a8..ce435127 100644 --- a/src/Core/test/Eventuous.Tests.Application/StateWithIdTests.cs +++ b/src/Core/test/Eventuous.Tests.Application/StateWithIdTests.cs @@ -11,9 +11,8 @@ public class StateWithIdTests { readonly AggregateStore _aggregateStore; public StateWithIdTests() { - var store = new InMemoryEventStore(); - _aggregateStore = new AggregateStore(store); - _service = new BookingService(_aggregateStore); + _aggregateStore = new(new InMemoryEventStore()); + _service = new(_aggregateStore); } [Fact] diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Eventuous.Tests.Persistence.Base.csproj b/src/Core/test/Eventuous.Tests.Persistence.Base/Eventuous.Tests.Persistence.Base.csproj index e3d05e80..71d65bf3 100644 --- a/src/Core/test/Eventuous.Tests.Persistence.Base/Eventuous.Tests.Persistence.Base.csproj +++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Eventuous.Tests.Persistence.Base.csproj @@ -1,8 +1,9 @@ - + true + diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Append.cs b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Append.cs index 57b6348c..14aac887 100644 --- a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Append.cs +++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Append.cs @@ -56,7 +56,7 @@ public async Task ShouldFailOnWrongVersion() { evt = fixture.CreateEvent(); - var task = () => fixture.AppendEvent(stream, evt, new ExpectedStreamVersion(3)); + var task = () => fixture.AppendEvent(stream, evt, new(3)); await task.Should().ThrowAsync(); } } diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs index 80acdb4f..ba841d8c 100644 --- a/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs +++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Store/Read.cs @@ -1,6 +1,8 @@ using System.Text.Json; using Eventuous.Tests.Persistence.Base.Fixtures; +// ReSharper disable CoVariantArrayConversion + namespace Eventuous.Tests.Persistence.Base.Store; public abstract class StoreReadTests(T fixture) : IClassFixture where T : StoreFixtureBase { @@ -19,7 +21,6 @@ public async Task ShouldReadOne() { [Fact] [Trait("Category", "Store")] public async Task ShouldReadMany() { - // ReSharper disable once CoVariantArrayConversion object[] events = fixture.CreateEvents(20).ToArray(); var streamName = fixture.GetStreamName(); await fixture.AppendEvents(streamName, events, ExpectedStreamVersion.NoStream); @@ -32,12 +33,11 @@ public async Task ShouldReadMany() { [Fact] [Trait("Category", "Store")] public async Task ShouldReadTail() { - // ReSharper disable once CoVariantArrayConversion object[] events = fixture.CreateEvents(20).ToArray(); var streamName = fixture.GetStreamName(); await fixture.AppendEvents(streamName, events, ExpectedStreamVersion.NoStream); - var result = await fixture.EventStore.ReadEvents(streamName, new StreamReadPosition(10), 100, default); + var result = await fixture.EventStore.ReadEvents(streamName, new(10), 100, default); var expected = events.Skip(10); var actual = result.Select(x => x.Payload); actual.Should().BeEquivalentTo(expected); @@ -46,7 +46,6 @@ public async Task ShouldReadTail() { [Fact] [Trait("Category", "Store")] public async Task ShouldReadHead() { - // ReSharper disable once CoVariantArrayConversion object[] events = fixture.CreateEvents(20).ToArray(); var streamName = fixture.GetStreamName(); await fixture.AppendEvents(streamName, events, ExpectedStreamVersion.NoStream); @@ -56,14 +55,14 @@ public async Task ShouldReadHead() { var actual = result.Select(x => x.Payload); actual.Should().BeEquivalentTo(expected); } - + [Fact] [Trait("Category", "Store")] public async Task ShouldReadMetadata() { var evt = fixture.CreateEvent(); var streamName = fixture.GetStreamName(); - await fixture.AppendEvent(streamName, evt, ExpectedStreamVersion.NoStream, new Metadata { { "Key1", "Value1" }, { "Key2", "Value2" } }); + await fixture.AppendEvent(streamName, evt, ExpectedStreamVersion.NoStream, new() { { "Key1", "Value1" }, { "Key2", "Value2" } }); var result = await fixture.EventStore.ReadEvents(streamName, StreamReadPosition.Start, 100, default); diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryAttribute.cs b/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryAttribute.cs index 77c9446c..6678197a 100644 --- a/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryAttribute.cs +++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryAttribute.cs @@ -1,4 +1,7 @@ +using JetBrains.Annotations; + // ReSharper disable once CheckNamespace + namespace Xunit; using Sdk; @@ -6,8 +9,10 @@ namespace Xunit; /// /// Apply this attribute to your test method to specify a category. /// +[UsedImplicitly] [TraitDiscoverer("CategoryDiscoverer", "TraitExtensibility")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class CategoryAttribute(string category) : Attribute, ITraitAttribute { + [UsedImplicitly] public string Name { get; } = category; } diff --git a/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryDiscoverer.cs b/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryDiscoverer.cs index f397f3e5..f696e803 100644 --- a/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryDiscoverer.cs +++ b/src/Core/test/Eventuous.Tests.Persistence.Base/Traits/CategoryDiscoverer.cs @@ -1,12 +1,15 @@ +using JetBrains.Annotations; + // ReSharper disable once CheckNamespace namespace Xunit; using Sdk; /// -/// This class discovers all of the tests and test classes that have +/// This class discovers all the tests and test classes that have /// applied the Category attribute /// +[UsedImplicitly] public class CategoryDiscoverer : ITraitDiscoverer { /// /// Gets the trait values from the Category attribute. @@ -16,6 +19,6 @@ public class CategoryDiscoverer : ITraitDiscoverer { public IEnumerable> GetTraits(IAttributeInfo traitAttribute) { var categoryName = traitAttribute.GetNamedArgument("Name"); - yield return new KeyValuePair("Category", categoryName); + yield return new("Category", categoryName); } } diff --git a/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TestEventHandler.cs b/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TestEventHandler.cs index b1610226..0f3f0f9b 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TestEventHandler.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TestEventHandler.cs @@ -2,6 +2,7 @@ using Eventuous.Subscriptions.Context; using Hypothesist; using Hypothesist.Builders; +// ReSharper disable NotAccessedPositionalProperty.Global namespace Eventuous.Tests.Subscriptions.Base; diff --git a/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TracedHandler.cs b/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TracedHandler.cs index 34f5994d..188c8a28 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TracedHandler.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions.Base/Fixtures/TracedHandler.cs @@ -9,16 +9,10 @@ public class TracedHandler : BaseEventHandler { public List Contexts { get; } = []; static readonly ValueTask Success = new(EventHandlingStatus.Success); - + public override ValueTask HandleEvent(IMessageConsumeContext context) { - Contexts.Add( - new RecordedTrace( - Activity.Current?.TraceId, - Activity.Current?.SpanId, - Activity.Current?.ParentSpanId - ) - ); + Contexts.Add(new(Activity.Current?.TraceId, Activity.Current?.SpanId, Activity.Current?.ParentSpanId)); return Success; } -} \ No newline at end of file +} diff --git a/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToAll.cs b/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToAll.cs index 2000d88a..b1b2dd98 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToAll.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToAll.cs @@ -57,7 +57,7 @@ protected async Task ShouldUseExistingCheckpoint() { await fixture.CheckpointStore.GetLastCheckpoint(fixture.SubscriptionId, default); var last = await fixture.GetLastPosition(); - await fixture.CheckpointStore.StoreCheckpoint(new Checkpoint(fixture.SubscriptionId, last), true, default); + await fixture.CheckpointStore.StoreCheckpoint(new(fixture.SubscriptionId, last), true, default); var l = await fixture.CheckpointStore.GetLastCheckpoint(fixture.SubscriptionId, default); outputHelper.WriteLine("Last checkpoint: {0}", l.Position); diff --git a/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToStream.cs b/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToStream.cs index 24b8df06..9d741e3e 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToStream.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions.Base/SubscribeToStream.cs @@ -8,14 +8,14 @@ namespace Eventuous.Tests.Subscriptions.Base; -public abstract class SubscribeToStreamBase( - ITestOutputHelper outputHelper, - StreamName streamName, - SubscriptionFixtureBase fixture +public abstract class SubscribeToStreamBase( + ITestOutputHelper outputHelper, + StreamName streamName, + SubscriptionFixtureBase fixture ) : IAsyncLifetime where TContainer : DockerContainer - where TSubscription : EventSubscription - where TSubscriptionOptions : SubscriptionOptions + where TSub : EventSubscription + where TSubOptions : SubscriptionOptions where TCheckpointStore : class, ICheckpointStore { protected async Task ShouldConsumeProducedEvents() { const int count = 10; @@ -82,6 +82,7 @@ static BookingImported ToEvent(ImportBooking cmd) async Task> GenerateAndProduceEvents(int count) { outputHelper.WriteLine($"Producing events to {streamName}"); + var commands = Enumerable .Range(0, count) .Select(_ => DomainFixture.CreateImportBooking(fixture.Auto)) diff --git a/src/Core/test/Eventuous.Tests.Subscriptions/AutofixtureExtensions.cs b/src/Core/test/Eventuous.Tests.Subscriptions/AutofixtureExtensions.cs index e1e2bd32..da366b83 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions/AutofixtureExtensions.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions/AutofixtureExtensions.cs @@ -1,14 +1,10 @@ using Eventuous.Subscriptions.Context; -using Eventuous.Subscriptions.Logging; namespace Eventuous.Tests.Subscriptions; public static class AutoFixtureExtensions { public static MessageConsumeContext CreateContext(this Fixture auto, ITestOutputHelper output) { var factory = new LoggerFactory().AddXunit(output, LogLevel.Trace); - - return auto.Build() - .With(x => x.LogContext, () => new LogContext("test", factory)) - .Create(); + return auto.Build().With(x => x.LogContext, () => new("test", factory)).Create(); } } diff --git a/src/Core/test/Eventuous.Tests.Subscriptions/ConsumePipeTests.cs b/src/Core/test/Eventuous.Tests.Subscriptions/ConsumePipeTests.cs index 4646e6f1..eae919e3 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions/ConsumePipeTests.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions/ConsumePipeTests.cs @@ -11,8 +11,7 @@ public class ConsumePipeTests(ITestOutputHelper outputHelper) { public async Task ShouldCallHandlers() { var handler = new TestHandler(); var pipe = new ConsumePipe().AddDefaultConsumer(handler); - - var ctx = Auto.CreateContext(outputHelper); + var ctx = Auto.CreateContext(outputHelper); await pipe.Send(ctx); @@ -25,7 +24,6 @@ public async Task ShouldCallHandlers() { public async Task ShouldAddContextBaggage() { var handler = new TestHandler(); var pipe = new ConsumePipe().AddDefaultConsumer(handler); - var baggage = Auto.Create(); pipe.AddFilterFirst(new TestFilter(Key, baggage)); @@ -39,17 +37,10 @@ public async Task ShouldAddContextBaggage() { handler.Received!.Items.GetItem(Key).Should().Be(baggage); } - class TestFilter : ConsumeFilter { - readonly string _key; - readonly string _payload; - - public TestFilter(string key, string payload) { - _key = key; - _payload = payload; - } - + class TestFilter(string key, string payload) : ConsumeFilter { protected override ValueTask Send(IMessageConsumeContext context, LinkedListNode? next) { - context.Items.AddItem(_key, _payload); + context.Items.AddItem(key, payload); + return next?.Value.Send(context, next.Next) ?? default; } } @@ -61,6 +52,7 @@ class TestHandler : BaseEventHandler { public override ValueTask HandleEvent(IMessageConsumeContext context) { Called++; Received = context; + return default; } } diff --git a/src/Core/test/Eventuous.Tests.Subscriptions/RegistrationTests.cs b/src/Core/test/Eventuous.Tests.Subscriptions/RegistrationTests.cs index 8c6c9ee6..e726fd2d 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions/RegistrationTests.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions/RegistrationTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging.Abstractions; + // ReSharper disable ClassNeverInstantiated.Local namespace Eventuous.Tests.Subscriptions; @@ -77,7 +78,7 @@ public async Task SubsShouldHaveHandlers(int position, Type handlerType) { public void ShouldRegisterBothAsHealthReporters() { var services = _server.Services.GetServices().ToArray(); var health = _server.Services.GetServices().ToArray(); - + services.Length.Should().Be(1); health.Length.Should().Be(1); services.Single().Should().BeSameAs(health.Single()); @@ -118,14 +119,8 @@ public static void ConfigureServices(IServiceCollection services) { .AddEventHandler() ); - services.AddSubscription( - "sub2", - builder => builder - .AddEventHandler() - ); - + services.AddSubscription("sub2", builder => builder.AddEventHandler()); services.AddOpenTelemetry().WithMetrics(builder => builder.AddEventuousSubscriptions()); - services.AddHealthChecks().AddSubscriptionsHealthCheck("subscriptions", HealthStatus.Unhealthy, ["tag"]); } diff --git a/src/Core/test/Eventuous.Tests.Subscriptions/SequenceTests.cs b/src/Core/test/Eventuous.Tests.Subscriptions/SequenceTests.cs index e88fae0e..129d889a 100644 --- a/src/Core/test/Eventuous.Tests.Subscriptions/SequenceTests.cs +++ b/src/Core/test/Eventuous.Tests.Subscriptions/SequenceTests.cs @@ -25,15 +25,15 @@ public void ShouldReturnFirstBefore(CommitPositionSequence sequence, CommitPosit [Fact] public void ShouldWorkForOne() { var timestamp = DateTime.Now; - var sequence = new CommitPositionSequence { new(0, 1, timestamp) }; + var sequence = new CommitPositionSequence { new(0, 1, timestamp) }; sequence.FirstBeforeGap().Should().Be(new CommitPosition(0, 1, timestamp)); } [Fact] public void ShouldWorkForRandomGap() { - var random = new Random(); + var random = new Random(); var sequence = new CommitPositionSequence(); - var start = (ulong)random.Next(1); + var start = (ulong)random.Next(1); for (var i = start; i < start + 100; i++) { sequence.Add(new CommitPosition(i, i, DateTime.Now)); @@ -49,11 +49,11 @@ public void ShouldWorkForRandomGap() { [Fact] public void ShouldWorkForNormalCase() { - var sequence = new CommitPositionSequence(); + var sequence = new CommitPositionSequence(); var timestamp = DateTime.Now; for (ulong i = 0; i < 10; i++) { - sequence.Add(new CommitPosition(i, i, timestamp)); + sequence.Add(new(i, i, timestamp)); } var first = sequence.FirstBeforeGap(); @@ -63,25 +63,18 @@ public void ShouldWorkForNormalCase() { public static IEnumerable TestData { get { var timestamp = DateTime.Now; - return new List { - new object[] { - new CommitPositionSequence { - new(0, 1, timestamp), - new(0, 2, timestamp), - new(0, 4, timestamp), - new(0, 6, timestamp) - }, - new CommitPosition(0, 2, timestamp) - }, - new object[] { - new CommitPositionSequence { - new(0, 1, timestamp), - new(0, 2, timestamp), - new(0, 8, timestamp), - new(0, 6, timestamp) - }, new CommitPosition(0, 2, timestamp) - } - }; + + object[] sequence1 = [ + new CommitPositionSequence { new(0, 1, timestamp), new(0, 2, timestamp), new(0, 4, timestamp), new(0, 6, timestamp) }, + new CommitPosition(0, 2, timestamp) + ]; + + object[] sequence2 = [ + new CommitPositionSequence { new(0, 1, timestamp), new(0, 2, timestamp), new(0, 8, timestamp), new(0, 6, timestamp) }, + new CommitPosition(0, 2, timestamp) + ]; + + return [sequence1, sequence2]; } } -} \ No newline at end of file +} diff --git a/src/Core/test/Eventuous.Tests/AggregateWithId/OperateOnAggregateWithId.cs b/src/Core/test/Eventuous.Tests/AggregateWithId/OperateOnAggregateWithId.cs new file mode 100644 index 00000000..59d0dd3d --- /dev/null +++ b/src/Core/test/Eventuous.Tests/AggregateWithId/OperateOnAggregateWithId.cs @@ -0,0 +1,27 @@ +using Eventuous.Testing; + +namespace Eventuous.Tests.AggregateWithId; + +public class OperateOnAggregateWithId : AggregateWithIdSpec { + protected override void When(TestAggregate aggregate) => aggregate.Process(); + + const string IdValue = "test"; + + protected override TestId? Id { get; } = new(IdValue); + + [Fact] + public void should_emit_event() => Emitted(new TestEvent()); + + [Fact] + public void should_set_id() => Then().State.Id.Value.Should().Be(IdValue); +} + +public class TestAggregate : Aggregate { + public void Process() => Apply(new TestEvent()); +} + +public record TestState : State; + +public record TestId(string Value) : Id(Value); + +record TestEvent; diff --git a/src/Core/test/Eventuous.Tests/Aggregates/OperateOnExistingSpec.cs b/src/Core/test/Eventuous.Tests/Aggregates/OperateOnExistingSpec.cs index 083d1c13..8dad42a0 100644 --- a/src/Core/test/Eventuous.Tests/Aggregates/OperateOnExistingSpec.cs +++ b/src/Core/test/Eventuous.Tests/Aggregates/OperateOnExistingSpec.cs @@ -3,40 +3,28 @@ namespace Eventuous.Tests.Aggregates; using Sut.Domain; using Testing; -public class OperateOnExistingSpec : AggregateSpec { +public class OperateOnExistingSpec : AggregateSpec { protected override object[] GivenEvents() => [ new BookingEvents.RoomBooked("room1", LocalDate.FromDateTime(DateTime.Today), LocalDate.FromDateTime(DateTime.Today.AddDays(2)), 100.0f), ]; - protected override void When(Booking booking) => booking.RecordPayment("payment1", new Money(50), DateTimeOffset.Now); + protected override void When(Booking booking) => booking.RecordPayment("payment1", new(50), DateTimeOffset.Now); [Fact] - public void should_produce_payment_registered() { - Then().Changes.Should().Contain(new BookingEvents.BookingPaymentRegistered("payment1", 50)); - } + public void should_produce_payment_registered() => Emitted(new BookingEvents.BookingPaymentRegistered("payment1", 50)); [Fact] - public void should_produce_outstanding_changed() { - Then().Changes.Should().Contain(new BookingEvents.BookingOutstandingAmountChanged(50)); - } + public void should_produce_outstanding_changed() => Emitted(new BookingEvents.BookingOutstandingAmountChanged(50)); [Fact] - public void should_not_be_fully_paid() { - Then().State.IsFullyPaid().Should().BeFalse(); - } + public void should_not_be_fully_paid() => Then().State.IsFullyPaid().Should().BeFalse(); [Fact] - public void should_record_payment() { - Then().HasPaymentRecord("payment1").Should().BeTrue(); - } + public void should_record_payment() => Then().HasPaymentRecord("payment1").Should().BeTrue(); [Fact] - public void should_not_be_overpaid() { - Then().State.IsOverpaid().Should().BeFalse(); - } + public void should_not_be_overpaid() => Then().State.IsOverpaid().Should().BeFalse(); [Fact] - public void should_produce_two_events() { - Then().Changes.Should().HaveCount(2); - } + public void should_produce_two_events() => Then().Changes.Should().HaveCount(2); } diff --git a/src/Core/test/Eventuous.Tests/Aggregates/TwoAggregateOpsSpec.cs b/src/Core/test/Eventuous.Tests/Aggregates/TwoAggregateOpsSpec.cs index 8b695413..d4b7008a 100644 --- a/src/Core/test/Eventuous.Tests/Aggregates/TwoAggregateOpsSpec.cs +++ b/src/Core/test/Eventuous.Tests/Aggregates/TwoAggregateOpsSpec.cs @@ -1,59 +1,45 @@ +using JetBrains.Annotations; + namespace Eventuous.Tests.Aggregates; using Sut.Domain; using Testing; using static Sut.Domain.BookingEvents; -public class TwoAggregateOpsSpec : AggregateSpec { +public class TwoAggregateOpsSpec : AggregateSpec { readonly Fixture _fixture = new(); - public TwoAggregateOpsSpec() { - _testData = _fixture.Create(); - } + public TwoAggregateOpsSpec() => _testData = _fixture.Create(); protected override void When(Booking booking) { - var amount = new Money(_testData.Amount); - - booking.BookRoom( - _fixture.Create(), - new StayPeriod(LocalDate.FromDateTime(DateTime.Today), LocalDate.FromDateTime(DateTime.Today.AddDays(2))), - amount - ); + var amount = new Money(_testData.Amount); + var checkIn = LocalDate.FromDateTime(DateTime.Today); + var checkOut = checkIn.Plus(Period.FromDays(2)); + booking.BookRoom(_fixture.Create(), new(checkIn, checkOut), amount); booking.RecordPayment(_testData.PaymentId, amount, _testData.PaidAt); } [Fact] - public void should_produce_fully_paid_event() { - var expected = new BookingFullyPaid(_testData.PaidAt); - Then().Changes.Should().Contain(expected); - } + public void should_produce_fully_paid_event() => Emitted(new BookingFullyPaid(_testData.PaidAt)); [Fact] - public void should_produce_payment_registered() { - var expected = new BookingPaymentRegistered(_testData.PaymentId, _testData.Amount); - Then().Changes.Should().Contain(expected); - } + public void should_produce_payment_registered() => Emitted(new BookingPaymentRegistered(_testData.PaymentId, _testData.Amount)); [Fact] - public void should_produce_outstanding_changed() { - var expected = new BookingOutstandingAmountChanged(0); - Then().Changes.Should().Contain(expected); - } + public void should_produce_outstanding_changed() => Emitted(new BookingOutstandingAmountChanged(0)); [Fact] - public void should_make_booking_fully_paid() - => Then().State.IsFullyPaid().Should().BeTrue(); + public void should_make_booking_fully_paid() => Then().State.IsFullyPaid().Should().BeTrue(); [Fact] - public void should_record_payment() - => Then().HasPaymentRecord(_testData.PaymentId).Should().BeTrue(); + public void should_record_payment() => Then().HasPaymentRecord(_testData.PaymentId).Should().BeTrue(); [Fact] - public void should_not_be_overpaid() - => Then().State.IsOverpaid().Should().BeFalse(); + public void should_not_be_overpaid() => Then().State.IsOverpaid().Should().BeFalse(); readonly TestData _testData; + [UsedImplicitly] record TestData(string PaymentId, float Amount, DateTimeOffset PaidAt); } diff --git a/src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj b/src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj index 01ef493c..912bd2e6 100644 --- a/src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj +++ b/src/Core/test/Eventuous.Tests/Eventuous.Tests.csproj @@ -4,6 +4,7 @@ true + diff --git a/src/Core/test/Eventuous.Tests/ForgotToSetId.cs b/src/Core/test/Eventuous.Tests/ForgotToSetId.cs index de14bca9..ceeed7cd 100644 --- a/src/Core/test/Eventuous.Tests/ForgotToSetId.cs +++ b/src/Core/test/Eventuous.Tests/ForgotToSetId.cs @@ -3,7 +3,7 @@ namespace Eventuous.Tests; public class ForgotToSetId : NaiveFixture { - public ForgotToSetId() => Service = new TestService(this.AggregateStore); + public ForgotToSetId() => Service = new(this.AggregateStore); [Fact] public async Task ShouldFailWithNoId() { @@ -16,7 +16,7 @@ public async Task ShouldFailWithNoId() { class TestService : CommandService { public TestService(IAggregateStore store) : base(store) - => On().InState(ExpectedState.New).GetId(cmd => new TestId(cmd.Id)).Act((test, _) => test.Process()); + => On().InState(ExpectedState.New).GetId(cmd => new(cmd.Id)).Act((test, _) => test.Process()); } record DoIt(string Id); @@ -27,9 +27,7 @@ class TestAggregate : Aggregate { record TestState : State; - record TestId : Id { - public TestId(string value) : base(value) { } - } + record TestId(string Value) : Id(Value); record TestEvent; } diff --git a/src/Core/test/Eventuous.Tests/StoringEvents.cs b/src/Core/test/Eventuous.Tests/StoringEvents.cs index 97440762..3ba97786 100644 --- a/src/Core/test/Eventuous.Tests/StoringEvents.cs +++ b/src/Core/test/Eventuous.Tests/StoringEvents.cs @@ -8,7 +8,7 @@ namespace Eventuous.Tests; public class StoringEvents : NaiveFixture { public StoringEvents() { - Service = new BookingService(AggregateStore); + Service = new(AggregateStore); TypeMap.RegisterKnownEventTypes(); } @@ -24,29 +24,14 @@ public async Task StoreInitial() { Auto.Create() ); - var expected = new Change[] { - new( - new BookingEvents.RoomBooked( - cmd.RoomId, - cmd.CheckIn, - cmd.CheckOut, - cmd.Price - ), - "RoomBooked" - ) - }; + Change[] expected = [new(new BookingEvents.RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price), "RoomBooked")]; var result = await Service.Handle(cmd, default); result.Success.Should().BeTrue(); result.Changes.Should().BeEquivalentTo(expected); - var evt = await EventStore.ReadEvents( - StreamName.For(cmd.BookingId), - StreamReadPosition.Start, - 1, - CancellationToken.None - ); + var evt = await EventStore.ReadEvents(StreamName.For(cmd.BookingId), StreamReadPosition.Start, 1, CancellationToken.None); evt[0].Payload.Should().BeEquivalentTo(result.Changes!.First().Event); } diff --git a/src/Core/test/Eventuous.Tests/StoringEventsWithCustomStream.cs b/src/Core/test/Eventuous.Tests/StoringEventsWithCustomStream.cs index 7c40d933..cc9c5e30 100644 --- a/src/Core/test/Eventuous.Tests/StoringEventsWithCustomStream.cs +++ b/src/Core/test/Eventuous.Tests/StoringEventsWithCustomStream.cs @@ -10,7 +10,7 @@ public class StoringEventsWithCustomStream : NaiveFixture { public StoringEventsWithCustomStream() { var streamNameMap = new StreamNameMap(); streamNameMap.Register(GetStreamName); - Service = new BookingService(AggregateStore, streamNameMap); + Service = new(AggregateStore, streamNameMap); TypeMap.RegisterKnownEventTypes(); } @@ -20,19 +20,14 @@ public StoringEventsWithCustomStream() { public async Task TestOnNew() { var cmd = CreateBookRoomCommand(); - Change[] expected = [ new Change(new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price), "RoomBooked") ]; + Change[] expected = [new(new RoomBooked(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price), "RoomBooked")]; var result = await Service.Handle(cmd, default); result.Success.Should().BeTrue(); result.Changes.Should().BeEquivalentTo(expected); - var evt = await EventStore.ReadEvents( - GetStreamName(new BookingId(cmd.BookingId)), - StreamReadPosition.Start, - 1, - CancellationToken.None - ); + var evt = await EventStore.ReadEvents(GetStreamName(new(cmd.BookingId)), StreamReadPosition.Start, 1, CancellationToken.None); evt[0].Payload.Should().BeEquivalentTo(result.Changes!.First().Event); } @@ -43,21 +38,10 @@ public async Task TestOnExisting() { await Service.Handle(cmd, default); - var secondCmd = new Commands.RecordPayment( - new BookingId(cmd.BookingId), - Auto.Create(), - new Money(cmd.Price), - DateTimeOffset.Now - ); + var secondCmd = new Commands.RecordPayment(new(cmd.BookingId), Auto.Create(), new(cmd.Price), DateTimeOffset.Now); var expected = new Change[] { - new( - new BookingPaymentRegistered( - secondCmd.PaymentId, - secondCmd.Amount.Amount - ), - "PaymentRegistered" - ), + new(new BookingPaymentRegistered(secondCmd.PaymentId, secondCmd.Amount.Amount), "PaymentRegistered"), new(new BookingOutstandingAmountChanged(0), "OutstandingAmountChanged"), new(new BookingFullyPaid(secondCmd.PaidAt), "BookingFullyPaid") }; @@ -67,18 +51,12 @@ public async Task TestOnExisting() { result.Success.Should().BeTrue(); result.Changes.Should().BeEquivalentTo(expected); - var evt = await EventStore.ReadEvents( - GetStreamName(new BookingId(cmd.BookingId)), - StreamReadPosition.Start, - 100, - CancellationToken.None - ); + var evt = await EventStore.ReadEvents(GetStreamName(new(cmd.BookingId)), StreamReadPosition.Start, 100, CancellationToken.None); var actual = evt.Skip(1).Select(x => x.Payload); actual.Should().BeEquivalentTo(expected.Select(x => x.Event)); } - static StreamName GetStreamName(BookingId bookingId) - => new($"hotel-booking-{bookingId}"); + static StreamName GetStreamName(BookingId bookingId) => new($"hotel-booking-{bookingId}"); } diff --git a/src/Core/test/Eventuous.Tests/TypeRegistrationTests.cs b/src/Core/test/Eventuous.Tests/TypeRegistrationTests.cs index f3874f41..a9c247f7 100644 --- a/src/Core/test/Eventuous.Tests/TypeRegistrationTests.cs +++ b/src/Core/test/Eventuous.Tests/TypeRegistrationTests.cs @@ -3,12 +3,9 @@ namespace Eventuous.Tests; public class TypeRegistrationTests { - readonly TypeMapper _typeMapper; + readonly TypeMapper _typeMapper = new(); - public TypeRegistrationTests() { - _typeMapper = new TypeMapper(); - _typeMapper.RegisterKnownEventTypes(typeof(BookingCancelled).Assembly); - } + public TypeRegistrationTests() => _typeMapper.RegisterKnownEventTypes(typeof(BookingCancelled).Assembly); [Fact] public void ShouldResolveDecoratedEvent() { diff --git a/src/Diagnostics/src/Eventuous.Diagnostics.Logging/LoggingEventListener.cs b/src/Diagnostics/src/Eventuous.Diagnostics.Logging/LoggingEventListener.cs index 8a4189b6..a53a0a4d 100644 --- a/src/Diagnostics/src/Eventuous.Diagnostics.Logging/LoggingEventListener.cs +++ b/src/Diagnostics/src/Eventuous.Diagnostics.Logging/LoggingEventListener.cs @@ -10,10 +10,12 @@ public sealed class LoggingEventListener : EventListener { readonly EventLevel _level; readonly EventKeywords _keywords; - public LoggingEventListener(ILoggerFactory loggerFactory, - string? prefix = null, - EventLevel level = EventLevel.Verbose, - EventKeywords keywords = EventKeywords.All) { + public LoggingEventListener( + ILoggerFactory loggerFactory, + string? prefix = null, + EventLevel level = EventLevel.Verbose, + EventKeywords keywords = EventKeywords.All + ) { if (prefix != null) _prefix = prefix; _log = loggerFactory.CreateLogger(DiagnosticName.BaseName); _level = level; @@ -35,12 +37,12 @@ protected override void OnEventWritten(EventWrittenEventArgs evt) { if (evt.Message == null) return; var level = evt.Level switch { - EventLevel.Critical => LogLevel.Critical, - EventLevel.Error => LogLevel.Error, + EventLevel.Critical => LogLevel.Critical, + EventLevel.Error => LogLevel.Error, EventLevel.Informational => LogLevel.Information, - EventLevel.Warning => LogLevel.Warning, - EventLevel.Verbose => LogLevel.Debug, - _ => LogLevel.Information + EventLevel.Warning => LogLevel.Warning, + EventLevel.Verbose => LogLevel.Debug, + _ => LogLevel.Information }; #pragma warning disable CA2254 @@ -60,4 +62,4 @@ public override void Dispose() { base.Dispose(); } -} \ No newline at end of file +} diff --git a/src/Diagnostics/src/Eventuous.Diagnostics.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/Diagnostics/src/Eventuous.Diagnostics.OpenTelemetry/TracerProviderBuilderExtensions.cs index 3a0eeb79..70e4c933 100644 --- a/src/Diagnostics/src/Eventuous.Diagnostics.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/Diagnostics/src/Eventuous.Diagnostics.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -8,16 +8,15 @@ namespace Eventuous.Diagnostics.OpenTelemetry; [PublicAPI] public static class TracerProviderBuilderExtensions { /// - /// Adds Eventuous activity source to OpenTelemetry trace collection + /// Adds an Eventuous activity source to OpenTelemetry trace collection /// /// instance /// public static TracerProviderBuilder AddEventuousTracing(this TracerProviderBuilder builder) { - // The DummyListener is added by default so the remote context is propagated regardless. - // After adding the activity source to OpenTelemetry we don't need the dummy listener. + // The DummyListener is added by default, so the remote context is propagated regardless. + // After adding the activity source to OpenTelemetry, we don't need a fake listener. EventuousDiagnostics.RemoveDummyListener(); - return Ensure.NotNull(builder) - .AddSource(EventuousDiagnostics.InstrumentationName); + return Ensure.NotNull(builder).AddSource(EventuousDiagnostics.InstrumentationName); } -} \ No newline at end of file +} diff --git a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fakes/TestExporter.cs b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fakes/TestExporter.cs index 0b60ede7..fb97e635 100644 --- a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fakes/TestExporter.cs +++ b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fakes/TestExporter.cs @@ -35,14 +35,7 @@ public MetricValue[] CollectValues() { _ => throw new ArgumentOutOfRangeException() }; - values.Add( - new MetricValue( - metric.Name, - tags.Select(x => x.Item1).ToArray(), - tags.Select(x => x.Item2).ToArray()!, - metricValue - ) - ); + values.Add(new(metric.Name, tags.Select(x => x.Item1).ToArray(), tags.Select(x => x.Item2).ToArray()!, metricValue)); } } diff --git a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fixtures/MetricsSubscriptionFixtureBase.cs b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fixtures/MetricsSubscriptionFixtureBase.cs index ea3d0b63..8e217a96 100644 --- a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fixtures/MetricsSubscriptionFixtureBase.cs +++ b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/Fixtures/MetricsSubscriptionFixtureBase.cs @@ -19,6 +19,7 @@ public abstract class MetricsSubscriptionFixtureBase DefaultTag = new("test", "foo"); static MetricsSubscriptionFixtureBase() { @@ -38,8 +39,9 @@ static MetricsSubscriptionFixtureBase() { protected override void SetupServices(IServiceCollection services) { if (Output != null) { - _listener = new TestListener(Output); + _listener = new(Output); } + services.AddProducer(); services.AddSingleton(); @@ -51,12 +53,7 @@ protected override void SetupServices(IServiceCollection services) { .AddEventHandler() ); - services.AddOpenTelemetry() - .WithMetrics( - builder => builder - .AddEventuousSubscriptions() - .AddReader(new BaseExportingMetricReader(Exporter)) - ); + services.AddOpenTelemetry().WithMetrics(builder => builder.AddEventuousSubscriptions().AddReader(new BaseExportingMetricReader(Exporter))); } protected override void GetDependencies(IServiceProvider provider) { @@ -76,6 +73,6 @@ public override async Task DisposeAsync() { } } -class TestListener(ITestOutputHelper output): GenericListener(SubscriptionMetrics.ListenerName) { +class TestListener(ITestOutputHelper output) : GenericListener(SubscriptionMetrics.ListenerName) { protected override void OnEvent(KeyValuePair obj) => output.WriteLine($"{obj.Key} {obj.Value}"); } diff --git a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/MetricsTests.cs b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/MetricsTests.cs index cf54f0eb..03a0f393 100644 --- a/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/MetricsTests.cs +++ b/src/Diagnostics/test/Eventuous.Tests.OpenTelemetry/MetricsTests.cs @@ -12,9 +12,7 @@ public abstract class MetricsTestsBase, IMeasuredSubscription where TSubscriptionOptions : SubscriptionWithCheckpointOptions { - T Fixture { get; } = new() { - Output = outputHelper - }; + T Fixture { get; } = new() { Output = outputHelper }; [Fact] [Trait("Category", "Diagnostics")] diff --git a/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProduceOptions.cs b/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProduceOptions.cs index c3820633..7ee7a82b 100644 --- a/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProduceOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProduceOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Producers; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProducer.cs b/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProducer.cs index 70ae64f2..771cf496 100644 --- a/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProducer.cs +++ b/src/EventStore/src/Eventuous.EventStore/Producers/EventStoreProducer.cs @@ -1,4 +1,7 @@ -using Eventuous.Producers; +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + +using Eventuous.Producers; using Eventuous.Producers.Diagnostics; using Eventuous.Tools; diff --git a/src/EventStore/src/Eventuous.EventStore/StreamRevisionExtensions.cs b/src/EventStore/src/Eventuous.EventStore/StreamRevisionExtensions.cs index a6a784b6..dab57f40 100644 --- a/src/EventStore/src/Eventuous.EventStore/StreamRevisionExtensions.cs +++ b/src/EventStore/src/Eventuous.EventStore/StreamRevisionExtensions.cs @@ -9,22 +9,19 @@ public static class StreamRevisionExtensions { /// /// Stream version /// - public static StreamRevision AsStreamRevision(this ExpectedStreamVersion version) - => StreamRevision.FromInt64(version.Value); + public static StreamRevision AsStreamRevision(this ExpectedStreamVersion version) => StreamRevision.FromInt64(version.Value); /// /// Converts to /// /// Position for stream truncation /// - public static StreamPosition AsStreamPosition(this StreamTruncatePosition position) - => StreamPosition.FromInt64(position.Value); + public static StreamPosition AsStreamPosition(this StreamTruncatePosition position) => StreamPosition.FromInt64(position.Value); /// /// Converts to /// /// Position for stream reads /// - public static StreamPosition AsStreamPosition(this StreamReadPosition position) - => StreamPosition.FromInt64(position.Value); + public static StreamPosition AsStreamPosition(this StreamReadPosition position) => StreamPosition.FromInt64(position.Value); } diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/AllPersistentSubscription.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/AllPersistentSubscription.cs index 77b707c0..ac5b01bc 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/AllPersistentSubscription.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/AllPersistentSubscription.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + using Eventuous.EventStore.Subscriptions.Diagnostics; using Eventuous.Subscriptions.Diagnostics; using Eventuous.Subscriptions.Filters; @@ -92,13 +95,11 @@ CancellationToken cance ); /// - protected override ulong GetContextStreamPosition(ResolvedEvent re) - => re.Event.Position.CommitPosition; + protected override ulong GetContextStreamPosition(ResolvedEvent re) => re.Event.Position.CommitPosition; /// /// Returns a measure callback for the subscription /// /// - public GetSubscriptionEndOfStream GetMeasure() - => new AllStreamSubscriptionMeasure(Options.SubscriptionId, EventStoreClient).GetEndOfStream; + public GetSubscriptionEndOfStream GetMeasure() => new AllStreamSubscriptionMeasure(Options.SubscriptionId, EventStoreClient).GetEndOfStream; } diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/AllStreamSubscriptionMeasure.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/AllStreamSubscriptionMeasure.cs index 8703623f..6037dba7 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/AllStreamSubscriptionMeasure.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/AllStreamSubscriptionMeasure.cs @@ -1,14 +1,13 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions.Diagnostics; -class AllStreamSubscriptionMeasure(string subscriptionId, EventStoreClient eventStoreClient) : BaseSubscriptionMeasure(subscriptionId, "$all", eventStoreClient) { +class AllStreamSubscriptionMeasure(string subscriptionId, EventStoreClient eventStoreClient) + : BaseSubscriptionMeasure(subscriptionId, "$all", eventStoreClient) { protected override IAsyncEnumerable Read(CancellationToken cancellationToken) - => EventStoreClient.ReadAllAsync( - Direction.Backwards, - Position.End, - 1, - cancellationToken: cancellationToken - ); + => EventStoreClient.ReadAllAsync(Direction.Backwards, Position.End, 1, cancellationToken: cancellationToken); protected override ulong GetLastPosition(ResolvedEvent resolvedEvent) => resolvedEvent.Event.Position.CommitPosition; -} \ No newline at end of file +} diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/StreamSubscriptionMeasure.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/StreamSubscriptionMeasure.cs index 4abc5273..d8cd62a9 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/StreamSubscriptionMeasure.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Diagnostics/StreamSubscriptionMeasure.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions.Diagnostics; class StreamSubscriptionMeasure(string subscriptionId, StreamName streamName, EventStoreClient eventStoreClient) diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/EsdbMappings.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/EsdbMappings.cs index 2c545bd9..25f4ccde 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/EsdbMappings.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/EsdbMappings.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; static class EsdbMappings { diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/EventStoreExtensions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/EventStoreExtensions.cs index 7ba6e63d..8904bd01 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/EventStoreExtensions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/EventStoreExtensions.cs @@ -1,11 +1,13 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + using System.Reflection; namespace Eventuous.EventStore.Subscriptions; static class EventStoreExtensions { public static EventStoreClientSettings GetSettings(this EventStoreClient client) { - var prop = - typeof(EventStoreClient).GetProperty("Settings", BindingFlags.NonPublic | BindingFlags.Instance); + var prop = typeof(EventStoreClient).GetProperty("Settings", BindingFlags.NonPublic | BindingFlags.Instance); var getter = prop!.GetGetMethod(true); return (EventStoreClientSettings) getter!.Invoke(client, null)!; diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllPersistentSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllPersistentSubscriptionOptions.cs index 435893b9..ac0a5734 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllPersistentSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllPersistentSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllStreamSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllStreamSubscriptionOptions.cs index aaee6770..e78116f2 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllStreamSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/AllStreamSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/CatchUpSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/CatchUpSubscriptionOptions.cs index bd92fa3c..34eaafa1 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/CatchUpSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/CatchUpSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + using Eventuous.Subscriptions.Filters; namespace Eventuous.EventStore.Subscriptions; diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/EventStoreSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/EventStoreSubscriptionOptions.cs index 98401a51..e1e21e02 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/EventStoreSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/EventStoreSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/PersistentSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/PersistentSubscriptionOptions.cs index 83f2e251..a833ed12 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/PersistentSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/PersistentSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// @@ -23,7 +26,7 @@ public abstract record PersistentSubscriptionOptions : EventStoreSubscriptionOpt // public uint ConcurrencyLevel { get; set; } = 1; /// - /// Allows to override the failure handling behaviour. By default, when the consumer crashes, the event is + /// Allows overriding the failure handling behavior. By default, when the consumer crashes, the event is /// retries and then NACKed. You can use this function to, for example, park the failed event. /// public HandleEventProcessingFailure? FailureHandler { get; set; } diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamPersistentSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamPersistentSubscriptionOptions.cs index 5d2bb339..687ab1a2 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamPersistentSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamPersistentSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamSubscriptionOptions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamSubscriptionOptions.cs index cb4e3639..1e77d51f 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamSubscriptionOptions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/Options/StreamSubscriptionOptions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + namespace Eventuous.EventStore.Subscriptions; /// diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamPersistentSubscription.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamPersistentSubscription.cs index f4e53f58..519901e0 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamPersistentSubscription.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamPersistentSubscription.cs @@ -19,11 +19,11 @@ public class StreamPersistentSubscription : PersistentSubscriptionBaseConsume pipe, provided automatically /// Optional logger factory public StreamPersistentSubscription( - EventStoreClient eventStoreClient, - StreamPersistentSubscriptionOptions options, - ConsumePipe consumePipe, - ILoggerFactory? loggerFactory - ) : base(eventStoreClient, options, consumePipe, loggerFactory) + EventStoreClient eventStoreClient, + StreamPersistentSubscriptionOptions options, + ConsumePipe consumePipe, + ILoggerFactory? loggerFactory + ) : base(eventStoreClient, options, consumePipe, loggerFactory) => Ensure.NotEmptyString(options.StreamName); /// @@ -37,14 +37,14 @@ public StreamPersistentSubscription( /// /// public StreamPersistentSubscription( - EventStoreClient eventStoreClient, - StreamName streamName, - string subscriptionId, - ConsumePipe consumerPipe, - IEventSerializer? eventSerializer = null, - IMetadataSerializer? metaSerializer = null, - ILoggerFactory? loggerFactory = null - ) : this( + EventStoreClient eventStoreClient, + StreamName streamName, + string subscriptionId, + ConsumePipe consumerPipe, + IEventSerializer? eventSerializer = null, + IMetadataSerializer? metaSerializer = null, + ILoggerFactory? loggerFactory = null + ) : this( eventStoreClient, new StreamPersistentSubscriptionOptions { StreamName = streamName, @@ -58,9 +58,9 @@ public StreamPersistentSubscription( /// protected override Task CreatePersistentSubscription( - PersistentSubscriptionSettings settings, - CancellationToken cancellationToken - ) + PersistentSubscriptionSettings settings, + CancellationToken cancellationToken + ) => SubscriptionClient.CreateToStreamAsync( Options.StreamName, Options.SubscriptionId, @@ -72,10 +72,10 @@ CancellationToken cancellationToken /// protected override Task LocalSubscribe( - Func eventAppeared, - Action? subscriptionDropped, - CancellationToken cancellationToken - ) + Func eventAppeared, + Action? subscriptionDropped, + CancellationToken cancellationToken + ) => SubscriptionClient.SubscribeToStreamAsync( Options.StreamName, Options.SubscriptionId, @@ -91,6 +91,5 @@ CancellationToken cance /// public GetSubscriptionEndOfStream GetMeasure() - => new StreamSubscriptionMeasure(Options.SubscriptionId, Options.StreamName, EventStoreClient) - .GetEndOfStream; -} \ No newline at end of file + => new StreamSubscriptionMeasure(Options.SubscriptionId, Options.StreamName, EventStoreClient).GetEndOfStream; +} diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamSubscription.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamSubscription.cs index 847935d5..f0f6f631 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamSubscription.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/StreamSubscription.cs @@ -77,9 +77,7 @@ public StreamSubscription( protected override async ValueTask Subscribe(CancellationToken cancellationToken) { var (_, position) = await GetCheckpoint(cancellationToken).NoContext(); - var fromStream = position == null - ? FromStream.Start - : FromStream.After(StreamPosition.FromInt64((long)position)); + var fromStream = position == null ? FromStream.Start : FromStream.After(StreamPosition.FromInt64((long)position)); Subscription = await EventStoreClient.SubscribeToStreamAsync( Options.StreamName, @@ -106,11 +104,7 @@ async Task HandleEvent(ResolvedEvent re, CancellationToken ct) { await HandleInternal(CreateContext(re, ct)).NoContext(); } - void HandleDrop( - global::EventStore.Client.StreamSubscription _, - SubscriptionDroppedReason reason, - Exception? ex - ) + void HandleDrop(global::EventStore.Client.StreamSubscription _, SubscriptionDroppedReason reason, Exception? ex) => Dropped(EsdbMappings.AsDropReason(reason), ex); } diff --git a/src/EventStore/src/Eventuous.EventStore/Subscriptions/SubscriptionBuilderExtensions.cs b/src/EventStore/src/Eventuous.EventStore/Subscriptions/SubscriptionBuilderExtensions.cs index 5edd5422..f2069777 100644 --- a/src/EventStore/src/Eventuous.EventStore/Subscriptions/SubscriptionBuilderExtensions.cs +++ b/src/EventStore/src/Eventuous.EventStore/Subscriptions/SubscriptionBuilderExtensions.cs @@ -1,3 +1,6 @@ +// Copyright (C) Ubiquitous AS. All rights reserved +// Licensed under the Apache License, Version 2.0. + using Eventuous.Subscriptions.Checkpoints; using Eventuous.Subscriptions.Registrations; diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/EsdbContainer.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/EsdbContainer.cs index 581ebe77..d904e850 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/EsdbContainer.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/EsdbContainer.cs @@ -6,8 +6,8 @@ namespace Eventuous.Tests.EventStore.Fixtures; public static class EsdbContainer { public static EventStoreDbContainer Create() { var image = RuntimeInformation.ProcessArchitecture == Architecture.Arm64 - ? "eventstore/eventstore:22.10.5-alpha-arm64v8" - : "eventstore/eventstore:22.10.5-bookworm-slim"; + ? "eventstore/eventstore:24.2.0-alpha-arm64v8" + : "eventstore/eventstore:24.2"; return new EventStoreDbBuilder() .WithImage(image) diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/Serializer.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/Serializer.cs deleted file mode 100644 index 864ab1c0..00000000 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/Serializer.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json; -using NodaTime.Serialization.SystemTextJson; - -namespace Eventuous.Tests.EventStore.Fixtures; - -public static class Serializer { - public static IEventSerializer Json { get; } = new DefaultEventSerializer( - new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - ); -} \ No newline at end of file diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/StoreFixture.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/StoreFixture.cs index a0aab6d7..5a8fe767 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/StoreFixture.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/StoreFixture.cs @@ -21,8 +21,7 @@ static StoreFixture() { } IEventSerializer Serializer { get; } = new DefaultEventSerializer( - new JsonSerializerOptions(JsonSerializerDefaults.Web) - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) + new JsonSerializerOptions(JsonSerializerDefaults.Web).ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) ); public StoreFixture() { diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/TestCheckpointStore.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/TestCheckpointStore.cs index c4196d45..424e371e 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/TestCheckpointStore.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Fixtures/TestCheckpointStore.cs @@ -4,15 +4,15 @@ public class TestCheckpointStore : ICheckpointStore { readonly Dictionary _checkpoints = new(); public ValueTask GetLastCheckpoint(string checkpointId, CancellationToken cancellationToken) { - var checkpoint = _checkpoints.TryGetValue(checkpointId, out var cp) ? cp : new Checkpoint(checkpointId, null); + var checkpoint = _checkpoints.TryGetValue(checkpointId, out var cp) ? cp : new(checkpointId, null); Logger.Current.CheckpointLoaded(this, checkpoint); - return new ValueTask(checkpoint); + return new(checkpoint); } public ValueTask StoreCheckpoint(Checkpoint checkpoint, bool force, CancellationToken cancellationToken) { Logger.Current.CheckpointStored(this, checkpoint, force); _checkpoints[checkpoint.Id] = checkpoint; - return new ValueTask(checkpoint); + return new(checkpoint); } public ulong? GetCheckpoint(string checkpointId) => _checkpoints.TryGetValue(checkpointId, out var cp) ? cp.Position : null; diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/ProducerTracesTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/ProducerTracesTests.cs index 331287ce..b8ab9983 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/ProducerTracesTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/ProducerTracesTests.cs @@ -10,8 +10,8 @@ namespace Eventuous.Tests.EventStore; public class TracesTests : LegacySubscriptionFixture, IDisposable { readonly ActivityListener _listener; - public TracesTests(ITestOutputHelper output) : base(output, new TracedHandler(), false) { - _listener = new ActivityListener { + public TracesTests(ITestOutputHelper output) : base(output, new(), false) { + _listener = new() { ShouldListenTo = _ => true, Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, ActivityStarted = activity => Log.LogInformation( diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Store/AggregateStoreTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Store/AggregateStoreTests.cs index 31449d7c..7195a920 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Store/AggregateStoreTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Store/AggregateStoreTests.cs @@ -21,9 +21,9 @@ public AggregateStoreTests(StoreFixture fixture, ITestOutputHelper output) { [Trait("Category", "Store")] public async Task AppendedEventShouldBeTraced() { var id = new TestId(Guid.NewGuid().ToString("N")); - var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); + var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); aggregate.DoIt("test"); - await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); + await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); } [Fact] @@ -38,7 +38,7 @@ public async Task ShouldReadLongAggregateStream() { .Select(x => new TestEvent(x.ToString())) .ToArray(); - var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); + var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); var counter = 0; @@ -49,15 +49,15 @@ public async Task ShouldReadLongAggregateStream() { if (counter != 1000) continue; _log.LogInformation("Storing batch of events.."); - await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); - aggregate = await _fixture.AggregateStore.Load(id, CancellationToken.None); + await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); + aggregate = await _fixture.AggregateStore.Load(id, CancellationToken.None); counter = 0; } - await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); + await _fixture.AggregateStore.Store(aggregate, id, CancellationToken.None); _log.LogInformation("Loading large aggregate stream.."); - var restored = await _fixture.AggregateStore.Load(id, CancellationToken.None); + var restored = await _fixture.AggregateStore.Load(id, CancellationToken.None); restored.State.Values.Count.Should().Be(count); restored.State.Values.Should().BeEquivalentTo(aggregate.State.Values); @@ -67,28 +67,22 @@ public async Task ShouldReadLongAggregateStream() { [Trait("Category", "Store")] public async Task ShouldReadAggregateStreamManyTimes() { var id = new TestId(Guid.NewGuid().ToString("N")); - var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); + var aggregate = AggregateFactoryRegistry.Instance.CreateInstance(); aggregate.DoIt("test"); - await _fixture.AggregateStore.Store(aggregate, id, default); + await _fixture.AggregateStore.Store(aggregate, id, default); const int numberOfReads = 100; foreach (var unused in Enumerable.Range(0, numberOfReads)) { - var read = await _fixture.AggregateStore.Load(id, default); + var read = await _fixture.AggregateStore.Load(id, default); read.State.Should().BeEquivalentTo(aggregate.State); } } - record TestId : Id { - public TestId(string value) - : base(value) { } - } + record TestId(string Value) : Id(Value); record TestState : State { - public TestState() - => On( - (state, evt) => state with { Values = state.Values.Add(evt.Data) } - ); + public TestState() => On((state, evt) => state with { Values = state.Values.Add(evt.Data) }); public ImmutableList Values { get; init; } = ImmutableList.Empty; } diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/LegacySubscriptionFixture.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/LegacySubscriptionFixture.cs index e71c2c77..16b4fb52 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/LegacySubscriptionFixture.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/LegacySubscriptionFixture.cs @@ -11,11 +11,11 @@ public abstract class LegacySubscriptionFixture : IAsyncLifetime where T : cl protected readonly Fixture Auto = new(); protected StreamName Stream { get; } = new($"test-{Guid.NewGuid():N}"); - protected StoreFixture StoreFixture { get; } + protected StoreFixture StoreFixture { get; } = new(); protected T Handler { get; } protected EventStoreProducer Producer { get; private set; } = null!; protected ILogger Log { get; } - protected TestCheckpointStore CheckpointStore { get; } + protected TestCheckpointStore CheckpointStore { get; } = new(); protected StreamSubscription Subscription { get; set; } = null!; protected LegacySubscriptionFixture( @@ -29,13 +29,8 @@ protected LegacySubscriptionFixture( if (stream is { } s) Stream = s; LoggerFactory = TestHelpers.Logging.GetLoggerFactory(output, logLevel); - - StoreFixture = new StoreFixture(); - Handler = handler; - Log = LoggerFactory.CreateLogger(GetType()); - CheckpointStore = new TestCheckpointStore(); - - // _listener = new LoggingEventListener(LoggerFactory); + Handler = handler; + Log = LoggerFactory.CreateLogger(GetType()); } protected ValueTask Start() => Subscription.SubscribeWithLog(Log); @@ -43,8 +38,7 @@ protected LegacySubscriptionFixture( protected ValueTask Stop() => Subscription.UnsubscribeWithLog(Log); ILoggerFactory LoggerFactory { get; } - readonly bool _autoStart; - // readonly LoggingEventListener _listener; + readonly bool _autoStart; public async Task InitializeAsync() { await StoreFixture.InitializeAsync(); @@ -54,9 +48,9 @@ public async Task InitializeAsync() { var pipe = new ConsumePipe(); pipe.AddDefaultConsumer(Handler); - Subscription = new StreamSubscription( + Subscription = new( StoreFixture.Client, - new StreamSubscriptionOptions { + new() { StreamName = Stream, SubscriptionId = subscriptionId, ResolveLinkTos = Stream.ToString().StartsWith('$') @@ -70,7 +64,6 @@ public async Task InitializeAsync() { public async Task DisposeAsync() { if (_autoStart) await Stop(); - // _listener.Dispose(); await StoreFixture.DisposeAsync(); } } diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentPublishAndSubscribeManyTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentPublishAndSubscribeManyTests.cs index 6e7f9f62..f2ca51b7 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentPublishAndSubscribeManyTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentPublishAndSubscribeManyTests.cs @@ -5,7 +5,7 @@ namespace Eventuous.Tests.EventStore.Subscriptions; [Collection("Database")] public class PersistentPublishAndSubscribeManyTests(ITestOutputHelper outputHelper) - : PersistentSubscriptionFixture(outputHelper, new TestEventHandler(), false) { + : PersistentSubscriptionFixture(outputHelper, new(), false) { [Fact] [Trait("Category", "Persistent subscription")] public async Task SubscribeAndProduceMany() { diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentSubscriptionFixture.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentSubscriptionFixture.cs index ee4c1241..4c731a65 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentSubscriptionFixture.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PersistentSubscriptionFixture.cs @@ -11,11 +11,8 @@ public abstract class PersistentSubscriptionFixture( T handler, bool autoStart = true, LogLevel logLevel = LogLevel.Information - ) - : IAsyncLifetime - where T : class, IEventHandler { - static PersistentSubscriptionFixture() - => TypeMap.Instance.RegisterKnownEventTypes(typeof(TestEvent).Assembly); + ) : IAsyncLifetime where T : class, IEventHandler { + static PersistentSubscriptionFixture() => TypeMap.Instance.RegisterKnownEventTypes(typeof(TestEvent).Assembly); protected readonly Fixture Auto = new(); @@ -30,20 +27,20 @@ static PersistentSubscriptionFixture() protected ValueTask Stop() => Subscription.UnsubscribeWithLog(Log); - LoggingEventListener _listener = null!; + LoggingEventListener _listener = null!; public async Task InitializeAsync() { await Fixture.InitializeAsync(); - Producer = new EventStoreProducer(Fixture.Client); + Producer = new(Fixture.Client); var loggerFactory = TestHelpers.Logging.GetLoggerFactory(outputHelper, logLevel); var subscriptionId = $"test-{Guid.NewGuid():N}"; Log = loggerFactory.CreateLogger(GetType()); - _listener = new LoggingEventListener(loggerFactory); + _listener = new(loggerFactory); - Subscription = new StreamPersistentSubscription( + Subscription = new( Fixture.Client, - new StreamPersistentSubscriptionOptions { + new() { StreamName = Stream, SubscriptionId = subscriptionId }, diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyPartitionedTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyPartitionedTests.cs index 1b7f82e8..ad7c4766 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyPartitionedTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyPartitionedTests.cs @@ -7,7 +7,7 @@ namespace Eventuous.Tests.EventStore.Subscriptions; public class PublishAndSubscribeManyPartitionedTests(ITestOutputHelper output) : LegacySubscriptionFixture( output, - new TestEventHandler(new TestEventHandlerOptions(5.Milliseconds(), output)), + new(new(5.Milliseconds(), output)), false, new StreamName(Guid.NewGuid().ToString("N")), logLevel: LogLevel.Trace diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyTests.cs index 131864ca..5576b6fd 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeManyTests.cs @@ -5,12 +5,7 @@ namespace Eventuous.Tests.EventStore.Subscriptions; [Collection("Database")] public class PublishAndSubscribeManyTests(ITestOutputHelper output) - : LegacySubscriptionFixture( - output, - new TestEventHandler(new TestEventHandlerOptions(1.Milliseconds(), output)), - false, - logLevel: LogLevel.Trace - ) { + : LegacySubscriptionFixture(output, new(new(1.Milliseconds(), output)), false, logLevel: LogLevel.Trace) { [Fact] [Trait("Category", "Stream catch-up subscription")] public async Task SubscribeAndProduceMany() { diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeOneTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeOneTests.cs index f1f268b9..199de15d 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeOneTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/PublishAndSubscribeOneTests.cs @@ -5,7 +5,7 @@ namespace Eventuous.Tests.EventStore.Subscriptions; [Collection("Database")] public class PublishAndSubscribeOneTests(ITestOutputHelper output) - : LegacySubscriptionFixture(output, new TestEventHandler(new TestEventHandlerOptions(null, output)), false, logLevel: LogLevel.Trace) { + : LegacySubscriptionFixture(output, new(new(null, output)), false, logLevel: LogLevel.Trace) { [Fact] [Trait("Category", "Stream catch-up subscription")] public async Task SubscribeAndProduce() { diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionDeletedEventsTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionDeletedEventsTests.cs index 698b1ef0..17874c1d 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionDeletedEventsTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionDeletedEventsTests.cs @@ -12,63 +12,39 @@ namespace Eventuous.Tests.EventStore.Subscriptions; [Collection("Database")] public sealed class StreamSubscriptionDeletedEventsTests : IClassFixture, IDisposable { - readonly StoreFixture _fixture; + readonly StoreFixture _fixture; readonly ILoggerFactory _loggerFactory; readonly LoggingEventListener _listener; public StreamSubscriptionDeletedEventsTests(StoreFixture fixture, ITestOutputHelper output) { - _fixture = fixture; - - _loggerFactory = LoggerFactory.Create( - cfg => cfg.AddXunit(output, LogLevel.Debug).SetMinimumLevel(LogLevel.Debug) - ); - - _listener = new LoggingEventListener(_loggerFactory); + _fixture = fixture; + _loggerFactory = LoggerFactory.Create(cfg => cfg.AddXunit(output, LogLevel.Debug).SetMinimumLevel(LogLevel.Debug)); + _listener = new(_loggerFactory); } [Fact] [Trait("Category", "Special cases")] public async Task StreamSubscriptionGetsDeletedEvents() { - var service = new BookingService(_fixture.AggregateStore); - - var categoryStream = new StreamName("$ce-Booking"); - - ulong? startPosition = null; + var service = new BookingService(_fixture.AggregateStore); + var categoryStream = new StreamName("$ce-Booking"); + ulong? startPosition = null; try { - var last = await _fixture.Client.ReadStreamAsync( - Direction.Backwards, - categoryStream, - StreamPosition.End, - 1 - ) - .ToArrayAsync(); - + var last = await _fixture.Client.ReadStreamAsync(Direction.Backwards, categoryStream, StreamPosition.End, 1).ToArrayAsync(); startPosition = last[0].OriginalEventNumber; } catch (StreamNotFoundException) { } const int produceCount = 20; const int deleteCount = 5; - var commands = Enumerable.Range(0, produceCount) - .Select(_ => DomainFixture.CreateImportBooking()) - .ToArray(); + var commands = Enumerable.Range(0, produceCount).Select(_ => DomainFixture.CreateImportBooking()).ToArray(); - await Task.WhenAll( - commands.Select(x => service.Handle(x, CancellationToken.None)) - ); + await Task.WhenAll(commands.Select(x => service.Handle(x, CancellationToken.None))); var delete = Enumerable.Range(5, deleteCount).Select(x => commands[x]).ToList(); await Task.WhenAll( - delete - .Select( - x => _fixture.EventStore.DeleteStream( - StreamName.For(x.BookingId), - ExpectedStreamVersion.Any, - CancellationToken.None - ) - ) + delete.Select(x => _fixture.EventStore.DeleteStream(StreamName.For(x.BookingId), ExpectedStreamVersion.Any, CancellationToken.None)) ); var handler = new TestHandler(); @@ -77,7 +53,7 @@ await Task.WhenAll( var subscription = new StreamSubscription( _fixture.Client, - new StreamSubscriptionOptions { + new() { StreamName = categoryStream, SubscriptionId = subscriptionId, ResolveLinkTos = true, @@ -99,17 +75,8 @@ await Task.WhenAll( await subscription.UnsubscribeWithLog(log); - log.LogInformation("Received {Count} events", handler.Count); - - var actual = handler.Processed - .Select(x => x.Stream.GetId()) - .ToList(); - - log.LogInformation("Actual contains {Count} events", actual.Count); - - actual - .Should() - .BeEquivalentTo(commands.Except(delete).Select(x => x.BookingId)); + var actual = handler.Processed.Select(x => x.Stream.GetId()).ToList(); + actual.Should().BeEquivalentTo(commands.Except(delete).Select(x => x.BookingId)); } class TestHandler : BaseEventHandler { diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionWithLinksTests.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionWithLinksTests.cs index 658cd9c0..539951db 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionWithLinksTests.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/StreamSubscriptionWithLinksTests.cs @@ -35,7 +35,7 @@ public async Task ShouldHandleHalfOfTheEvents() { const int expectedCount = count / 2; var checkpointStore = Provider.GetRequiredService(); - await checkpointStore.StoreCheckpoint(new Checkpoint(SubId, expectedCount - 1), true, default); + await checkpointStore.StoreCheckpoint(new(SubId, expectedCount - 1), true, default); await Start(); await Execute(count, expectedCount); @@ -70,7 +70,6 @@ async Task> Seed(IServiceProvider provider, int count) { void ValidateProcessed(IServiceProvider provider, IEnumerable events) { var handler = provider.GetRequiredKeyedService(SubId); Output?.WriteLine($"Processed {handler.Handled.Count} events"); - // handler.Handled.Should().BeEquivalentTo(events); foreach (var evt in events) { handler.Handled.Should().Contain(evt); } @@ -125,7 +124,7 @@ protected override void SetupServices(IServiceCollection services) { builder => builder .Configure( x => { - x.StreamName = new StreamName($"$ce-{_prefix}"); + x.StreamName = new($"$ce-{_prefix}"); x.ConcurrencyLimit = 5; x.ResolveLinkTos = true; } diff --git a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/SubscriptionFixture.cs b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/SubscriptionFixture.cs index 49c71528..94706938 100644 --- a/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/SubscriptionFixture.cs +++ b/src/EventStore/test/Eventuous.Tests.EventStore/Subscriptions/SubscriptionFixture.cs @@ -44,8 +44,7 @@ protected override void GetDependencies(IServiceProvider provider) { public EventStoreClient Client { get; set; } = null!; - protected override ILoggingBuilder ConfigureLogging(ILoggingBuilder builder) - => base.ConfigureLogging(builder).AddFilter(Filter); + protected override ILoggingBuilder ConfigureLogging(ILoggingBuilder builder) => base.ConfigureLogging(builder).AddFilter(Filter); public override async Task GetLastPosition() { return streamName == "$all" ? await GetLastFromAll() : await GetLastFromStream(); diff --git a/src/Experimental/src/Eventuous.Spyglass/InsidePeek.cs b/src/Experimental/src/Eventuous.Spyglass/InsidePeek.cs index 941349a2..7c6720a0 100644 --- a/src/Experimental/src/Eventuous.Spyglass/InsidePeek.cs +++ b/src/Experimental/src/Eventuous.Spyglass/InsidePeek.cs @@ -29,7 +29,7 @@ void Scan(Assembly assembly) { return; } - var aggregateType = typeof(Aggregate); + var aggregateType = typeof(Aggregate<>); var cl = assembly .ExportedTypes @@ -56,7 +56,7 @@ void Scan(Assembly assembly) { : type.BaseType!.GenericTypeArguments[0]; } - static dynamic CreateInstance(Dictionary> reg, Type aggregateType) { + static dynamic CreateInstance(Dictionary> reg, Type aggregateType) { var instance = reg.TryGetValue(aggregateType, out var factory) ? factory() : Activator.CreateInstance(aggregateType)!; diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Diagnostics/ExtensionsEventSource.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Diagnostics/ExtensionsEventSource.cs index 0ab62f60..8c69965e 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Diagnostics/ExtensionsEventSource.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Diagnostics/ExtensionsEventSource.cs @@ -18,12 +18,6 @@ public void HttpEndpointRegistered(string route) { HttpEndpointRegistered(typeof(T).Name, route); } - [Event( - HttpEndpointRegisteredId, - Message = "Http endpoint registered for {0} at {1}", - Level = EventLevel.Verbose - )] - void HttpEndpointRegistered(string type, string route) - => WriteEvent(HttpEndpointRegisteredId, type, route); - + [Event(HttpEndpointRegisteredId, Message = "Http endpoint registered for {0} at {1}", Level = EventLevel.Verbose)] + void HttpEndpointRegistered(string type, string route) => WriteEvent(HttpEndpointRegisteredId, type, route); } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBase.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBase.cs index 12af90d7..08539ab7 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBase.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBase.cs @@ -4,14 +4,23 @@ namespace Eventuous.AspNetCore.Web; /// -/// Base class for exposing commands via Web API using a controller. +/// Base class for exposing commands via Web API using a controller that returns the default result. /// -/// Aggregate type -/// Result type +/// State type [PublicAPI] -public abstract class CommandHttpApiBase(ICommandService service, MessageMap? commandMap = null) : ControllerBase - where TAggregate : Aggregate - where TResult : Result { +public abstract class CommandHttpApiBase(ICommandService service, MessageMap? commandMap = null) + : CommandHttpApiBase>(service, commandMap) where TState : State, new(); + +/// +/// Base class for exposing commands via Web API using a controller returning custom result type. +/// +/// Command service +/// Optional: Map between external and internal commands +/// State type +/// Custom result type +[PublicAPI] +public abstract class CommandHttpApiBase(ICommandService service, MessageMap? commandMap = null) : ControllerBase + where TState : State, new() { /// /// Call this method from your HTTP endpoints to handle commands and wrap the result properly. /// @@ -19,7 +28,7 @@ public abstract class CommandHttpApiBase(ICommandServiceRequest cancellation token /// Command type /// A custom result class that inherits from . - protected async Task> Handle(TCommand command, CancellationToken cancellationToken) + protected virtual async Task> Handle(TCommand command, CancellationToken cancellationToken) where TCommand : class { var result = await service.Handle(command, cancellationToken); @@ -36,7 +45,7 @@ protected async Task> Handle(TCommand command, C /// Domain command type /// A custom result class that inherits from . /// Throws if the command map hasn't been configured - protected async Task> Handle(TContract httpCommand, CancellationToken cancellationToken) + protected virtual async Task> Handle(TContract httpCommand, CancellationToken cancellationToken) where TContract : class where TCommand : class { if (commandMap == null) throw new InvalidOperationException("Command map is not configured"); @@ -46,14 +55,10 @@ protected async Task> Handle(TContrac return AsActionResult(result); } - protected virtual ActionResult AsActionResult(Result result) { - return result.AsActionResult(); - } + /// + /// Function to convert the default result to a custom result + /// + /// Command execution result + /// ActionResult with custom payload + protected virtual ActionResult AsActionResult(Result result) => result.AsActionResult(); } - -/// -/// Base class for exposing commands via Web API using a controller. -/// -/// Aggregate type -public abstract class CommandHttpApiBase(ICommandService service, MessageMap? commandMap = null) - : CommandHttpApiBase(service, commandMap) where TAggregate : Aggregate; diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBaseFunc.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBaseFunc.cs deleted file mode 100644 index 91be6940..00000000 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandHttpApiBaseFunc.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) Ubiquitous AS. All rights reserved -// Licensed under the Apache License, Version 2.0. - -namespace Eventuous.AspNetCore.Web; - -/// -/// Base class for exposing commands via Web API using a controller. -/// -/// State type -/// Result type -[PublicAPI] -public abstract class CommandHttpApiBaseFunc(IFuncCommandService service, MessageMap? commandMap = null) : ControllerBase - where TState : State, new() - where TResult : Result { - /// - /// Call this method from your HTTP endpoints to handle commands and wrap the result properly. - /// - /// Command instance - /// Request cancellation token - /// Command type - /// A custom result class that inherits from . - protected async Task> Handle(TCommand command, CancellationToken cancellationToken) - where TCommand : class { - var result = await service.Handle(command, cancellationToken); - - return result.AsActionResult(); - } - - /// - /// Call this method from your HTTP endpoints to handle commands where there is a mapping between - /// HTTP contract and the domain command, and wrap the result properly. - /// - /// HTTP command - /// Cancellation token - /// HTTP command type - /// Domain command type - /// A custom result class that inherits from . - /// Throws if the command map hasn't been configured - protected async Task> Handle(TContract httpCommand, CancellationToken cancellationToken) - where TContract : class where TCommand : class { - if (commandMap == null) throw new InvalidOperationException("Command map is not configured"); - - var command = commandMap.Convert(httpCommand); - var result = await service.Handle(command, cancellationToken); - - return result.AsActionResult(); - } -} - -/// -/// Base class for exposing commands via Web API using a controller. -/// -/// State type -public abstract class CommandHttpApiBaseFunc(IFuncCommandService service, MessageMap? commandMap = null) - : CommandHttpApiBaseFunc(service, commandMap) where TState : State, new(); diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandServiceRouteBuilder.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandServiceRouteBuilder.cs index 5bbdbe34..d7ecdb39 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandServiceRouteBuilder.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/CommandServiceRouteBuilder.cs @@ -6,9 +6,7 @@ namespace Eventuous.AspNetCore.Web; -public class CommandServiceRouteBuilder(IEndpointRouteBuilder builder) - where TAggregate : Aggregate - where TResult : Result { +public class CommandServiceRouteBuilder(IEndpointRouteBuilder builder) where TState : State, new() { /// /// Maps the given command type to an HTTP endpoint. The command class can be annotated with /// the if you need a custom route. @@ -17,12 +15,12 @@ public class CommandServiceRouteBuilder(IEndpointRouteBuild /// Additional route configuration /// Command class /// - public CommandServiceRouteBuilder MapCommand( + public CommandServiceRouteBuilder MapCommand( EnrichCommandFromHttpContext? enrichCommand = null, Action? configure = null ) where TCommand : class { - if (configure == null) { builder.MapCommand(enrichCommand); } - else { configure(builder.MapCommand(enrichCommand)); } + if (configure == null) { builder.MapCommand(enrichCommand); } + else { configure(builder.MapCommand(enrichCommand)); } return this; } @@ -35,13 +33,13 @@ public CommandServiceRouteBuilder MapCommand( /// Additional route configuration /// Command type /// - public CommandServiceRouteBuilder MapCommand( + public CommandServiceRouteBuilder MapCommand( string route, EnrichCommandFromHttpContext? enrichCommand = null, Action? configure = null ) where TCommand : class { - if (configure == null) { builder.MapCommand(route, enrichCommand); } - else { configure(builder.MapCommand(route, enrichCommand)); } + if (configure == null) { builder.MapCommand(route, enrichCommand); } + else { configure(builder.MapCommand(route, enrichCommand)); } return this; } @@ -56,13 +54,13 @@ public CommandServiceRouteBuilder MapCommand( /// /// /// - public CommandServiceRouteBuilder MapCommand( + public CommandServiceRouteBuilder MapCommand( string? route, ConvertAndEnrichCommand enrichCommand, Action? configure = null ) where TCommand : class where TContract : class { - if (configure == null) { builder.MapCommand(route, Ensure.NotNull(enrichCommand)); } - else { configure(builder.MapCommand(route, Ensure.NotNull(enrichCommand))); } + if (configure == null) { builder.MapCommand(route, Ensure.NotNull(enrichCommand)); } + else { configure(builder.MapCommand(route, Ensure.NotNull(enrichCommand))); } return this; } @@ -76,13 +74,14 @@ public CommandServiceRouteBuilder MapCommand /// /// - public CommandServiceRouteBuilder MapCommand( + public CommandServiceRouteBuilder MapCommand( ConvertAndEnrichCommand enrichCommand, Action? configure = null ) where TCommand : class where TContract : class { var attr = typeof(TContract).GetAttribute(); - AttributeCheck.EnsureCorrectAggregate(attr); + AttributeCheck.EnsureCorrectParent(attr); + return MapCommand(attr?.Route, Ensure.NotNull(enrichCommand), configure); } } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandAttribute.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandAttribute.cs index 7d67144a..9d2b6f96 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandAttribute.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandAttribute.cs @@ -4,8 +4,9 @@ namespace Eventuous.AspNetCore.Web; /// -/// Use this attribute on individual command contracts. It can be used in combination with -/// , in that case you won't need to specify the aggregate type. +/// Use this attribute on individual command contracts. +/// It can be used in combination with . +/// In that case, you won't need to specify the aggregate type. /// When used without a nesting class, the aggregate type is mandatory. The Route property /// is optional, if you omit it, we'll use the command class name as the route. /// @@ -17,14 +18,9 @@ public class HttpCommandAttribute : Attribute { public string? Route { get; set; } /// - /// Aggregate type for the command, will be used to resolve the command service + /// Aggregate type for the command will be used to resolve the command service /// - public Type? AggregateType { get; set; } - - /// - /// The result type with as default . - /// - public Type ResultType { get; set; } = typeof(Result); + public Type? StateType { get; set; } /// /// Authorization policy name @@ -33,22 +29,13 @@ public class HttpCommandAttribute : Attribute { } [AttributeUsage(AttributeTargets.Class)] -public class HttpCommandAttribute : HttpCommandAttribute where TAggregate : Aggregate { - public HttpCommandAttribute() => AggregateType = typeof(TAggregate); +public class HttpCommandAttribute : HttpCommandAttribute where TState : State { + public HttpCommandAttribute() => StateType = typeof(TState); } [AttributeUsage(AttributeTargets.Class)] -public class HttpCommandAttribute : HttpCommandAttribute where TAggregate : Aggregate where TResult : Result { - public HttpCommandAttribute() { - AggregateType = typeof(TAggregate); - ResultType = typeof(TResult); - } -} - -[AttributeUsage(AttributeTargets.Class)] -public class AggregateCommandsAttribute(Type aggregateType) : Attribute { - public Type AggregateType { get; set; } = aggregateType; - public Type? ResultType { get; set; } +public class StateCommandsAttribute(Type stateType) : Attribute { + public Type StateType { get; set; } = stateType; } /// @@ -57,24 +44,16 @@ public class AggregateCommandsAttribute(Type aggregateType) : Attribute { /// must operate on a single aggregate type. /// [AttributeUsage(AttributeTargets.Class)] -public class AggregateCommandsAttribute() : AggregateCommandsAttribute(typeof(TAggregate)) where TAggregate : Aggregate; - -[AttributeUsage(AttributeTargets.Class)] -public class AggregateCommandsAttribute : AggregateCommandsAttribute - where TAggregate : Aggregate where TResult : Result { - public AggregateCommandsAttribute() : base(typeof(TAggregate)) { - ResultType = typeof(TResult); - } -} +public class StateCommandsAttribute() : StateCommandsAttribute(typeof(TState)) where TState : State; static class AttributeCheck { - public static void EnsureCorrectAggregate(HttpCommandAttribute? attr) { + public static void EnsureCorrectParent(HttpCommandAttribute? attr) { if (attr != null && attr.GetType().IsGenericType) { - var aggregateType = attr.GetType().GetGenericArguments()[0]; + var stateType = attr.GetType().GetGenericArguments()[0]; - if (aggregateType != typeof(T)) { + if (stateType != typeof(T)) { throw new InvalidOperationException( - $"Command {typeof(TCommand).Name} is mapped to aggregate {aggregateType.Name} but the route builder is for aggregate {typeof(T).Name}" + $"Command {typeof(TCommand).Name} is mapped to state {stateType.Name} but the route builder is for state {typeof(T).Name}" ); } } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMapping.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMapping.cs index 7ff53aae..44575670 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMapping.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMapping.cs @@ -1,7 +1,6 @@ // Copyright (C) Ubiquitous AS. All rights reserved // Licensed under the Apache License, Version 2.0. -using System.Collections.Concurrent; using System.Reflection; using Eventuous.AspNetCore.Web; using Eventuous.AspNetCore.Web.Diagnostics; @@ -25,19 +24,17 @@ public static partial class RouteBuilderExtensions { /// Endpoint route builder instance /// A function to populate command props from HttpContext /// Command type - /// Aggregate type on which the command will operate - /// Result type that will be returned + /// State type on which the command will operate /// - public static RouteHandlerBuilder MapCommand( + public static RouteHandlerBuilder MapCommand( this IEndpointRouteBuilder builder, EnrichCommandFromHttpContext? enrichCommand = null ) - where TAggregate : Aggregate - where TCommand : class - where TResult : Result { + where TState : State, new() + where TCommand : class { var attr = typeof(TCommand).GetAttribute(); - return builder.MapCommand(attr?.Route, enrichCommand, attr?.PolicyName); + return builder.MapCommand(attr?.Route, enrichCommand, attr?.PolicyName); } /// @@ -48,65 +45,46 @@ public static RouteHandlerBuilder MapCommand( /// A function to populate command props from HttpContext /// Authorization policy /// Command type - /// Aggregate type on which the command will operate - /// Result type that will be returned + /// State type on which the command will operate /// - public static RouteHandlerBuilder MapCommand( + public static RouteHandlerBuilder MapCommand( this IEndpointRouteBuilder builder, string? route, EnrichCommandFromHttpContext? enrichCommand = null, string? policyName = null ) - where TAggregate : Aggregate + where TState : State, new() where TCommand : class - where TResult : Result - => Map( + => MapInternal( builder, route, - enrichCommand != null - ? (command, context) => enrichCommand(command, context) - : (command, _) => command, + enrichCommand != null ? (command, context) => enrichCommand(command, context) : (command, _) => command, policyName ); /// - /// Creates an instance of for a given aggregate type, so you + /// Creates an instance of for a given aggregate type, so you /// can explicitly map commands to HTTP endpoints. /// /// Endpoint route builder instance - /// Aggregate type + /// State type /// - public static CommandServiceRouteBuilder MapAggregateCommands(this IEndpointRouteBuilder builder) - where TAggregate : Aggregate => new(builder); - - /// - /// Creates an instance of for a given aggregate type, so you - /// can explicitly map commands to HTTP endpoints. - /// - /// Endpoint route builder instance - /// Aggregate type - /// Result type that will be returned - /// - public static CommandServiceRouteBuilder MapAggregateCommands(this IEndpointRouteBuilder builder) - where TAggregate : Aggregate - where TResult : Result - => new(builder); + public static CommandServiceRouteBuilder MapCommands(this IEndpointRouteBuilder builder) + where TState : State, new() => new(builder); /// /// Maps all commands annotated by to HTTP endpoints to be handled - /// by where T is the aggregate type provided. Only use it if your - /// application only handles commands for one aggregate type. + /// by where TState is the state type provided. + /// Only use it if your application only handles commands for one state type. /// /// Endpoint route builder instance /// List of assemblies to scan - /// Aggregate type + /// State type /// /// - public static IEndpointRouteBuilder MapDiscoveredCommands(this IEndpointRouteBuilder builder, params Assembly[] assemblies) - where TAggregate : Aggregate { - var assembliesToScan = assemblies.Length == 0 - ? AppDomain.CurrentDomain.GetAssemblies() - : assemblies; + public static IEndpointRouteBuilder MapDiscoveredCommands(this IEndpointRouteBuilder builder, params Assembly[] assemblies) + where TState : State { + var assembliesToScan = assemblies.Length == 0 ? AppDomain.CurrentDomain.GetAssemblies() : assemblies; var attributeType = typeof(HttpCommandAttribute); @@ -121,70 +99,20 @@ void MapAssemblyCommands(Assembly assembly) { x => x.IsClass && x.CustomAttributes.Any(a => a.AttributeType == attributeType) ); - var method = typeof(RouteBuilderExtensions).GetMethod(nameof(Map), BindingFlags.Static | BindingFlags.NonPublic)!; - foreach (var type in decoratedTypes) { var attr = type.GetAttribute()!; - if (attr.AggregateType != null && attr.AggregateType != typeof(TAggregate)) - throw new InvalidOperationException( - $"Command aggregate is {attr.AggregateType.Name} but expected to be {typeof(TAggregate).Name}" - ); + if (attr.StateType != null && attr.StateType != typeof(TState)) { + throw new InvalidOperationException($"Command state is {attr.StateType.Name} but expected to be {typeof(TState).Name}"); + } - var genericMethod = method.MakeGenericMethod(typeof(TAggregate), type, type, attr.ResultType); - genericMethod.Invoke(null, [builder, attr.Route, null, attr.PolicyName]); + builder.LocalMap(typeof(TState), type, attr.Route, attr.PolicyName); } } } - static RouteHandlerBuilder Map( - IEndpointRouteBuilder builder, - string? route, - ConvertAndEnrichCommand? convert = null, - string? policyName = null - ) - where TAggregate : Aggregate - where TCommand : class - where TContract : class - where TResult : Result { - if (convert == null && typeof(TCommand) != typeof(TContract)) - throw new InvalidOperationException($"Command type {typeof(TCommand).Name} is not assignable from {typeof(TContract).Name}"); - - var resolvedRoute = GetRoute(route); - ExtensionsEventSource.Log.HttpEndpointRegistered(resolvedRoute); - - var routeBuilder = builder - .MapPost( - resolvedRoute, - async Task (HttpContext context, ICommandService service) => { - var cmd = await context.Request.ReadFromJsonAsync(context.RequestAborted); - - if (cmd == null) throw new InvalidOperationException("Failed to deserialize the command"); - - var command = convert != null - ? convert(cmd, context) - : (cmd as TCommand)!; - - var result = await InvokeService(service, command, context.RequestAborted); - - return result.AsResult(); - } - ) - .Accepts() - .ProducesOk() - .ProducesProblemDetails(Status404NotFound) - .ProducesProblemDetails(Status409Conflict) - .ProducesProblemDetails(Status500InternalServerError) - .ProducesValidationProblemDetails(Status400BadRequest); - - routeBuilder.AddPolicy(policyName); - routeBuilder.AddAuthorization(typeof(TContract)); - - return routeBuilder; - } - /// - /// Maps commands that are annotated either with and/or + /// Maps commands that are annotated either with and/or /// in given assemblies. Will use assemblies of the current /// application domain if no assembly is specified explicitly. /// @@ -194,9 +122,7 @@ async Task (HttpContext context, ICommandService service) = /// [PublicAPI] public static IEndpointRouteBuilder MapDiscoveredCommands(this IEndpointRouteBuilder builder, params Assembly[] assemblies) { - var assembliesToScan = assemblies.Length == 0 - ? AppDomain.CurrentDomain.GetAssemblies() - : assemblies; + var assembliesToScan = assemblies.Length == 0 ? AppDomain.CurrentDomain.GetAssemblies() : assemblies; var attributeType = typeof(HttpCommandAttribute); @@ -213,85 +139,84 @@ void MapAssemblyCommands(Assembly assembly) { foreach (var type in decoratedTypes) { var attr = type.GetAttribute()!; - var parentAttribute = type.DeclaringType?.GetAttribute(); + var parentAttribute = type.DeclaringType?.GetAttribute(); - if (parentAttribute == null) continue; + var stateType = parentAttribute?.StateType ?? attr.StateType; - LocalMap(parentAttribute.AggregateType, type, attr.Route, attr.PolicyName, parentAttribute.ResultType ?? attr.ResultType); - } - } + if (stateType == null) continue; - void LocalMap(Type aggregateType, Type type, string? route, string? policyName, Type resultType) { - var appServiceBase = typeof(ICommandService<>); - var appServiceType = appServiceBase.MakeGenericType(aggregateType); + if (parentAttribute != null && stateType != parentAttribute.StateType) { + throw new InvalidOperationException( + $"Command state type {stateType.Name} doesn't match with parent state type {parentAttribute.StateType.Name}" + ); + } - var routeBuilder = builder - .MapPost( - GetRoute(type, route), - async Task (HttpContext context) => { - var cmd = await context.Request.ReadFromJsonAsync(type, context.RequestAborted); + builder.LocalMap(stateType, type, attr.Route, attr.PolicyName); + } + } + } - if (cmd == null) throw new InvalidOperationException("Failed to deserialize the command"); + static void LocalMap(this IEndpointRouteBuilder builder, Type stateType, Type type, string? route, string? policyName) { + var genericMethod = MapMethod.MakeGenericMethod(stateType, type, type); + genericMethod.Invoke(null, [builder, route, null, policyName]); + } - if (context.RequestServices.GetRequiredService(appServiceType) is not ICommandService service) - throw new InvalidOperationException("Unable to resolve the application service"); + static readonly MethodInfo MapMethod = typeof(RouteBuilderExtensions).GetMethod(nameof(MapInternal), BindingFlags.Static | BindingFlags.NonPublic)!; - var result = await InvokeService(service, cmd, context.RequestAborted); + static RouteHandlerBuilder MapInternal( + IEndpointRouteBuilder builder, + string? route, + ConvertAndEnrichCommand? convert = null, + string? policyName = null + ) + where TState : State, new() + where TCommand : class + where TContract : class { + if (convert == null && typeof(TCommand) != typeof(TContract)) + throw new InvalidOperationException($"Command type {typeof(TCommand).Name} is not assignable from {typeof(TContract).Name}"); - return result.AsResult(); - } - ) - .Accepts(type) - .ProducesOk(resultType) - .ProducesProblemDetails(Status404NotFound) - .ProducesProblemDetails(Status409Conflict) - .ProducesValidationProblemDetails(Status400BadRequest) - .ProducesProblemDetails(Status500InternalServerError); + var resolvedRoute = GetRoute(route); + ExtensionsEventSource.Log.HttpEndpointRegistered(resolvedRoute); - routeBuilder.AddPolicy(policyName); - routeBuilder.AddAuthorization(type); - } - } + var routeBuilder = builder + .MapPost( + resolvedRoute, + async Task (HttpContext context, ICommandService service) => { + var cmd = await context.Request.ReadFromJsonAsync(context.RequestAborted); - static string GetRoute(string? route) - => GetRoute(typeof(TCommand), route); + if (cmd == null) throw new InvalidOperationException("Failed to deserialize the command"); - static string GetRoute(MemberInfo type, string? route) { - return route ?? Generate(); + var command = convert != null ? convert(cmd, context) : (cmd as TCommand)!; - string Generate() { - var gen = type.Name; + var result = await service.Handle(command, context.RequestAborted); - return char.ToLowerInvariant(gen[0]) + gen[1..]; - } - } + return result.AsResult(); + } + ) + .Accepts() + .ProducesOk() + .ProducesProblemDetails(Status404NotFound) + .ProducesProblemDetails(Status409Conflict) + .ProducesProblemDetails(Status500InternalServerError) + .ProducesValidationProblemDetails(Status400BadRequest); - static void AddAuthorization(this RouteHandlerBuilder builder, Type contractType) { - var authAttr = contractType.GetAttribute(); - if (authAttr != null) builder.RequireAuthorization(authAttr); - } + // Add policy + if (policyName != null) routeBuilder.RequireAuthorization(policyName.Split(',')); - static void AddPolicy(this RouteHandlerBuilder builder, string? policyName) { - if (policyName != null) builder.RequireAuthorization(policyName.Split(',')); - } + // Add authorization + var authAttr = typeof(TContract).GetAttribute(); + if (authAttr != null) routeBuilder.RequireAuthorization(authAttr); - static readonly ConcurrentDictionary MethodCache = new(); + return routeBuilder; - static Task InvokeService(ICommandService service, object cmd, CancellationToken cancellationToken) { - var type = cmd.GetType(); + static string GetRoute(string? route) { + return route ?? Generate(); - var handleDelegate = MethodCache.GetOrAdd( - type, - t => { - var method = service.GetType().GetMethod(nameof(ICommandService.Handle)); - var genericMethod = method!.MakeGenericMethod(t); - var typeArgs = new[] { t, typeof(CancellationToken), typeof(Task) }; - var funcType = typeof(Func<,,>).MakeGenericType(typeArgs); + string Generate() { + var gen = typeof(TCommand).Name; - return Delegate.CreateDelegate(funcType, service, genericMethod); + return char.ToLowerInvariant(gen[0]) + gen[1..]; } - ); - - return ((Task)handleDelegate.DynamicInvoke(cmd, cancellationToken)!); + } } } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMappingExt.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMappingExt.cs index ddcc7c79..8dc3cde7 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMappingExt.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/HttpCommandMappingExt.cs @@ -20,56 +20,17 @@ public static partial class RouteBuilderExtensions { /// Function to convert HTTP command to domain command /// HTTP command type /// Domain command type - /// Aggregate type + /// State type /// - public static RouteHandlerBuilder MapCommand( + public static RouteHandlerBuilder MapCommand( this IEndpointRouteBuilder builder, ConvertAndEnrichCommand convert - ) where TAggregate : Aggregate where TCommand : class where TContract : class { + ) where TState : State, new() where TCommand : class where TContract : class { var attr = typeof(TContract).GetAttribute(); - return Map(builder, attr?.Route, convert, attr?.PolicyName); + return MapInternal(builder, attr?.Route, convert, attr?.PolicyName); } - /// - /// Map command to HTTP POST endpoint. - /// The HTTP command type should be annotated with attribute. - /// - /// Endpoint route builder instance - /// Function to convert HTTP command to domain command - /// HTTP command type - /// Domain command type - /// Aggregate type - /// Result type that will be returned - /// - public static RouteHandlerBuilder MapCommand( - this IEndpointRouteBuilder builder, - ConvertAndEnrichCommand convert - ) where TAggregate : Aggregate where TCommand : class where TContract : class where TResult : Result { - var attr = typeof(TContract).GetAttribute(); - - return Map(builder, attr?.Route, convert, attr?.PolicyName); - } - - /// - /// Map command to HTTP POST endpoint - /// - /// Endpoint route builder instance - /// API route for the POST endpoint - /// Function to convert HTTP command to domain command - /// Optional authorization policy name - /// HTTP command type - /// Domain command type - /// Aggregate type - /// - public static RouteHandlerBuilder MapCommand( - this IEndpointRouteBuilder builder, - string? route, - ConvertAndEnrichCommand convert, - string? policyName = null - ) where TAggregate : Aggregate where TCommand : class where TContract : class - => Map(builder, route, convert, policyName); - /// /// Map command to HTTP POST endpoint /// @@ -79,14 +40,13 @@ public static RouteHandlerBuilder MapCommand( /// Optional authorization policy name /// HTTP command type /// Domain command type - /// Aggregate type - /// Result type that will be returned + /// State type /// - public static RouteHandlerBuilder MapCommand( + public static RouteHandlerBuilder MapCommand( this IEndpointRouteBuilder builder, string? route, ConvertAndEnrichCommand convert, string? policyName = null - ) where TAggregate : Aggregate where TCommand : class where TContract : class where TResult : Result - => Map(builder, route, convert, policyName); + ) where TState : State, new() where TCommand : class where TContract : class + => MapInternal(builder, route, convert, policyName); } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/ResultExtensions.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/ResultExtensions.cs index d470b625..cd51143c 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/ResultExtensions.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/ResultExtensions.cs @@ -6,9 +6,9 @@ namespace Eventuous.AspNetCore.Web; -public static class ResultExtensions { - public static IResult AsResult(this Result result) { - return result is ErrorResult error +static class ResultExtensions { + public static IResult AsResult(this Result result) where TState : State, new() { + return result is ErrorResult error ? error.Exception switch { OptimisticConcurrencyException => AsProblem(Status409Conflict), AggregateNotFoundException => AsProblem(Status404NotFound), @@ -22,8 +22,8 @@ public static IResult AsResult(this Result result) { IResult AsValidationProblem(int statusCode) => Results.Problem(PopulateDetails(new ValidationProblemDetails(error.AsErrors()), error, statusCode)); } - public static ActionResult AsActionResult(this Result result) { - return result is ErrorResult error + public static ActionResult AsActionResult(this Result result) where TState : State, new() { + return result is ErrorResult error ? error.Exception switch { OptimisticConcurrencyException => AsProblem(Status409Conflict), AggregateNotFoundException => AsProblem(Status404NotFound), @@ -49,7 +49,7 @@ ActionResult CreateResult(T details, int statusCode) where T : ProblemDetails } } - static T PopulateDetails(T details, ErrorResult error, int statusCode) where T : ProblemDetails { + static T PopulateDetails(T details, ErrorResult error, int statusCode) where T : ProblemDetails where TState : State, new() { details.Status = statusCode; details.Title = error.ErrorMessage; details.Detail = error.Exception?.ToString(); @@ -58,5 +58,5 @@ static T PopulateDetails(T details, ErrorResult error, int statusCode) where return details; } - static Dictionary AsErrors(this ErrorResult error) => new Dictionary { ["Domain"] = [error.ErrorMessage] }; + static Dictionary AsErrors(this ErrorResult error) where TState : State, new() => new() { ["Domain"] = [error.ErrorMessage!] }; } diff --git a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/RouteHandlerBuilderExt.cs b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/RouteHandlerBuilderExt.cs index 6525709a..58cdf011 100644 --- a/src/Extensions/src/Eventuous.AspNetCore.Web/Http/RouteHandlerBuilderExt.cs +++ b/src/Extensions/src/Eventuous.AspNetCore.Web/Http/RouteHandlerBuilderExt.cs @@ -16,12 +16,9 @@ public static RouteHandlerBuilder ProducesProblemDetails(this RouteHandlerBuilde public static RouteHandlerBuilder ProducesOk(this RouteHandlerBuilder builder, Type resultType) => builder.Produces(StatusCodes.Status200OK, resultType, ContentTypes.Json); - public static RouteHandlerBuilder ProducesOk(this RouteHandlerBuilder builder) where T : Result - => builder.ProducesOk(typeof(T)); + public static RouteHandlerBuilder ProducesOk(this RouteHandlerBuilder builder) where TState : new() => builder.ProducesOk(typeof(Result)); - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, Type commandType) - => builder.Accepts(commandType, false, ContentTypes.Json); + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, Type commandType) => builder.Accepts(commandType, false, ContentTypes.Json); - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder) - => builder.Accepts(typeof(T)); + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder) => builder.Accepts(typeof(T)); } diff --git a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/AggregateFactory.cs b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/AggregateFactory.cs index 67ab477d..8fe9c951 100644 --- a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/AggregateFactory.cs +++ b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/AggregateFactory.cs @@ -15,8 +15,10 @@ public static class AggregateFactoryContainerExtensions { /// /// Aggregate factory function, which can get dependencies from the container. /// Aggregate type + /// Aggregate state type /// - public static IServiceCollection AddAggregate(this IServiceCollection services, Func createInstance) where T : Aggregate { + public static IServiceCollection AddAggregate(this IServiceCollection services, Func createInstance) + where T : Aggregate where TState : State, new() { services.TryAddSingleton(); services.AddSingleton(new ResolveAggregateFactory(typeof(T), createInstance)); @@ -30,8 +32,9 @@ public static IServiceCollection AddAggregate(this IServiceCollection service /// /// /// Aggregate type + /// Aggregate state type /// - public static IServiceCollection AddAggregate(this IServiceCollection services) where T : Aggregate { + public static IServiceCollection AddAggregate(this IServiceCollection services) where T : Aggregate where TState : State, new() { services.TryAddSingleton(); services.AddTransient(); // ReSharper disable once ConvertToLocalFunction @@ -41,4 +44,4 @@ public static IServiceCollection AddAggregate(this IServiceCollection service } } -public record ResolveAggregateFactory(Type Type, Func CreateInstance); +record ResolveAggregateFactory(Type Type, Func CreateInstance); diff --git a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Services.cs b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Services.cs index 17f8574b..dd4930d7 100644 --- a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Services.cs +++ b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Services.cs @@ -8,84 +8,6 @@ namespace Microsoft.Extensions.DependencyInjection; public static partial class ServiceCollectionExtensions { - /// - /// Registers the application service in the container - /// - /// - /// - /// - /// - public static IServiceCollection AddCommandService(this IServiceCollection services) - where T : class, ICommandService where TAggregate : Aggregate { - services.TryAddSingleton(); - services.AddSingleton(); - - if (EventuousDiagnostics.Enabled) { - services.AddSingleton(sp => TracedCommandService.Trace(sp.GetRequiredService())); - } - else { - services.AddSingleton>(sp => sp.GetRequiredService()); - } - - return services; - } - - /// - /// Registers the application service in the container - /// - /// - /// Set to true if you want the app service to throw instead of returning the error result - /// Application service implementation type - /// Aggregate state type - /// Aggregate identity type - /// Aggregate type - /// - public static IServiceCollection AddCommandService(this IServiceCollection services, bool throwOnError = false) - where T : class, ICommandService - where TState : State, new() - where TId : Id - where TAggregate : Aggregate { - services.TryAddSingleton(); - services.AddSingleton(); - services.AddSingleton(sp => GetThrowingService(GetTracedService(sp))); - - return services; - - ICommandService GetThrowingService(ICommandService inner) - => throwOnError - ? new ThrowingCommandService(inner) - : inner; - - ICommandService GetTracedService(IServiceProvider serviceProvider) - => EventuousDiagnostics.Enabled - ? TracedCommandService.Trace(serviceProvider.GetRequiredService()) - : serviceProvider.GetRequiredService(); - } - - /// - /// Registers the application service in the container - /// - /// - /// Function to create an app service instance - /// - /// - /// - public static IServiceCollection AddCommandService(this IServiceCollection services, Func getService) - where T : class, ICommandService - where TAggregate : Aggregate { - services.TryAddSingleton(); - services.AddSingleton(getService); - - if (EventuousDiagnostics.Enabled) { - services.AddSingleton(sp => TracedCommandService.Trace(sp.GetRequiredService())); - } - else { - services.AddSingleton>(sp => sp.GetRequiredService()); - } - - return services; - } - /// /// Registers the application service in the container /// @@ -93,8 +15,8 @@ public static IServiceCollection AddCommandService(this IServiceC /// /// /// - public static IServiceCollection AddFunctionalService(this IServiceCollection services) - where T : class, IFuncCommandService + public static IServiceCollection AddCommandService(this IServiceCollection services) + where T : class, ICommandService where TState : State, new() { services.AddSingleton(); @@ -102,7 +24,7 @@ public static IServiceCollection AddFunctionalService(this IServiceCo services.AddSingleton(sp => TracedFunctionalService.Trace(sp.GetRequiredService())); } else { - services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); } return services; @@ -116,15 +38,15 @@ public static IServiceCollection AddFunctionalService(this IServiceCo /// /// /// - public static IServiceCollection AddFunctionalService(this IServiceCollection services, Func getService) - where T : class, IFuncCommandService where TState : State, new() { + public static IServiceCollection AddCommandService(this IServiceCollection services, Func getService) + where T : class, ICommandService where TState : State, new() { services.AddSingleton(getService); if (EventuousDiagnostics.Enabled) { services.AddSingleton(sp => TracedFunctionalService.Trace(sp.GetRequiredService())); } else { - services.AddSingleton>(sp => sp.GetRequiredService()); + services.AddSingleton>(sp => sp.GetRequiredService()); } return services; diff --git a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingApi.cs b/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingApi.cs index 2397beae..2a5fdfd9 100644 --- a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingApi.cs +++ b/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingApi.cs @@ -8,15 +8,10 @@ namespace Eventuous.Sut.AspNetCore; -public class BookingApi(ICommandService service, MessageMap? commandMap = null) : CommandHttpApiBase(service, commandMap) { +public class BookingApi(ICommandService service, MessageMap? commandMap = null) : CommandHttpApiBase(service, commandMap) { [HttpPost("v2/pay")] - public Task> RegisterPayment([FromBody] RegisterPaymentHttp cmd, CancellationToken cancellationToken) + public Task>> RegisterPayment([FromBody] RegisterPaymentHttp cmd, CancellationToken cancellationToken) => Handle(cmd, cancellationToken); - public record RegisterPaymentHttp( - string BookingId, - string PaymentId, - float Amount, - DateTimeOffset PaidAt - ); + public record RegisterPaymentHttp(string BookingId, string PaymentId, float Amount, DateTimeOffset PaidAt); } diff --git a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingResult.cs b/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingResult.cs deleted file mode 100644 index 23293375..00000000 --- a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (C) Ubiquitous AS. All rights reserved -// Licensed under the Apache License, Version 2.0. - -using Eventuous.Sut.Domain; - -namespace Eventuous.Sut.AspNetCore; - -public record BookingResult : Result { - public new BookingState? State { get; init; } -} \ No newline at end of file diff --git a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingService.cs b/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingService.cs index b4b768bc..adadb8c0 100644 --- a/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingService.cs +++ b/src/Extensions/test/Eventuous.Sut.AspNetCore/BookingService.cs @@ -66,7 +66,7 @@ public record BookRoom( public record ImportBooking(BookingId BookingId, string RoomId, StayPeriod Period, Money Price); - [AggregateCommands] + [StateCommands] public static class NestedCommands { [HttpCommand(Route = NestedBookRoute)] public record NestedBookRoom( diff --git a/src/Extensions/test/Eventuous.Sut.AspNetCore/Program.cs b/src/Extensions/test/Eventuous.Sut.AspNetCore/Program.cs index 70bbf80a..522ec30d 100644 --- a/src/Extensions/test/Eventuous.Sut.AspNetCore/Program.cs +++ b/src/Extensions/test/Eventuous.Sut.AspNetCore/Program.cs @@ -14,7 +14,7 @@ ); var builder = WebApplication.CreateBuilder(args); -builder.Services.AddCommandService(); +builder.Services.AddCommandService(); builder.Services.AddAggregateStore(); builder.Services.Configure(options => options.SerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)); diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitly.verified.txt b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitly.verified.txt new file mode 100644 index 00000000..998b1f8c --- /dev/null +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitly.verified.txt @@ -0,0 +1,28 @@ +{ + streamPosition: 0, + state: { + price: { + amount: 100, + currency: EUR + }, + amountPaid: { + amount: 0, + currency: EUR + }, + id: { + value: Guid_1 + } + }, + success: true, + changes: [ + { + event: { + roomId: Guid_2, + price: 100, + checkIn: 2023-10-01, + checkOut: 2023-10-02 + }, + eventType: V1.BookingImported + } + ] +} \ No newline at end of file diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute.verified.txt b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute.verified.txt index c3dc7761..998b1f8c 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute.verified.txt +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRoute.verified.txt @@ -1,4 +1,5 @@ { + streamPosition: 0, state: { price: { amount: 100, diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr.verified.txt b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr.verified.txt index c3dc7761..998b1f8c 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr.verified.txt +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapAggregateContractToCommandExplicitlyWithoutRouteWithGenericAttr.verified.txt @@ -1,4 +1,5 @@ { + streamPosition: 0, state: { price: { amount: 100, diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapEnrichedCommand.verified.txt b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapEnrichedCommand.verified.txt index d4bf76fb..92189343 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapEnrichedCommand.verified.txt +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.MapEnrichedCommand.verified.txt @@ -1,4 +1,5 @@ { + streamPosition: 0, state: { price: { amount: 100, diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.cs index eba72afa..6f927f6b 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/AggregateCommandsTests.cs @@ -16,7 +16,7 @@ public void RegisterAggregateCommands() { using var app = builder.Build(); - var b = app.MapDiscoveredCommands(typeof(BookRoom).Assembly); + var b = app.MapDiscoveredCommands(typeof(BookRoom).Assembly); b.DataSources.First().Endpoints[0].DisplayName.Should().Be("HTTP: POST book"); } @@ -39,48 +39,26 @@ public void MapAggregateContractToCommandExplicitlyWithoutRouteWithWrongGenericA _output, _ => { }, app => app - .MapAggregateCommands() + .MapCommands() .MapCommand(Enricher.EnrichCommand) ); act.Should().Throw(); } - public static IEnumerable ResultTypesToTest() { - yield return [new BookingResult()]; - yield return [new Result()]; - } - [Theory] - [MemberData(nameof(ResultTypesToTest))] - public async Task MapContractToCommandExplicitly(TResult tResult) - where TResult : Result, new() { - var fixture = new ServerFixture( - factory, - _output, - _ => { }, - app => app.MapCommand(ImportRoute, Enricher.EnrichCommand) - ); - - var resultTypeName = tResult.GetType().Name; - await Execute(fixture, ImportRoute, resultTypeName); - } - - [Theory] - [MemberData(nameof(ResultTypesToTest))] - public async Task MapAggregateContractToCommandExplicitly(TResult tResult) - where TResult : Result, new() { + [Fact] + public async Task MapAggregateContractToCommandExplicitly() { var fixture = new ServerFixture( factory, _output, _ => { }, app => app - .MapAggregateCommands() + .MapCommands() .MapCommand(ImportRoute, Enricher.EnrichCommand) ); - var resultTypeName = tResult.GetType().Name; - await Execute(fixture, ImportRoute, resultTypeName); + await Execute(fixture, ImportRoute); } [Fact] @@ -90,7 +68,7 @@ public async Task MapAggregateContractToCommandExplicitlyWithoutRoute() { _output, _ => { }, app => app - .MapAggregateCommands() + .MapCommands() .MapCommand(Enricher.EnrichCommand) ); @@ -104,7 +82,7 @@ public async Task MapAggregateContractToCommandExplicitlyWithoutRouteWithGeneric _output, _ => { }, app => app - .MapAggregateCommands() + .MapCommands() .MapCommand(Enricher.EnrichCommand) ); @@ -118,15 +96,15 @@ public async Task MapEnrichedCommand() { _output, _ => { }, app => app - .MapAggregateCommands() + .MapCommands() .MapCommand((x, _) => x with { GuestId = TestData.GuestId }) ); var cmd = fixture.GetBookRoom(); - var content = await fixture.ExecuteRequest(cmd, "book", cmd.BookingId); + var content = await fixture.ExecuteRequest(cmd, "book", cmd.BookingId); await VerifyJson(content); } - static async Task Execute(ServerFixture fixture, string route, string typeName = "") { + static async Task Execute(ServerFixture fixture, string route) { var bookRoom = fixture.GetBookRoom(); var import = new ImportBookingHttp( @@ -136,8 +114,8 @@ static async Task Execute(ServerFixture fixture, string route, string typeName = bookRoom.CheckOut, bookRoom.Price ); - var content = await fixture.ExecuteRequest(import, route, bookRoom.BookingId); + var content = await fixture.ExecuteRequest(import, route, bookRoom.BookingId); - await VerifyJson(content).UseParameters(typeName); + await VerifyJson(content); } } diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/ControllerTests.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/ControllerTests.cs index 224987a6..b3918d46 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/ControllerTests.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/ControllerTests.cs @@ -14,10 +14,10 @@ public class ControllerTests : IDisposable, IClassFixture factory, ITestOutputHelper output) { var commandMap = new MessageMap() .Add( - x => new Commands.RecordPayment(new BookingId(x.BookingId), x.PaymentId, new Money(x.Amount), x.PaidAt) + x => new(new(x.BookingId), x.PaymentId, new Money(x.Amount), x.PaidAt) ); - _fixture = new ServerFixture( + _fixture = new( factory, output, services => { @@ -28,12 +28,12 @@ public ControllerTests(WebApplicationFactory factory, ITestOutputHelper app.MapControllers(); app - .MapAggregateCommands() + .MapCommands() .MapCommand(); } ); - _listener = new TestEventListener(output); + _listener = new(output); } [Fact] diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.CallDiscoveredCommandRoute.verified.txt b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.CallDiscoveredCommandRoute.verified.txt index 1a81173e..5214a0c3 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.CallDiscoveredCommandRoute.verified.txt +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.CallDiscoveredCommandRoute.verified.txt @@ -1,4 +1,5 @@ { + streamPosition: 0, state: { price: { amount: 100, diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.cs index 3df4c9ca..6653d539 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/DiscoveredCommandsTests.cs @@ -19,7 +19,7 @@ public async Task CallDiscoveredCommandRoute() { ); var cmd = fixture.GetNestedBookRoom(new DateTime(2023, 10, 1)); - var streamEvents = await fixture.ExecuteRequest(cmd, NestedBookRoute, cmd.BookingId); + var streamEvents = await fixture.ExecuteRequest(cmd, NestedBookRoute, cmd.BookingId); await VerifyJson(streamEvents); } } diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/Commands.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/Commands.cs index a11cb865..703f2f39 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/Commands.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/Commands.cs @@ -12,11 +12,11 @@ public record ImportBookingHttp(string BookingId, string RoomId, LocalDate Check public record ImportBookingHttp1(string BookingId, string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price) : ImportBookingHttp(BookingId, RoomId, CheckIn, CheckOut, Price); - [HttpCommand(Route = Import2Route)] + [HttpCommand(Route = Import2Route)] public record ImportBookingHttp2(string BookingId, string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price) : ImportBookingHttp(BookingId, RoomId, CheckIn, CheckOut, Price); - [HttpCommand(Route = ImportWrongRoute)] + [HttpCommand(Route = ImportWrongRoute)] public record ImportBookingHttp3(string BookingId, string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price) : ImportBookingHttp(BookingId, RoomId, CheckIn, CheckOut, Price); } diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/ServerFixture.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/ServerFixture.cs index 235f5ac8..9d47baa7 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/ServerFixture.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/ServerFixture.cs @@ -11,9 +11,7 @@ namespace Eventuous.Tests.AspNetCore.Web.Fixture; using static SutBookingCommands; public class ServerFixture { - //: IDisposable { readonly AutoFixture.Fixture _fixture = new(); - readonly ITestOutputHelper _output; public ServerFixture( WebApplicationFactory factory, @@ -21,8 +19,6 @@ public ServerFixture( Action? register = null, ConfigureWebApplication? configure = null ) { - _output = output; - var builder = factory .WithWebHostBuilder( builder => { @@ -52,8 +48,7 @@ public RestClient GetClient() { ); } - public T Resolve() where T : notnull - => _app.Services.GetRequiredService(); + public T Resolve() where T : notnull => _app.Services.GetRequiredService(); public Task ReadStream(string id) => Resolve().ReadEvents(StreamName.For(id), StreamReadPosition.Start, 100, default); @@ -71,7 +66,7 @@ internal NestedCommands.NestedBookRoom GetNestedBookRoom(DateTime? dateTime = nu return new(_fixture.Create(), _fixture.Create(), date, date.PlusDays(1), 100, "guest"); } - public async Task ExecuteRequest(TCommand cmd, string route, string id) where TCommand : class { + public async Task ExecuteRequest(TCommand cmd, string route, string id) where TCommand : class { using var client = GetClient(); var request = new RestRequest(route).AddJsonBody(cmd); diff --git a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/TestAggregate.cs b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/TestAggregate.cs index 84d7d447..185e79ec 100644 --- a/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/TestAggregate.cs +++ b/src/Extensions/test/Eventuous.Tests.AspNetCore.Web/Fixture/TestAggregate.cs @@ -1,5 +1,5 @@ namespace Eventuous.Tests.AspNetCore.Web.Fixture; -class Brooking : Aggregate { - public override void Load(IEnumerable events) { } -} +class Brooking : Aggregate; + +record BrookingState : State; \ No newline at end of file diff --git a/src/Extensions/test/Eventuous.Tests.DependencyInjection/AggregateFactoryRegistrationTests.cs b/src/Extensions/test/Eventuous.Tests.DependencyInjection/AggregateFactoryRegistrationTests.cs index 6801fd52..6b90791d 100644 --- a/src/Extensions/test/Eventuous.Tests.DependencyInjection/AggregateFactoryRegistrationTests.cs +++ b/src/Extensions/test/Eventuous.Tests.DependencyInjection/AggregateFactoryRegistrationTests.cs @@ -17,7 +17,7 @@ public AggregateFactoryRegistrationTests() { [Fact] public void ShouldCreateNewAggregateWithExplicitFunction() { - var instance = _registry.CreateInstance(); + var instance = _registry.CreateInstance(); instance.Should().BeOfType(); instance.Dependency.Should().NotBeNull(); instance.State.Should().NotBeNull(); @@ -25,7 +25,7 @@ public void ShouldCreateNewAggregateWithExplicitFunction() { [Fact] public void ShouldCreateNewAggregateByResolve() { - var instance = _registry.CreateInstance(); + var instance = _registry.CreateInstance(); instance.Should().BeOfType(); instance.Dependency.Should().NotBeNull(); instance.State.Should().NotBeNull(); @@ -33,8 +33,8 @@ public void ShouldCreateNewAggregateByResolve() { [Fact] public void ShouldCreateTwoSeparateInstances() { - var instance1 = _registry.CreateInstance(); - var instance2 = _registry.CreateInstance(); + var instance1 = _registry.CreateInstance(); + var instance2 = _registry.CreateInstance(); instance1.Should().NotBeSameAs(instance2); } @@ -42,8 +42,8 @@ static WebApplicationBuilder BuildHost() { var builder = WebApplication.CreateBuilder(); builder.Services.AddAggregateStore(); builder.Services.AddSingleton(); - builder.Services.AddAggregate(sp => new TestAggregate(sp.GetRequiredService())); - builder.Services.AddAggregate(); + builder.Services.AddAggregate(sp => new TestAggregate(sp.GetRequiredService())); + builder.Services.AddAggregate(); return builder; } diff --git a/src/Mongo/src/Eventuous.Projections.MongoDB/MongoCheckpointStore.cs b/src/Mongo/src/Eventuous.Projections.MongoDB/MongoCheckpointStore.cs index 76ce9c23..127b6260 100644 --- a/src/Mongo/src/Eventuous.Projections.MongoDB/MongoCheckpointStore.cs +++ b/src/Mongo/src/Eventuous.Projections.MongoDB/MongoCheckpointStore.cs @@ -18,42 +18,7 @@ namespace Eventuous.Projections.MongoDB; /// Checkpoint store for MongoDB, which stores checkpoints in a collection. /// Use it when you create read models in MongoDB too. /// -public class MongoCheckpointStore : ICheckpointStore { - MongoCheckpointStore(IMongoDatabase database, MongoCheckpointStoreOptions options, ILoggerFactory loggerFactory) { - _loggerFactory = loggerFactory; - Checkpoints = Ensure.NotNull(database).GetCollection(options.CollectionName); - _getSubject = GetSubject; - - return; - - Subject GetSubject() { - var subject = new Subject(); - - var observable = options switch { - { BatchSize: > 0, BatchIntervalSec: > 0 } => subject.Buffer( - TimeSpan.FromSeconds(options.BatchIntervalSec), - options.BatchSize - ), - { BatchSize: > 0, BatchIntervalSec: 0 } => subject.Buffer(options.BatchSize), - { BatchSize: 0, BatchIntervalSec: > 0 } => subject.Buffer( - TimeSpan.FromSeconds(options.BatchIntervalSec) - ), - _ => subject.Select(x => new List { x }) - }; - - observable - .Where(x => x.Count > 0) - .Select(x => Observable.FromAsync(ct => StoreInternal(x.Last(), false, ct))) - .Concat() - .Subscribe(); - - return subject; - } - } - - readonly Func> _getSubject; - readonly ILoggerFactory _loggerFactory; - +public class MongoCheckpointStore(IMongoDatabase database, MongoCheckpointStoreOptions options, ILoggerFactory loggerFactory) : ICheckpointStore { [PublicAPI] public MongoCheckpointStore(IMongoDatabase database, ILoggerFactory loggerFactory) : this(database, new MongoCheckpointStoreOptions(), loggerFactory) { } @@ -62,7 +27,7 @@ public MongoCheckpointStore(IMongoDatabase database, ILoggerFactory loggerFactor public MongoCheckpointStore(IMongoDatabase database, IOptions options, ILoggerFactory loggerFactory) : this(database, options.Value, loggerFactory) { } - IMongoCollection Checkpoints { get; } + IMongoCollection Checkpoints { get; } = Ensure.NotNull(database).GetCollection(options.CollectionName); public async ValueTask GetLastCheckpoint(string checkpointId, CancellationToken cancellationToken = default) { var storedCheckpoint = await Checkpoints.AsQueryable() @@ -74,16 +39,36 @@ public async ValueTask GetLastCheckpoint(string checkpointI Logger.Current.CheckpointLoaded(this, checkpoint); - _subjects[checkpointId] = _getSubject(); + _subjects[checkpointId] = GetSubject(); return checkpoint; } readonly Dictionary> _subjects = new(); + Subject GetSubject() { + var subject = new Subject(); + + var observable = options switch { + { BatchSize: > 0, BatchIntervalSec: > 0 } => subject.Buffer(TimeSpan.FromSeconds(options.BatchIntervalSec), options.BatchSize), + { BatchSize: > 0, BatchIntervalSec: 0 } => subject.Buffer(options.BatchSize), + { BatchSize: 0, BatchIntervalSec: > 0 } => subject.Buffer(TimeSpan.FromSeconds(options.BatchIntervalSec)), + _ => subject.Select(x => new List { x }) + }; + + observable + .Where(x => x.Count > 0) + .Select(x => Observable.FromAsync(ct => StoreInternal(x.Last(), false, ct))) + .Concat() + .Subscribe(); + + return subject; + } + public async ValueTask StoreCheckpoint(EventuousCheckpoint checkpoint, bool force, CancellationToken cancellationToken = default) { if (force) { await StoreInternal(checkpoint, true, cancellationToken).NoContext(); + return checkpoint; } @@ -101,7 +86,7 @@ await Checkpoints.ReplaceOneAsync( ) .NoContext(); - Logger.ConfigureIfNull(checkpoint.Id, _loggerFactory); + Logger.ConfigureIfNull(checkpoint.Id, loggerFactory); Logger.Current.CheckpointStored(this, checkpoint, force); } @@ -122,15 +107,15 @@ public record MongoCheckpointStoreOptions { /// /// Collection for checkpoint documents (one per subscription). Default is "checkpoint". /// - public string CollectionName { get; init; } = "checkpoint"; + public string CollectionName { get; init; } = "checkpoint"; /// /// Commit batch size, default is 1. Increase it to improve performance. /// - public int BatchSize { get; init; } = 1; + public int BatchSize { get; init; } = 1; /// /// Commit batch interval in seconds, default is 5. Increase it to improve performance. /// - public int BatchIntervalSec { get; init; } = 5; + public int BatchIntervalSec { get; init; } = 5; } diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProduceOptions.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProduceOptions.cs index 14951946..dc19738e 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProduceOptions.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProduceOptions.cs @@ -1,9 +1,10 @@ // Copyright (C) Ubiquitous AS. All rights reserved // Licensed under the Apache License, Version 2.0. +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global namespace Eventuous.RabbitMq.Producers; -[PublicAPI] public class RabbitMqProduceOptions { public string? RoutingKey { get; init; } public string? AppId { get; init; } diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProducer.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProducer.cs index 57d049fc..e01d2d1b 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProducer.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Producers/RabbitMqProducer.cs @@ -42,7 +42,7 @@ public RabbitMqProducer( _options = options; _serializer = serializer ?? DefaultEventSerializer.Instance; _connectionFactory = Ensure.NotNull(connectionFactory); - _exchangeCache = new(_log); + _exchangeCache = new ExchangeCache(_log); } public Task StartAsync(CancellationToken cancellationToken = default) { diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Shared/RabbitMqExchangeOptions.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Shared/RabbitMqExchangeOptions.cs index 6e48efd1..adc9c928 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Shared/RabbitMqExchangeOptions.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Shared/RabbitMqExchangeOptions.cs @@ -3,7 +3,6 @@ namespace Eventuous.RabbitMq.Shared; -[PublicAPI] public class RabbitMqExchangeOptions { public string Type { get; init; } = ExchangeType.Fanout; public bool Durable { get; init; } = true; diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs index e73ddc18..3f0c1328 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscription.cs @@ -29,11 +29,11 @@ public class RabbitMqSubscription : EventSubscription /// public RabbitMqSubscription( - ConnectionFactory connectionFactory, - IOptions options, - ConsumePipe consumePipe, - ILoggerFactory? loggerFactory - ) : this(connectionFactory, options.Value, consumePipe, loggerFactory) { } + ConnectionFactory connectionFactory, + IOptions options, + ConsumePipe consumePipe, + ILoggerFactory? loggerFactory + ) : this(connectionFactory, options.Value, consumePipe, loggerFactory) { } /// /// Creates RabbitMQ subscription service instance @@ -43,11 +43,11 @@ public RabbitMqSubscription( /// /// public RabbitMqSubscription( - ConnectionFactory connectionFactory, - RabbitMqSubscriptionOptions options, - ConsumePipe consumePipe, - ILoggerFactory? loggerFactory - ) + ConnectionFactory connectionFactory, + RabbitMqSubscriptionOptions options, + ConsumePipe consumePipe, + ILoggerFactory? loggerFactory + ) : base( Ensure.NotNull(options), consumePipe.AddFilterFirst(new AsyncHandlingFilter(options.ConcurrencyLimit * 10)), @@ -74,16 +74,15 @@ public RabbitMqSubscription( /// /// Event serializer instance public RabbitMqSubscription( - ConnectionFactory connectionFactory, - string exchange, - string subscriptionId, - ConsumePipe consumePipe, - ILoggerFactory? loggerFactory, - IEventSerializer? eventSerializer = null - ) : this( + ConnectionFactory connectionFactory, + string exchange, + string subscriptionId, + ConsumePipe consumePipe, + ILoggerFactory? loggerFactory, + IEventSerializer? eventSerializer = null + ) : this( connectionFactory, - new RabbitMqSubscriptionOptions - { Exchange = exchange, SubscriptionId = subscriptionId, EventSerializer = eventSerializer }, + new RabbitMqSubscriptionOptions { Exchange = exchange, SubscriptionId = subscriptionId, EventSerializer = eventSerializer }, consumePipe, loggerFactory ) { } @@ -136,8 +135,7 @@ async Task HandleReceived(object sender, BasicDeliverEventArgs received) { try { var ctx = CreateContext(sender, received).WithItem(ReceivedMessageKey, received); await Handler(new AsyncConsumeContext(ctx, Ack, Nack)).NoContext(); - } - catch (Exception) { + } catch (Exception) { // This won't stop the subscription, but the reader will be gone. Not sure how to solve this one. if (Options.ThrowOnError) throw; } @@ -146,6 +144,7 @@ async Task HandleReceived(object sender, BasicDeliverEventArgs received) { ValueTask Ack(IMessageConsumeContext ctx) { var received = ctx.Items.GetItem(ReceivedMessageKey)!; _channel.BasicAck(received.DeliveryTag, false); + return default; } @@ -154,6 +153,7 @@ ValueTask Nack(IMessageConsumeContext ctx, Exception exception) { var received = ctx.Items.GetItem(ReceivedMessageKey)!; _failureHandler(_channel, received, exception); + return default; } diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscriptionOptions.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscriptionOptions.cs index e6e29883..1c8798b8 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscriptionOptions.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/RabbitMqSubscriptionOptions.cs @@ -44,7 +44,7 @@ public record RabbitMqSubscriptionOptions : SubscriptionOptions { public uint ConcurrencyLimit { get; set; } = 1; /// - /// Number of messages to prefetch.. + /// Number of messages to prefetch. /// public ushort PrefetchCount { get; set; } diff --git a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/Timestamp.cs b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/Timestamp.cs index bd4eb5c6..6c04ffc3 100644 --- a/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/Timestamp.cs +++ b/src/RabbitMq/src/Eventuous.RabbitMq/Subscriptions/Timestamp.cs @@ -11,6 +11,5 @@ internal static AmqpTimestamp ToAmqpTimestamp(this DateTime datetime) { return new AmqpTimestamp((long) unixTime); } - internal static DateTime ToDateTime(this AmqpTimestamp timestamp) - => Epoch.AddSeconds(timestamp.UnixTime).ToLocalTime(); + internal static DateTime ToDateTime(this AmqpTimestamp timestamp) => Epoch.AddSeconds(timestamp.UnixTime).ToLocalTime(); } \ No newline at end of file diff --git a/src/RabbitMq/test/Eventuous.Tests.RabbitMq/RabbitMqFixture.cs b/src/RabbitMq/test/Eventuous.Tests.RabbitMq/RabbitMqFixture.cs index adcb1b47..7525b122 100644 --- a/src/RabbitMq/test/Eventuous.Tests.RabbitMq/RabbitMqFixture.cs +++ b/src/RabbitMq/test/Eventuous.Tests.RabbitMq/RabbitMqFixture.cs @@ -10,14 +10,8 @@ public class RabbitMqFixture : IAsyncLifetime { public async Task InitializeAsync() { _rabbitMq = new RabbitMqBuilder().Build(); await _rabbitMq.StartAsync(); - - ConnectionFactory = new ConnectionFactory { - Uri = new Uri(_rabbitMq.GetConnectionString()), - DispatchConsumersAsync = true - }; + ConnectionFactory = new ConnectionFactory { Uri = new Uri(_rabbitMq.GetConnectionString()), DispatchConsumersAsync = true }; } - public async Task DisposeAsync() { - await _rabbitMq.DisposeAsync(); - } + public async Task DisposeAsync() => await _rabbitMq.DisposeAsync(); } diff --git a/src/RabbitMq/test/Eventuous.Tests.RabbitMq/SubscriptionSpec.cs b/src/RabbitMq/test/Eventuous.Tests.RabbitMq/SubscriptionSpec.cs index 72e16202..109ff98a 100644 --- a/src/RabbitMq/test/Eventuous.Tests.RabbitMq/SubscriptionSpec.cs +++ b/src/RabbitMq/test/Eventuous.Tests.RabbitMq/SubscriptionSpec.cs @@ -22,15 +22,10 @@ public class SubscriptionSpec : IAsyncLifetime, IClassFixture { readonly RabbitMqFixture _fixture; public SubscriptionSpec(RabbitMqFixture fixture, ITestOutputHelper outputHelper) { - _fixture = fixture; - _es = new TestEventListener(outputHelper); - _exchange = new StreamName(Auto.Create()); - - _loggerFactory = LoggerFactory.Create( - builder => builder - .SetMinimumLevel(LogLevel.Debug) - .AddXunit(outputHelper, LogLevel.Trace) - ); + _fixture = fixture; + _es = new TestEventListener(outputHelper); + _exchange = new StreamName(Auto.Create()); + _loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug).AddXunit(outputHelper, LogLevel.Trace)); _log = _loggerFactory.CreateLogger(); } diff --git a/src/Testing/src/Eventuous.Testing/AggregateFactoryExtensions.cs b/src/Testing/src/Eventuous.Testing/AggregateFactoryExtensions.cs index 6e8b77c1..3544b2f7 100644 --- a/src/Testing/src/Eventuous.Testing/AggregateFactoryExtensions.cs +++ b/src/Testing/src/Eventuous.Testing/AggregateFactoryExtensions.cs @@ -16,7 +16,7 @@ public static class AggregateFactoryExtensions { [Obsolete("This overload is for backwards compability. Use CreateTestAggregateInstance that uses Id in stead of AggregateId as TId parameter.")] public static TAggregate CreateTestAggregateInstanceForAggregateId(this AggregateFactoryRegistry registry, TId id) where TAggregate : Aggregate where TState : State, new() where TId : AggregateId - => registry.CreateInstance().WithId(id); + => registry.CreateInstance().WithId(id); /// /// Creates an instance of the aggregate and assigns the ID of the aggregate @@ -29,5 +29,5 @@ public static TAggregate CreateTestAggregateInstanceForAggregateId public static TAggregate CreateTestAggregateInstance(this AggregateFactoryRegistry registry, TId id) where TAggregate : Aggregate where TState : State, new() where TId : Id - => registry.CreateInstance().WithId(id); + => registry.CreateInstance().WithId(id); } diff --git a/src/Testing/src/Eventuous.Testing/AggregateSpec.cs b/src/Testing/src/Eventuous.Testing/AggregateSpec.cs index ddb8f38b..035edebf 100644 --- a/src/Testing/src/Eventuous.Testing/AggregateSpec.cs +++ b/src/Testing/src/Eventuous.Testing/AggregateSpec.cs @@ -1,16 +1,20 @@ // Copyright (C) Ubiquitous AS.All rights reserved // Licensed under the Apache License, Version 2.0. +using Shouldly; + namespace Eventuous.Testing; /// /// Base class for aggregate tests with a given aggregate type. /// Operates on a given set of events and allows checking multiple assertions on the resulting state and emitted events. /// -/// -/// -public abstract class AggregateSpec(AggregateFactoryRegistry? registry = null) where TAggregate : Aggregate { - readonly AggregateFactoryRegistry _registry = registry ?? AggregateFactoryRegistry.Instance; +/// Optional: aggregate factory registry. When not provided, the default one will be used. +/// Aggregate type +/// Aggregate state type +public abstract class AggregateSpec(AggregateFactoryRegistry? registry = null) + where TAggregate : Aggregate where TState : State, new() { + protected readonly AggregateFactoryRegistry Registry = registry ?? AggregateFactoryRegistry.Instance; /// /// Collection of events to load into the aggregate before executing the test @@ -23,16 +27,41 @@ public abstract class AggregateSpec(AggregateFactoryRegistry? regist /// /// protected abstract void When(TAggregate aggregate); + + /// + /// Function to create aggregate instances. + /// + /// Aggregate instance creating using the aggregate factory + protected virtual TAggregate CreateInstance() => Registry.CreateInstance(); /// /// Executes the operation on the aggregate provided by and returns the resulting aggregate instance /// /// + [MemberNotNull(nameof(Instance))] protected TAggregate Then() { - var instance = _registry.CreateInstance(); - instance.Load(GivenEvents()); - When(instance); + Instance = CreateInstance(); + Instance.Load(GivenEvents()); + When(Instance); + + return Instance; + } - return instance; + /// + /// Checks if one or more events were emitted after the operation. + /// + /// Events to verify + /// Aggregate instance for further inspection + // ReSharper disable once UnusedMethodReturnValue.Global + protected TAggregate Emitted(params object[] events) { + if (Instance == null) { + Then(); + } + + events.ShouldBeSubsetOf(Instance.Changes); + + return Instance; } + + protected TAggregate? Instance { get; private set; } } diff --git a/src/Testing/src/Eventuous.Testing/AggregateWithIdSpec.cs b/src/Testing/src/Eventuous.Testing/AggregateWithIdSpec.cs new file mode 100644 index 00000000..46f931f1 --- /dev/null +++ b/src/Testing/src/Eventuous.Testing/AggregateWithIdSpec.cs @@ -0,0 +1,24 @@ +// Copyright (C) Ubiquitous AS.All rights reserved +// Licensed under the Apache License, Version 2.0. + +namespace Eventuous.Testing; + +/// +/// Base class for aggregate tests with a given aggregate type, where aggregate state has the id. +/// Operates on a given set of events and allows checking multiple assertions on the resulting state and emitted events. +/// +/// Optional: aggregate factory registry. When not provided, the default one will be used. +/// Aggregate type +/// Aggregate state type +/// Aggregate identity type +public abstract class AggregateWithIdSpec(AggregateFactoryRegistry? registry = null) : AggregateSpec(registry) + where TAggregate : Aggregate where TState : State, new() where TId : Id { + /// + /// Aggregate identity value that will be set when creating a new instance for the test. + /// + protected abstract TId? Id { get; } + + /// + protected override TAggregate CreateInstance() + => Id == null ? base.CreateInstance() : Registry.CreateTestAggregateInstance(Id); +} diff --git a/src/Testing/src/Eventuous.Testing/Eventuous.Testing.csproj b/src/Testing/src/Eventuous.Testing/Eventuous.Testing.csproj index 3f1a5d4f..0de89398 100644 --- a/src/Testing/src/Eventuous.Testing/Eventuous.Testing.csproj +++ b/src/Testing/src/Eventuous.Testing/Eventuous.Testing.csproj @@ -2,4 +2,7 @@ + + + diff --git a/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs b/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs index 84df6e12..03dc3e58 100644 --- a/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs +++ b/src/Testing/src/Eventuous.Testing/InMemoryEventStore.cs @@ -35,23 +35,6 @@ public Task ReadEvents(StreamName stream, StreamReadPosition star public Task ReadEventsBackwards(StreamName stream, int count, CancellationToken cancellationToken) => Task.FromResult(FindStream(stream).GetEventsBackwards(count).ToArray()); - public Task ReadStream( - StreamName stream, - StreamReadPosition start, - int count, - Action callback, - CancellationToken cancellationToken - ) { - var readCount = 0L; - - foreach (var streamEvent in FindStream(stream).GetEvents(start, count)) { - callback(streamEvent); - readCount++; - } - - return Task.FromResult(readCount); - } - /// public Task TruncateStream( StreamName stream, @@ -82,7 +65,6 @@ record StoredEvent(StreamEvent Event, int Position); class InMemoryStream(StreamName name) { public int Version { get; private set; } = -1; - StreamName _name = name; readonly List _events = []; public void CheckVersion(ExpectedStreamVersion expectedVersion) { diff --git a/test/Eventuous.Sut.App/BookingService.cs b/test/Eventuous.Sut.App/BookingService.cs index f372dd5f..159fc469 100644 --- a/test/Eventuous.Sut.App/BookingService.cs +++ b/test/Eventuous.Sut.App/BookingService.cs @@ -10,8 +10,7 @@ public BookingService(IAggregateStore store, StreamNameMap? streamNameMap = null .InState(ExpectedState.New) .GetId(cmd => new BookingId(cmd.BookingId)) .ActAsync( - (booking, cmd, _) - => { + (booking, cmd, _) => { booking.BookRoom(cmd.RoomId, new StayPeriod(cmd.CheckIn, cmd.CheckOut), new Money(cmd.Price)); return Task.CompletedTask; diff --git a/test/Eventuous.Sut.Domain/Booking.cs b/test/Eventuous.Sut.Domain/Booking.cs index b3406cae..ec175b79 100644 --- a/test/Eventuous.Sut.Domain/Booking.cs +++ b/test/Eventuous.Sut.Domain/Booking.cs @@ -20,8 +20,7 @@ public void RecordPayment(string paymentId, Money amount, DateTimeOffset paidAt) if (HasPaymentRecord(paymentId)) return; - var (previousState, currentState) = - Apply(new BookingPaymentRegistered(paymentId, amount.Amount)); + var (previousState, currentState) = Apply(new BookingPaymentRegistered(paymentId, amount.Amount)); if (previousState.AmountPaid != currentState.AmountPaid) { var outstandingAmount = currentState.Price - currentState.AmountPaid; @@ -32,8 +31,7 @@ public void RecordPayment(string paymentId, Money amount, DateTimeOffset paidAt) if (!previousState.IsFullyPaid() && currentState.IsFullyPaid()) Apply(new BookingFullyPaid(paidAt)); } - public bool HasPaymentRecord(string paymentId) - => Current.OfType().Any(x => x.PaymentId == paymentId); + public bool HasPaymentRecord(string paymentId) => Current.OfType().Any(x => x.PaymentId == paymentId); } public record BookingId(string Value) : Id(Value); diff --git a/test/Eventuous.Sut.Domain/BookingEvents.cs b/test/Eventuous.Sut.Domain/BookingEvents.cs index 3645a4b5..6d36c3c4 100644 --- a/test/Eventuous.Sut.Domain/BookingEvents.cs +++ b/test/Eventuous.Sut.Domain/BookingEvents.cs @@ -6,19 +6,10 @@ namespace Eventuous.Sut.Domain; public static class BookingEvents { [EventType("RoomBooked")] - public record RoomBooked( - string RoomId, - LocalDate CheckIn, - LocalDate CheckOut, - float Price, - string? GuestId = null - ); + public record RoomBooked(string RoomId, LocalDate CheckIn, LocalDate CheckOut, float Price, string? GuestId = null); [EventType("PaymentRegistered")] - public record BookingPaymentRegistered( - string PaymentId, - float AmountPaid - ); + public record BookingPaymentRegistered(string PaymentId, float AmountPaid); [EventType("OutstandingAmountChanged")] public record BookingOutstandingAmountChanged(float OutstandingAmount); @@ -33,18 +24,12 @@ public record BookingOverpaid(float OverpaidAmount); public record BookingCancelled; [EventType("V1.BookingImported")] - public record BookingImported( - string RoomId, - float Price, - LocalDate CheckIn, - LocalDate CheckOut - ); + public record BookingImported(string RoomId, float Price, LocalDate CheckIn, LocalDate CheckOut); // These constants are for test purpose, use inline names in real apps public static class TypeNames { public const string BookingCancelled = "V1.BookingCancelled"; } - public static void MapBookingEvents() - => TypeMap.RegisterKnownEventTypes(); + public static void MapBookingEvents() => TypeMap.RegisterKnownEventTypes(); } diff --git a/test/Eventuous.Sut.Domain/BookingState.cs b/test/Eventuous.Sut.Domain/BookingState.cs index 39d23804..06802f09 100644 --- a/test/Eventuous.Sut.Domain/BookingState.cs +++ b/test/Eventuous.Sut.Domain/BookingState.cs @@ -5,26 +5,13 @@ namespace Eventuous.Sut.Domain; public record BookingState : State { public BookingState() { - On( - (state, booked) => state with { - Price = new Money(booked.Price), - AmountPaid = new Money(0) - } - ); - - On( - (state, imported) => state with { - Price = new Money(imported.Price), - AmountPaid = new Money(0) - } - ); + On((state, booked) => state with { Price = new Money(booked.Price), AmountPaid = new Money(0) }); + On((state, imported) => state with { Price = new Money(imported.Price), AmountPaid = new Money(0) }); On( (state, paid) => state with { AmountPaid = state.AmountPaid + new Money(paid.AmountPaid), - _registeredPayments = state._registeredPayments.Add( - new Payment(paid.PaymentId, new Money(paid.AmountPaid)) - ) + _registeredPayments = state._registeredPayments.Add(new Payment(paid.PaymentId, new Money(paid.AmountPaid))) } ); } @@ -34,14 +21,11 @@ public BookingState() { public Money Price { get; private init; } = null!; public Money AmountPaid { get; private init; } = null!; - public bool IsFullyPaid() - => AmountPaid.Amount >= Price.Amount; + public bool IsFullyPaid() => AmountPaid.Amount >= Price.Amount; - public bool IsOverpaid() - => AmountPaid.Amount > Price.Amount; + public bool IsOverpaid() => AmountPaid.Amount > Price.Amount; - public bool HasPayment(string paymentId) - => _registeredPayments.Any(p => p.PaymentId == paymentId); + public bool HasPayment(string paymentId) => _registeredPayments.Any(p => p.PaymentId == paymentId); record Payment(string PaymentId, Money Amount); } diff --git a/test/Eventuous.TestHelpers/Logging.cs b/test/Eventuous.TestHelpers/Logging.cs index 3e1a20ee..7f95bccc 100644 --- a/test/Eventuous.TestHelpers/Logging.cs +++ b/test/Eventuous.TestHelpers/Logging.cs @@ -4,9 +4,5 @@ namespace Eventuous.TestHelpers; public static class Logging { public static ILoggerFactory GetLoggerFactory(ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Debug) - => LoggerFactory.Create( - builder => builder - .SetMinimumLevel(logLevel) - .AddXunit(outputHelper, logLevel) - ); -} \ No newline at end of file + => LoggerFactory.Create(builder => builder.SetMinimumLevel(logLevel).AddXunit(outputHelper, logLevel)); +} diff --git a/test/Eventuous.TestHelpers/RecordedTrace.cs b/test/Eventuous.TestHelpers/RecordedTrace.cs index fe265e77..19da4951 100644 --- a/test/Eventuous.TestHelpers/RecordedTrace.cs +++ b/test/Eventuous.TestHelpers/RecordedTrace.cs @@ -2,15 +2,11 @@ namespace Eventuous.TestHelpers; -public record RecordedTrace( - ActivityTraceId? TraceId, - ActivitySpanId? SpanId, - ActivitySpanId? ParentSpanId -) { +public record RecordedTrace(ActivityTraceId? TraceId, ActivitySpanId? SpanId, ActivitySpanId? ParentSpanId) { public const string DefaultTraceId = "00000000000000000000000000000000"; public const string DefaultSpanId = "0000000000000000"; public bool IsDefaultTraceId => TraceId == null || TraceId.ToString() == DefaultTraceId; public bool IsDefaultSpanId => SpanId == null || SpanId.ToString() == DefaultSpanId; -} \ No newline at end of file +} diff --git a/test/Eventuous.TestHelpers/TestHelper.cs b/test/Eventuous.TestHelpers/TestHelper.cs index 40c9f23d..95f1c8bb 100644 --- a/test/Eventuous.TestHelpers/TestHelper.cs +++ b/test/Eventuous.TestHelpers/TestHelper.cs @@ -3,12 +3,10 @@ namespace Eventuous.TestHelpers; public static class TestHelper { - public static TMember? GetPrivateMember(this object instance, string name) - where TMember : class + public static TMember? GetPrivateMember(this object instance, string name) where TMember : class => GetMember(instance.GetType(), instance, name); - static TMember? GetMember(Type instanceType, object instance, string name) - where TMember : class + static TMember? GetMember(Type instanceType, object instance, string name) where TMember : class => GetMember(instanceType, instance, name) as TMember; static object? GetMember(Type instanceType, object instance, string name) { @@ -21,8 +19,6 @@ public static class TestHelper { var prop = instanceType.GetProperty(name, flags); var member = prop?.GetValue(instance) ?? field?.GetValue(instance); - return member == null && instanceType.BaseType != null - ? GetMember(instanceType.BaseType, instance, name) - : member; + return member == null && instanceType.BaseType != null ? GetMember(instanceType.BaseType, instance, name) : member; } }