From eebf8d593e741f4634c151c9d61d889977fac17c Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 16:57:40 +0800 Subject: [PATCH 01/18] build: centralize shared MSBuild settings in Directory.Build.props Hoist the metadata duplicated across ten csproj files (version, package metadata, language settings, doc generation, pack items) into a single Directory.Build.props; add nuget.config with a cleared source list, LF normalization via .gitattributes, and the CSharpier tool manifest. Packed nuspec output verified byte-equivalent before and after. --- .config/dotnet-tools.json | 10 +++++ .gitattributes | 4 ++ Directory.Build.props | 42 +++++++++++++++++++ nuget.config | 7 ++++ src/EzDdd.Common/EzDdd.Common.csproj | 40 +----------------- src/EzDdd.Core/EzDdd.Core.csproj | 40 +----------------- src/EzDdd.Cqrs/EzDdd.Cqrs.csproj | 40 +----------------- src/EzDdd.Entity/EzDdd.Entity.csproj | 40 +----------------- src/EzDdd.UseCase/EzDdd.UseCase.csproj | 40 +----------------- .../EzDdd.Common.Tests.csproj | 4 -- .../EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj | 4 -- .../EzDdd.Entity.Tests.csproj | 4 -- .../EzDdd.Integration.Tests.csproj | 4 -- .../EzDdd.UseCase.Tests.csproj | 4 -- 14 files changed, 68 insertions(+), 215 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .gitattributes create mode 100644 Directory.Build.props create mode 100644 nuget.config diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..0121c16 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "csharpier": { + "version": "1.2.6", + "commands": ["csharpier"] + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ed446d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Normalize text file line endings to LF in the repository. +# Ensures cross-platform consistency between Windows and Linux CI. +* text=auto eol=lf +*.sln text eol=crlf diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..e087924 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,42 @@ + + + + net8.0 + 12.0 + + + enable + enable + false + true + + + 1.0.0-alpha.1 + 1.0.0.0 + 1.0.0.0 + + + TeddySoft + TeddySoft + ezDDD.NET + Copyright (c) 2025-2026 ezDDD.NET Contributors + MIT + https://github.com/cwouyang/ezDDD.NET + https://github.com/cwouyang/ezDDD.NET + git + README.md + See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. + + + true + $(NoWarn);1701;1702;CS1591 + + + embedded + + + + + + + diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..765346e --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/EzDdd.Common/EzDdd.Common.csproj b/src/EzDdd.Common/EzDdd.Common.csproj index 8c673ca..16cad85 100644 --- a/src/EzDdd.Common/EzDdd.Common.csproj +++ b/src/EzDdd.Common/EzDdd.Common.csproj @@ -1,26 +1,7 @@ - + - - net8.0 - 12.0 - - - enable - enable - false - - - 1.0.0.0 - 1.0.0.0 - Copyright (c) 2025-2026 ezDDD.NET Contributors - - ezDDD.Common - 1.0.0-alpha.1 - TeddySoft - TeddySoft - ezDDD.NET Foundation utilities for ezDDD.NET - Tactical Domain-Driven Design patterns library for .NET 8+. Features: @@ -31,25 +12,6 @@ Features: Part of the ezDDD.NET family of DDD tactical patterns libraries. ddd;domain-driven-design;utilities;bimap;json;converter;tactical-ddd;dotnet;csharp - MIT - https://github.com/cwouyang/ezDDD.NET - https://github.com/cwouyang/ezDDD.NET - git - README.md - See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. - - - true - bin\$(Configuration)\$(TargetFramework)\EzDdd.Common.xml - $(NoWarn);CS1591 - - - embedded - - - - - diff --git a/src/EzDdd.Core/EzDdd.Core.csproj b/src/EzDdd.Core/EzDdd.Core.csproj index 620ce03..6c5e7a9 100644 --- a/src/EzDdd.Core/EzDdd.Core.csproj +++ b/src/EzDdd.Core/EzDdd.Core.csproj @@ -1,4 +1,4 @@ - + @@ -8,26 +8,7 @@ - - net8.0 - 12.0 - - - enable - enable - false - - - 1.0.0.0 - 1.0.0.0 - Copyright (c) 2025-2026 ezDDD.NET Contributors - - ezDDD.Core - 1.0.0-alpha.1 - TeddySoft - TeddySoft - ezDDD.NET All-in-one package for ezDDD.NET - Tactical Domain-Driven Design patterns, CQRS, Event Sourcing, State Sourcing, and Clean Architecture for .NET 8+. This package references all 4 core modules: @@ -51,25 +32,6 @@ Features: ~98% semantic parity with Java ezddd. Perfect for DDD practitioners using C# and .NET. ddd;domain-driven-design;cqrs;event-sourcing;state-sourcing;clean-architecture;aggregate-root;tactical-ddd;outbox-pattern;message-bus;dotnet;csharp - MIT - https://github.com/cwouyang/ezDDD.NET - https://github.com/cwouyang/ezDDD.NET - git - README.md - See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. - - - true - bin\$(Configuration)\$(TargetFramework)\EzDdd.Core.xml - $(NoWarn);CS1591 - - - embedded - - - - - diff --git a/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj b/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj index 86fdf4c..dbab318 100644 --- a/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj +++ b/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj @@ -1,30 +1,11 @@ - + - - net8.0 - 12.0 - - - enable - enable - false - - - 1.0.0.0 - 1.0.0.0 - Copyright (c) 2025-2026 ezDDD.NET Contributors - - ezDDD.Cqrs - 1.0.0-alpha.1 - TeddySoft - TeddySoft - ezDDD.NET CQRS patterns for ezDDD.NET - Command, Query, Projection, Projector, and Archive interfaces. Features: @@ -40,25 +21,6 @@ Features: Enables Command Query Responsibility Segregation with eventual consistency. ddd;domain-driven-design;cqrs;command;query;projection;read-model;archive;inquiry;dotnet;csharp - MIT - https://github.com/cwouyang/ezDDD.NET - https://github.com/cwouyang/ezDDD.NET - git - README.md - See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. - - - true - bin\$(Configuration)\$(TargetFramework)\EzDdd.Cqrs.xml - $(NoWarn);CS1591 - - - embedded - - - - - diff --git a/src/EzDdd.Entity/EzDdd.Entity.csproj b/src/EzDdd.Entity/EzDdd.Entity.csproj index 8c6bd5e..bc7ed20 100644 --- a/src/EzDdd.Entity/EzDdd.Entity.csproj +++ b/src/EzDdd.Entity/EzDdd.Entity.csproj @@ -1,4 +1,4 @@ - + @@ -6,26 +6,7 @@ - - net8.0 - 12.0 - - - enable - enable - false - - - 1.0.0.0 - 1.0.0.0 - Copyright (c) 2025-2026 ezDDD.NET Contributors - - ezDDD.Entity - 1.0.0-alpha.1 - TeddySoft - TeddySoft - ezDDD.NET Core DDD tactical patterns for ezDDD.NET - Entity, AggregateRoot, EsAggregateRoot, and DomainEvent. Features: @@ -40,25 +21,6 @@ Features: Designed for Domain-Driven Design with event sourcing and state sourcing. ddd;domain-driven-design;aggregate-root;entity;value-object;domain-events;event-sourcing;tactical-ddd;dotnet;csharp - MIT - https://github.com/cwouyang/ezDDD.NET - https://github.com/cwouyang/ezDDD.NET - git - README.md - See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. - - - true - bin\$(Configuration)\$(TargetFramework)\EzDdd.Entity.xml - $(NoWarn);CS1591 - - - embedded - - - - - diff --git a/src/EzDdd.UseCase/EzDdd.UseCase.csproj b/src/EzDdd.UseCase/EzDdd.UseCase.csproj index 2326637..a26173d 100644 --- a/src/EzDdd.UseCase/EzDdd.UseCase.csproj +++ b/src/EzDdd.UseCase/EzDdd.UseCase.csproj @@ -1,30 +1,11 @@ - + - - net8.0 - 12.0 - - - enable - enable - false - - - 1.0.0.0 - 1.0.0.0 - Copyright (c) 2025-2026 ezDDD.NET Contributors - - ezDDD.UseCase - 1.0.0-alpha.1 - TeddySoft - TeddySoft - ezDDD.NET Use cases and repository patterns for ezDDD.NET - Clean Architecture support with event sourcing and state sourcing. Features: @@ -39,25 +20,6 @@ Features: Implements Clean Architecture layers with tactical DDD patterns. ddd;domain-driven-design;clean-architecture;use-case;repository;event-sourcing;state-sourcing;outbox-pattern;dotnet;csharp - MIT - https://github.com/cwouyang/ezDDD.NET - https://github.com/cwouyang/ezDDD.NET - git - README.md - See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. - - - true - bin\$(Configuration)\$(TargetFramework)\EzDdd.UseCase.xml - $(NoWarn);CS1591 - - - embedded - - - - - diff --git a/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj b/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj index cb5f2bc..02f2b0c 100644 --- a/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj +++ b/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true diff --git a/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj b/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj index 25e1959..8f9a771 100644 --- a/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj +++ b/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true diff --git a/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj b/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj index 69c5c47..8ec21e3 100644 --- a/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj +++ b/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true diff --git a/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj b/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj index 9c1b17d..071a9e5 100644 --- a/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj +++ b/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true diff --git a/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj b/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj index 0495dfd..f5be9aa 100644 --- a/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj +++ b/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj @@ -1,10 +1,6 @@ - net8.0 - enable - enable - false true From a22b8cce84178d3df1dbe12f795a0863314d722f Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 16:59:20 +0800 Subject: [PATCH 02/18] build: pin .NET SDK 8 and treat warnings as errors Pin the SDK via global.json (8.0.200, latestPatch roll-forward) so builds stop drifting onto newer installed SDKs, and enable TreatWarningsAsErrors now that the solution builds warning-free. --- Directory.Build.props | 2 +- global.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 global.json diff --git a/Directory.Build.props b/Directory.Build.props index e087924..1d1d62e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ enable enable - false + true true diff --git a/global.json b/global.json new file mode 100644 index 0000000..d031a76 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.200", + "rollForward": "latestPatch" + } +} From 6c620f2db3a9237a9c4f527712ee7759ca11d314 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 17:00:06 +0800 Subject: [PATCH 03/18] docs(core): refresh package description after Phase 7 sync Drop the MessageBus mentions and message-bus tag (removed in Phase 7), mention the Relay pattern instead, and update the parity claim to Java ezddd 6.0.1. --- src/EzDdd.Core/EzDdd.Core.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/EzDdd.Core/EzDdd.Core.csproj b/src/EzDdd.Core/EzDdd.Core.csproj index 6c5e7a9..19b9ea4 100644 --- a/src/EzDdd.Core/EzDdd.Core.csproj +++ b/src/EzDdd.Core/EzDdd.Core.csproj @@ -14,7 +14,7 @@ This package references all 4 core modules: • ezDDD.Common - Foundation utilities (BiMap, Converter, JsonUtil) • ezDDD.Entity - DDD tactical patterns (Entity, AggregateRoot, EsAggregateRoot, DomainEvent) -• ezDDD.UseCase - Use cases and repositories (IUseCase, EsRepository, OutboxRepository, MessageBus) +• ezDDD.UseCase - Use cases and repositories (IUseCase, EsRepository, OutboxRepository, IReactor) • ezDDD.Cqrs - CQRS patterns (Command, Query, Projection, Projector, Archive) Features: @@ -22,7 +22,7 @@ Features: • State sourcing with Transactional Outbox • Bridge pattern (IRepository ↔ IRepositoryPeer) • Template method pattern (EsAggregateRoot) -• Observer pattern (MessageBus) +• Transactional Outbox relay for event publishing • Clean Architecture layers • Async/await throughout • Nullable reference types @@ -30,8 +30,8 @@ Features: • Pattern matching for event handlers • Zero external dependencies (only .NET BCL + uContract.NET) -~98% semantic parity with Java ezddd. Perfect for DDD practitioners using C# and .NET. - ddd;domain-driven-design;cqrs;event-sourcing;state-sourcing;clean-architecture;aggregate-root;tactical-ddd;outbox-pattern;message-bus;dotnet;csharp +~99% semantic parity with Java ezddd 6.0.1. Perfect for DDD practitioners using C# and .NET. + ddd;domain-driven-design;cqrs;event-sourcing;state-sourcing;clean-architecture;aggregate-root;tactical-ddd;outbox-pattern;dotnet;csharp From bac134048742b2de141dfddf00f90daf6553dbfb Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 17:38:37 +0800 Subject: [PATCH 04/18] style: apply CSharpier 1.2.6 baseline format Repository-wide whitespace, wrapping, and trailing-comma normalization by the CSharpier formatter (printWidth 120, LF). No semantic changes: every file verified token-equivalent modulo whitespace and trailing commas. Add .csharpierrc.yaml and .csharpierignore (csproj excluded). --- .csharpierignore | 7 + .csharpierrc.yaml | 3 + .../EventInfrastructure/EventStoreRelay.cs | 32 +- examples/EventInfrastructure/IEventStore.cs | 3 +- .../EventInfrastructure/InMemoryEventStore.cs | 9 +- src/EzDdd.Common/BiMap.cs | 24 +- src/EzDdd.Common/Converter.cs | 6 +- src/EzDdd.Common/JsonUtil.cs | 14 +- src/EzDdd.Cqrs/Command/ICommand.cs | 14 +- src/EzDdd.Cqrs/Command/IInquiry.cs | 32 +- src/EzDdd.Cqrs/Command/IInquiryInput.cs | 4 +- src/EzDdd.Cqrs/CqrsOutput.cs | 6 +- src/EzDdd.Cqrs/Query/IArchive.cs | 18 +- src/EzDdd.Cqrs/Query/IProjection.cs | 18 +- src/EzDdd.Cqrs/Query/IProjectionInput.cs | 4 +- src/EzDdd.Cqrs/Query/IQuery.cs | 8 +- src/EzDdd.Entity/AggregateRoot.cs | 10 +- src/EzDdd.Entity/DomainEventTypeMapper.cs | 40 +-- src/EzDdd.Entity/EsAggregateRoot.cs | 32 +- src/EzDdd.Entity/IDomainEvent.cs | 2 +- src/EzDdd.Entity/IDomainEventSource.cs | 7 +- src/EzDdd.Entity/IEntity.cs | 6 +- src/EzDdd.Entity/IInternalDomainEvent.cs | 6 +- src/EzDdd.Entity/IValueObject.cs | 12 +- .../Exceptions/PostEventFailureException.cs | 14 +- .../Exceptions/RepositoryPeerSaveException.cs | 21 +- .../Exceptions/RepositorySaveException.cs | 19 +- .../Exceptions/UseCaseFailureException.cs | 14 +- src/EzDdd.UseCase/Port/In/ExitCode.cs | 4 +- src/EzDdd.UseCase/Port/In/IInput.cs | 2 +- src/EzDdd.UseCase/Port/In/IOutput.cs | 2 +- src/EzDdd.UseCase/Port/In/IReconciler.cs | 6 +- src/EzDdd.UseCase/Port/In/IUseCase.cs | 2 +- src/EzDdd.UseCase/Port/In/IVersionedInput.cs | 2 +- .../Port/InOut/DomainEventData.cs | 25 +- .../Port/InOut/DomainEventDataBuilder.cs | 28 +- .../Port/InOut/DomainEventMapper.cs | 15 +- .../Port/InOut/IExternalDomainEvent.cs | 4 +- .../Port/InOut/InternalDomainEventDto.cs | 6 +- .../IExternalDomainEventPublisher.cs | 3 +- src/EzDdd.UseCase/Port/Out/EsRepository.cs | 34 +- src/EzDdd.UseCase/Port/Out/EventStoreData.cs | 6 +- .../Port/Out/EventStoreMapper.cs | 19 +- src/EzDdd.UseCase/Port/Out/IOutboxData.cs | 4 +- src/EzDdd.UseCase/Port/Out/IRepository.cs | 12 +- src/EzDdd.UseCase/Port/Out/IRepositoryPeer.cs | 18 +- src/EzDdd.UseCase/Port/Out/IStoreData.cs | 6 +- src/EzDdd.UseCase/Port/Out/OutboxMapper.cs | 4 +- .../Port/Out/OutboxRepository.cs | 12 +- tests/EzDdd.Common.Tests/BiMapTests.cs | 269 +++++++-------- tests/EzDdd.Common.Tests/ConverterTests.cs | 5 +- tests/EzDdd.Common.Tests/JsonUtilTests.cs | 46 +-- .../EzDdd.Cqrs.Tests/Command/CommandTests.cs | 17 +- .../Command/InquiryInputTests.cs | 2 +- .../EzDdd.Cqrs.Tests/Command/InquiryTests.cs | 17 +- tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs | 56 ++- .../Integration/CompleteCqrsFlowTests.cs | 28 +- .../TestDomain/AccountProjector.cs | 15 +- .../TestDomain/AccountSummaryReadModel.cs | 5 +- .../TestDomain/GetAccountSummaryInput.cs | 2 +- .../TestDomain/GetAccountSummaryOutput.cs | 2 +- .../TestDomain/GetAccountSummaryQuery.cs | 21 +- .../Query/ArchiveCrudTests.cs | 42 +-- .../Query/ProjectionInputTests.cs | 8 +- .../EzDdd.Cqrs.Tests/Query/ProjectionTests.cs | 12 +- tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs | 13 +- .../Query/TestHelpers/InMemoryArchive.cs | 3 +- .../EzDdd.Entity.Tests/AggregateRootTests.cs | 65 ++-- tests/EzDdd.Entity.Tests/DomainEventTests.cs | 39 +-- .../DomainEventTypeMapperConcurrencyTests.cs | 160 ++++++--- .../DomainEventTypeMapperTests.cs | 148 ++++---- tests/EzDdd.Entity.Tests/EntityTests.cs | 5 +- .../EsAggregateRootTests.cs | 155 +++++---- tests/EzDdd.Entity.Tests/IntegrationTests.cs | 129 +++---- .../InternalDomainEventTests.cs | 75 ++-- tests/EzDdd.Entity.Tests/ValueObjectTests.cs | 11 +- .../ConcurrentOperationsTests.cs | 136 ++++---- .../CqrsFlowWithMetadataTests.cs | 54 ++- .../EventSourcingMetadataTests.cs | 64 ++-- .../ReconcilerExecutionTests.cs | 71 +--- .../InMemoryMetadataTestEventStorePeer.cs | 11 +- .../TestDomain/MetadataTestAggregate.cs | 45 +-- .../TestDomain/MetadataTestEvents.cs | 19 +- .../TestDomain/MetadataTestId.cs | 2 +- .../TestDomain/ReconcilerTestDomain.cs | 49 +-- .../PostEventFailureExceptionTests.cs | 2 +- .../RepositoryPeerSaveExceptionTests.cs | 17 +- .../RepositorySaveExceptionTests.cs | 14 +- .../UseCaseFailureExceptionTests.cs | 14 +- .../CrossComponentIntegrationTests.cs | 2 +- .../Integration/EventInfrastructureTests.cs | 69 ++-- .../Integration/EventSourcingWorkflowTests.cs | 22 +- .../Services/ITransferMoneyService.cs | 5 +- .../Services/TransferConfirmation.cs | 5 +- .../Services/TransferExceptions.cs | 9 +- .../Services/TransferMoneyService.cs | 8 +- .../Services/TransferMoneyServiceTests.cs | 56 +-- .../Integration/StateSourcingWorkflowTests.cs | 22 +- .../Integration/TestDomain/AccountId.cs | 2 +- .../Integration/TestDomain/BankAccount.cs | 39 +-- .../TestDomain/BankAccountEvents.cs | 34 +- .../CleanUpExpiredOrdersReconciler.cs | 7 +- .../TestDomain/CreateAccountUseCase.cs | 26 +- .../Integration/TestDomain/DepositUseCase.cs | 26 +- .../TestDomain/InMemoryEventStorePeer.cs | 11 +- .../TestDomain/InMemoryOutboxPeer.cs | 6 +- .../Integration/TestDomain/Money.cs | 16 +- .../Integration/TestDomain/Order.cs | 42 +-- .../Integration/TestDomain/OrderData.cs | 2 +- .../Integration/TestDomain/OrderEvents.cs | 25 +- .../Integration/TestDomain/OrderId.cs | 2 +- .../Integration/TestDomain/OrderMapper.cs | 36 +- .../Integration/TestDomain/WithdrawUseCase.cs | 26 +- .../Integration/TransactionBoundaryTests.cs | 2 +- .../Integration/UseCaseExecutionFlowTests.cs | 80 ++--- .../Port/In/InputOutputTests.cs | 32 +- .../Port/In/ReconcilerTests.cs | 81 ++--- .../Port/In/UseCaseTests.cs | 6 +- .../Port/InOut/DomainEventDataBuilderTests.cs | 122 +++---- .../Port/InOut/DomainEventDataTests.cs | 323 ++++-------------- .../Port/InOut/DomainEventMapperTests.cs | 82 ++--- .../Port/InOut/DomainEventMetadataTests.cs | 52 ++- .../Port/InOut/ExternalDomainEventTests.cs | 23 +- .../Port/InOut/InternalDomainEventDtoTests.cs | 72 ++-- .../ExternalDomainEventPublisherTests.cs | 9 +- .../Port/Out/EsRepositoryTests.cs | 54 ++- .../Port/Out/EventStoreDataTests.cs | 39 ++- .../Port/Out/EventStoreMapperTests.cs | 53 ++- .../Port/Out/OutboxDataTests.cs | 61 ++-- .../Port/Out/OutboxMapperTests.cs | 82 ++--- .../Port/Out/OutboxRepositoryFindByIdTests.cs | 4 +- .../Port/Out/OutboxRepositoryTests.cs | 56 ++- .../Port/Out/RepositoryPeerTests.cs | 53 +-- .../Port/Out/RepositoryTests.cs | 36 +- .../Port/Out/StoreDataTests.cs | 48 +-- 135 files changed, 1804 insertions(+), 2511 deletions(-) create mode 100644 .csharpierignore create mode 100644 .csharpierrc.yaml diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..f20ae3b --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,7 @@ +# CSharpier scope: exclude non-C# files. +# +# CSharpier 1.x formats .csproj files by default (blank-line normalization inside +# elements). We keep project files outside the formatter's scope so the +# .cs baseline reformat commit stays pure and so future `dotnet csharpier check` runs +# remain idempotent without touching build configuration. +*.csproj diff --git a/.csharpierrc.yaml b/.csharpierrc.yaml new file mode 100644 index 0000000..6e22af0 --- /dev/null +++ b/.csharpierrc.yaml @@ -0,0 +1,3 @@ +printWidth: 120 +useTabs: false +endOfLine: lf diff --git a/examples/EventInfrastructure/EventStoreRelay.cs b/examples/EventInfrastructure/EventStoreRelay.cs index 8ba5d05..75747af 100644 --- a/examples/EventInfrastructure/EventStoreRelay.cs +++ b/examples/EventInfrastructure/EventStoreRelay.cs @@ -116,7 +116,8 @@ public EventStoreRelay( IEventStore eventStore, IMessageProducer messageProducer, ILogger logger, - int pollingIntervalMs = 100) + int pollingIntervalMs = 100 + ) { ArgumentNullException.ThrowIfNull(eventStore); ArgumentNullException.ThrowIfNull(messageProducer); @@ -127,7 +128,8 @@ public EventStoreRelay( throw new ArgumentOutOfRangeException( nameof(pollingIntervalMs), pollingIntervalMs, - "Polling interval must be greater than 0"); + "Polling interval must be greater than 0" + ); } _eventStore = eventStore; @@ -170,24 +172,25 @@ public EventStoreRelay( /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation( - "EventStoreRelay started with polling interval {Interval}ms", - _pollingIntervalMs); + _logger.LogInformation("EventStoreRelay started with polling interval {Interval}ms", _pollingIntervalMs); while (!stoppingToken.IsCancellationRequested) { try { // Get events after current index - IReadOnlyList newEvents = - await _eventStore.GetEventsAfterAsync(_currentIndex, stoppingToken); + IReadOnlyList newEvents = await _eventStore.GetEventsAfterAsync( + _currentIndex, + stoppingToken + ); if (newEvents.Count > 0) { _logger.LogDebug( "Polling event store: found {Count} new events after index {Index}", newEvents.Count, - _currentIndex); + _currentIndex + ); foreach (IInternalDomainEvent domainEvent in newEvents) { @@ -206,7 +209,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) "Published event {EventType} with ID {EventId} (index: {Index})", eventData.EventType, eventData.EventId, - _currentIndex); + _currentIndex + ); } catch (Exception publishEx) { @@ -215,7 +219,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) publishEx, "Failed to publish event {EventType} at index {Index}, will retry on next poll", domainEvent.GetType().Name, - _currentIndex + 1); + _currentIndex + 1 + ); // Don't increment currentIndex - will retry this event next time break; // Stop processing this batch, retry from this event @@ -238,15 +243,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogError( ex, "EventStoreRelay encountered error at index {Index}, will retry on next poll", - _currentIndex); + _currentIndex + ); await Task.Delay(_pollingIntervalMs, stoppingToken); } } - _logger.LogInformation( - "EventStoreRelay stopped (published {Count} events total)", - _currentIndex + 1); + _logger.LogInformation("EventStoreRelay stopped (published {Count} events total)", _currentIndex + 1); } /// diff --git a/examples/EventInfrastructure/IEventStore.cs b/examples/EventInfrastructure/IEventStore.cs index 7513539..5f03e17 100644 --- a/examples/EventInfrastructure/IEventStore.cs +++ b/examples/EventInfrastructure/IEventStore.cs @@ -57,5 +57,6 @@ public interface IEventStore /// Task> GetEventsAfterAsync( int afterIndex, - CancellationToken cancellationToken = default); + CancellationToken cancellationToken = default + ); } diff --git a/examples/EventInfrastructure/InMemoryEventStore.cs b/examples/EventInfrastructure/InMemoryEventStore.cs index 99b3126..ea2ee0b 100644 --- a/examples/EventInfrastructure/InMemoryEventStore.cs +++ b/examples/EventInfrastructure/InMemoryEventStore.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; - using EzDdd.Entity; namespace EzDdd.Examples.EventInfrastructure; @@ -77,15 +76,15 @@ public class InMemoryEventStore : IEventStore /// public Task> GetEventsAfterAsync( int afterIndex, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { // Convert queue to list (thread-safe snapshot) List allEvents = _events.ToList(); // Return events after the specified index - IReadOnlyList result = afterIndex < 0 - ? allEvents.AsReadOnly() - : allEvents.Skip(afterIndex + 1).ToList().AsReadOnly(); + IReadOnlyList result = + afterIndex < 0 ? allEvents.AsReadOnly() : allEvents.Skip(afterIndex + 1).ToList().AsReadOnly(); return Task.FromResult(result); } diff --git a/src/EzDdd.Common/BiMap.cs b/src/EzDdd.Common/BiMap.cs index 818d79e..40de2e1 100644 --- a/src/EzDdd.Common/BiMap.cs +++ b/src/EzDdd.Common/BiMap.cs @@ -144,7 +144,7 @@ private void _AddInternal(TKey key, TValue value) _reverse[value] = key; } -#region IDictionary Implementation + #region IDictionary Implementation public TValue this[TKey key] { @@ -226,7 +226,7 @@ public bool Contains(KeyValuePair item) lock (_lock) { return _forward.TryGetValue(item.Key, out TValue? value) - && EqualityComparer.Default.Equals(value, item.Value); + && EqualityComparer.Default.Equals(value, item.Value); } } @@ -285,8 +285,10 @@ public bool Remove(KeyValuePair item) { lock (_lock) { - if (_forward.TryGetValue(item.Key, out TValue? value) - && EqualityComparer.Default.Equals(value, item.Value)) + if ( + _forward.TryGetValue(item.Key, out TValue? value) + && EqualityComparer.Default.Equals(value, item.Value) + ) { _forward.Remove(item.Key); _reverse.Remove(value); @@ -310,9 +312,9 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } -#endregion + #endregion -#region Additional HashMap-compatible Methods + #region Additional HashMap-compatible Methods /// /// Adds all key-value pairs from the specified dictionary to this BiMap. @@ -423,9 +425,11 @@ public bool Replace(TKey key, TValue oldValue, TValue newValue) lock (_lock) { #pragma warning disable CS8600 - if (_forward.TryGetValue(key, out TValue currentValue) + if ( + _forward.TryGetValue(key, out TValue currentValue) #pragma warning restore CS8600 - && EqualityComparer.Default.Equals(currentValue, oldValue)) + && EqualityComparer.Default.Equals(currentValue, oldValue) + ) { _AddInternal(key, newValue); return true; @@ -435,5 +439,5 @@ public bool Replace(TKey key, TValue oldValue, TValue newValue) } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/src/EzDdd.Common/Converter.cs b/src/EzDdd.Common/Converter.cs index f02f463..5c49a84 100644 --- a/src/EzDdd.Common/Converter.cs +++ b/src/EzDdd.Common/Converter.cs @@ -23,10 +23,10 @@ namespace EzDdd.Common; /// // Lambda implementation /// Converter<string, int> stringToInt = s => int.Parse(s); /// int result = stringToInt("42"); -/// +/// /// // Method reference /// Converter<string, int> parser = int.Parse; -/// +/// /// // Multi-line lambda /// Converter<User, UserDto> converter = user => /// { @@ -40,4 +40,4 @@ namespace EzDdd.Common; /// Consider using nullable reference types in the generic parameters when null values are expected. /// /// -public delegate TTarget Converter(TSource source); \ No newline at end of file +public delegate TTarget Converter(TSource source); diff --git a/src/EzDdd.Common/JsonUtil.cs b/src/EzDdd.Common/JsonUtil.cs index 706d2a0..5ba46af 100644 --- a/src/EzDdd.Common/JsonUtil.cs +++ b/src/EzDdd.Common/JsonUtil.cs @@ -94,7 +94,7 @@ static JsonUtil() WriteIndented = false, // Include all fields, even if null (no ignore conditions) - DefaultIgnoreCondition = JsonIgnoreCondition.Never + DefaultIgnoreCondition = JsonIgnoreCondition.Never, // DateTime serialization: ISO-8601 format (default, not timestamps) // Equivalent to Jackson's WRITE_DATES_AS_TIMESTAMPS: false @@ -189,7 +189,10 @@ public static string AsString(object value) } catch (Exception ex) { - throw new InvalidOperationException($"Failed to deserialize byte array to {typeof(T).Name}: {ex.Message}", ex); + throw new InvalidOperationException( + $"Failed to deserialize byte array to {typeof(T).Name}: {ex.Message}", + ex + ); } } @@ -305,7 +308,10 @@ public static JsonDocument ReadTree(byte[] bytes) } catch (Exception ex) { - throw new InvalidOperationException($"Failed to deep copy object of type {typeof(T).Name}: {ex.Message}", ex); + throw new InvalidOperationException( + $"Failed to deep copy object of type {typeof(T).Name}: {ex.Message}", + ex + ); } } -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Command/ICommand.cs b/src/EzDdd.Cqrs/Command/ICommand.cs index 82e01a8..85730a6 100644 --- a/src/EzDdd.Cqrs/Command/ICommand.cs +++ b/src/EzDdd.Cqrs/Command/ICommand.cs @@ -43,22 +43,22 @@ namespace EzDdd.Cqrs.Command; /// string AccountNumber, /// Money InitialBalance /// ) : IInput; -/// +/// /// public class CreateAccountOutput : CqrsOutput<CreateAccountOutput> /// { /// public string AccountNumber { get; set; } = string.Empty; /// } -/// +/// /// public class CreateAccountCommand /// : ICommand<CreateAccountInput, CreateAccountOutput> /// { /// private readonly IRepository<BankAccount, AccountId> _repository; -/// +/// /// public CreateAccountCommand(IRepository<BankAccount, AccountId> repository) /// { /// _repository = repository; /// } -/// +/// /// public async Task<CreateAccountOutput> ExecuteAsync(CreateAccountInput input) /// { /// var account = BankAccount.Create( @@ -66,9 +66,9 @@ namespace EzDdd.Cqrs.Command; /// input.AccountNumber, /// input.InitialBalance /// ); -/// +/// /// await _repository.SaveAsync(account); -/// +/// /// return CreateAccountOutput.Create() /// .SetId(input.AccountId.Value) /// .SetAccountNumber(input.AccountNumber) @@ -117,4 +117,4 @@ public interface ICommand : IUseCase where TOutput : CqrsOutput, new() { // Marker interface - inherits ExecuteAsync(TInput input) from IUseCase -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Command/IInquiry.cs b/src/EzDdd.Cqrs/Command/IInquiry.cs index 18dcfc0..31aea32 100644 --- a/src/EzDdd.Cqrs/Command/IInquiry.cs +++ b/src/EzDdd.Cqrs/Command/IInquiry.cs @@ -45,44 +45,44 @@ namespace EzDdd.Cqrs.Command; /// Example: /// /// public record CheckAccountExistsInput(AccountId AccountId) : IInquiryInput; -/// +/// /// public class CheckAccountExistsInquiry /// : IInquiry<CheckAccountExistsInput, bool> /// { /// private readonly IArchive<AccountSummary, AccountId> _archive; -/// +/// /// public CheckAccountExistsInquiry( /// IArchive<AccountSummary, AccountId> archive) /// { /// _archive = archive; /// } -/// +/// /// public async Task<bool> QueryAsync(CheckAccountExistsInput input) /// { /// var account = await _archive.FindByIdAsync(input.AccountId); /// return account != null; /// } /// } -/// +/// /// // Usage within a command: /// public class TransferMoneyCommand : ICommand<TransferInput, TransferOutput> /// { /// private readonly IInquiry<CheckAccountExistsInput, bool> _accountExistsInquiry; -/// +/// /// public async Task<TransferOutput> ExecuteAsync(TransferInput input) /// { /// // Validate source account exists /// var sourceExists = await _accountExistsInquiry.QueryAsync( /// new CheckAccountExistsInput(input.SourceAccountId) /// ); -/// +/// /// if (!sourceExists) /// { /// return TransferOutput.Create() /// .SetMessage("Source account not found") /// .Fail(); /// } -/// +/// /// // Continue with transfer logic... /// } /// } @@ -96,51 +96,51 @@ namespace EzDdd.Cqrs.Command; /// AccountId AccountId, /// decimal Amount /// ) : IInquiryInput; -/// +/// /// // Output: Validation result with sufficient balance flag /// public record ValidateWithdrawalOutput( /// bool IsSufficient, /// decimal CurrentBalance /// ); -/// +/// /// // Inquiry: Check if account has sufficient balance for withdrawal /// public class ValidateWithdrawalInquiry /// : IInquiry<ValidateWithdrawalInput, ValidateWithdrawalOutput> /// { /// private readonly IArchive<AccountSummary, AccountId> _archive; -/// +/// /// public async Task<ValidateWithdrawalOutput> QueryAsync( /// ValidateWithdrawalInput input) /// { /// var account = await _archive.FindByIdAsync(input.AccountId); /// if (account == null) /// return new ValidateWithdrawalOutput(false, 0m); -/// +/// /// bool isSufficient = account.Balance >= input.Amount; /// return new ValidateWithdrawalOutput(isSufficient, account.Balance); /// } /// } -/// +/// /// // Usage in command: /// public class WithdrawMoneyCommand : ICommand<WithdrawInput, WithdrawOutput> /// { /// private readonly IInquiry<ValidateWithdrawalInput, ValidateWithdrawalOutput> /// _validateInquiry; -/// +/// /// public async Task<WithdrawOutput> ExecuteAsync(WithdrawInput input) /// { /// // Pre-validation using inquiry /// var validation = await _validateInquiry.QueryAsync( /// new ValidateWithdrawalInput(input.AccountId, input.Amount) /// ); -/// +/// /// if (!validation.IsSufficient) /// { /// return WithdrawOutput.Create() /// .SetMessage($"Insufficient balance. Current: {validation.CurrentBalance}") /// .Fail(); /// } -/// +/// /// // Proceed with withdrawal logic... /// } /// } @@ -170,4 +170,4 @@ public interface IInquiry /// or check system state before executing business logic. /// Task QueryAsync(TInput input); -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Command/IInquiryInput.cs b/src/EzDdd.Cqrs/Command/IInquiryInput.cs index 043de6f..69b7c50 100644 --- a/src/EzDdd.Cqrs/Command/IInquiryInput.cs +++ b/src/EzDdd.Cqrs/Command/IInquiryInput.cs @@ -21,7 +21,7 @@ namespace EzDdd.Cqrs.Command; /// Example: /// /// public record CheckAccountExistsInput(AccountId AccountId) : IInquiryInput; -/// +/// /// public class CheckAccountExistsInquiry /// : IInquiry<CheckAccountExistsInput, bool> /// { @@ -37,4 +37,4 @@ namespace EzDdd.Cqrs.Command; public interface IInquiryInput { // Pure marker interface - no methods -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/CqrsOutput.cs b/src/EzDdd.Cqrs/CqrsOutput.cs index 27b2cae..dd93c2e 100644 --- a/src/EzDdd.Cqrs/CqrsOutput.cs +++ b/src/EzDdd.Cqrs/CqrsOutput.cs @@ -32,14 +32,14 @@ namespace EzDdd.Cqrs; /// public class CreateAccountOutput : CqrsOutput<CreateAccountOutput> /// { /// public string AccountNumber { get; set; } = string.Empty; -/// +/// /// public CreateAccountOutput SetAccountNumber(string accountNumber) /// { /// AccountNumber = accountNumber; /// return this; /// } /// } -/// +/// /// // Usage with fluent API: /// var output = CreateAccountOutput.Create() /// .SetId("ACC-001") @@ -209,4 +209,4 @@ private T _Self() { return (T)this; } -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Query/IArchive.cs b/src/EzDdd.Cqrs/Query/IArchive.cs index 2552b6c..9251aca 100644 --- a/src/EzDdd.Cqrs/Query/IArchive.cs +++ b/src/EzDdd.Cqrs/Query/IArchive.cs @@ -83,48 +83,48 @@ namespace EzDdd.Cqrs.Query; /// int TransactionCount, /// DateTimeOffset LastTransactionDate /// ); -/// +/// /// // Archive implementation (example: in-memory) /// public class InMemoryAccountArchive : IArchive<AccountSummaryReadModel, AccountId> /// { /// private readonly ConcurrentDictionary<AccountId, AccountSummaryReadModel> _store = new(); -/// +/// /// public async Task<AccountSummaryReadModel?> FindByIdAsync(AccountId id) /// { /// await Task.CompletedTask; /// _store.TryGetValue(id, out var readModel); /// return readModel; /// } -/// +/// /// public async Task SaveAsync(AccountSummaryReadModel data) /// { /// await Task.CompletedTask; /// _store[data.AccountId] = data; /// } -/// +/// /// public async Task DeleteAsync(AccountSummaryReadModel data) /// { /// await Task.CompletedTask; /// _store.TryRemove(data.AccountId, out _); /// } /// } -/// +/// /// // Usage in query /// public class GetAccountSummaryQuery /// : IQuery<GetAccountSummaryInput, GetAccountSummaryOutput> /// { /// private readonly IArchive<AccountSummaryReadModel, AccountId> _archive; -/// +/// /// public async Task<GetAccountSummaryOutput> ExecuteAsync( /// GetAccountSummaryInput input) /// { /// var readModel = await _archive.FindByIdAsync(input.AccountId); -/// +/// /// if (readModel == null) /// { /// throw new UseCaseFailureException("Account not found"); /// } -/// +/// /// return GetAccountSummaryOutput.Create() /// .SetAccountNumber(readModel.AccountNumber) /// .SetCustomerName(readModel.CustomerName) @@ -175,4 +175,4 @@ public interface IArchive /// should not throw an exception. /// Task DeleteAsync(TData data); -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Query/IProjection.cs b/src/EzDdd.Cqrs/Query/IProjection.cs index 2b4852c..7cb36a8 100644 --- a/src/EzDdd.Cqrs/Query/IProjection.cs +++ b/src/EzDdd.Cqrs/Query/IProjection.cs @@ -46,7 +46,7 @@ namespace EzDdd.Cqrs.Query; /// // Read model (stored in Archive) /// public record CustomerReadModel(Guid CustomerId, string Name, string Email); /// public record OrderReadModel(Guid OrderId, Guid CustomerId, decimal Total); -/// +/// /// // View model (projection output) /// public record CustomerSummaryView( /// Guid CustomerId, @@ -55,24 +55,24 @@ namespace EzDdd.Cqrs.Query; /// int OrderCount, /// decimal TotalPurchases /// ); -/// +/// /// // Projection input /// public record CustomerSummaryInput(Guid CustomerId) : IProjectionInput; -/// +/// /// // Projection implementation /// public class CustomerSummaryProjection /// : IProjection<CustomerSummaryInput, CustomerSummaryView> /// { /// private readonly IArchive<CustomerReadModel, Guid> _customerArchive; /// private readonly IArchive<OrderReadModel, Guid> _orderArchive; -/// +/// /// public async Task<CustomerSummaryView> QueryAsync(CustomerSummaryInput input) /// { /// var customer = await _customerArchive.FindByIdAsync(input.CustomerId); /// // Query orders for this customer... /// int orderCount = ...; // Count orders /// decimal totalPurchases = ...; // Sum order totals -/// +/// /// return new CustomerSummaryView( /// customer.CustomerId, /// customer.Name, @@ -82,19 +82,19 @@ namespace EzDdd.Cqrs.Query; /// ); /// } /// } -/// +/// /// // Use within query /// public class GetCustomerSummaryQuery /// : IQuery<GetCustomerSummaryInput, GetCustomerSummaryOutput> /// { /// private readonly IProjection<CustomerSummaryInput, CustomerSummaryView> _projection; -/// +/// /// public async Task<GetCustomerSummaryOutput> ExecuteAsync( /// GetCustomerSummaryInput input) /// { /// var view = await _projection.QueryAsync( /// new CustomerSummaryInput(input.CustomerId)); -/// +/// /// return GetCustomerSummaryOutput.Create() /// .SetCustomerSummary(view) /// .Succeed(); @@ -111,4 +111,4 @@ public interface IProjection /// The projection input. /// A task containing the projected view model. Task QueryAsync(TInput input); -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Query/IProjectionInput.cs b/src/EzDdd.Cqrs/Query/IProjectionInput.cs index be040d7..09688c6 100644 --- a/src/EzDdd.Cqrs/Query/IProjectionInput.cs +++ b/src/EzDdd.Cqrs/Query/IProjectionInput.cs @@ -25,7 +25,7 @@ namespace EzDdd.Cqrs.Query; /// AccountId AccountId, /// bool IncludeTransactionHistory /// ) : IProjectionInput; -/// +/// /// public class AccountSummaryProjection /// : IProjection<GetAccountSummaryProjectionInput, AccountSummaryViewModel> /// { @@ -43,4 +43,4 @@ namespace EzDdd.Cqrs.Query; public interface IProjectionInput { // Pure marker interface - no methods -} \ No newline at end of file +} diff --git a/src/EzDdd.Cqrs/Query/IQuery.cs b/src/EzDdd.Cqrs/Query/IQuery.cs index e346a2e..3e65147 100644 --- a/src/EzDdd.Cqrs/Query/IQuery.cs +++ b/src/EzDdd.Cqrs/Query/IQuery.cs @@ -57,17 +57,17 @@ namespace EzDdd.Cqrs.Query; /// : IQuery<GetAccountSummaryInput, GetAccountSummaryOutput> /// { /// private readonly IArchive<AccountSummaryReadModel, AccountId> _archive; -/// +/// /// public async Task<GetAccountSummaryOutput> ExecuteAsync( /// GetAccountSummaryInput input) /// { /// var readModel = await _archive.FindByIdAsync(input.AccountId); -/// +/// /// if (readModel == null) /// { /// throw new UseCaseFailureException("Account not found"); /// } -/// +/// /// return GetAccountSummaryOutput.Create() /// .SetAccountNumber(readModel.AccountNumber) /// .SetBalance(readModel.Balance) @@ -81,4 +81,4 @@ public interface IQuery : IUseCase where TOutput : CqrsOutput, new() { // Marker interface - no additional methods beyond IUseCase.ExecuteAsync() -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/AggregateRoot.cs b/src/EzDdd.Entity/AggregateRoot.cs index fe3c3f5..d61acaf 100644 --- a/src/EzDdd.Entity/AggregateRoot.cs +++ b/src/EzDdd.Entity/AggregateRoot.cs @@ -94,22 +94,22 @@ namespace EzDdd.Entity; /// { /// private OrderStatus _status = OrderStatus.Draft; /// private List<OrderItem> _items = new(); -/// +/// /// public Order(Guid orderId, Guid customerId) /// { /// Id = orderId; -/// +/// /// var created = new OrderCreated( /// Id: Guid.NewGuid(), /// OccurredOn: DateTimeOffset.UtcNow, /// Source: orderId.ToString(), /// CustomerId: customerId, /// Metadata: new Dictionary<string, string>()); -/// +/// /// Apply(created); // Adds event to collection, increments version /// _status = OrderStatus.Created; // State mutation (state sourcing) /// } -/// +/// /// public void AddItem(string productId, int quantity) /// { /// var itemAdded = new OrderItemAdded(/* ... */); @@ -375,4 +375,4 @@ protected void _AddDomainEvent(TEvent @event) Version++; // Increment version per event (matches Java ezddd) } } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/DomainEventTypeMapper.cs b/src/EzDdd.Entity/DomainEventTypeMapper.cs index 8d50168..7d0f0f5 100644 --- a/src/EzDdd.Entity/DomainEventTypeMapper.cs +++ b/src/EzDdd.Entity/DomainEventTypeMapper.cs @@ -1,5 +1,4 @@ using EzDdd.Common; - using uContract; namespace EzDdd.Entity; @@ -41,11 +40,11 @@ namespace EzDdd.Entity; /// DomainEventTypeMapper.Register<OrderCreated>("OrderCreated"); /// DomainEventTypeMapper.Register<OrderItemAdded>("OrderItemAdded"); /// DomainEventTypeMapper.Register<OrderCancelled>("OrderCancelled"); -/// +/// /// // Serialization - get type name from event /// var typeName = DomainEventTypeMapper.GetTypeName(orderCreatedEvent); /// // Returns: "OrderCreated" -/// +/// /// // Deserialization - get type from stored name /// var eventType = DomainEventTypeMapper.GetType("OrderCreated"); /// // Returns: typeof(OrderCreated) @@ -94,7 +93,8 @@ public static class DomainEventTypeMapper /// DomainEventTypeMapper.Register<MoneyDeposited>("MoneyDeposited"); /// /// - public static void Register(string typeName) where TEvent : IInternalDomainEvent + public static void Register(string typeName) + where TEvent : IInternalDomainEvent { Contract.Require("Type name cannot be null or empty", () => !string.IsNullOrWhiteSpace(typeName)); @@ -105,10 +105,9 @@ public static void Register(string typeName) where TEvent : IInternalDom { if (existingType != eventType) { - throw new ArgumentException - ( - $"Type name '{typeName}' is already registered to type '{existingType.Name}'. " + - $"Cannot register it to '{eventType.Name}'." + throw new ArgumentException( + $"Type name '{typeName}' is already registered to type '{existingType.Name}'. " + + $"Cannot register it to '{eventType.Name}'." ); } @@ -122,10 +121,9 @@ public static void Register(string typeName) where TEvent : IInternalDom { if (existingName != typeName) { - throw new ArgumentException - ( - $"Type '{eventType.Name}' is already registered with name '{existingName}'. " + - $"Cannot register it with name '{typeName}'." + throw new ArgumentException( + $"Type '{eventType.Name}' is already registered with name '{existingName}'. " + + $"Cannot register it with name '{typeName}'." ); } @@ -161,10 +159,9 @@ public static string GetTypeName(Type eventType) string? typeName = Mapper.Value.GetKey(eventType); if (typeName == null) { - throw new InvalidOperationException - ( - $"Event type '{eventType.Name}' is not registered. " + - $"Register it using DomainEventTypeMapper.Register<{eventType.Name}>(\"TypeName\") at application startup." + throw new InvalidOperationException( + $"Event type '{eventType.Name}' is not registered. " + + $"Register it using DomainEventTypeMapper.Register<{eventType.Name}>(\"TypeName\") at application startup." ); } @@ -211,7 +208,7 @@ public static string GetTypeName(IInternalDomainEvent @event) /// /// var eventType = DomainEventTypeMapper.GetType("OrderCreated"); /// // Returns: typeof(OrderCreated) - /// + /// /// // Use with reflection to deserialize /// var eventInstance = JsonSerializer.Deserialize(json, eventType); /// @@ -222,10 +219,9 @@ public static Type GetType(string typeName) if (!Mapper.Value.TryGetValue(typeName, out Type? type)) { - throw new InvalidOperationException - ( - $"Event type name '{typeName}' is not registered. " + - $"Available types: {string.Join(", ", Mapper.Value.Keys)}" + throw new InvalidOperationException( + $"Event type name '{typeName}' is not registered. " + + $"Available types: {string.Join(", ", Mapper.Value.Keys)}" ); } @@ -326,4 +322,4 @@ public static void Clear() { Mapper.Value.Clear(); } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/EsAggregateRoot.cs b/src/EzDdd.Entity/EsAggregateRoot.cs index 35096c4..53f3df6 100644 --- a/src/EzDdd.Entity/EsAggregateRoot.cs +++ b/src/EzDdd.Entity/EsAggregateRoot.cs @@ -71,23 +71,23 @@ namespace EzDdd.Entity; /// { /// private string _owner = string.Empty; /// private decimal _balance; -/// +/// /// // Constructor for new aggregate /// public Account(Guid id, string owner, decimal initialBalance) /// { /// var created = new AccountCreated(/* ... */); /// Apply(created); // Enforces R1 /// } -/// +/// /// // Constructor for event replay (REQUIRED) /// public Account(IEnumerable<IInternalDomainEvent> events) : base(events) { } -/// +/// /// public void Deposit(decimal amount) /// { /// var deposited = new MoneyDeposited(/* ... */); /// Apply(deposited); // Enforces R2 /// } -/// +/// /// protected override void When(IInternalDomainEvent @event) /// { /// switch (@event) @@ -102,14 +102,14 @@ namespace EzDdd.Entity; /// break; /// } /// } -/// +/// /// protected override void EnsureInvariant() /// { /// if (IsDeleted) return; /// if (_balance < 0) /// throw new InvalidOperationException("Balance cannot be negative"); /// } -/// +/// /// public override string GetCategory() => "account"; /// } /// @@ -124,9 +124,7 @@ public abstract class EsAggregateRoot : AggregateRoot /// Subclasses should call this default constructor, then apply a construction event /// implementing . /// - protected EsAggregateRoot() - { - } + protected EsAggregateRoot() { } /// /// Initializes an aggregate by replaying events from history. @@ -259,19 +257,19 @@ public sealed override void Apply(TEvent @event) /// _owner = e.Owner; /// _balance = e.InitialBalance; /// break; - /// + /// /// case MoneyDeposited e: /// _balance += e.Amount; /// break; - /// + /// /// case MoneyWithdrawn e: /// _balance -= e.Amount; /// break; - /// + /// /// case AccountClosed e: /// IsDeleted = true; /// break; - /// + /// /// default: /// throw new InvalidOperationException( /// $"Unknown event type: {@event.GetType().Name}"); @@ -337,14 +335,14 @@ public sealed override void Apply(TEvent @event) /// { /// // Skip invariant checks for deleted aggregates /// if (IsDeleted) return; - /// + /// /// // Check business rules /// if (Id == Guid.Empty) /// throw new InvalidOperationException("Account ID must be set"); - /// + /// /// if (string.IsNullOrEmpty(_owner)) /// throw new InvalidOperationException("Account must have owner"); - /// + /// /// if (_balance < 0) /// throw new InvalidOperationException("Balance cannot be negative"); /// } @@ -449,4 +447,4 @@ public string GetStreamName() { return $"{GetCategory()}-{Id}"; } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/IDomainEvent.cs b/src/EzDdd.Entity/IDomainEvent.cs index cc93eb4..604f760 100644 --- a/src/EzDdd.Entity/IDomainEvent.cs +++ b/src/EzDdd.Entity/IDomainEvent.cs @@ -120,4 +120,4 @@ public interface IDomainEvent /// A read-only dictionary of metadata key-value pairs (both keys and values are strings). /// IReadOnlyDictionary Metadata { get; } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/IDomainEventSource.cs b/src/EzDdd.Entity/IDomainEventSource.cs index ac69e86..7bc755f 100644 --- a/src/EzDdd.Entity/IDomainEventSource.cs +++ b/src/EzDdd.Entity/IDomainEventSource.cs @@ -31,13 +31,14 @@ namespace EzDdd.Entity; /// { /// // Event sourcing implementation... /// } -/// +/// /// // Usage: polymorphic event source /// IDomainEventSource<IInternalDomainEvent> eventSource = aggregateRoot; /// IReadOnlyList<IInternalDomainEvent> events = eventSource.GetDomainEvents(); /// /// -public interface IDomainEventSource where TEvent : class, IInternalDomainEvent +public interface IDomainEventSource + where TEvent : class, IInternalDomainEvent { /// /// Applies a domain event to the aggregate, adding it to the pending events collection. @@ -81,4 +82,4 @@ public interface IDomainEventSource where TEvent : class, IInternalDomai /// /// The count of events in the pending events collection. int GetDomainEventSize(); -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/IEntity.cs b/src/EzDdd.Entity/IEntity.cs index 3123bb2..9817220 100644 --- a/src/EzDdd.Entity/IEntity.cs +++ b/src/EzDdd.Entity/IEntity.cs @@ -37,11 +37,11 @@ namespace EzDdd.Entity; /// public Guid Id { get; private set; } /// public string CustomerName { get; private set; } /// public decimal TotalAmount { get; private set; } -/// +/// /// // Entity equality based on ID /// public override bool Equals(object? obj) => /// obj is Order other && Id == other.Id; -/// +/// /// public override int GetHashCode() => Id.GetHashCode(); /// } /// @@ -56,4 +56,4 @@ public interface IEntity /// of the same type. /// TId Id { get; } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/IInternalDomainEvent.cs b/src/EzDdd.Entity/IInternalDomainEvent.cs index 68db4bd..5586b45 100644 --- a/src/EzDdd.Entity/IInternalDomainEvent.cs +++ b/src/EzDdd.Entity/IInternalDomainEvent.cs @@ -37,7 +37,7 @@ namespace EzDdd.Entity; /// decimal InitialBalance, /// IReadOnlyDictionary<string, string> Metadata /// ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; -/// +/// /// // Command event (R2 rule - middle events) /// public record MoneyDeposited( /// Guid Id, @@ -46,7 +46,7 @@ namespace EzDdd.Entity; /// decimal Amount, /// IReadOnlyDictionary<string, string> Metadata /// ) : IInternalDomainEvent; -/// +/// /// // Destruction event (R3 rule - last event) /// public record AccountClosed( /// Guid Id, @@ -135,4 +135,4 @@ interface IDestructionEvent // Pure marker interface - no additional members // Semantic meaning: This event is the last in aggregate lifecycle } -} \ No newline at end of file +} diff --git a/src/EzDdd.Entity/IValueObject.cs b/src/EzDdd.Entity/IValueObject.cs index 073ab9b..7bdc206 100644 --- a/src/EzDdd.Entity/IValueObject.cs +++ b/src/EzDdd.Entity/IValueObject.cs @@ -37,25 +37,25 @@ namespace EzDdd.Entity; /// /// // Recommended: Record-based value object (structural equality + immutability) /// public record Money(decimal Amount, string Currency) : IValueObject; -/// +/// /// // Usage: /// var money1 = new Money(100m, "USD"); /// var money2 = new Money(100m, "USD"); /// Assert.Equal(money1, money2); // true - structural equality -/// +/// /// // Alternative: Class-based value object (manual equality) /// public class Email : IValueObject /// { /// public string Value { get; } -/// +/// /// public Email(string value) /// { /// Value = value; /// } -/// +/// /// public override bool Equals(object? obj) => /// obj is Email other && Value == other.Value; -/// +/// /// public override int GetHashCode() => Value.GetHashCode(); /// } /// @@ -64,4 +64,4 @@ public interface IValueObject { // Pure marker interface - zero methods // Implementations should provide structural equality and immutability -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Exceptions/PostEventFailureException.cs b/src/EzDdd.UseCase/Exceptions/PostEventFailureException.cs index bbc6bc7..d31b0d7 100644 --- a/src/EzDdd.UseCase/Exceptions/PostEventFailureException.cs +++ b/src/EzDdd.UseCase/Exceptions/PostEventFailureException.cs @@ -50,9 +50,7 @@ public class PostEventFailureException : Exception /// Initializes a new instance of the class. /// public PostEventFailureException() - : base("Failed to post message to message producer.") - { - } + : base("Failed to post message to message producer.") { } /// /// Initializes a new instance of the class @@ -60,9 +58,7 @@ public PostEventFailureException() /// /// The error message that explains the reason for the exception. public PostEventFailureException(string message) - : base(message) - { - } + : base(message) { } /// /// Initializes a new instance of the class @@ -74,7 +70,5 @@ public PostEventFailureException(string message) /// (typically from the message broker client). /// public PostEventFailureException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file + : base(message, innerException) { } +} diff --git a/src/EzDdd.UseCase/Exceptions/RepositoryPeerSaveException.cs b/src/EzDdd.UseCase/Exceptions/RepositoryPeerSaveException.cs index 30a5bbb..b06e0a6 100644 --- a/src/EzDdd.UseCase/Exceptions/RepositoryPeerSaveException.cs +++ b/src/EzDdd.UseCase/Exceptions/RepositoryPeerSaveException.cs @@ -42,7 +42,7 @@ namespace EzDdd.UseCase.Exceptions; /// { /// throw new RepositoryPeerSaveException("Database concurrency error", ex); /// } -/// +/// /// // In IRepository implementation (Domain layer) /// try /// { @@ -59,18 +59,15 @@ public class RepositoryPeerSaveException : Exception /// /// Initializes a new instance of the class. /// - public RepositoryPeerSaveException() - { - } + public RepositoryPeerSaveException() { } /// /// Initializes a new instance of the class /// with a specified error message. /// /// The error message that explains the reason for the exception. - public RepositoryPeerSaveException(string message) : base(message) - { - } + public RepositoryPeerSaveException(string message) + : base(message) { } /// /// Initializes a new instance of the class @@ -81,9 +78,7 @@ public RepositoryPeerSaveException(string message) : base(message) /// The inner exception's message is used as this exception's message. /// public RepositoryPeerSaveException(Exception innerException) - : base(innerException.Message, innerException) - { - } + : base(innerException.Message, innerException) { } /// /// Initializes a new instance of the class @@ -94,7 +89,5 @@ public RepositoryPeerSaveException(Exception innerException) /// The exception that is the cause of the current exception, or null if no inner exception is specified. /// public RepositoryPeerSaveException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file + : base(message, innerException) { } +} diff --git a/src/EzDdd.UseCase/Exceptions/RepositorySaveException.cs b/src/EzDdd.UseCase/Exceptions/RepositorySaveException.cs index cae47b4..dbe6151 100644 --- a/src/EzDdd.UseCase/Exceptions/RepositorySaveException.cs +++ b/src/EzDdd.UseCase/Exceptions/RepositorySaveException.cs @@ -60,18 +60,15 @@ public class RepositorySaveException : Exception /// /// Initializes a new instance of the class. /// - public RepositorySaveException() - { - } + public RepositorySaveException() { } /// /// Initializes a new instance of the class /// with a specified error message. /// /// The error message that explains the reason for the exception. - public RepositorySaveException(string message) : base(message) - { - } + public RepositorySaveException(string message) + : base(message) { } /// /// Initializes a new instance of the class @@ -82,9 +79,7 @@ public RepositorySaveException(string message) : base(message) /// The inner exception's message is used as this exception's message. /// public RepositorySaveException(Exception innerException) - : base(innerException.Message, innerException) - { - } + : base(innerException.Message, innerException) { } /// /// Initializes a new instance of the class @@ -95,7 +90,5 @@ public RepositorySaveException(Exception innerException) /// The exception that is the cause of the current exception, or null if no inner exception is specified. /// public RepositorySaveException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file + : base(message, innerException) { } +} diff --git a/src/EzDdd.UseCase/Exceptions/UseCaseFailureException.cs b/src/EzDdd.UseCase/Exceptions/UseCaseFailureException.cs index 0b51d24..f083198 100644 --- a/src/EzDdd.UseCase/Exceptions/UseCaseFailureException.cs +++ b/src/EzDdd.UseCase/Exceptions/UseCaseFailureException.cs @@ -8,18 +8,14 @@ public class UseCaseFailureException : Exception /// /// Initializes a new instance of the class. /// - public UseCaseFailureException() - { - } + public UseCaseFailureException() { } /// /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. public UseCaseFailureException(string message) - : base(message) - { - } + : base(message) { } /// /// Initializes a new instance of the class with a specified error message @@ -28,7 +24,5 @@ public UseCaseFailureException(string message) /// The error message that explains the reason for the exception. /// The exception that is the cause of the current exception. public UseCaseFailureException(string message, Exception innerException) - : base(message, innerException) - { - } -} \ No newline at end of file + : base(message, innerException) { } +} diff --git a/src/EzDdd.UseCase/Port/In/ExitCode.cs b/src/EzDdd.UseCase/Port/In/ExitCode.cs index 2e0bfde..12b8fe5 100644 --- a/src/EzDdd.UseCase/Port/In/ExitCode.cs +++ b/src/EzDdd.UseCase/Port/In/ExitCode.cs @@ -13,7 +13,7 @@ public enum ExitCode /// /// Indicates failed execution. /// - Failure = 1 + Failure = 1, } /// @@ -30,4 +30,4 @@ public static int Code(this ExitCode exitCode) { return (int)exitCode; } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/In/IInput.cs b/src/EzDdd.UseCase/Port/In/IInput.cs index 38054c6..e580699 100644 --- a/src/EzDdd.UseCase/Port/In/IInput.cs +++ b/src/EzDdd.UseCase/Port/In/IInput.cs @@ -18,4 +18,4 @@ static NullInput OfNull() /// Null object implementation of . /// sealed record NullInput : IInput; -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/In/IOutput.cs b/src/EzDdd.UseCase/Port/In/IOutput.cs index 4e1dec0..c3075a1 100644 --- a/src/EzDdd.UseCase/Port/In/IOutput.cs +++ b/src/EzDdd.UseCase/Port/In/IOutput.cs @@ -52,4 +52,4 @@ public interface IOutput /// The identifier to set. /// This output instance for fluent API. IOutput SetId(string id); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/In/IReconciler.cs b/src/EzDdd.UseCase/Port/In/IReconciler.cs index e57b08c..73f4503 100644 --- a/src/EzDdd.UseCase/Port/In/IReconciler.cs +++ b/src/EzDdd.UseCase/Port/In/IReconciler.cs @@ -43,7 +43,5 @@ public sealed class NullContext /// /// Prevents external instantiation. Use instead. /// - private NullContext() - { - } -} \ No newline at end of file + private NullContext() { } +} diff --git a/src/EzDdd.UseCase/Port/In/IUseCase.cs b/src/EzDdd.UseCase/Port/In/IUseCase.cs index c48498e..9ea366b 100644 --- a/src/EzDdd.UseCase/Port/In/IUseCase.cs +++ b/src/EzDdd.UseCase/Port/In/IUseCase.cs @@ -18,4 +18,4 @@ public interface IUseCase /// Thrown when the use case cannot fulfill its specifications. /// Task ExecuteAsync(TInput input); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/In/IVersionedInput.cs b/src/EzDdd.UseCase/Port/In/IVersionedInput.cs index 643efaf..168d5e6 100644 --- a/src/EzDdd.UseCase/Port/In/IVersionedInput.cs +++ b/src/EzDdd.UseCase/Port/In/IVersionedInput.cs @@ -10,4 +10,4 @@ public interface IVersionedInput : IInput /// Gets or sets the aggregate root version. /// long Version { get; set; } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs b/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs index eadebf4..940f8a3 100644 --- a/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs +++ b/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs @@ -42,7 +42,7 @@ namespace EzDdd.UseCase.Port.InOut; /// // Create event data /// var eventJson = JsonSerializer.SerializeToUtf8Bytes(new { Amount = 100 }); /// var metadataJson = JsonSerializer.SerializeToUtf8Bytes(new { User = "admin" }); -/// +/// /// var data = new DomainEventData( /// Guid.NewGuid(), /// "MoneyDeposited", @@ -50,21 +50,14 @@ namespace EzDdd.UseCase.Port.InOut; /// eventJson, /// metadataJson /// ); -/// +/// /// // JSON-aware equality: key order doesn't matter /// var data1 = new DomainEventData(..., UTF8("{\"a\":1,\"b\":2}"), ...); /// var data2 = new DomainEventData(..., UTF8("{\"b\":2,\"a\":1}"), ...); /// Assert.Equal(data1, data2); // true (same JSON content) /// /// -public record DomainEventData -( - Guid Id, - string EventType, - string ContentType, - byte[] EventBody, - byte[] UserMetadata -) +public record DomainEventData(Guid Id, string EventType, string ContentType, byte[] EventBody, byte[] UserMetadata) { /// /// Custom equality comparison that uses JSON-aware semantic comparison for JSON content. @@ -98,11 +91,11 @@ public virtual bool Equals(DomainEventData? other) return false; } - return Id == other.Id && - EventType == other.EventType && - ContentType == other.ContentType && - _JsonEquals(EventBody, other.EventBody) && - _JsonEquals(UserMetadata, other.UserMetadata); + return Id == other.Id + && EventType == other.EventType + && ContentType == other.ContentType + && _JsonEquals(EventBody, other.EventBody) + && _JsonEquals(UserMetadata, other.UserMetadata); } /// @@ -268,4 +261,4 @@ private static bool _JsonArrayEquals(JsonElement left, JsonElement right) return true; } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/DomainEventDataBuilder.cs b/src/EzDdd.UseCase/Port/InOut/DomainEventDataBuilder.cs index c91d9fd..5dafb2a 100644 --- a/src/EzDdd.UseCase/Port/InOut/DomainEventDataBuilder.cs +++ b/src/EzDdd.UseCase/Port/InOut/DomainEventDataBuilder.cs @@ -67,20 +67,20 @@ namespace EzDdd.UseCase.Port.InOut; /// // .EventId(...) optional - auto-generates Guid.NewGuid() /// // .MetadataAsJson(...) optional - defaults to "{}" /// .Build(); -/// +/// /// // Production code: With all options /// var eventData = DomainEventDataBuilder /// .Json("MoneyDeposited", depositEvent) /// .EventId(Guid.Parse("...")) /// .MetadataAsJson(new { CorrelationId = "123", UserId = "user@example.com" }) /// .Build(); -/// +/// /// // Binary payload support /// var eventData = DomainEventDataBuilder /// .Binary("LegacyEvent", avroBytes) /// .MetadataAsBytes(metadataBytes) /// .Build(); -/// +/// /// // Alternative: Direct construction (test code, full control) /// var eventData = new DomainEventData( /// Guid.NewGuid(), @@ -102,9 +102,7 @@ public class DomainEventDataBuilder /// /// Private constructor - use factory methods or . /// - private DomainEventDataBuilder() - { - } + private DomainEventDataBuilder() { } /// /// Creates a builder for JSON-serialized event data. @@ -159,7 +157,12 @@ public static DomainEventDataBuilder Binary(string eventType, byte[] payload) ArgumentNullException.ThrowIfNull(eventType); ArgumentNullException.ThrowIfNull(payload); - DomainEventDataBuilder builder = new() { _eventType = eventType, _eventBody = payload, _contentType = "application/octet-stream" }; + DomainEventDataBuilder builder = new() + { + _eventType = eventType, + _eventBody = payload, + _contentType = "application/octet-stream", + }; return builder; } @@ -249,13 +252,6 @@ public DomainEventData Build() Guid eventId = _id ?? Guid.NewGuid(); byte[] userMetadata = _metadata ?? "{}"u8.ToArray(); - return new DomainEventData - ( - eventId, - _eventType, - _contentType, - _eventBody, - userMetadata - ); + return new DomainEventData(eventId, _eventType, _contentType, _eventBody, userMetadata); } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/DomainEventMapper.cs b/src/EzDdd.UseCase/Port/InOut/DomainEventMapper.cs index a75e143..17fe125 100644 --- a/src/EzDdd.UseCase/Port/InOut/DomainEventMapper.cs +++ b/src/EzDdd.UseCase/Port/InOut/DomainEventMapper.cs @@ -1,5 +1,4 @@ using System.Text.Json; - using EzDdd.Entity; namespace EzDdd.UseCase.Port.InOut; @@ -42,11 +41,11 @@ namespace EzDdd.UseCase.Port.InOut; /// // Application startup - register event types /// DomainEventTypeMapper.Register<AccountCreated>("AccountCreated"); /// DomainEventTypeMapper.Register<MoneyDeposited>("MoneyDeposited"); -/// +/// /// // Convert event to data (for persistence) /// var @event = new AccountCreated(...); /// var data = DomainEventMapper.ToData(@event); -/// +/// /// // Convert data back to event (from persistence) /// var reconstructedEvent = DomainEventMapper.ToDomain<AccountCreated>(data); /// @@ -77,7 +76,8 @@ public static DomainEventData ToData(IInternalDomainEvent @event) /// The persisted event data /// The reconstructed domain event /// Thrown when deserialization fails or event type is not registered - public static T ToDomain(DomainEventData data) where T : IInternalDomainEvent + public static T ToDomain(DomainEventData data) + where T : IInternalDomainEvent { ArgumentNullException.ThrowIfNull(data); @@ -86,10 +86,7 @@ public static T ToDomain(DomainEventData data) where T : IInternalDomainEvent if (@event is not T typedEvent) { - throw new InvalidOperationException - ( - $"Failed to cast event of type {eventClass.Name} to {typeof(T).Name}" - ); + throw new InvalidOperationException($"Failed to cast event of type {eventClass.Name} to {typeof(T).Name}"); } return typedEvent; @@ -140,4 +137,4 @@ private static object _DeserializeFromJson(byte[] json, Type type) throw new InvalidOperationException($"Deserialization failed: {ex.Message}", ex); } } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/IExternalDomainEvent.cs b/src/EzDdd.UseCase/Port/InOut/IExternalDomainEvent.cs index 981d9c3..eaf06f3 100644 --- a/src/EzDdd.UseCase/Port/InOut/IExternalDomainEvent.cs +++ b/src/EzDdd.UseCase/Port/InOut/IExternalDomainEvent.cs @@ -26,7 +26,7 @@ namespace EzDdd.UseCase.Port.InOut; /// /// // Internal event (aggregate lifecycle) /// public record AccountCreated(...) : InternalDomainEvent, IConstructionEvent; -/// +/// /// // External event (cross-context integration) /// public record CustomerRegistered(...) : ExternalDomainEvent; /// @@ -39,4 +39,4 @@ public interface IExternalDomainEvent : IDomainEvent { // Marker interface - no additional methods // Inherits: Id, OccurredOn, Source, Metadata from IDomainEvent -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs b/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs index bc92233..d69db7b 100644 --- a/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs +++ b/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs @@ -73,7 +73,7 @@ namespace EzDdd.UseCase.Port.InOut; /// new Dictionary<string, string> { ["userId"] = "user-1" }, /// 500m /// ); -/// +/// /// var eventData = new { aggregateId = "account-123", amount = domainEvent.Amount }; /// var dto = new InternalDomainEventDto /// { @@ -84,7 +84,7 @@ namespace EzDdd.UseCase.Port.InOut; /// JsonEvent = JsonSerializer.Serialize(eventData), /// Metadata = domainEvent.Metadata /// }; -/// +/// /// // Serialize to JSON for REST API /// var json = JsonSerializer.Serialize(dto); /// @@ -140,4 +140,4 @@ public class InternalDomainEventDto /// /// public Dictionary Metadata { get; set; } = new(); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs b/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs index 73b0e0e..6a1c5c1 100644 --- a/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs +++ b/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs @@ -21,7 +21,8 @@ namespace EzDdd.UseCase.Port.InOut.Messaging; /// /// /// The type of external domain event this publisher publishes. -public interface IExternalDomainEventPublisher where TEvent : IExternalDomainEvent +public interface IExternalDomainEventPublisher + where TEvent : IExternalDomainEvent { /// /// Publishes the given external domain event to an external system asynchronously. diff --git a/src/EzDdd.UseCase/Port/Out/EsRepository.cs b/src/EzDdd.UseCase/Port/Out/EsRepository.cs index 6e9e2a0..91b7cc0 100644 --- a/src/EzDdd.UseCase/Port/Out/EsRepository.cs +++ b/src/EzDdd.UseCase/Port/Out/EsRepository.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Reflection; - using EzDdd.Entity; using EzDdd.UseCase.Exceptions; using EzDdd.UseCase.Port.InOut; @@ -51,12 +50,12 @@ namespace EzDdd.UseCase.Port.Out; /// // Create repository with event store peer /// var eventStorePeer = new PostgresEventStorePeer(); /// var repository = new EsRepository<BankAccount, AccountId>(eventStorePeer); -/// +/// /// // Save aggregate (stores events) /// var account = new BankAccount(accountId, "John Doe", 1000m); /// account.Deposit(500m); /// await repository.SaveAsync(account); -/// +/// /// // Load aggregate (reconstructs from events) /// var loaded = await repository.FindByIdAsync(accountId); /// // loaded.Balance == 1500m (reconstructed by replaying events) @@ -136,7 +135,7 @@ public EsRepository(IRepositoryPeer, TId> peer) /// /// var repository = new EsRepository<Order, OrderId>(eventStorePeer); /// var order = await repository.FindByIdAsync(orderId); - /// + /// /// if (order != null) /// { /// // Order reconstructed from its event stream @@ -206,7 +205,7 @@ public EsRepository(IRepositoryPeer, TId> peer) /// var account = new BankAccount(accountId, "Jane Doe", 500m); /// account.Deposit(200m); // Generates MoneyDeposited event /// account.Withdraw(100m); // Generates MoneyWithdrawn event - /// + /// /// try /// { /// await repository.SaveAsync(account); @@ -308,12 +307,11 @@ private static TAggregate _ReconstructAggregate(IReadOnlyList + ConstructorInfo constructor = ConstructorCache.GetOrAdd( + aggregateType, + type => { - ConstructorInfo? ctor = type.GetConstructor - ( + ConstructorInfo? ctor = type.GetConstructor( BindingFlags.Public | BindingFlags.Instance, null, [typeof(IEnumerable)], @@ -322,10 +320,9 @@ private static TAggregate _ReconstructAggregate(IReadOnlyList. " + - $"This constructor is required for event sourcing to reconstruct aggregates from event streams." + throw new InvalidOperationException( + $"{type.Name} must have a public constructor accepting IEnumerable. " + + $"This constructor is required for event sourcing to reconstruct aggregates from event streams." ); } @@ -339,12 +336,11 @@ private static TAggregate _ReconstructAggregate(IReadOnlyList @@ -121,7 +121,7 @@ public class EventStoreData : IStoreData /// new MoneyWithdrawn(...) // New event 2 /// } /// }; - /// + /// /// // Expected version after save: 5 + 2 = 7 /// var expectedVersion = data.GetOptimisticLockVersion(); // Returns 7 /// @@ -130,4 +130,4 @@ public long GetOptimisticLockVersion() { return Version + Events.Count; } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/EventStoreMapper.cs b/src/EzDdd.UseCase/Port/Out/EventStoreMapper.cs index ab7cec3..9129f0a 100644 --- a/src/EzDdd.UseCase/Port/Out/EventStoreMapper.cs +++ b/src/EzDdd.UseCase/Port/Out/EventStoreMapper.cs @@ -41,10 +41,10 @@ namespace EzDdd.UseCase.Port.Out; /// // Saving an aggregate (uses ToData) /// var aggregate = new BankAccount(accountId, "John Doe", 1000m); /// aggregate.Deposit(500m); -/// +/// /// var data = EventStoreMapper.ToData(aggregate); /// await eventStorePeer.SaveAsync(data); -/// +/// /// // Loading an aggregate (does NOT use ToDomain - uses constructor instead) /// var storedData = await eventStorePeer.FindByIdAsync(accountId); /// var reconstructed = new BankAccount(storedData.Events); // Event replay constructor @@ -89,7 +89,7 @@ public static class EventStoreMapper /// var account = new BankAccount(accountId, "Jane Doe", 500m); /// account.Deposit(200m); /// account.Withdraw(100m); - /// + /// /// var data = EventStoreMapper.ToData(account); /// // data.Events contains: [AccountCreated, MoneyDeposited, MoneyWithdrawn] /// // data.StreamName is "account-{accountId}" @@ -105,7 +105,7 @@ public static EventStoreData ToData(EsAggregateRoot ToData(EsAggregateRoot /// // ❌ WRONG: Attempting to use mapper (will throw) /// var aggregate = EventStoreMapper.ToDomain<BankAccount, AccountId>(data); - /// + /// /// // ✅ CORRECT: Use event replay constructor /// var aggregate = new BankAccount(data.Events); /// @@ -160,10 +160,9 @@ public static EventStoreData ToData(EsAggregateRoot(EventStoreData data) where T : EsAggregateRoot { - throw new NotSupportedException - ( - "Event sourcing aggregates are reconstructed from events, not from EventStoreData. " + - "Use the aggregate's event replay constructor instead: new T(data.Events)" + throw new NotSupportedException( + "Event sourcing aggregates are reconstructed from events, not from EventStoreData. " + + "Use the aggregate's event replay constructor instead: new T(data.Events)" ); } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/IOutboxData.cs b/src/EzDdd.UseCase/Port/Out/IOutboxData.cs index 97bbb5c..745d9fa 100644 --- a/src/EzDdd.UseCase/Port/Out/IOutboxData.cs +++ b/src/EzDdd.UseCase/Port/Out/IOutboxData.cs @@ -36,7 +36,7 @@ namespace EzDdd.UseCase.Port.Out; /// public long Version { get; set; } /// public List<IDomainEvent> Events { get; set; } /// public string StreamName { get; set; } -/// +/// /// // State fields (specific to BankAccount aggregate) /// public string Owner { get; set; } /// public decimal Balance { get; set; } @@ -51,4 +51,4 @@ public interface IOutboxData : IStoreData // - List Events { get; set; } // - string StreamName { get; set; } // - long GetOptimisticLockVersion() -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/IRepository.cs b/src/EzDdd.UseCase/Port/Out/IRepository.cs index 89f8888..5eca61f 100644 --- a/src/EzDdd.UseCase/Port/Out/IRepository.cs +++ b/src/EzDdd.UseCase/Port/Out/IRepository.cs @@ -63,17 +63,17 @@ namespace EzDdd.UseCase.Port.Out; /// public class DepositMoneyCommand : ICommand<DepositInput, DepositOutput> /// { /// private readonly IRepository<BankAccount, AccountId> _repository; -/// +/// /// public async Task<DepositOutput> ExecuteAsync(DepositInput input) /// { /// // 1. Load aggregate from repository /// var account = await _repository.FindByIdAsync(input.AccountId); /// if (account == null) /// return new DepositOutput { ExitCode = ExitCode.ResourceNotFoundFailure }; -/// +/// /// // 2. Execute domain logic /// account.Deposit(input.Amount); -/// +/// /// // 3. Save aggregate (may throw RepositorySaveException) /// try /// { @@ -84,7 +84,7 @@ namespace EzDdd.UseCase.Port.Out; /// { /// return new DepositOutput { ExitCode = ExitCode.ConflictFailure }; /// } -/// +/// /// return new DepositOutput { ExitCode = ExitCode.Success }; /// } /// } @@ -251,11 +251,11 @@ public interface IRepository /// { /// // Perform domain logic for deletion (e.g., check balances) /// account.Close(); - /// + /// /// // Delete aggregate /// await _repository.DeleteAsync(account); /// } /// /// Task DeleteAsync(TAggregate aggregate); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/IRepositoryPeer.cs b/src/EzDdd.UseCase/Port/Out/IRepositoryPeer.cs index f7162b1..ea573a6 100644 --- a/src/EzDdd.UseCase/Port/Out/IRepositoryPeer.cs +++ b/src/EzDdd.UseCase/Port/Out/IRepositoryPeer.cs @@ -94,13 +94,13 @@ namespace EzDdd.UseCase.Port.Out; /// { /// // 1. Save aggregate state /// _dbContext.Aggregates.Update(data); -/// +/// /// // 2. Save events (Transactional Outbox) /// foreach (var @event in data.Events) /// { /// _dbContext.OutboxEvents.Add(new OutboxEventEntity(@event)); /// } -/// +/// /// // 3. Commit atomically /// await _dbContext.SaveChangesAsync(); /// await transaction.CommitAsync(); @@ -111,7 +111,7 @@ namespace EzDdd.UseCase.Port.Out; /// throw new RepositoryPeerSaveException("Optimistic locking failure", ex); /// } /// } -/// +/// /// // Option 2: TransactionScope /// using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); /// // ... save operations ... @@ -126,13 +126,13 @@ namespace EzDdd.UseCase.Port.Out; /// public class SqlBankAccountRepositoryPeer : IRepositoryPeer<BankAccountData, AccountId> /// { /// private readonly ApplicationDbContext _dbContext; -/// +/// /// public async Task<BankAccountData?> FindByIdAsync(AccountId id) /// { /// return await _dbContext.BankAccounts /// .FirstOrDefaultAsync(a => a.Id == id); /// } -/// +/// /// public async Task SaveAsync(BankAccountData data) /// { /// using var transaction = await _dbContext.Database.BeginTransactionAsync(); @@ -143,7 +143,7 @@ namespace EzDdd.UseCase.Port.Out; /// _dbContext.BankAccounts.Add(data); /// else /// _dbContext.BankAccounts.Update(data); -/// +/// /// // Store events in outbox /// foreach (var @event in data.Events) /// { @@ -155,7 +155,7 @@ namespace EzDdd.UseCase.Port.Out; /// OccurredOn = @event.OccurredOn /// }); /// } -/// +/// /// await _dbContext.SaveChangesAsync(); /// await transaction.CommitAsync(); /// } @@ -170,7 +170,7 @@ namespace EzDdd.UseCase.Port.Out; /// throw new RepositoryPeerSaveException("Database save failed", ex); /// } /// } -/// +/// /// public async Task DeleteAsync(BankAccountData data) /// { /// _dbContext.BankAccounts.Remove(data); @@ -308,4 +308,4 @@ public interface IRepositoryPeer /// /// Task DeleteAsync(TData data); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/IStoreData.cs b/src/EzDdd.UseCase/Port/Out/IStoreData.cs index 07763fa..6091489 100644 --- a/src/EzDdd.UseCase/Port/Out/IStoreData.cs +++ b/src/EzDdd.UseCase/Port/Out/IStoreData.cs @@ -73,10 +73,10 @@ public interface IStoreData /// // New aggregate /// Version = -1, Events = [CreatedEvent] /// OptimisticLockVersion = -1 + 1 = 0 (expected version after save) - /// + /// /// // After save /// Version = 0, Events = [] (cleared) - /// + /// /// // After command /// Version = 0, Events = [CommandEvent] /// OptimisticLockVersion = 0 + 1 = 1 (expected version after save) @@ -90,4 +90,4 @@ long GetOptimisticLockVersion() { return Version + Events.Count; } -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/OutboxMapper.cs b/src/EzDdd.UseCase/Port/Out/OutboxMapper.cs index 40f9dec..48f87db 100644 --- a/src/EzDdd.UseCase/Port/Out/OutboxMapper.cs +++ b/src/EzDdd.UseCase/Port/Out/OutboxMapper.cs @@ -58,7 +58,7 @@ namespace EzDdd.UseCase.Port.Out; /// aggregate.Balance // State field /// ); /// } -/// +/// /// public override BankAccount ToDomain(BankAccountData data) /// { /// return new BankAccount( @@ -133,4 +133,4 @@ public abstract class OutboxMapper /// /// public abstract TAggregate ToDomain(TData data); -} \ No newline at end of file +} diff --git a/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs b/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs index 1a9181f..92a1292 100644 --- a/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs +++ b/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs @@ -61,14 +61,14 @@ namespace EzDdd.UseCase.Port.Out; /// var peer = new SqlRepositoryPeer(); /// var mapper = new BankAccountMapper(); /// var repository = new OutboxRepository<BankAccount, BankAccountData, AccountId>(peer, mapper); -/// +/// /// // Load aggregate /// var account = await repository.FindByIdAsync(accountId); /// if (account != null) /// { /// // Modify aggregate /// account.Deposit(100.00m); -/// +/// /// // Save (atomically persists state + events) /// await repository.SaveAsync(account); /// } @@ -92,11 +92,7 @@ public class OutboxRepository : IRepository /// - public OutboxRepository - ( - IRepositoryPeer peer, - OutboxMapper mapper - ) + public OutboxRepository(IRepositoryPeer peer, OutboxMapper mapper) { ArgumentNullException.ThrowIfNull(peer); ArgumentNullException.ThrowIfNull(mapper); @@ -239,4 +235,4 @@ public async Task DeleteAsync(TAggregate aggregate) TData data = _mapper.ToData(aggregate); await _peer.DeleteAsync(data); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Common.Tests/BiMapTests.cs b/tests/EzDdd.Common.Tests/BiMapTests.cs index c94f4af..bca213e 100644 --- a/tests/EzDdd.Common.Tests/BiMapTests.cs +++ b/tests/EzDdd.Common.Tests/BiMapTests.cs @@ -2,7 +2,7 @@ namespace EzDdd.Common.Tests; public class BiMapTests { -#region Add and Construction Tests + #region Add and Construction Tests [Fact] public void Add_AndGet_WorksBidirectionally() @@ -56,9 +56,9 @@ public void Constructor_WhenProvidingCapacity_CreatesEmptyBiMap() Assert.Empty(biMap); } -#endregion + #endregion -#region TryGetValue and GetKey Tests + #region TryGetValue and GetKey Tests [Fact] public void TryGetValue_WhenKeyExists_ReturnsTrue() @@ -115,9 +115,9 @@ public void GetKey_WhenValueNotExists_ReturnsNull() Assert.Null(key); } -#endregion + #endregion -#region Remove Tests + #region Remove Tests [Fact] public void Remove_RemovesBidirectionally() @@ -147,9 +147,9 @@ public void Remove_WhenUsingKeyValuePair_RemovesOnlyIfMatches() Assert.Empty(biMap); } -#endregion + #endregion -#region Contains Tests + #region Contains Tests [Fact] public void Contains_WhenUsingKeyValuePair_ReturnsCorrectResult() @@ -161,14 +161,19 @@ public void Contains_WhenUsingKeyValuePair_ReturnsCorrectResult() Assert.DoesNotContain(new KeyValuePair("other", 42), biMap); } -#endregion + #endregion -#region Clear Tests + #region Clear Tests [Fact] public void Clear_RemovesAllMappings() { - BiMap biMap = new() { { "one", 1 }, { "two", 2 }, { "three", 3 } }; + BiMap biMap = new() + { + { "one", 1 }, + { "two", 2 }, + { "three", 3 }, + }; biMap.Clear(); @@ -177,14 +182,19 @@ public void Clear_RemovesAllMappings() Assert.Null(biMap.GetKey(1)); } -#endregion + #endregion -#region Properties Tests + #region Properties Tests [Fact] public void GetEnumerator_ReturnsAllEntries() { - BiMap biMap = new() { { "one", 1 }, { "two", 2 }, { "three", 3 } }; + BiMap biMap = new() + { + { "one", 1 }, + { "two", 2 }, + { "three", 3 }, + }; List> entries = biMap.ToList(); @@ -218,9 +228,9 @@ public void Values_ReturnsAllValues() Assert.Contains(2, values); } -#endregion + #endregion -#region CopyTo Tests + #region CopyTo Tests [Fact] public void CopyTo_CopiesAllEntriesToArray() @@ -235,15 +245,20 @@ public void CopyTo_CopiesAllEntriesToArray() Assert.Contains(new KeyValuePair("two", 2), array); } -#endregion + #endregion -#region PutAll Tests + #region PutAll Tests [Fact] public void PutAll_WhenAddingMultipleEntries_AddsAllBidirectionally() { BiMap biMap = new() { { "existing", 0 } }; - Dictionary toAdd = new() { { "one", 1 }, { "two", 2 }, { "three", 3 } }; + Dictionary toAdd = new() + { + { "one", 1 }, + { "two", 2 }, + { "three", 3 }, + }; biMap.PutAll(toAdd); @@ -282,9 +297,9 @@ public void PutAll_WhenValueConflicts_RemovesOldKey() Assert.Equal("newKey", biMap.GetKey(100)); } -#endregion + #endregion -#region PutIfAbsent Tests + #region PutIfAbsent Tests [Fact] public void PutIfAbsent_WhenKeyDoesNotExist_AddsAndReturnsValue() @@ -311,9 +326,9 @@ public void PutIfAbsent_WhenKeyExists_ReturnsExistingValueWithoutModifying() Assert.Null(biMap.GetKey(200)); } -#endregion + #endregion -#region TryReplace Tests + #region TryReplace Tests [Fact] public void TryReplace_WhenKeyExists_ReplacesAndReturnsTrueWithOldValue() @@ -358,9 +373,9 @@ public void TryReplace_WhenReplacingValue_UpdatesReverseMappingCorrectly() Assert.Equal(200, biMap["key2"]); } -#endregion + #endregion -#region Replace Tests + #region Replace Tests [Fact] public void ReplaceConditional_WhenOldValueMatches_ReplacesAndReturnsTrue() @@ -400,9 +415,9 @@ public void ReplaceConditional_WhenKeyDoesNotExist_ReturnsFalseWithoutModifying( Assert.False(biMap.ContainsKey("missing")); } -#endregion + #endregion -#region Thread Safety Tests + #region Thread Safety Tests [Fact] public async Task ConcurrentAdds_MaintainConsistency() @@ -411,20 +426,19 @@ public async Task ConcurrentAdds_MaintainConsistency() const int threadCount = 100; const int operationsPerThread = 100; - IEnumerable tasks = Enumerable.Range(0, threadCount).Select - (threadId => - Task.Run - (() => - { - for (int j = 0; j < operationsPerThread; j++) - { - string key = $"thread{threadId}_key{j}"; - int value = (threadId * 1000) + j; - biMap.Add(key, value); - } - } - ) - ); + IEnumerable tasks = Enumerable + .Range(0, threadCount) + .Select(threadId => + Task.Run(() => + { + for (int j = 0; j < operationsPerThread; j++) + { + string key = $"thread{threadId}_key{j}"; + int value = (threadId * 1000) + j; + biMap.Add(key, value); + } + }) + ); await Task.WhenAll(tasks); @@ -458,21 +472,20 @@ public async Task ConcurrentRemoves_MaintainConsistency() const int threadCount = 10; int removeCount = 0; - IEnumerable tasks = Enumerable.Range(0, threadCount).Select - (threadId => - Task.Run - (() => - { - for (int j = threadId; j < entryCount; j += threadCount) - { - if (biMap.Remove($"key{j}")) - { - Interlocked.Increment(ref removeCount); - } - } - } - ) - ); + IEnumerable tasks = Enumerable + .Range(0, threadCount) + .Select(threadId => + Task.Run(() => + { + for (int j = threadId; j < entryCount; j += threadCount) + { + if (biMap.Remove($"key{j}")) + { + Interlocked.Increment(ref removeCount); + } + } + }) + ); await Task.WhenAll(tasks); @@ -492,15 +505,13 @@ public async Task ConcurrentPutAndRemove_MaintainConsistency() BiMap biMap = new(); const int operationCount = 10000; - IEnumerable addTasks = Enumerable.Range(0, operationCount).Select - (i => - Task.Run(() => biMap.Add($"key{i}", i)) - ); + IEnumerable addTasks = Enumerable + .Range(0, operationCount) + .Select(i => Task.Run(() => biMap.Add($"key{i}", i))); - IEnumerable> removeTasks = Enumerable.Range(0, operationCount).Select - (i => - Task.Run(() => biMap.Remove($"key{i}")) - ); + IEnumerable> removeTasks = Enumerable + .Range(0, operationCount) + .Select(i => Task.Run(() => biMap.Remove($"key{i}"))); await Task.WhenAll(addTasks.Concat(removeTasks)); @@ -516,31 +527,30 @@ public async Task ConcurrentClearOperations_Complete() { const int threadCount = 10; - IEnumerable tasks = Enumerable.Range(0, threadCount).Select - (_ => - Task.Run - (() => - { - BiMap biMap = new(); + IEnumerable tasks = Enumerable + .Range(0, threadCount) + .Select(_ => + Task.Run(() => + { + BiMap biMap = new(); - // Add initial data - for (int i = 0; i < 100; i++) - { - biMap.Add($"key{i}", i); - } + // Add initial data + for (int i = 0; i < 100; i++) + { + biMap.Add($"key{i}", i); + } - biMap.Clear(); + biMap.Clear(); - Assert.Empty(biMap); + Assert.Empty(biMap); - // Verify no stale reverse mappings - for (int i = 0; i < 100; i++) - { - Assert.Null(biMap.GetKey(i)); - } - } - ) - ); + // Verify no stale reverse mappings + for (int i = 0; i < 100; i++) + { + Assert.Null(biMap.GetKey(i)); + } + }) + ); await Task.WhenAll(tasks); } @@ -553,26 +563,24 @@ public async Task ConcurrentReadsAndWrites_MaintainConsistency() const int readCount = 1000; int successfulReads = 0; - IEnumerable writeTasks = Enumerable.Range(0, writeCount).Select - (i => - Task.Run(() => biMap.Add($"key{i}", i)) - ); - - IEnumerable readTasks = Enumerable.Range(0, readCount).Select - (i => - Task.Run - (() => - { - if (biMap.TryGetValue($"key{i}", out int value) && value == i) - { - if (biMap.TryGetKey(value, out string? key) && key == $"key{i}") - { - Interlocked.Increment(ref successfulReads); - } - } - } - ) - ); + IEnumerable writeTasks = Enumerable + .Range(0, writeCount) + .Select(i => Task.Run(() => biMap.Add($"key{i}", i))); + + IEnumerable readTasks = Enumerable + .Range(0, readCount) + .Select(i => + Task.Run(() => + { + if (biMap.TryGetValue($"key{i}", out int value) && value == i) + { + if (biMap.TryGetKey(value, out string? key) && key == $"key{i}") + { + Interlocked.Increment(ref successfulReads); + } + } + }) + ); await Task.WhenAll(writeTasks.Concat(readTasks)); @@ -593,18 +601,17 @@ public async Task ConcurrentValueOverwrites_MaintainConsistency() const int threadCount = 100; // Multiple threads try to update the same keys with different values - IEnumerable tasks = Enumerable.Range(0, threadCount).Select - (value => - Task.Run - (() => - { - for (int key = 0; key < 10; key++) - { - biMap[$"key{key}"] = value; - } - } - ) - ); + IEnumerable tasks = Enumerable + .Range(0, threadCount) + .Select(value => + Task.Run(() => + { + for (int key = 0; key < 10; key++) + { + biMap[$"key{key}"] = value; + } + }) + ); await Task.WhenAll(tasks); @@ -632,29 +639,25 @@ public async Task ConcurrentEnumeration_DoesNotThrow() biMap.Add($"key{i}", i); } - Task enumerationTask = Task.Run - (() => + Task enumerationTask = Task.Run(() => + { + for (int i = 0; i < 100; i++) { - for (int i = 0; i < 100; i++) - { - List> snapshot = biMap.ToList(); - Assert.NotEmpty(snapshot); - } + List> snapshot = biMap.ToList(); + Assert.NotEmpty(snapshot); } - ); + }); - Task modificationTask = Task.Run - (() => + Task modificationTask = Task.Run(() => + { + for (int i = 100; i < 200; i++) { - for (int i = 100; i < 200; i++) - { - biMap.Add($"key{i}", i); - } + biMap.Add($"key{i}", i); } - ); + }); await Task.WhenAll(enumerationTask, modificationTask); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Common.Tests/ConverterTests.cs b/tests/EzDdd.Common.Tests/ConverterTests.cs index b80b10b..99a5166 100644 --- a/tests/EzDdd.Common.Tests/ConverterTests.cs +++ b/tests/EzDdd.Common.Tests/ConverterTests.cs @@ -103,8 +103,7 @@ public void Converter_WhenUsingNullableReferenceTypes_HandlesNullAppropriately() [Fact] public void Converter_WhenPerformingComplexTransformation_TransformsAllProperties() { - Converter converter = order => new OrderSummary - ( + Converter converter = order => new OrderSummary( order.Id, $"{order.ProductName} (x{order.Quantity})", order.Quantity * order.UnitPrice @@ -161,4 +160,4 @@ public string IntToString(int value) return $"Number: {value}"; } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Common.Tests/JsonUtilTests.cs b/tests/EzDdd.Common.Tests/JsonUtilTests.cs index d812f1b..f7d8eee 100644 --- a/tests/EzDdd.Common.Tests/JsonUtilTests.cs +++ b/tests/EzDdd.Common.Tests/JsonUtilTests.cs @@ -5,7 +5,7 @@ namespace EzDdd.Common.Tests; public class JsonUtilTests { -#region AsString Tests + #region AsString Tests [Fact] public void AsString_WhenGivenSimpleObject_SerializesSuccessfully() @@ -53,9 +53,9 @@ public void AsString_WhenGivenPublicFields_SerializesFields() Assert.Contains("\"Data\":\"Field Value\"", json); } -#endregion + #endregion -#region ReadValue Tests + #region ReadValue Tests [Theory] [InlineData("{\"Id\":1,\"Name\":\"Alice\"}")] @@ -88,16 +88,16 @@ public void ReadValue_WhenGivenRecordType_DeserializesSuccessfully() [InlineData("")] public void ReadValue_WhenGivenInvalidJson_ThrowsException(string invalidJson) { - InvalidOperationException exception = Assert.Throws - (() => JsonUtil.ReadValue(invalidJson) + InvalidOperationException exception = Assert.Throws(() => + JsonUtil.ReadValue(invalidJson) ); Assert.Contains("Failed to deserialize JSON", exception.Message); } -#endregion + #endregion -#region ReadAs Tests + #region ReadAs Tests [Fact] public void ReadAs_WhenGivenByteArray_DeserializesSuccessfully() @@ -116,16 +116,16 @@ public void ReadAs_WhenGivenEmptyByteArray_ThrowsException() { byte[] emptyBytes = []; - InvalidOperationException exception = Assert.Throws - (() => JsonUtil.ReadAs(emptyBytes) + InvalidOperationException exception = Assert.Throws(() => + JsonUtil.ReadAs(emptyBytes) ); Assert.Contains("Failed to deserialize byte array", exception.Message); } -#endregion + #endregion -#region ReadTree Tests + #region ReadTree Tests [Fact] public void ReadTree_WhenGivenString_ParsesSuccessfully() @@ -154,8 +154,8 @@ public void ReadTree_WhenGivenInvalidJson_ThrowsException() { const string invalidJson = "{invalid}"; - InvalidOperationException exception = Assert.Throws - (() => JsonUtil.ReadTree(invalidJson) + InvalidOperationException exception = Assert.Throws(() => + JsonUtil.ReadTree(invalidJson) ); Assert.Contains("Failed to parse JSON", exception.Message); @@ -174,9 +174,9 @@ public void ReadTree_WhenGivenComplexStructure_NavigatesCorrectly() Assert.Equal("Alice", profile.GetProperty("name").GetString()); } -#endregion + #endregion -#region DeepCopy Tests + #region DeepCopy Tests [Fact] public void DeepCopy_WhenGivenSimpleObject_CreatesIndependentCopy() @@ -219,7 +219,13 @@ public void DeepCopy_WhenGivenNull_ReturnsDefault() [Fact] public void DeepCopy_WhenGivenComplexObject_CopiesAllProperties() { - Order original = new() { Id = 100, ProductName = "Widget", Quantity = 5, UnitPrice = 10.50m }; + Order original = new() + { + Id = 100, + ProductName = "Widget", + Quantity = 5, + UnitPrice = 10.50m, + }; Order? copy = JsonUtil.DeepCopy(original); @@ -230,9 +236,9 @@ public void DeepCopy_WhenGivenComplexObject_CopiesAllProperties() Assert.Equal(original.UnitPrice, copy.UnitPrice); } -#endregion + #endregion -#region Round-Trip Tests + #region Round-Trip Tests [Fact] public void AsString_AndReadValue_RoundTripSuccessfully() @@ -261,7 +267,7 @@ public void ReadAs_AndAsString_RoundTripSuccessfully() Assert.Equal(original.Name, restored.Name); } -#endregion + #endregion // Test domain classes private class User @@ -297,4 +303,4 @@ private record MutableRecord public int Id { get; set; } public string Data { get; set; } = string.Empty; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs b/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs index 35043c3..654c8fb 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs @@ -75,11 +75,12 @@ private class TestCommand : ICommand { public Task ExecuteAsync(TestInput input) { - TestOutput output = TestOutput.Create() - .SetId("test-id") - .SetMessage("Command executed successfully") - .SetProcessedData(input.Data) - .Succeed(); + TestOutput output = TestOutput + .Create() + .SetId("test-id") + .SetMessage("Command executed successfully") + .SetProcessedData(input.Data) + .Succeed(); return Task.FromResult(output); } @@ -93,9 +94,7 @@ public Task ExecuteAsync(TestInput input) { ExecutionCount++; - TestOutput output = TestOutput.Create() - .SetProcessedData($"Executed {ExecutionCount} times") - .Succeed(); + TestOutput output = TestOutput.Create().SetProcessedData($"Executed {ExecutionCount} times").Succeed(); return Task.FromResult(output); } @@ -105,4 +104,4 @@ public static void Reset() ExecutionCount = 0; } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs b/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs index 20857f6..3399120 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs @@ -53,4 +53,4 @@ public void MultipleImplementations_AreDistinct() private record TestInquiryInput(string Data) : IInquiryInput; private record AnotherTestInquiryInput(int Value) : IInquiryInput; -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs b/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs index 86451ae..778f4f0 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs @@ -96,7 +96,8 @@ public Task QueryAsync(TestInquiryInput input) } } - private class CommandWithInquiry(IInquiry inquiry) : ICommand + private class CommandWithInquiry(IInquiry inquiry) + : ICommand { public async Task ExecuteAsync(TestInquiryInput input) { @@ -104,18 +105,12 @@ public async Task ExecuteAsync(TestInquiryInput input) if (!isValid) { - return TestCommandOutput.Create() - .SetMessage("Validation failed") - .Fail(); + return TestCommandOutput.Create().SetMessage("Validation failed").Fail(); } - return TestCommandOutput.Create() - .SetMessage("Validation passed - Command executed") - .Succeed(); + return TestCommandOutput.Create().SetMessage("Validation passed - Command executed").Succeed(); } } - private class TestCommandOutput : CqrsOutput - { - } -} \ No newline at end of file + private class TestCommandOutput : CqrsOutput { } +} diff --git a/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs b/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs index 4b41d37..4be30d7 100644 --- a/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.Cqrs.Tests; public class CqrsOutputTests { -#region Factory and Properties Tests + #region Factory and Properties Tests [Fact] public void Create_WhenCalled_ShouldReturnNewInstance() @@ -25,9 +25,9 @@ public void Properties_DefaultValues_ShouldBeInitialized() Assert.Equal(ExitCode.Success, output.ExitCode); } -#endregion + #endregion -#region Fluent API Tests + #region Fluent API Tests [Fact] public void SetId_WhenCalled_ShouldSetIdAndReturnSelf() @@ -83,8 +83,7 @@ public void Fail_WhenCalled_ShouldSetExitCodeToFailureAndReturnSelf() [Fact] public void Succeed_WhenCalled_ShouldSetExitCodeToSuccessAndReturnSelf() { - TestOutput output = TestOutput.Create() - .Fail(); + TestOutput output = TestOutput.Create().Fail(); TestOutput result = output.Succeed(); @@ -99,10 +98,7 @@ public void FluentAPI_MethodChaining_ShouldWork() const string expectedId = "id-001"; const string expectedMessage = "Operation completed"; - TestOutput output = TestOutput.Create() - .SetId(expectedId) - .SetMessage(expectedMessage) - .Succeed(); + TestOutput output = TestOutput.Create().SetId(expectedId).SetMessage(expectedMessage).Succeed(); Assert.Equal(expectedId, output.Id); Assert.Equal(expectedMessage, output.Message); @@ -110,9 +106,9 @@ public void FluentAPI_MethodChaining_ShouldWork() Assert.IsType(output); } -#endregion + #endregion -#region Subclass Scenarios Tests + #region Subclass Scenarios Tests [Fact] public void Subclass_WithCustomProperties_ShouldSupportFluentAPI() @@ -120,10 +116,11 @@ public void Subclass_WithCustomProperties_ShouldSupportFluentAPI() const string expectedData = "custom-data"; const string expectedId = "sub-001"; - TestOutputWithCustomProperty output = TestOutputWithCustomProperty.Create() - .SetId(expectedId) - .SetCustomData(expectedData) - .Succeed(); + TestOutputWithCustomProperty output = TestOutputWithCustomProperty + .Create() + .SetId(expectedId) + .SetCustomData(expectedData) + .Succeed(); Assert.Equal(expectedId, output.Id); Assert.Equal(expectedData, output.CustomData); @@ -139,12 +136,13 @@ public void Subclass_MethodChaining_AcrossBaseAndDerived_ShouldPreserveType() const string expectedData = "custom-data"; const int expectedValue = 42; - TestOutputWithCustomProperty output = TestOutputWithCustomProperty.Create() - .SetId(expectedId) - .SetCustomData(expectedData) - .SetMessage(expectedMessage) - .SetCustomValue(expectedValue) - .Succeed(); + TestOutputWithCustomProperty output = TestOutputWithCustomProperty + .Create() + .SetId(expectedId) + .SetCustomData(expectedData) + .SetMessage(expectedMessage) + .SetCustomValue(expectedValue) + .Succeed(); Assert.IsType(output); Assert.Equal(expectedId, output.Id); @@ -154,9 +152,9 @@ public void Subclass_MethodChaining_AcrossBaseAndDerived_ShouldPreserveType() Assert.Equal(ExitCode.Success, output.ExitCode); } -#endregion + #endregion -#region Explicit IOutput Implementation Tests + #region Explicit IOutput Implementation Tests [Fact] public void ExplicitIOutput_SetMessage_ShouldWorkThroughInterface() @@ -216,13 +214,11 @@ public void ExplicitIOutput_SetId_ShouldWorkThroughInterface() Assert.Same(output, result); } -#endregion + #endregion -#region Test Helper Classes + #region Test Helper Classes - private class TestOutput : CqrsOutput - { - } + private class TestOutput : CqrsOutput { } private class TestOutputWithCustomProperty : CqrsOutput { @@ -242,5 +238,5 @@ public TestOutputWithCustomProperty SetCustomValue(int value) } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Cqrs.Tests/Integration/CompleteCqrsFlowTests.cs b/tests/EzDdd.Cqrs.Tests/Integration/CompleteCqrsFlowTests.cs index d26317a..62e569d 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/CompleteCqrsFlowTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/CompleteCqrsFlowTests.cs @@ -15,7 +15,7 @@ namespace EzDdd.Cqrs.Tests.Integration; /// public sealed class CompleteCqrsFlowTests { -#region Setup Infrastructure + #region Setup Infrastructure /// /// Creates the complete CQRS infrastructure for testing. @@ -38,7 +38,7 @@ private static CqrsTestInfrastructure _CreateInfrastructure() Repository = repository, Archive = archive, Projector = projector, - Query = query + Query = query, }; } @@ -71,9 +71,9 @@ public async Task SaveAndPublishAsync(BankAccount aggregate) } } -#endregion + #endregion -#region Command to Query Flow Tests + #region Command to Query Flow Tests [Fact] public async Task CreateAccount_Command_ShouldBeQueryable() @@ -109,9 +109,9 @@ public async Task NonExistentAccount_Query_ShouldThrowException() await Assert.ThrowsAsync(() => infra.Query.ExecuteAsync(input)); } -#endregion + #endregion -#region Event Projection Tests + #region Event Projection Tests [Fact] public async Task AccountCreatedEvent_ShouldCreateReadModel() @@ -179,9 +179,9 @@ public async Task MoneyWithdrawnEvent_ShouldUpdateReadModel() Assert.Equal(1, readModel.TransactionCount); } -#endregion + #endregion -#region Complete CQRS Flow Tests + #region Complete CQRS Flow Tests [Fact] public async Task CompleteFlow_CreateDepositWithdrawQuery_ShouldWork() @@ -241,9 +241,9 @@ public async Task MultipleOperations_ShouldMaintainConsistency() Assert.Equal(5, output.TransactionCount); } -#endregion + #endregion -#region Deletion Flow Tests + #region Deletion Flow Tests [Fact] public async Task AccountClosed_ShouldRemoveReadModel() @@ -288,9 +288,9 @@ public async Task AccountClosedThenQueried_ShouldThrowException() await Assert.ThrowsAsync(() => infra.Query.ExecuteAsync(input)); } -#endregion + #endregion -#region Event Replay Consistency Tests + #region Event Replay Consistency Tests [Fact] public async Task EventReplay_ShouldProduceSameReadModel() @@ -366,5 +366,5 @@ public async Task ConcurrentArchiveSaves_IdempotentOperation_ShouldHandleCorrect Assert.Equal(2, result.TransactionCount); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs index aad7872..1bfc8f9 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs @@ -79,7 +79,9 @@ public async Task ExecuteAsync(DomainEventData eventData) { // In test scenarios: rethrow to make failures visible in test results // In production: log error and continue processing (don't crash projector) - await Console.Error.WriteLineAsync($"Error processing event {eventData.Id} (type: {eventData.EventType}): {ex.Message}"); + await Console.Error.WriteLineAsync( + $"Error processing event {eventData.Id} (type: {eventData.EventType}): {ex.Message}" + ); throw; // Rethrow for test observability } } @@ -89,8 +91,7 @@ public async Task ExecuteAsync(DomainEventData eventData) /// private async Task _HandleAccountCreatedAsync(AccountCreated @event) { - AccountSummaryReadModel readModel = new - ( + AccountSummaryReadModel readModel = new( @event.AccountId, @event.Owner, @event.InitialBalance.Amount, @@ -117,7 +118,7 @@ private async Task _HandleMoneyDepositedAsync(MoneyDeposited @event) { Balance = existing.Balance + @event.Amount.Amount, LastTransactionDate = @event.OccurredOn, - TransactionCount = existing.TransactionCount + 1 + TransactionCount = existing.TransactionCount + 1, }; await _archive.SaveAsync(updated); @@ -138,7 +139,7 @@ private async Task _HandleMoneyWithdrawnAsync(MoneyWithdrawn @event) { Balance = existing.Balance - @event.Amount.Amount, LastTransactionDate = @event.OccurredOn, - TransactionCount = existing.TransactionCount + 1 + TransactionCount = existing.TransactionCount + 1, }; await _archive.SaveAsync(updated); @@ -169,7 +170,7 @@ private static IInternalDomainEvent _DeserializeDomainEvent(DomainEventData even "MoneyDeposited" => DomainEventMapper.ToDomain(eventData), "MoneyWithdrawn" => DomainEventMapper.ToDomain(eventData), "AccountClosed" => DomainEventMapper.ToDomain(eventData), - _ => throw new InvalidOperationException($"Unknown event type: {eventData.EventType}") + _ => throw new InvalidOperationException($"Unknown event type: {eventData.EventType}"), }; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountSummaryReadModel.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountSummaryReadModel.cs index b0289a9..5d25d1d 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountSummaryReadModel.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountSummaryReadModel.cs @@ -13,12 +13,11 @@ namespace EzDdd.Cqrs.Tests.Integration.TestDomain; /// When the account was created. /// When the last transaction occurred. /// Total number of transactions (deposits + withdrawals). -public sealed record AccountSummaryReadModel -( +public sealed record AccountSummaryReadModel( AccountId AccountId, string Owner, decimal Balance, DateTimeOffset CreatedOn, DateTimeOffset LastTransactionDate, int TransactionCount -); \ No newline at end of file +); diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryInput.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryInput.cs index 297b777..6cbee3a 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryInput.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryInput.cs @@ -7,4 +7,4 @@ namespace EzDdd.Cqrs.Tests.Integration.TestDomain; /// Input for getting account summary query. /// /// The account identifier to query. -public sealed record GetAccountSummaryInput(AccountId AccountId) : IInput; \ No newline at end of file +public sealed record GetAccountSummaryInput(AccountId AccountId) : IInput; diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryOutput.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryOutput.cs index aab795d..f678993 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryOutput.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryOutput.cs @@ -88,4 +88,4 @@ public GetAccountSummaryOutput SetTransactionCount(int transactionCount) TransactionCount = transactionCount; return this; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryQuery.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryQuery.cs index acccc9a..860aa79 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryQuery.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/GetAccountSummaryQuery.cs @@ -36,14 +36,15 @@ public async Task ExecuteAsync(GetAccountSummaryInput i throw new UseCaseFailureException($"Account not found: {input.AccountId}"); } - return GetAccountSummaryOutput.Create() - .SetAccountId(readModel.AccountId.Value) - .SetOwner(readModel.Owner) - .SetBalance(readModel.Balance) - .SetCreatedOn(readModel.CreatedOn) - .SetLastTransactionDate(readModel.LastTransactionDate) - .SetTransactionCount(readModel.TransactionCount) - .Succeed() - .SetMessage("Account summary retrieved successfully"); + return GetAccountSummaryOutput + .Create() + .SetAccountId(readModel.AccountId.Value) + .SetOwner(readModel.Owner) + .SetBalance(readModel.Balance) + .SetCreatedOn(readModel.CreatedOn) + .SetLastTransactionDate(readModel.LastTransactionDate) + .SetTransactionCount(readModel.TransactionCount) + .Succeed() + .SetMessage("Account summary retrieved successfully"); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs index 0f2d293..ceed2f5 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.Cqrs.Tests.Query; public class ArchiveCrudTests { -#region Thread Safety Tests + #region Thread Safety Tests [Fact] public async Task ConcurrentOperations_ShouldBeThreadSafe() @@ -15,15 +15,12 @@ public async Task ConcurrentOperations_ShouldBeThreadSafe() for (int i = 0; i < 100; i++) { int id = i; - tasks.Add - ( - Task.Run - (async () => - { - TestReadModel item = new($"item-{id}", $"Data {id}", id); - await archive.SaveAsync(item); - } - ) + tasks.Add( + Task.Run(async () => + { + TestReadModel item = new($"item-{id}", $"Data {id}", id); + await archive.SaveAsync(item); + }) ); } @@ -38,11 +35,11 @@ public async Task ConcurrentOperations_ShouldBeThreadSafe() } } -#endregion + #endregion private record TestReadModel(string Id, string Data, int IntId = 0); -#region FindByIdAsync Tests + #region FindByIdAsync Tests [Fact] public async Task FindByIdAsync_WhenItemExists_ShouldReturnItem() @@ -68,9 +65,9 @@ public async Task FindByIdAsync_WhenItemDoesNotExist_ShouldReturnNull() Assert.Null(result); } -#endregion + #endregion -#region SaveAsync Tests + #region SaveAsync Tests [Fact] public async Task SaveAsync_WhenNewItem_ShouldInsertItem() @@ -125,9 +122,9 @@ public async Task SaveAsync_WhenDataIsNull_ShouldThrowArgumentNullException() await Assert.ThrowsAsync(() => archive.SaveAsync(null!)); } -#endregion + #endregion -#region DeleteAsync Tests + #region DeleteAsync Tests [Fact] public async Task DeleteAsync_WhenItemExists_ShouldRemoveItem() @@ -178,9 +175,9 @@ public async Task DeleteAsync_WhenDataIsNull_ShouldThrowArgumentNullException() await Assert.ThrowsAsync(() => archive.DeleteAsync(null!)); } -#endregion + #endregion -#region Complete CRUD Flow Tests + #region Complete CRUD Flow Tests [Fact] public async Task CompleteCrudFlow_ShouldWorkCorrectly() @@ -218,10 +215,7 @@ public async Task MultipleItems_ShouldBeStoredIndependently() { InMemoryArchive archive = new(x => x.Id); - TestReadModel[] items = - [ - new("id-1", "Data 1"), new("id-2", "Data 2"), new("id-3", "Data 3") - ]; + TestReadModel[] items = [new("id-1", "Data 1"), new("id-2", "Data 2"), new("id-3", "Data 3")]; foreach (TestReadModel item in items) { @@ -235,5 +229,5 @@ public async Task MultipleItems_ShouldBeStoredIndependently() Assert.Equal("Data 2", retrieved.Data); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs index 394a6db..98bd403 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs @@ -54,9 +54,5 @@ public void MultipleImplementations_AreDistinct() private record TestProjectionInput(string Id) : IProjectionInput; - private record ComplexProjectionInput - ( - string Id, - bool IncludeDetails - ) : IProjectionInput; -} \ No newline at end of file + private record ComplexProjectionInput(string Id, bool IncludeDetails) : IProjectionInput; +} diff --git a/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs index a3d5786..e0d7297 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs @@ -107,16 +107,18 @@ public Task QueryAsync(TestProjectionInput input) } } - private class QueryWithProjection(IProjection projection) : IQuery + private class QueryWithProjection(IProjection projection) + : IQuery { public async Task ExecuteAsync(TestInput input) { TestProjectionInput projectionInput = new(input.CustomerId); CustomerViewModel viewModel = await projection.QueryAsync(projectionInput); - return TestQueryOutput.Create() - .SetCustomerInfo($"{viewModel.CustomerId}: {viewModel.CustomerName}") - .Succeed(); + return TestQueryOutput + .Create() + .SetCustomerInfo($"{viewModel.CustomerId}: {viewModel.CustomerName}") + .Succeed(); } } @@ -130,4 +132,4 @@ public TestQueryOutput SetCustomerInfo(string info) return this; } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs b/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs index 9a668bf..d9ddc41 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs @@ -70,13 +70,14 @@ private class TestQuery : IQuery { public Task ExecuteAsync(TestInput input) { - TestOutput output = TestOutput.Create() - .SetId(input.Id) - .SetMessage("Query executed successfully") - .SetRetrievedData($"Retrieved data for {input.Id}") - .Succeed(); + TestOutput output = TestOutput + .Create() + .SetId(input.Id) + .SetMessage("Query executed successfully") + .SetRetrievedData($"Retrieved data for {input.Id}") + .Succeed(); return Task.FromResult(output); } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Cqrs.Tests/Query/TestHelpers/InMemoryArchive.cs b/tests/EzDdd.Cqrs.Tests/Query/TestHelpers/InMemoryArchive.cs index a7ca4e4..4eb7b1d 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/TestHelpers/InMemoryArchive.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/TestHelpers/InMemoryArchive.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; - using EzDdd.Cqrs.Query; namespace EzDdd.Cqrs.Tests.Query.TestHelpers; @@ -92,4 +91,4 @@ public IEnumerable GetAll() { return _store.Values; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/AggregateRootTests.cs b/tests/EzDdd.Entity.Tests/AggregateRootTests.cs index 1932e68..31becb3 100644 --- a/tests/EzDdd.Entity.Tests/AggregateRootTests.cs +++ b/tests/EzDdd.Entity.Tests/AggregateRootTests.cs @@ -2,7 +2,7 @@ namespace EzDdd.Entity.Tests; public class AggregateRootTests { -#region Construction Tests + #region Construction Tests [Fact] public void AggregateRoot_Constructor_InitializesProperties() @@ -27,9 +27,9 @@ public void AggregateRoot_InitialVersion_IsMinusOne() Assert.Equal(0L, aggregate.Version); } -#endregion + #endregion -#region Event Collection Tests + #region Event Collection Tests [Fact] public void AggregateRoot_Apply_AddsEventToCollection() @@ -111,9 +111,9 @@ public void AggregateRoot_GetDomainEventSize_ReturnsCorrectCount() Assert.Equal(0, aggregate.GetDomainEventSize()); } -#endregion + #endregion -#region Versioning Tests + #region Versioning Tests [Fact] public void AggregateRoot_Apply_IncrementsVersion() @@ -140,9 +140,9 @@ public void AggregateRoot_MultipleApplies_IncrementsVersionSequentially() Assert.Equal(4, aggregate.GetDomainEventSize()); // 1 constructor + 3 updates } -#endregion + #endregion -#region PublishAndClear Tests + #region PublishAndClear Tests [Fact] public void AggregateRoot_ClearDomainEvents_RemovesAllEvents() @@ -170,9 +170,9 @@ public void AggregateRoot_ClearDomainEvents_DoesNotResetVersion() Assert.Equal(versionBeforeClear, aggregate.Version); } -#endregion + #endregion -#region IsDeleted Tests + #region IsDeleted Tests [Fact] public void AggregateRoot_IsDeleted_CanBeSetBySubclass() @@ -185,9 +185,9 @@ public void AggregateRoot_IsDeleted_CanBeSetBySubclass() Assert.True(aggregate.IsDeleted); } -#endregion + #endregion -#region Contract Tests + #region Contract Tests [Fact] public void AggregateRoot_ImplementsIEntity() @@ -210,9 +210,9 @@ public void AggregateRoot_GenericConstraint_EnforcesIInternalDomainEvent() Assert.All(events, e => Assert.IsAssignableFrom(e)); } -#endregion + #endregion -#region Thread Safety Tests + #region Thread Safety Tests [Fact] public async Task AggregateRoot_ThreadSafety_ConcurrentEventAddition() @@ -249,16 +249,13 @@ public async Task AggregateRoot_ThreadSafety_ConcurrentReadAndWrite() tasks.Add(Task.Run(() => aggregate.UpdateValue(i))); // Read task - tasks.Add - ( - Task.Run - (() => - { - IReadOnlyList _ = aggregate.GetDomainEvents(); - int __ = aggregate.GetDomainEventSize(); - IInternalDomainEvent? ___ = aggregate.GetLastDomainEvent(); - } - ) + tasks.Add( + Task.Run(() => + { + IReadOnlyList _ = aggregate.GetDomainEvents(); + int __ = aggregate.GetDomainEventSize(); + IInternalDomainEvent? ___ = aggregate.GetLastDomainEvent(); + }) ); } @@ -268,11 +265,10 @@ public async Task AggregateRoot_ThreadSafety_ConcurrentReadAndWrite() Assert.True(aggregate.GetDomainEventSize() > 0); } -#endregion + #endregion // Test events - private record TestCreatedEvent - ( + private record TestCreatedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -280,8 +276,7 @@ private record TestCreatedEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record TestUpdatedEvent - ( + private record TestUpdatedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -289,8 +284,7 @@ private record TestUpdatedEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestDeletedEvent - ( + private record TestDeletedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -306,8 +300,7 @@ public TestAggregate(Guid id, string name) Id = id; Name = name; - TestCreatedEvent created = new - ( + TestCreatedEvent created = new( Guid.NewGuid(), DateTimeOffset.UtcNow, id.ToString(), @@ -326,8 +319,7 @@ public void UpdateValue(int newValue) { Value = newValue; - TestUpdatedEvent updated = new - ( + TestUpdatedEvent updated = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -342,8 +334,7 @@ public void Delete(string reason) { IsDeleted = true; - TestDeletedEvent deleted = new - ( + TestDeletedEvent deleted = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -354,4 +345,4 @@ public void Delete(string reason) Apply(deleted); } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/DomainEventTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTests.cs index 5ead825..cdc3394 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTests.cs @@ -23,14 +23,7 @@ public void IDomainEvent_WithAllProperties_ReturnsCorrectValues() public void IDomainEvent_Metadata_IsImmutable() { Dictionary metadata = new() { ["Key1"] = "Value1" }; - TestCommandEvent @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - "source", - 0, - metadata - ); + TestCommandEvent @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source", 0, metadata); Assert.IsAssignableFrom>(@event.Metadata); @@ -45,14 +38,7 @@ public void IDomainEvent_WithEmptyMetadata_WorksCorrectly() // ReSharper disable once CollectionNeverUpdated.Local Dictionary emptyMetadata = new(); - TestCommandEvent @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - "source", - 0, - emptyMetadata - ); + TestCommandEvent @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source", 0, emptyMetadata); Assert.Empty(@event.Metadata); } @@ -89,8 +75,7 @@ public void IDomainEvent_RecordType_HasStructuralEquality() [Fact] public void IDomainEvent_RecordType_IsImmutable() { - TestCommandEvent @event = new - ( + TestCommandEvent @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "source", @@ -107,8 +92,7 @@ public void IDomainEvent_RecordType_IsImmutable() [Fact] public void IDomainEvent_CanBeUsedAsTypeConstraint() { - TestCommandEvent @event = new - ( + TestCommandEvent @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "source", @@ -122,17 +106,16 @@ public void IDomainEvent_CanBeUsedAsTypeConstraint() return; // Helper method demonstrating generic constraint - static Guid GetEventId(TEvent @event) where TEvent : IDomainEvent + static Guid GetEventId(TEvent @event) + where TEvent : IDomainEvent { return @event.Id; } } - // Test implementation - Construction event // ReSharper disable once UnusedType.Local - private record TestConstructionEvent - ( + private record TestConstructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -141,8 +124,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; // Test implementation - Command event (middle event) - private record TestCommandEvent - ( + private record TestCommandEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -152,12 +134,11 @@ IReadOnlyDictionary Metadata // Test implementation - Destruction event // ReSharper disable once UnusedType.Local - private record TestDestructionEvent - ( + private record TestDestructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, string Reason, IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent; -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs index e04f855..d186eff 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs @@ -34,9 +34,10 @@ public void ConcurrentRegistration_DifferentTypes_ShouldSucceed() ConcurrentBag exceptions = []; // Act - Launch multiple threads registering different types - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { @@ -96,9 +97,10 @@ public void ConcurrentRegistration_SameTypeAndName_ShouldBeIdempotent() ConcurrentBag exceptions = []; // Act - Launch multiple threads registering the same type/name - Parallel.For - ( - 0, THREAD_COUNT, _ => + Parallel.For( + 0, + THREAD_COUNT, + _ => { try { @@ -132,9 +134,10 @@ public void ConcurrentGetTypeName_AfterRegistration_ShouldSucceed() ConcurrentBag exceptions = []; // Act - Launch multiple threads reading type names - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { @@ -142,7 +145,7 @@ public void ConcurrentGetTypeName_AfterRegistration_ShouldSucceed() { 0 => typeof(TestEvent1), 1 => typeof(TestEvent2), - _ => typeof(TestEvent3) + _ => typeof(TestEvent3), }; string typeName = DomainEventTypeMapper.GetTypeName(type); @@ -176,9 +179,10 @@ public void ConcurrentGetType_AfterRegistration_ShouldSucceed() ConcurrentBag exceptions = []; // Act - Launch multiple threads reading types - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { @@ -186,7 +190,7 @@ public void ConcurrentGetType_AfterRegistration_ShouldSucceed() { 0 => "TestEvent1", 1 => "TestEvent2", - _ => "TestEvent3" + _ => "TestEvent3", }; Type type = DomainEventTypeMapper.GetType(typeName); @@ -217,9 +221,10 @@ public void ConcurrentMixedOperations_ShouldSucceed() ConcurrentBag readResults = []; // Act - Launch threads doing different operations - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { @@ -301,15 +306,17 @@ public void ConcurrentContains_AfterRegistration_ShouldSucceed() ConcurrentBag results = []; ConcurrentBag exceptions = []; - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { - bool contains = i % 2 == 0 - ? DomainEventTypeMapper.Contains("TestEvent1") - : DomainEventTypeMapper.Contains("TestEvent2"); + bool contains = + i % 2 == 0 + ? DomainEventTypeMapper.Contains("TestEvent1") + : DomainEventTypeMapper.Contains("TestEvent2"); results.Add(contains); } catch (Exception ex) @@ -336,9 +343,10 @@ public void StressTest_HighConcurrency_ShouldSucceed() var readCount = 0; // Act - High concurrency with mixed operations - Parallel.For - ( - 0, THREAD_COUNT, i => + Parallel.For( + 0, + THREAD_COUNT, + i => { try { @@ -404,33 +412,73 @@ public void StressTest_HighConcurrency_ShouldSucceed() } // Test event types (need 10 different types for concurrency tests) - private record TestEvent1(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent2(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent3(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent4(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent5(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent6(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent7(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent8(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent9(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; - - private record TestEvent10(Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata) - : IInternalDomainEvent; -} \ No newline at end of file + private record TestEvent1( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent2( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent3( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent4( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent5( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent6( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent7( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent8( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent9( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private record TestEvent10( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; +} diff --git a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs index daa8d59..913e6a6 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs @@ -3,7 +3,7 @@ namespace EzDdd.Entity.Tests; [Collection("DomainEventTypeMapper")] public class DomainEventTypeMapperTests { -#region Register Tests + #region Register Tests [Fact] public void Register_WithTypeAndName_StoresMapping() @@ -21,10 +21,7 @@ public void Register_WithSameNameTwice_ThrowsException() DomainEventTypeMapper.Clear(); DomainEventTypeMapper.Register("TestEvent1"); - Assert.Throws - (() => - DomainEventTypeMapper.Register("TestEvent1") - ); + Assert.Throws(() => DomainEventTypeMapper.Register("TestEvent1")); } [Fact] @@ -33,15 +30,12 @@ public void Register_WithSameTypeTwice_ThrowsException() DomainEventTypeMapper.Clear(); DomainEventTypeMapper.Register("TestEvent1"); - Assert.Throws - (() => - DomainEventTypeMapper.Register("TestEvent1_Duplicate") - ); + Assert.Throws(() => DomainEventTypeMapper.Register("TestEvent1_Duplicate")); } -#endregion + #endregion -#region GetTypeName Tests + #region GetTypeName Tests [Fact] public void GetTypeName_FromType_ReturnsRegisteredName() @@ -59,13 +53,7 @@ public void GetTypeName_FromEvent_ReturnsRegisteredName() { DomainEventTypeMapper.Clear(); DomainEventTypeMapper.Register("TestEvent1"); - TestEvent1 @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - "source", - new Dictionary() - ); + TestEvent1 @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source", new Dictionary()); string typeName = DomainEventTypeMapper.GetTypeName(@event); @@ -77,15 +65,12 @@ public void GetTypeName_UnregisteredType_ThrowsException() { DomainEventTypeMapper.Clear(); - Assert.Throws - (() => - DomainEventTypeMapper.GetTypeName(typeof(TestEvent1)) - ); + Assert.Throws(() => DomainEventTypeMapper.GetTypeName(typeof(TestEvent1))); } -#endregion + #endregion -#region GetType Tests + #region GetType Tests [Fact] public void GetType_WithRegisteredName_ReturnsType() @@ -103,15 +88,12 @@ public void GetType_WithUnregisteredName_ThrowsException() { DomainEventTypeMapper.Clear(); - Assert.Throws - (() => - DomainEventTypeMapper.GetType("UnknownEvent") - ); + Assert.Throws(() => DomainEventTypeMapper.GetType("UnknownEvent")); } -#endregion + #endregion -#region Contains Tests + #region Contains Tests [Fact] public void Contains_WithRegisteredName_ReturnsTrue() @@ -134,9 +116,9 @@ public void Contains_WithUnregisteredName_ReturnsFalse() Assert.False(contains); } -#endregion + #endregion -#region GetAllMappings Tests + #region GetAllMappings Tests [Fact] public void GetAllMappings_ReturnsAllRegisteredMappings() @@ -166,9 +148,9 @@ public void GetAllMappings_ReturnsReadOnlyDictionary() Assert.IsAssignableFrom>(mappings); } -#endregion + #endregion -#region Clear Tests + #region Clear Tests [Fact] public void Clear_RemovesAllMappings() @@ -183,9 +165,9 @@ public void Clear_RemovesAllMappings() Assert.Empty(DomainEventTypeMapper.GetAllMappings()); } -#endregion + #endregion -#region Thread Safety Tests + #region Thread Safety Tests [Fact] public async Task ThreadSafety_ConcurrentRegister_HandlesCorrectly() @@ -198,34 +180,31 @@ public async Task ThreadSafety_ConcurrentRegister_HandlesCorrectly() for (int i = 0; i < eventCount; i++) { int index = i; - tasks.Add - ( - Task.Run - (() => + tasks.Add( + Task.Run(() => + { + try { - try + // Each task registers a unique event type + // Using TestEvent1, TestEvent2, TestEvent3 repeatedly + switch (index % 3) { - // Each task registers a unique event type - // Using TestEvent1, TestEvent2, TestEvent3 repeatedly - switch (index % 3) - { - case 0: - DomainEventTypeMapper.Register($"Event_{index}"); - break; - case 1: - DomainEventTypeMapper.Register($"Event_{index}"); - break; - default: - DomainEventTypeMapper.Register($"Event_{index}"); - break; - } - } - catch (ArgumentException) - { - // Duplicate registration is expected in concurrent scenario + case 0: + DomainEventTypeMapper.Register($"Event_{index}"); + break; + case 1: + DomainEventTypeMapper.Register($"Event_{index}"); + break; + default: + DomainEventTypeMapper.Register($"Event_{index}"); + break; } } - ) + catch (ArgumentException) + { + // Duplicate registration is expected in concurrent scenario + } + }) ); } await Task.WhenAll(tasks); @@ -250,35 +229,29 @@ public async Task ThreadSafety_ConcurrentReadWrite_NoDeadlock() int index = i; // Write task - tasks.Add - ( - Task.Run - (() => + tasks.Add( + Task.Run(() => + { + try { - try - { - DomainEventTypeMapper.Register($"Event_{index}"); - } - catch (ArgumentException) { } + DomainEventTypeMapper.Register($"Event_{index}"); } - ) + catch (ArgumentException) { } + }) ); // Read task - tasks.Add - ( - Task.Run - (() => + tasks.Add( + Task.Run(() => + { + try { - try - { - string _ = DomainEventTypeMapper.GetTypeName(typeof(TestEvent1)); - bool __ = DomainEventTypeMapper.Contains("TestEvent1"); - IReadOnlyDictionary ___ = DomainEventTypeMapper.GetAllMappings(); - } - catch { } + string _ = DomainEventTypeMapper.GetTypeName(typeof(TestEvent1)); + bool __ = DomainEventTypeMapper.Contains("TestEvent1"); + IReadOnlyDictionary ___ = DomainEventTypeMapper.GetAllMappings(); } - ) + catch { } + }) ); } await Task.WhenAll(tasks); @@ -286,19 +259,17 @@ public async Task ThreadSafety_ConcurrentReadWrite_NoDeadlock() Assert.True(DomainEventTypeMapper.Contains("TestEvent1")); } -#endregion + #endregion // Test events - private record TestEvent1 - ( + private record TestEvent1( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent2 - ( + private record TestEvent2( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -306,8 +277,7 @@ private record TestEvent2 IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent3 - ( + private record TestEvent3( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/EntityTests.cs b/tests/EzDdd.Entity.Tests/EntityTests.cs index 7fa25ee..7f6d973 100644 --- a/tests/EzDdd.Entity.Tests/EntityTests.cs +++ b/tests/EzDdd.Entity.Tests/EntityTests.cs @@ -57,8 +57,7 @@ public void IEntity_GenericConstraint_CanBeUsedInGenericMethod() return; // Helper method demonstrating generic constraint usage - static TId GetEntityId(IEntity entity) - => entity.Id; + static TId GetEntityId(IEntity entity) => entity.Id; } // Test implementation classes @@ -71,4 +70,4 @@ private class TestStringEntity(string id) : IEntity { public string Id { get; } = id; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs b/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs index 0b4b461..5103c8e 100644 --- a/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs +++ b/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.Entity.Tests; public class EsAggregateRootTests { -#region R1: Construction Event Tests + #region R1: Construction Event Tests [Fact] public void EsAggregateRoot_Constructor_AppliesConstructionEvent() @@ -30,15 +30,12 @@ public void EsAggregateRoot_R1_ConstructionEvent_DoesNotCheckPrecondition() public void EsAggregateRoot_R1_ConstructionEvent_ChecksPostcondition() { // Negative initial balance violates invariant - Assert.Throws - (() => - new Account(Guid.NewGuid(), "Alice", -100m) - ); + Assert.Throws(() => new Account(Guid.NewGuid(), "Alice", -100m)); } -#endregion + #endregion -#region R2: Command Event Tests + #region R2: Command Event Tests [Fact] public void EsAggregateRoot_R2_CommandEvent_ChecksPrecondition() @@ -83,9 +80,9 @@ public void EsAggregateRoot_When_MutatesState() Assert.Equal(120m, account.Balance); // 100 + 50 - 30 } -#endregion + #endregion -#region R3: Destruction Event Tests + #region R3: Destruction Event Tests [Fact] public void EsAggregateRoot_R3_DestructionEvent_ChecksPrecondition() @@ -110,9 +107,9 @@ public void EsAggregateRoot_R3_DestructionEvent_DoesNotCheckPostcondition() Assert.True(account.IsDeleted); } -#endregion + #endregion -#region Replay and Reconstruction Tests + #region Replay and Reconstruction Tests [Fact] public void EsAggregateRoot_ReplayConstructor_ReconstructsState() @@ -121,23 +118,28 @@ public void EsAggregateRoot_ReplayConstructor_ReconstructsState() Guid accountId = Guid.NewGuid(); List events = [ - new AccountCreated - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - "Alice", 100m, new Dictionary() + new AccountCreated( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + "Alice", + 100m, + new Dictionary() ), - - new MoneyDeposited - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - 50m, new Dictionary() + new MoneyDeposited( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + 50m, + new Dictionary() + ), + new MoneyWithdrawn( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + 30m, + new Dictionary() ), - - new MoneyWithdrawn - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - 30m, new Dictionary() - ) ]; // Reconstruct from events @@ -154,11 +156,14 @@ public void EsAggregateRoot_ReplayConstructor_ClearsEventsAfterReplay() Guid accountId = Guid.NewGuid(); List events = [ - new AccountCreated - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - "Alice", 100m, new Dictionary() - ) + new AccountCreated( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + "Alice", + 100m, + new Dictionary() + ), ]; Account account = new(events); @@ -173,23 +178,28 @@ public void EsAggregateRoot_ReplayConstructor_SetsCorrectVersion() Guid accountId = Guid.NewGuid(); List events = [ - new AccountCreated - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - "Alice", 100m, new Dictionary() + new AccountCreated( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + "Alice", + 100m, + new Dictionary() ), - - new MoneyDeposited - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - 50m, new Dictionary() + new MoneyDeposited( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + 50m, + new Dictionary() + ), + new MoneyWithdrawn( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + accountId.ToString(), + 30m, + new Dictionary() ), - - new MoneyWithdrawn - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, accountId.ToString(), - 30m, new Dictionary() - ) ]; Account account = new(events); @@ -198,9 +208,9 @@ public void EsAggregateRoot_ReplayConstructor_SetsCorrectVersion() Assert.Equal(2L, account.Version); } -#endregion + #endregion -#region API and Contract Tests + #region API and Contract Tests [Fact] public void EsAggregateRoot_GetCategory_ReturnsAggregateType() @@ -235,8 +245,10 @@ public void EsAggregateRoot_Apply_IsSealed_CannotBeOverridden() [Fact] public void EsAggregateRoot_When_IsAbstract_MustBeImplemented() { - MethodInfo? whenMethod = typeof(EsAggregateRoot) - .GetMethod("_When", BindingFlags.NonPublic | BindingFlags.Instance); + MethodInfo? whenMethod = typeof(EsAggregateRoot).GetMethod( + "_When", + BindingFlags.NonPublic | BindingFlags.Instance + ); Assert.True(whenMethod?.IsAbstract); } @@ -244,16 +256,18 @@ public void EsAggregateRoot_When_IsAbstract_MustBeImplemented() [Fact] public void EsAggregateRoot_EnsureInvariant_IsVirtual_CanBeOverridden() { - MethodInfo? ensureInvariantMethod = typeof(EsAggregateRoot) - .GetMethod("_EnsureInvariant", BindingFlags.NonPublic | BindingFlags.Instance); + MethodInfo? ensureInvariantMethod = typeof(EsAggregateRoot).GetMethod( + "_EnsureInvariant", + BindingFlags.NonPublic | BindingFlags.Instance + ); Assert.True(ensureInvariantMethod?.IsVirtual); Assert.False(ensureInvariantMethod?.IsAbstract); } -#endregion + #endregion -#region Integration Tests + #region Integration Tests [Fact] public void EsAggregateRoot_CompleteLifecycle_AllRulesEnforced() @@ -268,11 +282,10 @@ public void EsAggregateRoot_CompleteLifecycle_AllRulesEnforced() Assert.Equal(3L, account.Version); // 4 events total (0, 1, 2, 3) } -#endregion + #endregion // Test events - private record AccountCreated - ( + private record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -281,8 +294,7 @@ private record AccountCreated IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited - ( + private record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -290,8 +302,7 @@ private record MoneyDeposited IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record MoneyWithdrawn - ( + private record MoneyWithdrawn( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -299,8 +310,7 @@ private record MoneyWithdrawn IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record AccountClosed - ( + private record AccountClosed( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -314,8 +324,7 @@ private class Account : EsAggregateRoot // Constructor for new aggregate public Account(Guid id, string owner, decimal initialBalance) { - AccountCreated created = new - ( + AccountCreated created = new( Guid.NewGuid(), DateTimeOffset.UtcNow, id.ToString(), @@ -328,9 +337,8 @@ public Account(Guid id, string owner, decimal initialBalance) } // Constructor for event replay (required by framework) - public Account(IEnumerable events) : base(events) - { - } + public Account(IEnumerable events) + : base(events) { } public string Owner { get; private set; } = string.Empty; @@ -338,8 +346,7 @@ public Account(IEnumerable events) : base(events) public void Deposit(decimal amount) { - MoneyDeposited deposited = new - ( + MoneyDeposited deposited = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -352,8 +359,7 @@ public void Deposit(decimal amount) public void Withdraw(decimal amount) { - MoneyWithdrawn withdrawn = new - ( + MoneyWithdrawn withdrawn = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -366,8 +372,7 @@ public void Withdraw(decimal amount) public void Close(string reason) { - AccountClosed closed = new - ( + AccountClosed closed = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -434,4 +439,4 @@ public override string GetCategory() return "account"; } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/IntegrationTests.cs b/tests/EzDdd.Entity.Tests/IntegrationTests.cs index 37a8e60..1f9536a 100644 --- a/tests/EzDdd.Entity.Tests/IntegrationTests.cs +++ b/tests/EzDdd.Entity.Tests/IntegrationTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; - using EzDdd.Common; namespace EzDdd.Entity.Tests; @@ -126,9 +125,8 @@ public void Integration_InvariantViolation_PreventsInvalidState() BankAccount account = new(accountId, "Charlie Brown", new Money(100m, "USD")); // R2 postcondition check prevents negative balance - InvalidOperationException exception = Assert.Throws - (() => - account.Withdraw(new Money(150m, "USD"), "Over-withdrawal attempt") + InvalidOperationException exception = Assert.Throws(() => + account.Withdraw(new Money(150m, "USD"), "Over-withdrawal attempt") ); Assert.Contains("balance cannot be negative", exception.Message); @@ -155,7 +153,10 @@ public void Integration_ValueObjectEquality_WorksWithMoneyType() Assert.NotEqual(money1, money4); // Different amount // Immutability via with expression - Money money5 = money1 with { Amount = 200m }; + Money money5 = money1 with + { + Amount = 200m, + }; Assert.Equal(100m, money1.Amount); // Original unchanged Assert.Equal(200m, money5.Amount); } @@ -164,16 +165,14 @@ public void Integration_ValueObjectEquality_WorksWithMoneyType() public void Integration_EntityIdentity_WorksWithTransactionType() { Guid transactionId = Guid.NewGuid(); - Transaction transaction1 = new - ( + Transaction transaction1 = new( transactionId, DateTimeOffset.UtcNow, new Money(100m, "USD"), "Test transaction", TransactionType.Deposit ); - Transaction transaction2 = new - ( + Transaction transaction2 = new( transactionId, DateTimeOffset.UtcNow.AddMinutes(1), new Money(200m, "USD"), @@ -234,24 +233,20 @@ public async Task Integration_ConcurrentOperations_OnDifferentAggregates() for (int i = 0; i < accountCount; i++) { int accountNumber = i; - tasks.Add - ( - Task.Run - (() => - { - BankAccount account = new - ( - AccountId.NewId(), - $"User{accountNumber}", - new Money(1000m + accountNumber, "USD") - ); - - account.Deposit(new Money(100m, "USD"), "Deposit 1"); - account.Withdraw(new Money(50m, "USD"), "Withdrawal 1"); - - return account; - } - ) + tasks.Add( + Task.Run(() => + { + BankAccount account = new( + AccountId.NewId(), + $"User{accountNumber}", + new Money(1000m + accountNumber, "USD") + ); + + account.Deposit(new Money(100m, "USD"), "Deposit 1"); + account.Withdraw(new Money(50m, "USD"), "Withdrawal 1"); + + return account; + }) ); } @@ -259,19 +254,19 @@ public async Task Integration_ConcurrentOperations_OnDifferentAggregates() // All accounts created successfully Assert.Equal(accountCount, accounts.Length); - Assert.All - ( - accounts, account => + Assert.All( + accounts, + account => { Assert.False(account.IsDeleted); Assert.Equal(2L, account.Version); // 3 events (0-2) } ); } + // ========== Domain Events ========== - private record AccountOpenedEvent - ( + private record AccountOpenedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -280,8 +275,7 @@ private record AccountOpenedEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDepositedEvent - ( + private record MoneyDepositedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -290,8 +284,7 @@ private record MoneyDepositedEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record MoneyWithdrawnEvent - ( + private record MoneyWithdrawnEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -300,8 +293,7 @@ private record MoneyWithdrawnEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record AccountClosedEvent - ( + private record AccountClosedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -320,7 +312,10 @@ public Money Add(Money other) throw new InvalidOperationException($"Cannot add {other.Currency} to {Currency}"); } - return this with { Amount = Amount + other.Amount }; + return this with + { + Amount = Amount + other.Amount, + }; } public Money Subtract(Money other) @@ -330,7 +325,10 @@ public Money Subtract(Money other) throw new InvalidOperationException($"Cannot subtract {other.Currency} from {Currency}"); } - return this with { Amount = Amount - other.Amount }; + return this with + { + Amount = Amount - other.Amount, + }; } public bool IsNegative() @@ -367,7 +365,7 @@ private class Transaction(Guid id, DateTimeOffset timestamp, Money amount, strin private enum TransactionType { Deposit, - Withdrawal + Withdrawal, } // ========== Aggregate Root ========== @@ -384,8 +382,7 @@ public BankAccount(AccountId id, string accountHolder, Money initialDeposit) throw new ArgumentException("Initial deposit cannot be negative", nameof(initialDeposit)); } - AccountOpenedEvent opened = new - ( + AccountOpenedEvent opened = new( Guid.NewGuid(), DateTimeOffset.UtcNow, id.ToString(), @@ -398,9 +395,8 @@ public BankAccount(AccountId id, string accountHolder, Money initialDeposit) } // Constructor for event replay - public BankAccount(IEnumerable events) : base(events) - { - } + public BankAccount(IEnumerable events) + : base(events) { } public string AccountHolder { get; private set; } = string.Empty; @@ -415,8 +411,7 @@ public void Deposit(Money amount, string description) throw new ArgumentException("Deposit amount cannot be negative", nameof(amount)); } - MoneyDepositedEvent deposited = new - ( + MoneyDepositedEvent deposited = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -435,8 +430,7 @@ public void Withdraw(Money amount, string description) throw new ArgumentException("Withdrawal amount cannot be negative", nameof(amount)); } - MoneyWithdrawnEvent withdrawn = new - ( + MoneyWithdrawnEvent withdrawn = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -450,8 +444,7 @@ public void Withdraw(Money amount, string description) public void Close(string reason) { - AccountClosedEvent closed = new - ( + AccountClosedEvent closed = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.ToString(), @@ -470,10 +463,8 @@ protected override void _When(IInternalDomainEvent @event) Id = new AccountId(Guid.Parse(e.Source)); AccountHolder = e.AccountHolder; Balance = e.InitialDeposit; - _transactions.Add - ( - new Transaction - ( + _transactions.Add( + new Transaction( e.Id, e.OccurredOn, e.InitialDeposit, @@ -485,31 +476,15 @@ protected override void _When(IInternalDomainEvent @event) case MoneyDepositedEvent e: Balance = Balance.Add(e.Amount); - _transactions.Add - ( - new Transaction - ( - e.Id, - e.OccurredOn, - e.Amount, - e.Description, - TransactionType.Deposit - ) + _transactions.Add( + new Transaction(e.Id, e.OccurredOn, e.Amount, e.Description, TransactionType.Deposit) ); break; case MoneyWithdrawnEvent e: Balance = Balance.Subtract(e.Amount); - _transactions.Add - ( - new Transaction - ( - e.Id, - e.OccurredOn, - e.Amount, - e.Description, - TransactionType.Withdrawal - ) + _transactions.Add( + new Transaction(e.Id, e.OccurredOn, e.Amount, e.Description, TransactionType.Withdrawal) ); break; @@ -551,4 +526,4 @@ public override string GetCategory() return "bank-account"; } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs b/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs index 50966e0..f0561eb 100644 --- a/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs +++ b/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs @@ -7,8 +7,7 @@ public class InternalDomainEventTests [Fact] public void IInternalDomainEvent_ExtendsIDomainEvent() { - AggregateUpdated @event = new - ( + AggregateUpdated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "source", @@ -23,12 +22,12 @@ public void IInternalDomainEvent_ExtendsIDomainEvent() [Fact] public void IInternalDomainEvent_IsMarkerInterface_HasNoAdditionalMembers() { - IEnumerable internalMethods = typeof(IInternalDomainEvent).GetMethods() - .Where(m => !m.IsSpecialName); // Exclude property getters - IEnumerable internalProperties = typeof(IInternalDomainEvent).GetProperties() - .Where - (p => p.DeclaringType == typeof(IInternalDomainEvent) - ); // Only declared in IInternalDomainEvent + IEnumerable internalMethods = typeof(IInternalDomainEvent) + .GetMethods() + .Where(m => !m.IsSpecialName); // Exclude property getters + IEnumerable internalProperties = typeof(IInternalDomainEvent) + .GetProperties() + .Where(p => p.DeclaringType == typeof(IInternalDomainEvent)); // Only declared in IInternalDomainEvent // No additional members beyond IDomainEvent Assert.Empty(internalMethods); @@ -38,8 +37,7 @@ public void IInternalDomainEvent_IsMarkerInterface_HasNoAdditionalMembers() [Fact] public void ConstructionEvent_ImplementsIConstructionEvent() { - AggregateCreated @event = new - ( + AggregateCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "aggregate-123", @@ -56,8 +54,7 @@ public void ConstructionEvent_ImplementsIConstructionEvent() [Fact] public void CommandEvent_DoesNotImplementLifecycleMarkers() { - AggregateUpdated @event = new - ( + AggregateUpdated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "aggregate-123", @@ -74,8 +71,7 @@ public void CommandEvent_DoesNotImplementLifecycleMarkers() [Fact] public void DestructionEvent_ImplementsIDestructionEvent() { - AggregateDeleted @event = new - ( + AggregateDeleted @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "aggregate-123", @@ -107,17 +103,26 @@ public void LifecycleMarkers_AreMarkerInterfaces_HaveNoMembers() [Fact] public void IInternalDomainEvent_PatternMatching_WorksWithSwitchExpression() { - AggregateCreated constructionEvent = new - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, "agg-1", "Test", new Dictionary() + AggregateCreated constructionEvent = new( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + "agg-1", + "Test", + new Dictionary() ); - AggregateUpdated commandEvent = new - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, "agg-1", "Updated", new Dictionary() + AggregateUpdated commandEvent = new( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + "agg-1", + "Updated", + new Dictionary() ); - AggregateDeleted destructionEvent = new - ( - Guid.NewGuid(), DateTimeOffset.UtcNow, "agg-1", "Deleted", new Dictionary() + AggregateDeleted destructionEvent = new( + Guid.NewGuid(), + DateTimeOffset.UtcNow, + "agg-1", + "Deleted", + new Dictionary() ); string constructionResult = ClassifyEvent(constructionEvent); @@ -130,20 +135,19 @@ public void IInternalDomainEvent_PatternMatching_WorksWithSwitchExpression() return; // Helper methods for pattern matching test - static string ClassifyEvent(IInternalDomainEvent @event) - => @event switch + static string ClassifyEvent(IInternalDomainEvent @event) => + @event switch { IInternalDomainEvent.IConstructionEvent => "Construction", IInternalDomainEvent.IDestructionEvent => "Destruction", - _ => "Command" + _ => "Command", }; } [Fact] public void IInternalDomainEvent_TypeConstraint_WorksWithGenericMethods() { - AggregateUpdated @event = new - ( + AggregateUpdated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "source", @@ -156,8 +160,8 @@ public void IInternalDomainEvent_TypeConstraint_WorksWithGenericMethods() Assert.Equal(@event.Source, source); return; - static string GetEventSource(TEvent @event) where TEvent : IInternalDomainEvent - => @event.Source; + static string GetEventSource(TEvent @event) + where TEvent : IInternalDomainEvent => @event.Source; } [Fact] @@ -173,8 +177,7 @@ public void ConstructionEvent_NestedInterface_AccessibleViaParent() } // Construction event (R1 rule) - private record AggregateCreated - ( + private record AggregateCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -183,8 +186,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; // Command event (R2 rule) - private record AggregateUpdated - ( + private record AggregateUpdated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -193,12 +195,11 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent; // Destruction event (R3 rule) - private record AggregateDeleted - ( + private record AggregateDeleted( Guid Id, DateTimeOffset OccurredOn, string Source, string Reason, IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent; -} \ No newline at end of file +} diff --git a/tests/EzDdd.Entity.Tests/ValueObjectTests.cs b/tests/EzDdd.Entity.Tests/ValueObjectTests.cs index 66fe5d0..9c98d91 100644 --- a/tests/EzDdd.Entity.Tests/ValueObjectTests.cs +++ b/tests/EzDdd.Entity.Tests/ValueObjectTests.cs @@ -76,10 +76,11 @@ public void IValueObject_RecordWithMethod_WithExpression_CreatesNewInstance() } // Helper method demonstrating generic constraint usage - private static bool _ProcessValueObject(T valueObject) where T : IValueObject + private static bool _ProcessValueObject(T valueObject) + where T : IValueObject // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract { - return valueObject is not null; + return valueObject is not null; } // Record-based value object (recommended approach) @@ -93,9 +94,7 @@ private class MoneyClass(decimal amount, string currency) : IValueObject public override bool Equals(object? obj) { - return obj is MoneyClass other && - Amount == other.Amount && - Currency == other.Currency; + return obj is MoneyClass other && Amount == other.Amount && Currency == other.Currency; } public override int GetHashCode() @@ -103,4 +102,4 @@ public override int GetHashCode() return HashCode.Combine(Amount, Currency); } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs index 46d53f3..0ba0c4d 100644 --- a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs +++ b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs @@ -31,7 +31,7 @@ namespace EzDdd.Integration.Tests; /// public sealed class ConcurrentOperationsTests { -#region DomainEventTypeMapper Concurrent Registration Tests + #region DomainEventTypeMapper Concurrent Registration Tests [Fact] public async Task DomainEventTypeMapper_ConcurrentRegistration_ShouldBeThreadSafe() @@ -44,31 +44,28 @@ public async Task DomainEventTypeMapper_ConcurrentRegistration_ShouldBeThreadSaf for (int t = 0; t < threadCount; t++) { int threadId = t; - tasks.Add - ( - Task.Run - (() => + tasks.Add( + Task.Run(() => + { + for (int i = 0; i < registrationsPerThread; i++) { - for (int i = 0; i < registrationsPerThread; i++) + // Each thread registers unique event types + string eventTypeName = $"ConcurrentEvent_T{threadId:D2}_E{i:D2}"; + + // Create a unique event type dynamically + // For testing, we'll just register the same type multiple times + // (The mapper should handle duplicate registrations gracefully) + try + { + DomainEventTypeMapper.Register(eventTypeName); + } + catch { - // Each thread registers unique event types - string eventTypeName = $"ConcurrentEvent_T{threadId:D2}_E{i:D2}"; - - // Create a unique event type dynamically - // For testing, we'll just register the same type multiple times - // (The mapper should handle duplicate registrations gracefully) - try - { - DomainEventTypeMapper.Register(eventTypeName); - } - catch - { - // Duplicate registration may throw - that's acceptable - // We're testing thread safety, not duplicate handling - } + // Duplicate registration may throw - that's acceptable + // We're testing thread safety, not duplicate handling } } - ) + }) ); } @@ -77,9 +74,9 @@ public async Task DomainEventTypeMapper_ConcurrentRegistration_ShouldBeThreadSaf Assert.Equal(threadCount, tasks.Count); } -#endregion + #endregion -#region Mixed Concurrent Operations Tests + #region Mixed Concurrent Operations Tests [Fact] public async Task MixedOperations_ConcurrentSaveAndLoad_ShouldMaintainConsistency() @@ -104,45 +101,39 @@ public async Task MixedOperations_ConcurrentSaveAndLoad_ShouldMaintainConsistenc for (int i = 0; i < updateCount / 2; i++) { int updateId = i; - tasks.Add - ( - Task.Run - (async () => + tasks.Add( + Task.Run(async () => + { + try { - try - { - // Load current state - MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); - if (agg != null && !agg.IsClosed) - { - // Update value - agg.UpdateValue((updateId + 1) * 100); - await repository.SaveAsync(agg); - } - } - catch (RepositorySaveException) + // Load current state + MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); + if (agg != null && !agg.IsClosed) { - // Expected: Optimistic locking conflict due to concurrent updates - // Some updates will fail - this is correct behavior + // Update value + agg.UpdateValue((updateId + 1) * 100); + await repository.SaveAsync(agg); } } - ) + catch (RepositorySaveException) + { + // Expected: Optimistic locking conflict due to concurrent updates + // Some updates will fail - this is correct behavior + } + }) ); } // Other half performs reads for (int i = 0; i < updateCount / 2; i++) { - tasks.Add - ( - Task.Run - (async () => - { - MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); - Assert.NotNull(agg); - // Just verify we can read - } - ) + tasks.Add( + Task.Run(async () => + { + MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); + Assert.NotNull(agg); + // Just verify we can read + }) ); } @@ -158,9 +149,9 @@ public async Task MixedOperations_ConcurrentSaveAndLoad_ShouldMaintainConsistenc Assert.True(finalAgg.Value >= 0); } -#endregion + #endregion -#region Repository Concurrent Operations Tests + #region Repository Concurrent Operations Tests [Fact] public async Task Repository_ConcurrentSaves_ShouldHandleCorrectly() @@ -177,20 +168,16 @@ public async Task Repository_ConcurrentSaves_ShouldHandleCorrectly() for (int i = 0; i < concurrentAggregates; i++) { int aggId = i; - tasks.Add - ( - Task.Run - (async () => - { - MetadataTestAggregate agg = new - ( - new MetadataTestId($"CONCURRENT-AGG-{aggId:D3}"), - $"Aggregate {aggId}", - aggId * 100 - ); - await repository.SaveAsync(agg); - } - ) + tasks.Add( + Task.Run(async () => + { + MetadataTestAggregate agg = new( + new MetadataTestId($"CONCURRENT-AGG-{aggId:D3}"), + $"Aggregate {aggId}", + aggId * 100 + ); + await repository.SaveAsync(agg); + }) ); } @@ -212,12 +199,7 @@ public async Task Repository_ConcurrentLoadOperations_ShouldReturnCorrectData() const int aggregateCount = 10; for (int i = 0; i < aggregateCount; i++) { - MetadataTestAggregate agg = new - ( - new MetadataTestId($"AGG-LOAD-{i:D3}"), - $"Aggregate {i}", - i * 10 - ); + MetadataTestAggregate agg = new(new MetadataTestId($"AGG-LOAD-{i:D3}"), $"Aggregate {i}", i * 10); await repository.SaveAsync(agg); } @@ -243,5 +225,5 @@ public async Task Repository_ConcurrentLoadOperations_ShouldReturnCorrectData() } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs index 445819a..342b3f1 100644 --- a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; using EzDdd.Integration.Tests.TestDomain; using EzDdd.UseCase.Port.InOut; @@ -40,7 +39,7 @@ namespace EzDdd.Integration.Tests; /// public sealed class CqrsFlowWithMetadataTests { -#region Event Replay with Metadata Tests + #region Event Replay with Metadata Tests [Fact] public async Task EventReplay_ShouldPreserveMetadata() @@ -75,9 +74,9 @@ public async Task EventReplay_ShouldPreserveMetadata() Assert.Equal(correlationId, updatedEvent.Metadata["CorrelationId"]); } -#endregion + #endregion -#region Test Infrastructure Setup + #region Test Infrastructure Setup /// /// Creates the CQRS infrastructure with metadata tracking support. @@ -129,8 +128,7 @@ public async Task SaveAndPublishAsync(MetadataTestAggregate aggregate) /// /// Creates metadata with correlation and causation IDs for distributed tracing. /// - private static IReadOnlyDictionary _CreateMetadata - ( + private static IReadOnlyDictionary _CreateMetadata( string correlationId, string? causationId = null, string? userId = null @@ -151,9 +149,9 @@ private static IReadOnlyDictionary _CreateMetadata return new ReadOnlyDictionary(metadata); } -#endregion + #endregion -#region Metadata Creation and Propagation Tests + #region Metadata Creation and Propagation Tests [Fact] public async Task CreateAggregate_WithMetadata_ShouldPreserveMetadataInPublishedEvents() @@ -193,7 +191,11 @@ public async Task UpdateValue_WithMetadata_ShouldPreserveCausationChain() await infra.SaveAndPublishAsync(aggregate); // Act 2: Update value with causation ID linking to creation - IReadOnlyDictionary updateMetadata = _CreateMetadata(correlationId, creationEventId, "bob@example.com"); + IReadOnlyDictionary updateMetadata = _CreateMetadata( + correlationId, + creationEventId, + "bob@example.com" + ); aggregate.UpdateValue(75, updateMetadata); await infra.SaveAndPublishAsync(aggregate); @@ -226,12 +228,11 @@ public async Task MetadataWithSpecialCharacters_ShouldSerializeCorrectly() ["UserEmail"] = "test@example.com", ["RequestPath"] = "/api/test?filter=active&sort=name", ["SpecialChars"] = "hello \"world\" with 'quotes' and \\ backslash", - ["Unicode"] = "你好世界 🚀 Ñoño" + ["Unicode"] = "你好世界 🚀 Ñoño", }; // Act: Create aggregate with complex metadata - MetadataTestAggregate aggregate = new - ( + MetadataTestAggregate aggregate = new( id, "Complex Metadata Test", 123, @@ -241,10 +242,7 @@ public async Task MetadataWithSpecialCharacters_ShouldSerializeCorrectly() // Assert: Special characters should survive serialization round-trip Assert.Single(infra.PublishedEvents); - AggregateCreated deserialized = DomainEventMapper.ToDomain - ( - infra.PublishedEvents.First() - ); + AggregateCreated deserialized = DomainEventMapper.ToDomain(infra.PublishedEvents.First()); Assert.Equal(complexMetadata["UserEmail"], deserialized.Metadata["UserEmail"]); Assert.Equal(complexMetadata["RequestPath"], deserialized.Metadata["RequestPath"]); @@ -252,9 +250,9 @@ public async Task MetadataWithSpecialCharacters_ShouldSerializeCorrectly() Assert.Equal(complexMetadata["Unicode"], deserialized.Metadata["Unicode"]); } -#endregion + #endregion -#region Idempotency Detection Tests + #region Idempotency Detection Tests [Fact] public async Task DuplicateCorrelationId_ShouldBeDetectable() @@ -263,8 +261,7 @@ public async Task DuplicateCorrelationId_ShouldBeDetectable() string sharedCorrelationId = Guid.NewGuid().ToString(); // Act: Process same operation twice with same CorrelationId - MetadataTestAggregate agg1 = new - ( + MetadataTestAggregate agg1 = new( new MetadataTestId("AGG-IDEM-001"), "First", 100, @@ -272,8 +269,7 @@ public async Task DuplicateCorrelationId_ShouldBeDetectable() ); await infra.SaveAndPublishAsync(agg1); - MetadataTestAggregate agg2 = new - ( + MetadataTestAggregate agg2 = new( new MetadataTestId("AGG-IDEM-002"), "Second", 200, @@ -315,9 +311,9 @@ public void IdempotencyCheck_UsingCorrelationId_ShouldWork() Assert.Contains(correlationId, processedCorrelationIds); } -#endregion + #endregion -#region Complete Lifecycle Tests + #region Complete Lifecycle Tests [Fact] public async Task CompleteLifecycle_WithMetadata_ShouldPreserveMetadataThroughAllOperations() @@ -328,8 +324,7 @@ public async Task CompleteLifecycle_WithMetadata_ShouldPreserveMetadataThroughAl const string userId = "integration-test-user"; // Act 1: Create aggregate - MetadataTestAggregate aggregate = new - ( + MetadataTestAggregate aggregate = new( id, "Complete Lifecycle Test", 1000, @@ -382,8 +377,7 @@ public async Task EmptyMetadata_ShouldStillSerializeCorrectly() // Assert: Event should be published with empty metadata Assert.Single(infra.PublishedEvents); - AggregateCreated deserializedEvent = DomainEventMapper.ToDomain - ( + AggregateCreated deserializedEvent = DomainEventMapper.ToDomain( infra.PublishedEvents.First() ); @@ -391,5 +385,5 @@ public async Task EmptyMetadata_ShouldStillSerializeCorrectly() Assert.Empty(deserializedEvent.Metadata); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs index 65c88ec..ba52831 100644 --- a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; using EzDdd.Integration.Tests.TestDomain; using EzDdd.UseCase.Port.InOut; @@ -35,7 +34,7 @@ namespace EzDdd.Integration.Tests; /// public sealed class EventSourcingMetadataTests { -#region Version Control and Metadata Tests + #region Version Control and Metadata Tests [Fact] public async Task MultipleUpdates_ShouldPreserveMetadataIndependently() @@ -72,9 +71,9 @@ public async Task MultipleUpdates_ShouldPreserveMetadataIndependently() Assert.NotEqual(v1Event.Metadata["UserId"], v2Event.Metadata["UserId"]); } -#endregion + #endregion -#region Test Infrastructure + #region Test Infrastructure private static TestInfrastructure _CreateInfrastructure() { @@ -118,8 +117,7 @@ public async Task SaveAndPublishAsync(MetadataTestAggregate aggregate) } } - private static IReadOnlyDictionary _CreateMetadata - ( + private static IReadOnlyDictionary _CreateMetadata( string correlationId, string? causationId = null, string? userId = null, @@ -146,9 +144,9 @@ private static IReadOnlyDictionary _CreateMetadata return new ReadOnlyDictionary(metadata); } -#endregion + #endregion -#region Event Replay with Metadata Tests + #region Event Replay with Metadata Tests [Fact] public async Task SingleSaveLoadCycle_ShouldPreserveMetadata() @@ -172,8 +170,7 @@ public async Task SingleSaveLoadCycle_ShouldPreserveMetadata() // Assert: Events with metadata should be in the published events Assert.Single(infra.PublishedEvents); - AggregateCreated deserializedEvent = DomainEventMapper.ToDomain - ( + AggregateCreated deserializedEvent = DomainEventMapper.ToDomain( infra.PublishedEvents.First() ); Assert.Equal(correlationId, deserializedEvent.Metadata["CorrelationId"]); @@ -238,8 +235,7 @@ public async Task LargeEventStream_ShouldHandleMetadataCorrectly() const int eventCount = 50; // Create 50 events // Act: Create aggregate and perform many updates - MetadataTestAggregate aggregate = new - ( + MetadataTestAggregate aggregate = new( id, "Large Stream Test", 0, @@ -248,11 +244,7 @@ public async Task LargeEventStream_ShouldHandleMetadataCorrectly() for (int i = 1; i <= eventCount - 1; i++) { - aggregate.UpdateValue - ( - i * 10, - _CreateMetadata(correlationId, userId: $"user{i}", traceId: $"trace-{i:D3}") - ); + aggregate.UpdateValue(i * 10, _CreateMetadata(correlationId, userId: $"user{i}", traceId: $"trace-{i:D3}")); } await infra.SaveAndPublishAsync(aggregate); @@ -268,26 +260,20 @@ public async Task LargeEventStream_ShouldHandleMetadataCorrectly() Assert.Equal(eventCount, infra.PublishedEvents.Count); // Verify first and last events have correct metadata - AggregateCreated firstEvent = DomainEventMapper.ToDomain - ( - infra.PublishedEvents.First() - ); + AggregateCreated firstEvent = DomainEventMapper.ToDomain(infra.PublishedEvents.First()); Assert.Equal(correlationId, firstEvent.Metadata["CorrelationId"]); Assert.Equal("system", firstEvent.Metadata["UserId"]); Assert.Equal("trace-001", firstEvent.Metadata["TraceId"]); - ValueUpdated lastEvent = DomainEventMapper.ToDomain - ( - infra.PublishedEvents.Last() - ); + ValueUpdated lastEvent = DomainEventMapper.ToDomain(infra.PublishedEvents.Last()); Assert.Equal(correlationId, lastEvent.Metadata["CorrelationId"]); Assert.Equal($"user{eventCount - 1}", lastEvent.Metadata["UserId"]); Assert.Equal($"trace-{eventCount - 1:D3}", lastEvent.Metadata["TraceId"]); } -#endregion + #endregion -#region Event Store Persistence Tests + #region Event Store Persistence Tests [Fact] public async Task EventStore_ShouldPersistMetadataInEventStoreData() @@ -297,8 +283,7 @@ public async Task EventStore_ShouldPersistMetadataInEventStoreData() string correlationId = Guid.NewGuid().ToString(); // Act: Create and save aggregate - MetadataTestAggregate aggregate = new - ( + MetadataTestAggregate aggregate = new( id, "Persistence Test", 200, @@ -314,10 +299,7 @@ public async Task EventStore_ShouldPersistMetadataInEventStoreData() // Verify the stored event has correct type and metadata via the published events // (EventStoreData stores IDomainEvent, which includes metadata) Assert.Single(infra.PublishedEvents); - AggregateCreated publishedEvent = DomainEventMapper.ToDomain - ( - infra.PublishedEvents.First() - ); + AggregateCreated publishedEvent = DomainEventMapper.ToDomain(infra.PublishedEvents.First()); Assert.Equal(correlationId, publishedEvent.Metadata["CorrelationId"]); Assert.Equal("alice", publishedEvent.Metadata["UserId"]); @@ -371,9 +353,9 @@ public async Task EventStoreAppend_ShouldAccumulateEventsWithMetadata() Assert.Equal(30, final.Value); } -#endregion + #endregion -#region Metadata Consistency Tests + #region Metadata Consistency Tests [Fact] public async Task ReloadedAggregate_ShouldHaveNoUnpublishedEvents() @@ -403,12 +385,11 @@ public async Task MetadataRoundTrip_ThroughSerialization_ShouldBeIdentical() ["CorrelationId"] = "abc-123", ["UserId"] = "test@example.com", ["TraceId"] = "trace-456", - ["CustomKey"] = "CustomValue with spaces and 特殊字符" + ["CustomKey"] = "CustomValue with spaces and 特殊字符", }; // Act: Create aggregate with specific metadata - MetadataTestAggregate aggregate = new - ( + MetadataTestAggregate aggregate = new( id, "Round Trip Test", 999, @@ -418,8 +399,7 @@ public async Task MetadataRoundTrip_ThroughSerialization_ShouldBeIdentical() // Assert: Deserialized event should have identical metadata Assert.Single(infra.PublishedEvents); - AggregateCreated deserializedEvent = DomainEventMapper.ToDomain - ( + AggregateCreated deserializedEvent = DomainEventMapper.ToDomain( infra.PublishedEvents.First() ); @@ -432,5 +412,5 @@ public async Task MetadataRoundTrip_ThroughSerialization_ShouldBeIdentical() } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Integration.Tests/ReconcilerExecutionTests.cs b/tests/EzDdd.Integration.Tests/ReconcilerExecutionTests.cs index c1c60d9..de18aea 100644 --- a/tests/EzDdd.Integration.Tests/ReconcilerExecutionTests.cs +++ b/tests/EzDdd.Integration.Tests/ReconcilerExecutionTests.cs @@ -30,7 +30,7 @@ namespace EzDdd.Integration.Tests; /// public sealed class ReconcilerExecutionTests { -#region Realistic Workflow Tests + #region Realistic Workflow Tests [Fact] public async Task CleanupWorkflow_WithRealisticData_ShouldWorkEndToEnd() @@ -44,46 +44,19 @@ public async Task CleanupWorkflow_WithRealisticData_ShouldWorkEndToEnd() // Add 10 draft orders created 30-60 days ago (expired) for (int i = 1; i <= 10; i++) { - repository.Add - ( - new DataItem - ( - $"ORDER-DRAFT-{i:D3}", - "DRAFT", - now.AddDays(-30 - i), - now.AddDays(-i) - ) - ); // Expired i days ago + repository.Add(new DataItem($"ORDER-DRAFT-{i:D3}", "DRAFT", now.AddDays(-30 - i), now.AddDays(-i))); // Expired i days ago } // Add 5 active orders (should not be deleted) for (int i = 1; i <= 5; i++) { - repository.Add - ( - new DataItem - ( - $"ORDER-ACTIVE-{i:D3}", - "ACTIVE", - now.AddDays(-10), - now.AddDays(30) - ) - ); // Expires in future + repository.Add(new DataItem($"ORDER-ACTIVE-{i:D3}", "ACTIVE", now.AddDays(-10), now.AddDays(30))); // Expires in future } // Add 3 pending orders (different status, should not be deleted even if expired) for (int i = 1; i <= 3; i++) { - repository.Add - ( - new DataItem - ( - $"ORDER-PENDING-{i:D3}", - "PENDING", - now.AddDays(-40), - now.AddDays(-5) - ) - ); + repository.Add(new DataItem($"ORDER-PENDING-{i:D3}", "PENDING", now.AddDays(-40), now.AddDays(-5))); } CleanupContext context = new(now, "DRAFT"); @@ -109,9 +82,9 @@ public async Task CleanupWorkflow_WithRealisticData_ShouldWorkEndToEnd() Assert.Null(await repository.FindByIdAsync("ORDER-DRAFT-001")); } -#endregion + #endregion -#region Basic Reconciler Execution Tests + #region Basic Reconciler Execution Tests [Fact] public async Task Reconciler_WithNoExpiredItems_ShouldReturnEmptyReport() @@ -188,16 +161,7 @@ public async Task Reconciler_WithMixedResults_ShouldReportAllOutcomes() // Add 5 expired draft items for (int i = 1; i <= 5; i++) { - repository.Add - ( - new DataItem - ( - $"DRAFT-{i:D3}", - "DRAFT", - now.AddDays(-30), - now.AddDays(-10) - ) - ); + repository.Add(new DataItem($"DRAFT-{i:D3}", "DRAFT", now.AddDays(-30), now.AddDays(-10))); } CleanupContext context = new(now, "DRAFT"); @@ -215,9 +179,9 @@ public async Task Reconciler_WithMixedResults_ShouldReportAllOutcomes() Assert.Equal(0, repository.Count); } -#endregion + #endregion -#region NullContext Reconciler Tests + #region NullContext Reconciler Tests [Fact] public async Task NullContextReconciler_ShouldExecuteCorrectly() @@ -253,9 +217,9 @@ public async Task NullContextReconciler_WithEmptyRepository_ShouldReportEmpty() Assert.Equal("EMPTY", report.Status); } -#endregion + #endregion -#region Context Validation Tests + #region Context Validation Tests [Fact] public async Task Reconciler_WithNullContext_ShouldThrowArgumentNullException() @@ -264,10 +228,7 @@ public async Task Reconciler_WithNullContext_ShouldThrowArgumentNullException() ExpiredDataCleanupReconciler reconciler = new(repository); // Act & Assert - await Assert.ThrowsAsync - (() => - reconciler.ReconcileAsync(null!) - ); + await Assert.ThrowsAsync(() => reconciler.ReconcileAsync(null!)); } [Fact] @@ -284,10 +245,8 @@ public async Task Reconciler_WithFutureCutoffDate_ShouldThrowInvalidOperationExc ); // Act & Assert - await Assert.ThrowsAsync( - () => reconciler.ReconcileAsync(invalidContext) - ); + await Assert.ThrowsAsync(() => reconciler.ReconcileAsync(invalidContext)); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.Integration.Tests/TestDomain/InMemoryMetadataTestEventStorePeer.cs b/tests/EzDdd.Integration.Tests/TestDomain/InMemoryMetadataTestEventStorePeer.cs index f593413..88c0d58 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/InMemoryMetadataTestEventStorePeer.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/InMemoryMetadataTestEventStorePeer.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; - using EzDdd.Entity; using EzDdd.UseCase.Exceptions; using EzDdd.UseCase.Port.Out; @@ -30,8 +29,7 @@ public Task SaveAsync(EventStoreData data) { if (existing.Version != data.Version - 1) { - throw new RepositoryPeerSaveException - ( + throw new RepositoryPeerSaveException( $"Optimistic lock failure: expected version {existing.Version}, but got {data.Version - 1}", new InvalidOperationException("Version mismatch") ); @@ -43,7 +41,10 @@ public Task SaveAsync(EventStoreData data) _storage[data.Id.Value] = new EventStoreData { - Id = data.Id, Version = data.Version, Events = allEvents, StreamName = data.StreamName + Id = data.Id, + Version = data.Version, + Events = allEvents, + StreamName = data.StreamName, }; } else @@ -72,4 +73,4 @@ public bool Contains(MetadataTestId id) { return _storage.ContainsKey(id.Value); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestAggregate.cs b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestAggregate.cs index 8287de1..c43051b 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestAggregate.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestAggregate.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; namespace EzDdd.Integration.Tests.TestDomain; @@ -31,8 +30,7 @@ public sealed class MetadataTestAggregate : EsAggregateRootAggregate name /// Initial value /// Metadata to attach to the creation event - public MetadataTestAggregate - ( + public MetadataTestAggregate( MetadataTestId id, string name, int initialValue, @@ -43,14 +41,10 @@ public MetadataTestAggregate ArgumentNullException.ThrowIfNull(name); Id = id; - AggregateCreated @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - id, - name, - initialValue - ) { Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()) }; + AggregateCreated @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, id, name, initialValue) + { + Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()), + }; Apply(@event); } @@ -58,9 +52,7 @@ public MetadataTestAggregate /// Constructor for event replay. /// public MetadataTestAggregate(IEnumerable events) - : base(events) - { - } + : base(events) { } // Properties public string Name { get; private set; } = string.Empty; @@ -79,14 +71,10 @@ public void UpdateValue(int newValue, IReadOnlyDictionary? metad throw new InvalidOperationException("Cannot update a closed aggregate"); } - ValueUpdated @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - Value, - newValue - ) { Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()) }; + ValueUpdated @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, Value, newValue) + { + Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()), + }; Apply(@event); } @@ -104,13 +92,10 @@ public void Close(string reason, IReadOnlyDictionary? metadata = ArgumentNullException.ThrowIfNull(reason); - AggregateClosed @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - reason - ) { Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()) }; + AggregateClosed @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, reason) + { + Metadata = metadata ?? new ReadOnlyDictionary(new Dictionary()), + }; Apply(@event); } @@ -157,4 +142,4 @@ public override string GetCategory() { return "metadata-test"; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestEvents.cs b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestEvents.cs index 8cf1d58..009f059 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestEvents.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestEvents.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; namespace EzDdd.Integration.Tests.TestDomain; @@ -7,8 +6,7 @@ namespace EzDdd.Integration.Tests.TestDomain; /// /// Aggregate created event with metadata support (construction event). /// -public sealed record AggregateCreated -( +public sealed record AggregateCreated( Guid Id, DateTimeOffset OccurredOn, MetadataTestId AggregateId, @@ -28,8 +26,7 @@ int InitialValue /// /// Value updated event with metadata support. /// -public sealed record ValueUpdated -( +public sealed record ValueUpdated( Guid Id, DateTimeOffset OccurredOn, MetadataTestId AggregateId, @@ -49,13 +46,9 @@ int NewValue /// /// Aggregate closed event with metadata support (destruction event). /// -public sealed record AggregateClosed -( - Guid Id, - DateTimeOffset OccurredOn, - MetadataTestId AggregateId, - string Reason -) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent +public sealed record AggregateClosed(Guid Id, DateTimeOffset OccurredOn, MetadataTestId AggregateId, string Reason) + : IInternalDomainEvent, + IInternalDomainEvent.IDestructionEvent { public string Source => AggregateId.Value; @@ -64,4 +57,4 @@ string Reason /// public IReadOnlyDictionary Metadata { get; init; } = new ReadOnlyDictionary(new Dictionary()); -} \ No newline at end of file +} diff --git a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestId.cs b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestId.cs index 9aeb630..f901a9e 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestId.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/MetadataTestId.cs @@ -9,4 +9,4 @@ public override string ToString() { return Value; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs b/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs index 8e43e5e..061f004 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs @@ -5,13 +5,7 @@ namespace EzDdd.Integration.Tests.TestDomain; /// /// Simple data item for reconciler testing. /// -public sealed record DataItem -( - string Id, - string Status, - DateTimeOffset CreatedAt, - DateTimeOffset? ExpiresAt = null -); +public sealed record DataItem(string Id, string Status, DateTimeOffset CreatedAt, DateTimeOffset? ExpiresAt = null); /// /// In-memory repository for testing reconciler operations. @@ -36,17 +30,15 @@ public void Add(DataItem item) public Task> FindByStatusAsync(string status) { - List items = _storage.Values - .Where(i => i.Status == status) - .ToList(); + List items = _storage.Values.Where(i => i.Status == status).ToList(); return Task.FromResult(items); } public Task> FindExpiredAsync(DateTimeOffset cutoffDate) { - List items = _storage.Values - .Where(i => i.ExpiresAt.HasValue && i.ExpiresAt.Value < cutoffDate) - .ToList(); + List items = _storage + .Values.Where(i => i.ExpiresAt.HasValue && i.ExpiresAt.Value < cutoffDate) + .ToList(); return Task.FromResult(items); } @@ -70,8 +62,7 @@ public sealed record CleanupContext(DateTimeOffset CutoffDate, string TargetStat /// /// Report for cleanup reconciliation results. /// -public sealed record CleanupReport -( +public sealed record CleanupReport( int TotalChecked, int DeletedCount, int SkippedCount, @@ -109,9 +100,7 @@ public async Task ReconcileAsync(CleanupContext context) // Filter by target status if specified if (!string.IsNullOrEmpty(context.TargetStatus)) { - expiredItems = expiredItems - .Where(i => i.Status == context.TargetStatus) - .ToList(); + expiredItems = expiredItems.Where(i => i.Status == context.TargetStatus).ToList(); } int totalChecked = expiredItems.Count; @@ -137,15 +126,7 @@ public async Task ReconcileAsync(CleanupContext context) } } - return new CleanupReport - ( - totalChecked, - deletedCount, - skippedCount, - errorCount, - deletedIds, - errors - ); + return new CleanupReport(totalChecked, deletedCount, skippedCount, errorCount, deletedIds, errors); } } @@ -167,12 +148,7 @@ public Task ReconcileAsync(NullContext context) ArgumentNullException.ThrowIfNull(context); int totalItems = _repository.Count; - StatusCheckReport report = new - ( - totalItems, - DateTimeOffset.UtcNow, - totalItems > 0 ? "OK" : "EMPTY" - ); + StatusCheckReport report = new(totalItems, DateTimeOffset.UtcNow, totalItems > 0 ? "OK" : "EMPTY"); return Task.FromResult(report); } @@ -181,9 +157,4 @@ public Task ReconcileAsync(NullContext context) /// /// Report for simple status check. /// -public sealed record StatusCheckReport -( - int TotalItems, - DateTimeOffset CheckedAt, - string Status -); \ No newline at end of file +public sealed record StatusCheckReport(int TotalItems, DateTimeOffset CheckedAt, string Status); diff --git a/tests/EzDdd.UseCase.Tests/Exceptions/PostEventFailureExceptionTests.cs b/tests/EzDdd.UseCase.Tests/Exceptions/PostEventFailureExceptionTests.cs index 5630b5c..060c80b 100644 --- a/tests/EzDdd.UseCase.Tests/Exceptions/PostEventFailureExceptionTests.cs +++ b/tests/EzDdd.UseCase.Tests/Exceptions/PostEventFailureExceptionTests.cs @@ -37,4 +37,4 @@ public void Constructor_WithInnerException() Assert.NotNull(exception.InnerException); Assert.Equal("Network connection lost", exception.InnerException.Message); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Exceptions/RepositoryPeerSaveExceptionTests.cs b/tests/EzDdd.UseCase.Tests/Exceptions/RepositoryPeerSaveExceptionTests.cs index fedc3a1..96ec22c 100644 --- a/tests/EzDdd.UseCase.Tests/Exceptions/RepositoryPeerSaveExceptionTests.cs +++ b/tests/EzDdd.UseCase.Tests/Exceptions/RepositoryPeerSaveExceptionTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.UseCase.Tests.Exceptions; public class RepositoryPeerSaveExceptionTests { -#region Constructor Tests + #region Constructor Tests [Fact] public void Constructor_NoArgs_CreatesException() @@ -48,9 +48,9 @@ public void Constructor_WithInnerExceptionOnly_UsesInnerMessage() Assert.Same(innerException, exception.InnerException); } -#endregion + #endregion -#region Type Hierarchy Tests + #region Type Hierarchy Tests [Fact] public void ExceptionCanBeCaughtAsBaseException() @@ -61,17 +61,16 @@ public void ExceptionCanBeCaughtAsBaseException() Assert.IsType(exception); } -#endregion + #endregion -#region Exception Translation Tests + #region Exception Translation Tests [Fact] public void ExceptionTranslation_PeerToRepository_WorksCorrectly() { var peerException = new RepositoryPeerSaveException("Database error"); - var repositoryException = new RepositorySaveException - ( + var repositoryException = new RepositorySaveException( RepositorySaveException.OptimisticLockingFailure, peerException ); @@ -81,5 +80,5 @@ public void ExceptionTranslation_PeerToRepository_WorksCorrectly() Assert.IsType(repositoryException.InnerException); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Exceptions/RepositorySaveExceptionTests.cs b/tests/EzDdd.UseCase.Tests/Exceptions/RepositorySaveExceptionTests.cs index bd203fc..b48d193 100644 --- a/tests/EzDdd.UseCase.Tests/Exceptions/RepositorySaveExceptionTests.cs +++ b/tests/EzDdd.UseCase.Tests/Exceptions/RepositorySaveExceptionTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.UseCase.Tests.Exceptions; public class RepositorySaveExceptionTests { -#region Constructor Tests + #region Constructor Tests [Fact] public void Constructor_NoArgs_CreatesException() @@ -48,9 +48,9 @@ public void Constructor_WithInnerExceptionOnly_UsesInnerMessage() Assert.Same(innerException, exception.InnerException); } -#endregion + #endregion -#region Optimistic Locking Tests + #region Optimistic Locking Tests [Fact] public void OptimisticLockingFailure_Constant_HasCorrectValue() @@ -66,9 +66,9 @@ public void ThrowWithOptimisticLockingFailure_ContainsCorrectMessage() Assert.Equal("Optimistic locking failure", exception.Message); } -#endregion + #endregion -#region Type Hierarchy Tests + #region Type Hierarchy Tests [Fact] public void ExceptionCanBeCaughtAsBaseException() @@ -79,5 +79,5 @@ public void ExceptionCanBeCaughtAsBaseException() Assert.IsType(exception); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Exceptions/UseCaseFailureExceptionTests.cs b/tests/EzDdd.UseCase.Tests/Exceptions/UseCaseFailureExceptionTests.cs index 7b342e7..196fd6b 100644 --- a/tests/EzDdd.UseCase.Tests/Exceptions/UseCaseFailureExceptionTests.cs +++ b/tests/EzDdd.UseCase.Tests/Exceptions/UseCaseFailureExceptionTests.cs @@ -51,14 +51,12 @@ static void ThrowException() [Fact] public async Task UseCaseFailureException_CanBeThrownAsync() { - UseCaseFailureException exception = await Assert.ThrowsAsync - (async () => - { - await Task.Yield(); - throw new UseCaseFailureException("Async test exception"); - } - ); + UseCaseFailureException exception = await Assert.ThrowsAsync(async () => + { + await Task.Yield(); + throw new UseCaseFailureException("Async test exception"); + }); Assert.Equal("Async test exception", exception.Message); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/CrossComponentIntegrationTests.cs b/tests/EzDdd.UseCase.Tests/Integration/CrossComponentIntegrationTests.cs index 2a4ed68..2cea2b4 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/CrossComponentIntegrationTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/CrossComponentIntegrationTests.cs @@ -149,4 +149,4 @@ public async Task CompleteEventSourcingLifecycle_MultipleOperations_WorksCorrect Assert.True(finalAccount.IsClosed); Assert.Equal(1300m, finalAccount.Balance.Amount); // 1000 + 500 - 200 } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/EventInfrastructureTests.cs b/tests/EzDdd.UseCase.Tests/Integration/EventInfrastructureTests.cs index 9e3021e..19878b5 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/EventInfrastructureTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/EventInfrastructureTests.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using System.Text.Json; - using EzDdd.Entity; using EzDdd.UseCase.Port.InOut; using EzDdd.UseCase.Port.Out; @@ -25,14 +24,16 @@ public void CompleteEventSerializationRoundTrip_PreservesAllProperties() AccountId accountId = new("acc-001"); Dictionary metadata = new() { ["TransactionId"] = "tx-12345", ["IpAddress"] = "192.168.1.1" }; - AccountCreated originalEvent = new - ( + AccountCreated originalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, accountId, "John Doe", new Money(1000m) - ) { Metadata = new ReadOnlyDictionary(metadata) }; + ) + { + Metadata = new ReadOnlyDictionary(metadata), + }; DomainEventData eventData = DomainEventMapper.ToData(originalEvent); @@ -59,8 +60,7 @@ public void CompleteEventSerializationRoundTrip_PreservesAllProperties() [Fact] public void ExternalDomainEvent_IsProperlyDistinguishedFromInternal() { - AccountCreated internalEvent = new - ( + AccountCreated internalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, new AccountId("acc-001"), @@ -68,13 +68,7 @@ public void ExternalDomainEvent_IsProperlyDistinguishedFromInternal() new Money(1000m) ); - PaymentReceived externalEvent = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - "payment-001", - 500m - ); + PaymentReceived externalEvent = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "payment-001", 500m); Assert.IsAssignableFrom(internalEvent); Assert.IsNotAssignableFrom(internalEvent); @@ -92,16 +86,16 @@ public void DomainEventData_WithMetadata_PreservesAllData() { Dictionary metadata = new() { - ["User"] = "admin@example.com", ["SessionId"] = "sess-99999", ["Platform"] = "Windows", ["Browser"] = "Chrome" + ["User"] = "admin@example.com", + ["SessionId"] = "sess-99999", + ["Platform"] = "Windows", + ["Browser"] = "Chrome", }; - MoneyDeposited @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - new AccountId("acc-002"), - new Money(250m) - ) { Metadata = new ReadOnlyDictionary(metadata) }; + MoneyDeposited @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, new AccountId("acc-002"), new Money(250m)) + { + Metadata = new ReadOnlyDictionary(metadata), + }; DomainEventData eventData = DomainEventMapper.ToData(@event); @@ -128,16 +122,23 @@ public void InternalDomainEventDto_ConvertsToJsonFriendlyFormat() { Dictionary metadata = new() { ["Channel"] = "Web" }; - AccountCreated @event = new - ( + AccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, new AccountId("acc-003"), "Jane Doe", new Money(5000m) - ) { Metadata = new ReadOnlyDictionary(metadata) }; + ) + { + Metadata = new ReadOnlyDictionary(metadata), + }; - var eventData = new { AccountId = @event.AccountId.Value, @event.Owner, InitialBalance = @event.InitialBalance.Amount }; + var eventData = new + { + AccountId = @event.AccountId.Value, + @event.Owner, + InitialBalance = @event.InitialBalance.Amount, + }; string jsonEvent = JsonSerializer.Serialize(eventData); InternalDomainEventDto dto = new() @@ -147,7 +148,7 @@ public void InternalDomainEventDto_ConvertsToJsonFriendlyFormat() BoundedContext = "banking", // Bounded context for event routing EventSimpleName = DomainEventTypeMapper.GetTypeName(@event), JsonEvent = jsonEvent, - Metadata = @event.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value) // String-to-string dictionary + Metadata = @event.Metadata.ToDictionary(kv => kv.Key, kv => kv.Value), // String-to-string dictionary }; Assert.Equal(@event.Id, dto.Id); @@ -191,7 +192,10 @@ public void EventStoreData_HasCorrectPersistenceFormat() List events = account.GetDomainEvents().ToList(); EventStoreData eventStoreData = new() { - Id = accountId, StreamName = $"{account.GetCategory()}-{accountId.Value}", Events = events, Version = account.Version + Id = accountId, + StreamName = $"{account.GetCategory()}-{accountId.Value}", + Events = events, + Version = account.Version, }; Assert.Equal(accountId, eventStoreData.Id); @@ -227,17 +231,12 @@ public void DomainEventTypeMapper_BidirectionalMapping_WorksCorrectly() } // Test domain: External domain event example - private sealed record PaymentReceived - ( - Guid Id, - DateTimeOffset OccurredOn, - string PaymentId, - decimal Amount - ) : IExternalDomainEvent + private sealed record PaymentReceived(Guid Id, DateTimeOffset OccurredOn, string PaymentId, decimal Amount) + : IExternalDomainEvent { public string Source => "PaymentService"; public IReadOnlyDictionary Metadata { get; } = new ReadOnlyDictionary(new Dictionary()); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/EventSourcingWorkflowTests.cs b/tests/EzDdd.UseCase.Tests/Integration/EventSourcingWorkflowTests.cs index 726f2c7..bc6e7a7 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/EventSourcingWorkflowTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/EventSourcingWorkflowTests.cs @@ -116,12 +116,10 @@ public void WithdrawBeyondBalance_ViolatesInvariant_ThrowsException() AccountId accountId = new("acc-006"); BankAccount account = new(accountId, "David Lee", new Money(100m)); - InvalidOperationException exception = Assert.Throws - (() => - { - account.Withdraw(new Money(200m)); // Withdraw more than balance - } - ); + InvalidOperationException exception = Assert.Throws(() => + { + account.Withdraw(new Money(200m)); // Withdraw more than balance + }); Assert.Contains("balance cannot be negative", exception.Message); } @@ -160,12 +158,10 @@ public async Task ConcurrentSave_OptimisticLock_ThrowsException() await _repository.SaveAsync(account2); account3.Deposit(new Money(200m)); - await Assert.ThrowsAsync - (async () => - { - await _repository.SaveAsync(account3); - } - ); + await Assert.ThrowsAsync(async () => + { + await _repository.SaveAsync(account3); + }); } [Fact] @@ -206,4 +202,4 @@ public async Task MultipleOperations_CompleteWorkflow_Success() Assert.Equal(6000m, reloaded.Balance.Amount); Assert.Equal(5, reloaded.Version); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/ITransferMoneyService.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/ITransferMoneyService.cs index 5064c73..cd5ee31 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/ITransferMoneyService.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/ITransferMoneyService.cs @@ -22,8 +22,5 @@ public interface ITransferMoneyService /// When amount is zero or negative /// When either account is closed /// When attempting to transfer to the same account - Task TransferAsync( - AccountId fromAccountId, - AccountId toAccountId, - Money amount); + Task TransferAsync(AccountId fromAccountId, AccountId toAccountId, Money amount); } diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferConfirmation.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferConfirmation.cs index bbeb60d..d39b18f 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferConfirmation.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferConfirmation.cs @@ -11,7 +11,8 @@ public sealed record TransferConfirmation( AccountId FromAccountId, AccountId ToAccountId, Money Amount, - DateTimeOffset Timestamp); + DateTimeOffset Timestamp +); /// /// Transfer status enumeration. @@ -19,5 +20,5 @@ public sealed record TransferConfirmation( public enum TransferStatus { Success, - Failed + Failed, } diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferExceptions.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferExceptions.cs index e56bcd2..5213a33 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferExceptions.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferExceptions.cs @@ -21,11 +21,10 @@ public AccountNotFoundException(AccountId accountId) /// public sealed class InsufficientBalanceException : Exception { - public InsufficientBalanceException( - AccountId accountId, - Money currentBalance, - Money requestedAmount) - : base($"Insufficient balance in account {accountId.Value}. Current: {currentBalance}, Requested: {requestedAmount}") + public InsufficientBalanceException(AccountId accountId, Money currentBalance, Money requestedAmount) + : base( + $"Insufficient balance in account {accountId.Value}. Current: {currentBalance}, Requested: {requestedAmount}" + ) { AccountId = accountId; CurrentBalance = currentBalance; diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyService.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyService.cs index 6594de1..eab0c7c 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyService.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyService.cs @@ -18,10 +18,7 @@ public TransferMoneyService(IRepository TransferAsync( - AccountId fromAccountId, - AccountId toAccountId, - Money amount) + public async Task TransferAsync(AccountId fromAccountId, AccountId toAccountId, Money amount) { // 1. Validate inputs (fail fast) ValidateTransferAmount(amount); @@ -61,7 +58,8 @@ public async Task TransferAsync( FromAccountId: fromAccountId, ToAccountId: toAccountId, Amount: amount, - Timestamp: DateTimeOffset.UtcNow); + Timestamp: DateTimeOffset.UtcNow + ); } #region Private Validation Methods diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs index 5f684d3..f5d46bc 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs @@ -12,7 +12,8 @@ public sealed class TransferMoneyServiceTests #region Setup private static ITransferMoneyService CreateService( - IRepository repository) + IRepository repository + ) { return new TransferMoneyService(repository); } @@ -20,12 +21,10 @@ private static ITransferMoneyService CreateService( private static async Task CreateAndSaveAccount( IRepository repository, string owner, - Money initialBalance) + Money initialBalance + ) { - var account = new BankAccount( - new AccountId(Guid.NewGuid().ToString()), - owner, - initialBalance); + var account = new BankAccount(new AccountId(Guid.NewGuid().ToString()), owner, initialBalance); await repository.SaveAsync(account); return account; @@ -136,8 +135,9 @@ public async Task TransferAsync_WithInsufficientBalance_ThrowsException() var transferAmount = new Money(200, "USD"); // Act & Assert - var exception = await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + var exception = await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); Assert.Equal(fromAccount.Id, exception.AccountId); Assert.Equal(new Money(100, "USD"), exception.CurrentBalance); @@ -156,8 +156,9 @@ public async Task TransferAsync_WithZeroAmount_ThrowsException() var transferAmount = new Money(0, "USD"); // Act & Assert - await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); } [Fact] @@ -172,8 +173,9 @@ public async Task TransferAsync_WithNegativeAmount_ThrowsException() var transferAmount = new Money(-100, "USD"); // Act & Assert - await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); } [Fact] @@ -188,8 +190,9 @@ public async Task TransferAsync_ExceedingTransferLimit_ThrowsException() var transferAmount = new Money(15000, "USD"); // Exceeds $10,000 limit // Act & Assert - var exception = await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + var exception = await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); Assert.Equal(new Money(15000, "USD"), exception.RequestedAmount); Assert.Equal(new Money(10000, "USD"), exception.MaxLimit); @@ -211,8 +214,9 @@ public async Task TransferAsync_FromClosedAccount_ThrowsException() var transferAmount = new Money(100, "USD"); // Act & Assert - await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); } [Fact] @@ -231,8 +235,9 @@ public async Task TransferAsync_ToClosedAccount_ThrowsException() var transferAmount = new Money(100, "USD"); // Act & Assert - await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount)); + await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, toAccount.Id, transferAmount) + ); } #endregion @@ -251,8 +256,9 @@ public async Task TransferAsync_FromAccountNotFound_ThrowsException() var transferAmount = new Money(100, "USD"); // Act & Assert - var exception = await Assert.ThrowsAsync( - () => service.TransferAsync(nonExistentAccountId, toAccount.Id, transferAmount)); + var exception = await Assert.ThrowsAsync(() => + service.TransferAsync(nonExistentAccountId, toAccount.Id, transferAmount) + ); Assert.Equal(nonExistentAccountId, exception.AccountId); } @@ -269,8 +275,9 @@ public async Task TransferAsync_ToAccountNotFound_ThrowsException() var transferAmount = new Money(100, "USD"); // Act & Assert - var exception = await Assert.ThrowsAsync( - () => service.TransferAsync(fromAccount.Id, nonExistentAccountId, transferAmount)); + var exception = await Assert.ThrowsAsync(() => + service.TransferAsync(fromAccount.Id, nonExistentAccountId, transferAmount) + ); Assert.Equal(nonExistentAccountId, exception.AccountId); } @@ -290,8 +297,9 @@ public async Task TransferAsync_SameAccount_ThrowsException() var transferAmount = new Money(100, "USD"); // Act & Assert - await Assert.ThrowsAsync( - () => service.TransferAsync(account.Id, account.Id, transferAmount)); + await Assert.ThrowsAsync(() => + service.TransferAsync(account.Id, account.Id, transferAmount) + ); } [Fact] diff --git a/tests/EzDdd.UseCase.Tests/Integration/StateSourcingWorkflowTests.cs b/tests/EzDdd.UseCase.Tests/Integration/StateSourcingWorkflowTests.cs index d618c13..2c46b1e 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/StateSourcingWorkflowTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/StateSourcingWorkflowTests.cs @@ -142,12 +142,10 @@ public void BusinessRuleViolation_ThrowsException() OrderId orderId = new("order-007"); Order order = new(orderId, "Grace Kim"); - InvalidOperationException exception = Assert.Throws - (() => - { - order.Confirm(); - } - ); + InvalidOperationException exception = Assert.Throws(() => + { + order.Confirm(); + }); Assert.Contains("no items", exception.Message); } @@ -169,12 +167,10 @@ public async Task ConcurrentSave_OptimisticLock_ThrowsException() await _repository.SaveAsync(order2); order3.AddItem("Item B", 1, 200m); - await Assert.ThrowsAsync - (async () => - { - await _repository.SaveAsync(order3); - } - ); + await Assert.ThrowsAsync(async () => + { + await _repository.SaveAsync(order3); + }); } [Fact] @@ -216,4 +212,4 @@ public async Task CompleteWorkflow_MultipleOperations_Success() Assert.Equal(3, reloaded.Items.Count); Assert.Equal(4, reloaded.Version); // + Confirm } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/AccountId.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/AccountId.cs index 6d67243..bdd811b 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/AccountId.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/AccountId.cs @@ -11,4 +11,4 @@ public override string ToString() { return Value; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccount.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccount.cs index 73a03bf..0dc944f 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccount.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccount.cs @@ -11,22 +11,13 @@ public sealed class BankAccount : EsAggregateRoot events) - : base(events) - { - } + : base(events) { } // Properties for testing public string Owner { get; private set; } = string.Empty; @@ -42,13 +33,7 @@ public void Deposit(Money amount) throw new InvalidOperationException("Deposit amount must be positive"); } - MoneyDeposited @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - amount - ); + MoneyDeposited @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount); Apply(@event); } @@ -59,13 +44,7 @@ public void Withdraw(Money amount) throw new InvalidOperationException("Withdrawal amount must be positive"); } - MoneyWithdrawn @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - amount - ); + MoneyWithdrawn @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, amount); Apply(@event); } @@ -76,13 +55,7 @@ public void Close(string reason) throw new InvalidOperationException("Account is already closed"); } - AccountClosed @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - reason - ); + AccountClosed @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, reason); Apply(@event); } @@ -139,4 +112,4 @@ public override string GetCategory() { return "account"; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccountEvents.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccountEvents.cs index b8b4455..7b4fbf3 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccountEvents.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/BankAccountEvents.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; namespace EzDdd.UseCase.Tests.Integration.TestDomain; @@ -7,8 +6,7 @@ namespace EzDdd.UseCase.Tests.Integration.TestDomain; /// /// Account created event (construction event). /// -public sealed record AccountCreated -( +public sealed record AccountCreated( Guid Id, DateTimeOffset OccurredOn, AccountId AccountId, @@ -25,13 +23,8 @@ Money InitialBalance /// /// Money deposited event. /// -public sealed record MoneyDeposited -( - Guid Id, - DateTimeOffset OccurredOn, - AccountId AccountId, - Money Amount -) : IInternalDomainEvent +public sealed record MoneyDeposited(Guid Id, DateTimeOffset OccurredOn, AccountId AccountId, Money Amount) + : IInternalDomainEvent { public string Source => AccountId.Value; @@ -42,13 +35,8 @@ Money Amount /// /// Money withdrawn event. /// -public sealed record MoneyWithdrawn -( - Guid Id, - DateTimeOffset OccurredOn, - AccountId AccountId, - Money Amount -) : IInternalDomainEvent +public sealed record MoneyWithdrawn(Guid Id, DateTimeOffset OccurredOn, AccountId AccountId, Money Amount) + : IInternalDomainEvent { public string Source => AccountId.Value; @@ -59,16 +47,12 @@ Money Amount /// /// Account closed event (destruction event). /// -public sealed record AccountClosed -( - Guid Id, - DateTimeOffset OccurredOn, - AccountId AccountId, - string Reason -) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent +public sealed record AccountClosed(Guid Id, DateTimeOffset OccurredOn, AccountId AccountId, string Reason) + : IInternalDomainEvent, + IInternalDomainEvent.IDestructionEvent { public string Source => AccountId.Value; public IReadOnlyDictionary Metadata { get; init; } = new ReadOnlyDictionary(new Dictionary()); -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs index ab6c82d..80aee15 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs @@ -95,12 +95,7 @@ public record OrderCleanupContext(int ExpirationDays); /// Number of orders successfully deleted. /// Number of errors encountered during cleanup. /// List of error messages (if any). -public record OrderCleanupReport( - int TotalChecked, - int DeletedCount, - int ErrorCount, - IReadOnlyList Errors -); +public record OrderCleanupReport(int TotalChecked, int DeletedCount, int ErrorCount, IReadOnlyList Errors); /// /// Repository interface for Order aggregate (simplified for example). diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CreateAccountUseCase.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CreateAccountUseCase.cs index 2d3cb0c..2368fed 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CreateAccountUseCase.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CreateAccountUseCase.cs @@ -8,12 +8,7 @@ namespace EzDdd.UseCase.Tests.Integration.TestDomain; /// /// Input for creating a new bank account. /// -public sealed record CreateAccountInput -( - AccountId AccountId, - string Owner, - Money InitialBalance -) : IInput; +public sealed record CreateAccountInput(AccountId AccountId, string Owner, Money InitialBalance) : IInput; /// /// Output for account creation. @@ -61,20 +56,15 @@ public IOutput Succeed() /// /// Use case for creating a new bank account. /// -public sealed class CreateAccountUseCase - (IRepository repository) : IUseCase +public sealed class CreateAccountUseCase(IRepository repository) + : IUseCase { public async Task ExecuteAsync(CreateAccountInput input) { try { // Create new account aggregate - BankAccount account = new - ( - input.AccountId, - input.Owner, - input.InitialBalance - ); + BankAccount account = new(input.AccountId, input.Owner, input.InitialBalance); // Save to repository await repository.SaveAsync(account); @@ -87,12 +77,8 @@ public async Task ExecuteAsync(CreateAccountInput input) catch (InvalidOperationException ex) { // Business rule violation - throw new UseCaseFailureException - ( - $"Failed to create account: {ex.Message}", - ex - ); + throw new UseCaseFailureException($"Failed to create account: {ex.Message}", ex); } // RepositorySaveException propagates to caller } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/DepositUseCase.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/DepositUseCase.cs index 3225f3c..4b7c9fd 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/DepositUseCase.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/DepositUseCase.cs @@ -9,11 +9,7 @@ namespace EzDdd.UseCase.Tests.Integration.TestDomain; /// Input for depositing money into an account. /// Implements IVersionedInput for optimistic locking. /// -public sealed record DepositInput -( - AccountId AccountId, - Money Amount -) : IVersionedInput +public sealed record DepositInput(AccountId AccountId, Money Amount) : IVersionedInput { public long Version { get; set; } } @@ -65,8 +61,8 @@ public IOutput Succeed() /// Use case for depositing money into a bank account. /// Validates version for optimistic locking. /// -public sealed class DepositUseCase - (IRepository repository) : IUseCase +public sealed class DepositUseCase(IRepository repository) + : IUseCase { public async Task ExecuteAsync(DepositInput input) { @@ -76,17 +72,13 @@ public async Task ExecuteAsync(DepositInput input) BankAccount? account = await repository.FindByIdAsync(input.AccountId); if (account is null) { - throw new UseCaseFailureException - ( - $"Account not found: {input.AccountId.Value}" - ); + throw new UseCaseFailureException($"Account not found: {input.AccountId.Value}"); } // Validate version (optimistic locking) if (account.Version != input.Version) { - throw new UseCaseFailureException - ( + throw new UseCaseFailureException( $"Version mismatch: expected {input.Version}, actual {account.Version}" ); } @@ -105,12 +97,8 @@ public async Task ExecuteAsync(DepositInput input) catch (InvalidOperationException ex) { // Business rule violation - throw new UseCaseFailureException - ( - $"Failed to deposit: {ex.Message}", - ex - ); + throw new UseCaseFailureException($"Failed to deposit: {ex.Message}", ex); } // RepositorySaveException propagates to caller } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryEventStorePeer.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryEventStorePeer.cs index 462b05f..03a457b 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryEventStorePeer.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryEventStorePeer.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; - using EzDdd.Entity; using EzDdd.UseCase.Exceptions; using EzDdd.UseCase.Port.Out; @@ -28,8 +27,7 @@ public Task SaveAsync(EventStoreData data) { if (existing.Version != data.Version - 1) { - throw new RepositoryPeerSaveException - ( + throw new RepositoryPeerSaveException( $"Optimistic lock failure: expected version {existing.Version}, but got {data.Version - 1}", new InvalidOperationException("Version mismatch") ); @@ -41,7 +39,10 @@ public Task SaveAsync(EventStoreData data) _storage[data.Id.Value] = new EventStoreData { - Id = data.Id, Version = data.Version, Events = allEvents, StreamName = data.StreamName + Id = data.Id, + Version = data.Version, + Events = allEvents, + StreamName = data.StreamName, }; } else @@ -69,4 +70,4 @@ public bool Contains(AccountId id) { return _storage.ContainsKey(id.Value); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryOutboxPeer.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryOutboxPeer.cs index a8ee3c9..f9a53d5 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryOutboxPeer.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/InMemoryOutboxPeer.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; - using EzDdd.UseCase.Exceptions; using EzDdd.UseCase.Port.Out; @@ -32,8 +31,7 @@ public Task SaveAsync(OrderData data) if (expectedVersion != data.Version) { - throw new RepositoryPeerSaveException - ( + throw new RepositoryPeerSaveException( $"Optimistic lock failure: expected version {expectedVersion}, but got {data.Version}", new InvalidOperationException("Version mismatch") ); @@ -62,4 +60,4 @@ public bool Contains(OrderId id) { return _storage.ContainsKey(id.Value); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs index cd25939..91aa3b6 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs @@ -14,17 +14,25 @@ public Money Add(Money other) throw new InvalidOperationException($"Cannot add different currencies: {Currency} and {other.Currency}"); } - return this with { Amount = Amount + other.Amount }; + return this with + { + Amount = Amount + other.Amount, + }; } public Money Subtract(Money other) { if (Currency != other.Currency) { - throw new InvalidOperationException($"Cannot subtract different currencies: {Currency} and {other.Currency}"); + throw new InvalidOperationException( + $"Cannot subtract different currencies: {Currency} and {other.Currency}" + ); } - return this with { Amount = Amount - other.Amount }; + return this with + { + Amount = Amount - other.Amount, + }; } public bool IsPositive() @@ -46,4 +54,4 @@ public override string ToString() { return $"{Amount:F2} {Currency}"; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Order.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Order.cs index 77aa592..0b0453d 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Order.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Order.cs @@ -10,9 +10,7 @@ public sealed class Order : AggregateRoot private readonly List _items = []; // Parameterless constructor for OutboxMapper reconstruction - public Order() - { - } + public Order() { } // Constructor for creation public Order(OrderId id, string customerName) @@ -22,14 +20,7 @@ public Order(OrderId id, string customerName) TotalAmount = 0; Status = OrderStatus.Draft; - OrderCreated @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - id, - customerName, - 0 - ); + OrderCreated @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, id, customerName, 0); Apply(@event); } @@ -62,15 +53,7 @@ public void AddItem(string productName, int quantity, decimal price) _items.Add(item); TotalAmount += item.Subtotal; - OrderItemAdded @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - productName, - quantity, - price - ); + OrderItemAdded @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, productName, quantity, price); Apply(@event); } @@ -88,12 +71,7 @@ public void Confirm() Status = OrderStatus.Confirmed; - OrderConfirmed @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id - ); + OrderConfirmed @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id); Apply(@event); } @@ -106,13 +84,7 @@ public void Cancel(string reason) Status = OrderStatus.Cancelled; - OrderCancelled @event = new - ( - Guid.NewGuid(), - DateTimeOffset.UtcNow, - Id, - reason - ); + OrderCancelled @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, Id, reason); Apply(@event); } } @@ -142,5 +114,5 @@ public enum OrderStatus { Draft, Confirmed, - Cancelled -} \ No newline at end of file + Cancelled, +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs index 45434d9..14ebc2b 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs @@ -27,4 +27,4 @@ public sealed class OrderItemData public string ProductName { get; set; } = string.Empty; public int Quantity { get; set; } public decimal Price { get; set; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderEvents.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderEvents.cs index 923b43e..3c4653a 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderEvents.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderEvents.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; - using EzDdd.Entity; namespace EzDdd.UseCase.Tests.Integration.TestDomain; @@ -7,8 +6,7 @@ namespace EzDdd.UseCase.Tests.Integration.TestDomain; /// /// Order created event. /// -public sealed record OrderCreated -( +public sealed record OrderCreated( Guid Id, DateTimeOffset OccurredOn, OrderId OrderId, @@ -25,8 +23,7 @@ decimal TotalAmount /// /// Order item added event. /// -public sealed record OrderItemAdded -( +public sealed record OrderItemAdded( Guid Id, DateTimeOffset OccurredOn, OrderId OrderId, @@ -44,12 +41,7 @@ decimal Price /// /// Order confirmed event. /// -public sealed record OrderConfirmed -( - Guid Id, - DateTimeOffset OccurredOn, - OrderId OrderId -) : IInternalDomainEvent +public sealed record OrderConfirmed(Guid Id, DateTimeOffset OccurredOn, OrderId OrderId) : IInternalDomainEvent { public string Source => OrderId.Value; @@ -60,16 +52,11 @@ OrderId OrderId /// /// Order cancelled event. /// -public sealed record OrderCancelled -( - Guid Id, - DateTimeOffset OccurredOn, - OrderId OrderId, - string Reason -) : IInternalDomainEvent +public sealed record OrderCancelled(Guid Id, DateTimeOffset OccurredOn, OrderId OrderId, string Reason) + : IInternalDomainEvent { public string Source => OrderId.Value; public IReadOnlyDictionary Metadata { get; init; } = new ReadOnlyDictionary(new Dictionary()); -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderId.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderId.cs index 875009d..5a94819 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderId.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderId.cs @@ -11,4 +11,4 @@ public override string ToString() { return Value; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderMapper.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderMapper.cs index 6372f32..b134cc6 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderMapper.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderMapper.cs @@ -1,5 +1,4 @@ using System.Reflection; - using EzDdd.UseCase.Port.Out; namespace EzDdd.UseCase.Tests.Integration.TestDomain; @@ -19,9 +18,15 @@ public override OrderData ToData(Order aggregate) StreamName = $"order-{aggregate.Id.Value}", CustomerName = aggregate.CustomerName, TotalAmount = aggregate.TotalAmount, - Items = aggregate.Items.Select - (item => new OrderItemData { ProductName = item.ProductName, Quantity = item.Quantity, Price = item.Price }).ToList(), - Status = aggregate.Status + Items = aggregate + .Items.Select(item => new OrderItemData + { + ProductName = item.ProductName, + Quantity = item.Quantity, + Price = item.Price, + }) + .ToList(), + Status = aggregate.Status, }; } @@ -31,39 +36,31 @@ public override Order ToDomain(OrderData data) Order order = new(); // Restore state using reflection (accessing private fields) - FieldInfo? idField = typeof(Order).BaseType!.GetField - ( + FieldInfo? idField = typeof(Order).BaseType!.GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic ); idField!.SetValue(order, data.Id); - FieldInfo? versionField = typeof(Order).BaseType!.GetField - ( + FieldInfo? versionField = typeof(Order).BaseType!.GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic ); versionField!.SetValue(order, data.Version); - FieldInfo? customerNameField = typeof(Order).GetField - ( + FieldInfo? customerNameField = typeof(Order).GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic ); customerNameField!.SetValue(order, data.CustomerName); - FieldInfo? totalAmountField = typeof(Order).GetField - ( + FieldInfo? totalAmountField = typeof(Order).GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic ); totalAmountField!.SetValue(order, data.TotalAmount); - FieldInfo? itemsField = typeof(Order).GetField - ( - "_items", - BindingFlags.Instance | BindingFlags.NonPublic - ); + FieldInfo? itemsField = typeof(Order).GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic); List itemsList = (List)itemsField!.GetValue(order)!; itemsList.Clear(); foreach (OrderItemData itemData in data.Items) @@ -71,8 +68,7 @@ public override Order ToDomain(OrderData data) itemsList.Add(new OrderItem(itemData.ProductName, itemData.Quantity, itemData.Price)); } - FieldInfo? statusField = typeof(Order).GetField - ( + FieldInfo? statusField = typeof(Order).GetField( "k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic ); @@ -80,4 +76,4 @@ public override Order ToDomain(OrderData data) return order; } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/WithdrawUseCase.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/WithdrawUseCase.cs index 0c3db49..1ccd343 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/WithdrawUseCase.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/WithdrawUseCase.cs @@ -9,11 +9,7 @@ namespace EzDdd.UseCase.Tests.Integration.TestDomain; /// Input for withdrawing money from an account. /// Implements IVersionedInput for optimistic locking. /// -public sealed record WithdrawInput -( - AccountId AccountId, - Money Amount -) : IVersionedInput +public sealed record WithdrawInput(AccountId AccountId, Money Amount) : IVersionedInput { public long Version { get; set; } } @@ -65,8 +61,8 @@ public IOutput Succeed() /// Use case for withdrawing money from a bank account. /// Validates version for optimistic locking. /// -public sealed class WithdrawUseCase - (IRepository repository) : IUseCase +public sealed class WithdrawUseCase(IRepository repository) + : IUseCase { public async Task ExecuteAsync(WithdrawInput input) { @@ -76,17 +72,13 @@ public async Task ExecuteAsync(WithdrawInput input) BankAccount? account = await repository.FindByIdAsync(input.AccountId); if (account is null) { - throw new UseCaseFailureException - ( - $"Account not found: {input.AccountId.Value}" - ); + throw new UseCaseFailureException($"Account not found: {input.AccountId.Value}"); } // Validate version (optimistic locking) if (account.Version != input.Version) { - throw new UseCaseFailureException - ( + throw new UseCaseFailureException( $"Version mismatch: expected {input.Version}, actual {account.Version}" ); } @@ -105,12 +97,8 @@ public async Task ExecuteAsync(WithdrawInput input) catch (InvalidOperationException ex) { // Business rule violation (e.g., insufficient balance) - throw new UseCaseFailureException - ( - $"Failed to withdraw: {ex.Message}", - ex - ); + throw new UseCaseFailureException($"Failed to withdraw: {ex.Message}", ex); } // RepositorySaveException propagates to caller } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/TransactionBoundaryTests.cs b/tests/EzDdd.UseCase.Tests/Integration/TransactionBoundaryTests.cs index 1355af0..6dc7f29 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TransactionBoundaryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TransactionBoundaryTests.cs @@ -162,4 +162,4 @@ public void TransactionBoundaryGuide_ExplainsWhyRuleExists() Assert.Contains("layer separation", content, StringComparison.OrdinalIgnoreCase); Assert.Contains("infrastructure concern", content, StringComparison.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs b/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs index 8a16d10..4e3ee62 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs @@ -13,12 +13,7 @@ public async Task CreateAccountUseCase_WithValidInput_CreatesAccountSuccessfully { IRepository repository = CreateRepository(); CreateAccountUseCase useCase = new(repository); - CreateAccountInput input = new - ( - new AccountId("acc-001"), - "John Doe", - new Money(1000m) - ); + CreateAccountInput input = new(new AccountId("acc-001"), "John Doe", new Money(1000m)); CreateAccountOutput output = await useCase.ExecuteAsync(input); @@ -47,11 +42,7 @@ public async Task DepositUseCase_WithValidInputAndVersion_DepositsSuccessfully() long currentVersion = account.Version; DepositUseCase useCase = new(repository); - DepositInput input = new - ( - accountId, - new Money(300m) - ) { Version = currentVersion }; + DepositInput input = new(accountId, new Money(300m)) { Version = currentVersion }; DepositOutput output = await useCase.ExecuteAsync(input); @@ -77,11 +68,7 @@ public async Task WithdrawUseCase_WithValidInput_WithdrawsSuccessfully() long currentVersion = account.Version; WithdrawUseCase useCase = new(repository); - WithdrawInput input = new - ( - accountId, - new Money(200m) - ) { Version = currentVersion }; + WithdrawInput input = new(accountId, new Money(200m)) { Version = currentVersion }; WithdrawOutput output = await useCase.ExecuteAsync(input); @@ -100,14 +87,16 @@ public async Task DepositUseCase_WithNegativeAmount_ThrowsUseCaseFailureExceptio await repository.SaveAsync(account); DepositUseCase useCase = new(repository); - DepositInput input = new - ( + DepositInput input = new( accountId, new Money(-100m) // Invalid: negative amount - ) { Version = account.Version }; + ) + { + Version = account.Version, + }; - UseCaseFailureException exception = await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) + UseCaseFailureException exception = await Assert.ThrowsAsync(async () => + await useCase.ExecuteAsync(input) ); Assert.Contains("positive", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -123,14 +112,16 @@ public async Task WithdrawUseCase_WithInsufficientBalance_ThrowsUseCaseFailureEx await repository.SaveAsync(account); WithdrawUseCase useCase = new(repository); - WithdrawInput input = new - ( + WithdrawInput input = new( accountId, new Money(500m) // More than balance - ) { Version = account.Version }; + ) + { + Version = account.Version, + }; - UseCaseFailureException exception = await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) + UseCaseFailureException exception = await Assert.ThrowsAsync(async () => + await useCase.ExecuteAsync(input) ); Assert.Contains("negative", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -147,17 +138,13 @@ public async Task DepositUseCase_WithWrongVersion_ThrowsUseCaseFailureException( long currentVersion = account.Version; DepositUseCase useCase = new(repository); - DepositInput input = new - ( - accountId, - new Money(100m) - ) + DepositInput input = new(accountId, new Money(100m)) { - Version = currentVersion + 999 // Wrong version (optimistic locking) + Version = currentVersion + 999, // Wrong version (optimistic locking) }; - UseCaseFailureException exception = await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) + UseCaseFailureException exception = await Assert.ThrowsAsync(async () => + await useCase.ExecuteAsync(input) ); Assert.Contains("version", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -168,14 +155,10 @@ public async Task DepositUseCase_WhenAccountNotFound_ThrowsUseCaseFailureExcepti { IRepository repository = CreateRepository(); DepositUseCase useCase = new(repository); - DepositInput input = new - ( - new AccountId("non-existent"), - new Money(100m) - ) { Version = 0 }; - - UseCaseFailureException exception = await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) + DepositInput input = new(new AccountId("non-existent"), new Money(100m)) { Version = 0 }; + + UseCaseFailureException exception = await Assert.ThrowsAsync(async () => + await useCase.ExecuteAsync(input) ); Assert.Contains("not found", exception.Message, StringComparison.OrdinalIgnoreCase); @@ -187,15 +170,10 @@ public async Task CreateAccountUseCase_WhenRepositorySaveFails_ThrowsRepositoryS FailingRepositoryPeer failingPeer = new(); EsRepository repository = new(failingPeer); CreateAccountUseCase useCase = new(repository); - CreateAccountInput input = new - ( - new AccountId("acc-007"), - "Eve Black", - new Money(1000m) - ); + CreateAccountInput input = new(new AccountId("acc-007"), "Eve Black", new Money(1000m)); - RepositorySaveException exception = await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) + RepositorySaveException exception = await Assert.ThrowsAsync(async () => + await useCase.ExecuteAsync(input) ); // Verify the exception is properly wrapped @@ -232,4 +210,4 @@ public Task DeleteAsync(EventStoreData data) throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs index 0a6471a..ff90c97 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs @@ -1,12 +1,11 @@ using EzDdd.UseCase.Port.In; - using Xunit; namespace EzDdd.UseCase.Tests.Port.In; public class InputOutputTests { -#region IInput Tests + #region IInput Tests [Fact] public void Input_CanBeImplemented_AsMarkerInterface() @@ -35,9 +34,9 @@ public void NullInput_CanBeCreatedDirectly() Assert.IsAssignableFrom(nullInput); } -#endregion + #endregion -#region ExitCode Tests + #region ExitCode Tests [Fact] public void ExitCode_Success_HasCodeZero() @@ -57,9 +56,9 @@ public void ExitCode_Failure_HasCodeOne() Assert.Equal("Failure", exitCode.ToString()); } -#endregion + #endregion -#region IOutput Tests + #region IOutput Tests [Fact] public void Output_SetMessage_ReturnsOutputWithMessage() @@ -124,10 +123,7 @@ public void Output_FluentAPI_CanChainMultipleCalls() { var output = new TestOutput(); - var result = output - .SetId("chain-test") - .SetMessage("Chained message") - .Succeed(); + var result = output.SetId("chain-test").SetMessage("Chained message").Succeed(); Assert.Equal("chain-test", result.Id); Assert.Equal("Chained message", result.Message); @@ -135,9 +131,9 @@ public void Output_FluentAPI_CanChainMultipleCalls() Assert.Same(output, result); } -#endregion + #endregion -#region IVersionedInput Tests + #region IVersionedInput Tests [Fact] public void VersionedInput_CanGetAndSetVersion() @@ -159,13 +155,11 @@ public void VersionedInput_IsAlsoAnInput() Assert.IsAssignableFrom(input); } -#endregion + #endregion -#region Test Helper Classes + #region Test Helper Classes - private class TestInput : IInput - { - } + private class TestInput : IInput { } private class TestOutput : IOutput { @@ -209,5 +203,5 @@ private class TestVersionedInput : IVersionedInput public long Version { get; set; } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs index 17b542d..0c3fdae 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs @@ -4,7 +4,7 @@ namespace EzDdd.UseCase.Tests.Port.In; public class ReconcilerTests { -#region Basic Reconciliation Tests + #region Basic Reconciliation Tests [Fact] public async Task ReconcileAsync_WithValidContext_ReturnsReport() @@ -43,9 +43,9 @@ public async Task ReconcileAsync_Async_CompletesSuccessfully() Assert.True(report.WasAsync); } -#endregion + #endregion -#region Reconciliation Logic Tests + #region Reconciliation Logic Tests [Fact] public async Task ReconcileAsync_WithMultipleItems_ReconcilesAll() @@ -96,18 +96,16 @@ public async Task ReconcileAsync_WithPartialFailure_ReportsErrors() Assert.Equal(3, report.ErrorCount); // 3 failed } -#endregion + #endregion -#region Edge Cases and Validation Tests + #region Edge Cases and Validation Tests [Fact] public async Task ReconcileAsync_WithNullContextParameter_ThrowsArgumentNullException() { TestReconciler reconciler = new(); - await Assert.ThrowsAsync - (async () => await reconciler.ReconcileAsync(null!) - ); + await Assert.ThrowsAsync(async () => await reconciler.ReconcileAsync(null!)); } [Fact] @@ -116,9 +114,7 @@ public async Task ReconcileAsync_WithInvalidContext_ThrowsInvalidOperationExcept InvalidContextReconciler reconciler = new(); TestContext context = new(-1); // Invalid count - await Assert.ThrowsAsync - (async () => await reconciler.ReconcileAsync(context) - ); + await Assert.ThrowsAsync(async () => await reconciler.ReconcileAsync(context)); } [Fact] @@ -127,14 +123,12 @@ public async Task ReconcileAsync_WithCancellation_ThrowsOperationCanceledExcepti CancellableReconciler reconciler = new(); TestContext context = new(100); - await Assert.ThrowsAsync - (async () => await reconciler.ReconcileAsync(context) - ); + await Assert.ThrowsAsync(async () => await reconciler.ReconcileAsync(context)); } -#endregion + #endregion -#region NullContext Tests + #region NullContext Tests [Fact] public void NullContext_Instance_IsSingleton() @@ -156,14 +150,13 @@ public async Task IReconciler_WithNullContext_WorksCorrectly() Assert.True(report.WasGlobalCleanup); } -#endregion + #endregion -#region Test Helper Classes + #region Test Helper Classes private record TestContext(int ItemCount); - private record TestReport - ( + private record TestReport( int ProcessedCount, int ReconciledCount = 0, int ErrorCount = 0, @@ -177,12 +170,7 @@ public Task ReconcileAsync(TestContext context) { ArgumentNullException.ThrowIfNull(context); - TestReport report = new - ( - context.ItemCount, - 0, - 0 - ); + TestReport report = new(context.ItemCount, 0, 0); return Task.FromResult(report); } @@ -203,11 +191,7 @@ public async Task ReconcileAsync(TestContext context) { await Task.Delay(10); // Simulate async work - TestReport report = new - ( - context.ItemCount, - WasAsync: true - ); + TestReport report = new(context.ItemCount, WasAsync: true); return report; } @@ -218,11 +202,7 @@ private class MultiItemReconciler : IReconciler public Task ReconcileAsync(TestContext context) { // All items reconciled - TestReport report = new - ( - context.ItemCount, - context.ItemCount - ); + TestReport report = new(context.ItemCount, context.ItemCount); return Task.FromResult(report); } @@ -233,11 +213,7 @@ private class NoChangeReconciler : IReconciler public Task ReconcileAsync(TestContext context) { // All items checked, but no changes needed - TestReport report = new - ( - context.ItemCount, - 0 - ); + TestReport report = new(context.ItemCount, 0); return Task.FromResult(report); } @@ -250,11 +226,7 @@ public Task ReconcileAsync(TestContext context) // Simulate conflict resolution (every 3rd item has conflict) int conflicts = context.ItemCount / 3; - TestReport report = new - ( - context.ItemCount, - conflicts - ); + TestReport report = new(context.ItemCount, conflicts); return Task.FromResult(report); } @@ -268,12 +240,7 @@ public Task ReconcileAsync(TestContext context) int errors = context.ItemCount * 3 / 10; int succeeded = context.ItemCount - errors; - TestReport report = new - ( - context.ItemCount, - succeeded, - errors - ); + TestReport report = new(context.ItemCount, succeeded, errors); return Task.FromResult(report); } @@ -306,15 +273,11 @@ private class GlobalCleanupReconciler : IReconciler public Task ReconcileAsync(NullContext context) { // Global cleanup without specific context - TestReport report = new - ( - 0, - WasGlobalCleanup: true - ); + TestReport report = new(0, WasGlobalCleanup: true); return Task.FromResult(report); } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs index 19f474e..86565a3 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs @@ -23,9 +23,7 @@ public async Task UseCase_ExecuteAsync_CanThrowUseCaseFailureException() FailingUseCase useCase = new(); TestInput input = new() { Value = "fail" }; - await Assert.ThrowsAsync - (async () => await useCase.ExecuteAsync(input) - ); + await Assert.ThrowsAsync(async () => await useCase.ExecuteAsync(input)); } [Fact] @@ -112,4 +110,4 @@ public Task ExecuteAsync(TestInput input) return Task.FromResult(new TestOutput()); } } -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs index 82f8bc7..555bc13 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; - using EzDdd.UseCase.Port.InOut; namespace EzDdd.UseCase.Tests.Port.InOut; @@ -14,7 +13,7 @@ public class DomainEventDataBuilderTests /// private record TestPayload(int Amount, string Currency); -#region Factory Methods Tests + #region Factory Methods Tests [Fact] public void Json_ShouldCreateBuilderWithJsonPayload() @@ -63,9 +62,9 @@ public void Binary_ShouldSetContentTypeToOctetStream() Assert.Equal("application/octet-stream", result.ContentType); } -#endregion + #endregion -#region Fluent API Tests + #region Fluent API Tests [Fact] public void EventId_ShouldSetCustomEventId() @@ -73,9 +72,9 @@ public void EventId_ShouldSetCustomEventId() Guid customEventId = Guid.NewGuid(); DomainEventData result = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test" }) - .EventId(customEventId) - .Build(); + .Json("TestEvent", new { Value = "test" }) + .EventId(customEventId) + .Build(); Assert.Equal(customEventId, result.Id); } @@ -86,11 +85,13 @@ public void MetadataAsJson_ShouldSerializeMetadataToJson() Dictionary metadata = new() { ["CorrelationId"] = "123", ["UserId"] = "user@example.com" }; DomainEventData result = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test" }) - .MetadataAsJson(metadata) - .Build(); + .Json("TestEvent", new { Value = "test" }) + .MetadataAsJson(metadata) + .Build(); - Dictionary? deserializedMetadata = JsonSerializer.Deserialize>(result.UserMetadata); + Dictionary? deserializedMetadata = JsonSerializer.Deserialize>( + result.UserMetadata + ); Assert.NotNull(deserializedMetadata); Assert.Equal("123", deserializedMetadata["CorrelationId"]); Assert.Equal("user@example.com", deserializedMetadata["UserId"]); @@ -102,23 +103,21 @@ public void MetadataAsBytes_ShouldSetRawMetadataBytes() byte[] metadataBytes = "{\"key\":\"value\"}"u8.ToArray(); DomainEventData result = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test" }) - .MetadataAsBytes(metadataBytes) - .Build(); + .Json("TestEvent", new { Value = "test" }) + .MetadataAsBytes(metadataBytes) + .Build(); Assert.Equal(metadataBytes, result.UserMetadata); } -#endregion + #endregion -#region Build Method Tests + #region Build Method Tests [Fact] public void Build_ShouldAutoGenerateEventIdIfNotSet() { - DomainEventData result = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test" }) - .Build(); + DomainEventData result = DomainEventDataBuilder.Json("TestEvent", new { Value = "test" }).Build(); Assert.NotEqual(Guid.Empty, result.Id); } @@ -126,9 +125,7 @@ public void Build_ShouldAutoGenerateEventIdIfNotSet() [Fact] public void Build_ShouldUseEmptyJsonForMetadataIfNotSet() { - DomainEventData result = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test" }) - .Build(); + DomainEventData result = DomainEventDataBuilder.Json("TestEvent", new { Value = "test" }).Build(); Assert.Equal("{}"u8.ToArray(), result.UserMetadata); } @@ -136,13 +133,9 @@ public void Build_ShouldUseEmptyJsonForMetadataIfNotSet() [Fact] public void Build_ShouldGenerateUniqueEventIdsForMultipleCalls() { - DomainEventData result1 = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test1" }) - .Build(); + DomainEventData result1 = DomainEventDataBuilder.Json("TestEvent", new { Value = "test1" }).Build(); - DomainEventData result2 = DomainEventDataBuilder - .Json("TestEvent", new { Value = "test2" }) - .Build(); + DomainEventData result2 = DomainEventDataBuilder.Json("TestEvent", new { Value = "test2" }).Build(); // Each build should generate unique event IDs Assert.NotEqual(Guid.Empty, result1.Id); @@ -158,10 +151,10 @@ public void Build_ShouldConstructValidDomainEventData() Dictionary metadata = new() { ["TraceId"] = "trace-123" }; DomainEventData result = DomainEventDataBuilder - .Json("PaymentReceived", payload) - .EventId(customEventId) - .MetadataAsJson(metadata) - .Build(); + .Json("PaymentReceived", payload) + .EventId(customEventId) + .MetadataAsJson(metadata) + .Build(); Assert.Equal(customEventId, result.Id); Assert.Equal("PaymentReceived", result.EventType); @@ -175,9 +168,9 @@ public void Build_ShouldConstructValidDomainEventData() Assert.Equal("EUR", deserializedPayload.Currency); } -#endregion + #endregion -#region Integration Tests + #region Integration Tests [Fact] public void Builder_ShouldSupportFullFluentChain() @@ -187,17 +180,19 @@ public void Builder_ShouldSupportFullFluentChain() Dictionary metadata = new() { ["CorrelationId"] = "corr-456", ["UserId"] = "user-789" }; DomainEventData result = DomainEventDataBuilder - .Json("OrderCreated", payload) - .EventId(eventId) - .MetadataAsJson(metadata) - .Build(); + .Json("OrderCreated", payload) + .EventId(eventId) + .MetadataAsJson(metadata) + .Build(); Assert.Equal(eventId, result.Id); Assert.Equal("OrderCreated", result.EventType); Assert.Equal("application/json", result.ContentType); Assert.NotEmpty(result.EventBody); - Dictionary? deserializedMetadata = JsonSerializer.Deserialize>(result.UserMetadata); + Dictionary? deserializedMetadata = JsonSerializer.Deserialize>( + result.UserMetadata + ); Assert.NotNull(deserializedMetadata); Assert.Equal("corr-456", deserializedMetadata["CorrelationId"]); Assert.Equal("user-789", deserializedMetadata["UserId"]); @@ -212,14 +207,13 @@ public void Builder_ShouldProduceSameResultAsDirectConstruction() // Act - using builder DomainEventData builderResult = DomainEventDataBuilder - .Json("TestEvent", payload) - .EventId(eventId) - .MetadataAsJson(metadata) - .Build(); + .Json("TestEvent", payload) + .EventId(eventId) + .MetadataAsJson(metadata) + .Build(); // Act - using direct construction (old way) - DomainEventData directResult = new - ( + DomainEventData directResult = new( eventId, "TestEvent", "application/json", @@ -237,9 +231,9 @@ public void Builder_ShouldProduceSameResultAsDirectConstruction() Assert.Equal(directResult, builderResult); } -#endregion + #endregion -#region Null Safety Tests + #region Null Safety Tests [Fact] public void Json_ShouldThrowArgumentNullExceptionWhenEventTypeIsNull() @@ -247,10 +241,7 @@ public void Json_ShouldThrowArgumentNullExceptionWhenEventTypeIsNull() string? nullEventType = null; var payload = new { Value = "test" }; - Assert.Throws - (() => - DomainEventDataBuilder.Json(nullEventType!, payload) - ); + Assert.Throws(() => DomainEventDataBuilder.Json(nullEventType!, payload)); } [Fact] @@ -258,10 +249,7 @@ public void Json_ShouldThrowArgumentNullExceptionWhenPayloadIsNull() { TestPayload? nullPayload = null; - Assert.Throws - (() => - DomainEventDataBuilder.Json("TestEvent", nullPayload!) - ); + Assert.Throws(() => DomainEventDataBuilder.Json("TestEvent", nullPayload!)); } [Fact] @@ -270,10 +258,7 @@ public void Binary_ShouldThrowArgumentNullExceptionWhenEventTypeIsNull() string? nullEventType = null; byte[] payload = [0x01, 0x02]; - Assert.Throws - (() => - DomainEventDataBuilder.Binary(nullEventType!, payload) - ); + Assert.Throws(() => DomainEventDataBuilder.Binary(nullEventType!, payload)); } [Fact] @@ -281,10 +266,7 @@ public void Binary_ShouldThrowArgumentNullExceptionWhenPayloadIsNull() { byte[]? nullPayload = null; - Assert.Throws - (() => - DomainEventDataBuilder.Binary("TestEvent", nullPayload!) - ); + Assert.Throws(() => DomainEventDataBuilder.Binary("TestEvent", nullPayload!)); } [Fact] @@ -293,10 +275,7 @@ public void MetadataAsJson_ShouldThrowArgumentNullExceptionWhenMetadataIsNull() DomainEventDataBuilder builder = DomainEventDataBuilder.Json("TestEvent", new { Value = "test" }); Dictionary? nullMetadata = null; - Assert.Throws - (() => - builder.MetadataAsJson(nullMetadata!) - ); + Assert.Throws(() => builder.MetadataAsJson(nullMetadata!)); } [Fact] @@ -305,11 +284,8 @@ public void MetadataAsBytes_ShouldThrowArgumentNullExceptionWhenMetadataIsNull() DomainEventDataBuilder builder = DomainEventDataBuilder.Json("TestEvent", new { Value = "test" }); byte[]? nullMetadata = null; - Assert.Throws - (() => - builder.MetadataAsBytes(nullMetadata!) - ); + Assert.Throws(() => builder.MetadataAsBytes(nullMetadata!)); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataTests.cs index fbea5bd..fe25a07 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataTests.cs @@ -1,21 +1,20 @@ using System.Diagnostics; using System.Text; - using EzDdd.UseCase.Port.InOut; namespace EzDdd.UseCase.Tests.Port.InOut; public class DomainEventDataTests { -#region Test Fixtures + #region Test Fixtures private readonly Guid _eventId = Guid.NewGuid(); private readonly byte[] _sampleEventBody = "{\"amount\":100}"u8.ToArray(); private readonly byte[] _sampleMetadata = "{\"user\":\"admin\"}"u8.ToArray(); -#endregion + #endregion -#region Creation Tests + #region Creation Tests [Fact] public void DomainEventData_Creation_ShouldSetAllProperties() @@ -35,30 +34,16 @@ public void DomainEventData_Creation_ShouldSetAllProperties() Assert.Equal(metadata, data.UserMetadata); } -#endregion + #endregion -#region Equality Tests - Basic Scenarios + #region Equality Tests - Basic Scenarios [Fact] public void DomainEventData_Equals_ShouldReturnTrueForSameData() { - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); @@ -67,8 +52,7 @@ public void DomainEventData_Equals_ShouldReturnTrueForSameData() [Fact] public void DomainEventData_Equals_ShouldReturnFalseForDifferentEventBody() { - DomainEventData data1 = new - ( + DomainEventData data1 = new( _eventId, "MoneyDeposited", "application/json", @@ -76,8 +60,7 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentEventBody() _sampleMetadata ); - DomainEventData data2 = new - ( + DomainEventData data2 = new( _eventId, "MoneyDeposited", "application/json", @@ -92,8 +75,7 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentEventBody() [Fact] public void DomainEventData_Equals_ShouldReturnFalseForDifferentMetadata() { - DomainEventData data1 = new - ( + DomainEventData data1 = new( _eventId, "MoneyDeposited", "application/json", @@ -101,8 +83,7 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentMetadata() "{\"user\":\"admin\"}"u8.ToArray() ); - DomainEventData data2 = new - ( + DomainEventData data2 = new( _eventId, "MoneyDeposited", "application/json", @@ -114,30 +95,16 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentMetadata() Assert.False(data1.Equals(data2)); } -#endregion + #endregion -#region HashCode Tests - Basic Scenarios + #region HashCode Tests - Basic Scenarios [Fact] public void DomainEventData_HashCode_ShouldBeConsistentForSameData() { - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); int hash1 = data1.GetHashCode(); int hash2 = data2.GetHashCode(); @@ -148,8 +115,7 @@ public void DomainEventData_HashCode_ShouldBeConsistentForSameData() [Fact] public void DomainEventData_HashCode_ShouldBeDifferentForDifferentId() { - DomainEventData data1 = new - ( + DomainEventData data1 = new( Guid.NewGuid(), "MoneyDeposited", "application/json", @@ -157,8 +123,7 @@ public void DomainEventData_HashCode_ShouldBeDifferentForDifferentId() _sampleMetadata ); - DomainEventData data2 = new - ( + DomainEventData data2 = new( Guid.NewGuid(), "MoneyDeposited", "application/json", @@ -172,21 +137,14 @@ public void DomainEventData_HashCode_ShouldBeDifferentForDifferentId() Assert.NotEqual(hash1, hash2); } -#endregion + #endregion -#region Equality Tests - Advanced Scenarios + #region Equality Tests - Advanced Scenarios [Fact] public void DomainEventData_Equals_ShouldHandleNullCorrectly() { - DomainEventData? data = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData? data = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); Assert.False(data.Equals(null)); } @@ -194,14 +152,7 @@ public void DomainEventData_Equals_ShouldHandleNullCorrectly() [Fact] public void DomainEventData_Equals_ShouldHandleSelfReference() { - DomainEventData data = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); Assert.True(data.Equals(data)); Assert.Equal(data, data); @@ -215,23 +166,9 @@ public void DomainEventData_Equals_ShouldCompareByteArrayContent_NotReference() byte[] metadata1 = "{\"user\":\"admin\"}"u8.ToArray(); byte[] metadata2 = "{\"user\":\"admin\"}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody1, - metadata1 - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", eventBody1, metadata1); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody2, - metadata2 - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", eventBody2, metadata2); Assert.NotSame(eventBody1, eventBody2); Assert.NotSame(metadata1, metadata2); @@ -241,8 +178,7 @@ public void DomainEventData_Equals_ShouldCompareByteArrayContent_NotReference() [Fact] public void DomainEventData_Equals_ShouldReturnFalseForDifferentId() { - DomainEventData data1 = new - ( + DomainEventData data1 = new( Guid.NewGuid(), "MoneyDeposited", "application/json", @@ -250,8 +186,7 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentId() _sampleMetadata ); - DomainEventData data2 = new - ( + DomainEventData data2 = new( Guid.NewGuid(), "MoneyDeposited", "application/json", @@ -265,17 +200,9 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentId() [Fact] public void DomainEventData_Equals_ShouldReturnFalseForDifferentEventType() { - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, _sampleMetadata); - DomainEventData data2 = new - ( + DomainEventData data2 = new( _eventId, "MoneyWithdrawn", // Different event type "application/json", @@ -289,23 +216,16 @@ public void DomainEventData_Equals_ShouldReturnFalseForDifferentEventType() [Fact] public void DomainEventData_WithEmptyByteArrays_ShouldWork() { - DomainEventData data = new - ( - _eventId, - "EmptyEvent", - "application/json", - [], - [] - ); + DomainEventData data = new(_eventId, "EmptyEvent", "application/json", [], []); Assert.NotNull(data); Assert.Empty(data.EventBody); Assert.Empty(data.UserMetadata); } -#endregion + #endregion -#region JSON-Aware Equality Tests + #region JSON-Aware Equality Tests [Fact] public void Equals_WithSameJsonDifferentKeyOrder_ShouldReturnTrue() @@ -313,23 +233,9 @@ public void Equals_WithSameJsonDifferentKeyOrder_ShouldReturnTrue() byte[] eventBody1 = "{\"amount\":100,\"currency\":\"USD\"}"u8.ToArray(); byte[] eventBody2 = "{\"currency\":\"USD\",\"amount\":100}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", eventBody2, _sampleMetadata); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); @@ -341,23 +247,9 @@ public void Equals_WithSameJsonDifferentWhitespace_ShouldReturnTrue() byte[] eventBody1 = "{\"amount\":100}"u8.ToArray(); byte[] eventBody2 = "{ \"amount\" : 100 }"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", eventBody2, _sampleMetadata); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); @@ -369,23 +261,9 @@ public void Equals_WithDifferentJsonValues_ShouldReturnFalse() byte[] eventBody1 = "{\"amount\":100}"u8.ToArray(); byte[] eventBody2 = "{\"amount\":200}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", eventBody2, _sampleMetadata); Assert.NotEqual(data1, data2); Assert.False(data1.Equals(data2)); @@ -397,23 +275,9 @@ public void Equals_WithNestedJsonDifferentKeyOrder_ShouldReturnTrue() byte[] eventBody1 = "{\"user\":{\"name\":\"John\",\"age\":30}}"u8.ToArray(); byte[] eventBody2 = "{\"user\":{\"age\":30,\"name\":\"John\"}}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "UserCreated", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "UserCreated", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "UserCreated", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "UserCreated", "application/json", eventBody2, _sampleMetadata); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); @@ -425,23 +289,9 @@ public void Equals_WithJsonArrayDifferentOrder_ShouldReturnFalse() byte[] eventBody1 = "{\"items\":[1,2,3]}"u8.ToArray(); byte[] eventBody2 = "{\"items\":[3,2,1]}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "OrderCreated", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "OrderCreated", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "OrderCreated", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "OrderCreated", "application/json", eventBody2, _sampleMetadata); Assert.NotEqual(data1, data2); Assert.False(data1.Equals(data2)); @@ -453,23 +303,9 @@ public void Equals_WithNonJsonBytes_ShouldFallbackToByteComparison() byte[] binaryData1 = [0x01, 0x02, 0x03, 0x04]; byte[] binaryData2 = [0x01, 0x02, 0x03, 0x04]; - DomainEventData data1 = new - ( - _eventId, - "BinaryEvent", - "application/octet-stream", - binaryData1, - [] - ); + DomainEventData data1 = new(_eventId, "BinaryEvent", "application/octet-stream", binaryData1, []); - DomainEventData data2 = new - ( - _eventId, - "BinaryEvent", - "application/octet-stream", - binaryData2, - [] - ); + DomainEventData data2 = new(_eventId, "BinaryEvent", "application/octet-stream", binaryData2, []); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); @@ -481,31 +317,17 @@ public void Equals_WithMetadataDifferentKeyOrder_ShouldReturnTrue() byte[] metadata1 = "{\"user\":\"admin\",\"timestamp\":\"2024-01-01\"}"u8.ToArray(); byte[] metadata2 = "{\"timestamp\":\"2024-01-01\",\"user\":\"admin\"}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - metadata1 - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, metadata1); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - _sampleEventBody, - metadata2 - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", _sampleEventBody, metadata2); Assert.Equal(data1, data2); Assert.True(data1.Equals(data2)); } -#endregion + #endregion -#region JSON-Aware HashCode Tests + #region JSON-Aware HashCode Tests [Fact] public void GetHashCode_WithDifferentJsonKeyOrder_ShouldBeStable() @@ -513,23 +335,9 @@ public void GetHashCode_WithDifferentJsonKeyOrder_ShouldBeStable() byte[] eventBody1 = "{\"amount\":100,\"currency\":\"USD\"}"u8.ToArray(); byte[] eventBody2 = "{\"currency\":\"USD\",\"amount\":100}"u8.ToArray(); - DomainEventData data1 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "MoneyDeposited", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "MoneyDeposited", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "MoneyDeposited", "application/json", eventBody2, _sampleMetadata); int hash1 = data1.GetHashCode(); int hash2 = data2.GetHashCode(); @@ -537,36 +345,24 @@ public void GetHashCode_WithDifferentJsonKeyOrder_ShouldBeStable() Assert.Equal(hash1, hash2); } -#endregion + #endregion -#region Performance Tests + #region Performance Tests [Fact] public void Equals_Performance_ShouldCompleteWithin50ms() { - const string json1 = "{\"amount\":1000,\"currency\":\"USD\",\"timestamp\":\"2024-01-01T12:00:00Z\",\"metadata\":{\"source\":\"mobile-app\",\"version\":\"1.0.0\",\"userId\":\"user-123\"},\"items\":[{\"id\":1,\"name\":\"Item 1\",\"price\":100},{\"id\":2,\"name\":\"Item 2\",\"price\":200}]}"; - const string json2 = "{\"currency\":\"USD\",\"amount\":1000,\"metadata\":{\"userId\":\"user-123\",\"version\":\"1.0.0\",\"source\":\"mobile-app\"},\"timestamp\":\"2024-01-01T12:00:00Z\",\"items\":[{\"name\":\"Item 1\",\"id\":1,\"price\":100},{\"name\":\"Item 2\",\"id\":2,\"price\":200}]}"; + const string json1 = + "{\"amount\":1000,\"currency\":\"USD\",\"timestamp\":\"2024-01-01T12:00:00Z\",\"metadata\":{\"source\":\"mobile-app\",\"version\":\"1.0.0\",\"userId\":\"user-123\"},\"items\":[{\"id\":1,\"name\":\"Item 1\",\"price\":100},{\"id\":2,\"name\":\"Item 2\",\"price\":200}]}"; + const string json2 = + "{\"currency\":\"USD\",\"amount\":1000,\"metadata\":{\"userId\":\"user-123\",\"version\":\"1.0.0\",\"source\":\"mobile-app\"},\"timestamp\":\"2024-01-01T12:00:00Z\",\"items\":[{\"name\":\"Item 1\",\"id\":1,\"price\":100},{\"name\":\"Item 2\",\"id\":2,\"price\":200}]}"; byte[] eventBody1 = Encoding.UTF8.GetBytes(json1); byte[] eventBody2 = Encoding.UTF8.GetBytes(json2); - DomainEventData data1 = new - ( - _eventId, - "OrderCreated", - "application/json", - eventBody1, - _sampleMetadata - ); + DomainEventData data1 = new(_eventId, "OrderCreated", "application/json", eventBody1, _sampleMetadata); - DomainEventData data2 = new - ( - _eventId, - "OrderCreated", - "application/json", - eventBody2, - _sampleMetadata - ); + DomainEventData data2 = new(_eventId, "OrderCreated", "application/json", eventBody2, _sampleMetadata); _ = data1.Equals(data2); @@ -576,12 +372,11 @@ public void Equals_Performance_ShouldCompleteWithin50ms() stopwatch.Stop(); Assert.True(result, "JSON equality should return true for same content with different key order"); - Assert.True - ( + Assert.True( stopwatch.ElapsedMilliseconds < 50, $"Equality check took {stopwatch.ElapsedMilliseconds}ms (expected < 50ms)" ); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs index ce9546f..65c0e14 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; - using EzDdd.Entity; using EzDdd.UseCase.Port.InOut; @@ -7,7 +6,7 @@ namespace EzDdd.UseCase.Tests.Port.InOut; public class DomainEventMapperTests { -#region Setup + #region Setup public DomainEventMapperTests() { @@ -17,15 +16,14 @@ public DomainEventMapperTests() DomainEventTypeMapper.Register("TestMoneyDeposited"); } -#endregion + #endregion -#region ToData Conversion Tests (Domain → Data) + #region ToData Conversion Tests (Domain → Data) [Fact] public void ToData_ShouldConvertDomainEventToDomainEventData() { - TestAccountCreated @event = new - ( + TestAccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-123", @@ -47,8 +45,7 @@ public void ToData_ShouldConvertDomainEventToDomainEventData() [Fact] public void ToData_ShouldSerializeEventBodyCorrectly() { - TestMoneyDeposited @event = new - ( + TestMoneyDeposited @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-123", @@ -70,8 +67,7 @@ public void ToData_ShouldSerializeMetadataCorrectly() { Dictionary metadata = new() { ["userId"] = "user-1", ["correlationId"] = "corr-123" }; - TestAccountCreated @event = new - ( + TestAccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-123", @@ -81,7 +77,9 @@ public void ToData_ShouldSerializeMetadataCorrectly() ); DomainEventData data = DomainEventMapper.ToData(@event); - Dictionary? deserializedMetadata = JsonSerializer.Deserialize>(data.UserMetadata); + Dictionary? deserializedMetadata = JsonSerializer.Deserialize>( + data.UserMetadata + ); Assert.NotNull(deserializedMetadata); Assert.Equal(2, deserializedMetadata.Count); @@ -92,8 +90,7 @@ public void ToData_ShouldSerializeMetadataCorrectly() [Fact] public void ToData_WithEmptyMetadata_ShouldWork() { - TestMoneyDeposited @event = new - ( + TestMoneyDeposited @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-123", @@ -110,8 +107,7 @@ public void ToData_WithEmptyMetadata_ShouldWork() [Fact] public void ToData_WithComplexNestedObject_ShouldSerializeCorrectly() { - TestAccountCreated @event = new - ( + TestAccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-789", @@ -140,15 +136,14 @@ public void ToData_EmptyCollection_ShouldReturnEmptyList() Assert.Empty(dataList); } -#endregion + #endregion -#region ToDomain Conversion Tests (Data → Domain) + #region ToDomain Conversion Tests (Data → Domain) [Fact] public void ToDomain_ShouldConvertDomainEventDataToDomainEvent() { - TestMoneyDeposited originalEvent = new - ( + TestMoneyDeposited originalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-123", @@ -178,17 +173,16 @@ public void ToDomain_EmptyCollection_ShouldReturnEmptyList() Assert.Empty(events); } -#endregion + #endregion -#region Batch Conversion Tests + #region Batch Conversion Tests [Fact] public void ToData_BatchConversion_ShouldConvertMultipleEvents() { List events = [ - new TestAccountCreated - ( + new TestAccountCreated( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-1", @@ -196,24 +190,20 @@ public void ToData_BatchConversion_ShouldConvertMultipleEvents() "Alice", 1000m ), - - new TestMoneyDeposited - ( + new TestMoneyDeposited( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-1", new Dictionary(), 500m ), - - new TestMoneyDeposited - ( + new TestMoneyDeposited( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-1", new Dictionary(), 300m - ) + ), ]; IReadOnlyList dataList = DomainEventMapper.ToData(events); @@ -230,28 +220,27 @@ public void ToDomain_BatchConversion_ShouldConvertMultipleEventData() { List originalEvents = [ - new TestMoneyDeposited - ( + new TestMoneyDeposited( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-1", new Dictionary(), 100m ), - - new TestMoneyDeposited - ( + new TestMoneyDeposited( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-1", new Dictionary(), 200m - ) + ), ]; IReadOnlyList dataList = DomainEventMapper.ToData(originalEvents); - IReadOnlyList reconstructedEvents = DomainEventMapper.ToDomain(dataList); + IReadOnlyList reconstructedEvents = DomainEventMapper.ToDomain( + dataList + ); Assert.NotNull(reconstructedEvents); Assert.Equal(2, reconstructedEvents.Count); @@ -259,15 +248,14 @@ public void ToDomain_BatchConversion_ShouldConvertMultipleEventData() Assert.Equal(200m, reconstructedEvents[1].Amount); } -#endregion + #endregion -#region Round-Trip Tests + #region Round-Trip Tests [Fact] public void RoundTrip_ShouldPreserveEventData() { - TestAccountCreated originalEvent = new - ( + TestAccountCreated originalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-456", @@ -286,13 +274,12 @@ public void RoundTrip_ShouldPreserveEventData() Assert.Equal(originalEvent.Metadata["key"], reconstructedEvent.Metadata["key"]); } -#endregion + #endregion -#region Test Event Definitions + #region Test Event Definitions // Test internal domain event - private record TestAccountCreated - ( + private record TestAccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -301,8 +288,7 @@ private record TestAccountCreated decimal InitialBalance ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record TestMoneyDeposited - ( + private record TestMoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -310,5 +296,5 @@ private record TestMoneyDeposited decimal Amount ) : IInternalDomainEvent; -#endregion + #endregion } diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs index 6298d76..fe107f0 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; - using EzDdd.Entity; using EzDdd.UseCase.Port.InOut; @@ -25,7 +24,7 @@ public DomainEventMetadataTests() DomainEventTypeMapper.Register("TestDestructionEvent"); } -#region Metadata Immutability + #region Metadata Immutability [Fact] public void Metadata_IsReadOnly_CannotBeModified() @@ -36,9 +35,9 @@ public void Metadata_IsReadOnly_CannotBeModified() Assert.IsAssignableFrom>(@event.Metadata); } -#endregion + #endregion -#region Metadata Serialization/Deserialization + #region Metadata Serialization/Deserialization [Fact] public void ToData_WithMetadata_SerializesMetadataCorrectly() @@ -52,8 +51,9 @@ public void ToData_WithMetadata_SerializesMetadataCorrectly() Assert.NotEmpty(data.UserMetadata); // Verify metadata can be deserialized - Dictionary? deserializedMetadata = - JsonSerializer.Deserialize>(data.UserMetadata); + Dictionary? deserializedMetadata = JsonSerializer.Deserialize>( + data.UserMetadata + ); Assert.NotNull(deserializedMetadata); Assert.Equal(2, deserializedMetadata.Count); @@ -72,8 +72,9 @@ public void ToData_WithEmptyMetadata_SerializesEmptyDictionary() Assert.NotNull(data.UserMetadata); - Dictionary? deserializedMetadata = - JsonSerializer.Deserialize>(data.UserMetadata); + Dictionary? deserializedMetadata = JsonSerializer.Deserialize>( + data.UserMetadata + ); Assert.NotNull(deserializedMetadata); Assert.Empty(deserializedMetadata); @@ -101,7 +102,7 @@ public void RoundTrip_WithMetadata_PreservesMetadata() ["CorrelationId"] = "corr-123", ["CausationId"] = "cause-456", ["UserId"] = "user-789", - ["TraceContext"] = "trace-context-abc" + ["TraceContext"] = "trace-context-abc", }; TestEvent originalEvent = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source-1", "test-data", metadata); @@ -116,9 +117,9 @@ public void RoundTrip_WithMetadata_PreservesMetadata() } } -#endregion + #endregion -#region Metadata Equality + #region Metadata Equality [Fact] public void DomainEventData_WithSameMetadata_AreEqual() @@ -163,9 +164,9 @@ public void DomainEventData_WithDifferentMetadata_AreNotEqual() Assert.NotEqual(data1, data2); } -#endregion + #endregion -#region Metadata Special Cases + #region Metadata Special Cases [Fact] public void Metadata_WithSpecialCharacters_HandledCorrectly() @@ -176,7 +177,7 @@ public void Metadata_WithSpecialCharacters_HandledCorrectly() ["Key.With.Dots"] = "value.with.dots", ["Key_With_Underscores"] = "value_with_underscores", ["KeyWithNumbers123"] = "ValueWithNumbers456", - ["Key@Symbol"] = "value@domain.com" + ["Key@Symbol"] = "value@domain.com", }; TestEvent originalEvent = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source-1", "test", metadata); @@ -199,7 +200,7 @@ public void Metadata_WithUnicodeCharacters_HandledCorrectly() ["Japanese"] = "日本語テスト", ["Korean"] = "한국어 테스트", ["Emoji"] = "🎉🚀✅", - ["Arabic"] = "اختبار" + ["Arabic"] = "اختبار", }; TestEvent originalEvent = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source-1", "test", metadata); @@ -260,9 +261,9 @@ public void Metadata_WithEmptyStringValues_HandledCorrectly() Assert.Equal("value", reconstructedEvent.Metadata["NonEmptyKey"]); } -#endregion + #endregion -#region Metadata in Different Event Types + #region Metadata in Different Event Types [Fact] public void ConstructionEvent_WithMetadata_PreservesMetadata() @@ -292,15 +293,14 @@ public void DestructionEvent_WithMetadata_PreservesMetadata() Assert.Equal("cleanup", reconstructedEvent.Metadata["Reason"]); } -#endregion + #endregion -#region Helper Test Events + #region Helper Test Events /// /// Test event for metadata testing. /// - private sealed record TestEvent - ( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -311,8 +311,7 @@ IReadOnlyDictionary Metadata /// /// Test construction event for metadata testing. /// - private sealed record TestConstructionEvent - ( + private sealed record TestConstructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -323,8 +322,7 @@ IReadOnlyDictionary Metadata /// /// Test destruction event for metadata testing. /// - private sealed record TestDestructionEvent - ( + private sealed record TestDestructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -332,5 +330,5 @@ private sealed record TestDestructionEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent; -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs index e002d9d..53b04a5 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs @@ -8,8 +8,7 @@ public class ExternalDomainEventTests [Fact] public void ExternalDomainEvent_ShouldExtendIDomainEvent() { - TestExternalEvent externalEvent = new - ( + TestExternalEvent externalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "customer-123", @@ -23,8 +22,7 @@ public void ExternalDomainEvent_ShouldExtendIDomainEvent() [Fact] public void ExternalDomainEvent_ShouldBeDistinguishableFromInternalDomainEvent() { - TestExternalEvent externalEvent = new - ( + TestExternalEvent externalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "customer-123", @@ -32,8 +30,7 @@ public void ExternalDomainEvent_ShouldBeDistinguishableFromInternalDomainEvent() "customer-123" ); - TestInternalEvent internalEvent = new - ( + TestInternalEvent internalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "account-456", @@ -49,8 +46,7 @@ public void ExternalDomainEvent_ShouldBeDistinguishableFromInternalDomainEvent() [Fact] public void ExternalDomainEvent_ShouldSupportTypeChecking() { - TestExternalEvent externalEvent = new - ( + TestExternalEvent externalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "customer-123", @@ -63,14 +59,12 @@ public void ExternalDomainEvent_ShouldSupportTypeChecking() Assert.True(domainEvent is IExternalDomainEvent); Assert.False(domainEvent is IInternalDomainEvent); - bool isExternal = domainEvent.GetType().GetInterfaces() - .Any(i => i == typeof(IExternalDomainEvent)); + bool isExternal = domainEvent.GetType().GetInterfaces().Any(i => i == typeof(IExternalDomainEvent)); Assert.True(isExternal); } // Test event for external domain event - private record TestExternalEvent - ( + private record TestExternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -79,12 +73,11 @@ string CustomerId ) : IExternalDomainEvent; // Test event for internal domain event (for comparison) - private record TestInternalEvent - ( + private record TestInternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata, string AccountId ) : IInternalDomainEvent; -} \ No newline at end of file +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs index 1b3fd5a..b799aa4 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs @@ -1,12 +1,11 @@ using System.Text.Json; - using EzDdd.UseCase.Port.InOut; namespace EzDdd.UseCase.Tests.Port.InOut; public class InternalDomainEventDtoTests { -#region Construction and Properties + #region Construction and Properties [Fact] public void InternalDomainEventDto_Creation_ShouldSetAllProperties() @@ -25,7 +24,7 @@ public void InternalDomainEventDto_Creation_ShouldSetAllProperties() BoundedContext = boundedContext, EventSimpleName = eventSimpleName, JsonEvent = jsonEvent, - Metadata = metadata + Metadata = metadata, }; Assert.Equal(id, dto.Id); @@ -53,7 +52,12 @@ public void InternalDomainEventDto_DefaultValues_ShouldBeInitialized() [Fact] public void InternalDomainEventDto_PropertiesAreMutable() { - InternalDomainEventDto dto = new() { Id = Guid.NewGuid(), EventSimpleName = "InitialType", BoundedContext = "initialContext" }; + InternalDomainEventDto dto = new() + { + Id = Guid.NewGuid(), + EventSimpleName = "InitialType", + BoundedContext = "initialContext", + }; Guid newId = Guid.NewGuid(); @@ -66,9 +70,9 @@ public void InternalDomainEventDto_PropertiesAreMutable() Assert.Equal("updatedContext", dto.BoundedContext); } -#endregion + #endregion -#region JSON Serialization + #region JSON Serialization [Fact] public void InternalDomainEventDto_JsonSerialization_ShouldWork() @@ -80,7 +84,7 @@ public void InternalDomainEventDto_JsonSerialization_ShouldWork() BoundedContext = "banking", EventSimpleName = "AccountCreated", JsonEvent = "{\"owner\":\"John Doe\",\"balance\":1000}", - Metadata = new Dictionary { ["key"] = "value" } + Metadata = new Dictionary { ["key"] = "value" }, }; string json = JsonSerializer.Serialize(dto); @@ -94,14 +98,15 @@ public void InternalDomainEventDto_JsonSerialization_ShouldWork() [Fact] public void InternalDomainEventDto_JsonDeserialization_ShouldWork() { - string json = "{\n" + - " \"Id\": \"12345678-1234-1234-1234-123456789012\",\n" + - " \"OccurredOn\": \"2025-11-05T10:30:00+00:00\",\n" + - " \"BoundedContext\": \"banking\",\n" + - " \"EventSimpleName\": \"MoneyWithdrawn\",\n" + - " \"JsonEvent\": \"{\\\"aggregateId\\\": \\\"account-789\\\", \\\"amount\\\": 500}\",\n" + - " \"Metadata\": { \"userId\": \"user-1\" }\n" + - "}"; + string json = + "{\n" + + " \"Id\": \"12345678-1234-1234-1234-123456789012\",\n" + + " \"OccurredOn\": \"2025-11-05T10:30:00+00:00\",\n" + + " \"BoundedContext\": \"banking\",\n" + + " \"EventSimpleName\": \"MoneyWithdrawn\",\n" + + " \"JsonEvent\": \"{\\\"aggregateId\\\": \\\"account-789\\\", \\\"amount\\\": 500}\",\n" + + " \"Metadata\": { \"userId\": \"user-1\" }\n" + + "}"; InternalDomainEventDto? dto = JsonSerializer.Deserialize(json); @@ -123,7 +128,7 @@ public void InternalDomainEventDto_RoundTripJsonConversion_ShouldPreserveData() BoundedContext = "inventory", EventSimpleName = "AccountClosed", JsonEvent = "{\"aggregateId\":\"account-999\",\"reason\":\"Customer request\"}", - Metadata = new Dictionary { ["correlationId"] = "corr-123" } + Metadata = new Dictionary { ["correlationId"] = "corr-123" }, }; string json = JsonSerializer.Serialize(original); @@ -136,16 +141,17 @@ public void InternalDomainEventDto_RoundTripJsonConversion_ShouldPreserveData() Assert.Equal(original.JsonEvent, deserialized.JsonEvent); } -#endregion + #endregion -#region Complex Scenarios + #region Complex Scenarios [Fact] public void InternalDomainEventDto_WithNestedJsonInJsonEvent_ShouldSerialize() { var eventData = new { - aggregateId = "order-123", items = new[] { new { productId = "p1", quantity = 2 }, new { productId = "p2", quantity = 1 } } + aggregateId = "order-123", + items = new[] { new { productId = "p1", quantity = 2 }, new { productId = "p2", quantity = 1 } }, }; string jsonEvent = JsonSerializer.Serialize(eventData); @@ -156,7 +162,7 @@ public void InternalDomainEventDto_WithNestedJsonInJsonEvent_ShouldSerialize() BoundedContext = "sales", EventSimpleName = "OrderCreated", JsonEvent = jsonEvent, - Metadata = new Dictionary() + Metadata = new Dictionary(), }; string json = JsonSerializer.Serialize(dto); @@ -178,7 +184,7 @@ public void InternalDomainEventDto_WithEmptyCollections_ShouldWork() BoundedContext = "test", EventSimpleName = "TestEvent", JsonEvent = "{}", - Metadata = new Dictionary() + Metadata = new Dictionary(), }; Assert.NotNull(dto); @@ -203,7 +209,7 @@ public void InternalDomainEventDto_WithComplexJsonEvent_ShouldPreserveStructure( amount = 1000m, currency = "USD", timestamp = "2025-11-10T10:00:00Z", - metadata = new { source = "mobile-app", version = "1.0.0" } + metadata = new { source = "mobile-app", version = "1.0.0" }, }; string jsonEvent = JsonSerializer.Serialize(complexEvent); @@ -214,7 +220,7 @@ public void InternalDomainEventDto_WithComplexJsonEvent_ShouldPreserveStructure( BoundedContext = "banking", EventSimpleName = "MoneyDeposited", JsonEvent = jsonEvent, - Metadata = new Dictionary { ["userId"] = "user-123" } + Metadata = new Dictionary { ["userId"] = "user-123" }, }; string serialized = JsonSerializer.Serialize(dto); @@ -224,15 +230,17 @@ public void InternalDomainEventDto_WithComplexJsonEvent_ShouldPreserveStructure( Assert.Equal(dto.JsonEvent, deserialized.JsonEvent); // Parse JsonEvent to verify structure preserved - Dictionary? parsedEvent = JsonSerializer.Deserialize>(deserialized.JsonEvent); + Dictionary? parsedEvent = JsonSerializer.Deserialize>( + deserialized.JsonEvent + ); Assert.NotNull(parsedEvent); Assert.Equal("account-456", parsedEvent["aggregateId"].GetString()); Assert.Equal(1000m, parsedEvent["amount"].GetDecimal()); } -#endregion + #endregion -#region Specific Features + #region Specific Features [Fact] public void InternalDomainEventDto_Metadata_ShouldOnlyAcceptStringValues() @@ -246,8 +254,10 @@ public void InternalDomainEventDto_Metadata_ShouldOnlyAcceptStringValues() JsonEvent = "{}", Metadata = new Dictionary { - ["userId"] = "user-1", ["correlationId"] = "corr-123", ["causationId"] = "cause-456" - } + ["userId"] = "user-1", + ["correlationId"] = "corr-123", + ["causationId"] = "cause-456", + }, }; Assert.All(dto.Metadata.Values, value => Assert.IsType(value)); @@ -263,7 +273,7 @@ public void InternalDomainEventDto_CrossPlatformCompatibility_ShouldMatchJavaStr BoundedContext = "banking", EventSimpleName = "AccountCreated", JsonEvent = "{\"aggregateId\":\"account-123\",\"owner\":\"John Doe\"}", - Metadata = new Dictionary { ["userId"] = "user-1" } + Metadata = new Dictionary { ["userId"] = "user-1" }, }; string json = JsonSerializer.Serialize(dto); @@ -281,5 +291,5 @@ public void InternalDomainEventDto_CrossPlatformCompatibility_ShouldMatchJavaStr Assert.DoesNotContain("\"EventType\":", json); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs index 422aa0d..00dd269 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs @@ -17,8 +17,7 @@ public void Interface_CanBeImplemented() public async Task PublishAsync_WhenCalled_PublishesEvent() { TestPublisher publisher = new(); - BaseExternalEvent externalEvent = new - ( + BaseExternalEvent externalEvent = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "customer-123", @@ -54,8 +53,7 @@ public Task PublishAsync(BaseExternalEvent @event) } // Base external event for publisher tests - private record BaseExternalEvent - ( + private record BaseExternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -63,8 +61,7 @@ IReadOnlyDictionary Metadata ) : IExternalDomainEvent; // Derived external event for the contravariance test - private record DerivedExternalEvent - ( + private record DerivedExternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs index 7adeb3e..6f6d34e 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs @@ -1,22 +1,20 @@ using EzDdd.Entity; using EzDdd.UseCase.Exceptions; using EzDdd.UseCase.Port.Out; - using Xunit; namespace EzDdd.UseCase.Tests.Port.Out; public class EsRepositoryTests { -#region Test Helpers + #region Test Helpers private record TestId(string Value) { public override string ToString() => Value; } - private record TestEvent - ( + private record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -29,11 +27,11 @@ private class TestAggregate : EsAggregateRoot private string _name = string.Empty; public string Name => _name; - public TestAggregate(TestId id, string name) : base() + public TestAggregate(TestId id, string name) + : base() { Id = id; - var @event = new TestEvent - ( + var @event = new TestEvent( Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, @@ -44,14 +42,12 @@ public TestAggregate(TestId id, string name) : base() _name = name; } - public TestAggregate(IEnumerable events) : base(events) - { - } + public TestAggregate(IEnumerable events) + : base(events) { } public void DoSomething(string action) { - var @event = new TestEvent - ( + var @event = new TestEvent( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.Value, @@ -121,9 +117,9 @@ public Task DeleteAsync(EventStoreData data) } } -#endregion + #endregion -#region FindByIdAsync Tests + #region FindByIdAsync Tests [Fact] public async Task FindByIdAsync_WithExistingAggregate_ReturnsReconstructedAggregate() @@ -171,9 +167,9 @@ public async Task FindByIdAsync_ReconstructsAggregateFromEvents() Assert.Empty(loaded.GetDomainEvents()); // Events cleared after replay } -#endregion + #endregion -#region SaveAsync Tests + #region SaveAsync Tests [Fact] public async Task SaveAsync_WithValidAggregate_DelegatesToPeer() @@ -213,10 +209,7 @@ public async Task SaveAsync_WhenPeerThrows_WrapsExceptionInRepositorySaveExcepti var id = new TestId("test-error"); var aggregate = new TestAggregate(id, "TestName"); - var exception = await Assert.ThrowsAsync - (() => - repository.SaveAsync(aggregate) - ); + var exception = await Assert.ThrowsAsync(() => repository.SaveAsync(aggregate)); Assert.Contains("Failed to save aggregate", exception.Message); Assert.IsType(exception.InnerException); @@ -243,9 +236,9 @@ public async Task SaveAsync_WhenPeerThrows_DoesNotClearDomainEvents() Assert.Equal(originalEventCount, aggregate.GetDomainEvents().Count); // Events NOT cleared } -#endregion + #endregion -#region DeleteAsync Tests + #region DeleteAsync Tests [Fact] public async Task DeleteAsync_DelegatesToPeer() @@ -270,15 +263,12 @@ public async Task DeleteAsync_WhenPeerThrows_PropagatesException() var id = new TestId("test-delete-error"); var aggregate = new TestAggregate(id, "TestName"); - await Assert.ThrowsAsync - (() => - repository.DeleteAsync(aggregate) - ); + await Assert.ThrowsAsync(() => repository.DeleteAsync(aggregate)); } -#endregion + #endregion -#region Reflection and Constructor Caching Tests + #region Reflection and Constructor Caching Tests [Fact] public async Task FindByIdAsync_UsesReflectionToInstantiateAggregate() @@ -317,9 +307,9 @@ public async Task FindByIdAsync_CachesConstructorInfo_ForPerformance() Assert.Equal(id2, loaded2.Id); } -#endregion + #endregion -#region IRepository Interface Implementation Tests + #region IRepository Interface Implementation Tests [Fact] public void EsRepository_ImplementsIRepositoryInterface() @@ -330,5 +320,5 @@ public void EsRepository_ImplementsIRepositoryInterface() Assert.IsAssignableFrom>(repository); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs index 1229ac5..0c1fcaa 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs @@ -1,18 +1,16 @@ using EzDdd.Entity; using EzDdd.UseCase.Port.Out; - using Xunit; namespace EzDdd.UseCase.Tests.Port.Out; public class EventStoreDataTests { -#region Test Helpers + #region Test Helpers private record TestId(string Value); - private record TestEvent - ( + private record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -20,9 +18,9 @@ private record TestEvent IReadOnlyDictionary Metadata ) : IInternalDomainEvent; -#endregion + #endregion -#region Constructor and Default Values + #region Constructor and Default Values [Fact] public void Constructor_WhenCreated_SetsDefaultValues() @@ -36,9 +34,9 @@ public void Constructor_WhenCreated_SetsDefaultValues() Assert.Equal(string.Empty, data.StreamName); } -#endregion + #endregion -#region Property Setters and Getters + #region Property Setters and Getters [Fact] public void Id_WhenSet_CanBeRetrieved() @@ -68,7 +66,7 @@ public void Events_WhenSet_CanBeRetrieved() var events = new List { new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event1", new Dictionary()), - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event2", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event2", new Dictionary()), }; data.Events = events; @@ -88,9 +86,9 @@ public void StreamName_WhenSet_CanBeRetrieved() Assert.Equal("account-123", data.StreamName); } -#endregion + #endregion -#region GetOptimisticLockVersion + #region GetOptimisticLockVersion [Fact] public void GetOptimisticLockVersion_WithNoEvents_ReturnsVersion() @@ -109,7 +107,7 @@ public void GetOptimisticLockVersion_WithEvents_ReturnsVersionPlusEventCount() { new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event1", new Dictionary()), new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event2", new Dictionary()), - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event3", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event3", new Dictionary()), }; var data = new EventStoreData { Version = 5, Events = events }; @@ -123,7 +121,7 @@ public void GetOptimisticLockVersion_WithZeroVersion_ReturnsEventCount() { var events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event1", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event1", new Dictionary()), }; var data = new EventStoreData { Version = 0, Events = events }; @@ -132,9 +130,9 @@ public void GetOptimisticLockVersion_WithZeroVersion_ReturnsEventCount() Assert.Equal(1, lockVersion); // Version (0) + 1 event } -#endregion + #endregion -#region IStoreData Interface Implementation + #region IStoreData Interface Implementation [Fact] public void EventStoreData_ImplementsIStoreDataInterface() @@ -147,12 +145,17 @@ public void EventStoreData_ImplementsIStoreDataInterface() [Fact] public void EventStoreData_IStoreDataMembers_AreAccessible() { - IStoreData data = new EventStoreData { Id = new TestId("test-id"), Version = 3, StreamName = "test-stream" }; + IStoreData data = new EventStoreData + { + Id = new TestId("test-id"), + Version = 3, + StreamName = "test-stream", + }; Assert.Equal(new TestId("test-id"), data.Id); Assert.Equal("test-stream", data.StreamName); Assert.Equal(3, data.GetOptimisticLockVersion()); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs index 498a24f..f206147 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs @@ -1,21 +1,19 @@ using EzDdd.Entity; using EzDdd.UseCase.Port.Out; - using Xunit; namespace EzDdd.UseCase.Tests.Port.Out; public class EventStoreMapperTests { -#region Test Helpers + #region Test Helpers private record TestId(string Value) { public override string ToString() => Value; } - private record TestEvent - ( + private record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -29,11 +27,11 @@ private class TestAggregate : EsAggregateRoot public string Name => _name; - public TestAggregate(TestId id, string name) : base() + public TestAggregate(TestId id, string name) + : base() { Id = id; // Set Id directly since we're not calling base(id) - var @event = new TestEvent - ( + var @event = new TestEvent( Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, @@ -44,14 +42,12 @@ public TestAggregate(TestId id, string name) : base() _name = name; } - public TestAggregate(IEnumerable events) : base(events) - { - } + public TestAggregate(IEnumerable events) + : base(events) { } public void DoSomething(string action) { - var @event = new TestEvent - ( + var @event = new TestEvent( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.Value, @@ -87,9 +83,9 @@ protected override void _EnsureInvariant() public override string GetCategory() => "test"; } -#endregion + #endregion -#region ToData Tests + #region ToData Tests [Fact] public void ToData_WithValidAggregate_ReturnsEventStoreData() @@ -155,7 +151,7 @@ public void ToData_WithVersionedAggregate_PreservesVersion() var id = new TestId("test-versioned"); var events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, "Event1", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, "Event1", new Dictionary()), }; var aggregate = new TestAggregate(events); @@ -168,21 +164,23 @@ public void ToData_WithVersionedAggregate_PreservesVersion() Assert.Equal(aggregate.Version, data.Version); } -#endregion + #endregion -#region ToDomain Tests + #region ToDomain Tests [Fact] public void ToDomain_ThrowsNotSupportedException() { var data = new EventStoreData { - Id = new TestId("test-123"), Version = 0, Events = new List(), StreamName = "test-123" + Id = new TestId("test-123"), + Version = 0, + Events = new List(), + StreamName = "test-123", }; - var exception = Assert.Throws - (() => - EventStoreMapper.ToDomain(data) + var exception = Assert.Throws(() => + EventStoreMapper.ToDomain(data) ); Assert.Contains("Event sourcing aggregates are reconstructed from events", exception.Message); @@ -197,16 +195,13 @@ public void ToDomain_WithAnyData_AlwaysThrows() Version = 10, Events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", "Event", new Dictionary()), }, - StreamName = "test-456" + StreamName = "test-456", }; - Assert.Throws - (() => - EventStoreMapper.ToDomain(data) - ); + Assert.Throws(() => EventStoreMapper.ToDomain(data)); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs index dbcf569..7c72cd9 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs @@ -5,7 +5,7 @@ namespace EzDdd.UseCase.Tests.Port.Out; public class OutboxDataTests { -#region Interface Characteristics Tests + #region Interface Characteristics Tests [Fact] public void IOutboxData_ExtendsIStoreData() @@ -23,16 +23,15 @@ public void IOutboxData_IsInterface() Assert.True(type.IsInterface); } -#endregion + #endregion -#region Implementation Tests + #region Implementation Tests [Fact] public void IOutboxData_CanBeImplemented() { AccountId accountId = new("acc-123"); - AccountCreated @event = new - ( + AccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "acc-123", @@ -41,15 +40,7 @@ public void IOutboxData_CanBeImplemented() ); IReadOnlyList events = new List { @event }; - BankAccountData data = new - ( - accountId, - 1, - events, - "account-acc-123", - "John Doe", - 1000.00m - ); + BankAccountData data = new(accountId, 1, events, "account-acc-123", "John Doe", 1000.00m); Assert.NotNull(data); Assert.Equal(accountId, data.Id); @@ -60,9 +51,9 @@ public void IOutboxData_CanBeImplemented() Assert.Equal(1000.00m, data.Balance); } -#endregion + #endregion -#region Inherited Property Tests + #region Inherited Property Tests [Fact] public void IOutboxData_InheritsIdProperty() @@ -90,8 +81,7 @@ public void IOutboxData_InheritsVersionProperty() public void IOutboxData_InheritsEventsProperty() { AccountId accountId = new("acc-101"); - AccountCreated @event = new - ( + AccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, "acc-101", @@ -130,9 +120,9 @@ public void IOutboxData_InheritsGetOptimisticLockVersion() Assert.Equal(3, lockVersion); } -#endregion + #endregion -#region State Field Tests + #region State Field Tests [Fact] public void IOutboxData_CanStoreStateFields() @@ -152,15 +142,7 @@ public void IOutboxData_SupportsMultipleStateFields() { AccountId accountId = new("acc-505"); - BankAccountData data = new - ( - accountId, - 2, - [], - "account-acc-505", - "Grace", - 4500.00m - ); + BankAccountData data = new(accountId, 2, [], "account-acc-505", "Grace", 4500.00m); Assert.Equal(accountId, data.Id); Assert.Equal(2, data.Version); @@ -169,14 +151,13 @@ public void IOutboxData_SupportsMultipleStateFields() Assert.Equal(4500.00m, data.Balance); } -#endregion + #endregion -#region Test Data Structures + #region Test Data Structures private record AccountId(string Value); - private record AccountCreated - ( + private record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -186,8 +167,14 @@ IReadOnlyDictionary Metadata private class BankAccountData : IOutboxData { - public BankAccountData - (AccountId id, long version, IReadOnlyList events, string streamName, string owner, decimal balance) + public BankAccountData( + AccountId id, + long version, + IReadOnlyList events, + string streamName, + string owner, + decimal balance + ) { Id = id; Version = version; @@ -211,5 +198,5 @@ public long GetOptimisticLockVersion() } } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs index 27a6064..43d3b7a 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs @@ -5,7 +5,7 @@ namespace EzDdd.UseCase.Tests.Port.Out; public class OutboxMapperTests { -#region Mapper Creation Tests + #region Mapper Creation Tests [Fact] public void OutboxMapper_CanCreateConcreteImplementation() @@ -16,9 +16,9 @@ public void OutboxMapper_CanCreateConcreteImplementation() Assert.IsAssignableFrom>(mapper); } -#endregion + #endregion -#region ToData Conversion Tests + #region ToData Conversion Tests [Fact] public void ToData_CopiesId() @@ -83,24 +83,16 @@ public void ToData_CopiesStateFields() Assert.Equal(3000.00m, data.Balance); } -#endregion + #endregion -#region ToDomain Conversion Tests + #region ToDomain Conversion Tests [Fact] public void ToDomain_ReconstructsId() { BankAccountMapper mapper = new(); AccountId accountId = new("acc-303"); - BankAccountData data = new - ( - accountId, - 2, - Array.Empty(), - "account-acc-303", - "Eve", - 1500.00m - ); + BankAccountData data = new(accountId, 2, Array.Empty(), "account-acc-303", "Eve", 1500.00m); BankAccount account = mapper.ToDomain(data); @@ -112,15 +104,7 @@ public void ToDomain_ReconstructsVersion() { BankAccountMapper mapper = new(); AccountId accountId = new("acc-404"); - BankAccountData data = new - ( - accountId, - 5, - Array.Empty(), - "account-acc-404", - "Frank", - 4500.00m - ); + BankAccountData data = new(accountId, 5, Array.Empty(), "account-acc-404", "Frank", 4500.00m); BankAccount account = mapper.ToDomain(data); @@ -132,15 +116,7 @@ public void ToDomain_ReconstructsStateFields() { BankAccountMapper mapper = new(); AccountId accountId = new("acc-505"); - BankAccountData data = new - ( - accountId, - 3, - Array.Empty(), - "account-acc-505", - "Grace", - 6000.00m - ); + BankAccountData data = new(accountId, 3, Array.Empty(), "account-acc-505", "Grace", 6000.00m); BankAccount account = mapper.ToDomain(data); @@ -148,9 +124,9 @@ public void ToDomain_ReconstructsStateFields() Assert.Equal(6000.00m, account.Balance); } -#endregion + #endregion -#region Roundtrip Tests + #region Roundtrip Tests [Fact] public void Roundtrip_PreservesIdentity() @@ -169,9 +145,9 @@ public void Roundtrip_PreservesIdentity() Assert.Equal(originalAccount.Balance, reconstructedAccount.Balance); } -#endregion + #endregion -#region Complex Scenario Tests + #region Complex Scenario Tests [Fact] public void ToData_WithMultipleDeposits_PreservesAllEvents() @@ -203,14 +179,13 @@ public void ToData_AfterMultipleOperations_VersionIncremented() Assert.Equal(2, data.Version); // 0 + 1 (first deposit) + 1 (second deposit) } -#endregion + #endregion -#region Test Domain Model + #region Test Domain Model private record AccountId(string Value); - private record AccountCreated - ( + private record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -219,8 +194,7 @@ private record AccountCreated IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited - ( + private record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -237,8 +211,7 @@ public BankAccount(AccountId id, string owner, decimal initialBalance) Owner = owner; Balance = initialBalance; - AccountCreated @event = new - ( + AccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, @@ -263,8 +236,7 @@ public BankAccount(AccountId id, string owner, decimal balance, long version) public void Deposit(decimal amount) { - MoneyDeposited @event = new - ( + MoneyDeposited @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.Value, @@ -278,8 +250,7 @@ public void Deposit(decimal amount) private class BankAccountData : IOutboxData { - public BankAccountData - ( + public BankAccountData( AccountId id, long version, IReadOnlyList events, @@ -314,8 +285,7 @@ private class BankAccountMapper : OutboxMapper NewReposi return new OutboxRepository(peer, new TestOutboxMapper()); } -#region Test Infrastructure + #region Test Infrastructure private class TestAggregate : AggregateRoot { @@ -109,5 +109,5 @@ public Task DeleteAsync(TestOutboxData data) } } -#endregion + #endregion } diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs index 11e5063..8e0ef09 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs @@ -6,7 +6,7 @@ namespace EzDdd.UseCase.Tests.Port.Out; public class OutboxRepositoryTests { -#region Repository Instantiation Tests + #region Repository Instantiation Tests [Fact] public void OutboxRepository_CanBeInstantiated() @@ -20,9 +20,9 @@ public void OutboxRepository_CanBeInstantiated() Assert.IsAssignableFrom>(repository); } -#endregion + #endregion -#region Query Tests + #region Query Tests [Fact] public async Task FindByIdAsync_WhenNotFound_ReturnsNull() @@ -56,9 +56,9 @@ public async Task FindByIdAsync_WhenFound_ReturnsAggregate() Assert.Equal(1000.00m, result.Balance); } -#endregion + #endregion -#region Save Tests + #region Save Tests [Fact] public async Task SaveAsync_PersistsAggregate() @@ -105,8 +105,8 @@ public async Task SaveAsync_WhenPeerThrows_TranslatesException() AccountId accountId = new("acc-101"); BankAccount account = new(accountId, "Charlie", 750.00m); - RepositorySaveException exception = await Assert.ThrowsAsync - (async () => await repository.SaveAsync(account) + RepositorySaveException exception = await Assert.ThrowsAsync(async () => + await repository.SaveAsync(account) ); Assert.NotNull(exception.InnerException); @@ -178,9 +178,9 @@ public async Task SaveAsync_PreservesVersion() Assert.Equal(2, loaded.Version); // 0 (AccountCreated) + 1 (Deposit 1) + 1 (Deposit 2) = 2 } -#endregion + #endregion -#region Delete Tests + #region Delete Tests [Fact] public async Task DeleteAsync_RemovesAggregate() @@ -199,9 +199,9 @@ public async Task DeleteAsync_RemovesAggregate() Assert.Null(result); } -#endregion + #endregion -#region Integration Tests + #region Integration Tests [Fact] public async Task FindByIdAsync_UsesMapper() @@ -249,14 +249,13 @@ public async Task SaveAsync_WithMultipleOperations_MaintainsConsistency() Assert.Equal(1750.00m, final.Balance); // 1000 + 500 + 250 } -#endregion + #endregion -#region Test Infrastructure + #region Test Infrastructure private record AccountId(string Value); - private record AccountCreated - ( + private record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -265,8 +264,7 @@ private record AccountCreated IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited - ( + private record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -282,8 +280,7 @@ public BankAccount(AccountId id, string owner, decimal initialBalance) Owner = owner; Balance = initialBalance; - AccountCreated @event = new - ( + AccountCreated @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, id.Value, @@ -307,8 +304,7 @@ public BankAccount(AccountId id, string owner, decimal balance, long version) public void Deposit(decimal amount) { - MoneyDeposited @event = new - ( + MoneyDeposited @event = new( Guid.NewGuid(), DateTimeOffset.UtcNow, Id.Value, @@ -322,8 +318,7 @@ public void Deposit(decimal amount) private class BankAccountData : IOutboxData { - public BankAccountData - ( + public BankAccountData( AccountId id, long version, IReadOnlyList events, @@ -357,8 +352,7 @@ private class BankAccountMapper : OutboxMapper public string Data { get; set; } = string.Empty; } -#endregion + #endregion -#region Mock Implementations + #region Mock Implementations private class MockRepositoryPeer : IRepositoryPeer { @@ -54,16 +54,21 @@ public Task DeleteAsync(TestStoreDataImpl data) } } -#endregion + #endregion -#region FindByIdAsync Tests + #region FindByIdAsync Tests [Fact] public async Task FindByIdAsync_ExistingData_ReturnsData() { var peer = new MockRepositoryPeer(); var dataId = new TestDataId("test-1"); - var storeData = new TestStoreDataImpl { Id = dataId, Version = 0, Data = "Test Data" }; + var storeData = new TestStoreDataImpl + { + Id = dataId, + Version = 0, + Data = "Test Data", + }; await peer.SaveAsync(storeData); var result = await peer.FindByIdAsync(dataId); @@ -84,16 +89,21 @@ public async Task FindByIdAsync_NonExistingData_ReturnsNull() Assert.Null(result); } -#endregion + #endregion -#region SaveAsync Tests + #region SaveAsync Tests [Fact] public async Task SaveAsync_NewData_PersistsData() { var peer = new MockRepositoryPeer(); var dataId = new TestDataId("test-2"); - var storeData = new TestStoreDataImpl { Id = dataId, Version = -1, Data = "New Data" }; + var storeData = new TestStoreDataImpl + { + Id = dataId, + Version = -1, + Data = "New Data", + }; await peer.SaveAsync(storeData); @@ -107,7 +117,12 @@ public async Task SaveAsync_ExistingData_UpdatesData() { var peer = new MockRepositoryPeer(); var dataId = new TestDataId("test-3"); - var storeData = new TestStoreDataImpl { Id = dataId, Version = 0, Data = "Updated Data" }; + var storeData = new TestStoreDataImpl + { + Id = dataId, + Version = 0, + Data = "Updated Data", + }; await peer.SaveAsync(storeData); await peer.SaveAsync(storeData); @@ -123,16 +138,16 @@ public async Task SaveAsync_DatabaseError_ThrowsRepositoryPeerSaveException() var dataId = new TestDataId("test-4"); var storeData = new TestStoreDataImpl { Id = dataId, Data = "Test" }; - var exception = await Assert.ThrowsAsync - (async () => await peer.SaveAsync(storeData) + var exception = await Assert.ThrowsAsync(async () => + await peer.SaveAsync(storeData) ); Assert.Equal("Database error", exception.Message); } -#endregion + #endregion -#region DeleteAsync Tests + #region DeleteAsync Tests [Fact] public async Task DeleteAsync_ExistingData_RemovesData() @@ -148,9 +163,9 @@ public async Task DeleteAsync_ExistingData_RemovesData() Assert.Null(retrieved); } -#endregion + #endregion -#region Type Constraint Tests + #region Type Constraint Tests [Fact] public async Task RepositoryPeer_WorksWithStoreDataConstraint() @@ -185,7 +200,7 @@ public async Task RepositoryPeer_TransactionBoundaryNote_CompileTimeCheck() // 2. Save aggregate state // 3. Save events (outbox) // 4. Commit transaction atomically - } + }, }; await peer.SaveAsync(storeData); @@ -194,5 +209,5 @@ public async Task RepositoryPeer_TransactionBoundaryNote_CompileTimeCheck() Assert.NotNull(retrieved); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs index 59f7de8..08b8fb8 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs @@ -6,13 +6,12 @@ namespace EzDdd.UseCase.Tests.Port.Out; public class RepositoryTests { -#region Test Fixtures + #region Test Fixtures // Test aggregate for repository testing private record TestAggregateId(string Value); - private record TestEvent - ( + private record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -25,8 +24,7 @@ private class TestAggregate : AggregateRoot { @@ -74,9 +72,9 @@ public Task DeleteAsync(TestAggregate aggregate) } } -#endregion + #endregion -#region FindByIdAsync Tests + #region FindByIdAsync Tests [Fact] public async Task FindByIdAsync_ExistingAggregate_ReturnsAggregate() @@ -104,9 +102,9 @@ public async Task FindByIdAsync_NonExistingAggregate_ReturnsNull() Assert.Null(result); } -#endregion + #endregion -#region SaveAsync Tests + #region SaveAsync Tests [Fact] public async Task SaveAsync_NewAggregate_PersistsAggregate() @@ -143,16 +141,16 @@ public async Task SaveAsync_OptimisticLockingFailure_ThrowsRepositorySaveExcepti var aggregateId = new TestAggregateId("test-4"); var aggregate = new TestAggregate(aggregateId, "Test"); - var exception = await Assert.ThrowsAsync - (async () => await repository.SaveAsync(aggregate) + var exception = await Assert.ThrowsAsync(async () => + await repository.SaveAsync(aggregate) ); Assert.Equal(RepositorySaveException.OptimisticLockingFailure, exception.Message); } -#endregion + #endregion -#region DeleteAsync Tests + #region DeleteAsync Tests [Fact] public async Task DeleteAsync_ExistingAggregate_RemovesAggregate() @@ -168,9 +166,9 @@ public async Task DeleteAsync_ExistingAggregate_RemovesAggregate() Assert.Null(retrieved); } -#endregion + #endregion -#region Type Constraint Tests + #region Type Constraint Tests [Fact] public async Task Repository_WorksWithAggregateRootConstraint() @@ -187,5 +185,5 @@ public async Task Repository_WorksWithAggregateRootConstraint() Assert.IsAssignableFrom>(result); } -#endregion -} \ No newline at end of file + #endregion +} diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs index 02b1b39..bcaf10b 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs @@ -5,7 +5,7 @@ namespace EzDdd.UseCase.Tests.Port.Out; public class StoreDataTests { -#region Test Helpers + #region Test Helpers private class TestStoreData : IStoreData { @@ -15,17 +15,16 @@ private class TestStoreData : IStoreData public string StreamName { get; set; } = string.Empty; } - private record TestEvent - ( + private record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; -#endregion + #endregion -#region Property Tests + #region Property Tests [Fact] public void Version_DefaultValue_IsMinusOne() @@ -55,9 +54,9 @@ public void Events_InitiallyEmpty() Assert.Empty(storeData.Events); } -#endregion + #endregion -#region GetOptimisticLockVersion Tests + #region GetOptimisticLockVersion Tests [Fact] public void GetOptimisticLockVersion_NewAggregate_ReturnsZero() @@ -67,8 +66,8 @@ public void GetOptimisticLockVersion_NewAggregate_ReturnsZero() Version = -1, Events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()) - } + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), + }, }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -84,8 +83,8 @@ public void GetOptimisticLockVersion_ExistingAggregateWithOneEvent_ReturnsOne() Version = 0, Events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()) - } + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), + }, }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -103,8 +102,8 @@ public void GetOptimisticLockVersion_ExistingAggregateWithMultipleEvents_Returns { new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()) - } + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), + }, }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -112,9 +111,9 @@ public void GetOptimisticLockVersion_ExistingAggregateWithMultipleEvents_Returns Assert.Equal(8, optimisticLockVersion); // 5 + 3 = 8 } -#endregion + #endregion -#region StreamName Tests + #region StreamName Tests [Fact] public void StreamName_SetAndGet_ReturnsCorrectValue() @@ -136,18 +135,23 @@ public void StreamName_FollowsConvention_CategoryDashId() Assert.StartsWith("bankaccount-", storeData.StreamName); } -#endregion + #endregion -#region Lifecycle Tests + #region Lifecycle Tests [Fact] public void CompleteLifecycle_NewToExisting_VersionProgression() { - IStoreData storeData = new TestStoreData { Id = "account-789", Version = -1, StreamName = "account-789" }; + IStoreData storeData = new TestStoreData + { + Id = "account-789", + Version = -1, + StreamName = "account-789", + }; storeData.Events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), }; Assert.Equal(0, storeData.GetOptimisticLockVersion()); @@ -159,11 +163,11 @@ public void CompleteLifecycle_NewToExisting_VersionProgression() storeData.Events = new List { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()) + new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), }; Assert.Equal(1, storeData.GetOptimisticLockVersion()); } -#endregion -} \ No newline at end of file + #endregion +} From d3c842e7eabb9d80fb9714c3a24060c938a2f005 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 17:40:12 +0800 Subject: [PATCH 05/18] chore: ignore the CSharpier baseline commit in git blame --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..f7c3bab --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Commits to ignore in git blame. +# GitHub honors this file automatically in web UI blame views. +# Local one-time setup: git config blame.ignoreRevsFile .git-blame-ignore-revs + +# CSharpier 1.2.6 baseline format — repository-wide whitespace/wrapping reformat (P2 rollout, 2026-07-04). +ed7908f908d090c33ab0f7fb179f52868d94575c From 235d82a01640dcb6e8657f98160ae3d53d7d8478 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 21:59:41 +0800 Subject: [PATCH 06/18] refactor: resolve analyzer findings ahead of Level Z enforcement Mechanical cleanup driven by the Meziantou/Roslynator measurement pass: seal private test helpers (CA1852), collection expressions (IDE0028), ConfigureAwait(false) in library code (MA0004), explicit ordinal string comparisons (MA0006/MA0002), invariant culture in tests (CA1304/05/11, MA0011), move ExitCodeExtensions and NullContext to their own files (MA0048), and split oversized concurrency tests (MA0051). Two observable (pre-release) changes: InternalDomainEventDto.Metadata is now typed IDictionary (MA0016, aligns with Java Map), and DomainEventTypeMapper argument exceptions carry the parameter name (MA0015). --- src/EzDdd.Entity/DomainEventTypeMapper.cs | 12 +- src/EzDdd.UseCase/Port/In/ExitCode.cs | 16 -- .../Port/In/ExitCodeExtensions.cs | 17 ++ src/EzDdd.UseCase/Port/In/IReconciler.cs | 22 --- src/EzDdd.UseCase/Port/In/NullContext.cs | 23 +++ .../Port/InOut/DomainEventData.cs | 14 +- .../Port/InOut/InternalDomainEventDto.cs | 2 +- src/EzDdd.UseCase/Port/Out/EsRepository.cs | 6 +- .../Port/Out/OutboxRepository.cs | 6 +- tests/EzDdd.Common.Tests/BiMapTests.cs | 48 +++--- tests/EzDdd.Common.Tests/JsonUtilTests.cs | 12 +- .../EzDdd.Cqrs.Tests/Command/CommandTests.cs | 8 +- .../Command/InquiryInputTests.cs | 4 +- .../EzDdd.Cqrs.Tests/Command/InquiryTests.cs | 16 +- tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs | 4 +- .../TestDomain/AccountProjector.cs | 8 +- .../Query/ArchiveCrudTests.cs | 2 +- tests/EzDdd.Cqrs.Tests/Query/NotifierTests.cs | 4 +- .../Query/ProjectionInputTests.cs | 4 +- .../EzDdd.Cqrs.Tests/Query/ProjectionTests.cs | 20 +-- .../EzDdd.Cqrs.Tests/Query/ProjectorTests.cs | 4 +- tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs | 6 +- .../EzDdd.Entity.Tests/AggregateRootTests.cs | 6 +- tests/EzDdd.Entity.Tests/DomainEventTests.cs | 8 +- .../DomainEventTypeMapperConcurrencyTests.cs | 150 ++++++++++-------- .../DomainEventTypeMapperTests.cs | 6 +- tests/EzDdd.Entity.Tests/EntityTests.cs | 4 +- .../EsAggregateRootTests.cs | 10 +- tests/EzDdd.Entity.Tests/IntegrationTests.cs | 27 ++-- .../InternalDomainEventTests.cs | 6 +- tests/EzDdd.Entity.Tests/ValueObjectTests.cs | 8 +- .../ConcurrentOperationsTests.cs | 50 +++--- .../CqrsFlowWithMetadataTests.cs | 2 +- .../EventSourcingMetadataTests.cs | 2 +- .../TestDomain/ReconcilerTestDomain.cs | 20 ++- .../Services/TransferMoneyServiceTests.cs | 6 +- .../CleanUpExpiredOrdersReconciler.cs | 2 +- .../Integration/TestDomain/Money.cs | 4 +- .../Integration/TestDomain/OrderData.cs | 2 +- .../Integration/UseCaseExecutionFlowTests.cs | 18 +-- .../Port/In/InputOutputTests.cs | 6 +- .../Port/In/ReactorTests.cs | 4 +- .../Port/In/ReconcilerTests.cs | 24 +-- .../Port/In/UseCaseTests.cs | 10 +- .../Port/InOut/DomainEventDataBuilderTests.cs | 2 +- .../Port/InOut/DomainEventMapperTests.cs | 4 +- .../Port/InOut/DomainEventMetadataTests.cs | 4 +- .../Port/InOut/ExternalDomainEventTests.cs | 4 +- .../Port/InOut/InternalDomainEventDtoTests.cs | 4 +- .../ExternalDomainEventPublisherTests.cs | 4 +- .../Port/Out/EsRepositoryTests.cs | 10 +- .../Port/Out/EventStoreDataTests.cs | 6 +- .../Port/Out/EventStoreMapperTests.cs | 6 +- .../Port/Out/OutboxDataTests.cs | 10 +- .../Port/Out/OutboxMapperTests.cs | 12 +- .../Port/Out/OutboxRepositoryFindByIdTests.cs | 10 +- .../Port/Out/OutboxRepositoryTests.cs | 16 +- .../Port/Out/RepositoryPeerTests.cs | 16 +- .../Port/Out/RepositoryTests.cs | 10 +- .../Port/Out/StoreDataTests.cs | 36 ++--- 60 files changed, 412 insertions(+), 375 deletions(-) create mode 100644 src/EzDdd.UseCase/Port/In/ExitCodeExtensions.cs create mode 100644 src/EzDdd.UseCase/Port/In/NullContext.cs diff --git a/src/EzDdd.Entity/DomainEventTypeMapper.cs b/src/EzDdd.Entity/DomainEventTypeMapper.cs index 7d0f0f5..30db3e3 100644 --- a/src/EzDdd.Entity/DomainEventTypeMapper.cs +++ b/src/EzDdd.Entity/DomainEventTypeMapper.cs @@ -54,7 +54,7 @@ public static class DomainEventTypeMapper { // Use Lazy for thread-safe lazy initialization (Java 4.1.0 alignment) // Ensures no race conditions during static initialization in multi-threaded scenarios - private static readonly Lazy> Mapper = new(() => new BiMap()); + private static readonly Lazy> Mapper = new(() => []); /// /// Registers a domain event type with its string identifier. @@ -107,7 +107,8 @@ public static void Register(string typeName) { throw new ArgumentException( $"Type name '{typeName}' is already registered to type '{existingType.Name}'. " - + $"Cannot register it to '{eventType.Name}'." + + $"Cannot register it to '{eventType.Name}'.", + nameof(typeName) ); } @@ -119,11 +120,12 @@ public static void Register(string typeName) string? existingName = Mapper.Value.GetKey(eventType); if (existingName != null) { - if (existingName != typeName) + if (!string.Equals(existingName, typeName, StringComparison.Ordinal)) { throw new ArgumentException( $"Type '{eventType.Name}' is already registered with name '{existingName}'. " - + $"Cannot register it with name '{typeName}'." + + $"Cannot register it with name '{typeName}'.", + nameof(typeName) ); } @@ -290,7 +292,7 @@ public static IReadOnlyDictionary GetAllMappings() { // BiMap implements IDictionary, so we can enumerate it // Create a defensive copy as a read-only dictionary - return new Dictionary(Mapper.Value); + return new Dictionary(Mapper.Value, StringComparer.Ordinal); } /// diff --git a/src/EzDdd.UseCase/Port/In/ExitCode.cs b/src/EzDdd.UseCase/Port/In/ExitCode.cs index 12b8fe5..6bdd1d4 100644 --- a/src/EzDdd.UseCase/Port/In/ExitCode.cs +++ b/src/EzDdd.UseCase/Port/In/ExitCode.cs @@ -15,19 +15,3 @@ public enum ExitCode /// Failure = 1, } - -/// -/// Extension methods for . -/// -public static class ExitCodeExtensions -{ - /// - /// Gets the integer code value of the exit code. - /// - /// The exit code. - /// The integer code value. - public static int Code(this ExitCode exitCode) - { - return (int)exitCode; - } -} diff --git a/src/EzDdd.UseCase/Port/In/ExitCodeExtensions.cs b/src/EzDdd.UseCase/Port/In/ExitCodeExtensions.cs new file mode 100644 index 0000000..469c082 --- /dev/null +++ b/src/EzDdd.UseCase/Port/In/ExitCodeExtensions.cs @@ -0,0 +1,17 @@ +namespace EzDdd.UseCase.Port.In; + +/// +/// Extension methods for . +/// +public static class ExitCodeExtensions +{ + /// + /// Gets the integer code value of the exit code. + /// + /// The exit code. + /// The integer code value. + public static int Code(this ExitCode exitCode) + { + return (int)exitCode; + } +} diff --git a/src/EzDdd.UseCase/Port/In/IReconciler.cs b/src/EzDdd.UseCase/Port/In/IReconciler.cs index 73f4503..aaf8368 100644 --- a/src/EzDdd.UseCase/Port/In/IReconciler.cs +++ b/src/EzDdd.UseCase/Port/In/IReconciler.cs @@ -23,25 +23,3 @@ public interface IReconciler /// Task ReconcileAsync(TContext context); } - -/// -/// NullContext is a null object pattern implementation for reconcilers that -/// do not require any input context. Use this instead of null or object -/// to maintain type safety. -/// -/// -/// This class uses the singleton pattern to avoid unnecessary object allocations. -/// Access the single instance via . -/// -public sealed class NullContext -{ - /// - /// Gets the singleton instance of NullContext. - /// - public static readonly NullContext Instance = new(); - - /// - /// Prevents external instantiation. Use instead. - /// - private NullContext() { } -} diff --git a/src/EzDdd.UseCase/Port/In/NullContext.cs b/src/EzDdd.UseCase/Port/In/NullContext.cs new file mode 100644 index 0000000..4cf2f1a --- /dev/null +++ b/src/EzDdd.UseCase/Port/In/NullContext.cs @@ -0,0 +1,23 @@ +namespace EzDdd.UseCase.Port.In; + +/// +/// NullContext is a null object pattern implementation for reconcilers that +/// do not require any input context. Use this instead of null or object +/// to maintain type safety. +/// +/// +/// This class uses the singleton pattern to avoid unnecessary object allocations. +/// Access the single instance via . +/// +public sealed class NullContext +{ + /// + /// Gets the singleton instance of NullContext. + /// + public static readonly NullContext Instance = new(); + + /// + /// Prevents external instantiation. Use instead. + /// + private NullContext() { } +} diff --git a/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs b/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs index 940f8a3..70e3d25 100644 --- a/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs +++ b/src/EzDdd.UseCase/Port/InOut/DomainEventData.cs @@ -92,8 +92,8 @@ public virtual bool Equals(DomainEventData? other) } return Id == other.Id - && EventType == other.EventType - && ContentType == other.ContentType + && string.Equals(EventType, other.EventType, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal) && _JsonEquals(EventBody, other.EventBody) && _JsonEquals(UserMetadata, other.UserMetadata); } @@ -186,11 +186,11 @@ private static bool _JsonElementEquals(JsonElement left, JsonElement right) return _JsonArrayEquals(left, right); case JsonValueKind.String: - return left.GetString() == right.GetString(); + return string.Equals(left.GetString(), right.GetString(), StringComparison.Ordinal); case JsonValueKind.Number: // Compare as raw text to preserve precision - return left.GetRawText() == right.GetRawText(); + return string.Equals(left.GetRawText(), right.GetRawText(), StringComparison.Ordinal); case JsonValueKind.True: case JsonValueKind.False: @@ -217,7 +217,11 @@ private static bool _JsonObjectEquals(JsonElement left, JsonElement right) } // Build dictionary for efficient lookup (key order independent) - Dictionary rightDict = rightProperties.ToDictionary(p => p.Name, p => p.Value); + Dictionary rightDict = rightProperties.ToDictionary( + p => p.Name, + p => p.Value, + StringComparer.Ordinal + ); // Check all properties match foreach (JsonProperty leftProp in leftProperties) diff --git a/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs b/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs index d69db7b..b157d1a 100644 --- a/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs +++ b/src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs @@ -139,5 +139,5 @@ public class InternalDomainEventDto /// Complex metadata values should be serialized to JSON strings. /// /// - public Dictionary Metadata { get; set; } = new(); + public IDictionary Metadata { get; set; } = new Dictionary(StringComparer.Ordinal); } diff --git a/src/EzDdd.UseCase/Port/Out/EsRepository.cs b/src/EzDdd.UseCase/Port/Out/EsRepository.cs index 91b7cc0..311c319 100644 --- a/src/EzDdd.UseCase/Port/Out/EsRepository.cs +++ b/src/EzDdd.UseCase/Port/Out/EsRepository.cs @@ -151,7 +151,7 @@ public EsRepository(IRepositoryPeer, TId> peer) throw new ArgumentNullException(nameof(id)); } - EventStoreData? data = await _peer.FindByIdAsync(id); + EventStoreData? data = await _peer.FindByIdAsync(id).ConfigureAwait(false); if (data == null) { return null; @@ -226,7 +226,7 @@ public async Task SaveAsync(TAggregate aggregate) try { - await _peer.SaveAsync(data); + await _peer.SaveAsync(data).ConfigureAwait(false); } catch (RepositoryPeerSaveException ex) { @@ -270,7 +270,7 @@ public async Task DeleteAsync(TAggregate aggregate) ArgumentNullException.ThrowIfNull(aggregate); EventStoreData data = EventStoreMapper.ToData(aggregate); - await _peer.DeleteAsync(data); + await _peer.DeleteAsync(data).ConfigureAwait(false); } /// diff --git a/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs b/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs index 92a1292..0ae1704 100644 --- a/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs +++ b/src/EzDdd.UseCase/Port/Out/OutboxRepository.cs @@ -137,7 +137,7 @@ public OutboxRepository(IRepositoryPeer peer, OutboxMapper public async Task FindByIdAsync(TId id) { - TData? data = await _peer.FindByIdAsync(id); + TData? data = await _peer.FindByIdAsync(id).ConfigureAwait(false); if (data == null) { return null; @@ -202,7 +202,7 @@ public async Task SaveAsync(TAggregate aggregate) try { - await _peer.SaveAsync(data); + await _peer.SaveAsync(data).ConfigureAwait(false); } catch (RepositoryPeerSaveException e) { @@ -233,6 +233,6 @@ public async Task DeleteAsync(TAggregate aggregate) ArgumentNullException.ThrowIfNull(aggregate); TData data = _mapper.ToData(aggregate); - await _peer.DeleteAsync(data); + await _peer.DeleteAsync(data).ConfigureAwait(false); } } diff --git a/tests/EzDdd.Common.Tests/BiMapTests.cs b/tests/EzDdd.Common.Tests/BiMapTests.cs index bca213e..1b3da73 100644 --- a/tests/EzDdd.Common.Tests/BiMapTests.cs +++ b/tests/EzDdd.Common.Tests/BiMapTests.cs @@ -8,11 +8,12 @@ public class BiMapTests public void Add_AndGet_WorksBidirectionally() { // ReSharper disable once UseObjectOrCollectionInitializer - BiMap biMap = new(); - - biMap.Add("one", 1); - biMap.Add("two", 2); - biMap.Add("three", 3); + BiMap biMap = new() + { + { "one", 1 }, + { "two", 2 }, + { "three", 3 }, + }; Assert.Equal(1, biMap["one"]); Assert.Equal(2, biMap["two"]); @@ -37,9 +38,7 @@ public void Add_WhenValueChanges_UpdatesReverseMappingCorrectly() public void Add_WhenSameValueHasDifferentKey_RemovesOldKey() { // ReSharper disable once UseObjectOrCollectionInitializer - BiMap biMap = new() { { "key1", 100 } }; - - biMap.Add("key2", 100); + BiMap biMap = new() { { "key1", 100 }, { "key2", 100 } }; Assert.Equal("key2", biMap.GetKey(100)); Assert.False(biMap.ContainsKey("key1")); @@ -75,7 +74,7 @@ public void TryGetValue_WhenKeyExists_ReturnsTrue() public void TryGetValue_WhenKeyNotExists_ReturnsFalse() { // ReSharper disable once CollectionNeverUpdated.Local - BiMap biMap = new(); + BiMap biMap = []; bool found = biMap.TryGetValue("missing", out int value); @@ -97,7 +96,7 @@ public void TryGetKey_WhenValueExists_ReturnsTrue() [Fact] public void TryGetKey_WhenValueNotExists_ReturnsFalse() { - BiMap biMap = new(); + BiMap biMap = []; bool found = biMap.TryGetKey(999, out string? key); @@ -199,9 +198,9 @@ public void GetEnumerator_ReturnsAllEntries() List> entries = biMap.ToList(); Assert.Equal(3, entries.Count); - Assert.Contains(entries, e => e.Key == "one" && e.Value == 1); - Assert.Contains(entries, e => e.Key == "two" && e.Value == 2); - Assert.Contains(entries, e => e.Key == "three" && e.Value == 3); + Assert.Contains(entries, e => string.Equals(e.Key, "one", StringComparison.Ordinal) && e.Value == 1); + Assert.Contains(entries, e => string.Equals(e.Key, "two", StringComparison.Ordinal) && e.Value == 2); + Assert.Contains(entries, e => string.Equals(e.Key, "three", StringComparison.Ordinal) && e.Value == 3); } [Fact] @@ -275,7 +274,7 @@ public void PutAll_WhenAddingMultipleEntries_AddsAllBidirectionally() public void PutAll_WhenAddingEmptyDictionary_DoesNotModifyBiMap() { BiMap biMap = new() { { "key", 42 } }; - Dictionary empty = new(); + Dictionary empty = []; biMap.PutAll(empty); @@ -304,7 +303,7 @@ public void PutAll_WhenValueConflicts_RemovesOldKey() [Fact] public void PutIfAbsent_WhenKeyDoesNotExist_AddsAndReturnsValue() { - BiMap biMap = new(); + BiMap biMap = []; int? result = biMap.PutIfAbsent("key", 42); @@ -422,7 +421,7 @@ public void ReplaceConditional_WhenKeyDoesNotExist_ReturnsFalseWithoutModifying( [Fact] public async Task ConcurrentAdds_MaintainConsistency() { - BiMap biMap = new(); + BiMap biMap = []; const int threadCount = 100; const int operationsPerThread = 100; @@ -460,7 +459,7 @@ public async Task ConcurrentAdds_MaintainConsistency() [Fact] public async Task ConcurrentRemoves_MaintainConsistency() { - BiMap biMap = new(); + BiMap biMap = []; const int entryCount = 1000; // Add initial entries @@ -502,7 +501,7 @@ public async Task ConcurrentRemoves_MaintainConsistency() [Fact] public async Task ConcurrentPutAndRemove_MaintainConsistency() { - BiMap biMap = new(); + BiMap biMap = []; const int operationCount = 10000; IEnumerable addTasks = Enumerable @@ -532,7 +531,7 @@ public async Task ConcurrentClearOperations_Complete() .Select(_ => Task.Run(() => { - BiMap biMap = new(); + BiMap biMap = []; // Add initial data for (int i = 0; i < 100; i++) @@ -558,7 +557,7 @@ public async Task ConcurrentClearOperations_Complete() [Fact] public async Task ConcurrentReadsAndWrites_MaintainConsistency() { - BiMap biMap = new(); + BiMap biMap = []; const int writeCount = 1000; const int readCount = 1000; int successfulReads = 0; @@ -574,7 +573,10 @@ public async Task ConcurrentReadsAndWrites_MaintainConsistency() { if (biMap.TryGetValue($"key{i}", out int value) && value == i) { - if (biMap.TryGetKey(value, out string? key) && key == $"key{i}") + if ( + biMap.TryGetKey(value, out string? key) + && string.Equals(key, $"key{i}", StringComparison.Ordinal) + ) { Interlocked.Increment(ref successfulReads); } @@ -597,7 +599,7 @@ public async Task ConcurrentReadsAndWrites_MaintainConsistency() [Fact] public async Task ConcurrentValueOverwrites_MaintainConsistency() { - BiMap biMap = new(); + BiMap biMap = []; const int threadCount = 100; // Multiple threads try to update the same keys with different values @@ -631,7 +633,7 @@ public async Task ConcurrentValueOverwrites_MaintainConsistency() [Fact] public async Task ConcurrentEnumeration_DoesNotThrow() { - BiMap biMap = new(); + BiMap biMap = []; // Add initial data for (int i = 0; i < 100; i++) diff --git a/tests/EzDdd.Common.Tests/JsonUtilTests.cs b/tests/EzDdd.Common.Tests/JsonUtilTests.cs index f7d8eee..7466a42 100644 --- a/tests/EzDdd.Common.Tests/JsonUtilTests.cs +++ b/tests/EzDdd.Common.Tests/JsonUtilTests.cs @@ -270,27 +270,27 @@ public void ReadAs_AndAsString_RoundTripSuccessfully() #endregion // Test domain classes - private class User + private sealed class User { public int Id { get; set; } public string Name { get; set; } = string.Empty; } - private record UserDto(int Id, string Name); + private sealed record UserDto(int Id, string Name); - private class OrderCreated + private sealed class OrderCreated { public int OrderId { get; set; } public DateTime CreatedAt { get; set; } } - private class EntityWithPublicFields + private sealed class EntityWithPublicFields { public string Data = string.Empty; public int Id; } - private class Order + private sealed class Order { public int Id { get; set; } public string ProductName { get; set; } = string.Empty; @@ -298,7 +298,7 @@ private class Order public decimal UnitPrice { get; set; } } - private record MutableRecord + private sealed record MutableRecord { public int Id { get; set; } public string Data { get; set; } = string.Empty; diff --git a/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs b/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs index 654c8fb..3ff6d06 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/CommandTests.cs @@ -58,9 +58,9 @@ public async Task Command_CanModifySystemState() Assert.Equal(1, StatefulTestCommand.ExecutionCount); } - private record TestInput(string Data) : IInput; + private sealed record TestInput(string Data) : IInput; - private class TestOutput : CqrsOutput + private sealed class TestOutput : CqrsOutput { public string ProcessedData { get; set; } = string.Empty; @@ -71,7 +71,7 @@ public TestOutput SetProcessedData(string data) } } - private class TestCommand : ICommand + private sealed class TestCommand : ICommand { public Task ExecuteAsync(TestInput input) { @@ -86,7 +86,7 @@ public Task ExecuteAsync(TestInput input) } } - private class StatefulTestCommand : ICommand + private sealed class StatefulTestCommand : ICommand { public static int ExecutionCount { get; private set; } diff --git a/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs b/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs index 3399120..6d23853 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/InquiryInputTests.cs @@ -50,7 +50,7 @@ public void MultipleImplementations_AreDistinct() Assert.NotEqual(input1.GetType(), input2.GetType()); } - private record TestInquiryInput(string Data) : IInquiryInput; + private sealed record TestInquiryInput(string Data) : IInquiryInput; - private record AnotherTestInquiryInput(int Value) : IInquiryInput; + private sealed record AnotherTestInquiryInput(int Value) : IInquiryInput; } diff --git a/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs b/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs index 778f4f0..830a4ca 100644 --- a/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Command/InquiryTests.cs @@ -66,11 +66,11 @@ public async Task Inquiry_WhenValidationFails_CommandShouldFail() Assert.Contains("Validation failed", output.Message); } - private record TestInquiryInput(string Value) : IInput, IInquiryInput; + private sealed record TestInquiryInput(string Value) : IInput, IInquiryInput; - private record TestInquiryDto(string Id, string Status); + private sealed record TestInquiryDto(string Id, string Status); - private class TestInquiry : IInquiry + private sealed class TestInquiry : IInquiry { public Task QueryAsync(TestInquiryInput input) { @@ -78,16 +78,16 @@ public Task QueryAsync(TestInquiryInput input) } } - private class BooleanInquiry : IInquiry + private sealed class BooleanInquiry : IInquiry { public Task QueryAsync(TestInquiryInput input) { - bool isValid = input.Value == "valid"; + bool isValid = string.Equals(input.Value, "valid", StringComparison.Ordinal); return Task.FromResult(isValid); } } - private class DtoInquiry : IInquiry + private sealed class DtoInquiry : IInquiry { public Task QueryAsync(TestInquiryInput input) { @@ -96,7 +96,7 @@ public Task QueryAsync(TestInquiryInput input) } } - private class CommandWithInquiry(IInquiry inquiry) + private sealed class CommandWithInquiry(IInquiry inquiry) : ICommand { public async Task ExecuteAsync(TestInquiryInput input) @@ -112,5 +112,5 @@ public async Task ExecuteAsync(TestInquiryInput input) } } - private class TestCommandOutput : CqrsOutput { } + private sealed class TestCommandOutput : CqrsOutput { } } diff --git a/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs b/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs index 4be30d7..b96943c 100644 --- a/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/CqrsOutputTests.cs @@ -218,9 +218,9 @@ public void ExplicitIOutput_SetId_ShouldWorkThroughInterface() #region Test Helper Classes - private class TestOutput : CqrsOutput { } + private sealed class TestOutput : CqrsOutput { } - private class TestOutputWithCustomProperty : CqrsOutput + private sealed class TestOutputWithCustomProperty : CqrsOutput { public string CustomData { get; set; } = string.Empty; public int CustomValue { get; set; } diff --git a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs index 1bfc8f9..172f89a 100644 --- a/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs +++ b/tests/EzDdd.Cqrs.Tests/Integration/TestDomain/AccountProjector.cs @@ -37,7 +37,7 @@ public AccountProjector(IArchive archive) /// Executes the projector logic to update the read model based on the received domain event. /// This method is called by the event relay infrastructure when events are published. /// - /// The domain event data to process. + /// The domain event data to process. /// A task representing the asynchronous operation. /// /// @@ -50,11 +50,11 @@ public AccountProjector(IArchive archive) /// exceptions to make test failures visible. /// /// - public async Task ExecuteAsync(DomainEventData eventData) + public async Task ExecuteAsync(DomainEventData input) { try { - IInternalDomainEvent domainEvent = _DeserializeDomainEvent(eventData); + IInternalDomainEvent domainEvent = _DeserializeDomainEvent(input); switch (domainEvent) { @@ -80,7 +80,7 @@ public async Task ExecuteAsync(DomainEventData eventData) // In test scenarios: rethrow to make failures visible in test results // In production: log error and continue processing (don't crash projector) await Console.Error.WriteLineAsync( - $"Error processing event {eventData.Id} (type: {eventData.EventType}): {ex.Message}" + $"Error processing event {input.Id} (type: {input.EventType}): {ex.Message}" ); throw; // Rethrow for test observability } diff --git a/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs index ceed2f5..24dd3a3 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ArchiveCrudTests.cs @@ -37,7 +37,7 @@ public async Task ConcurrentOperations_ShouldBeThreadSafe() #endregion - private record TestReadModel(string Id, string Data, int IntId = 0); + private sealed record TestReadModel(string Id, string Data, int IntId = 0); #region FindByIdAsync Tests diff --git a/tests/EzDdd.Cqrs.Tests/Query/NotifierTests.cs b/tests/EzDdd.Cqrs.Tests/Query/NotifierTests.cs index dac8e70..3c4aaa9 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/NotifierTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/NotifierTests.cs @@ -33,7 +33,7 @@ public void Interface_InputIsContravariant() Assert.Same(notifier, stringNotifier); } - private class TestNotifier : INotifier + private sealed class TestNotifier : INotifier { public Task ExecuteAsync(string input) { @@ -41,7 +41,7 @@ public Task ExecuteAsync(string input) } } - private class ObjectNotifier : INotifier + private sealed class ObjectNotifier : INotifier { public Task ExecuteAsync(object input) { diff --git a/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs index 98bd403..70c48b5 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ProjectionInputTests.cs @@ -52,7 +52,7 @@ public void MultipleImplementations_AreDistinct() Assert.NotEqual(input1.GetType(), input2.GetType()); } - private record TestProjectionInput(string Id) : IProjectionInput; + private sealed record TestProjectionInput(string Id) : IProjectionInput; - private record ComplexProjectionInput(string Id, bool IncludeDetails) : IProjectionInput; + private sealed record ComplexProjectionInput(string Id, bool IncludeDetails) : IProjectionInput; } diff --git a/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs index e0d7297..9801ddf 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ProjectionTests.cs @@ -70,17 +70,17 @@ public async Task Projection_InputMustImplementIProjectionInput() Assert.NotNull(result); } - private record TestProjectionInput(string Id) : IProjectionInput; + private sealed record TestProjectionInput(string Id) : IProjectionInput; - private record TestInput(string CustomerId) : IInput; + private sealed record TestInput(string CustomerId) : IInput; - private record TestProjectionDto(string Id, string Name); + private sealed record TestProjectionDto(string Id, string Name); - private record CustomerViewModel(string CustomerId, string CustomerName); + private sealed record CustomerViewModel(string CustomerId, string CustomerName); - private record AccountDto(string AccountId, decimal Balance); + private sealed record AccountDto(string AccountId, decimal Balance); - private class TestProjection : IProjection + private sealed class TestProjection : IProjection { public Task QueryAsync(TestProjectionInput input) { @@ -89,7 +89,7 @@ public Task QueryAsync(TestProjectionInput input) } } - private class ViewModelProjection : IProjection + private sealed class ViewModelProjection : IProjection { public Task QueryAsync(TestProjectionInput input) { @@ -98,7 +98,7 @@ public Task QueryAsync(TestProjectionInput input) } } - private class DtoProjection : IProjection + private sealed class DtoProjection : IProjection { public Task QueryAsync(TestProjectionInput input) { @@ -107,7 +107,7 @@ public Task QueryAsync(TestProjectionInput input) } } - private class QueryWithProjection(IProjection projection) + private sealed class QueryWithProjection(IProjection projection) : IQuery { public async Task ExecuteAsync(TestInput input) @@ -122,7 +122,7 @@ public async Task ExecuteAsync(TestInput input) } } - private class TestQueryOutput : CqrsOutput + private sealed class TestQueryOutput : CqrsOutput { public string CustomerInfo { get; set; } = string.Empty; diff --git a/tests/EzDdd.Cqrs.Tests/Query/ProjectorTests.cs b/tests/EzDdd.Cqrs.Tests/Query/ProjectorTests.cs index 77e296a..6d41728 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/ProjectorTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/ProjectorTests.cs @@ -33,7 +33,7 @@ public void Interface_InputIsContravariant() Assert.Same(projector, stringProjector); } - private class TestProjector : IProjector + private sealed class TestProjector : IProjector { public Task ExecuteAsync(string input) { @@ -41,7 +41,7 @@ public Task ExecuteAsync(string input) } } - private class ObjectProjector : IProjector + private sealed class ObjectProjector : IProjector { public Task ExecuteAsync(object input) { diff --git a/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs b/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs index d9ddc41..af04ccb 100644 --- a/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs +++ b/tests/EzDdd.Cqrs.Tests/Query/QueryTests.cs @@ -53,9 +53,9 @@ public async Task Query_ShouldNotModifySystemState() Assert.Equal(firstResult.RetrievedData, secondResult.RetrievedData); } - private record TestInput(string Id) : IInput; + private sealed record TestInput(string Id) : IInput; - private class TestOutput : CqrsOutput + private sealed class TestOutput : CqrsOutput { public string RetrievedData { get; set; } = string.Empty; @@ -66,7 +66,7 @@ public TestOutput SetRetrievedData(string data) } } - private class TestQuery : IQuery + private sealed class TestQuery : IQuery { public Task ExecuteAsync(TestInput input) { diff --git a/tests/EzDdd.Entity.Tests/AggregateRootTests.cs b/tests/EzDdd.Entity.Tests/AggregateRootTests.cs index 31becb3..7c6abc6 100644 --- a/tests/EzDdd.Entity.Tests/AggregateRootTests.cs +++ b/tests/EzDdd.Entity.Tests/AggregateRootTests.cs @@ -268,7 +268,7 @@ public async Task AggregateRoot_ThreadSafety_ConcurrentReadAndWrite() #endregion // Test events - private record TestCreatedEvent( + private sealed record TestCreatedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -276,7 +276,7 @@ private record TestCreatedEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record TestUpdatedEvent( + private sealed record TestUpdatedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -284,7 +284,7 @@ private record TestUpdatedEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestDeletedEvent( + private sealed record TestDeletedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/DomainEventTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTests.cs index cdc3394..3f91d51 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTests.cs @@ -36,7 +36,7 @@ public void IDomainEvent_Metadata_IsImmutable() public void IDomainEvent_WithEmptyMetadata_WorksCorrectly() { // ReSharper disable once CollectionNeverUpdated.Local - Dictionary emptyMetadata = new(); + Dictionary emptyMetadata = []; TestCommandEvent @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source", 0, emptyMetadata); @@ -115,7 +115,7 @@ static Guid GetEventId(TEvent @event) // Test implementation - Construction event // ReSharper disable once UnusedType.Local - private record TestConstructionEvent( + private sealed record TestConstructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -124,7 +124,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; // Test implementation - Command event (middle event) - private record TestCommandEvent( + private sealed record TestCommandEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -134,7 +134,7 @@ IReadOnlyDictionary Metadata // Test implementation - Destruction event // ReSharper disable once UnusedType.Local - private record TestDestructionEvent( + private sealed record TestDestructionEvent( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs index d186eff..96405b1 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperConcurrencyTests.cs @@ -22,6 +22,7 @@ public void Dispose() { // Clean up after each test DomainEventTypeMapper.Clear(); + GC.SuppressFinalize(this); } /// @@ -232,28 +233,7 @@ public void ConcurrentMixedOperations_ShouldSucceed() { case < 10: // First 10 threads: Register - switch (i) - { - case 0: - DomainEventTypeMapper.Register("Event1"); - break; - case 1: - DomainEventTypeMapper.Register("Event2"); - break; - case 2: - DomainEventTypeMapper.Register("Event3"); - break; - case 3: - DomainEventTypeMapper.Register("Event4"); - break; - case 4: - DomainEventTypeMapper.Register("Event5"); - break; - default: - // Re-register existing types (idempotent) - DomainEventTypeMapper.Register("Event1"); - break; - } + _RegisterEventForThread(i); break; case < 20: @@ -353,41 +333,7 @@ public void StressTest_HighConcurrency_ShouldSucceed() if (i % 10 == 0) { // Every 10th thread registers - int eventNumber = i / 10; - switch (eventNumber % 10) - { - case 0: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 1: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 2: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 3: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 4: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 5: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 6: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 7: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 8: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - case 9: - DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); - break; - } - + _RegisterStressEvent(i / 10); Interlocked.Increment(ref registrationCount); } else @@ -411,71 +357,141 @@ public void StressTest_HighConcurrency_ShouldSucceed() Assert.True(DomainEventTypeMapper.GetAllMappings().Count >= 10); } + /// + /// Registers the event type assigned to the given registering thread (idempotent re-registration + /// for thread indexes above the distinct event count). + /// + private static void _RegisterEventForThread(int threadIndex) + { + switch (threadIndex) + { + case 0: + DomainEventTypeMapper.Register("Event1"); + break; + case 1: + DomainEventTypeMapper.Register("Event2"); + break; + case 2: + DomainEventTypeMapper.Register("Event3"); + break; + case 3: + DomainEventTypeMapper.Register("Event4"); + break; + case 4: + DomainEventTypeMapper.Register("Event5"); + break; + default: + // Re-register existing types (idempotent) + DomainEventTypeMapper.Register("Event1"); + break; + } + } + + /// + /// Registers one of the ten stress-test event types based on the event number. + /// + private static void _RegisterStressEvent(int eventNumber) + { + switch (eventNumber % 10) + { + case 0: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 1: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 2: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 3: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 4: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 5: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 6: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 7: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 8: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + case 9: + DomainEventTypeMapper.Register($"StressEvent{eventNumber}"); + break; + } + } + // Test event types (need 10 different types for concurrency tests) - private record TestEvent1( + private sealed record TestEvent1( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent2( + private sealed record TestEvent2( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent3( + private sealed record TestEvent3( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent4( + private sealed record TestEvent4( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent5( + private sealed record TestEvent5( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent6( + private sealed record TestEvent6( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent7( + private sealed record TestEvent7( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent8( + private sealed record TestEvent8( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent9( + private sealed record TestEvent9( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent10( + private sealed record TestEvent10( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs index 913e6a6..966cacf 100644 --- a/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs +++ b/tests/EzDdd.Entity.Tests/DomainEventTypeMapperTests.cs @@ -262,14 +262,14 @@ public async Task ThreadSafety_ConcurrentReadWrite_NoDeadlock() #endregion // Test events - private record TestEvent1( + private sealed record TestEvent1( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent2( + private sealed record TestEvent2( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -277,7 +277,7 @@ private record TestEvent2( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record TestEvent3( + private sealed record TestEvent3( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/EntityTests.cs b/tests/EzDdd.Entity.Tests/EntityTests.cs index 7f6d973..8775b6c 100644 --- a/tests/EzDdd.Entity.Tests/EntityTests.cs +++ b/tests/EzDdd.Entity.Tests/EntityTests.cs @@ -61,12 +61,12 @@ public void IEntity_GenericConstraint_CanBeUsedInGenericMethod() } // Test implementation classes - private class TestEntity(Guid id) : IEntity + private sealed class TestEntity(Guid id) : IEntity { public Guid Id { get; } = id; } - private class TestStringEntity(string id) : IEntity + private sealed class TestStringEntity(string id) : IEntity { public string Id { get; } = id; } diff --git a/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs b/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs index 5103c8e..9a40a34 100644 --- a/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs +++ b/tests/EzDdd.Entity.Tests/EsAggregateRootTests.cs @@ -285,7 +285,7 @@ public void EsAggregateRoot_CompleteLifecycle_AllRulesEnforced() #endregion // Test events - private record AccountCreated( + private sealed record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -294,7 +294,7 @@ private record AccountCreated( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited( + private sealed record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -302,7 +302,7 @@ private record MoneyDeposited( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record MoneyWithdrawn( + private sealed record MoneyWithdrawn( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -310,7 +310,7 @@ private record MoneyWithdrawn( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record AccountClosed( + private sealed record AccountClosed( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -319,7 +319,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IDestructionEvent; // Test aggregate - Bank Account with event sourcing - private class Account : EsAggregateRoot + private sealed class Account : EsAggregateRoot { // Constructor for new aggregate public Account(Guid id, string owner, decimal initialBalance) diff --git a/tests/EzDdd.Entity.Tests/IntegrationTests.cs b/tests/EzDdd.Entity.Tests/IntegrationTests.cs index 1f9536a..259ffcf 100644 --- a/tests/EzDdd.Entity.Tests/IntegrationTests.cs +++ b/tests/EzDdd.Entity.Tests/IntegrationTests.cs @@ -266,7 +266,7 @@ public async Task Integration_ConcurrentOperations_OnDifferentAggregates() // ========== Domain Events ========== - private record AccountOpenedEvent( + private sealed record AccountOpenedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -275,7 +275,7 @@ private record AccountOpenedEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDepositedEvent( + private sealed record MoneyDepositedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -284,7 +284,7 @@ private record MoneyDepositedEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record MoneyWithdrawnEvent( + private sealed record MoneyWithdrawnEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -293,7 +293,7 @@ private record MoneyWithdrawnEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private record AccountClosedEvent( + private sealed record AccountClosedEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -303,11 +303,11 @@ IReadOnlyDictionary Metadata // ========== Value Objects ========== - private record Money(decimal Amount, string Currency) : IValueObject + private sealed record Money(decimal Amount, string Currency) : IValueObject { public Money Add(Money other) { - if (Currency != other.Currency) + if (!string.Equals(Currency, other.Currency, StringComparison.Ordinal)) { throw new InvalidOperationException($"Cannot add {other.Currency} to {Currency}"); } @@ -320,7 +320,7 @@ public Money Add(Money other) public Money Subtract(Money other) { - if (Currency != other.Currency) + if (!string.Equals(Currency, other.Currency, StringComparison.Ordinal)) { throw new InvalidOperationException($"Cannot subtract {other.Currency} from {Currency}"); } @@ -337,7 +337,7 @@ public bool IsNegative() } } - private record AccountId(Guid Value) : IValueObject + private sealed record AccountId(Guid Value) : IValueObject { public static AccountId NewId() { @@ -352,8 +352,13 @@ public override string ToString() // ========== Entities ========== - private class Transaction(Guid id, DateTimeOffset timestamp, Money amount, string description, TransactionType type) - : IEntity + private sealed class Transaction( + Guid id, + DateTimeOffset timestamp, + Money amount, + string description, + TransactionType type + ) : IEntity { public DateTimeOffset Timestamp { get; } = timestamp; public Money Amount { get; } = amount; @@ -370,7 +375,7 @@ private enum TransactionType // ========== Aggregate Root ========== - private class BankAccount : EsAggregateRoot + private sealed class BankAccount : EsAggregateRoot { private readonly List _transactions = []; diff --git a/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs b/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs index f0561eb..ace2282 100644 --- a/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs +++ b/tests/EzDdd.Entity.Tests/InternalDomainEventTests.cs @@ -177,7 +177,7 @@ public void ConstructionEvent_NestedInterface_AccessibleViaParent() } // Construction event (R1 rule) - private record AggregateCreated( + private sealed record AggregateCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -186,7 +186,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; // Command event (R2 rule) - private record AggregateUpdated( + private sealed record AggregateUpdated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -195,7 +195,7 @@ IReadOnlyDictionary Metadata ) : IInternalDomainEvent; // Destruction event (R3 rule) - private record AggregateDeleted( + private sealed record AggregateDeleted( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.Entity.Tests/ValueObjectTests.cs b/tests/EzDdd.Entity.Tests/ValueObjectTests.cs index 9c98d91..9584438 100644 --- a/tests/EzDdd.Entity.Tests/ValueObjectTests.cs +++ b/tests/EzDdd.Entity.Tests/ValueObjectTests.cs @@ -84,17 +84,19 @@ private static bool _ProcessValueObject(T valueObject) } // Record-based value object (recommended approach) - private record MoneyRecord(decimal Amount, string Currency) : IValueObject; + private sealed record MoneyRecord(decimal Amount, string Currency) : IValueObject; // Class-based value object (manual equality) - private class MoneyClass(decimal amount, string currency) : IValueObject + private sealed class MoneyClass(decimal amount, string currency) : IValueObject { public decimal Amount { get; } = amount; public string Currency { get; } = currency; public override bool Equals(object? obj) { - return obj is MoneyClass other && Amount == other.Amount && Currency == other.Currency; + return obj is MoneyClass other + && Amount == other.Amount + && string.Equals(Currency, other.Currency, StringComparison.Ordinal); } public override int GetHashCode() diff --git a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs index 0ba0c4d..93aa1af 100644 --- a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs +++ b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs @@ -101,27 +101,7 @@ public async Task MixedOperations_ConcurrentSaveAndLoad_ShouldMaintainConsistenc for (int i = 0; i < updateCount / 2; i++) { int updateId = i; - tasks.Add( - Task.Run(async () => - { - try - { - // Load current state - MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); - if (agg != null && !agg.IsClosed) - { - // Update value - agg.UpdateValue((updateId + 1) * 100); - await repository.SaveAsync(agg); - } - } - catch (RepositorySaveException) - { - // Expected: Optimistic locking conflict due to concurrent updates - // Some updates will fail - this is correct behavior - } - }) - ); + tasks.Add(Task.Run(() => _TryUpdateSharedAggregateAsync(repository, sharedId, updateId))); } // Other half performs reads @@ -149,6 +129,34 @@ public async Task MixedOperations_ConcurrentSaveAndLoad_ShouldMaintainConsistenc Assert.True(finalAgg.Value >= 0); } + /// + /// Loads the shared aggregate and applies one update, swallowing the optimistic-locking + /// conflicts that concurrent updates are expected to produce. + /// + private static async Task _TryUpdateSharedAggregateAsync( + EsRepository repository, + MetadataTestId sharedId, + int updateId + ) + { + try + { + // Load current state + MetadataTestAggregate? agg = await repository.FindByIdAsync(sharedId); + if (agg != null && !agg.IsClosed) + { + // Update value + agg.UpdateValue((updateId + 1) * 100); + await repository.SaveAsync(agg); + } + } + catch (RepositorySaveException) + { + // Expected: Optimistic locking conflict due to concurrent updates + // Some updates will fail - this is correct behavior + } + } + #endregion #region Repository Concurrent Operations Tests diff --git a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs index 342b3f1..953042e 100644 --- a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs @@ -128,7 +128,7 @@ public async Task SaveAndPublishAsync(MetadataTestAggregate aggregate) /// /// Creates metadata with correlation and causation IDs for distributed tracing. /// - private static IReadOnlyDictionary _CreateMetadata( + private static ReadOnlyDictionary _CreateMetadata( string correlationId, string? causationId = null, string? userId = null diff --git a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs index ba52831..c0a7f81 100644 --- a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs @@ -117,7 +117,7 @@ public async Task SaveAndPublishAsync(MetadataTestAggregate aggregate) } } - private static IReadOnlyDictionary _CreateMetadata( + private static ReadOnlyDictionary _CreateMetadata( string correlationId, string? causationId = null, string? userId = null, diff --git a/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs b/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs index 061f004..5d1806b 100644 --- a/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs +++ b/tests/EzDdd.Integration.Tests/TestDomain/ReconcilerTestDomain.cs @@ -12,7 +12,7 @@ public sealed record DataItem(string Id, string Status, DateTimeOffset CreatedAt /// public sealed class InMemoryDataItemRepository { - private readonly Dictionary _storage = new(); + private readonly Dictionary _storage = []; public int Count => _storage.Count; @@ -28,18 +28,20 @@ public void Add(DataItem item) return Task.FromResult(item); } - public Task> FindByStatusAsync(string status) + public Task> FindByStatusAsync(string status) { - List items = _storage.Values.Where(i => i.Status == status).ToList(); - return Task.FromResult(items); + List items = _storage + .Values.Where(i => string.Equals(i.Status, status, StringComparison.Ordinal)) + .ToList(); + return Task.FromResult>(items); } - public Task> FindExpiredAsync(DateTimeOffset cutoffDate) + public Task> FindExpiredAsync(DateTimeOffset cutoffDate) { List items = _storage .Values.Where(i => i.ExpiresAt.HasValue && i.ExpiresAt.Value < cutoffDate) .ToList(); - return Task.FromResult(items); + return Task.FromResult>(items); } public Task DeleteAsync(string id) @@ -95,12 +97,14 @@ public async Task ReconcileAsync(CleanupContext context) } // Find expired items - List expiredItems = await _repository.FindExpiredAsync(context.CutoffDate); + IList expiredItems = await _repository.FindExpiredAsync(context.CutoffDate); // Filter by target status if specified if (!string.IsNullOrEmpty(context.TargetStatus)) { - expiredItems = expiredItems.Where(i => i.Status == context.TargetStatus).ToList(); + expiredItems = expiredItems + .Where(i => string.Equals(i.Status, context.TargetStatus, StringComparison.Ordinal)) + .ToList(); } int totalChecked = expiredItems.Count; diff --git a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs index f5d46bc..3ad2eef 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/Services/TransferMoneyServiceTests.cs @@ -11,15 +11,13 @@ public sealed class TransferMoneyServiceTests { #region Setup - private static ITransferMoneyService CreateService( - IRepository repository - ) + private static TransferMoneyService CreateService(InMemoryRepository repository) { return new TransferMoneyService(repository); } private static async Task CreateAndSaveAccount( - IRepository repository, + InMemoryRepository repository, string owner, Money initialBalance ) diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs index 80aee15..c9e9834 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/CleanUpExpiredOrdersReconciler.cs @@ -73,7 +73,7 @@ public async Task ReconcileAsync(OrderCleanupContext context /// In a real system, this would use a query interface to efficiently find /// orders by status and creation date. /// - private Task> FindExpiredDraftOrdersAsync(DateTimeOffset cutoffDate) + private static Task> FindExpiredDraftOrdersAsync(DateTimeOffset cutoffDate) { // In a real system, this would query the database // For demonstration purposes, return empty list diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs index 91aa3b6..ce690fe 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/Money.cs @@ -9,7 +9,7 @@ public sealed record Money(decimal Amount, string Currency = "USD") : IValueObje { public Money Add(Money other) { - if (Currency != other.Currency) + if (!string.Equals(Currency, other.Currency, StringComparison.Ordinal)) { throw new InvalidOperationException($"Cannot add different currencies: {Currency} and {other.Currency}"); } @@ -22,7 +22,7 @@ public Money Add(Money other) public Money Subtract(Money other) { - if (Currency != other.Currency) + if (!string.Equals(Currency, other.Currency, StringComparison.Ordinal)) { throw new InvalidOperationException( $"Cannot subtract different currencies: {Currency} and {other.Currency}" diff --git a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs index 14ebc2b..b97e29d 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/TestDomain/OrderData.cs @@ -11,7 +11,7 @@ public sealed class OrderData : IOutboxData // State fields (current snapshot) public string CustomerName { get; set; } = string.Empty; public decimal TotalAmount { get; set; } - public List Items { get; set; } = []; + public IList Items { get; set; } = []; public OrderStatus Status { get; set; } = OrderStatus.Draft; public OrderId Id { get; set; } = default!; public long Version { get; set; } diff --git a/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs b/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs index 4e3ee62..357fee2 100644 --- a/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs +++ b/tests/EzDdd.UseCase.Tests/Integration/UseCaseExecutionFlowTests.cs @@ -11,7 +11,7 @@ public sealed class UseCaseExecutionFlowTests [Fact] public async Task CreateAccountUseCase_WithValidInput_CreatesAccountSuccessfully() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); CreateAccountUseCase useCase = new(repository); CreateAccountInput input = new(new AccountId("acc-001"), "John Doe", new Money(1000m)); @@ -33,7 +33,7 @@ public async Task CreateAccountUseCase_WithValidInput_CreatesAccountSuccessfully [Fact] public async Task DepositUseCase_WithValidInputAndVersion_DepositsSuccessfully() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); AccountId accountId = new("acc-002"); // Create account first @@ -60,7 +60,7 @@ public async Task DepositUseCase_WithValidInputAndVersion_DepositsSuccessfully() [Fact] public async Task WithdrawUseCase_WithValidInput_WithdrawsSuccessfully() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); AccountId accountId = new("acc-003"); BankAccount account = new(accountId, "Bob Smith", new Money(1000m)); @@ -80,7 +80,7 @@ public async Task WithdrawUseCase_WithValidInput_WithdrawsSuccessfully() [Fact] public async Task DepositUseCase_WithNegativeAmount_ThrowsUseCaseFailureException() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); AccountId accountId = new("acc-004"); BankAccount account = new(accountId, "Alice Brown", new Money(1000m)); @@ -105,7 +105,7 @@ await useCase.ExecuteAsync(input) [Fact] public async Task WithdrawUseCase_WithInsufficientBalance_ThrowsUseCaseFailureException() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); AccountId accountId = new("acc-005"); BankAccount account = new(accountId, "Charlie Green", new Money(100m)); @@ -130,7 +130,7 @@ await useCase.ExecuteAsync(input) [Fact] public async Task DepositUseCase_WithWrongVersion_ThrowsUseCaseFailureException() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); AccountId accountId = new("acc-006"); BankAccount account = new(accountId, "David White", new Money(1000m)); @@ -153,7 +153,7 @@ await useCase.ExecuteAsync(input) [Fact] public async Task DepositUseCase_WhenAccountNotFound_ThrowsUseCaseFailureException() { - IRepository repository = CreateRepository(); + EsRepository repository = CreateRepository(); DepositUseCase useCase = new(repository); DepositInput input = new(new AccountId("non-existent"), new Money(100m)) { Version = 0 }; @@ -184,7 +184,7 @@ await useCase.ExecuteAsync(input) } // Helper method - private static IRepository CreateRepository() + private static EsRepository CreateRepository() { InMemoryEventStorePeer peer = new(); return new EsRepository(peer); @@ -207,7 +207,7 @@ public Task SaveAsync(EventStoreData data) public Task DeleteAsync(EventStoreData data) { - throw new NotImplementedException(); + throw new NotSupportedException(); } } } diff --git a/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs index ff90c97..08182cc 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/InputOutputTests.cs @@ -159,9 +159,9 @@ public void VersionedInput_IsAlsoAnInput() #region Test Helper Classes - private class TestInput : IInput { } + private sealed class TestInput : IInput { } - private class TestOutput : IOutput + private sealed class TestOutput : IOutput { public string Message { get; private set; } = string.Empty; public ExitCode ExitCode { get; private set; } @@ -198,7 +198,7 @@ public IOutput Succeed() } } - private class TestVersionedInput : IVersionedInput + private sealed class TestVersionedInput : IVersionedInput { public long Version { get; set; } } diff --git a/tests/EzDdd.UseCase.Tests/Port/In/ReactorTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/ReactorTests.cs index b2bceb4..32f53dc 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/ReactorTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/ReactorTests.cs @@ -34,7 +34,7 @@ public void Interface_InputIsContravariant() Assert.Same(reactor, stringReactor); } - private class TestReactor : IReactor + private sealed class TestReactor : IReactor { public string? LastInput { get; private set; } @@ -45,7 +45,7 @@ public Task ExecuteAsync(string input) } } - private class ObjectReactor : IReactor + private sealed class ObjectReactor : IReactor { public Task ExecuteAsync(object input) { diff --git a/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs index 0c3fdae..6d8f806 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/ReconcilerTests.cs @@ -154,9 +154,9 @@ public async Task IReconciler_WithNullContext_WorksCorrectly() #region Test Helper Classes - private record TestContext(int ItemCount); + private sealed record TestContext(int ItemCount); - private record TestReport( + private sealed record TestReport( int ProcessedCount, int ReconciledCount = 0, int ErrorCount = 0, @@ -164,7 +164,7 @@ private record TestReport( bool WasGlobalCleanup = false ); - private class TestReconciler : IReconciler + private sealed class TestReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -176,7 +176,7 @@ public Task ReconcileAsync(TestContext context) } } - private class NullContextReconciler : IReconciler + private sealed class NullContextReconciler : IReconciler { public Task ReconcileAsync(NullContext context) { @@ -185,7 +185,7 @@ public Task ReconcileAsync(NullContext context) } } - private class AsyncReconciler : IReconciler + private sealed class AsyncReconciler : IReconciler { public async Task ReconcileAsync(TestContext context) { @@ -197,7 +197,7 @@ public async Task ReconcileAsync(TestContext context) } } - private class MultiItemReconciler : IReconciler + private sealed class MultiItemReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -208,7 +208,7 @@ public Task ReconcileAsync(TestContext context) } } - private class NoChangeReconciler : IReconciler + private sealed class NoChangeReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -219,7 +219,7 @@ public Task ReconcileAsync(TestContext context) } } - private class ConflictReconciler : IReconciler + private sealed class ConflictReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -232,7 +232,7 @@ public Task ReconcileAsync(TestContext context) } } - private class PartialFailureReconciler : IReconciler + private sealed class PartialFailureReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -246,7 +246,7 @@ public Task ReconcileAsync(TestContext context) } } - private class InvalidContextReconciler : IReconciler + private sealed class InvalidContextReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -259,7 +259,7 @@ public Task ReconcileAsync(TestContext context) } } - private class CancellableReconciler : IReconciler + private sealed class CancellableReconciler : IReconciler { public Task ReconcileAsync(TestContext context) { @@ -268,7 +268,7 @@ public Task ReconcileAsync(TestContext context) } } - private class GlobalCleanupReconciler : IReconciler + private sealed class GlobalCleanupReconciler : IReconciler { public Task ReconcileAsync(NullContext context) { diff --git a/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs b/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs index 86565a3..ef61a19 100644 --- a/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/In/UseCaseTests.cs @@ -43,12 +43,12 @@ public async Task UseCase_WithGenericConstraints_WorksCorrectly() // Test Helper Classes // ======================================== - private class TestInput : IInput + private sealed class TestInput : IInput { public string Value { get; set; } = string.Empty; } - private class TestOutput : IOutput + private sealed class TestOutput : IOutput { public string Result { get; set; } = string.Empty; public string Message { get; private set; } = string.Empty; @@ -86,7 +86,7 @@ public IOutput Succeed() } } - private class TestUseCase : IUseCase + private sealed class TestUseCase : IUseCase { public Task ExecuteAsync(TestInput input) { @@ -95,7 +95,7 @@ public Task ExecuteAsync(TestInput input) } } - private class FailingUseCase : IUseCase + private sealed class FailingUseCase : IUseCase { public Task ExecuteAsync(TestInput input) { @@ -103,7 +103,7 @@ public Task ExecuteAsync(TestInput input) } } - private class GenericUseCase : IUseCase + private sealed class GenericUseCase : IUseCase { public Task ExecuteAsync(TestInput input) { diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs index 555bc13..f0d2966 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventDataBuilderTests.cs @@ -11,7 +11,7 @@ public class DomainEventDataBuilderTests /// /// Test helper record for payload serialization. /// - private record TestPayload(int Amount, string Currency); + private sealed record TestPayload(int Amount, string Currency); #region Factory Methods Tests diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs index 65c0e14..2509b7f 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMapperTests.cs @@ -279,7 +279,7 @@ public void RoundTrip_ShouldPreserveEventData() #region Test Event Definitions // Test internal domain event - private record TestAccountCreated( + private sealed record TestAccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -288,7 +288,7 @@ private record TestAccountCreated( decimal InitialBalance ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record TestMoneyDeposited( + private sealed record TestMoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs index fe107f0..b2f13d6 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/DomainEventMetadataTests.cs @@ -65,7 +65,7 @@ public void ToData_WithMetadata_SerializesMetadataCorrectly() public void ToData_WithEmptyMetadata_SerializesEmptyDictionary() { // ReSharper disable once CollectionNeverUpdated.Local - Dictionary emptyMetadata = new(); + Dictionary emptyMetadata = []; TestEvent @event = new(Guid.NewGuid(), DateTimeOffset.UtcNow, "source-1", "test", emptyMetadata); DomainEventData data = DomainEventMapper.ToData(@event); @@ -217,7 +217,7 @@ public void Metadata_WithUnicodeCharacters_HandledCorrectly() [Fact] public void Metadata_WithLargeNumberOfKeys_HandledCorrectly() { - Dictionary metadata = new(); + Dictionary metadata = []; for (int i = 0; i < 100; i++) { metadata[$"Key{i}"] = $"Value{i}"; diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs index 53b04a5..c26878b 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/ExternalDomainEventTests.cs @@ -64,7 +64,7 @@ public void ExternalDomainEvent_ShouldSupportTypeChecking() } // Test event for external domain event - private record TestExternalEvent( + private sealed record TestExternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -73,7 +73,7 @@ string CustomerId ) : IExternalDomainEvent; // Test event for internal domain event (for comparison) - private record TestInternalEvent( + private sealed record TestInternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs index b799aa4..42d9998 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/InternalDomainEventDtoTests.cs @@ -162,7 +162,7 @@ public void InternalDomainEventDto_WithNestedJsonInJsonEvent_ShouldSerialize() BoundedContext = "sales", EventSimpleName = "OrderCreated", JsonEvent = jsonEvent, - Metadata = new Dictionary(), + Metadata = new Dictionary(StringComparer.Ordinal), }; string json = JsonSerializer.Serialize(dto); @@ -184,7 +184,7 @@ public void InternalDomainEventDto_WithEmptyCollections_ShouldWork() BoundedContext = "test", EventSimpleName = "TestEvent", JsonEvent = "{}", - Metadata = new Dictionary(), + Metadata = new Dictionary(StringComparer.Ordinal), }; Assert.NotNull(dto); diff --git a/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs b/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs index 00dd269..ab43b4f 100644 --- a/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/InOut/Messaging/ExternalDomainEventPublisherTests.cs @@ -41,7 +41,7 @@ public void Interface_InputIsContravariant() Assert.Same(publisher, derivedPublisher); } - private class TestPublisher : IExternalDomainEventPublisher + private sealed class TestPublisher : IExternalDomainEventPublisher { public BaseExternalEvent? LastPublishedEvent { get; private set; } @@ -61,7 +61,7 @@ IReadOnlyDictionary Metadata ) : IExternalDomainEvent; // Derived external event for the contravariance test - private record DerivedExternalEvent( + private sealed record DerivedExternalEvent( Guid Id, DateTimeOffset OccurredOn, string Source, diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs index 6f6d34e..d1d2f8a 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EsRepositoryTests.cs @@ -9,12 +9,12 @@ public class EsRepositoryTests { #region Test Helpers - private record TestId(string Value) + private sealed record TestId(string Value) { public override string ToString() => Value; } - private record TestEvent( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -22,7 +22,7 @@ private record TestEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private class TestAggregate : EsAggregateRoot + private sealed class TestAggregate : EsAggregateRoot { private string _name = string.Empty; public string Name => _name; @@ -82,9 +82,9 @@ protected override void _EnsureInvariant() public override string GetCategory() => "test"; } - private class MockRepositoryPeer : IRepositoryPeer, TestId> + private sealed class MockRepositoryPeer : IRepositoryPeer, TestId> { - private readonly Dictionary> _store = new(); + private readonly Dictionary> _store = []; public bool ThrowOnSave { get; set; } public bool ThrowOnDelete { get; set; } diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs index 0c1fcaa..4317077 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreDataTests.cs @@ -8,9 +8,9 @@ public class EventStoreDataTests { #region Test Helpers - private record TestId(string Value); + private sealed record TestId(string Value); - private record TestEvent( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -93,7 +93,7 @@ public void StreamName_WhenSet_CanBeRetrieved() [Fact] public void GetOptimisticLockVersion_WithNoEvents_ReturnsVersion() { - var data = new EventStoreData { Version = 5, Events = new List() }; + var data = new EventStoreData { Version = 5, Events = [] }; var lockVersion = data.GetOptimisticLockVersion(); diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs index f206147..02822f0 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/EventStoreMapperTests.cs @@ -8,12 +8,12 @@ public class EventStoreMapperTests { #region Test Helpers - private record TestId(string Value) + private sealed record TestId(string Value) { public override string ToString() => Value; } - private record TestEvent( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -21,7 +21,7 @@ private record TestEvent( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private class TestAggregate : EsAggregateRoot + private sealed class TestAggregate : EsAggregateRoot { private string _name = string.Empty; diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs index 7c72cd9..9daefa9 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxDataTests.cs @@ -38,7 +38,7 @@ public void IOutboxData_CanBeImplemented() "John Doe", new Dictionary() ); - IReadOnlyList events = new List { @event }; + IReadOnlyList events = [@event]; BankAccountData data = new(accountId, 1, events, "account-acc-123", "John Doe", 1000.00m); @@ -88,7 +88,7 @@ public void IOutboxData_InheritsEventsProperty() "Charlie", new Dictionary() ); - IReadOnlyList events = new List { @event }; + IReadOnlyList events = [@event]; BankAccountData data = new(accountId, 0, events, "stream", "Charlie", 0.00m); IReadOnlyList retrievedEvents = data.Events; @@ -155,9 +155,9 @@ public void IOutboxData_SupportsMultipleStateFields() #region Test Data Structures - private record AccountId(string Value); + private sealed record AccountId(string Value); - private record AccountCreated( + private sealed record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -165,7 +165,7 @@ private record AccountCreated( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private class BankAccountData : IOutboxData + private sealed class BankAccountData : IOutboxData { public BankAccountData( AccountId id, diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs index 43d3b7a..1cfb2e4 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxMapperTests.cs @@ -183,9 +183,9 @@ public void ToData_AfterMultipleOperations_VersionIncremented() #region Test Domain Model - private record AccountId(string Value); + private sealed record AccountId(string Value); - private record AccountCreated( + private sealed record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -194,7 +194,7 @@ private record AccountCreated( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited( + private sealed record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -202,7 +202,7 @@ private record MoneyDeposited( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private class BankAccount : AggregateRoot + private sealed class BankAccount : AggregateRoot { // Constructor for new aggregate public BankAccount(AccountId id, string owner, decimal initialBalance) @@ -248,7 +248,7 @@ public void Deposit(decimal amount) } } - private class BankAccountData : IOutboxData + private sealed class BankAccountData : IOutboxData { public BankAccountData( AccountId id, @@ -281,7 +281,7 @@ public long GetOptimisticLockVersion() } } - private class BankAccountMapper : OutboxMapper + private sealed class BankAccountMapper : OutboxMapper { public override BankAccountData ToData(BankAccount aggregate) { diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryFindByIdTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryFindByIdTests.cs index cd7b92e..d51dc0e 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryFindByIdTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryFindByIdTests.cs @@ -44,7 +44,7 @@ private static OutboxRepository NewReposi #region Test Infrastructure - private class TestAggregate : AggregateRoot + private sealed class TestAggregate : AggregateRoot { public TestAggregate(string id, bool isDeleted) { @@ -53,7 +53,7 @@ public TestAggregate(string id, bool isDeleted) } } - private class TestOutboxData : IOutboxData + private sealed class TestOutboxData : IOutboxData { public TestOutboxData(string id, bool isDeleted) { @@ -73,7 +73,7 @@ public long GetOptimisticLockVersion() } } - private class TestOutboxMapper : OutboxMapper + private sealed class TestOutboxMapper : OutboxMapper { public override TestOutboxData ToData(TestAggregate aggregate) { @@ -86,9 +86,9 @@ public override TestAggregate ToDomain(TestOutboxData data) } } - private class InMemoryPeer : IRepositoryPeer + private sealed class InMemoryPeer : IRepositoryPeer { - private readonly Dictionary _storage = new(); + private readonly Dictionary _storage = []; public Task FindByIdAsync(string id) { diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs index 8e0ef09..2e4bf79 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/OutboxRepositoryTests.cs @@ -253,9 +253,9 @@ public async Task SaveAsync_WithMultipleOperations_MaintainsConsistency() #region Test Infrastructure - private record AccountId(string Value); + private sealed record AccountId(string Value); - private record AccountCreated( + private sealed record AccountCreated( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -264,7 +264,7 @@ private record AccountCreated( IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private record MoneyDeposited( + private sealed record MoneyDeposited( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -272,7 +272,7 @@ private record MoneyDeposited( IReadOnlyDictionary Metadata ) : IInternalDomainEvent; - private class BankAccount : AggregateRoot + private sealed class BankAccount : AggregateRoot { public BankAccount(AccountId id, string owner, decimal initialBalance) { @@ -316,7 +316,7 @@ public void Deposit(decimal amount) } } - private class BankAccountData : IOutboxData + private sealed class BankAccountData : IOutboxData { public BankAccountData( AccountId id, @@ -348,7 +348,7 @@ public long GetOptimisticLockVersion() } } - private class BankAccountMapper : OutboxMapper + private sealed class BankAccountMapper : OutboxMapper { public override BankAccountData ToData(BankAccount aggregate) { @@ -369,9 +369,9 @@ public override BankAccount ToDomain(BankAccountData data) } // Mock RepositoryPeer for testing - private class InMemoryRepositoryPeer : IRepositoryPeer + private sealed class InMemoryRepositoryPeer : IRepositoryPeer { - private readonly Dictionary _storage = new(); + private readonly Dictionary _storage = []; public bool ThrowOnSave { get; set; } public Task FindByIdAsync(AccountId id) diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryPeerTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryPeerTests.cs index 703b338..a59e19a 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryPeerTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryPeerTests.cs @@ -9,13 +9,13 @@ public class RepositoryPeerTests #region Test Fixtures // Test data structure implementing IStoreData - private record TestDataId(string Value); + private sealed record TestDataId(string Value); - private class TestStoreDataImpl : IStoreData + private sealed class TestStoreDataImpl : IStoreData { public long Version { get; set; } = -1; public TestDataId Id { get; set; } = new("default"); - public IReadOnlyList Events { get; set; } = new List(); + public IReadOnlyList Events { get; set; } = []; public string StreamName { get; set; } = string.Empty; public string Data { get; set; } = string.Empty; } @@ -24,9 +24,9 @@ private class TestStoreDataImpl : IStoreData #region Mock Implementations - private class MockRepositoryPeer : IRepositoryPeer + private sealed class MockRepositoryPeer : IRepositoryPeer { - private readonly Dictionary _store = new(); + private readonly Dictionary _store = []; public bool ThrowOnSave { get; set; } public Task FindByIdAsync(TestDataId id) @@ -193,14 +193,14 @@ public async Task RepositoryPeer_TransactionBoundaryNote_CompileTimeCheck() { Id = dataId, Data = "Test", - Events = new List - { + Events = + [ // In real implementation, SaveAsync would: // 1. Begin transaction // 2. Save aggregate state // 3. Save events (outbox) // 4. Commit transaction atomically - }, + ], }; await peer.SaveAsync(storeData); diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs index 08b8fb8..d9c92e2 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/RepositoryTests.cs @@ -9,16 +9,16 @@ public class RepositoryTests #region Test Fixtures // Test aggregate for repository testing - private record TestAggregateId(string Value); + private sealed record TestAggregateId(string Value); - private record TestEvent( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, IReadOnlyDictionary Metadata ) : IInternalDomainEvent, IInternalDomainEvent.IConstructionEvent; - private class TestAggregate : AggregateRoot + private sealed class TestAggregate : AggregateRoot { public string Name { get; } = string.Empty; @@ -43,9 +43,9 @@ public TestAggregate() { } #region Mock Implementations - private class MockRepository : IRepository + private sealed class MockRepository : IRepository { - private readonly Dictionary _store = new(); + private readonly Dictionary _store = []; public bool ThrowOnSave { get; set; } public Task FindByIdAsync(TestAggregateId id) diff --git a/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs b/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs index bcaf10b..1a04253 100644 --- a/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs +++ b/tests/EzDdd.UseCase.Tests/Port/Out/StoreDataTests.cs @@ -7,15 +7,15 @@ public class StoreDataTests { #region Test Helpers - private class TestStoreData : IStoreData + private sealed class TestStoreData : IStoreData { public long Version { get; set; } = -1; public string Id { get; set; } = string.Empty; - public IReadOnlyList Events { get; set; } = new List(); + public IReadOnlyList Events { get; set; } = []; public string StreamName { get; set; } = string.Empty; } - private record TestEvent( + private sealed record TestEvent( Guid Id, DateTimeOffset OccurredOn, string Source, @@ -64,10 +64,7 @@ public void GetOptimisticLockVersion_NewAggregate_ReturnsZero() IStoreData storeData = new TestStoreData { Version = -1, - Events = new List - { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - }, + Events = [new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary())], }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -81,10 +78,7 @@ public void GetOptimisticLockVersion_ExistingAggregateWithOneEvent_ReturnsOne() IStoreData storeData = new TestStoreData { Version = 0, - Events = new List - { - new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - }, + Events = [new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary())], }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -98,12 +92,12 @@ public void GetOptimisticLockVersion_ExistingAggregateWithMultipleEvents_Returns IStoreData storeData = new TestStoreData { Version = 5, - Events = new List - { + Events = + [ new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - }, + ], }; var optimisticLockVersion = storeData.GetOptimisticLockVersion(); @@ -149,22 +143,22 @@ public void CompleteLifecycle_NewToExisting_VersionProgression() StreamName = "account-789", }; - storeData.Events = new List - { + storeData.Events = + [ new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - }; + ]; Assert.Equal(0, storeData.GetOptimisticLockVersion()); storeData.Version = 0; - storeData.Events = new List(); + storeData.Events = []; Assert.Equal(0, storeData.GetOptimisticLockVersion()); - storeData.Events = new List - { + storeData.Events = + [ new TestEvent(Guid.NewGuid(), DateTimeOffset.UtcNow, "test", new Dictionary()), - }; + ]; Assert.Equal(1, storeData.GetOptimisticLockVersion()); } From afa5e029eba68fa17fb9a843582474858032ba6b Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 22:00:04 +0800 Subject: [PATCH 07/18] build: enforce Level Z with Meziantou and Roslynator analyzers Add Meziantou.Analyzer and Roslynator.Analyzers (PrivateAssets=all) with EnforceCodeStyleInBuild and latest-recommended analysis level. Rebase .editorconfig on the uContract.NET Level Z profile: every :suggestion promoted to :warning (build-blocking with TreatWarningsAsErrors), the formatting section removed in favor of CSharpier, and the broken type_parameter naming rule fixed. Test-only suppressions (CA1707, MA0002, MA0004, MA0048, RCS1194) and thirteen targeted SuppressMessage sites document their rationale inline; BiMap.cs and ConverterTests.cs also carry their remaining mechanical fixes. --- .editorconfig | 194 ++++++++---------- Directory.Build.props | 13 ++ src/EzDdd.Common/BiMap.cs | 9 +- src/EzDdd.Cqrs/CqrsOutput.cs | 6 + src/EzDdd.Entity/AggregateRoot.cs | 12 ++ src/EzDdd.Entity/EsAggregateRoot.cs | 31 +++ src/EzDdd.Entity/IDomainEventSource.cs | 7 + src/EzDdd.UseCase/GlobalSuppressions.cs | 14 ++ .../IExternalDomainEventPublisher.cs | 7 + tests/EzDdd.Common.Tests/ConverterTests.cs | 32 +-- .../EzDdd.UseCase.Tests/GlobalSuppressions.cs | 14 ++ 11 files changed, 217 insertions(+), 122 deletions(-) create mode 100644 src/EzDdd.UseCase/GlobalSuppressions.cs create mode 100644 tests/EzDdd.UseCase.Tests/GlobalSuppressions.cs diff --git a/.editorconfig b/.editorconfig index 3868bd7..2cc0db1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -20,6 +20,27 @@ tab_width = 4 # New line preferences insert_final_newline = false +#### Analyzer suppressions #### + +# Suppressed in test files only: xUnit's Assert.Contains/DoesNotContain string overloads in the xUnit version used here do not accept a StringComparison argument; the generic collection overload matches instead, producing an unrelated conversion error. Test assertions use xUnit's default comparison semantics, which are appropriate for test code. Production-code string operations remain enforced at Level Z. +[tests/**/*.cs] +dotnet_diagnostic.MA0074.severity = none + +# Suppressed in test files only: test methods follow the idiomatic xUnit Method_Scenario_ExpectedResult underscore naming convention; removing underscores would destroy the readable test names that document behavior. Production identifiers remain enforced at Level Z. +dotnet_diagnostic.CA1707.severity = none + +# Suppressed in test files only: dictionaries and sets in tests key on short ASCII literals where the default ordinal semantics are exactly what the test intends; threading explicit comparers through hundreds of fixture collections is churn without a behavior payoff. Production collections remain enforced at Level Z. +dotnet_diagnostic.MA0002.severity = none + +# Suppressed in test files only: xUnit v2 runs test methods without a SynchronizationContext, so ConfigureAwait(false) in test bodies cannot prevent any deadlock and only adds noise. Library code keeps MA0004 enforced because it runs under arbitrary consumer synchronization contexts. +dotnet_diagnostic.MA0004.severity = none + +# Suppressed in test files only: test suites intentionally co-locate small helper types (fakes, fixtures, scenario exceptions) in the same file as the scenarios they serve; one-type-per-file would scatter the reading path of a test. Production files remain one type per file at Level Z. +dotnet_diagnostic.MA0048.severity = none + +# Suppressed in test files only: scenario exceptions in the banking test domain intentionally require domain-specific constructor data (AccountId, Money); adding the parameterless/(string)/(string, Exception) constructors would leave their non-nullable properties uninitialized (forcing null-forgiving hacks) and would let tests construct invalid scenarios. Production exceptions implement the standard constructors and remain enforced at Level Z. +dotnet_diagnostic.RCS1194.severity = none + #### .NET Coding Conventions #### [*.{cs,vb}] @@ -48,24 +69,24 @@ dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:sil dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent # Expression-level preferences -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_namespace_match_folder = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_object_initializer = true:suggestion +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_namespace_match_folder = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_collection_expression = when_types_loosely_match:warning +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_return = true:warning +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning # Field preferences dotnet_style_readonly_field = true:warning @@ -88,181 +109,138 @@ csharp_style_var_when_type_is_apparent = false:silent csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_lambdas = true:warning csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_prefer_extended_property_pattern = true:suggestion -csharp_style_prefer_not_pattern = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_extended_property_pattern = true:warning +csharp_style_prefer_not_pattern = true:warning csharp_style_prefer_pattern_matching = true:silent -csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_switch_expression = true:warning # Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion +csharp_style_conditional_delegate_call = true:warning # Modifier preferences -csharp_prefer_static_anonymous_function = true:suggestion +csharp_prefer_static_anonymous_function = true:warning csharp_prefer_static_local_function = true:warning -csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,file,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:warning +csharp_style_prefer_readonly_struct = true:warning +csharp_style_prefer_readonly_struct_member = true:warning # Code-block preferences csharp_prefer_braces = true:silent -csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = file_scoped:suggestion +csharp_prefer_simple_using_statement = true:warning +csharp_style_namespace_declarations = file_scoped:warning csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_primary_constructors = true:warning csharp_style_prefer_top_level_statements = true:silent # Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_local_over_anonymous_function = true:suggestion -csharp_style_prefer_null_check_over_type_check = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_prefer_tuple_swap = true:suggestion -csharp_style_prefer_utf8_string_literals = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_local_over_anonymous_function = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_tuple_swap = true:warning +csharp_style_prefer_utf8_string_literals = true:warning +csharp_style_throw_expression = true:warning +csharp_style_unused_value_assignment_preference = discard_variable:warning csharp_style_unused_value_expression_statement_preference = discard_variable:silent # 'using' directive preferences csharp_using_directive_placement = outside_namespace:silent #### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true +# Formatting is delegated to CSharpier (see .csharpierrc.yaml). +# Rules previously defined here have been removed to avoid conflicts. #### Naming styles #### [*.{cs,vb}] # Naming rules -dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = warning dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion +dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = warning dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase -dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion +dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = warning dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase -dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.methods_should_be_pascalcase.severity = warning dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.properties_should_be_pascalcase.severity = warning dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.events_should_be_pascalcase.severity = warning dotnet_naming_rule.events_should_be_pascalcase.symbols = events dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_variables_should_be_camelcase.severity = warning dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase -dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion +dotnet_naming_rule.local_constants_should_be_camelcase.severity = warning dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase -dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion +dotnet_naming_rule.parameters_should_be_camelcase.severity = warning dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase -dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascalcase.severity = warning dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be__camelcase.severity = warning dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase -dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion +dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = warning dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = s_camelcase -dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = warning dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = warning dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = warning dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = warning dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.enums_should_be_pascalcase.severity = warning dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascalcase.severity = warning dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase -dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = warning dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase @@ -308,7 +286,7 @@ dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, meth dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = -dotnet_naming_symbols.type_parameters.applicable_kinds = namespace +dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter dotnet_naming_symbols.type_parameters.applicable_accessibilities = * dotnet_naming_symbols.type_parameters.required_modifiers = diff --git a/Directory.Build.props b/Directory.Build.props index 1d1d62e..4611644 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,6 +8,8 @@ enable enable true + true + latest-recommended true @@ -35,6 +37,17 @@ embedded + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/src/EzDdd.Common/BiMap.cs b/src/EzDdd.Common/BiMap.cs index 40de2e1..23812df 100644 --- a/src/EzDdd.Common/BiMap.cs +++ b/src/EzDdd.Common/BiMap.cs @@ -31,6 +31,11 @@ namespace EzDdd.Common; /// /// /// +[SuppressMessage( + "Naming", + "CA1710:Identifiers should have correct suffix", + Justification = "BiMap is the established bidirectional-map name shared with Java ezddd (and Guava); renaming to *Dictionary or *Collection would break cross-language parity for a published API." +)] public class BiMap : IDictionary where TKey : notnull where TValue : notnull @@ -44,8 +49,8 @@ public class BiMap : IDictionary /// public BiMap() { - _forward = new Dictionary(); - _reverse = new Dictionary(); + _forward = []; + _reverse = []; } /// diff --git a/src/EzDdd.Cqrs/CqrsOutput.cs b/src/EzDdd.Cqrs/CqrsOutput.cs index dd93c2e..0bd37e8 100644 --- a/src/EzDdd.Cqrs/CqrsOutput.cs +++ b/src/EzDdd.Cqrs/CqrsOutput.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using EzDdd.UseCase.Port.In; namespace EzDdd.Cqrs; @@ -131,6 +132,11 @@ IOutput IOutput.SetId(string id) /// This static factory method requires that T has a parameterless constructor. /// The new() constraint at the class level enables instantiation. /// + [SuppressMessage( + "Design", + "CA1000:Do not declare static members on generic types", + Justification = "CqrsOutput.Create() is the designed fluent entry point of the self-referential generic builder, matching Java ezddd's CqrsOutput.create() API; the type argument is always inferred from the concrete subclass." + )] public static T Create() { return new T(); diff --git a/src/EzDdd.Entity/AggregateRoot.cs b/src/EzDdd.Entity/AggregateRoot.cs index d61acaf..d32f302 100644 --- a/src/EzDdd.Entity/AggregateRoot.cs +++ b/src/EzDdd.Entity/AggregateRoot.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace EzDdd.Entity; /// @@ -210,6 +212,11 @@ public abstract class AggregateRoot : IEntity, IDomainEventSou /// } /// /// + [SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The parameter name 'event' is the established domain-event vocabulary inherited from Java ezddd's public API; C# callers use the escaped identifier @event. Renaming would break named-argument compatibility and cross-language parity." + )] public virtual void Apply(TEvent @event) { ArgumentNullException.ThrowIfNull(@event); @@ -367,6 +374,11 @@ public void ClearDomainEvents() /// (matching Java ezddd's behavior). /// /// + [SuppressMessage( + "Naming", + "CA1707:Identifiers should not contain underscores", + Justification = "The leading underscore marks framework-internal template methods that only subclasses may call, mirroring Java ezddd's protected API surface; renaming would break semantic parity and the published protected contract." + )] protected void _AddDomainEvent(TEvent @event) { lock (_domainEventsLock) diff --git a/src/EzDdd.Entity/EsAggregateRoot.cs b/src/EzDdd.Entity/EsAggregateRoot.cs index 53f3df6..5a21441 100644 --- a/src/EzDdd.Entity/EsAggregateRoot.cs +++ b/src/EzDdd.Entity/EsAggregateRoot.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using uContract; namespace EzDdd.Entity; @@ -149,6 +150,16 @@ protected EsAggregateRoot() { } /// /// Thrown when event replay violates invariants or encounters unknown event types /// + [SuppressMessage( + "Design", + "MA0017:Abstract types should not have public or internal constructors", + Justification = "The replay constructor is deliberately public so repositories (EsRepository) can reconstruct aggregates via reflection, matching Java ezddd's design (documented in the XML remarks above)." + )] + [SuppressMessage( + "Design", + "MA0056:Do not call overridable members in constructor", + Justification = "Event-sourcing replay is the designed exception: the constructor rebuilds state by replaying history through the _ReplayEvents/_When template methods under the R1-R3 invariant rules. Derived classes start from default field values, which is exactly what replay expects; consequently derived _When() implementations must not depend on fields initialized in their own constructor." + )] public EsAggregateRoot(IEnumerable events) : this() { @@ -277,6 +288,16 @@ public sealed override void Apply(TEvent @event) /// } /// /// + [SuppressMessage( + "Naming", + "CA1707:Identifiers should not contain underscores", + Justification = "The leading underscore marks framework-internal template methods that only subclasses may call, mirroring Java ezddd's protected API surface; renaming would break semantic parity and the published protected contract." + )] + [SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The parameter name 'event' is the established domain-event vocabulary inherited from Java ezddd's public API; C# implementers use the escaped identifier @event. Renaming would break named-argument compatibility and cross-language parity." + )] protected abstract void _When(TEvent @event); /// @@ -348,6 +369,11 @@ public sealed override void Apply(TEvent @event) /// } /// /// + [SuppressMessage( + "Naming", + "CA1707:Identifiers should not contain underscores", + Justification = "The leading underscore marks framework-internal template methods that only subclasses may call, mirroring Java ezddd's protected API surface; renaming would break semantic parity and the published protected contract." + )] protected virtual void _EnsureInvariant() { // Default: no-op @@ -369,6 +395,11 @@ protected virtual void _EnsureInvariant() /// replay behavior if needed (e.g., skip certain events, apply optimizations). /// /// + [SuppressMessage( + "Naming", + "CA1707:Identifiers should not contain underscores", + Justification = "The leading underscore marks framework-internal template methods that only subclasses may call, mirroring Java ezddd's protected API surface; renaming would break semantic parity and the published protected contract." + )] protected virtual void _ReplayEvents(IEnumerable events) { foreach (TEvent @event in events) diff --git a/src/EzDdd.Entity/IDomainEventSource.cs b/src/EzDdd.Entity/IDomainEventSource.cs index 7bc755f..acc9620 100644 --- a/src/EzDdd.Entity/IDomainEventSource.cs +++ b/src/EzDdd.Entity/IDomainEventSource.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace EzDdd.Entity; /// @@ -50,6 +52,11 @@ public interface IDomainEventSource /// For state-sourced aggregates, this method only collects the event without /// automatically mutating state. /// + [SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The parameter name 'event' is the established domain-event vocabulary inherited from Java ezddd's public API; C# implementers use the escaped identifier @event. Renaming would break named-argument compatibility and cross-language parity." + )] void Apply(TEvent @event); /// diff --git a/src/EzDdd.UseCase/GlobalSuppressions.cs b/src/EzDdd.UseCase/GlobalSuppressions.cs new file mode 100644 index 0000000..a4648b3 --- /dev/null +++ b/src/EzDdd.UseCase/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "Port.In is the Clean Architecture in-port namespace mirrored from Java ezddd's package layout (ezddd.usecase.port.in); renaming would break the structural parity that the port maintains. Visual Basic consumers are not a target audience for this library.", + Scope = "namespace", + Target = "~N:EzDdd.UseCase.Port.In" +)] diff --git a/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs b/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs index 6a1c5c1..acacc96 100644 --- a/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs +++ b/src/EzDdd.UseCase/Port/InOut/Messaging/IExternalDomainEventPublisher.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace EzDdd.UseCase.Port.InOut.Messaging; /// @@ -29,5 +31,10 @@ public interface IExternalDomainEventPublisher /// /// The external domain event to publish. /// A task representing the asynchronous publish operation. + [SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The parameter name 'event' is the established domain-event vocabulary inherited from Java ezddd's public API; C# implementers use the escaped identifier @event. Renaming would break named-argument compatibility and cross-language parity." + )] Task PublishAsync(TEvent @event); } diff --git a/tests/EzDdd.Common.Tests/ConverterTests.cs b/tests/EzDdd.Common.Tests/ConverterTests.cs index 99a5166..f508d03 100644 --- a/tests/EzDdd.Common.Tests/ConverterTests.cs +++ b/tests/EzDdd.Common.Tests/ConverterTests.cs @@ -1,5 +1,8 @@ // ReSharper disable ConvertToLocalFunction +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + namespace EzDdd.Common.Tests; public class ConverterTests @@ -8,7 +11,7 @@ public class ConverterTests public void Converter_WhenUsingLambdaExpression_ConvertsSuccessfully() { // ReSharper disable once ConvertClosureToMethodGroup - Converter converter = s => int.Parse(s); + Converter converter = s => int.Parse(s, CultureInfo.InvariantCulture); int result = converter("42"); @@ -44,9 +47,9 @@ public void Converter_WhenUsingMultiLineExpression_ConvertsSuccessfully() [Fact] public void Converter_WhenUsingMultipleConverters_MaintainsIndependence() { - Converter intToString = i => i.ToString(); + Converter intToString = i => i.ToString(CultureInfo.InvariantCulture); // ReSharper disable once ConvertClosureToMethodGroup - Converter stringToInt = s => int.Parse(s); + Converter stringToInt = s => int.Parse(s, CultureInfo.InvariantCulture); string str = intToString(123); int num = stringToInt("456"); @@ -58,9 +61,9 @@ public void Converter_WhenUsingMultipleConverters_MaintainsIndependence() [Fact] public void Converter_WhenChainingConversions_WorksCorrectly() { - Converter intToString = i => i.ToString(); + Converter intToString = i => i.ToString(CultureInfo.InvariantCulture); // ReSharper disable once ConvertClosureToMethodGroup - Converter stringToDouble = s => double.Parse(s); + Converter stringToDouble = s => double.Parse(s, CultureInfo.InvariantCulture); string intermediate = intToString(42); double result = stringToDouble(intermediate); @@ -71,7 +74,7 @@ public void Converter_WhenChainingConversions_WorksCorrectly() [Fact] public void Converter_SupportsCovariance_AllowsDerivedTypeReturn() { - Converter stringToString = s => s.ToUpper(); + Converter stringToString = s => s.ToUpperInvariant(); Converter stringToObject = stringToString; object result = stringToObject("hello"); @@ -141,20 +144,25 @@ public void Converter_WhenUsingInstanceMethod_WorksAsMethodGroup() // Test helper methods private static string _ConvertToUpper(string input) { - return input.ToUpper(); + return input.ToUpperInvariant(); } // Test domain classes - private record User(int Id, string Name); + private sealed record User(int Id, string Name); - private record UserDto(int Id, string Name); + private sealed record UserDto(int Id, string Name); - private record Order(int Id, string ProductName, int Quantity, decimal UnitPrice); + private sealed record Order(int Id, string ProductName, int Quantity, decimal UnitPrice); - private record OrderSummary(int OrderId, string Description, decimal TotalAmount); + private sealed record OrderSummary(int OrderId, string Description, decimal TotalAmount); - private class ConversionHelper + private sealed class ConversionHelper { + [SuppressMessage( + "Performance", + "CA1822:Mark members as static", + Justification = "Converter_WhenUsingInstanceMethod_WorksAsMethodGroup exercises instance-method group conversion; making this static would change the very scenario under test." + )] public string IntToString(int value) { return $"Number: {value}"; diff --git a/tests/EzDdd.UseCase.Tests/GlobalSuppressions.cs b/tests/EzDdd.UseCase.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..b7d924c --- /dev/null +++ b/tests/EzDdd.UseCase.Tests/GlobalSuppressions.cs @@ -0,0 +1,14 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage( + "Naming", + "CA1716:Identifiers should not match keywords", + Justification = "The test namespace mirrors the production EzDdd.UseCase.Port.In namespace (itself mirrored from Java ezddd's package layout and suppressed for the same reason); keeping the folder structure aligned makes each test discoverable next to its subject.", + Scope = "namespace", + Target = "~N:EzDdd.UseCase.Tests.Port.In" +)] From 45a03b8d7c8682f7c86ad19e946b71df47c567ad Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 22:14:46 +0800 Subject: [PATCH 08/18] build: track the public API surface with PublicApiAnalyzers Add Microsoft.CodeAnalysis.PublicApiAnalyzers 3.3.4 to the five packable projects with empty PublicAPI.Shipped.txt baselines (nothing released yet) and the current surface captured in PublicAPI.Unshipped.txt. Any public API change now fails the build until the baseline is updated. --- src/EzDdd.Common/EzDdd.Common.csproj | 14 +++ src/EzDdd.Common/PublicAPI.Shipped.txt | 1 + src/EzDdd.Common/PublicAPI.Unshipped.txt | 35 ++++++ src/EzDdd.Core/EzDdd.Core.csproj | 14 +++ src/EzDdd.Core/PublicAPI.Shipped.txt | 1 + src/EzDdd.Core/PublicAPI.Unshipped.txt | 1 + src/EzDdd.Cqrs/EzDdd.Cqrs.csproj | 14 +++ src/EzDdd.Cqrs/PublicAPI.Shipped.txt | 1 + src/EzDdd.Cqrs/PublicAPI.Unshipped.txt | 29 +++++ src/EzDdd.Entity/EzDdd.Entity.csproj | 14 +++ src/EzDdd.Entity/PublicAPI.Shipped.txt | 1 + src/EzDdd.Entity/PublicAPI.Unshipped.txt | 49 ++++++++ src/EzDdd.UseCase/EzDdd.UseCase.csproj | 14 +++ src/EzDdd.UseCase/PublicAPI.Shipped.txt | 1 + src/EzDdd.UseCase/PublicAPI.Unshipped.txt | 138 ++++++++++++++++++++++ 15 files changed, 327 insertions(+) create mode 100644 src/EzDdd.Common/PublicAPI.Shipped.txt create mode 100644 src/EzDdd.Common/PublicAPI.Unshipped.txt create mode 100644 src/EzDdd.Core/PublicAPI.Shipped.txt create mode 100644 src/EzDdd.Core/PublicAPI.Unshipped.txt create mode 100644 src/EzDdd.Cqrs/PublicAPI.Shipped.txt create mode 100644 src/EzDdd.Cqrs/PublicAPI.Unshipped.txt create mode 100644 src/EzDdd.Entity/PublicAPI.Shipped.txt create mode 100644 src/EzDdd.Entity/PublicAPI.Unshipped.txt create mode 100644 src/EzDdd.UseCase/PublicAPI.Shipped.txt create mode 100644 src/EzDdd.UseCase/PublicAPI.Unshipped.txt diff --git a/src/EzDdd.Common/EzDdd.Common.csproj b/src/EzDdd.Common/EzDdd.Common.csproj index 16cad85..43a018b 100644 --- a/src/EzDdd.Common/EzDdd.Common.csproj +++ b/src/EzDdd.Common/EzDdd.Common.csproj @@ -14,4 +14,18 @@ Part of the ezDDD.NET family of DDD tactical patterns libraries. ddd;domain-driven-design;utilities;bimap;json;converter;tactical-ddd;dotnet;csharp + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/src/EzDdd.Common/PublicAPI.Shipped.txt b/src/EzDdd.Common/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.Common/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.Common/PublicAPI.Unshipped.txt b/src/EzDdd.Common/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7f5493d --- /dev/null +++ b/src/EzDdd.Common/PublicAPI.Unshipped.txt @@ -0,0 +1,35 @@ +#nullable enable +EzDdd.Common.BiMap +EzDdd.Common.BiMap.Add(System.Collections.Generic.KeyValuePair item) -> void +EzDdd.Common.BiMap.Add(TKey key, TValue value) -> void +EzDdd.Common.BiMap.BiMap() -> void +EzDdd.Common.BiMap.BiMap(int capacity) -> void +EzDdd.Common.BiMap.Clear() -> void +EzDdd.Common.BiMap.Contains(System.Collections.Generic.KeyValuePair item) -> bool +EzDdd.Common.BiMap.ContainsKey(TKey key) -> bool +EzDdd.Common.BiMap.CopyTo(System.Collections.Generic.KeyValuePair[]! array, int arrayIndex) -> void +EzDdd.Common.BiMap.Count.get -> int +EzDdd.Common.BiMap.GetEnumerator() -> System.Collections.Generic.IEnumerator>! +EzDdd.Common.BiMap.GetKey(TValue value) -> TKey? +EzDdd.Common.BiMap.IsReadOnly.get -> bool +EzDdd.Common.BiMap.Keys.get -> System.Collections.Generic.ICollection! +EzDdd.Common.BiMap.PutAll(System.Collections.Generic.IDictionary! dictionary) -> void +EzDdd.Common.BiMap.PutIfAbsent(TKey key, TValue value) -> TValue? +EzDdd.Common.BiMap.Remove(System.Collections.Generic.KeyValuePair item) -> bool +EzDdd.Common.BiMap.Remove(TKey key) -> bool +EzDdd.Common.BiMap.Replace(TKey key, TValue oldValue, TValue newValue) -> bool +EzDdd.Common.BiMap.this[TKey key].get -> TValue +EzDdd.Common.BiMap.this[TKey key].set -> void +EzDdd.Common.BiMap.TryGetKey(TValue value, out TKey key) -> bool +EzDdd.Common.BiMap.TryGetValue(TKey key, out TValue value) -> bool +EzDdd.Common.BiMap.TryReplace(TKey key, TValue newValue, out TValue oldValue) -> bool +EzDdd.Common.BiMap.Values.get -> System.Collections.Generic.ICollection! +EzDdd.Common.Converter +EzDdd.Common.JsonUtil +static EzDdd.Common.JsonUtil.AsString(object! value) -> string! +static EzDdd.Common.JsonUtil.DeepCopy(T value) -> T? +static EzDdd.Common.JsonUtil.ReadAs(byte[]! bytes) -> T? +static EzDdd.Common.JsonUtil.ReadTree(byte[]! bytes) -> System.Text.Json.JsonDocument! +static EzDdd.Common.JsonUtil.ReadTree(string! json) -> System.Text.Json.JsonDocument! +static EzDdd.Common.JsonUtil.ReadValue(string! json) -> T? +static readonly EzDdd.Common.JsonUtil.Options -> System.Text.Json.JsonSerializerOptions! diff --git a/src/EzDdd.Core/EzDdd.Core.csproj b/src/EzDdd.Core/EzDdd.Core.csproj index 19b9ea4..6e8573a 100644 --- a/src/EzDdd.Core/EzDdd.Core.csproj +++ b/src/EzDdd.Core/EzDdd.Core.csproj @@ -34,4 +34,18 @@ Features: ddd;domain-driven-design;cqrs;event-sourcing;state-sourcing;clean-architecture;aggregate-root;tactical-ddd;outbox-pattern;dotnet;csharp + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/src/EzDdd.Core/PublicAPI.Shipped.txt b/src/EzDdd.Core/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.Core/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.Core/PublicAPI.Unshipped.txt b/src/EzDdd.Core/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.Core/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj b/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj index dbab318..469dd15 100644 --- a/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj +++ b/src/EzDdd.Cqrs/EzDdd.Cqrs.csproj @@ -23,4 +23,18 @@ Enables Command Query Responsibility Segregation with eventual consistency.ddd;domain-driven-design;cqrs;command;query;projection;read-model;archive;inquiry;dotnet;csharp + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/src/EzDdd.Cqrs/PublicAPI.Shipped.txt b/src/EzDdd.Cqrs/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.Cqrs/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.Cqrs/PublicAPI.Unshipped.txt b/src/EzDdd.Cqrs/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..4d4361b --- /dev/null +++ b/src/EzDdd.Cqrs/PublicAPI.Unshipped.txt @@ -0,0 +1,29 @@ +#nullable enable +EzDdd.Cqrs.Command.ICommand +EzDdd.Cqrs.Command.IInquiry +EzDdd.Cqrs.Command.IInquiry.QueryAsync(TInput input) -> System.Threading.Tasks.Task! +EzDdd.Cqrs.Command.IInquiryInput +EzDdd.Cqrs.CqrsOutput +EzDdd.Cqrs.CqrsOutput.CqrsOutput() -> void +EzDdd.Cqrs.CqrsOutput.ExitCode.get -> EzDdd.UseCase.Port.In.ExitCode +EzDdd.Cqrs.CqrsOutput.ExitCode.set -> void +EzDdd.Cqrs.CqrsOutput.Fail() -> T! +EzDdd.Cqrs.CqrsOutput.Id.get -> string! +EzDdd.Cqrs.CqrsOutput.Id.set -> void +EzDdd.Cqrs.CqrsOutput.Message.get -> string! +EzDdd.Cqrs.CqrsOutput.Message.set -> void +EzDdd.Cqrs.CqrsOutput.SetExitCode(EzDdd.UseCase.Port.In.ExitCode exitCode) -> T! +EzDdd.Cqrs.CqrsOutput.SetId(string! id) -> T! +EzDdd.Cqrs.CqrsOutput.SetMessage(string! message) -> T! +EzDdd.Cqrs.CqrsOutput.Succeed() -> T! +EzDdd.Cqrs.Query.IArchive +EzDdd.Cqrs.Query.IArchive.DeleteAsync(TData data) -> System.Threading.Tasks.Task! +EzDdd.Cqrs.Query.IArchive.FindByIdAsync(TId id) -> System.Threading.Tasks.Task! +EzDdd.Cqrs.Query.IArchive.SaveAsync(TData data) -> System.Threading.Tasks.Task! +EzDdd.Cqrs.Query.INotifier +EzDdd.Cqrs.Query.IProjection +EzDdd.Cqrs.Query.IProjection.QueryAsync(TInput input) -> System.Threading.Tasks.Task! +EzDdd.Cqrs.Query.IProjectionInput +EzDdd.Cqrs.Query.IProjector +EzDdd.Cqrs.Query.IQuery +static EzDdd.Cqrs.CqrsOutput.Create() -> T! diff --git a/src/EzDdd.Entity/EzDdd.Entity.csproj b/src/EzDdd.Entity/EzDdd.Entity.csproj index bc7ed20..34414a6 100644 --- a/src/EzDdd.Entity/EzDdd.Entity.csproj +++ b/src/EzDdd.Entity/EzDdd.Entity.csproj @@ -23,4 +23,18 @@ Designed for Domain-Driven Design with event sourcing and state sourcing.ddd;domain-driven-design;aggregate-root;entity;value-object;domain-events;event-sourcing;tactical-ddd;dotnet;csharp + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/src/EzDdd.Entity/PublicAPI.Shipped.txt b/src/EzDdd.Entity/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.Entity/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.Entity/PublicAPI.Unshipped.txt b/src/EzDdd.Entity/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..020738b --- /dev/null +++ b/src/EzDdd.Entity/PublicAPI.Unshipped.txt @@ -0,0 +1,49 @@ +#nullable enable +abstract EzDdd.Entity.EsAggregateRoot.GetCategory() -> string! +abstract EzDdd.Entity.EsAggregateRoot._When(TEvent! event) -> void +EzDdd.Entity.AggregateRoot +EzDdd.Entity.AggregateRoot.AggregateRoot() -> void +EzDdd.Entity.AggregateRoot.ClearDomainEvents() -> void +EzDdd.Entity.AggregateRoot.GetDomainEvents() -> System.Collections.Generic.IReadOnlyList! +EzDdd.Entity.AggregateRoot.GetDomainEventSize() -> int +EzDdd.Entity.AggregateRoot.GetLastDomainEvent() -> TEvent? +EzDdd.Entity.AggregateRoot.Id.get -> TId +EzDdd.Entity.AggregateRoot.Id.set -> void +EzDdd.Entity.AggregateRoot.IsDeleted.get -> bool +EzDdd.Entity.AggregateRoot.IsDeleted.set -> void +EzDdd.Entity.AggregateRoot.Version.get -> long +EzDdd.Entity.AggregateRoot.Version.set -> void +EzDdd.Entity.AggregateRoot._AddDomainEvent(TEvent! event) -> void +EzDdd.Entity.DomainEventTypeMapper +EzDdd.Entity.EsAggregateRoot +EzDdd.Entity.EsAggregateRoot.EsAggregateRoot() -> void +EzDdd.Entity.EsAggregateRoot.EsAggregateRoot(System.Collections.Generic.IEnumerable! events) -> void +EzDdd.Entity.EsAggregateRoot.GetStreamName() -> string! +EzDdd.Entity.IDomainEvent +EzDdd.Entity.IDomainEvent.Id.get -> System.Guid +EzDdd.Entity.IDomainEvent.Metadata.get -> System.Collections.Generic.IReadOnlyDictionary! +EzDdd.Entity.IDomainEvent.OccurredOn.get -> System.DateTimeOffset +EzDdd.Entity.IDomainEvent.Source.get -> string! +EzDdd.Entity.IDomainEventSource +EzDdd.Entity.IDomainEventSource.Apply(TEvent! event) -> void +EzDdd.Entity.IDomainEventSource.ClearDomainEvents() -> void +EzDdd.Entity.IDomainEventSource.GetDomainEvents() -> System.Collections.Generic.IReadOnlyList! +EzDdd.Entity.IDomainEventSource.GetDomainEventSize() -> int +EzDdd.Entity.IDomainEventSource.GetLastDomainEvent() -> TEvent? +EzDdd.Entity.IEntity +EzDdd.Entity.IEntity.Id.get -> TId +EzDdd.Entity.IInternalDomainEvent +EzDdd.Entity.IInternalDomainEvent.IConstructionEvent +EzDdd.Entity.IInternalDomainEvent.IDestructionEvent +EzDdd.Entity.IValueObject +override sealed EzDdd.Entity.EsAggregateRoot.Apply(TEvent! event) -> void +static EzDdd.Entity.DomainEventTypeMapper.Clear() -> void +static EzDdd.Entity.DomainEventTypeMapper.Contains(string! typeName) -> bool +static EzDdd.Entity.DomainEventTypeMapper.GetAllMappings() -> System.Collections.Generic.IReadOnlyDictionary! +static EzDdd.Entity.DomainEventTypeMapper.GetType(string! typeName) -> System.Type! +static EzDdd.Entity.DomainEventTypeMapper.GetTypeName(EzDdd.Entity.IInternalDomainEvent! event) -> string! +static EzDdd.Entity.DomainEventTypeMapper.GetTypeName(System.Type! eventType) -> string! +static EzDdd.Entity.DomainEventTypeMapper.Register(string! typeName) -> void +virtual EzDdd.Entity.AggregateRoot.Apply(TEvent! event) -> void +virtual EzDdd.Entity.EsAggregateRoot._EnsureInvariant() -> void +virtual EzDdd.Entity.EsAggregateRoot._ReplayEvents(System.Collections.Generic.IEnumerable! events) -> void diff --git a/src/EzDdd.UseCase/EzDdd.UseCase.csproj b/src/EzDdd.UseCase/EzDdd.UseCase.csproj index a26173d..53654a0 100644 --- a/src/EzDdd.UseCase/EzDdd.UseCase.csproj +++ b/src/EzDdd.UseCase/EzDdd.UseCase.csproj @@ -22,4 +22,18 @@ Implements Clean Architecture layers with tactical DDD patterns. ddd;domain-driven-design;clean-architecture;use-case;repository;event-sourcing;state-sourcing;outbox-pattern;dotnet;csharp + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/src/EzDdd.UseCase/PublicAPI.Shipped.txt b/src/EzDdd.UseCase/PublicAPI.Shipped.txt new file mode 100644 index 0000000..7dc5c58 --- /dev/null +++ b/src/EzDdd.UseCase/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/EzDdd.UseCase/PublicAPI.Unshipped.txt b/src/EzDdd.UseCase/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..ed03b0f --- /dev/null +++ b/src/EzDdd.UseCase/PublicAPI.Unshipped.txt @@ -0,0 +1,138 @@ +#nullable enable +abstract EzDdd.UseCase.Port.Out.OutboxMapper.ToData(TAggregate! aggregate) -> TData +abstract EzDdd.UseCase.Port.Out.OutboxMapper.ToDomain(TData data) -> TAggregate! +const EzDdd.UseCase.Exceptions.RepositorySaveException.OptimisticLockingFailure = "Optimistic locking failure" -> string! +EzDdd.UseCase.Exceptions.PostEventFailureException +EzDdd.UseCase.Exceptions.PostEventFailureException.PostEventFailureException() -> void +EzDdd.UseCase.Exceptions.PostEventFailureException.PostEventFailureException(string! message) -> void +EzDdd.UseCase.Exceptions.PostEventFailureException.PostEventFailureException(string! message, System.Exception! innerException) -> void +EzDdd.UseCase.Exceptions.RepositoryPeerSaveException +EzDdd.UseCase.Exceptions.RepositoryPeerSaveException.RepositoryPeerSaveException() -> void +EzDdd.UseCase.Exceptions.RepositoryPeerSaveException.RepositoryPeerSaveException(string! message) -> void +EzDdd.UseCase.Exceptions.RepositoryPeerSaveException.RepositoryPeerSaveException(string! message, System.Exception! innerException) -> void +EzDdd.UseCase.Exceptions.RepositoryPeerSaveException.RepositoryPeerSaveException(System.Exception! innerException) -> void +EzDdd.UseCase.Exceptions.RepositorySaveException +EzDdd.UseCase.Exceptions.RepositorySaveException.RepositorySaveException() -> void +EzDdd.UseCase.Exceptions.RepositorySaveException.RepositorySaveException(string! message) -> void +EzDdd.UseCase.Exceptions.RepositorySaveException.RepositorySaveException(string! message, System.Exception! innerException) -> void +EzDdd.UseCase.Exceptions.RepositorySaveException.RepositorySaveException(System.Exception! innerException) -> void +EzDdd.UseCase.Exceptions.UseCaseFailureException +EzDdd.UseCase.Exceptions.UseCaseFailureException.UseCaseFailureException() -> void +EzDdd.UseCase.Exceptions.UseCaseFailureException.UseCaseFailureException(string! message) -> void +EzDdd.UseCase.Exceptions.UseCaseFailureException.UseCaseFailureException(string! message, System.Exception! innerException) -> void +EzDdd.UseCase.Port.In.ExitCode +EzDdd.UseCase.Port.In.ExitCode.Failure = 1 -> EzDdd.UseCase.Port.In.ExitCode +EzDdd.UseCase.Port.In.ExitCode.Success = 0 -> EzDdd.UseCase.Port.In.ExitCode +EzDdd.UseCase.Port.In.ExitCodeExtensions +EzDdd.UseCase.Port.In.IInput +EzDdd.UseCase.Port.In.IInput.NullInput +EzDdd.UseCase.Port.In.IInput.OfNull() -> EzDdd.UseCase.Port.In.IInput.NullInput! +EzDdd.UseCase.Port.In.IOutput +EzDdd.UseCase.Port.In.IOutput.ExitCode.get -> EzDdd.UseCase.Port.In.ExitCode +EzDdd.UseCase.Port.In.IOutput.Fail() -> EzDdd.UseCase.Port.In.IOutput! +EzDdd.UseCase.Port.In.IOutput.Id.get -> string! +EzDdd.UseCase.Port.In.IOutput.Message.get -> string! +EzDdd.UseCase.Port.In.IOutput.SetExitCode(EzDdd.UseCase.Port.In.ExitCode exitCode) -> EzDdd.UseCase.Port.In.IOutput! +EzDdd.UseCase.Port.In.IOutput.SetId(string! id) -> EzDdd.UseCase.Port.In.IOutput! +EzDdd.UseCase.Port.In.IOutput.SetMessage(string! message) -> EzDdd.UseCase.Port.In.IOutput! +EzDdd.UseCase.Port.In.IOutput.Succeed() -> EzDdd.UseCase.Port.In.IOutput! +EzDdd.UseCase.Port.In.IReactor +EzDdd.UseCase.Port.In.IReactor.ExecuteAsync(TInput input) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.In.IReconciler +EzDdd.UseCase.Port.In.IReconciler.ReconcileAsync(TContext context) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.In.IUseCase +EzDdd.UseCase.Port.In.IUseCase.ExecuteAsync(TInput input) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.In.IVersionedInput +EzDdd.UseCase.Port.In.IVersionedInput.Version.get -> long +EzDdd.UseCase.Port.In.IVersionedInput.Version.set -> void +EzDdd.UseCase.Port.In.NullContext +EzDdd.UseCase.Port.InOut.DomainEventData +EzDdd.UseCase.Port.InOut.DomainEventData.ContentType.get -> string! +EzDdd.UseCase.Port.InOut.DomainEventData.ContentType.init -> void +EzDdd.UseCase.Port.InOut.DomainEventData.DomainEventData(System.Guid Id, string! EventType, string! ContentType, byte[]! EventBody, byte[]! UserMetadata) -> void +EzDdd.UseCase.Port.InOut.DomainEventData.EventBody.get -> byte[]! +EzDdd.UseCase.Port.InOut.DomainEventData.EventBody.init -> void +EzDdd.UseCase.Port.InOut.DomainEventData.EventType.get -> string! +EzDdd.UseCase.Port.InOut.DomainEventData.EventType.init -> void +EzDdd.UseCase.Port.InOut.DomainEventData.Id.get -> System.Guid +EzDdd.UseCase.Port.InOut.DomainEventData.Id.init -> void +EzDdd.UseCase.Port.InOut.DomainEventData.UserMetadata.get -> byte[]! +EzDdd.UseCase.Port.InOut.DomainEventData.UserMetadata.init -> void +EzDdd.UseCase.Port.InOut.DomainEventDataBuilder +EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.Build() -> EzDdd.UseCase.Port.InOut.DomainEventData! +EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.EventId(System.Guid id) -> EzDdd.UseCase.Port.InOut.DomainEventDataBuilder! +EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.MetadataAsBytes(byte[]! metadata) -> EzDdd.UseCase.Port.InOut.DomainEventDataBuilder! +EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.MetadataAsJson(T metadata) -> EzDdd.UseCase.Port.InOut.DomainEventDataBuilder! +EzDdd.UseCase.Port.InOut.DomainEventMapper +EzDdd.UseCase.Port.InOut.IExternalDomainEvent +EzDdd.UseCase.Port.InOut.InternalDomainEventDto +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.BoundedContext.get -> string! +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.BoundedContext.set -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.EventSimpleName.get -> string! +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.EventSimpleName.set -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.Id.get -> System.Guid +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.Id.set -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.InternalDomainEventDto() -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.JsonEvent.get -> string! +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.JsonEvent.set -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.Metadata.get -> System.Collections.Generic.IDictionary! +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.Metadata.set -> void +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.OccurredOn.get -> System.DateTimeOffset +EzDdd.UseCase.Port.InOut.InternalDomainEventDto.OccurredOn.set -> void +EzDdd.UseCase.Port.InOut.Messaging.IExternalDomainEventPublisher +EzDdd.UseCase.Port.InOut.Messaging.IExternalDomainEventPublisher.PublishAsync(TEvent event) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.EsRepository +EzDdd.UseCase.Port.Out.EsRepository.DeleteAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.EsRepository.EsRepository(EzDdd.UseCase.Port.Out.IRepositoryPeer!, TId>! peer) -> void +EzDdd.UseCase.Port.Out.EsRepository.FindByIdAsync(TId id) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.EsRepository.SaveAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.EventStoreData +EzDdd.UseCase.Port.Out.EventStoreData.Events.get -> System.Collections.Generic.IReadOnlyList! +EzDdd.UseCase.Port.Out.EventStoreData.Events.set -> void +EzDdd.UseCase.Port.Out.EventStoreData.EventStoreData() -> void +EzDdd.UseCase.Port.Out.EventStoreData.GetOptimisticLockVersion() -> long +EzDdd.UseCase.Port.Out.EventStoreData.Id.get -> TId +EzDdd.UseCase.Port.Out.EventStoreData.Id.set -> void +EzDdd.UseCase.Port.Out.EventStoreData.StreamName.get -> string! +EzDdd.UseCase.Port.Out.EventStoreData.StreamName.set -> void +EzDdd.UseCase.Port.Out.EventStoreData.Version.get -> long +EzDdd.UseCase.Port.Out.EventStoreData.Version.set -> void +EzDdd.UseCase.Port.Out.EventStoreMapper +EzDdd.UseCase.Port.Out.IOutboxData +EzDdd.UseCase.Port.Out.IRepository +EzDdd.UseCase.Port.Out.IRepository.DeleteAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IRepository.FindByIdAsync(TId id) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IRepository.SaveAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IRepositoryPeer +EzDdd.UseCase.Port.Out.IRepositoryPeer.DeleteAsync(TData data) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IRepositoryPeer.FindByIdAsync(TId id) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IRepositoryPeer.SaveAsync(TData data) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.IStoreData +EzDdd.UseCase.Port.Out.IStoreData.Events.get -> System.Collections.Generic.IReadOnlyList! +EzDdd.UseCase.Port.Out.IStoreData.Events.set -> void +EzDdd.UseCase.Port.Out.IStoreData.GetOptimisticLockVersion() -> long +EzDdd.UseCase.Port.Out.IStoreData.Id.get -> TId +EzDdd.UseCase.Port.Out.IStoreData.Id.set -> void +EzDdd.UseCase.Port.Out.IStoreData.StreamName.get -> string! +EzDdd.UseCase.Port.Out.IStoreData.StreamName.set -> void +EzDdd.UseCase.Port.Out.IStoreData.Version.get -> long +EzDdd.UseCase.Port.Out.IStoreData.Version.set -> void +EzDdd.UseCase.Port.Out.OutboxMapper +EzDdd.UseCase.Port.Out.OutboxMapper.OutboxMapper() -> void +EzDdd.UseCase.Port.Out.OutboxRepository +EzDdd.UseCase.Port.Out.OutboxRepository.DeleteAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.OutboxRepository.FindByIdAsync(TId id) -> System.Threading.Tasks.Task! +EzDdd.UseCase.Port.Out.OutboxRepository.OutboxRepository(EzDdd.UseCase.Port.Out.IRepositoryPeer! peer, EzDdd.UseCase.Port.Out.OutboxMapper! mapper) -> void +EzDdd.UseCase.Port.Out.OutboxRepository.SaveAsync(TAggregate! aggregate) -> System.Threading.Tasks.Task! +override EzDdd.UseCase.Port.InOut.DomainEventData.GetHashCode() -> int +static EzDdd.UseCase.Port.In.ExitCodeExtensions.Code(this EzDdd.UseCase.Port.In.ExitCode exitCode) -> int +static EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.Binary(string! eventType, byte[]! payload) -> EzDdd.UseCase.Port.InOut.DomainEventDataBuilder! +static EzDdd.UseCase.Port.InOut.DomainEventDataBuilder.Json(string! eventType, T payload) -> EzDdd.UseCase.Port.InOut.DomainEventDataBuilder! +static EzDdd.UseCase.Port.InOut.DomainEventMapper.ToData(EzDdd.Entity.IInternalDomainEvent! event) -> EzDdd.UseCase.Port.InOut.DomainEventData! +static EzDdd.UseCase.Port.InOut.DomainEventMapper.ToData(System.Collections.Generic.IEnumerable! events) -> System.Collections.Generic.IReadOnlyList! +static EzDdd.UseCase.Port.InOut.DomainEventMapper.ToDomain(EzDdd.UseCase.Port.InOut.DomainEventData! data) -> T +static EzDdd.UseCase.Port.InOut.DomainEventMapper.ToDomain(System.Collections.Generic.IEnumerable! datas) -> System.Collections.Generic.IReadOnlyList! +static EzDdd.UseCase.Port.Out.EventStoreMapper.ToData(EzDdd.Entity.EsAggregateRoot! aggregate) -> EzDdd.UseCase.Port.Out.EventStoreData! +static EzDdd.UseCase.Port.Out.EventStoreMapper.ToDomain(EzDdd.UseCase.Port.Out.EventStoreData! data) -> T! +static readonly EzDdd.UseCase.Port.In.NullContext.Instance -> EzDdd.UseCase.Port.In.NullContext! +virtual EzDdd.UseCase.Port.InOut.DomainEventData.Equals(EzDdd.UseCase.Port.InOut.DomainEventData? other) -> bool From 6584ce9259951571915ab2af265abb82a781dd2f Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 22:29:08 +0800 Subject: [PATCH 09/18] test(integration): make concurrent mapper registration parallel-safe The concurrent-registration test bound a shared event type to 100 random names with swallowed exceptions; whichever registration won the race could poison the process-global DomainEventTypeMapper and cascade-fail ~24 tests in this assembly (order-dependent, never observed only by scheduling luck). Rewrite it around dedicated event types with fixed, idempotent mappings and real bidirectional assertions, and serialize the three mapper-touching test classes into the DomainEventTypeMapper collection. --- .../ConcurrentOperationsTests.cs | 100 ++++++++++++++---- .../CqrsFlowWithMetadataTests.cs | 1 + .../EventSourcingMetadataTests.cs | 1 + 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs index 93aa1af..c031043 100644 --- a/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs +++ b/tests/EzDdd.Integration.Tests/ConcurrentOperationsTests.cs @@ -29,6 +29,7 @@ namespace EzDdd.Integration.Tests; /// in concurrent scenarios. /// /// +[Collection("DomainEventTypeMapper")] public sealed class ConcurrentOperationsTests { #region DomainEventTypeMapper Concurrent Registration Tests @@ -39,7 +40,9 @@ public async Task DomainEventTypeMapper_ConcurrentRegistration_ShouldBeThreadSaf const int threadCount = 20; const int registrationsPerThread = 5; - // Act: Register event types concurrently from multiple threads + // Act: Each thread repeatedly registers its dedicated event type under a fixed, + // consistent name (idempotent path). Consistent type-name mappings must never throw, + // so no exception is caught - any exception fails the test. List tasks = []; for (int t = 0; t < threadCount; t++) { @@ -49,31 +52,92 @@ public async Task DomainEventTypeMapper_ConcurrentRegistration_ShouldBeThreadSaf { for (int i = 0; i < registrationsPerThread; i++) { - // Each thread registers unique event types - string eventTypeName = $"ConcurrentEvent_T{threadId:D2}_E{i:D2}"; - - // Create a unique event type dynamically - // For testing, we'll just register the same type multiple times - // (The mapper should handle duplicate registrations gracefully) - try - { - DomainEventTypeMapper.Register(eventTypeName); - } - catch - { - // Duplicate registration may throw - that's acceptable - // We're testing thread safety, not duplicate handling - } + _RegisterConcurrentEvent(threadId % 5); } }) ); } - // Assert: All tasks should complete without deadlock or race conditions + // Assert: All tasks complete without deadlock, race condition, or exception await Task.WhenAll(tasks); - Assert.Equal(threadCount, tasks.Count); + + // Assert: Bidirectional lookups resolve correctly after concurrent registration + Assert.Equal("ConcurrentEvent0", DomainEventTypeMapper.GetTypeName(typeof(ConcurrentEvent0))); + Assert.Equal("ConcurrentEvent1", DomainEventTypeMapper.GetTypeName(typeof(ConcurrentEvent1))); + Assert.Equal("ConcurrentEvent2", DomainEventTypeMapper.GetTypeName(typeof(ConcurrentEvent2))); + Assert.Equal("ConcurrentEvent3", DomainEventTypeMapper.GetTypeName(typeof(ConcurrentEvent3))); + Assert.Equal("ConcurrentEvent4", DomainEventTypeMapper.GetTypeName(typeof(ConcurrentEvent4))); + Assert.Equal(typeof(ConcurrentEvent0), DomainEventTypeMapper.GetType("ConcurrentEvent0")); + Assert.Equal(typeof(ConcurrentEvent1), DomainEventTypeMapper.GetType("ConcurrentEvent1")); + Assert.Equal(typeof(ConcurrentEvent2), DomainEventTypeMapper.GetType("ConcurrentEvent2")); + Assert.Equal(typeof(ConcurrentEvent3), DomainEventTypeMapper.GetType("ConcurrentEvent3")); + Assert.Equal(typeof(ConcurrentEvent4), DomainEventTypeMapper.GetType("ConcurrentEvent4")); } + /// + /// Registers the dedicated concurrent-test event type assigned to the given slot, + /// always using the same fixed type name (idempotent, consistent mapping). + /// + private static void _RegisterConcurrentEvent(int eventNumber) + { + switch (eventNumber) + { + case 0: + DomainEventTypeMapper.Register("ConcurrentEvent0"); + break; + case 1: + DomainEventTypeMapper.Register("ConcurrentEvent1"); + break; + case 2: + DomainEventTypeMapper.Register("ConcurrentEvent2"); + break; + case 3: + DomainEventTypeMapper.Register("ConcurrentEvent3"); + break; + case 4: + DomainEventTypeMapper.Register("ConcurrentEvent4"); + break; + } + } + + // Dedicated event types for the concurrent registration test. + // Never reuse shared TestDomain types here: binding a shared type to a random name + // would poison the process-global mapper for every other test in this assembly. + private sealed record ConcurrentEvent0( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private sealed record ConcurrentEvent1( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private sealed record ConcurrentEvent2( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private sealed record ConcurrentEvent3( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + + private sealed record ConcurrentEvent4( + Guid Id, + DateTimeOffset OccurredOn, + string Source, + IReadOnlyDictionary Metadata + ) : IInternalDomainEvent; + #endregion #region Mixed Concurrent Operations Tests diff --git a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs index 953042e..e382e0a 100644 --- a/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/CqrsFlowWithMetadataTests.cs @@ -37,6 +37,7 @@ namespace EzDdd.Integration.Tests; /// introduced in Java ezddd 4.1.0 for idempotency and distributed tracing support. /// /// +[Collection("DomainEventTypeMapper")] public sealed class CqrsFlowWithMetadataTests { #region Event Replay with Metadata Tests diff --git a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs index c0a7f81..a547dae 100644 --- a/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs +++ b/tests/EzDdd.Integration.Tests/EventSourcingMetadataTests.cs @@ -32,6 +32,7 @@ namespace EzDdd.Integration.Tests; /// Java 4.1.0 Feature: Validates IDomainEvent.Metadata in event sourcing context. /// /// +[Collection("DomainEventTypeMapper")] public sealed class EventSourcingMetadataTests { #region Version Control and Metadata Tests From bedc898ec225e3cd641d07ec712ca3bf4766d62d Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 23:06:39 +0800 Subject: [PATCH 10/18] ci: add build/test matrix, NuGet publish, and Dependabot workflows Adapt the uContract.NET workflows: CSharpier gate plus build and test on ubuntu/windows for every branch; release-triggered publish that validates the tag against the Directory.Build.props version, packs the solution, asserts exactly five packages, and pushes through the nuget environment gate; weekly Dependabot for NuGet and Actions. --- .github/dependabot.yml | 25 ++++++++ .github/workflows/build-and-test.yml | 38 ++++++++++++ .github/workflows/publish.yml | 86 ++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build-and-test.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..aabeb99 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + # NuGet dependencies in src/, tests/, and Directory.Build.props + - package-ecosystem: "nuget" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + include: "scope" + labels: + - "dependencies" + + # GitHub Actions used in .github/workflows/ + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(ci)" + include: "scope" + labels: + - "dependencies" diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..8d12d0d --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,38 @@ +name: Build and Test + +on: + push: + branches: ['**'] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: actions/checkout@v7 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Restore dotnet tools + run: dotnet tool restore + + - name: Check formatting (CSharpier) + run: dotnet csharpier check . + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore -p:ContinuousIntegrationBuild=true + + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a746234 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,86 @@ +name: Publish to NuGet + +on: + release: + types: [published] + +jobs: + build-and-validate: + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - uses: actions/checkout@v7 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Validate version + run: | + TAG_VERSION="${GITHUB_REF_NAME#v}" + PROPS_VERSION=$(grep -oP '\K[^<]+' Directory.Build.props) + echo "Tag version: $TAG_VERSION" + echo "Directory.Build.props version: $PROPS_VERSION" + if [ "$TAG_VERSION" != "$PROPS_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) does not match Directory.Build.props version ($PROPS_VERSION)" + exit 1 + fi + + - name: Restore + run: dotnet restore + + - name: Test + run: dotnet test --verbosity normal + + - name: Pack + # Solution-level pack produces all five packages; test projects opt out + # via IsPackable=false. + run: dotnet pack ezDDD.sln -c Release --no-restore -o ./nupkg + + - name: Verify package count + run: | + COUNT=$(ls ./nupkg/*.nupkg | wc -l) + echo "Packed $COUNT packages" + if [ "$COUNT" != "5" ]; then + echo "::error::Expected 5 packages (Common, Entity, UseCase, Cqrs, Core), got $COUNT" + exit 1 + fi + + - name: Upload package artifact + uses: actions/upload-artifact@v7 + with: + name: nupkg + path: ./nupkg/ + retention-days: 7 + + publish: + runs-on: ubuntu-latest + needs: build-and-validate + environment: nuget + + permissions: + contents: write + + steps: + - name: Download package artifact + uses: actions/download-artifact@v8 + with: + name: nupkg + path: ./nupkg/ + + - name: Push to NuGet + run: > + dotnet nuget push ./nupkg/*.nupkg + --source https://api.nuget.org/v3/index.json + --api-key ${{ secrets.NUGET_API_KEY }} + --skip-duplicate + + - name: Upload to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release upload "${{ github.event.release.tag_name }}" ./nupkg/*.nupkg --clobber From 9d731db9e60e37314c94771efd918ceabc5d2506 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 23:21:19 +0800 Subject: [PATCH 11/18] docs: add community health files Adapt CONTRIBUTING, SECURITY, CODE_OF_CONDUCT (Contributor Covenant 2.1), and issue/PR templates from uContract.NET. The security policy scope is rewritten for this library's actual attack surface: the event/aggregate reconstruction pipeline (System.Text.Json paths, registration-map type resolution, reflection-based replay constructor invocation). --- .github/ISSUE_TEMPLATE/bug_report.md | 30 +++++++ .github/ISSUE_TEMPLATE/feature_request.md | 23 +++++ .github/pull_request_template.md | 17 ++++ CODE_OF_CONDUCT.md | 83 +++++++++++++++++ CONTRIBUTING.md | 104 ++++++++++++++++++++++ SECURITY.md | 24 +++++ 6 files changed, 281 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..8d30be9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug Report +about: Report a bug in ezDDD.NET +title: '' +labels: bug +assignees: '' +--- + +## Description + +A clear description of the bug. + +## Steps to Reproduce + +1. ... +2. ... + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. Include exception messages and stack traces if applicable. + +## Environment + +- .NET version: +- ezDDD version: +- OS: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c65ff86 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: Suggest a new feature or improvement +title: '' +labels: enhancement +assignees: '' +--- + +## Problem + +What problem does this feature solve? + +## Proposed Solution + +Describe the desired behavior. + +## Alternatives Considered + +Any alternative approaches you've considered. + +## Additional Context + +Any other context, code samples, or references. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..ca54a73 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +## Summary + +Brief description of the changes. + +## Changes + +- ... + +## Testing + +- [ ] All existing tests pass (`dotnet test`) +- [ ] New tests added for new functionality +- [ ] No mixed structural/behavioral changes + +## Related Issues + +Closes # diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8d4e846 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at cwouyang+ezddd@pm.me. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..325fb6c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,104 @@ +# Contributing to ezDDD.NET + +## Setup + +```bash +git clone https://github.com/cwouyang/ezDDD.NET.git +cd ezDDD.NET +dotnet test +``` + +## Development Setup + +Rider configuration (one-time, per-developer): + +1. Install the CSharpier plugin from the Rider marketplace; restart Rider. +2. Settings -> Tools -> CSharpier: enable "Run on save" and "Reformat code on save". Pin the plugin version to match the CLI version in `.config/dotnet-tools.json` (currently `1.2.6`). +3. Settings -> Keymap: rebind `Ctrl+Alt+L` (Reformat Code) to the CSharpier Format action so the IDE shortcut matches CI. +4. Settings -> Editor -> Code Cleanup: create an `ezDDD-safe` profile containing only "CSharpier Format". Do not run Full Cleanup on this codebase. +5. Settings -> Editor -> Code Style -> C#: enable "Load from EditorConfig" so `.editorconfig` governs per-project style. +6. Run once per clone: + + ```bash + git config blame.ignoreRevsFile .git-blame-ignore-revs + ``` + +To verify, introduce a whitespace error in any `.cs` file and trigger Rider Reformat. The result must be byte-identical to `dotnet csharpier format` output; if it differs, re-check the plugin version pin and the CSharpier-only cleanup profile. + +## Code Quality Tools + +### Daily Verification + +Run before every commit: + +```bash +dotnet tool restore +dotnet csharpier check . +dotnet build -p:ContinuousIntegrationBuild=true +dotnet test --no-build --verbosity normal +``` + +### Reproducing CI Locally + +The GitHub Actions workflow runs the same steps in this order: + +```bash +dotnet tool restore +dotnet csharpier check . +dotnet build -p:ContinuousIntegrationBuild=true --no-restore +dotnet test --no-build --verbosity normal +``` + +If all four succeed locally, CI will succeed. + +### Upgrading CSharpier + +Land each step as its own commit so the formatting-only commit can be added to `.git-blame-ignore-revs` cleanly: + +1. Bump the version in `.config/dotnet-tools.json`. + Commit: `chore(build): bump CSharpier to ` +2. `dotnet tool restore && dotnet csharpier format .` + Commit: `style: apply CSharpier reformat` +3. Append the SHA of the reformat commit to `.git-blame-ignore-revs`. + Commit: `chore: add CSharpier reformat to blame-ignore list` + +### Upgrading Roslynator.Analyzers + +1. Bump the `Version` attribute on the `Roslynator.Analyzers` `PackageReference` in `Directory.Build.props`. + Commit: `chore(build): bump Roslynator.Analyzers to ` +2. Address any new diagnostics, one logical fix group per commit. + Commit: `refactor: fix RCSxxxx ` + +The same two-step procedure applies to `Meziantou.Analyzer` (also referenced in `Directory.Build.props`). + +## Pull Requests + +- One concern per PR -- do not mix refactoring with behavioral changes +- Tests are required for all changes +- For API changes, open an issue first + +## Releasing + +1. Update `` in `Directory.Build.props` -- the single version source for all five packages (ezDDD.Common, ezDDD.Entity, ezDDD.UseCase, ezDDD.Cqrs, ezDDD.Core) +2. Promote the public API baselines -- each of the five `src/` projects tracks its own baseline pair: + - In every project (`src/EzDdd.Common`, `src/EzDdd.Entity`, `src/EzDdd.UseCase`, `src/EzDdd.Cqrs`, `src/EzDdd.Core`), move every entry from `PublicAPI.Unshipped.txt` to `PublicAPI.Shipped.txt` + - Keep the `#nullable enable` header in both files; Unshipped retains only the header after promotion + - This makes the five `PublicAPI.Shipped.txt` files the canonical record of the APIs committed at `v{version}` +3. Update `CHANGELOG.md` -- move Unreleased items under the new version heading +4. Commit: `release: prepare v{version}` (version bump, baseline promotions, and CHANGELOG in a single commit) +5. Push to master +6. On GitHub, create a Release with tag `v{version}` targeting master -- the tag must match `` in `Directory.Build.props` +7. The publish workflow runs automatically: + - Validates the tag against the `Directory.Build.props` version + - Runs tests and packs all five NuGet packages (fails if the count is not exactly 5) + - Waits for manual approval (check the Actions tab) +8. Approve the deployment in the Actions tab +9. All five packages are published to NuGet.org and attached to the GitHub Release + +### One-Time Setup + +Before the first release, configure the GitHub Environment: + +1. GitHub repo > Settings > Environments > create `nuget` +2. Enable "Required reviewers" > add yourself +3. Add `NUGET_API_KEY` as an environment secret (generate at https://www.nuget.org/account/apikeys) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2a8f813 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security vulnerabilities via [GitHub Security Advisory](https://github.com/cwouyang/ezDDD.NET/security/advisories/new). Do not open a public issue. + +## Scope + +ezDDD.NET is a tactical DDD / CQRS library with zero third-party runtime dependencies. Its realistic attack surface is the path by which domain events and aggregates are rebuilt from persisted data: bytes read back from an event store or outbox table should be treated as untrusted input, and the library owns part of that reconstruction pipeline. The following list distinguishes what the maintainer will treat as a security issue from what is out of scope. Edge cases not listed here are welcome as a question on the advisory channel. + +### In scope + +- **JSON serialization and deserialization performed by the library itself** — `JsonUtil` (including `DeepCopy`) in `EzDdd.Common`, and the event persistence round-trip in `DomainEventMapper` / `DomainEventDataBuilder` in `EzDdd.UseCase`, all built on `System.Text.Json`. Crafted JSON payloads or poisoned event store contents that cause the library's own serialize / deserialize code path to behave unsafely (e.g., unexpected exception leakage, resource exhaustion amplified by library-internal choices) are in scope. +- **Event type resolution in `DomainEventTypeMapper`** (`EzDdd.Entity`). The mapper deliberately resolves stored type-name strings only through an explicit registration map — never via `Type.GetType` on attacker-controlled strings. Any way a crafted stored type name could cause the library to resolve or instantiate a type outside that registration map is in scope. +- **Reflection-based aggregate reconstruction in `EsRepository`** (`EzDdd.UseCase`), which locates a public replay constructor via cached `ConstructorInfo` and invokes it with the loaded event stream. If crafted event store data could cause the library to invoke members other than the documented `TAggregate(IEnumerable)` constructor, or to bypass the aggregate's replay path, that is in scope. +- **Exception message content generated by the library**, if that content unexpectedly discloses information beyond type names and the data the caller or the event store supplied. + +### Out of scope + +- **User-supplied domain logic.** `When()`, `EnsureInvariant()`, and use case implementations are the caller's code; the library replays events through them by design. Side effects, slow logic, or resource usage inside user domain code — including during event replay — are out of scope. +- **User implementations of the persistence and query SPIs** (`IRepositoryPeer`, `IArchive`, projector/notifier infrastructure, etc.). Securing the event store, outbox table, and message transport — including authentication, integrity, and access control of stored events — is the integrating application's responsibility. +- **Documented, expected exceptions.** `InvalidOperationException`, `ArgumentException`, `RepositorySaveException`, and uContract `*ViolationException` types thrown on invalid input, unregistered event types, or invariant violations (see [ADR-0011](docs/adr/0011-event-replay-invariant-checking.md)) are the library's correctness signal and not a fault. +- **Denial of service via caller-supplied data volume.** Serializing huge object graphs, deep-copying large aggregates, or replaying very long event streams costs what it costs; the library's pipeline is a thin wrapper over `System.Text.Json` and constructor invocation. +- **Vulnerabilities in uContract.NET itself** (the only ecosystem dependency, see [ADR-0004](docs/adr/0004-zero-third-party-dependency-principle.md)) — please report those upstream via the [uContract.NET security advisory channel](https://github.com/cwouyang/uContract.NET/security/advisories/new). From a67ef0fa34f0226bbb196dff84bd2459e237e993 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sat, 4 Jul 2026 23:30:41 +0800 Subject: [PATCH 12/18] build: add package icon and embed untracked sources Add a 128x128 flat hexagon icon in the uContract.NET family palette and pack it into all five packages via Directory.Build.props. Enable EmbedUntrackedSources so generated sources are embedded alongside the in-box SDK Source Link (sourcelink.json verified for all five projects). Trim/AOT compatibility is deliberately not claimed: the replay pipeline relies on reflection and reflection-based System.Text.Json. --- .gitattributes | 1 + Directory.Build.props | 3 +++ icon.png | Bin 0 -> 6137 bytes 3 files changed, 4 insertions(+) create mode 100644 icon.png diff --git a/.gitattributes b/.gitattributes index 8ed446d..e0df437 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ # Ensures cross-platform consistency between Windows and Linux CI. * text=auto eol=lf *.sln text eol=crlf +*.png binary diff --git a/Directory.Build.props b/Directory.Build.props index 4611644..9f18889 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -27,6 +27,7 @@ https://github.com/cwouyang/ezDDD.NET git README.md + icon.png See https://github.com/cwouyang/ezDDD.NET/blob/master/CHANGELOG.md for release notes. @@ -35,6 +36,7 @@ embedded + true @@ -51,5 +53,6 @@ + diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..96684fe34e7bc738686ee1d44f123e4647060e75 GIT binary patch literal 6137 zcmV#VQ9l$)4KiaeVdB9ru0C9;gC_;ovv;d;{ZQ#a*&&{ynHRtG5H#3n! znS+QKVz89uu~U0{XL4G1_MM8c1UAY6n`9u82G;!$cKvA-mz!Jm@(;j1DAowScZ&l| zUyjosx3Lx;Dg>ps0L^S6Nq*B=>&~HgZIZpdGyn?~0Rk8dvKUGY*lud}j$h7w1+5^o z$$&0@l-L3=0ZBAA|D^$$?r%Z+ayy;MQGWy~#I8@)mFpMh zEk05m|3&bS57oGup4Shfm)Ds9R)&maHs=ckhu|5r|T zX;7t@{vkoEefAY@!ABm8V+Ob^Tx{76fcADJVYKI`TFI_%0DCNLo{v;NqK8Ou3{ zbIKQ`7yyJB&5Y&^F~xLS5b>AyO&SIiw9DkwOj0U+J=)qdnoAsK9R$U{I5CTDq?=Ny zb161vHhX=Q8Th>wh!kgfG+FT_V;BAYT}VQkc&AKJFXR3mml4biFi#q_s9JaM-Yhd8ZnNLj$ibJhsH3(F30>5UKE*TSj5A2tHZd^DJL1&npx+YK2Hv=oMoJQ9G zFo7TGu?=-gRIKh@?4|UGO8X85Qi?NOnw;uSja#sOw&@d;QCb-Cg|sR$dt&~W?CZrR zg38Yih71A=zr~JgqT7r+H2jD1K)AvXbJs>%%pch`jbaWfy54^{X`$EaQM~>in);`n zS5Do+@CJaGxg_+sk3T(!oW!Tt?Utg?Xw&{YNLG+ySoXDs|6l^b(#Jw7WSSTIKHowV zi;Ec?(P`hop`g<~#T%vz|G@-=!iOj^C{&u)qV>&W*Q_2jY2QI1N+9$xtH%P(+4=KX zFJpOW>0>G^nwNC@7<2>J{I=1o`FS7`-h=QTga;vf2*ZmgAyg=7U$Vs?lj^@x#p;h` zuYY89`wBrHOLzIggAhK1@FGgtC!m4{e#^n*&)zW213yEoYr&qx0+9tcpifY(*dKVN zBOX6B=o6S8LDBmJs^EcNmgc4G>UyxJBjLYQpP}r|qSA zfy!gzN!ilJ`n2#O3_qeo^sxetx@O}7H1qWVzpt#ip5>YrvhX5=A5kj$7!@?_Te$v0 ziZ|TN={=#7JSD17BpxS7uFM1-?EAlFHv6|b_fR3@FA=M+Y+Igk~=(KgbUOp6k?8oFU`k1h}>O|F_0Dy zHO#Opd%S#qbzBvSKBk2qq5X`|$3BcrtBYy{s8HJ9xM1B4s;cp8obC)w`$e##)qDDU zOdjXp1Mh9XsWWQuj!AJOVhVjJjaUDg#_u-u;@8i1vG;{3Dti9Dj6T z*fTeB$M+2Qc=o0K!sRv$pPXuoGkqWKc=Y5oIsOES04fB{Yus7SzEeB)hA?Sf(nZL) zY)t!QADf8F|7jwQ92a}b<6qyA!Oai7i4~9TL`{|2YxX;OJX~_-c&zx+I}Z6;HoiK5 zQ!m?um?KsPg{6;mchADSdAeBiF;$3qzqa%ZWW__HdF6zkh$|#pkHt4HJ<15**K9Oe z-gEuz-edFnQ4?bL?&Zhe`_KTGzxV=|sL`=K^%Hte@~+4BZ6LTM;;TA0$p zmk{1WG3jIKaMM1ad68Y)%$eR$>iu$`edE^&tFL`0K5|Nf5q4nG364!h6CEqNx5*qU z@5^zYTQDA1esU5zdwmltn9n4@QUu!~vma_xX2X74?mQVlvK4fs6AXt?coW5>kEue? zyx7d^3nzyaL3H(bSbpJ=IPIi5bK^zWhEGnw=GRmB=UZRJSxaBU72n^2m$#+&YNxFT zz6P#3Z!%6erV9OOZ3HQH%FDADZr)}zJgv>&`~BjqZIg3a?|LKqDixkSX4fXG-obs#-ia}_!B3L! zlmR23^Y1HOH=i}3(J`+T7(qGMin)q2@89*`v!4k&Ewm3!`@u5#k;%Uvo}_)@_$v{J z`0r1Dpb2q@0wJhC`(@-_edin4(dij~FXn#y7zZ!Dp2E+b?7)(R6U_0sy1)nLG~ny2 zwwWBhr(a`t_x`K6h&vIaG8#93{wOroS;p^+#mEh!pFYuvTmNf2#*eYX5Wcw#6A)4? z?tAH50Gb!}ViW36NuDzKF`#*cSMQe`zBXaujQ7?DLZ=`Y6CeNl%LDl7;~ge2dwpjH z+dEur+m(Ue?cvGidk-iUO*VX$L!&!edG(fRon3;W-4?@t=?SDF4{JIa@=UV z3GTA`ACD2PTRa6-F*1Wfj+bLn8OE%s3G*5G)xNrF8)NHjGf3pfd?DHvVaFa1=Y4Q2 z{^jh6d(C~$MTlFtTT3j=fZa}S%FZGn^J{q&Y%32=jSX&uE#3cT;*i7H?IO{ zDKdqflcrbW_A8DFI6K)wX3q34z2i0f--aGc7~>dM6FMX<^ap&3H%`^M`>P_ouD{%F5r+6( z@xc;&$%{S*LRz=E4@+)&71dQi3^3&F$-6T7ye-=^sER3byrh&n_PF@sCnn)D9~`%z z-EW(ToX7+*Ea7(jZ~Z0$_a_NE2v zZl$WmuLsSa!q)&q5K`3>AxYqN?3AjkUk@ZYWn2tf{+c#*0ukI$8k*DQoN%Q(DDGYU zmdfDiC)Jy>vK*81`)2W1MfmE9EqLzF{U)`PU{pGQ`6eXX0U_VzYejTTP!I}_GdO%7 zo?Y^!Ba!&FPIql4JE0_Z4dddBpn7~f^+KQqx-$ngkrsoyQDx8pw^eABpyzjnH&fz;9M z1^XTkd-_6L36`}_@p?=91FLpDJm-lZ_zSNycnF0cS@LAHFn=?&VEq*!>sP@#zlDcr zqX_rG$2;(u4~^Sv&P78-0j0z%TD{`Y9j5ggly4c1txP3QO2=RP#6b;sh&_ca8)0DJ$-mNf2#Eh+5k z@{EQ{>L|h;pDhR9c;7ZG`|(yB(GulCts;(t9g9nOGeWcz3TK7FFER zneLZ56NztUxfY>Q!wx-<&-Sc$KYYq&_R}lKsSPC-kee?4`^^p)3$A(*Prc9=w0yJe zS%Ku|X?Ne2>*UFK(jv|g)a3Ok<74JrG4ConV`1=?YZVXPWFVS-ER^zxbhxA$unfsr znH<*wWz7kcfRLH=g6FRxukTzadrTNhzKY+jt+MdijtoA!bTcmb=y+T(e=KHAtq%J4 za?l{>Bj)*~H&S@yH+yi;uXdXvPyc{#(mF{krK6~ZNnOlYfIU54C~2Q|d)9V7a&od5 zroZ5vz_44xf}k@8boKgZsI@TT=md^Gx(Y{*kC}F`nDwqcA8+i+;LmTQ@t18WlSN1k zK`epE|7{9t3WovBFmXwxwt(|d08M2w7u?>i8tdyfC_8?fnFk+Yk3h_~Xt)%JGG0L6 z$^{=%Cy;?6<~klzrr;wMBdtuqJ?ME!nG}4 z*lD3aM3?Ku3-!A;o5SySjD)8gl#mDa?d90PS;V2BFezXz?d$2VH7^YUzi`vO!6=ND zW>>-#O0#zR){s+UiC;s=5R;y(^yP1c)cH2IF&H3+w-qe%cWM-T@7A4ELE__Y;v zt$@Or>Ah*8mb5UFog5O)4}06RFQKHGDpl3ok~%0d3O4}q(?TzOGsWt|?G{!%NFFvN zPoBFXNc)17REvHUh5a}Q2$xg+rsgqr#@z^56TqiJmLK-yPpX}5Px=OZV7xR^76 z2BW}&k$`YHbth+aK>Jr2{}xOZiLeib(#$&JiM!lOu`dw{ubD{-x`pnJ2Xv0ou<@&B;IsXKVzwmEf^*#aqc!(n4mk z&)(4Hd;N<*cAd=g8L>;rGHa=VN;S08-SI$nX0_3Nd4fz!5-LgLC$ay8&*s%^C0iy4zIM*j7$=2)S;gE+@;RETvq0S*oI_GjsfPE3+ap1Hr#! zH8+uunT`V+3fm|A0h`-e$Zu_B)mAx_u;J6k8aKUMN9pdTq3mhFbA(1%TFC56^^@J6 zWXHoNx8zK}2)~U;e`+O9uB*Y1FSwgesjtI&CxMGtW2~2f&I~|{!+^rO zW7W9-;-Bz;Qq~=WeX+&YylkJKqxzNmIKAfyp+k?TJ^^bjWEiP_N)^;-KZDxnT%QaB>j1DAcXw3^Ia5jn7zhEDautr>$Q7q7y#5(|Vq2ufsnlZHnaq=iEC9x-vC zhKeb6d-qHGOr$U7+{aC^YD{obaAk=@Gb?o;4567A_CObWJBDQ!+|M&*5Qy~{Sp&e3 zNZFFvuQCf(X2S;cfeHs}Hq}n;Ej888P0P)aW+KG%@8Pi^d_2={wEnFFUjk_jwyWUS z8lN_=wTCCC(gk0#*o35op1)LP!wNmRfPCBHgZEq1VO1{h%jNm;7-m>8)M`JwuKn9U zx|NAB+*}@*QAKK4_ge-@u-sXQ zC~f+czt}{k`z4tjTakl+rQ+9MX|M18!HYU1n*&~i_VI>xk&$H!5BhPEF9$2)y(a&k z8DfxB;v$Tsg+h#&m7zuJmx0tnAh-NjD*U3{l>_Xcf04>hMsy7a3W|WT;qx8E(89 zPNpBvc^*!E$E(PQfZwh}d^qLf@+iaZeUS2VUx3$r4yU`N4p#OffXFN<1@==Os+w*$ zzhUixi#g^}xi+HJ7vIYtsAn9 zLtatMN%%F6CBLRPbJGjJ1iNThpkC5S#VbC^}b^0EM< z2(8(Oe$!5{kU=)h1nt-eTHBVjBxZ>?;zcXDZhRG9P&OuOG5v!BM+Peg&>pt!OyPuw z7gJB}+{&{6qX=zc?f!;Uz#1IHz^F&?+^eC5D(^As&9TEo*+<%=7DT3n{Mymz0^v_e zJpIt_QJeP&f0AQO>_EigW@Hmfb9yV=#wrYf$O4oz+M(8-LEiLLe4ic1TsLJ>O!FX= zTo5SCB0CPtP2oN{cHZM`x3)5bS(NulVr?*#YlvciGKXk%dpp#zTk(skF*q~b3;14O z2`n2bUJIo5;IWsc;A8W$6+#IhFTRWa(W=H56rq7Np7eTg>zzZy2hjj1cVta>a}rL2 ziyJKmpOnSYv+r*8T|Cs~;A_cGQl|N#pcr)Vz5LmD4ek=@N|W750I6>LzRSk^85_|f zsby{uL<68Au!Nj5{v+ye9W#(+%2K3?&KVQiaa^rY)3 z9SdLCh#5-mp!RZ<238tK1_Jq}@sl6XXDf_%o==;758tnDge^g$41lUS#M}XNGUG-W zKpXSh)e%M#m~suI#XsSzFMW`=T>2pIx-`jaF20A~E3fC?k1z_*yqXhBpXA2e|0Cg# zA~fgCClE$bB>d4@L*SuHz!4%uh!7z{ga{ELM2HX}LWBqrMj8GejeBw&96HmC00000 LNkvXXu0mjfG5qV0 literal 0 HcmV?d00001 From 05a5abecd53bca5cabba2ade618f24a6f1cc1ba4 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sun, 5 Jul 2026 00:17:45 +0800 Subject: [PATCH 13/18] build(deps): upgrade the test stack xunit 2.5.3->2.9.3, xunit.runner.visualstudio 2.5.3->3.1.5, Microsoft.NET.Test.Sdk 17.8.0->18.7.0, coverlet.collector 6.0.0->10.0.1. Clears two High transitive advisories pulled in via the old xunit (System.Net.Http 4.3.0, System.Text.RegularExpressions 4.3.0) and the known coverlet 6.0.0 hang. 543 tests verified stable across three runs under the 3.x runner scheduling. --- tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj | 8 ++++---- tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj | 8 ++++---- tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj | 8 ++++---- .../EzDdd.Integration.Tests.csproj | 8 ++++---- tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj b/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj index 02f2b0c..4eb94e1 100644 --- a/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj +++ b/tests/EzDdd.Common.Tests/EzDdd.Common.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj b/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj index 8f9a771..192de23 100644 --- a/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj +++ b/tests/EzDdd.Cqrs.Tests/EzDdd.Cqrs.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj b/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj index 8ec21e3..d231d0a 100644 --- a/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj +++ b/tests/EzDdd.Entity.Tests/EzDdd.Entity.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj b/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj index 071a9e5..4d194a8 100644 --- a/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj +++ b/tests/EzDdd.Integration.Tests/EzDdd.Integration.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj b/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj index f5be9aa..517016b 100644 --- a/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj +++ b/tests/EzDdd.UseCase.Tests/EzDdd.UseCase.Tests.csproj @@ -6,10 +6,10 @@ - - - - + + + + From 8fc5df075ce8a0ae1a603954c2d703beba6825ce Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sun, 5 Jul 2026 00:51:11 +0800 Subject: [PATCH 14/18] docs: distill agent guidance into AGENTS.md and retire porting scaffolding Replace the 36KB CLAUDE.md with a concise AGENTS.md (commands, standards, key rules, gotchas) plus a one-line CLAUDE.md pointer, following the uContract.NET pattern. Remove DOTNET_PORT.md (internal porting log) and the .claude/ directory (personal tooling config) from the repository, rewrite ROADMAP.md as a lean public roadmap, sweep every reference (ADRs, README, templates), replace local filesystem paths in ADRs with upstream-relative ones, and point feature requests at Issues instead of the unprovisioned Discussions. --- AGENTS.md | 56 ++ CHANGELOG.md | 2 +- CLAUDE.md | 750 +----------------- README.md | 26 +- ROADMAP.md | 682 +--------------- docs/MIGRATION_GUIDE.md | 1 - docs/RELEASE_CHECKLIST.md | 15 +- docs/adr/0001-target-framework.md | 2 +- docs/adr/0002-package-naming-and-structure.md | 2 +- ...03-module-architecture-dependency-chain.md | 2 +- ...4-zero-third-party-dependency-principle.md | 2 +- ...0005-complete-reimplementation-approach.md | 4 +- ...contract-integration-design-by-contract.md | 2 +- docs/adr/0007-ientity-ivalueobject-design.md | 6 +- docs/adr/0008-idomain-event-hierarchy.md | 10 +- .../0009-aggregate-root-base-class-design.md | 8 +- ...gate-root-event-sourcing-implementation.md | 6 +- .../0011-event-replay-invariant-checking.md | 8 +- ...resource-management-event-bus-producers.md | 6 +- ...0014-domaineventdata-equality-semantics.md | 2 +- docs/adr/0015-cross-platform-dto-structure.md | 2 +- docs/adr/0016-async-await-throughout.md | 2 +- ...onciler-interface-system-reconciliation.md | 4 +- ...oducer-refactoring-java-4-1-0-alignment.md | 4 +- docs/adr/0027-thread-null-safety-review.md | 4 +- docs/adr/ADR.template.md | 2 +- docs/adr/ADR_PLANNING.md | 34 +- docs/adr/ADR_PLANNING.template.md | 4 +- docs/adr/README.md | 67 +- docs/examples/USAGE_EXAMPLES.md | 1 - docs/patterns/SERVICE_LAYER_PATTERN.md | 2 +- 31 files changed, 158 insertions(+), 1560 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..51d0fa6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# ezDDD.NET + +Port of [Java ezddd 6.0.1](https://gitlab.com/TeddyChen/ezddd) (commit `3aac0f5`), ~99% semantic parity. Tactical DDD, CQRS, and Clean Architecture for .NET 8, with both state sourcing and event sourcing. + +## Commands + +- Build: `dotnet build ezDDD.sln` +- Test all: `dotnet test` +- Single test: `dotnet test --filter "FullyQualifiedName~TestName"` +- Pack: `dotnet pack` +- Format check: `dotnet tool restore && dotnet csharpier check .` + +## Module Architecture + +``` +EzDdd.Common → EzDdd.Entity → EzDdd.UseCase → EzDdd.Cqrs → EzDdd.Core (aggregator) +``` + +Package IDs use `ezDDD.*` (brand), namespaces use `EzDdd.*` (.NET convention). + +## Development Standards + +### Workflow + +Always follow: **Plan → Confirm → Execute** + +1. Present approach before writing code +2. Wait for explicit user approval +3. Implement the approved plan + +### Testing + +**Never write implementation code before a failing test.** Follow TDD (Red → Green → Refactor). xUnit, AAA pattern, no mocking libraries. + +### Code Changes + +Never mix structural and behavioral changes in the same commit (Tidy First). + +### Architecture Decisions + +- Location: `docs/adr/` +- ADRs are the source of truth +- Check existing ADRs before making architectural suggestions + +## Key Rules + +- **Event sourcing correctness (R1–R3)**: construction events establish invariants, command events must hold them before and after, destruction events may break them last — enforced by `EsAggregateRoot.Apply()`; see [ADR-0011](docs/adr/0011-event-replay-invariant-checking.md). +- **Level Z analyzers**: Meziantou + Roslynator + a `.editorconfig` that promotes style suggestions to warnings; `TreatWarningsAsErrors` makes the build fail on any of them. Fix findings, don't suppress without an inline rationale. +- **PublicAPI baseline**: PublicApiAnalyzers tracks each `src/` project's API surface — new public members must be added to that project's `PublicAPI.Unshipped.txt` (promoted to `Shipped` at release; see CONTRIBUTING.md). +- **CSharpier is the only formatter**: never hand-format or run IDE cleanup profiles other than CSharpier; CI runs `dotnet csharpier check .`. + +## Gotchas + +- `DomainEventTypeMapper` is a process-global static registry. Tests touching it must share a consistent mapping; follow the `[Collection("DomainEventTypeMapper")]` precedent in `tests/EzDdd.Entity.Tests`. +- The `EsAggregateRoot` replay constructor intentionally calls virtual `_When()` while rebuilding state — derived classes must not depend on fields initialized in their own constructors. +- Soft-deleted aggregates (`IsDeleted == true`) are filtered by `OutboxRepository.FindByIdAsync` (returns `null`) but remain in storage so their domain events can still be relayed. diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb6a3f..c59e2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,7 +134,7 @@ See the [Migration Guide](docs/MIGRATION_GUIDE.md) for complete migration instru If you encounter any issues or have suggestions: - **Bug Reports**: [GitHub Issues](https://github.com/cwouyang/ezDDD.NET/issues) -- **Feature Requests**: [GitHub Discussions](https://github.com/cwouyang/ezDDD.NET/discussions) +- **Feature Requests**: [GitHub Issues](https://github.com/cwouyang/ezDDD.NET/issues) --- diff --git a/CLAUDE.md b/CLAUDE.md index 56f21ac..54f3935 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,749 +1,3 @@ -# CLAUDE.md - ezddd.NET Port +# CLAUDE.md -This file provides guidance to Claude Code when working on the **.NET port** of ezddd. - -> **Note**: This is for the .NET/C# version. For the Java version, see [../ezddd/CLAUDE.md](../ezddd/CLAUDE.md) - ---- - -## 📌 Project Overview - -**ezddd.NET** is a .NET port of the Java ezddd library, providing tactical Domain-Driven Design (DDD) patterns, Command Query Responsibility Segregation (CQRS), and Clean Architecture (CA) support. It supports both **state sourcing** and **event sourcing** for implementing aggregates and repositories. - -**Based on**: Java ezddd 6.0.1 (commit `3aac0f5`) - ✅ **Synchronized 2.1.0 → 4.1.0 in Phase 6, 4.1.0 → 6.0.1 in Phase 7** - -- **Language**: C# / .NET 8+ -- **Build Tool**: dotnet CLI -- **Status**: first public release in preparation (not yet published to NuGet) -- **Target Release**: 1.0.0 (based on Java ezddd 6.0.1) -- **Target Framework**: .NET 8.0 -- **Main Namespace**: `EzDdd` -- **Package ID Prefix**: `ezDDD` -- **Semantic Parity**: ~99% with Java ezddd 6.0.1 (commit `3aac0f5`) - ---- - -## ⚠️ Project Status - -**✅ PHASE 6 STAGE S6 COMPLETE** (2026-01-11) — first public release in preparation - -**Implementation Progress**: -- ✅ **Phase 1-5 Complete** (Based on Java ezddd 2.1.0): - - ✅ **Phase 1** - EzDdd.Common (69 tests passing) - - ✅ **Phase 2** - EzDdd.Entity (85 tests passing) - - ✅ **Phase 3** - EzDdd.UseCase (279 tests passing) - Complete with all fixes - - ✅ **Phase 4** - EzDdd.Cqrs (68 tests passing) - Complete - - ✅ **Phase 5** - EzDdd.Core + Documentation (11,212+ lines) - Complete -- ✅ **Phase 6 Complete** (Sync to Java ezddd 4.1.0): - - 44 commits synchronized (+5,167/-2,132 lines) - - 4 ADRs written (ADR-0024 to ADR-0027) - All architecture decisions documented - - 3 originally planned ADRs (0028-0030) not needed - process/review docs, not architecture decisions - - Actual: 25.5 hours (vs estimated 44-62 hours - 58% time saved) - -**ADRs Status**: -- ✅ **Completed**: 27 ADRs (Stage 1-5 + Phase 6) - - Stage 1 (6 ADRs): Core Architecture - - Stage 2 (5 ADRs): Core DDD Patterns - - Stage 3 (5 ADRs): Phase 3 Post-Review - - Stage 4 (3 ADRs): Phase 4 Critical - - Stage 5 (4 ADRs): Phase 4 Post-Implementation - - Phase 6 (4 ADRs): Java 4.1.0 Sync - ADR-0024, ADR-0025, ADR-0026, ADR-0027 -- ❌ **Not Needed**: 3 originally planned ADRs (process/review docs, not architecture decisions) - - ADR-0028 (Pre-Publication Sync) - Process decision, covered in CHANGELOG.md - - ADR-0029 (Post-Implementation Review) - Project retrospective, not architecture - - ADR-0030 (Feature Parity Verification) - QA report, covered in README/MIGRATION_GUIDE - - **Reason**: ADRs document "why we designed it this way" (architecture), not "how we developed" (process) - -**Test Coverage**: 543 tests passing (100% pass rate, >90% code coverage) - Updated 2026-07-04 after Phase 7 (Common 69 / Entity 92 / UseCase 283 / Cqrs 71 / Integration 28; earlier "562" figure included MessageProducer tests removed in Phase 7 I3) - -**Current Status** (2026-01-11): -- ✅ Phase 1-5 complete (based on Java ezddd 2.1.0, commit `6e94aee`) -- ✅ Phase 6 Stage S6 complete (7/8 stages, 87.5%) - **S7 deferred to post-1.0.0** -- ✅ Pre-publication synchronization complete (all Java 4.1.0 features implemented) -- 🎯 First public release in preparation (not yet published) - -**Next Steps**: Prepare the first public NuGet release (Stage S7 deferred to post-release maintenance) - -**📊 Complete Roadmap**: See [ROADMAP.md](ROADMAP.md) for detailed workflow, timeline, and progress tracking - ---- - -## 📊 Java Version Correspondence - -### Historical Base (Phase 1-5) - Development Foundation - -- **Based on**: Java ezddd 2.1.0 -- **GitLab Repository**: https://gitlab.com/TeddyChen/ezddd -- **GitLab Commit**: `6e94aee` -- **Commit Message**: "[Refactoring and Deployment and Feature Addition] (1) Revised DomainEventDto to InternalDomainEventDto. (2) Release version 2.1.0. (3) Added readTree to support byte array." -- **Release Date**: 2024-03 -- **Semantic Parity**: ~98% -- **Status**: ✅ Complete (501 tests passing, 23 ADRs) -- **Note**: Phase 1-5 were developed against Java 2.1.0, then synchronized to 4.1.0 in Phase 6 - -### Phase 6 Synchronization (Java 4.1.0) ✅ - -- **Based on**: Java ezddd 4.1.0 -- **GitLab Commit**: `91fac63` (HEAD, master) -- **Changes Since 2.1.0**: 44 commits (+5,167 lines, -2,132 lines) -- **Major Version Jump**: 2.1.0 → 3.0.0/3.0.1 → 4.0.0 → 4.1.0 -- **Semantic Parity Achieved**: ~99% ✅ - -**Major Changes in Java 4.1.0**: -- ⚠️ **BREAKING**: `IDomainEvent.Metadata` property - Add metadata for idempotency support -- ⚠️ **BREAKING**: `MessageBus` → `MessageProducer` refactoring - New messaging pattern -- ✨ **NEW**: `IReconciler` interface - System state reconciliation -- 🔄 **REFACTOR**: Service Layer pattern - Explicit service classes -- 🐛 **FIX**: Thread safety improvements (CopyOnWriteArrayList, AtomicReference) -- 🐛 **FIX**: Null safety - Comprehensive null validation -- 🐛 **FIX**: Equals/HashCode contract compliance - -### Current Implementation (Phase 7 Complete) ✅ - -- **Based on**: Java ezddd 6.0.1 -- **GitLab Commit**: `3aac0f5` (HEAD, master, 2026-07-03) -- **Changes Since 4.1.0**: 7 commits (5.0.0 → 5.0.1 → 6.0.0 → 6.0.1), 10 files, +211/−33 -- **Semantic Parity Verification**: [docs/PHASE7_SEMANTIC_PARITY_REPORT.md](docs/PHASE7_SEMANTIC_PARITY_REPORT.md) - -**Major Changes in Java 5.x/6.x (synchronized in Phase 7, 2026-07-04)**: -1. ⚠️ **BREAKING** (5.0.0, `b7a336f`): `Projector` → `Projector extends Reactor`, new `Notifier extends Reactor` — .NET: `IReactor` re-added, `IProjector : IReactor`, `INotifier : IReactor` ([ADR-0028](docs/adr/0028-reactor-hierarchy-projector-notifier-genericization.md)) -2. 🗑️ **REMOVED** (`dc4b2e1`): `ExternalDomainEventDto` deleted upstream — .NET no-op (never ported) -3. ✨ **NEW** (`f440d15`): `ExternalDomainEventPublisher` out-port — .NET: `IExternalDomainEventPublisher` -4. ⚠️ **BREAKING** (6.0.0, `67686ac`): `MessageProducer` moved out of core into ezddd-gateway artifact — .NET: `IMessageProducer`/`InMemoryMessageProducer` removed from core ([ADR-0029](docs/adr/0029-messageproducer-removal-gateway-deferral.md)); ezDDD.Gateway package deferred post-1.0 -5. 🐛 **FIX** (6.0.1, `3aac0f5`): `OutboxRepository.findById` must filter soft-deleted aggregates — .NET: same bug fixed in `OutboxRepository.FindByIdAsync` + ported test - -### Version Strategy - Pre-Publication Synchronization - -**⚠️ Critical Decision**: Since ezDDD.NET has **NOT been published to NuGet yet**, we have a unique opportunity to: - -- ✅ Incorporate all Java 4.1.0 changes (including breaking changes) into the **initial 1.0.0 release** -- ✅ Avoid publishing an "outdated" API based on older Java version -- ✅ Deliver a complete, up-to-date API aligned with Java ezddd 4.1.0 from day one -- ✅ Users receive a mature, feature-complete 1.0.0 from the start -- ✅ No migration needed - users never see the old API - -**Approach**: -1. Complete Phase 6 synchronization work (44-62 hours) -2. Update all internal references from "based on Java 2.1.0" to "based on Java 4.1.0" -3. Publish initial version as **1.0.0** to NuGet -4. Users get the full Java 4.1.0 feature set from the start - -**This is only possible because we haven't published yet**. Once published, breaking changes would require a major version bump (2.0.0). - -**Complete Sync Plan**: See [DOTNET_PORT.md](DOTNET_PORT.md) "Java 4.1.0 Synchronization Plan" section (lines 45-1040) for detailed implementation plan. - ---- - -## 📖 Planning Documents - -### Core Planning -- **[ROADMAP.md](ROADMAP.md)** - 🎯 Development roadmap and progress tracking -- **[DOTNET_PORT.md](DOTNET_PORT.md)** - Technical planning and API design -- **[CLAUDE.md](CLAUDE.md)** - This file (development guidance) - -### Architecture Decision Records -- **[ADR_PLANNING.md](docs/adr/ADR_PLANNING.md)** - 28 planned ADRs roadmap -- **[README.md](docs/adr/README.md)** - ADR index and workflow -- **[ADR.template.md](docs/adr/ADR.template.md)** - Standard template - ---- - -## 🎯 Project Goals - -Create a .NET library that: - -1. **Maintains Java ezddd's Design Philosophy**: - - Same tactical DDD patterns (Entity, ValueObject, AggregateRoot, EsAggregateRoot) - - Same Clean Architecture layers (Entity, UseCase, Interface Adapters) - - Same CQRS patterns (Command, Query, Projection, Projector) - - Same event sourcing correctness rules (R1, R2, R3) - -2. **Leverages .NET Platform Strengths**: - - Async/await throughout - - Nullable reference types for compile-time safety - - Record types for immutable domain events and value objects - - Pattern matching for event handling - - Modern C# idioms - -3. **Maintains Minimal Dependencies**: - - Zero third-party external dependencies for core libraries - - Only .NET built-in APIs (System.Text.Json, System.Reflection, System.Collections.Concurrent) - - Only TeddySoft ecosystem dependencies (uContract.NET for Design by Contract) - ---- - -## 📦 Naming Convention - -**Confirmed Decision** (2025-10-28): - -### Package IDs (NuGet) -- `ezDDD.Common` -- `ezDDD.Entity` -- `ezDDD.UseCase` -- `ezDDD.Cqrs` -- `ezDDD.Core` - -### Namespaces (C# Code) -- `EzDdd.Common` -- `EzDdd.Entity` -- `EzDdd.UseCase` -- `EzDdd.Cqrs` -- `EzDdd.Core` - -**Rationale**: -- **Package ID** preserves brand identity (`ezDDD` corresponds to Java's `ezddd`) -- **Namespace** follows .NET PascalCase convention -- Balances brand recognition with .NET ecosystem standards - ---- - -## 📂 Module Architecture - -The project consists of 5 modules with clear dependency hierarchy: - -``` -EzDdd.Common (utilities: BiMap, IConverter, JsonUtil) - ↓ -EzDdd.Entity (core DDD: IEntity, IValueObject, IDomainEvent, AggregateRoot, EsAggregateRoot) - ↓ -EzDdd.UseCase (use cases: IUseCase, IRepository, IRepositoryPeer, IReactor, mappers) - ↓ -EzDdd.Cqrs (CQRS: ICommand, IQuery, IProjection, IProjector, IArchive, IInquiry) - ↓ -EzDdd.Core (aggregator module + documentation) -``` - -### EzDdd.Common -Foundation utilities for the entire framework: -- **BiMap**: Bidirectional mapping (thread-safe version of Java BiMap) -- **IConverter**: Generic type conversion interface -- **JsonUtil**: System.Text.Json utilities - -### EzDdd.Entity -Core DDD building blocks (entities layer): -- **IEntity**: Interface for entities with unique identity -- **IValueObject**: Marker interface for immutable value objects -- **IDomainEvent**: Base interface with Id, OccurredOn, Source, Metadata - - **InternalDomainEvent**: Events within a bounded context - - **IConstructionEvent**: Marker for aggregate creation (must be first event) - - **IDestructionEvent**: Marker for aggregate deletion (must be last event) -- **AggregateRoot**: Base class for aggregates with event collection and versioning -- **EsAggregateRoot**: Event-sourced aggregate root - - Reconstructs state from event history via replay - - Template method `Apply()` enforces invariant checking before/after calling abstract `When()` - - Stream naming convention: `{category}-{id}` - -> **Note**: `ExternalDomainEvent` deferred to Phase 3 (EzDdd.UseCase module) - belongs to integration layer - -### EzDdd.UseCase -Use cases layer with persistence abstractions: -- **IUseCase**: Command pattern interface with `ExecuteAsync(input)` method -- **IInput/IOutput**: Base interfaces for use case inputs and outputs -- **IRepository**: Aggregate persistence interface (FindByIdAsync, SaveAsync, DeleteAsync) -- **IRepositoryPeer**: SPI for actual persistence implementation (interface adapters layer) -- **EsRepository**: Generic event sourcing repository implementation -- **OutboxRepository**: Generic state sourcing repository with Transactional Outbox pattern -- **DomainEventMapper**: Converts domain events to/from DomainEventData records -- **IReactor**: In-port for reacting to messages (base of IProjector/INotifier; Phase 7) -- **IExternalDomainEventPublisher**: Out-port for publishing external domain events (Phase 7) - -### EzDdd.Cqrs -CQRS pattern separation: -- **ICommand**: Marker extending IUseCase for write operations -- **IQuery**: Marker extending IUseCase for read operations -- **IInquiry**: Validation queries usable within commands -- **IProjection**: Read model builder that generates view models from query database -- **IProjector**: Reactor that writes read models in a query database (generic since Phase 7, ADR-0028) -- **INotifier**: Reactor that converts internal events to external events and dispatches them outward (Phase 7) -- **IArchive**: Query database interface (query-side counterpart to IRepository) -- **CqrsOutput**: Unified output class with builder pattern - ---- - -## 🔑 Key Architectural Patterns - -### Clean Architecture Layers -- **Entities Layer**: IEntity, IValueObject, IDomainEvent, AggregateRoot -- **Use Cases Layer**: IUseCase, IInput, IOutput, IRepository -- **Interface Adapters Layer**: IRepositoryPeer implementations, Mappers -- **Frameworks/Drivers Layer**: ASP.NET Core, databases (implementation-specific) - -### Event Sourcing -1. All aggregate state is reconstructed from events -2. `EsAggregateRoot` constructor accepts `IEnumerable` for replay -3. Events persisted in append-only event store -4. `DomainEventTypeMapper` maps event types to strings for serialization -5. Invariants enforced before/after each event via `EnsureInvariant()` - -### State Sourcing with Transactional Outbox -1. `OutboxRepository` stores current aggregate state AND events -2. Ensures atomic persistence of both via database transactions -3. Transaction boundary MUST be at `IRepositoryPeer` implementation level (not IRepository level) -4. Faster reads than pure event sourcing at cost of dual write - -### CQRS -- **Write Model**: Commands use IRepository to persist aggregates -- **Read Model**: Queries use IProjection/IArchive for optimized reads -- **Projectors**: Background services listen to events and update read models -- **Eventual Consistency**: Read models eventually consistent with write model - -### Bridge Pattern for Persistence -- **IRepository** (abstraction) defines domain-level persistence contract -- **IRepositoryPeer** (implementor) provides actual database implementation -- Ensures aggregates in entities layer don't leak to adapters layer - ---- - -## ⚡ Important Implementation Rules - -### Aggregate Correctness Rules -EsAggregateRoot enforces three invariant rules (same as Java): - -- **R1 (Construction)**: `{pre₀} fun₀ {post₀ & INV}` - - Construction events establish initial invariants - - No precondition invariant check - -- **R2 (Command)**: `{preₜ & INV} funₜ {postₜ & INV}` - - Command events must maintain invariants before and after - - Invariant checked both before and after `When()` - -- **R3 (Destruction)**: `{preᵤ & INV} funᵤ {postᵤ}` - - Destruction events may break invariants as final operation - - No postcondition invariant check - -### Event Sourcing Implementation -When creating event-sourced aggregates: - -1. First event MUST implement `InternalDomainEvent.IConstructionEvent` -2. Last event (deletion) MUST implement `InternalDomainEvent.IDestructionEvent` -3. Override `EnsureInvariant()` to check business rules -4. Override `When(TEvent @event)` to handle state changes (use pattern matching) -5. Override `GetCategory()` to return category string for stream naming -6. Provide constructor accepting `IEnumerable` for replay - -**Pattern**: -```csharp -public class Aggregate : EsAggregateRoot -{ - public Aggregate(IEnumerable events) : base(events) { } - protected override void EnsureInvariant() { /* check rules */ } - protected override void When(InternalDomainEvent @event) { /* pattern match */ } - public override string GetCategory() => "category"; -} -``` - -### Transaction Management -Production `IRepositoryPeer` implementations MUST: - -- Use `async/await` for all I/O operations -- Use `@Transactional` equivalent (e.g., `TransactionScope` or EF Core transactions) on `SaveAsync()` method -- Ensure atomic persistence of both aggregate state and events -- Rollback completely if either operation fails -- Transaction boundary ONLY at RepositoryPeer level, NOT Repository level - -### Event Type Mapping -Register all domain event types with `DomainEventTypeMapper` for serialization. - ---- - -## 🆚 Differences from Java Version - -### Expected Syntax Differences - -**Naming Conventions**: -- Java uses concrete classes: `Entity`, `AggregateRoot`, `UseCase` -- C# uses interfaces: `IEntity`, `IAggregateRoot`, `IUseCase` -- C# uses PascalCase: `ExecuteAsync()`, `FindByIdAsync()` - -**Generic Parameters**: -- Java: `` -- C#: `` - -**Async/Await**: -```java -// Java: Synchronous -O execute(I input) throws UseCaseFailureException; -Optional findById(ID id); - -// C#: Asynchronous -Task ExecuteAsync(TInput input); -Task FindByIdAsync(TId id); -``` - -**Collections**: -```java -// Java -List getDomainEvents(); -Optional findById(ID id); - -// C# -IReadOnlyList GetDomainEvents(); -Task FindByIdAsync(TId id); // Nullable reference -``` - -### .NET Platform Improvements - -1. **Pattern Matching**: Use switch expressions with type patterns (more concise than instanceof) -2. **Record Types**: Immutable events with primary constructors and init-only properties -3. **Nullable Reference Types**: Compile-time null safety with `?` annotations -4. **Async/Await**: Modern asynchronous programming throughout all I/O operations - ---- - -## 🏗️ Build Commands - -```bash -dotnet build # Build the solution -dotnet test # Run all tests -dotnet test --filter "ClassName~Foo" # Run specific tests -dotnet pack # Create NuGet packages -dotnet build -c Release # Release build -``` - ---- - -## 📦 Dependencies - -### Runtime - -**Zero third-party external dependencies** (same philosophy as uContract.NET) - -**Built-in Dependencies (.NET BCL)**: -- `System.Text.Json` for event serialization and deep copy -- `System.Reflection` for EsAggregateRoot reflection instantiation -- `System.Collections.Concurrent` for thread-safe collections - -**Ecosystem Dependencies (TeddySoft Libraries)**: -- **uContract.NET** (v1.0.0+) - Design by Contract support - - Provides `Contract.Require()`, `Contract.Ensure()`, `Contract.Invariant()`, `Contract.Check()` - - Maintains semantic parity with Java ezddd's use of uContract 2.0.0 - - Essential for EsAggregateRoot invariant checking (R1, R2, R3 rules) - - Part of TeddySoft ecosystem, not considered third-party dependency - -**Rationale for uContract.NET dependency**: -- **Semantic Parity**: Java ezddd depends on uContract 2.0.0 for Design by Contract -- **Correctness**: Event sourcing invariant rules require robust contract checking -- **Ecosystem Consistency**: Both libraries are part of TeddySoft DDD toolkit -- **Avoid Duplication**: Reuse existing, tested DbC implementation - -### Test -- **xUnit** - Testing framework (same as uContract.NET) -- **No mocking libraries** - Keep tests simple and clear - ---- - -## 📚 References - -### Original Java Version (2.1.0) - -**This .NET port is based on Java ezddd 2.1.0** (GitLab commit: `6e94aee`) - -- **Java ezddd Repository**: https://gitlab.com/TeddyChen/ezddd -- **Commit**: `6e94aee` (Release 2.1.0, 2024) -- **Java ezddd CLAUDE.md**: [../ezddd/CLAUDE.md](../ezddd/CLAUDE.md) -- **Module Documentation**: See Java version modules - -### Related .NET Port -- **uContract.NET Repository**: [../uContract.NET](../uContract.NET) -- **uContract.NET DOTNET_PORT.md**: Porting experience reference -- **uContract.NET CLAUDE.md**: Development guidance reference - -### .NET Resources -- **Async/Await Best Practices**: https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/ -- **Nullable Reference Types**: https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references -- **Records**: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record -- **System.Text.Json**: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview -- **Pattern Matching**: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns - -### DDD & Event Sourcing -- **Event Sourcing**: https://martinfowler.com/eaaDev/EventSourcing.html -- **CQRS**: https://martinfowler.com/bliki/CQRS.html -- **DDD Tactical Patterns**: Eric Evans - Domain-Driven Design -- **Clean Architecture**: Robert C. Martin - Clean Architecture - ---- - -## 🚨 Important Reminders for Claude Code - -### Before Any Development Task - -1. **Context Awareness**: If user doesn't specify "Java" or ".NET", **ASK FIRST** - - User might be working on either Java ezddd or .NET ezddd.NET - - Always clarify which version before proceeding - -2. **Check Project Status**: - - ✅ **Phase 1-7 全部完成**(2026-07-04)— 基準 Java ezddd 6.0.1 (`3aac0f5`),543 tests passing - - 🎯 **下一步**: 1.0.0 發佈準備(見 HANDOFF.md 的 P0/P1 清單) - - See "Current Phase: Phase 7" section above for the latest sync details - -3. **Consult Planning Documents**: - - Check [ADR_PLANNING.md](docs/adr/ADR_PLANNING.md) for ADR priorities and dependencies - - Check [DOTNET_PORT.md](DOTNET_PORT.md) for technical decisions (references completed ADRs) - - Use [ADR.template.md](docs/adr/ADR.template.md) when writing new ADRs - - Follow ADR maintenance workflow in [docs/adr/README.md](docs/adr/README.md) - -### When Implementing - -1. **Follow .NET Conventions**: - - Use PascalCase for public members - - Use `_camelCase` for private fields - - Use `async/await` for all I/O operations - - Enable nullable reference types (`#nullable enable`) - - Use `record` for immutable types (events, value objects, DTOs) - -2. **Maintain Java Parity**: - - Keep same design philosophy and patterns - - Preserve same behavior (especially event sourcing rules R1, R2, R3) - - Cross-reference Java implementation when uncertain - -3. **Testing Requirements**: - - Write tests BEFORE implementation (TDD) - - Use xUnit with AAA pattern (Arrange, Act, Assert) - - >90% unit test coverage - - Integration tests for complete scenarios - -4. **Documentation**: - - XML documentation comments for all public APIs - - Clear examples in documentation - - Update DOTNET_PORT.md when making architectural changes - - Write ADRs as decisions are confirmed (following ADR workflow below) - -### ADR Workflow - -**Standard Workflow for Each ADR**: -1. **Write ADR** using [ADR.template.md](docs/adr/ADR.template.md) - - Reference [ADR_PLANNING.md](docs/adr/ADR_PLANNING.md) for key points and dependencies - - Set Status to "Accepted" for confirmed decisions - - Fill in all sections: Context, Decision, Consequences, Alternatives, Related, References -2. **Update [docs/adr/README.md](docs/adr/README.md)** (add to ADR Index) -3. **Update [DOTNET_PORT.md](DOTNET_PORT.md)** (add ADR link `[ADR-NNNN](docs/adr/NNNN-title.md)`) -4. **Update this CLAUDE.md** (mark decision as ✅ with ADR reference) - ---- - -## 🎯 Current Phase: Phase 7 - Java 6.0.1 Synchronization ✅ COMPLETE - -**Phase 7 Complete** (2026-07-04): Synchronized from Java ezddd 4.1.0 (`91fac63`) to 6.0.1 (`3aac0f5`, upstream HEAD). Plan: [docs/PHASE7_SYNC_PLAN.md](docs/PHASE7_SYNC_PLAN.md). - -- **I1** `9683b36`: `IReactor` re-added; `IProjector : IReactor` (breaking); `INotifier` added (ADR-0028) -- **I2** `ffb304e`: `IExternalDomainEventPublisher` out-port added; `ExternalDomainEventDto` removal confirmed no-op (never ported) -- **I3** `1b9b1e4`: `IMessageProducer`/`InMemoryMessageProducer` removed from core per upstream 6.0.0 (ADR-0029; ezDDD.Gateway package deferred post-1.0) -- **I4** `c8bdb5b`: `OutboxRepository.FindByIdAsync` soft-delete filter bug fix + 2 tests (upstream 6.0.1 bug fix) -- **I5**: Docs alignment + semantic parity spot-check ([docs/PHASE7_SEMANTIC_PARITY_REPORT.md](docs/PHASE7_SEMANTIC_PARITY_REPORT.md)) - -**Result**: build 0 errors / 0 warnings; **543 tests passing** (2026-07-04); semantic parity ≥98% vs `3aac0f5`. - ---- - -The section below documents Phase 6 (Java 4.1.0 synchronization, completed 2026-01-11) and is retained for historical reference. - -**Phase 6 Overview** (2026-01-06, estimated 44-62 hours / ~1-2 weeks): - -### 📋 Objective - -Synchronize ezDDD.NET from Java ezddd 2.1.0 (commit `6e94aee`) to Java ezddd 4.1.0 (commit `91fac63`) **before first NuGet publication**. - -This ensures users receive a complete, up-to-date API aligned with Java 4.1.0 from day one, with all breaking changes incorporated into the initial 1.0.0 release. - -### 🔥 Major Changes (Breaking) - -1. **IDomainEvent.Metadata Property** - Add `IDictionary Metadata { get; }` for idempotency support -2. **MessageBus → MessageProducer Refactoring** - Replace Observer pattern with new `IMessageProducer` abstraction -3. **IReconciler Interface** - New `IReconciler` for system state reconciliation -4. **Service Layer Pattern** - Extract complex business logic to explicit Service classes (optional pattern) -5. **Thread Safety Improvements** - Fix concurrency issues (DomainEventTypeMapper, BlockingMessageBus, etc.) -6. **Null Safety Enhancements** - Comprehensive null validation across all public APIs -7. **Equals/HashCode Compliance** - Fix contract violations in domain event data classes - -### 📊 Work Breakdown (8 Stages) - -| Stage | Description | Est. Hours | Status | ADRs | -|-------|-------------|------------|--------|------| -| **S0** | Planning & Preparation | 4-6 | ✅ Complete (2026-01-06) | - | -| **S1** | IDomainEvent.Metadata | 6-8 | ✅ Complete (2026-01-06) | ADR-0008 (existing) | -| **S2** | IReconciler Interface | 4-6 | ✅ Complete (2026-01-07) | [ADR-0024](docs/adr/0024-ireconciler-interface-system-reconciliation.md) | -| **S3** | MessageProducer Refactoring | 10-14 (3) | ✅ Complete (2026-01-07) | [ADR-0025](docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md) | -| **S4** | Service Layer Pattern | 6-8 (6) | ✅ Complete (2026-01-08) | [ADR-0026](docs/adr/0026-service-layer-pattern.md) | -| **S5** | Thread/Null Safety Review | 6-8 (6) | ✅ Complete (2026-01-08) | [ADR-0027](docs/adr/0027-thread-null-safety-review.md) | -| **S6** | Integration Testing & Docs | 6-8 (2) | ✅ Complete (2026-01-11) | - (not needed) | -| **S7** | Final Review & Completion | 2-4 | ⏭️ Deferred | - (post-1.0.0) | -| **TOTAL** | | **44-62 hours** | **7/8 complete (87.5%)** | **4 ADRs (all needed ADRs complete)** | - -### 🎯 Success Criteria - -**Phase 6 Complete When**: -- [x] All needed ADRs written and reviewed - **4/4 complete** ✅ ADR-0024, ADR-0025, ADR-0026, ADR-0027 (ADR-0028 to 0030 deferred) -- [x] All Java 4.1.0 features implemented and tested - **6/6 complete** ✅ Metadata, IReconciler, MessageProducer, Service Layer, Thread Safety, Null Safety -- [x] 520+ tests passing (>95% coverage) - **Current: 562 tests (100%)** ✅ -- [x] Zero compiler warnings - **Current: 0 warnings** ✅ -- [x] Zero static analysis errors - **✅ Verified** -- [x] All "Java 2.1.0" references updated to "Java 4.1.0" - **✅ Complete** -- [x] Feature parity with Java 4.1.0 verified (~99%) - **✅ Verified** -- [x] Documentation updated (README, CHANGELOG, API docs) - **✅ Complete** -- [x] NuGet packages prepared for the first release (5 packages) - **✅ Ready (at the time)** -- [x] Integration tests for all Java 4.1.0 features - **✅ 38 tests** - -### 📚 Complete Implementation Plan - -See [DOTNET_PORT.md](DOTNET_PORT.md) "Java 4.1.0 Synchronization Plan" section (lines 45-1040) for: -- Detailed stage-by-stage implementation guide -- Code examples and patterns -- Testing strategy -- Risk analysis -- Reference commits from Java ezddd - -**Current Progress**: Stage S1 Complete - 2/8 stages complete (25%), 0/7 new ADRs written (ADR-0008 existing covers S1) - ---- - -## 📅 Key Milestones - -### Phase 1: EzDdd.Common (2025-10-31) ✅ -- 3 components: IConverter, JsonUtil, BiMap -- 69 tests passing -- Thread-safe BiMap with lock-based synchronization - -### Phase 2: EzDdd.Entity (2025-11-01) ✅ -- 7 components: IEntity, IValueObject, IDomainEvent, AggregateRoot, EsAggregateRoot, DomainEventTypeMapper -- 85 tests passing -- R1/R2/R3 event sourcing correctness rules enforced -- ~95% semantic parity with Java ezddd - -### Phase 3: EzDdd.UseCase (2025-11-05 to 2025-11-06) ✅ - -**Accomplishments**: -- ✅ 8 iterations (Foundation → Use Case → Repository → Events → ES/SS → Message Bus → Integration) -- ✅ 25 core components + 6 integration test suites -- ✅ 254 tests passing, >95% coverage -- ✅ ~98% semantic parity with Java ezddd - -**Key Technical Achievements**: -1. Bridge Pattern (IRepository ↔ IRepositoryPeer) - Clean Architecture layer separation -2. Event Sourcing (EsRepository) - Reflection + ConstructorInfo caching -3. State Sourcing (OutboxRepository) - Transactional Outbox pattern -4. Message Bus (BlockingMessageBus) - Observer pattern with thread-safe snapshot -5. Generic variance (IUseCase) - Covariant/contravariant correct - -**Critical Design Decisions**: -- ConstructorInfo caching with ConcurrentDictionary (performance) -- IStoreData.Version type: int → long (Java parity) -- Event clearing: only after successful save (NOT on failure) -- IReactor async update: Execute() → ExecuteAsync() -- Events captured BEFORE SaveAsync() (repository clears after save) - -**Banking Test Domain**: BankAccount (event-sourced), Money (value object), 3 Use Cases - -### Phase 4: EzDdd.Cqrs (2025-11-10 to 2025-11-18) ✅ - -**Planning Complete** ✅ (Phase P, ~8 hours): -- ✅ Java CQRS analysis, C# API design, implementation plan, ADR planning, kickoff document -- ✅ 3 Critical ADRs written: ADR-0017 (CqrsOutput), ADR-0018 (IArchive), ADR-0019 (IInquiry/IProjection) - -**Implementation Complete** ✅ (All 7 iterations, ~13 hours): -- ✅ 9 core components: IInquiryInput, IProjectionInput, CqrsOutput, ICommand, IInquiry, IQuery, IProjection, IProjector, IArchive -- ✅ 67 tests passing (8 + 15 + 9 + 14 + 7 + 14), >90% coverage -- ✅ Complete CQRS flow integration tests (Command → Event → Projector → Archive → Query) -- ✅ 4 additional ADRs written: ADR-0020 (Projector), ADR-0021 (Variance), ADR-0022 (ReadModel), ADR-0023 (Idempotency) -- ✅ 0 compiler warnings (fixed all XML documentation warnings) -- ✅ 42% ahead of schedule (~13/19 hours) - -**Key Technical Achievements**: -1. Fluent API CqrsOutput with builder pattern (SetXxx() methods) -2. IArchive async methods throughout (FindByIdAsync, SaveAsync, DeleteAsync) -3. IInquiry/IProjection independence (both extend IUseCase, usable independently or within commands) -4. Complete event projection pattern (AccountProjector updating AccountSummaryReadModel) -5. Marker interfaces for compile-time type safety (IInquiryInput, IProjectionInput) -6. Generic variance annotations (contravariant input, covariant output) -7. Idempotent archive operations (upsert semantics for SaveAsync) -8. Read model design with C# record types - -**Critical Design Decisions**: -- IProjector as pure marker interface (lifecycle managed separately via BackgroundService) -- IArchive operations MUST be idempotent for reliable event replay -- Read models use C# record types with positional parameters -- Generic variance: `ICommand`, `IArchive` -- IInquiry and IProjection are independent from each other (separate use cases) - -### Phase 5: EzDdd.Core & Documentation (2025-11-22) ✅ -- 4 iterations (Documentation → Release Prep → README → Verification) -- 11,212+ lines of documentation -- 5 NuGet packages created (Common 35K, Entity 41K, UseCase 63K, Cqrs 37K, Core 28K) -- Version 1.0.0-alpha.1 (internal, not published) -- Complete documentation ecosystem -- Based on Java ezddd 2.1.0 - -### Phase 6: Java 4.1.0 Synchronization (2026-01-06 to 2026-01-11) ✅ **COMPLETE** - -**Objective**: Sync from Java ezddd 2.1.0 → 4.1.0 before first NuGet publication - ✅ **ACHIEVED** - -**Scope**: -- 44 commits synchronized (+5,167/-2,132 lines) ✅ -- 7 implementation stages completed (S0-S6, S7 deferred) -- 4 architecture ADRs written (ADR-0024 to ADR-0027) -- 3 process ADRs (0028-0030) determined not needed -- Actual: 25.5 hours (vs estimated 44-62 hours - **58% time saved**) - -**Major Changes Implemented**: -- ✅ **BREAKING**: IDomainEvent.Metadata property (idempotency support) - Stage S1 -- ✅ **BREAKING**: MessageBus → MessageProducer refactoring - Stage S3 -- ✅ **NEW**: IReconciler interface (system reconciliation) - Stage S2 -- ✅ **PATTERN**: Service Layer pattern (optional) - Stage S4 -- ✅ **FIX**: Thread safety (Lazy, ConcurrentQueue) - Stage S5 -- ✅ **FIX**: Null safety (22 checks added) - Stage S5 -- ✅ **FIX**: Equals/HashCode contract compliance - Stage S5 - -**Outcomes Achieved**: -- ✅ **562 tests passing** (100% pass rate, exceeded 520+ target) -- ✅ **~99% semantic parity** with Java 4.1.0 -- ✅ **Feature-complete codebase** for the first release (at the time; publication still pending) -- ✅ **All breaking changes** in initial release (no migration needed) -- ✅ **37 integration tests** for Java 4.1.0 features -- ✅ **Complete documentation** (README, CHANGELOG, API docs) - -**Final Status**: -- **Stage S6 Complete** (2026-01-11) -- **7/8 stages complete (87.5%)** - S7 deferred to post-1.0.0 -- **4/4 architecture ADRs complete** (ADRs 0024-0027) -- **562 tests passing** (500→562, +37 integration tests, +25 other tests, 100% pass rate) - ---- - -## ⚠️ Pending Tasks (Post-Phase 6) - -### ✅ COMPLETE: Remove Repository MessageProducer Integration - -**Date Identified**: 2026-01-13 (Feature Parity Verification) -**Status**: ✅ **COMPLETE** — implemented in `9e7e842` / `c06ec24` / `70ee225` / `bb7f0e4`; verified 2026-07-04 during Phase 7 planning (semantic parity with the Java Relay pattern achieved) -**Priority**: 🔴 HIGH - Blocks 100% semantic parity → **RESOLVED** -**Estimated Effort**: 1.5-2 hours - -**Issue**: C# Repository classes (EsRepository, OutboxRepository) have optional MessageProducer integration for direct event publishing, but Java 4.1.0 does NOT. Java uses independent Relay pattern to strictly follow Transactional Outbox Pattern. - -**Current Semantic Parity**: ~85-90% (serious architectural difference) -**Target Semantic Parity**: **100%** (after removal) - -**Decision**: Remove MessageProducer integration from Repository, implement EventStoreRelay example matching Java 4.1.0. - -**Rationale**: See [ADR-0025](docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md) and [ADR-0029](docs/adr/0029-messageproducer-removal-gateway-deferral.md) — why the Relay pattern is required and how Repository was decoupled from MessageProducer. - -**Files to Modify**: -- `src/EzDdd.UseCase/Port/Out/EsRepository.cs` (remove eventProducer) -- `src/EzDdd.UseCase/Port/Out/OutboxRepository.cs` (remove eventProducer) -- `tests/EzDdd.UseCase.Tests/Integration/CrossComponentIntegrationTests.cs` (update tests) -- `docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md` (update example) - -**Files to Create**: -- `examples/EventInfrastructure/EventStoreRelay.cs` (NEW - Relay implementation) -- `examples/EventInfrastructure/IEventStore.cs` (NEW - Interface) -- `examples/EventInfrastructure/InMemoryEventStore.cs` (NEW - Test implementation) -- `examples/EventInfrastructure/README.md` (NEW - Usage guide) - -**Success Criteria**: -- [x] Repository classes have NO MessageProducer dependency ✅ (verified 2026-07-04) -- [x] EventStoreRelay example implemented ✅ (`examples/EventInfrastructure/`) -- [x] All tests passing ✅ (543 tests as of 2026-07-04; MessageProducer tests removed from core in Phase 7 I3) -- [x] Semantic parity restored with the Java Relay pattern ✅ (re-verified against Java 6.0.1 in Phase 7) - ---- - -*Last updated: 2026-07-04 (Phase 7 complete: synchronized to Java ezddd 6.0.1 `3aac0f5`; closed MessageProducer removal task)* +See @AGENTS.md for all project context and development standards. diff --git a/README.md b/README.md index 0bc82dd..3abd4c4 100644 --- a/README.md +++ b/README.md @@ -1155,20 +1155,14 @@ public sealed class BankAccount : EsAggregateRoot90% unit test coverage 6. Update ADRs and documentation when making architectural changes @@ -1277,7 +1271,7 @@ See [THIRD-PARTY-NOTICES.txt](THIRD-PARTY-NOTICES.txt) for the required attribut - 📖 **Documentation**: See [docs/](docs/) directory for comprehensive guides - 🐛 **Bug Reports**: [GitHub Issues](https://github.com/cwouyang/ezDDD.NET/issues) -- 💡 **Feature Requests**: [GitHub Discussions](https://github.com/cwouyang/ezDDD.NET/discussions) +- 💡 **Feature Requests**: [GitHub Issues](https://github.com/cwouyang/ezDDD.NET/issues) - 💬 **Questions**: [Stack Overflow](https://stackoverflow.com/questions/tagged/ezddd-dotnet) (tag: `ezddd-dotnet`) --- diff --git a/ROADMAP.md b/ROADMAP.md index a756e7d..9a1e6d6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,657 +1,25 @@ -# ezDDD.NET Development Roadmap +# ezDDD.NET Roadmap **Project**: ezDDD.NET - Tactical DDD Framework for .NET -**Status**: first public release in preparation (not yet published to NuGet) -**Target Release**: 1.0.0 (based on Java ezddd 6.0.1 after Phase 7) -**Last Updated**: 2026-07-04 --- -## 📌 Project Overview +## Current Status -**ezDDD.NET** is a .NET port of the Java ezddd library, providing tactical Domain-Driven Design (DDD) patterns, Command Query Responsibility Segregation (CQRS), and Clean Architecture (CA) support. It supports both **state sourcing** and **event sourcing** for implementing aggregates and repositories. +ezDDD.NET is a .NET port of the Java [ezddd](https://gitlab.com/TeddyChen/ezddd) library, providing tactical Domain-Driven Design (DDD) patterns, Command Query Responsibility Segregation (CQRS), and Clean Architecture support. It supports both **state sourcing** and **event sourcing** for implementing aggregates and repositories. -### Key Metrics -- **Based on**: Java ezddd 6.0.1 (commit `3aac0f5`) - ✅ Synchronized 2.1.0 → 4.1.0 in Phase 6, 4.1.0 → 6.0.1 in Phase 7 -- **Language**: C# / .NET 8+ -- **Status**: first public release in preparation (not yet published to NuGet) -- **Test Coverage**: 543 tests passing (>90% coverage, 100% pass rate) -- **ADRs**: 29 architecture decisions documented -- **Semantic Parity**: ~99% with Java ezddd 6.0.1 achieved ✅ +- **Based on**: Java ezddd 6.0.1 (commit `3aac0f5`), ~99% semantic parity +- **Status**: first public release (1.0.0) in preparation - not yet published to NuGet +- **Modules**: ezDDD.Common, ezDDD.Entity, ezDDD.UseCase, ezDDD.Cqrs, ezDDD.Core (aggregator) +- **Architecture decisions**: documented as ADRs in [docs/adr/](docs/adr/) --- -## 🎯 Overall Progress +## Future Considerations (Post-1.0.0, Not Committed) -``` -Phase 1: EzDdd.Common ████████████████████ 100% ✅ Complete -Phase 2: EzDdd.Entity ████████████████████ 100% ✅ Complete -Phase 3: EzDdd.UseCase ████████████████████ 100% ✅ Complete -Phase 4: EzDdd.Cqrs ████████████████████ 100% ✅ Complete -Phase 5: EzDdd.Core + Docs ████████████████████ 100% ✅ Complete -Phase 6: Java 4.1.0 Sync ██████████████████░░ 87% ✅ S6 Complete (S7 deferred) - -Overall Progress: ███████████████████░ 98% (5.87/6 phases) -``` - -**Current Status** (2026-01-11): -- ✅ Phase 1-5 Complete (500 tests passing, 23 ADRs) -- ✅ Phase 6 Stage S6 Complete (562 tests, 27 ADRs, 37 integration tests) -- ✅ Phase 6 code complete (at the time); first public release still in preparation (S7 deferred to post-release) - ---- - -## 📅 Development Timeline - -### Phase 1: EzDdd.Common ✅ Complete -**Timeline**: 2025-10-31 (1 day) -**Status**: ✅ Complete - -#### Scope -Foundation utilities for the entire framework: -- BiMap (bidirectional mapping, thread-safe) -- IConverter (generic type conversion) -- JsonUtil (System.Text.Json utilities) - -#### Achievements -- ✅ 3 core components implemented -- ✅ 69 unit tests passing (>95% coverage) -- ✅ 6 ADRs completed (ADR-0001 to ADR-0006) -- ✅ Zero compiler warnings -- ✅ Thread-safe BiMap with lock-based synchronization - -#### ADRs -- ADR-0001: Target Framework (.NET 8.0+) -- ADR-0002: Package Naming (ezDDD vs. EzDdd) -- ADR-0003: Module Architecture -- ADR-0004: Zero Third-Party Dependencies -- ADR-0005: Complete Reimplementation Approach -- ADR-0006: uContract.NET Integration - ---- - -### Phase 2: EzDdd.Entity ✅ Complete -**Timeline**: 2025-11-01 (1 day) -**Status**: ✅ Complete - -#### Scope -Core DDD building blocks (entities layer): -- IEntity, IValueObject -- IDomainEvent hierarchy (Internal/External, Construction/Destruction) -- AggregateRoot (state sourcing) -- EsAggregateRoot (event sourcing with R1/R2/R3 rules) -- DomainEventTypeMapper (event serialization) - -#### Achievements -- ✅ 7 core components implemented -- ✅ 85 unit tests passing (>95% coverage) -- ✅ 5 ADRs completed (ADR-0007 to ADR-0011) -- ✅ R1/R2/R3 event sourcing correctness rules enforced -- ✅ ~95% semantic parity with Java ezddd - -#### ADRs -- ADR-0007: IEntity and IValueObject Design -- ADR-0008: IDomainEvent Hierarchy -- ADR-0009: AggregateRoot Base Class Design -- ADR-0010: EsAggregateRoot Event Sourcing Implementation -- ADR-0011: Event Replay and Invariant Checking (R1/R2/R3) - ---- - -### Phase 3: EzDdd.UseCase ✅ Complete -**Timeline**: 2025-11-05 to 2025-11-06 (2 days, 8 iterations) -**Status**: ✅ Complete - -#### Scope -Use cases layer with persistence abstractions: -- IUseCase, IInput, IOutput -- IRepository, IRepositoryPeer (Bridge pattern) -- EsRepository (event sourcing), OutboxRepository (state sourcing + outbox) -- DomainEventMapper, DomainEventData, StoreData -- IMessageBus, IReactor, IEventBusProducer (messaging) - -#### Achievements -- ✅ 25 core components + 6 integration test suites -- ✅ 279 unit tests passing (>95% coverage) -- ✅ 5 ADRs completed (ADR-0012 to ADR-0016) -- ✅ Bridge Pattern (IRepository ↔ IRepositoryPeer) -- ✅ Event Sourcing (EsRepository) with reflection + ConstructorInfo caching -- ✅ State Sourcing (OutboxRepository) with Transactional Outbox pattern -- ✅ Message Bus (BlockingMessageBus) with thread-safe snapshot -- ✅ ~98% semantic parity with Java ezddd - -#### Key Technical Decisions -- ConstructorInfo caching with ConcurrentDictionary (performance) -- IStoreData.Version type: int → long (Java parity) -- Event clearing: only after successful save (NOT on failure) -- IReactor async update: Execute() → ExecuteAsync() -- Events captured BEFORE SaveAsync() (repository clears after save) - -#### ADRs -- ADR-0012: Resource Management for Event Bus Producers -- ADR-0013: Transaction Boundaries in Repository Pattern -- ADR-0014: DomainEventData Equality Semantics -- ADR-0015: Cross-Platform DTO Structure -- ADR-0016: Async/Await Throughout - -#### Test Domain -Banking domain (BankAccount event-sourced aggregate, Money value object, 3 Use Cases) - ---- - -### Phase 4: EzDdd.Cqrs ✅ Complete -**Timeline**: 2025-11-10 to 2025-11-18 (9 days: 1 day planning + 7 iterations + 1 day review) -**Status**: ✅ Complete - -#### Scope -CQRS pattern separation: -- ICommand, IQuery -- IInquiry (validation queries) -- IProjection (read model builders) -- IProjector (background service marker) -- IArchive (query database interface) -- CqrsOutput (unified output with builder pattern) - -#### Achievements -- ✅ 9 core components implemented -- ✅ 68 unit tests passing (>90% coverage) -- ✅ 7 ADRs completed (ADR-0017 to ADR-0023) -- ✅ Complete CQRS flow integration tests (Command → Event → Projector → Archive → Query) -- ✅ Zero compiler warnings (fixed all XML documentation warnings) -- ✅ 42% ahead of schedule (~13/19 hours) - -#### Key Technical Achievements -- Fluent API CqrsOutput with builder pattern (SetXxx() methods) -- IArchive async methods throughout (FindByIdAsync, SaveAsync, DeleteAsync) -- IInquiry/IProjection independence (both extend IUseCase, usable independently or within commands) -- Complete event projection pattern (AccountProjector updating AccountSummaryReadModel) -- Marker interfaces for compile-time type safety (IInquiryInput, IProjectionInput) -- Generic variance annotations (contravariant input, covariant output) -- Idempotent archive operations (upsert semantics for SaveAsync) -- Read model design with C# record types - -#### Critical Design Decisions -- IProjector as pure marker interface (lifecycle managed separately via BackgroundService) -- IArchive operations MUST be idempotent for reliable event replay -- Read models use C# record types with positional parameters -- Generic variance: `ICommand`, `IArchive` -- IInquiry and IProjection are independent from each other (separate use cases) - -#### ADRs -- ADR-0017: CqrsOutput Implementation Strategy -- ADR-0018: IArchive Async Method Design -- ADR-0019: IInquiry and IProjection Independence -- ADR-0020: IProjector Lifecycle Management -- ADR-0021: Generic Variance Annotations -- ADR-0022: Read Model Design Patterns -- ADR-0023: Archive Idempotency Requirements - ---- - -### Phase 5: EzDdd.Core + Documentation ✅ Complete -**Timeline**: 2025-11-22 (1 day, 4 iterations) -**Status**: ✅ Complete - -#### Scope -Aggregator module and comprehensive documentation: -- EzDdd.Core (meta-package referencing all modules) -- README.md (11,212+ lines comprehensive guide) -- CHANGELOG.md (version history) -- XML documentation for all public APIs -- NuGet package preparation (5 packages) - -#### Achievements -- ✅ 11,212+ lines of documentation -- ✅ 5 NuGet packages created: - - ezDDD.Common (35K) - - ezDDD.Entity (41K) - - ezDDD.UseCase (63K) - - ezDDD.Cqrs (37K) - - ezDDD.Core (28K - meta-package) -- ✅ Version 1.0.0-alpha.1 (internal, not published) -- ✅ Complete documentation ecosystem -- ✅ Based on Java ezddd 2.1.0 - -#### Documentation Deliverables -- Comprehensive README with architecture diagrams -- Quick start guides and usage examples -- Complete API reference -- CHANGELOG for version tracking -- All public APIs have XML documentation - ---- - -### Phase 6: Java 4.1.0 Synchronization 🚀 In Progress -**Timeline**: 2026-01-06 to TBD (44-62 hours / ~1-2 weeks, 8 stages) -**Status**: 🚀 Stage S5 Complete - Thread/Null Safety Review (6/8 stages, 75%) - -#### Objective -Synchronize ezDDD.NET from Java ezddd 2.1.0 (commit `6e94aee`) to Java ezddd 4.1.0 (commit `91fac63`) **before first NuGet publication**. - -This ensures users receive a complete, up-to-date API aligned with Java 4.1.0 from day one, with all breaking changes incorporated into the initial 1.0.0 release. - -#### Scope -**44 commits to synchronize** (+5,167 lines, -2,132 lines) - -**Major Changes**: -1. ⚠️ **BREAKING**: IDomainEvent.Metadata property (idempotency support) -2. ⚠️ **BREAKING**: MessageBus → MessageProducer refactoring -3. ✨ **NEW**: IReconciler interface -4. 🔄 **REFACTOR**: Service Layer pattern (optional) -5. 🐛 **FIX**: Thread safety (DomainEventTypeMapper, BlockingMessageBus) -6. 🐛 **FIX**: Null safety (comprehensive validation) -7. 🐛 **FIX**: Equals/HashCode contract compliance - -#### Work Breakdown (8 Stages) - -##### Stage S0: Planning & Preparation (4-6 hours) ✅ Complete -**Status**: ✅ Complete (2026-01-06) -**Actual Time**: ~2-3 hours -**Tasks**: -- ✅ Finalize synchronization plan (DOTNET_PORT.md) -- ✅ Create ADR roadmap (ADR_PLANNING.md - 7 ADRs planned) -- ✅ Update ROADMAP.md with Phase 6 milestones (this file) -- ✅ Review Java 4.1.0 commit history (44 commits, 20/20 key commits verified) -- ✅ Migration guide decision (not needed - pre-publication sync) - -**Deliverables**: -- ✅ DOTNET_PORT.md with Phase 6 plan (verified complete) -- ✅ ADR_PLANNING.md created with ADRs 0024-0030 (7 ADRs) -- ✅ ROADMAP.md created with Phase 6 schedule (this file) -- ✅ Java 4.1.0 commit review complete - ---- - -##### Stage S1: IDomainEvent.Metadata (6-8 hours) ✅ Complete -**Status**: ✅ Complete (2026-01-06) -**ADR**: ADR-0008 (Metadata design already documented in IDomainEvent Hierarchy ADR) -**Priority**: Breaking Change (already implemented) -**Actual Time**: ~1.5 hours (implementation pre-existing, added tests only) - -**Completed Tasks**: -1. ✅ IDomainEvent interface with Metadata (pre-existing) -2. ✅ All domain events implement Metadata (pre-existing) -3. ✅ DomainEventData with UserMetadata field (pre-existing) -4. ✅ DomainEventMapper serialization (pre-existing) -5. ✅ All test events updated (pre-existing) -6. ✅ **Added**: DomainEventMetadataTests.cs (15 comprehensive tests) - -**Key Decision**: -- 📝 ADR-0024 not needed - Metadata design fully documented in ADR-0008 (IDomainEvent Hierarchy) -- ✅ Metadata is intrinsic part of IDomainEvent design, not a separate feature -- ✅ This stage added test coverage, not design decisions - -**Outcome**: -- ✅ IDomainEvent interface has Metadata property -- ✅ All 516 tests passing (501 existing + 15 new) -- ✅ 15 new metadata tests covering: serialization, equality, special cases, event types, immutability -- ✅ Zero compiler warnings -- ✅ Metadata functionality verified and tested - ---- - -##### Stage S2: IReconciler Interface (4-6 hours) ✅ Complete -**Status**: ✅ Complete (2026-01-07) -**ADR**: [ADR-0024](docs/adr/0024-ireconciler-interface-system-reconciliation.md) (🟢 NEW FEATURE) -**Priority**: Non-Breaking Addition -**Actual Time**: ~5 hours - -**Completed Tasks**: -1. ✅ Created IReconciler interface -2. ✅ Created NullContext singleton class -3. ✅ Written comprehensive XML documentation (375 lines ADR) -4. ✅ Created example reconciler (CleanUpExpiredOrdersReconciler) -5. ✅ Written 12 reconciler tests (organized with C# regions) -6. ✅ Updated README.md (added System Reconciliation Example section) - -**Purpose**: -- ✅ System state reconciliation (cleanup orphaned workflows) -- ✅ Data consistency enforcement -- ✅ Periodic maintenance tasks -- ✅ Business rule enforcement - -**Success Criteria**: -- [x] IReconciler interface created in EzDdd.UseCase -- [x] Example reconciler implemented with tests -- [x] 12 tests passing (exceeded 10+ target) -- [x] ADR-0024 written and accepted (375 lines) -- [x] Documentation updated (README, DOTNET_PORT, ROADMAP, ADR index) - ---- - -##### Stage S3: MessageProducer Refactoring (10-14 hours) ✅ Complete -**Status**: ✅ Complete (2026-01-07) -**ADR**: [ADR-0025](docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md) (🔴 BREAKING) -**Priority**: Breaking Change (Most Complex) -**Actual Time**: ~3 hours (70% faster than estimated, simplified strategy) - -**Completed Tasks**: -1. ✅ Updated IMessageProducer documentation (pure producer pattern) -2. ✅ Created InMemoryMessageProducer implementation (17 tests) -3. ✅ Created PostEventFailureException (3 tests) -4. ✅ Removed IMessageBus, BlockingMessageBus, EventBusProducer (9 files) -5. ✅ Removed IReactor and GenericReactor (subscription management) -6. ✅ Updated EsRepository with optional eventProducer parameter -7. ✅ Updated OutboxRepository with optional eventProducer parameter -8. ✅ Updated all integration tests (CrossComponentIntegrationTests, CompleteCqrsFlowTests) -9. ✅ Removed obsolete MessageBusIntegrationTests.cs -10. ✅ Updated AccountProjector (removed IReactor dependency) -11. ✅ Written ADR-0025 (complete documentation) - -**Impact**: -- ⚠️ BREAKING: IMessageBus removed entirely (clean break, not published yet) -- ✅ Pure producer pattern aligned with Java 4.1.0 -- ✅ Cleaner separation of concerns -- ✅ Simplified API surface (only PostAsync + IDisposable) -- ✅ Application layer handles subscription management - -**Migration Strategy Applied**: -- ✅ **Clean Break** (Option A): Removed IMessageBus immediately -- ✅ No backward compatibility needed (not yet published) -- ✅ Users get clean API from day one (1.0.0) - -**Outcome**: -- [x] IMessageProducer simplified to pure producer pattern -- [x] InMemoryMessageProducer implemented (17 tests) -- [x] PostEventFailureException created (3 tests) -- [x] All repositories updated with optional eventProducer -- [x] All 487 tests passing (100% pass rate) -- [x] 20 new MessageProducer tests passing -- [x] ADR-0025 written and accepted -- [x] Documentation updated with new pattern -- [x] Zero compiler warnings - ---- - -##### Stage S4: Service Layer Pattern (6-8 hours) ✅ Complete -**Status**: ✅ Complete (2026-01-08) -**ADR**: [ADR-0026](docs/adr/0026-service-layer-pattern.md) (🟡 OPTIONAL) -**Priority**: Non-Breaking Guidance -**Actual Time**: ~6 hours (3 iterations) - -**Completed Tasks**: -1. ✅ Document Service Layer pattern -2. ✅ Create pattern documentation (docs/patterns/SERVICE_LAYER_PATTERN.md, 564 lines) -3. ✅ Create example service (TransferMoneyService + 6 support classes, 585 lines) -4. ✅ Refactor complex Use Case example (Before/After comparison, 672 lines) -5. ✅ Write 13 unit tests (all passing) -6. ✅ Write ADR-0026 (complete documentation) -7. ✅ Update architecture documentation (ROADMAP, DOTNET_PORT, CLAUDE, ADR index) - -**Pattern**: -Extract business logic >20 lines from UseCases to Service classes for reusability and testability. - -**Success Criteria**: -- [x] SERVICE_LAYER_PATTERN.md created (564 lines) -- [x] Example service implemented with 13 tests (all passing) -- [x] Before/after comparison documented (672 lines) -- [x] ADR-0026 written and accepted -- [x] Documentation updated (README, DOTNET_PORT, CLAUDE, ADR index) - -**Outcome**: -- ✅ Complete pattern documentation (SERVICE_LAYER_PATTERN.md) -- ✅ Complete Before/After comparison (SERVICE_LAYER_BEFORE_AFTER.md) -- ✅ TransferMoneyService example (interface + impl + 6 support types + 13 tests) -- ✅ ADR-0026 accepted (Service Layer Pattern) - ---- - -##### Stage S5: Thread/Null Safety Review (6-8 hours) ⏳ Pending -**Status**: ⏳ Pending (depends on all core changes) -**ADR**: ADR-0027 (🔵 QUALITY) -**Priority**: Quality Improvement - -**Tasks**: -1. Review all concurrent collections and static state -2. Fix thread safety in DomainEventTypeMapper (use Lazy) -3. Fix thread safety in BlockingMessageBus (if retained) -4. Review InMemoryOutboxStore concurrent access -5. Add ArgumentNullException.ThrowIfNull() to all public APIs -6. Review equals/hashCode in record types (verify correctness) -7. Run static analysis tools (Roslyn, ReSharper, SonarLint) -8. Write 10+ thread safety tests - -**Code Review Checklist**: -- [ ] DomainEventTypeMapper initialization (thread-safe) -- [ ] BlockingMessageBus (thread-safe or removed) -- [ ] InMemoryOutboxStore (concurrent access safe) -- [ ] All public API methods have null validation -- [ ] Nullable reference types enabled in all projects -- [ ] Record types equals/hashCode verified - -**Success Criteria**: -- [ ] All thread safety issues fixed -- [ ] Comprehensive null checks in public APIs -- [ ] 10+ thread safety tests passing -- [ ] Zero static analysis warnings -- [ ] ADR-0027 written and accepted - ---- - -##### Stage S6: Integration Testing & Documentation (6-8 hours) ⏳ Pending -**Status**: ⏳ Pending (depends on all stages) -**ADR**: ADR-0028 (🟠 PROCESS) -**Priority**: Integration & Documentation - -**Tasks**: -1. Write comprehensive integration tests (all Java 4.1.0 features) -2. Full CQRS flow test with Metadata + Reconciler -3. Update README.md (Java 4.1.0 features) -4. Update CHANGELOG.md (1.0.0 release notes based on Java 4.1.0) -5. Update all API documentation -6. Update architecture diagrams -7. Verify 520+ tests passing (>95% coverage) - -**Integration Test Scenarios**: -- Full CQRS flow with metadata -- Event replay with metadata -- Reconciler execution -- MessageProducer resource cleanup -- Concurrent operations (thread safety) - -**Success Criteria**: -- [ ] 520+ tests passing (>95% coverage) -- [ ] Complete integration test suite -- [ ] README updated with Java 4.1.0 features -- [ ] CHANGELOG shows 1.0.0 based on Java 4.1.0 -- [ ] ADR-0028 written and accepted -- [ ] All documentation accurate and complete - ---- - -##### Stage S7: Final Review & Completion (2-4 hours) ⏳ Pending -**Status**: ⏳ Pending (final stage) -**ADRs**: ADR-0029 (📊 REVIEW), ADR-0030 (✅ VERIFICATION) -**Priority**: Final Review & Release Prep - -**Tasks**: -1. Run full test suite (verify 520+ tests passing) -2. Verify zero compiler warnings -3. Code review all Phase 6 changes -4. Update ROADMAP.md (Phase 6 complete) -5. Update CLAUDE.md (Java 2.1.0 → 4.1.0 references) -6. Update README.md (Java version reference) -7. Update all internal documentation -8. Create git tag for 1.0.0 -9. Prepare NuGet packages (5 packages) -10. Write ADR-0029 (post-implementation review) -11. Write ADR-0030 (feature parity verification) - -**Quality Gates**: -- [ ] All tests passing (520+ tests, >95% coverage) -- [ ] Zero compiler warnings -- [ ] Zero static analysis errors -- [ ] All 7 ADRs written and reviewed (ADR-0024 to ADR-0030) -- [ ] Documentation complete and accurate -- [ ] CHANGELOG.md reflects 1.0.0 based on Java 4.1.0 -- [ ] All "Java 2.1.0" references updated to "Java 4.1.0" -- [ ] Feature parity ~99% verified - -**Deliverables**: -- ✅ ezDDD.NET first-release packages prepared (based on Java ezddd 4.1.0 at the time) -- ✅ Complete documentation (README, CHANGELOG, API docs) -- ✅ 8 ADRs (ADR-0024 to ADR-0030) -- ✅ ~99% semantic parity with Java ezddd 4.1.0 - ---- - -#### Phase 6 Progress Tracking - -| Stage | Description | Hours | Status | Start Date | End Date | -|-------|-------------|-------|--------|------------|----------| -| S0 | Planning & Preparation | 4-6 (2-3) | ✅ Complete | 2026-01-06 | 2026-01-06 | -| S1 | IDomainEvent.Metadata | 6-8 (1.5) | ✅ Complete | 2026-01-06 | 2026-01-06 | -| S2 | IReconciler Interface | 4-6 (5) | ✅ Complete | 2026-01-07 | 2026-01-07 | -| S3 | MessageProducer Refactoring | 10-14 (3) | ✅ Complete | 2026-01-07 | 2026-01-07 | -| S4 | Service Layer Pattern | 6-8 (6) | ✅ Complete | 2026-01-08 | 2026-01-08 | -| S5 | Thread/Null Safety Review | 6-8 (6) | ✅ Complete | 2026-01-08 | 2026-01-08 | -| S6 | Integration Testing & Docs | 6-8 | ⏳ Pending | TBD | TBD | -| S7 | Final Review & Completion | 2-4 | ⏳ Pending | TBD | TBD | -| **TOTAL** | | **44-62 (23.5)** | **6/8 stages** | | | - -**Current Progress**: Stage S5 Complete - 6/8 stages complete (75%) - ---- - -#### Phase 6 Success Criteria - -**Phase 6 Complete When**: -- [ ] All 8 stages (S0-S7) completed -- [ ] All 7 ADRs written and reviewed (ADR-0024 to ADR-0030) -- [ ] All Java 4.1.0 features implemented and tested -- [ ] 520+ tests passing (>95% coverage) -- [ ] Zero compiler warnings -- [ ] Zero static analysis errors -- [ ] All "Java 2.1.0" references updated to "Java 4.1.0" -- [ ] Documentation updated (README, CHANGELOG, API docs) -- [ ] NuGet packages ready for 1.0.0 release (5 packages) -- [ ] Feature parity with Java 4.1.0 verified (~99%) -- [ ] ROADMAP.md and CLAUDE.md reflect Java 4.1.0 base - ---- - -## 📊 Overall Project Metrics - -### Test Coverage Progress -``` -Phase 1: 69 tests ████████████████████ 100% -Phase 2: 85 tests ████████████████████ 100% -Phase 3: 279 tests ████████████████████ 100% -Phase 4: 68 tests ████████████████████ 100% -Phase 5: 0 tests (documentation only) -Phase 6: +14 tests ██████████████░░░░░░ 70% (S1: 0, S5: +14 concurrency tests) - -Total: 486 → 500 ████████████████████ 98.6% pass rate -``` - -### ADR Progress -``` -Stage 1: 6 ADRs ████████████████████ 100% ✅ -Stage 2: 5 ADRs ████████████████████ 100% ✅ -Stage 3: 5 ADRs ████████████████████ 100% ✅ -Stage 4: 3 ADRs ████████████████████ 100% ✅ -Stage 5: 4 ADRs ████████████████████ 100% ✅ -Stage 6: 4/7 ADRs ███████████░░░░░░░░░ 57% 🚀 (ADR-0024 to 0027) - -Total: 27/30 ADRs ██████████████████░░ 90% -``` - -### Module Completion -- ✅ **EzDdd.Common** - 100% (69 tests) -- ✅ **EzDdd.Entity** - 100% (85 tests) -- ✅ **EzDdd.UseCase** - 100% (279 tests) -- ✅ **EzDdd.Cqrs** - 100% (68 tests) -- ✅ **EzDdd.Core** - 100% (meta-package + docs) -- 🔄 **All Modules** - Java 4.1.0 Sync In Progress (0/8 stages) - ---- - -## 🎯 Release Planning - -### Version 1.0.0-alpha.1 (Internal) -**Status**: ✅ Created (not published) -**Date**: 2025-11-22 -**Based on**: Java ezddd 2.1.0 (commit `6e94aee`) -**Purpose**: Internal testing and validation - -**Packages**: -- ezDDD.Common 1.0.0-alpha.1 (35K) -- ezDDD.Entity 1.0.0-alpha.1 (41K) -- ezDDD.UseCase 1.0.0-alpha.1 (63K) -- ezDDD.Cqrs 1.0.0-alpha.1 (37K) -- ezDDD.Core 1.0.0-alpha.1 (28K) - ---- - -### Version 1.0.0 (Target - First Public Release) -**Status**: ⏳ In Development (Phase 6 S0) -**Target Date**: TBD (after Phase 6 completion, ~44-62 hours / 1-2 weeks) -**Based on**: Java ezddd 4.1.0 (commit `91fac63`) -**Purpose**: First public release on NuGet - -**Strategy**: Pre-Publication Synchronization -- ✅ Incorporate all Java 4.1.0 changes (including breaking changes) into **initial 1.0.0 release** -- ✅ Users get up-to-date API from day one -- ✅ No migration needed (users never see Java 2.1.0 API) - -**Target Metrics**: -- 520+ tests passing (>95% coverage) -- 31 ADRs complete -- ~99% semantic parity with Java ezddd 4.1.0 -- Zero compiler warnings -- Zero static analysis errors - -**Packages**: -- ezDDD.Common 1.0.0 -- ezDDD.Entity 1.0.0 -- ezDDD.UseCase 1.0.0 -- ezDDD.Cqrs 1.0.0 -- ezDDD.Core 1.0.0 - ---- - -## 🔧 Post-Phase 6 Refinements - -### Task: Remove Repository MessageProducer Integration ⏳ Pending - -**Date Identified**: 2026-01-13 -**Priority**: 🔴 HIGH - Affects semantic parity -**Status**: ⏳ Pending (handoff document ready) -**Estimated Effort**: 1.5-2 hours -**Impact**: Improves semantic parity from ~85-90% to **100%** - -**Issue**: C# Repository classes have optional MessageProducer integration for direct event publishing, but Java 4.1.0 does NOT have this design. Java uses independent Relay pattern to strictly follow Transactional Outbox Pattern. - -**Decision**: Remove MessageProducer integration from Repository, implement EventStoreRelay example to match Java 4.1.0. - -**Work Breakdown**: -1. Remove eventProducer parameter from EsRepository (15 min) -2. Remove eventProducer parameter from OutboxRepository (15 min) -3. Create EventStoreRelay example implementation (30 min) -4. Update integration tests to use Relay pattern (30 min) -5. Update documentation (ADR-0025, README, CHANGELOG) (15 min) -6. Verify build and semantic parity (10 min) - -**Success Criteria**: -- [ ] Repository classes have NO MessageProducer dependency -- [ ] EventStoreRelay example implemented (examples/EventInfrastructure/) -- [ ] All tests passing (562 tests) -- [ ] Documentation updated -- [ ] Semantic parity: **100%** with Java 4.1.0 - -**Related Documents**: -- [ADR-0025](docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md) - Why Relay is required -- [ADR-0029](docs/adr/0029-messageproducer-removal-gateway-deferral.md) - MessageProducer removal rationale and design comparison -- Java Reference: `ezddd/ezddd-core-sample/src/.../InMemoryEventStoreRelay.java` - ---- - -## 🚀 Future Considerations (Post-1.0.0) - -### Potential Future Work (Not Committed) - **ezDDD.Gateway package** — port of the upstream ezddd-gateway artifact (`MessageProducer` - moved out of core in Java 6.0.0); deferred to post-1.0 (see ADR-0029) -- **Phase 6 Stage S7** — final review & completion pass (deferred to post-release maintenance) + moved out of core in Java 6.0.0); deferred to post-1.0 (see [ADR-0029](docs/adr/0029-messageproducer-removal-gateway-deferral.md)) +- **Post-release review pass** — final review & completion pass deferred to post-release maintenance - **Performance** — benchmarks and optimization profiles (Expression Trees instead of reflection, AOT support, ValueTask for hot paths) - **Event store adapters** — EventStoreDB, Marten @@ -660,35 +28,13 @@ Total: 27/30 ADRs ██████████████████░░ - **Additional examples** — e-commerce, reservation system, gRPC integration - **Advanced event sourcing** — snapshots, temporal queries, saga pattern support -### Java ezddd Tracking -- Monitor Java ezddd releases (post-6.0.1) -- Assess need for additional synchronization -- Maintain ~99% semantic parity - ---- - -## 📚 Related Documents - -### Planning Documents -- **[DOTNET_PORT.md](DOTNET_PORT.md)** - Technical planning and API design -- **[CLAUDE.md](CLAUDE.md)** - Development guidance for AI assistant -- **[ROADMAP.md](ROADMAP.md)** - This file (development roadmap) - -### ADR Documents -- **[docs/adr/ADR_PLANNING.md](docs/adr/ADR_PLANNING.md)** - ADR roadmap (31 ADRs) -- **[docs/adr/README.md](docs/adr/README.md)** - ADR index and workflow -- **[docs/adr/](docs/adr/)** - Individual ADR files (ADR-0001 to ADR-0023 complete) - --- -## 📞 Project Contacts +## Java Upstream Tracking -**Project Lead**: TeddyChen -**Repository**: [GitHub - cwouyang/ezDDD.NET](https://github.com/cwouyang/ezDDD.NET) -**Java Version**: [GitLab - TeddyChen/ezddd](https://gitlab.com/TeddyChen/ezddd) +Monitor Java ezddd releases (post-6.0.1) and synchronize as needed to maintain ~99% semantic parity. --- -**Last Updated**: 2026-01-08 (Phase 6 Stage S5 Complete - 75% Progress) - -_This roadmap is a living document and will be updated as the project progresses through Phase 6._ +**Repository**: [github.com/cwouyang/ezDDD.NET](https://github.com/cwouyang/ezDDD.NET) +**Java upstream**: [gitlab.com/TeddyChen/ezddd](https://gitlab.com/TeddyChen/ezddd) diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md index f43ecae..0c14f4a 100644 --- a/docs/MIGRATION_GUIDE.md +++ b/docs/MIGRATION_GUIDE.md @@ -1399,7 +1399,6 @@ public class SqlRepositoryPeer : IRepositoryPeer ## Further Reading -- **DOTNET_PORT.md** - Technical design decisions and architecture - **ROADMAP.md** - Development progress and milestones - **ADR Index** (docs/adr/README.md) - Architecture Decision Records diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index 374ac61..8c5d0ca 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -219,19 +219,8 @@ Complete checklist for releasing ezDDD.NET to NuGet. - [ ] Quick start examples use correct version - [ ] **Update `ROADMAP.md`** - - [ ] Phase 5 marked complete ✅ - - [ ] Overall progress updated to 100% - - [ ] Final statistics recorded (501 tests, 5 modules, 44 APIs) - - [ ] Release date recorded - -- [ ] **Update `CLAUDE.md`** - - [ ] Project status updated to "Released 1.0.0-alpha.1" - - [ ] Current version updated to 1.0.0-alpha.1 - - [ ] Last updated date set to release date - -- [ ] **Update `DOTNET_PORT.md`** (if exists) - - [ ] Port status updated to "Complete" - - [ ] Version information updated + - [ ] Project status updated to the released version + - [ ] Last updated information reflects the release ### Update Module-Specific Documentation diff --git a/docs/adr/0001-target-framework.md b/docs/adr/0001-target-framework.md index f958f5c..3d00f56 100644 --- a/docs/adr/0001-target-framework.md +++ b/docs/adr/0001-target-framework.md @@ -234,7 +234,7 @@ dotnet --list-sdks - [System.Text.Json Overview](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview) - [Nullable Reference Types](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references) - [Records in C#](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record) -- [DOTNET_PORT.md](../../DOTNET_PORT.md) +- Internal porting notes (not retained) - [uContract.NET ADR-0001](../../../uContract.NET/docs/adr/0001-target-framework.md) - Similar decision for ecosystem consistency --- diff --git a/docs/adr/0002-package-naming-and-structure.md b/docs/adr/0002-package-naming-and-structure.md index 56e4188..4edb782 100644 --- a/docs/adr/0002-package-naming-and-structure.md +++ b/docs/adr/0002-package-naming-and-structure.md @@ -215,7 +215,7 @@ ezDDD.sln (Solution file) ## References -- [DOTNET_PORT.md - 命名決策](../../DOTNET_PORT.md#0-命名決策-已確認-2025-10-28) +- Internal porting notes (not retained) - 命名決策 - [.NET Naming Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines) - [NuGet Package ID Guidelines](https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package#choose-a-unique-package-identifier-and-version-number) - [Java ezddd Repository](https://gitlab.com/TeddyChen/ezddd) diff --git a/docs/adr/0003-module-architecture-dependency-chain.md b/docs/adr/0003-module-architecture-dependency-chain.md index 503179e..698c3e6 100644 --- a/docs/adr/0003-module-architecture-dependency-chain.md +++ b/docs/adr/0003-module-architecture-dependency-chain.md @@ -523,7 +523,7 @@ Minimize breaking changes in foundational modules (Common, Entity). - [Clean Architecture (Robert C. Martin)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Java ezddd Module Structure](https://gitlab.com/TeddyChen/ezddd) -- [DOTNET_PORT.md - 模組架構](../../DOTNET_PORT.md#模組架構) +- Internal porting notes (not retained) - 模組架構 - [ADR-0002: Package Naming and Structure](0002-package-naming-and-structure.md) - [.NET Library Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/) diff --git a/docs/adr/0004-zero-third-party-dependency-principle.md b/docs/adr/0004-zero-third-party-dependency-principle.md index 28fd432..b899b09 100644 --- a/docs/adr/0004-zero-third-party-dependency-principle.md +++ b/docs/adr/0004-zero-third-party-dependency-principle.md @@ -393,7 +393,7 @@ If users request features requiring third-party libraries: - [NuGet Package Dependencies](https://learn.microsoft.com/en-us/nuget/consume-packages/dependency-resolution) - [.NET API Browser](https://learn.microsoft.com/en-us/dotnet/api/) - [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview) -- [DOTNET_PORT.md - 最小依賴原則](../../DOTNET_PORT.md#2-最小依賴原則) +- Internal porting notes (not retained) - 最小依賴原則 - [uContract.NET ADR-0011: Zero-Dependency Principle](../../../uContract.NET/docs/adr/0011-zero-dependency-principle.md) - [Java ezddd Dependencies](https://gitlab.com/TeddyChen/ezddd) diff --git a/docs/adr/0005-complete-reimplementation-approach.md b/docs/adr/0005-complete-reimplementation-approach.md index 9d4731a..3b2d51d 100644 --- a/docs/adr/0005-complete-reimplementation-approach.md +++ b/docs/adr/0005-complete-reimplementation-approach.md @@ -279,8 +279,8 @@ public class CreateWorkflowHandler : IRequestHandler entity = new Order(); // Works due to ## References - **Java ezddd Source**: - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\Entity.java` - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\ValueObject.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/Entity.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/ValueObject.java` - **DDD Reference**: - Evans, Eric. *Domain-Driven Design: Tackling Complexity in the Heart of Software*. Addison-Wesley, 2003. @@ -378,7 +378,7 @@ IEntity entity = new Order(); // Works due to - [Covariance and Contravariance](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/covariance-contravariance/) - **Internal Documents**: - - [DOTNET_PORT.md](../../DOTNET_PORT.md) - Section "API 設計 > 核心介面 > 實體層" + - Internal porting notes (not retained) - Section "API 設計 > 核心介面 > 實體層" - [CLAUDE.md](../../CLAUDE.md) - Section "Module Architecture > EzDdd.Entity" --- diff --git a/docs/adr/0008-idomain-event-hierarchy.md b/docs/adr/0008-idomain-event-hierarchy.md index 64719c6..da66a4f 100644 --- a/docs/adr/0008-idomain-event-hierarchy.md +++ b/docs/adr/0008-idomain-event-hierarchy.md @@ -423,7 +423,7 @@ public interface IDomainEvent - **Related to**: - [ADR-0009: AggregateRoot Base Class Design](0009-aggregate-root-base-class-design.md) - Aggregates raise internal domain events - [ADR-0010: EsAggregateRoot Event Sourcing Implementation](0010-esaggregate-root-event-sourcing-implementation.md) - R1/R2/R3 rules depend on construction/destruction markers - - [ADR-0013: Record Types for Immutability](planned) - Events should be implemented as records + - ADR-0013: Record Types for Immutability (planned) - Events should be implemented as records --- @@ -506,9 +506,9 @@ public void OrderCancelled_ImplementsDestructionEvent() ## References - **Java ezddd Source**: - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\DomainEvent.java` - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\InternalDomainEvent.java` - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\ExternalDomainEvent.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/DomainEvent.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/InternalDomainEvent.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/ExternalDomainEvent.java` - **Event Sourcing**: - Vernon, Vaughn. *Implementing Domain-Driven Design*. Addison-Wesley, 2013. Chapter 8: "Domain Events" @@ -521,7 +521,7 @@ public void OrderCancelled_ImplementsDestructionEvent() - [Pattern Matching](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching) - **Internal Documents**: - - [DOTNET_PORT.md](../../DOTNET_PORT.md) - Section "核心介面 > 實體層 > IDomainEvent" + - Internal porting notes (not retained) - Section "核心介面 > 實體層 > IDomainEvent" - [CLAUDE.md](../../CLAUDE.md) - Section "Module Architecture > EzDdd.Entity" --- diff --git a/docs/adr/0009-aggregate-root-base-class-design.md b/docs/adr/0009-aggregate-root-base-class-design.md index 7a4d4fd..e80199c 100644 --- a/docs/adr/0009-aggregate-root-base-class-design.md +++ b/docs/adr/0009-aggregate-root-base-class-design.md @@ -439,7 +439,7 @@ public interface IAggregateRoot : IEntity - **Related to**: - [ADR-0010: EsAggregateRoot Event Sourcing Implementation](0010-esaggregate-root-event-sourcing-implementation.md) - Extends AggregateRoot - - [ADR-0011: Async/Await Throughout](planned) - Why Apply() is synchronous + - ADR-0011: Async/Await Throughout (planned) - Why Apply() is synchronous --- @@ -562,8 +562,8 @@ public void AggregateRoot_ThreadSafe_ConcurrentEventAddition() ## References - **Java ezddd Source**: - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\AggregateRoot.java` - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\DomainEventSource.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/AggregateRoot.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/DomainEventSource.java` - **DDD Patterns**: - Evans, Eric. *Domain-Driven Design*. Addison-Wesley, 2003. Chapter 6: "Aggregates" @@ -578,7 +578,7 @@ public void AggregateRoot_ThreadSafe_ConcurrentEventAddition() - Gamma et al. *Design Patterns*. Addison-Wesley, 1994. "Template Method" pattern - **Internal Documents**: - - [DOTNET_PORT.md](../../DOTNET_PORT.md) - Section "核心介面 > 實體層 > AggregateRoot" + - Internal porting notes (not retained) - Section "核心介面 > 實體層 > AggregateRoot" - [CLAUDE.md](../../CLAUDE.md) - Section "Important Implementation Rules > Event Sourcing Implementation" --- diff --git a/docs/adr/0010-esaggregate-root-event-sourcing-implementation.md b/docs/adr/0010-esaggregate-root-event-sourcing-implementation.md index a336c1f..500e006 100644 --- a/docs/adr/0010-esaggregate-root-event-sourcing-implementation.md +++ b/docs/adr/0010-esaggregate-root-event-sourcing-implementation.md @@ -583,7 +583,7 @@ public sealed override async Task ApplyAsync(TEvent @event) - **Related to**: - [ADR-0011: Event Replay and Invariant Checking](0011-event-replay-invariant-checking.md) - Details replay mechanism - - [ADR-0016: Reflection for Aggregate Reconstruction](planned) - How repositories instantiate aggregates via replay constructor + - ADR-0016: Reflection for Aggregate Reconstruction (planned) - How repositories instantiate aggregates via replay constructor --- @@ -813,7 +813,7 @@ public void ReplayEvents_ReconstructsCorrectState() ## References - **Java ezddd Source**: - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\EsAggregateRoot.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/EsAggregateRoot.java` - **Event Sourcing Correctness**: - Chen, Teddy. "Aggregate Correctness Rules in Event Sourcing" (internal specification) @@ -829,7 +829,7 @@ public void ReplayEvents_ReconstructsCorrectState() - [Type Patterns](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast#type-testing-with-pattern-matching) - **Internal Documents**: - - [DOTNET_PORT.md](../../DOTNET_PORT.md) - Section "重要實作規則 > Aggregate Correctness Rules" + - Internal porting notes (not retained) - Section "重要實作規則 > Aggregate Correctness Rules" - [CLAUDE.md](../../CLAUDE.md) - Section "Event Sourcing Rules (Priority: 43)" --- diff --git a/docs/adr/0011-event-replay-invariant-checking.md b/docs/adr/0011-event-replay-invariant-checking.md index b649b46..1c84dbe 100644 --- a/docs/adr/0011-event-replay-invariant-checking.md +++ b/docs/adr/0011-event-replay-invariant-checking.md @@ -528,8 +528,8 @@ public abstract class EsAggregateRoot - [ADR-0010: EsAggregateRoot Event Sourcing Implementation](0010-esaggregate-root-event-sourcing-implementation.md) - Defines Apply() and R1/R2/R3 rules - **Related to**: - - [ADR-0016: Reflection for Aggregate Reconstruction](planned) - Details constructor caching and optimization - - [ADR-0018: Exception Hierarchy Design](planned) - AggregateReplayException and EventReplayException + - ADR-0016: Reflection for Aggregate Reconstruction (planned) - Details constructor caching and optimization + - ADR-0018: Exception Hierarchy Design (planned) - AggregateReplayException and EventReplayException --- @@ -652,7 +652,7 @@ public class Order : EsAggregateRoot ## References - **Java ezddd Source**: - - `path/to/local/checkout Frame\ezddd\ezddd-entity\src\main\java\tw\teddysoft\ezddd\entity\EsAggregateRoot.java` + - `ezddd-entity/src/main/java/tw/teddysoft/ezddd/entity/EsAggregateRoot.java` - **Event Sourcing References**: - Young, Greg. ["Versioning in an Event Sourced System"](https://leanpub.com/esversioning) @@ -668,7 +668,7 @@ public class Order : EsAggregateRoot - Richter, Jeffrey. *CLR via C#*. Chapter on "Reflection" performance characteristics - **Internal Documents**: - - [DOTNET_PORT.md](../../DOTNET_PORT.md) - Section "技術實作細節 > 1. 事件溯源實作" + - Internal porting notes (not retained) - Section "技術實作細節 > 1. 事件溯源實作" - [CLAUDE.md](../../CLAUDE.md) - Section "Event Sourcing Implementation" --- diff --git a/docs/adr/0012-resource-management-event-bus-producers.md b/docs/adr/0012-resource-management-event-bus-producers.md index ad73d90..f3677c8 100644 --- a/docs/adr/0012-resource-management-event-bus-producers.md +++ b/docs/adr/0012-resource-management-event-bus-producers.md @@ -300,9 +300,9 @@ When implementing external event bus adapters: - Phase 3 final review report - Identified IDisposable as critical issue (internal working note, not retained in the repository) - Phase 3 Group 7 review - Detailed analysis of Message Bus group (Critical Issue #1) (internal working note, not retained in the repository) -- [IMessageProducer.cs](../../src/EzDdd.UseCase/Port/InOut/Messaging/IMessageProducer.cs) - Implementation (lines 64-79) -- [EventBusProducer.cs](../../src/EzDdd.UseCase/Port/InOut/Messaging/EventBusProducer.cs) - Implementation (lines 113-116) -- [Java MessageProducer.java](../../../../ezddd/ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/messaging/MessageProducer.java) - Java equivalent with Closeable +- `IMessageProducer` - Implementation (later removed from core in the Java 6.0.0 sync; see [ADR-0029](0029-messageproducer-removal-gateway-deferral.md)) +- `EventBusProducer` - Implementation (later removed in the MessageProducer refactoring; see [ADR-0025](0025-messageproducer-refactoring-java-4-1-0-alignment.md)) +- `ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/messaging/MessageProducer.java` - Java equivalent with Closeable - [.NET IDisposable Pattern](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose) - Microsoft documentation - Phase 3 post-review session notes - F.1 implementation record, lines 29-58 (internal working note, not retained in the repository) diff --git a/docs/adr/0014-domaineventdata-equality-semantics.md b/docs/adr/0014-domaineventdata-equality-semantics.md index 25e7ff3..09e6e7a 100644 --- a/docs/adr/0014-domaineventdata-equality-semantics.md +++ b/docs/adr/0014-domaineventdata-equality-semantics.md @@ -470,7 +470,7 @@ for (int i = 0; i < 10000; i++) - Phase 3 final review report - Identified equality semantics as critical issue (internal working note, not retained in the repository) - Phase 3 Group 4 review - Critical Issue #1: DomainEventData Equality, lines 38-99 (internal working note, not retained in the repository) - [DomainEventData.cs](../../src/EzDdd.UseCase/Port/InOut/DomainEventData.cs) - Implementation (lines 82-257) -- [Java DomainEventData.java](../../../../ezddd/ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/domainevent/DomainEventData.java) - Java JSONObject.similar() (lines 43-63) +- `ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/domainevent/DomainEventData.java` - Java JSONObject.similar() (lines 43-63) - Phase 3 post-review session notes - F.2 implementation record, lines 60-105 (internal working note, not retained in the repository) - [RFC 8259: JSON Specification](https://www.rfc-editor.org/rfc/rfc8259) - JSON object key order irrelevance - [System.Text.Json.JsonDocument](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsondocument) - .NET JSON parsing API diff --git a/docs/adr/0015-cross-platform-dto-structure.md b/docs/adr/0015-cross-platform-dto-structure.md index f454476..c8defeb 100644 --- a/docs/adr/0015-cross-platform-dto-structure.md +++ b/docs/adr/0015-cross-platform-dto-structure.md @@ -500,7 +500,7 @@ function EventLog() { - Phase 3 final review report - Identified DTO incompatibility as critical issue (internal working note, not retained in the repository) - Phase 3 Group 4 review - Critical Issue #2: Structure Incompatibility, lines 102-170 (internal working note, not retained in the repository) - [InternalDomainEventDto.cs](../../src/EzDdd.UseCase/Port/InOut/InternalDomainEventDto.cs) - Implementation (lines 66-117) -- [Java InternalDomainEventDto.java](../../../../ezddd/ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/domainevent/InternalDomainEventDto.java) - Java reference +- `ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/inout/domainevent/InternalDomainEventDto.java` - Java reference - Phase 3 post-review session notes - F.3 implementation record, lines 106-144 (internal working note, not retained in the repository) --- diff --git a/docs/adr/0016-async-await-throughout.md b/docs/adr/0016-async-await-throughout.md index 222377b..0f091a2 100644 --- a/docs/adr/0016-async-await-throughout.md +++ b/docs/adr/0016-async-await-throughout.md @@ -484,7 +484,7 @@ public async Task ExecuteAsync_InvalidAccount_ThrowsException() - Phase 3 Group 2 review - IUseCase async verification (internal working note, not retained in the repository) - Phase 3 Group 6 review - OutboxRepository async confirmation (internal working note, not retained in the repository) - [IReactor.cs](../../src/EzDdd.UseCase/Port/In/IReactor.cs) - Async ExecuteAsync (lines 17) -- [Java Reactor.java](../../../../ezddd/ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/in/interactor/Reactor.java) - Sync execute() (line 16) +- `ezddd-usecase/src/main/java/tw/teddysoft/ezddd/usecase/port/in/interactor/Reactor.java` - Sync execute() (line 16) - [IRepository.cs](../../src/EzDdd.UseCase/Port/Out/IRepository.cs) - Async repository APIs - Phase 3 post-review session notes - Phase 3 completion, line 247 (internal working note, not retained in the repository) diff --git a/docs/adr/0024-ireconciler-interface-system-reconciliation.md b/docs/adr/0024-ireconciler-interface-system-reconciliation.md index a256806..e40b5ff 100644 --- a/docs/adr/0024-ireconciler-interface-system-reconciliation.md +++ b/docs/adr/0024-ireconciler-interface-system-reconciliation.md @@ -360,8 +360,8 @@ public async Task ReconcileAsync_WithExpiredOrders_DeletesThem() - `da156c6` - Add Reconciler interface - `f377dcf` - Add NullContext class - `d4ed869` - Add reconciler examples -- **[DOTNET_PORT.md](../../DOTNET_PORT.md)** - Java 4.1.0 Synchronization Plan, Stage S2 (lines 293-318) -- **[ROADMAP.md](../../ROADMAP.md)** - Phase 6 Stage S2 (lines 293-318) +- **Internal porting notes (not retained)** - Java 4.1.0 Synchronization Plan, Stage S2 (lines 293-318) +- ROADMAP.md Phase 6 Stage S2 section (historical; the phase breakdown has since been removed from the roadmap) - **Null Object Pattern**: [Martin Fowler](https://martinfowler.com/eaaCatalog/specialCase.html) --- diff --git a/docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md b/docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md index 815578c..7a86ee8 100644 --- a/docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md +++ b/docs/adr/0025-messageproducer-refactoring-java-4-1-0-alignment.md @@ -503,7 +503,7 @@ See "Alternatives Considered" section above for detailed analysis of: - `InMemoryOutboxStoreRelay.java` (relay pattern example) ### Internal Documents -- [DOTNET_PORT.md](../../DOTNET_PORT.md) - Java 4.1.0 Synchronization Plan +- Internal porting notes (not retained) - Java 4.1.0 Synchronization Plan - Stage S3 implementation plan (internal working note, superseded by this ADR) - Checkpoint 3 redo guide - Repository integration guidance (internal working note, not retained in the repository) - [ADR-0012](0012-resource-management-event-bus-producers.md) - Resource management foundation @@ -541,7 +541,7 @@ See "Alternatives Considered" section above for detailed analysis of: - [x] Remove IReactor tests from InputOutputTests and ProjectorTests - [x] Verify all 487 tests passing - [x] Update README.md with new pattern examples -- [x] Update DOTNET_PORT.md with ADR-0025 reference +- [x] Update planning documents with ADR-0025 reference - [x] Update ROADMAP.md marking Stage S3 complete - [x] Update CLAUDE.md reflecting Stage S3 completion - [x] Update docs/adr/README.md with ADR-0025 entry diff --git a/docs/adr/0027-thread-null-safety-review.md b/docs/adr/0027-thread-null-safety-review.md index 2ed02ef..26c00f8 100644 --- a/docs/adr/0027-thread-null-safety-review.md +++ b/docs/adr/0027-thread-null-safety-review.md @@ -338,7 +338,7 @@ private static readonly Lazy> Mapper = **Iteration 6: ADR-0027 & Documentation** (1.5 hours) - Created: ADR-0027 (~400 lines) -- Updated: docs/adr/README.md, DOTNET_PORT.md, CLAUDE.md +- Updated: docs/adr/README.md and project guidance documents ### Files Modified (9 production files + 1 test file + 1 new test file) @@ -408,7 +408,7 @@ private static readonly Lazy> Mapper = - [.NET Lazy Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1) - Thread-safe lazy initialization pattern - [ArgumentNullException.ThrowIfNull Documentation](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull) - .NET 6+ null validation - Phase 6 Stage S4 completion handoff - Stage S4 completion and S5 planning (internal working note, not retained in the repository) -- [DOTNET_PORT.md - Java 4.1.0 Synchronization Plan](../../DOTNET_PORT.md#java-410-synchronization-plan) - Complete Phase 6 roadmap +- Internal porting notes (not retained) - Java 4.1.0 Synchronization Plan - Complete Phase 6 roadmap --- diff --git a/docs/adr/ADR.template.md b/docs/adr/ADR.template.md index 8b286d1..a1667f9 100644 --- a/docs/adr/ADR.template.md +++ b/docs/adr/ADR.template.md @@ -144,6 +144,6 @@ Template Usage Notes: 4. Keep the structure consistent across all ADRs 5. After creating ADR, update: - docs/adr/README.md (ADR Index) - - ../../DOTNET_PORT.md (relevant section) + - Related project planning documents (relevant section) - ../../CLAUDE.md (Confirmed Decisions) --> diff --git a/docs/adr/ADR_PLANNING.md b/docs/adr/ADR_PLANNING.md index 2f39181..07dcf4c 100644 --- a/docs/adr/ADR_PLANNING.md +++ b/docs/adr/ADR_PLANNING.md @@ -33,7 +33,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Modern C# features (nullable reference types, records, pattern matching) - Async/await maturity - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Target Framework" +- **Planning Docs Section**: Internal porting notes (not retained) - "Target Framework" - **Dependencies**: None (foundational) #### ADR-0002: Package Naming and Structure @@ -43,7 +43,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Package IDs: ezDDD.Common, ezDDD.Entity, etc. - Namespaces: EzDdd.Common, EzDdd.Entity, etc. - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Naming Convention" +- **Planning Docs Section**: Internal porting notes (not retained) - "Naming Convention" - **Dependencies**: None #### ADR-0003: Module Architecture and Dependency Chain @@ -53,7 +53,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - No circular dependencies - Clear separation of concerns - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Module Architecture" +- **Planning Docs Section**: Internal porting notes (not retained) - "Module Architecture" - **Dependencies**: None (foundational) #### ADR-0004: Zero Third-Party Dependency Principle @@ -63,7 +63,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - System.Text.Json for serialization - No MediatR, no AutoMapper, no Newtonsoft.Json - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Dependencies" +- **Planning Docs Section**: Internal porting notes (not retained) - "Dependencies" - **Dependencies**: ADR-0001 #### ADR-0005: Complete Reimplementation Approach @@ -73,7 +73,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Maintain semantic parity (~95-99%) - Leverage .NET strengths (async/await, nullable types) - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Porting Strategy" +- **Planning Docs Section**: Internal porting notes (not retained) - "Porting Strategy" - **Dependencies**: ADR-0001 #### ADR-0006: uContract.NET Integration for Design by Contract @@ -83,7 +83,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Essential for EsAggregateRoot invariant checking - Part of TeddySoft ecosystem (not third-party) - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Dependencies" +- **Planning Docs Section**: Internal porting notes (not retained) - "Dependencies" - **Dependencies**: ADR-0004 - **Related**: ADR-0010, ADR-0011 (use uContract for invariants) @@ -101,7 +101,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - IValueObject as marker interface - Immutability enforcement strategies - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Entity Layer" +- **Planning Docs Section**: Internal porting notes (not retained) - "Entity Layer" - **Dependencies**: ADR-0003 #### ADR-0008: IDomainEvent Hierarchy @@ -111,7 +111,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - InternalDomainEvent vs. ExternalDomainEvent - IConstructionEvent and IDestructionEvent markers - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Domain Events" +- **Planning Docs Section**: Internal porting notes (not retained) - "Domain Events" - **Dependencies**: ADR-0007 #### ADR-0009: AggregateRoot Base Class Design @@ -121,7 +121,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Version tracking - Event clearing semantics - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Aggregate Patterns" +- **Planning Docs Section**: Internal porting notes (not retained) - "Aggregate Patterns" - **Dependencies**: ADR-0007, ADR-0008 #### ADR-0010: EsAggregateRoot Event Sourcing Implementation @@ -131,7 +131,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Template method pattern (Apply calls When) - ConstructorInfo caching for performance - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Event Sourcing" +- **Planning Docs Section**: Internal porting notes (not retained) - "Event Sourcing" - **Dependencies**: ADR-0006, ADR-0009 #### ADR-0011: Event Replay and Invariant Checking (R1, R2, R3) @@ -141,7 +141,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - R2 (Command): Check invariants before and after - R3 (Destruction): No post-invariant check - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Event Sourcing Rules" +- **Planning Docs Section**: Internal porting notes (not retained) - "Event Sourcing Rules" - **Dependencies**: ADR-0010 --- @@ -198,7 +198,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - IUseCase: ExecuteAsync - In-memory operations remain sync - **Status**: ✅ Completed -- **Planning Docs Section**: DOTNET_PORT.md - "Async/Await" +- **Planning Docs Section**: Internal porting notes (not retained) - "Async/Await" - **Dependencies**: ADR-0001 - **Related**: All UseCase and Repository ADRs @@ -303,7 +303,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Java 4.1.0 feature addition - **Status**: ⏳ Planned (S2) - **Priority**: 🟢 NEW FEATURE -- **Planning Docs Section**: DOTNET_PORT.md - "Java 4.1.0 Synchronization Plan, Stage 3" +- **Planning Docs Section**: Internal porting notes (not retained) - "Java 4.1.0 Synchronization Plan, Stage 3" - **Dependencies**: ADR-0003 (UseCase layer pattern) - **Related**: ADR-0020 (similar to IProjector lifecycle) - **Estimated Effort**: 4-6 hours @@ -320,7 +320,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - **Status**: ✅ Completed (S3, 2026-01-07, 3 hours) - **ADR Document**: [ADR-0025](0025-messageproducer-refactoring-java-4-1-0-alignment.md) - **Priority**: 🔴 BREAKING (Critical) -- **Planning Docs Section**: DOTNET_PORT.md - "Java 4.1.0 Synchronization Plan, Stage 4" +- **Planning Docs Section**: Internal porting notes (not retained) - "Java 4.1.0 Synchronization Plan, Stage 4" - **Dependencies**: ADR-0012 (extends resource management), ADR-0024 (metadata in messages) - **Related**: ADR-0013 (messaging patterns) - **Estimated Effort**: 10-14 hours (most complex) @@ -336,7 +336,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - **Status**: ✅ Completed (S4, 2026-01-08, 6 hours) - **ADR Document**: [ADR-0026](0026-service-layer-pattern.md) - **Priority**: 🟡 OPTIONAL (Guidance) -- **Planning Docs Section**: DOTNET_PORT.md - "Java 4.1.0 Synchronization Plan, Stage 5" +- **Planning Docs Section**: Internal porting notes (not retained) - "Java 4.1.0 Synchronization Plan, Stage 5" - **Dependencies**: ADR-0003 (UseCase layer) - **Related**: ADR-0017 (UseCase patterns) - **Estimated Effort**: 6-8 hours @@ -352,7 +352,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* - Java 4.1.0 quality improvements - **Status**: ⏳ Planned (S5) - **Priority**: 🔵 QUALITY -- **Planning Docs Section**: DOTNET_PORT.md - "Java 4.1.0 Synchronization Plan, Stage 6" +- **Planning Docs Section**: Internal porting notes (not retained) - "Java 4.1.0 Synchronization Plan, Stage 6" - **Dependencies**: All core implementation ADRs - **Related**: ADR-0004 (BCL usage), ADR-0014 (equals/hashCode) - **Estimated Effort**: 6-8 hours @@ -532,7 +532,7 @@ Before marking any ADR as "Accepted", verify it meets quality standards: - ✅ At least 2 alternatives documented - ✅ Consequences analyzed (positive/negative/neutral) - ✅ Related ADRs cross-referenced -- ✅ All three locations updated: ADR file → DOTNET_PORT.md → CLAUDE.md +- ✅ All locations updated: ADR file → docs/adr/README.md index → AGENTS.md --- diff --git a/docs/adr/ADR_PLANNING.template.md b/docs/adr/ADR_PLANNING.template.md index 44839c8..1b0b62a 100644 --- a/docs/adr/ADR_PLANNING.template.md +++ b/docs/adr/ADR_PLANNING.template.md @@ -77,7 +77,7 @@ This document provides a **roadmap for all Architecture Decision Records (ADRs)* 3. Update metadata (Created, Last Updated dates) ### Step 2: Identify ADR Topics -- Review your project planning documents (e.g., DOTNET_PORT.md, DESIGN_CHECKLIST.md) +- Review your project planning documents (e.g., DESIGN.md, DESIGN_CHECKLIST.md) - List all significant architectural decisions to be made - For each decision, create an ADR entry below @@ -269,7 +269,7 @@ Add custom fields if helpful: ### 4. Integration with Project Docs - Reference your project's planning documents in "Planning Docs Section" -- Examples: DOTNET_PORT.md, DESIGN.md, ARCHITECTURE.md, CLAUDE.md +- Examples: DESIGN.md, ARCHITECTURE.md, CLAUDE.md --- diff --git a/docs/adr/README.md b/docs/adr/README.md index 6ac3bf3..9c0360e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -54,7 +54,7 @@ Create an ADR when making decisions about: - Code formatting changes - Internal refactoring without API impact -**Rule of thumb**: If a decision from [DOTNET_PORT.md](../../DOTNET_PORT.md) is confirmed, write an ADR. +**Rule of thumb**: If an architectural decision is confirmed, write an ADR. --- @@ -123,10 +123,8 @@ What decision did we make? ``` ADR (docs/adr/*.md) - ↓ Referenced by -DOTNET_PORT.md ↓ Summarized in -CLAUDE.md +AGENTS.md ``` ### 1. ADR = Single Source of Truth @@ -136,39 +134,10 @@ CLAUDE.md - To change a decision, write a **new ADR** that supersedes the old one - File naming: `0001-target-framework.md`, `0002-package-naming.md` -### 2. DOTNET_PORT.md = Planning Document + ADR Index +### 2. AGENTS.md = Quick Reference Card -- Maintains **overview** of the porting plan -- Each decision includes an **ADR link** -- Example format: - -```markdown -## Core Design Decisions - -### 1. Complete Reimplementation - -**Status**: ✅ Accepted (See [ADR-0005](docs/adr/0005-complete-reimplementation-approach.md)) - -**Summary**: We will completely reimplement ezddd in .NET rather than wrap existing libraries... -``` - -### 3. CLAUDE.md = Quick Reference Card - -- **Most concise** summary for Claude Code -- Listed format with ADR links -- Example: - -```markdown -## Key Design Decisions - -> For detailed decisions, see ADR or DOTNET_PORT.md - -### Confirmed Decisions - -- **Target Framework**: .NET 8+ ([ADR-0001](docs/adr/0001-target-framework.md)) -- **Package Naming**: ezDDD/EzDdd convention ([ADR-0002](docs/adr/0002-package-naming-and-structure.md)) -- **Zero Dependencies**: Built-in .NET APIs only ([ADR-0004](docs/adr/0004-zero-third-party-dependency-principle.md)) -``` +- **Most concise** summary of the rules that affect day-to-day development +- References ADRs for the full rationale (e.g. the R1–R3 event sourcing rules point to ADR-0011) ### Standard Workflow After Decision Confirmation @@ -185,30 +154,23 @@ CLAUDE.md └──────────┬──────────────────────────┘ ↓ ┌─────────────────────────────────────┐ -│ Step 2: Update DOTNET_PORT.md │ -│ - Add summary in relevant section │ -│ - Add ADR link [ADR-NNNN](...) │ -└──────────┬──────────────────────────┘ - ↓ -┌─────────────────────────────────────┐ -│ Step 3: Update CLAUDE.md │ -│ - Add to "Confirmed Decisions" │ -│ - Include ADR link │ +│ Step 2: Update docs/adr/README.md │ +│ - Add to ADR Index below │ +│ - Update status and date │ └──────────┬──────────────────────────┘ ↓ ┌─────────────────────────────────────┐ -│ Step 4: Update docs/adr/README.md │ -│ - Add to ADR Index below │ -│ - Update status and date │ +│ Step 3: Update AGENTS.md if the │ +│ decision changes day-to-day rules │ └─────────────────────────────────────┘ ``` ### Preventing Conflicts -1. **Unidirectional References**: Only DOTNET_PORT.md and CLAUDE.md reference ADRs, not vice versa +1. **Unidirectional References**: Guidance documents reference ADRs; ADRs do not depend on guidance documents 2. **ADR Immutability**: Once Accepted, freeze content; changes require new ADR 3. **Explicit ADR Numbers**: Always include `[ADR-NNNN]` links in all locations -4. **Periodic Sync Check**: When confirming decisions, update all three locations simultaneously +4. **Periodic Sync Check**: When confirming decisions, update the ADR, this index, and AGENTS.md together --- @@ -353,10 +315,9 @@ done - [ADR Planning Document](ADR_PLANNING.md) - Roadmap of planned ADRs - [ADR Template](ADR.template.md) - Standard ADR format - [ADR Maintenance Checklist](ADR_MAINTENANCE_CHECKLIST.md) - Quality guidelines -- [DOTNET_PORT.md](../../DOTNET_PORT.md) - Technical decisions and planning -- [CLAUDE.md](../../CLAUDE.md) - Development guidance +- [AGENTS.md](../../AGENTS.md) - Development guidance - [Java ezddd](https://gitlab.com/TeddyChen/ezddd) - Original implementation -- [uContract.NET ADRs](../../../uContract.NET/docs/adr/) - Reference implementation +- [uContract.NET ADRs](https://github.com/cwouyang/uContract.NET/tree/master/docs/adr) - Reference implementation --- diff --git a/docs/examples/USAGE_EXAMPLES.md b/docs/examples/USAGE_EXAMPLES.md index 7636a80..a44e029 100644 --- a/docs/examples/USAGE_EXAMPLES.md +++ b/docs/examples/USAGE_EXAMPLES.md @@ -3790,7 +3790,6 @@ builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); - [API Reference](API_REFERENCE.md) - Complete API documentation - [Architecture Decision Records](../adr/) - Design decisions - [README.md](../../README.md) - Project overview -- [DOTNET_PORT.md](../../DOTNET_PORT.md) - Technical planning and design --- diff --git a/docs/patterns/SERVICE_LAYER_PATTERN.md b/docs/patterns/SERVICE_LAYER_PATTERN.md index e6fcafa..7a5f405 100644 --- a/docs/patterns/SERVICE_LAYER_PATTERN.md +++ b/docs/patterns/SERVICE_LAYER_PATTERN.md @@ -768,7 +768,7 @@ public class WithdrawMoneyService ### ezDDD.NET Documentation -- **[DOTNET_PORT.md](../../DOTNET_PORT.md)** - Java 4.1.0 Synchronization Plan (Stage S4) +- Internal porting notes (not retained) - Java 4.1.0 Synchronization Plan (Stage S4) - **[ADR-0026](../adr/0026-service-layer-pattern.md)** - Service Layer Pattern Decision - **[TRANSACTION_BOUNDARY_GUIDE.md](../TRANSACTION_BOUNDARY_GUIDE.md)** - Where transactions belong From 1b2af21f2f653df7eb21b2378091a9191add76de Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sun, 5 Jul 2026 01:01:43 +0800 Subject: [PATCH 15/18] build: enforce XML documentation on packable projects Drop the CS1591 suppression so missing public API docs fail the build, document the 15 BiMap members it caught (inheritdoc for pure contract forwarding, real docs where BiMap semantics diverge from Dictionary: Add overwrites instead of throwing, values are unique, CopyTo truncates), and exempt test projects via a nested Directory.Build.props that turns off documentation generation. --- Directory.Build.props | 3 +- src/EzDdd.Common/BiMap.cs | 96 +++++++++++++++++++++++++++++++++++++ tests/Directory.Build.props | 9 ++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props index 9f18889..344c79b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -32,11 +32,12 @@ true - $(NoWarn);1701;1702;CS1591 + $(NoWarn);1701;1702 embedded true + true diff --git a/src/EzDdd.Common/BiMap.cs b/src/EzDdd.Common/BiMap.cs index 23812df..a0665f6 100644 --- a/src/EzDdd.Common/BiMap.cs +++ b/src/EzDdd.Common/BiMap.cs @@ -151,6 +151,18 @@ private void _AddInternal(TKey key, TValue value) #region IDictionary Implementation + /// + /// Gets or sets the value associated with the specified key, maintaining the bidirectional mapping. + /// + /// The key of the value to get or set + /// The value associated with the specified key + /// The property is retrieved and is not found + /// + /// The setter enforces the BiMap uniqueness constraint on values: if the assigned value is already + /// associated with a different key, that key is removed from the map so that only the new + /// key → value pair remains. Setting a new value for an existing key also removes the old + /// value's reverse mapping. + /// public TValue this[TKey key] { get @@ -169,6 +181,11 @@ public TValue this[TKey key] } } + /// + /// + /// Returns a snapshot copy of the keys, not a live view, so it can be enumerated safely + /// while other threads modify the map. + /// public ICollection Keys { get @@ -180,6 +197,11 @@ public ICollection Keys } } + /// + /// + /// Returns a snapshot copy of the values, not a live view, so it can be enumerated safely + /// while other threads modify the map. + /// public ICollection Values { get @@ -191,6 +213,7 @@ public ICollection Values } } + /// public int Count { get @@ -202,8 +225,25 @@ public int Count } } + /// public bool IsReadOnly => false; + /// + /// Adds the specified key-value pair to the map, maintaining the bidirectional mapping. + /// + /// The key to add + /// The value to associate with the key + /// + /// + /// Unlike , this method does not throw when the key + /// already exists; the existing entry is overwritten (equivalent to Java's put). + /// + /// + /// BiMap enforces a uniqueness constraint on values: if is already + /// associated with a different key, that key is removed from the map so that only the new + /// key → value pair remains. See the class-level documentation for an example. + /// + /// public void Add(TKey key, TValue value) { lock (_lock) @@ -212,11 +252,20 @@ public void Add(TKey key, TValue value) } } + /// + /// Adds the specified key-value pair to the map, maintaining the bidirectional mapping. + /// + /// The key-value pair to add + /// + /// Delegates to ; the same overwrite and value-uniqueness + /// semantics apply. + /// public void Add(KeyValuePair item) { Add(item.Key, item.Value); } + /// public void Clear() { lock (_lock) @@ -226,6 +275,7 @@ public void Clear() } } + /// public bool Contains(KeyValuePair item) { lock (_lock) @@ -235,6 +285,7 @@ public bool Contains(KeyValuePair item) } } + /// public bool ContainsKey(TKey key) { lock (_lock) @@ -243,6 +294,18 @@ public bool ContainsKey(TKey key) } } + /// + /// Copies the key-value pairs of the map to the specified array, starting at the specified index. + /// + /// The destination array + /// The zero-based index in at which copying begins + /// is null + /// + /// Copies from a snapshot taken under the lock. Unlike the standard + /// contract, if the destination array is too small the copy + /// is truncated to the available space instead of throwing, because in concurrent scenarios the + /// map may have grown between reading and calling this method. + /// public void CopyTo(KeyValuePair[] array, int arrayIndex) { ArgumentNullException.ThrowIfNull(array); @@ -263,6 +326,15 @@ public void CopyTo(KeyValuePair[] array, int arrayIndex) } } + /// + /// Returns an enumerator that iterates over a snapshot of the key-value pairs. + /// + /// An enumerator over a snapshot of the map's key-value pairs + /// + /// The snapshot is taken under the lock, so enumeration does not block other threads and + /// never throws due to concurrent modification; changes made after the enumerator is obtained + /// are not reflected in the enumeration. + /// public IEnumerator> GetEnumerator() { lock (_lock) @@ -272,6 +344,16 @@ public IEnumerator> GetEnumerator() } } + /// + /// Removes the entry with the specified key from the map, including its reverse mapping. + /// + /// The key of the entry to remove + /// true if the entry was found and removed; otherwise, false + /// + /// Removal is bidirectional: both the forward mapping (key → value) and the reverse mapping + /// (value → key) are removed, so a subsequent for the removed value + /// returns null. + /// public bool Remove(TKey key) { lock (_lock) @@ -286,6 +368,19 @@ public bool Remove(TKey key) } } + /// + /// Removes the entry matching both the key and the value of the specified pair, including its + /// reverse mapping. + /// + /// The key-value pair to remove + /// + /// true if the key was found with the matching value and the entry was removed; + /// otherwise, false + /// + /// + /// The entry is removed only if the key exists and its current value equals + /// item.Value. Removal is bidirectional: both the forward and reverse mappings are removed. + /// public bool Remove(KeyValuePair item) { lock (_lock) @@ -304,6 +399,7 @@ public bool Remove(KeyValuePair item) } } + /// public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) { lock (_lock) diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..d2c4992 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + + false + + From 544c776f345f52df9d0af094599264d045940ee2 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sun, 5 Jul 2026 01:01:44 +0800 Subject: [PATCH 16/18] chore: publish-readiness hardening Add CODEOWNERS, a CI status badge, extra secret ignore patterns, and route the release tag through an env var in publish.yml instead of inline interpolation. --- .github/CODEOWNERS | 2 ++ .github/workflows/publish.yml | 3 ++- .gitignore | 7 +++++++ README.md | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..fbfebf2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner for everything in the repository. +* @cwouyang diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a746234..4db7b5d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -83,4 +83,5 @@ jobs: env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - run: gh release upload "${{ github.event.release.tag_name }}" ./nupkg/*.nupkg --clobber + TAG_NAME: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG_NAME" ./nupkg/*.nupkg --clobber diff --git a/.gitignore b/.gitignore index aa64e9d..7669150 100644 --- a/.gitignore +++ b/.gitignore @@ -484,3 +484,10 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp + +# Local secrets and environment files +appsettings.*.json +!appsettings.json +*.key +**/secrets/ +.claude/settings.local.json diff --git a/README.md b/README.md index 3abd4c4..aa9bf43 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ A modern tactical DDD library for .NET, specifically designed for Domain-Driven Design with event sourcing, state sourcing, and CQRS patterns. This is a faithful .NET port of the **Java ezddd 6.0.1** library (GitLab commit: `3aac0f5`) with **~99% semantic parity** and .NET-specific improvements. +[![Build and Test](https://github.com/cwouyang/ezDDD.NET/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/cwouyang/ezDDD.NET/actions/workflows/build-and-test.yml) [![.NET](https://img.shields.io/badge/.NET-8.0+-512BD4?logo=dotnet)](https://dotnet.microsoft.com/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Status](https://img.shields.io/badge/status-alpha-orange.svg)](#status) From 1f94e68b6f8e4ca5d2c59e96b2df09c0518e8956 Mon Sep 17 00:00:00 2001 From: cwouyang Date: Sun, 5 Jul 2026 01:20:12 +0800 Subject: [PATCH 17/18] chore: refresh the blame-ignore SHA after history rewrite The CSharpier baseline commit was rehashed by the pre-publication history rewrite; point .git-blame-ignore-revs at its new id. --- .git-blame-ignore-revs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f7c3bab..d89d1bc 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -3,4 +3,4 @@ # Local one-time setup: git config blame.ignoreRevsFile .git-blame-ignore-revs # CSharpier 1.2.6 baseline format — repository-wide whitespace/wrapping reformat (P2 rollout, 2026-07-04). -ed7908f908d090c33ab0f7fb179f52868d94575c +bac134048742b2de141dfddf00f90daf6553dbfb From efd8d268a0ad61dacd1a8eab5898c6b69ff77342 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:29:12 +0000 Subject: [PATCH 18/18] chore(deps): Bump Meziantou.Analyzer from 3.0.54 to 3.0.119 --- updated-dependencies: - dependency-name: Meziantou.Analyzer dependency-version: 3.0.119 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 344c79b..4da60c9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -41,7 +41,7 @@ - + all runtime; build; native; contentfiles; analyzers