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