diff --git a/Brighter.sln b/Brighter.sln index 3babe86283..c77b4465a6 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -104,6 +104,12 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.AzureServiceBus.Tests", "tests\Paramore.Brighter.AzureServiceBus.Tests\Paramore.Brighter.AzureServiceBus.Tests.csproj", "{48F584DF-0BA1-4485-A612-14FD4F6A4CF7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ASBTaskQueue", "ASBTaskQueue", "{48D0EECA-B928-4B80-BE46-2C08CF3A946B}" + ProjectSection(SolutionItems) = preProject + samples\TaskQueue\ASBTaskQueue\Config.Json = samples\TaskQueue\ASBTaskQueue\Config.Json + samples\TaskQueue\ASBTaskQueue\.env = samples\TaskQueue\ASBTaskQueue\.env + samples\TaskQueue\ASBTaskQueue\Docker-Compose.yml = samples\TaskQueue\ASBTaskQueue\Docker-Compose.yml + samples\TaskQueue\ASBTaskQueue\Readme.md = samples\TaskQueue\ASBTaskQueue\Readme.md + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.MsSql", "src\Paramore.Brighter.MsSql\Paramore.Brighter.MsSql.csproj", "{36CADB1E-3777-4A7E-86E3-BF650A951AC9}" EndProject @@ -315,6 +321,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutation_Sweeper", "sampl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.MsSql", "src\Paramore.Brighter.Locking.MsSql\Paramore.Brighter.Locking.MsSql.csproj", "{758EE237-C722-4A0A-908C-2D08C1E59025}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Mediator", "src\Paramore.Brighter.Mediator\Paramore.Brighter.Mediator.csproj", "{F00B137A-C187-4C33-A37B-22AD40B71600}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1765,6 +1773,18 @@ Global {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|Mixed Platforms.Build.0 = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.ActiveCfg = Release|Any CPU {758EE237-C722-4A0A-908C-2D08C1E59025}.Release|x86.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|x86.ActiveCfg = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Debug|x86.Build.0 = Debug|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Any CPU.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|x86.ActiveCfg = Release|Any CPU + {F00B137A-C187-4C33-A37B-22AD40B71600}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Directory.Packages.props b/Directory.Packages.props index 675a2354fb..128ae0eaa8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -87,12 +87,14 @@ + + all diff --git a/docs/adr/0020-reduce-esb-complexity.md b/docs/adr/0020-reduce-esb-complexity.md index 926c49e7d2..82d91ec747 100644 --- a/docs/adr/0020-reduce-esb-complexity.md +++ b/docs/adr/0020-reduce-esb-complexity.md @@ -1,6 +1,6 @@ # 20. Reduce External Service Bus Complexity -Date: 2019-08-01 +Date: 2024-08-01 ## Status diff --git a/docs/adr/0022-add-a-mediator.md b/docs/adr/0022-add-a-mediator.md new file mode 100644 index 0000000000..317b2c8681 --- /dev/null +++ b/docs/adr/0022-add-a-mediator.md @@ -0,0 +1,58 @@ +# 22. Add a Mediator to Brighter + +Date: 2024-10-22 + +## Status + +Proposed + +## Context +We have two approaches to a workflow: orchestration and choreography. In choreography the workflow emerges from the interaction of the participants. In orchestration, one participant executes the workflow, calling other participants as needed. Whilst choreography has low-coupling, it also has low-cohesion. At scale this can lead to the Pinball anti-pattern, where it is difficult to maintain the workflow. + +The [Mediator](https://www.oodesign.com/mediator-pattern) pattern provides an orchestrator that manages a workflow that involves multiple objects. In its simplest form, instead of talking to each other, objects talk to the mediator, which then calls other objects as required to execute the workflow. + +Brighter provides `IHandleRequests<>` to provide a handler for an individual request, either a command or an event. It is possible to have an emergent workflow, within Brighter, through the choreography of these handlers. However, Brighter provides no model for an orchestrator that manages a workflow that involves multiple handlers. In particular, Brighter does not support a class that can listen to multiple requests and then call other handlers as required to execute the workflow. + +In principle, nothing stops an end user from implementing a `Mediator` class that listens to multiple requests and then calls other handlers as required to execute the workflow. So orchestration has always been viable, but left as an exercise to the user. However, competing OSS projects provide popular workflow functionality, suggesting there is demand for an off-the-shelf solution. + +Other dotnet messaging platforms erroneously conflate the Saga and Mediator patterns. A Saga is a long-running transaction that spans multiple services. A Mediator is an orchestrator that manages a workflow that involves multiple objects. One aspect of those implementations is typically the ability to store workflow state. + +There is a pattern catalogue associated with workflows. [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) describes both basic and advanced patterns for workflows. We intend to use these patters as guidance for our offering, over traditional .NET workflow offerings in competing products such as Mass Transit and NServicBus, which have tended to be ersatz in design. + +A particular reference for the requirements for this work is [AWS step functions](https://states-language.net/spec.html). AWS Step functions provide a state machine that mediates calls to AWS Lambda functions. When thinking about Brighter's `IHandleRequests` it is attractive to compare them to Lambda functions in the Step functions model : + + 1. The AWS Step funcions state machine does not hold the business logic, that is located in the functions called; the Step function handles calling the Lambda functions and state transitions (as well as error paths) + 2. We want to use the Mediator to orchestrate both internal bus and external bus hosted workflows. Step functions provide a useful model of requirements for the latter. + +This approach is intended to enable flexible, event-driven workflows that can handle various business processes and requirements, including asynchronous event handling and conditional branching. + +Our experience has been that many teams adopt Step Functions to gain access to it as a workflow engine. But this forces them into Lambda Pinball architectures. We believe that Brighter could offer a compelling alternative. + +## Decision + +We will add a `Mediator` class to Brighter that will: + + 1. Manages and tracks a WorkflowState object representing the current step in the workflow. + 2. Support multiple steps: sequence, choice, parallel, wait. + 3. Supports multiple tasks, mapped to typical ws-messaging patterns including: + • FireAndForget: Dispatches a `Command` and immediately advances to the next state. + • RequestReaction: Dispatches a `Command` and waits for an event response before advancing. + • RobustRequestReaction: Reaction event can kick off an error flow. + 4. Uses a CommandProcessor for routing commands and events to appropriate handlers. + 5. Work is handled within Brighter handlers. They use glue code to call back to the workflow where necessary + 6. Can be passed events, and uses the correlation IDs to match events to specific workflow instances and advance the workflow accordingly. + +The Specification Pattern in a Choice step will allow flexible conditional logic by combining specifications with And and Or conditions, enabling complex branching decisions within the workflow. + +We assume that the initial V10 of Brighter will contain a minimum viable product version of the `Mediator`. Additional functionality, workflows, etc. will be a feature of later releases. Broady our goal within V10 would be to ensure that from [Workflow Patterns](http://www.workflowpatterns.com/patterns/control/index.php) we can deliver the Basic Control Flow patterns. A stretch goal would be to offer some Iteration and Cnacellation patterns. + +## Consequences + +Positive Consequences + + 1. Simplicity: Providing orchestration for a workflow, which is easier to understand + 2. Modularity: It is possible to extend the `Mediator' relativey easy by adding new process states. + +Negative Consequences + + 1. Increased Brighter scope: Previously we had assumed that developers would use an off-the-shelf workflow solution like [Stateless](https://github.com/nblumhardt/stateless) or [Workflow Core]. The decision to provide our own workflow, to orchestrate via CommandProcessor means that we increase our scope to include the complexity of workflow management. \ No newline at end of file diff --git a/docs/adr/0023-add-the_specification-pattern.md b/docs/adr/0023-add-the_specification-pattern.md new file mode 100644 index 0000000000..1d14baaf94 --- /dev/null +++ b/docs/adr/0023-add-the_specification-pattern.md @@ -0,0 +1,25 @@ +# 22. Add the Specification Pattern + +Date: 2024-11-09 + +## Status + +Proposed + +## Context + +The Specification Pattern is a software design pattern that is used to define business rules that can be combined to create complex rules. It is used to encapsulate business rules that can be used to determine if an object meets a certain criteria. The pattern was described by Eric Evans and Martin Fowler in [this article](https://martinfowler.com/apsupp/spec.pdf). + +Brighter needs the addition of the specification pattern, for two reasons: + +1. For use with its Mediator. The Mediator allows Brighter to execute a workflow that has a branching condition. The Specification Pattern can be used to define the branching conditions. See [ADR-0022](0022-use-the-mediator-pattern.md). +2. For use when implementing the [Agreement Dispatcher](https://martinfowler.com/eaaDev/AgreementDispatcher.html) pattern from Martin Fowler. The Agreement Dispatcher pattern is used to dispatch a message to a handler based on a set of criteria. The Specification Pattern can be used to define the criteria. + +## Decision +Add the Specification Pattern to Brighter. We could have taken a dependency on an off-the-shelf implementation. Many of the Brighter team worked at Huddle Engineering, and worked on [this](https://github.com/HuddleEng/Specification) implementation of the Specification Pattern. However, this forces Brighter to take a dependency on another project, and we would like to keep Brighter as self-contained as possible. So, whilst we may be inspired by Huddle's implementation, we will write our own. + +In this version, we don't need some of the complexity of Huddle's usage of the Visitor pattern, as we only need to control branching. In addition, Huddle's version was written before the wide usage of lambda expressions via delegates in C#, so we can simplify the implementation. + +## Consequences + +Brighter will provide an implementation of the Specification pattern. \ No newline at end of file diff --git a/docs/adr/0024-add-parallel-split-to-mediator.md b/docs/adr/0024-add-parallel-split-to-mediator.md new file mode 100644 index 0000000000..d6ad4883f8 --- /dev/null +++ b/docs/adr/0024-add-parallel-split-to-mediator.md @@ -0,0 +1,67 @@ +# ADR: Implementing Parallel Split Step for Concurrent Workflow Execution + +## Status + +Proposed + +## Context + +Our workflow currently supports sequential steps executed in a single thread of control. Each step in the workflow proceeds one after another, and the Mediator has been designed with this single-threaded assumption. + +To support more advanced control flow, we want to introduce a Parallel Split Step based on the Workflow Patterns Basic Control Flow Patterns. The Parallel Split Step is defined as “the divergence of a branch into two or more parallel branches, each of which execute concurrently.” This will enable the workflow to branch into parallel paths, executing multiple threads of control simultaneously. Each branch will operate independently of the others, continuing the workflow until either completion or a synchronization step (such as a Simple Merge) later in the process. + +We would expect a some point to implement the Simple Merge step to allow parallel branches to converge back into a single thread of control. However, this ADR will focus on the Parallel Split Step implementation, with the understanding that future steps will be added to support synchronization. + +### Key Requirements +1. Parallel Execution: + * Parallel Split Step must initiate two or more parallel branches within the workflow. + * Each branch should proceed as a separate thread of control, executing steps independently. +2. Concurrency Handling in the Mediator: + * The Mediator needs to manage multiple threads of execution rather than assuming a single-threaded flow. + * It must be able to initiate and track multiple branches for each Parallel Split Step within the workflow. +3. State Persistence for Parallel Branches: + * Workflow state management and persistence will need to be adapted to track the branches of the flow. + * In the case of a crash, each branch should be able to resume from its last saved state. +4. Integration with Future Synchronization Steps: + * The Parallel Split Step should integrate seamlessly with a future Simple Merge step, which will allow parallel branches to converge back into a single thread. + +## Decision +1. Parallel Split Step Implementation: + * Introduce a new class, ParallelSplitStep, derived from Step. + * Ths class will define multiple branches by specifying two or more independent workflow sequences to be executed in parallel. +2. Producer and Consumer Model for Parallel Execution + * The Mediator will now consist of two classes: a producer (Scheduler) and a consumer (Runner). + * Scheduling a workflow via the Scheduler causes it to send a job to a shared channel or blocking collection. + * The Runner class will act as a consumer, reading workflow jobs from the channel and executing them. + * The Runner is single-threaded, and runs a message pump to process jobs sequentially. + * The job queue is bounded to prevent excessive memory usage and ensure fair scheduling. + * The user can configure the job scheduler for backpressure (producer stalls) or load shedding (dropping jobs). + * The user configures the number of Runners; we don't just pull them from the thread pool. This allows users to control how many threads are used to process jobs. For example, a user could configure a single Runner for a single-threaded workflow, or multiple Runners for parallel execution. +3. In the In-Memory version the job channels will be implemented using a BlockingCollection with a bounded capacity. + * We won't separately store workflow data in a database; the job channel is the storage for work to be done, or in flight + * When we branch, we schedule onto the same channel; this means a Runner has a dependency on the Mediator +4. For resilience, we will need to use a persistent queue for the workflow channels. + * We assume that workflow will become unlocked when their owning Runner crashes, allowing another runner to pick them up + * We will use one derived from a database, not a message queue. + * This will be covered in a later ADR, and likely create some changes + +## Consequences + +### Positive Consequences +* Concurrency and Flexibility: The addition of Parallel Split allows workflows to handle concurrent tasks and enables more complex control flows. +* Scalability: Running parallel branches improves throughput, as tasks that are independent of each other can execute simultaneously. +* Adaptability for Future Steps: Implementing parallel branching prepares the workflow for synchronization steps (e.g., Simple Merge), allowing flexible convergence of parallel tasks. +* Resilience: + +### Negative Consequences +* Increased Complexity in State Management: Tracking multiple branches requires more complex state management to ensure each branch persists and resumes accurately. +* Concurrency Overhead in the Mediator: Managing multiple threads of control adds overhead. We now have both a Runner and a Scheduler. + +### Use of Middleware or Db for The Job Channel +* We could use a middleware library to manage the job channel. Brighter itself manages a queue or stream of work with a single-threaded pump +* This would mean the scheduler uses the commandprocessor to deposit a job on a queue, and the runner would be our existing message pump, which would pass to a job handler that executed the workflow. +* The alternative here is to use the database as the job channel. This would mean that the scheduler would write a job to the database, and the runner would read from the database. +* For now, we defer this decision to a later ADR. First we want to understand the whole scope of the work, through an in-memory implementation, then we will determine what an out-of-process implementation would look like. + +### Merge of parallel branches +* Future ADR for implementing Simple Merge Step for synchronization of parallel branches. diff --git a/docs/adr/0025-use-reactive-programming-for-mediator.md b/docs/adr/0025-use-reactive-programming-for-mediator.md new file mode 100644 index 0000000000..59da97a5ce --- /dev/null +++ b/docs/adr/0025-use-reactive-programming-for-mediator.md @@ -0,0 +1,61 @@ +# 25. Use Reactive Progamming For Mediator + +Date: 2025-01-13 + +## Status + +Accepted + +## Context + +We have scenarios in any workflow where we need to split and then later merge. Our decision to handle the split in +[0024](./0024-add-parallel-split-to-mediator.md) led us on the path to seperating a scheduler and a runner - a +classic producer and consumer pattern. We can use `Channels` (or a `BlockingCollection`) in dotnet to support the +implementation of an internal producer-consumer (as opposed to one using messaging. + +Our approach to resolve split was simply to have one channel for the workflow to be scheduled on, so that we could +schedule the splits back to the channel. We don't have a solution for merging those splits. + +We also have an approach to waiting for an external event, that we halt the flow, save it's state, and then reschedule +once we are notified of the event we are waiting for. This works well for a single event, but external. It works +less well for multiple events, or internal events, that go best over a channel. + +## Decision + +We will move to a Flow Based Programming approach to implementing the work. Each `Step<>` in the workflow will +derive from a new type `Component`. + +As a FBP component it has an `In` port, an instance of `IAmAJobChannel`. When a component is activated it runs a +message pump to read work from the `In` port, until the port is marked as completed. Once there is no more work, the +`Component` deactivates. A component should save state before it deactivates, to indicate that it was completed. + +An `Out` port is actually a call to the next component. Putting work on the `Out`port activates the next component +and puts work on its `In` port. + +``` +--> [In][Component][Out] --> +``` + +On a split, there is an array of `Out` ports to write to, instead of a single port. Generically then we require an +overload of any Out method call on the base 'Component' that takes an array of `IAmAJobChannel` + +``` +--> [In][Component][Out...] --> +``` + +On a merge that is an array of 'In' ports to write to, instead of a single port. We may force you to wait for +everything to arrive before continuing, or allow you to proceed as soon as you arrive in the joined flow. + +We may choose to use the FBP brackets approach to any merge. The upstream sends an 'opening bracket' to 'In' +indicating a sequence follows. The 'bracket' indicates whether we are 'WaitAll' or 'WaitAny' and the channels to +listen on. The downstream component then listens to those channels, until they complete, and obeys the +'WaitAll' or 'WaitAny' as appropriate. + +For configuration of a downstream a component needs an `Opt` channel which can take generic configuration information +(most likely the payload here is a `Configuration` class with an `object` payload). + +## Consequences + +FBP is stongly aligned with workflows, so adopting concepts from FBP gives us a strong programming model to work with. +FBP has already solved many of the problems around running workflows, so it gives us a strong plan to work with. + diff --git a/samples/CommandProcessor/HelloWorld/HelloWorld.csproj b/samples/CommandProcessor/HelloWorld/HelloWorld.csproj index 4b7aa07aee..bb366252e6 100644 --- a/samples/CommandProcessor/HelloWorld/HelloWorld.csproj +++ b/samples/CommandProcessor/HelloWorld/HelloWorld.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/CommandProcessor/HelloWorldAsync/HelloWorldAsync.csproj b/samples/CommandProcessor/HelloWorldAsync/HelloWorldAsync.csproj index 83b173598a..f38311ceb8 100644 --- a/samples/CommandProcessor/HelloWorldAsync/HelloWorldAsync.csproj +++ b/samples/CommandProcessor/HelloWorldAsync/HelloWorldAsync.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/CommandProcessor/HelloWorldInternalBus/HelloWorldInternalBus.csproj b/samples/CommandProcessor/HelloWorldInternalBus/HelloWorldInternalBus.csproj index 8a8b8f1932..6ade50573d 100644 --- a/samples/CommandProcessor/HelloWorldInternalBus/HelloWorldInternalBus.csproj +++ b/samples/CommandProcessor/HelloWorldInternalBus/HelloWorldInternalBus.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable diff --git a/samples/TaskQueue/ASBTaskQueue/.env b/samples/TaskQueue/ASBTaskQueue/.env new file mode 100644 index 0000000000..c12b64a688 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/.env @@ -0,0 +1,13 @@ +# Environment file for user defined variables in docker-compose.yml + +# 1. CONFIG_PATH: Path to Config.json file +# Ex: CONFIG_PATH="C:\\Config\\Config.json" +CONFIG_PATH="./Config.json" + +# 2. ACCEPT_EULA: Pass 'Y' to accept license terms for Azure SQL Edge and Azure Service Bus emulator. +# Service Bus emulator EULA : https://github.com/Azure/azure-service-bus-emulator-installer/blob/main/EMULATOR_EULA.txt +# SQL Edge EULA : https://go.microsoft.com/fwlink/?linkid=2139274 +ACCEPT_EULA="Y" + +# 3. MSSQL_SA_PASSWORD to be filled by user as per policy : https://learn.microsoft.com/en-us/sql/relational-databases/security/strong-passwords?view=sql-server-linux-ver16 +SQL_PASSWORD: "Password1!" \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Config.Json b/samples/TaskQueue/ASBTaskQueue/Config.Json new file mode 100644 index 0000000000..39cd9b0b88 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Config.Json @@ -0,0 +1,135 @@ +{ + "UserConfig": { + "Namespaces": [ + { + "Name": "local", + "Queues": [], + "Topics": [ + { + "Name": "greeting.event", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + }, + { + "Name": "greeting.Asyncevent", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + }, + { + "Name": "greeting.addGreetingCommand", + "Properties": { + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "RequiresDuplicateDetection": false + }, + "Subscriptions": [ + { + "Name": "paramore.example.worker", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + }, + { + "Name": "paramore.example.greeting", + "Properties": { + "DeadLetteringOnMessageExpiration": false, + "DefaultMessageTimeToLive": "PT1H", + "DuplicateDetectionHistoryTimeWindow": "PT20S", + "ForwardDeadLetteredMessagesTo": "", + "ForwardTo": "", + "LockDuration": "PT1M", + "MaxDeliveryCount": 3, + "RequiresDuplicateDetection": false, + "RequiresSession": false + }, + "Rules": [] + } + ] + } + ] + } + ], + "Logging": { + "Type": "File" + } + } +} \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml b/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml new file mode 100644 index 0000000000..42b2a12dda --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Docker-Compose.yml @@ -0,0 +1,34 @@ +name: microsoft-azure-servicebus-emulator +services: + emulator: + container_name: "servicebus-emulator" + image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest + volumes: + - "${CONFIG_PATH}:/ServiceBus_Emulator/ConfigFiles/Config.json" + ports: + - "5672:5672" + environment: + SQL_SERVER: sqledge + MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # Password should be same as what is set for SQL Edge + ACCEPT_EULA: ${ACCEPT_EULA} + depends_on: + - sqledge + networks: + sb-emulator: + aliases: + - "sb-emulator" + sqledge: + container_name: "sqledge" + image: "mcr.microsoft.com/azure-sql-edge:latest" + ports: + - "11433:1433" + networks: + sb-emulator: + aliases: + - "sqledge" + environment: + ACCEPT_EULA: ${ACCEPT_EULA} + MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # To be filled by user as per policy : https://learn.microsoft.com/en-us/sql/relational-databases/security/strong-passwords?view=sql-server-linux-ver16 + +networks: + sb-emulator: \ No newline at end of file diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Greetings.csproj b/samples/TaskQueue/ASBTaskQueue/Greetings/Greetings.csproj index 31e49c202d..0236c62046 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Greetings.csproj +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Greetings.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs index 780c08114b..7693afc61d 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingAsyncEvent.cs @@ -5,13 +5,13 @@ namespace Greetings.Ports.Events { public class GreetingAsyncEvent : Event { - public GreetingAsyncEvent() : base(Guid.NewGuid()) { } + public GreetingAsyncEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingAsyncEvent(string greeting) : base(Guid.NewGuid()) + public GreetingAsyncEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; set; } } } diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs index 49d708e43d..ee5b2ed499 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Events/GreetingEvent.cs @@ -5,13 +5,13 @@ namespace Greetings.Ports.Events { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; set; } } } diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs index 72a2d9e816..4d1d5ca9f2 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/AddGreetingMessageMapper.cs @@ -1,13 +1,23 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Commands; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class AddGreetingMessageMapper : IAmAMessageMapper + public class AddGreetingMessageMapper : IAmAMessageMapper, IAmAMessageMapperAsync { public IRequestContext Context { get; set; } + public Task MapToMessageAsync(AddGreetingCommand request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); + public Message MapToMessage(AddGreetingCommand request, Publication publication) { var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_COMMAND); diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs index 4445aa5942..ed51fedfff 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventAsyncMessageMapper.cs @@ -1,13 +1,23 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Events; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class GreetingEventAsyncMessageMapper : IAmAMessageMapper + public class GreetingEventAsyncMessageMapper : IAmAMessageMapperAsync, IAmAMessageMapper { public IRequestContext Context { get; set; } + public Task MapToMessageAsync(GreetingAsyncEvent request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); + public Message MapToMessage(GreetingAsyncEvent request, Publication publication) { var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); diff --git a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs index 2ea5105881..7f2cd5ab1a 100644 --- a/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs +++ b/samples/TaskQueue/ASBTaskQueue/Greetings/Ports/Mappers/GreetingEventMessageMapper.cs @@ -1,12 +1,22 @@ using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Greetings.Ports.Events; using Paramore.Brighter; namespace Greetings.Ports.Mappers { - public class GreetingEventMessageMapper : IAmAMessageMapper + public class GreetingEventMessageMapper : IAmAMessageMapper, IAmAMessageMapperAsync { public IRequestContext Context { get; set; } + + public Task MapToMessageAsync(GreetingEvent request, Publication publication, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToMessage(request,publication)); + + public Task MapToRequestAsync(Message message, + CancellationToken cancellationToken = default) + => Task.FromResult(MapToRequest(message)); public Message MapToMessage(GreetingEvent request, Publication publication) { diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 4548b4832b..7b963a79ec 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs index 9652c3d265..cbf488d56b 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsReceiverConsole/Program.cs @@ -34,7 +34,7 @@ public async static Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Proactor), @@ -43,13 +43,13 @@ public async static Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Reactor) }; //TODO: add your ASB qualified name here - var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); + var clientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(clientProvider); services.AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/GreetingsScopedReceiverConsole.csproj b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/GreetingsScopedReceiverConsole.csproj index 64e39582ea..8e96318137 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/GreetingsScopedReceiverConsole.csproj +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/GreetingsScopedReceiverConsole.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs index a57cfe199b..749535903e 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs @@ -36,7 +36,7 @@ public static async Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Proactor), @@ -45,13 +45,13 @@ public static async Task Main(string[] args) new ChannelName("paramore.example.greeting"), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Proactor) }; //TODO: add your ASB qualified name here - var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); + var asbClientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(asbClientProvider); services .AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/GreetingsSender.Web.csproj b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/GreetingsSender.Web.csproj index f2add70d01..fa3cb5cdab 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/GreetingsSender.Web.csproj +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/GreetingsSender.Web.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs index d325ab613f..6143563f78 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -33,9 +33,9 @@ builder.Services.AddScoped(); //Brighter -string asbEndpoint = ".servicebus.windows.net"; +string asbEndpoint = "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; -var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); +var asbConnection = new ServiceBusConnectionStringClientProvider(asbEndpoint); var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); @@ -43,9 +43,9 @@ asbConnection, new AzureServiceBusPublication[] { - new() { Topic = new RoutingKey("greeting.event") }, - new() { Topic = new RoutingKey("greeting.addGreetingCommand") }, - new() { Topic = new RoutingKey("greeting.Asyncevent") } + new() { Topic = new RoutingKey("greeting.event"), MakeChannels = OnMissingChannel.Assume}, + new() { Topic = new RoutingKey("greeting.addGreetingCommand"), MakeChannels = OnMissingChannel.Assume }, + new() { Topic = new RoutingKey("greeting.Asyncevent"), MakeChannels = OnMissingChannel.Assume } } ) .Create(); diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/GreetingsSender.csproj index 465bc338c0..78334d7129 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs index 3a2695d38c..f7b6ef5f44 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsSender/Program.cs @@ -21,7 +21,7 @@ static void Main(string[] args) serviceCollection.AddLogging(); //TODO: add your ASB qualified name here - var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider("fim-development-bus.servicebus.windows.net"); + var asbClientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var producerRegistry = new AzureServiceBusProducerRegistryFactory( asbClientProvider, @@ -30,17 +30,21 @@ static void Main(string[] args) new AzureServiceBusPublication { Topic = new RoutingKey("greeting.event"), - RequestType = typeof(GreetingEvent) + RequestType = typeof(GreetingEvent), + MakeChannels = OnMissingChannel.Assume }, new AzureServiceBusPublication { Topic = new RoutingKey("greeting.addGreetingCommand"), - RequestType = typeof(AddGreetingCommand) + RequestType = typeof(AddGreetingCommand), + MakeChannels = OnMissingChannel.Assume + }, new AzureServiceBusPublication { Topic = new RoutingKey("greeting.Asyncevent"), - RequestType = typeof(GreetingAsyncEvent) + RequestType = typeof(GreetingAsyncEvent), + MakeChannels = OnMissingChannel.Assume } } ).Create(); diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/GreetingsWorker.csproj b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/GreetingsWorker.csproj index edcbe7411d..5051524757 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/GreetingsWorker.csproj +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/GreetingsWorker.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable diff --git a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs index 2a607272b3..672044b79a 100644 --- a/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/TaskQueue/ASBTaskQueue/GreetingsWorker/Program.cs @@ -34,7 +34,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.event"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Proactor, noOfPerformers: 2, unacceptableMessageLimit: 1), @@ -43,7 +43,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.Asyncevent"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Reactor, noOfPerformers: 2), @@ -52,7 +52,7 @@ new ChannelName(subscriptionName), new RoutingKey("greeting.addGreetingCommand"), timeOut: TimeSpan.FromMilliseconds(400), - makeChannels: OnMissingChannel.Create, + makeChannels: OnMissingChannel.Assume, requeueCount: 3, messagePumpType: MessagePumpType.Reactor, noOfPerformers: 2) @@ -66,8 +66,7 @@ o.UseSqlServer(dbConnString); }); -//TODO: add your ASB qualified name here -var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); +var clientProvider = new ServiceBusConnectionStringClientProvider("Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); var asbConsumerFactory = new AzureServiceBusConsumerFactory(clientProvider); builder.Services.AddServiceActivator(options => diff --git a/samples/TaskQueue/ASBTaskQueue/Readme.md b/samples/TaskQueue/ASBTaskQueue/Readme.md new file mode 100644 index 0000000000..9ea97249c1 --- /dev/null +++ b/samples/TaskQueue/ASBTaskQueue/Readme.md @@ -0,0 +1,5 @@ +#Docker Command + +```bash +podman compose -f .\Docker-Compose.yml up -d +``` \ No newline at end of file diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Greetings.csproj b/samples/TaskQueue/AWSTaskQueue/Greetings/Greetings.csproj index 6306699275..95f7fe2ee7 100644 --- a/samples/TaskQueue/AWSTaskQueue/Greetings/Greetings.csproj +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/GreetingsPumper.csproj b/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/GreetingsPumper.csproj index ace8afcfc4..3c0cf9f48e 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/GreetingsPumper.csproj +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/GreetingsPumper.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index c105a1f402..ca83cecf3f 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/GreetingsSender.csproj index e75a5ee9d7..42a35d75d5 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Greetings.csproj b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Greetings.csproj index 0cc26284ef..94882df7f4 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Greetings.csproj +++ b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/KafkaSchemaRegistry/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 54f052d3e2..a60566e311 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj index cbfb1e2490..cbfeaad003 100644 --- a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/KafkaTaskQueue/Greetings/Greetings.csproj b/samples/TaskQueue/KafkaTaskQueue/Greetings/Greetings.csproj index 834ffcf78d..c9ba4da872 100644 --- a/samples/TaskQueue/KafkaTaskQueue/Greetings/Greetings.csproj +++ b/samples/TaskQueue/KafkaTaskQueue/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/KafkaTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 522c7cc0e4..8fa38af1d5 100644 --- a/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/KafkaTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/GreetingsSender.csproj index 024f95e987..0baf08ba95 100644 --- a/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/MsSqlMessagingGateway/CompetingReceiverConsole/CompetingReceiverConsole.csproj b/samples/TaskQueue/MsSqlMessagingGateway/CompetingReceiverConsole/CompetingReceiverConsole.csproj index 0d6b236e4f..2e4a8d6a3a 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/CompetingReceiverConsole/CompetingReceiverConsole.csproj +++ b/samples/TaskQueue/MsSqlMessagingGateway/CompetingReceiverConsole/CompetingReceiverConsole.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/MsSqlMessagingGateway/CompetingSender/CompetingSender.csproj b/samples/TaskQueue/MsSqlMessagingGateway/CompetingSender/CompetingSender.csproj index 68b90c438f..591fc7d03b 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/CompetingSender/CompetingSender.csproj +++ b/samples/TaskQueue/MsSqlMessagingGateway/CompetingSender/CompetingSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/MsSqlMessagingGateway/Events/Events.csproj b/samples/TaskQueue/MsSqlMessagingGateway/Events/Events.csproj index 37cc8c8efb..4c6eb14627 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/Events/Events.csproj +++ b/samples/TaskQueue/MsSqlMessagingGateway/Events/Events.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs index 8d3d9250f0..0e361dd5b9 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/MsSqlMessagingGateway/Events/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Events.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/MsSqlMessagingGateway/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/MsSqlMessagingGateway/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 0b795d0f59..1da7fdb613 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/MsSqlMessagingGateway/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/TaskQueue/MsSqlMessagingGateway/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/MsSqlMessagingGateway/GreetingsSender/GreetingsSender.csproj index 1ddb077d5b..e759ec77c3 100644 --- a/samples/TaskQueue/MsSqlMessagingGateway/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/MsSqlMessagingGateway/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/RMQRequestReply/Greetings/Greetings.csproj b/samples/TaskQueue/RMQRequestReply/Greetings/Greetings.csproj index 53c9f9b5b5..71752606ba 100644 --- a/samples/TaskQueue/RMQRequestReply/Greetings/Greetings.csproj +++ b/samples/TaskQueue/RMQRequestReply/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 8 diff --git a/samples/TaskQueue/RMQRequestReply/GreetingsClient/GreetingsClient.csproj b/samples/TaskQueue/RMQRequestReply/GreetingsClient/GreetingsClient.csproj index 4a0232d8fc..2ccb7717ef 100644 --- a/samples/TaskQueue/RMQRequestReply/GreetingsClient/GreetingsClient.csproj +++ b/samples/TaskQueue/RMQRequestReply/GreetingsClient/GreetingsClient.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/RMQRequestReply/GreetingsServer/GreetingsServer.csproj b/samples/TaskQueue/RMQRequestReply/GreetingsServer/GreetingsServer.csproj index 8a2547a187..9c3d851b8f 100644 --- a/samples/TaskQueue/RMQRequestReply/GreetingsServer/GreetingsServer.csproj +++ b/samples/TaskQueue/RMQRequestReply/GreetingsServer/GreetingsServer.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/RMQTaskQueue/Greetings/Greetings.csproj b/samples/TaskQueue/RMQTaskQueue/Greetings/Greetings.csproj index 01265af12c..8bfedfa9d4 100644 --- a/samples/TaskQueue/RMQTaskQueue/Greetings/Greetings.csproj +++ b/samples/TaskQueue/RMQTaskQueue/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs index 4cc3cbdf09..ca64bfa2e7 100644 --- a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs +++ b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs @@ -5,17 +5,8 @@ namespace Greetings.Ports.Commands { [MessagePackObject(keyAsPropertyName: true)] - public class FarewellEvent : Event + public class FarewellEvent(string farewell) : Event(Guid.NewGuid().ToString()) { - public FarewellEvent() : base(Guid.NewGuid()) - { - } - - public FarewellEvent(string farewell) : base(Guid.NewGuid()) - { - Farewell = farewell; - } - - public string Farewell { get; set; } + public string Farewell { get; set; } = farewell; } } diff --git a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..7ba19b4a39 100644 --- a/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/TaskQueue/RMQTaskQueue/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,13 +29,13 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } - public string Greeting { get; set; } + public string? Greeting { get; } } } diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/TaskQueue/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 0bca6766d1..3777984cf2 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj index 17a4be5a6a..6bdfdcf24c 100644 --- a/samples/TaskQueue/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/RedisTaskQueue/Greetings/Greetings.csproj b/samples/TaskQueue/RedisTaskQueue/Greetings/Greetings.csproj index 9778a47699..a28455719c 100644 --- a/samples/TaskQueue/RedisTaskQueue/Greetings/Greetings.csproj +++ b/samples/TaskQueue/RedisTaskQueue/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs b/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs index 0b5e2de006..77877b3378 100644 --- a/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs +++ b/samples/TaskQueue/RedisTaskQueue/Greetings/Ports/Events/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Events { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/TaskQueue/RedisTaskQueue/GreetingsReceiver/GreetingsReceiver.csproj b/samples/TaskQueue/RedisTaskQueue/GreetingsReceiver/GreetingsReceiver.csproj index b1c46d334c..7f91fd28f1 100644 --- a/samples/TaskQueue/RedisTaskQueue/GreetingsReceiver/GreetingsReceiver.csproj +++ b/samples/TaskQueue/RedisTaskQueue/GreetingsReceiver/GreetingsReceiver.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/TaskQueue/RedisTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/TaskQueue/RedisTaskQueue/GreetingsSender/GreetingsSender.csproj index 62913c88f7..d1a5779d2b 100644 --- a/samples/TaskQueue/RedisTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/TaskQueue/RedisTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Greetings.csproj b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Greetings.csproj index 7d66f7b2c3..f532ad4fd0 100644 --- a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Greetings.csproj +++ b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/Transforms/AWSTransfomers/ClaimCheck/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 52257cb0ee..d1d7a76c73 100644 --- a/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsSender/GreetingsSender.csproj b/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsSender/GreetingsSender.csproj index 1b51e3d77d..74eadc4b86 100644 --- a/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsSender/GreetingsSender.csproj +++ b/samples/Transforms/AWSTransfomers/ClaimCheck/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/Transforms/AWSTransfomers/Compression/Greetings/Greetings.csproj b/samples/Transforms/AWSTransfomers/Compression/Greetings/Greetings.csproj index 7d66f7b2c3..f532ad4fd0 100644 --- a/samples/Transforms/AWSTransfomers/Compression/Greetings/Greetings.csproj +++ b/samples/Transforms/AWSTransfomers/Compression/Greetings/Greetings.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 diff --git a/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs b/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs index 7b07b481f8..010954cf68 100644 --- a/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs +++ b/samples/Transforms/AWSTransfomers/Compression/Greetings/Ports/Commands/GreetingEvent.cs @@ -29,9 +29,9 @@ namespace Greetings.Ports.Commands { public class GreetingEvent : Event { - public GreetingEvent() : base(Guid.NewGuid()) { } + public GreetingEvent() : base(Guid.NewGuid().ToString()) { } - public GreetingEvent(string greeting) : base(Guid.NewGuid()) + public GreetingEvent(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/Transforms/AWSTransfomers/Compression/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/Transforms/AWSTransfomers/Compression/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 52257cb0ee..d1d7a76c73 100644 --- a/samples/Transforms/AWSTransfomers/Compression/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/Transforms/AWSTransfomers/Compression/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 Exe diff --git a/samples/Transforms/AWSTransfomers/Compression/GreetingsSender/GreetingsSender.csproj b/samples/Transforms/AWSTransfomers/Compression/GreetingsSender/GreetingsSender.csproj index 1b51e3d77d..74eadc4b86 100644 --- a/samples/Transforms/AWSTransfomers/Compression/GreetingsSender/GreetingsSender.csproj +++ b/samples/Transforms/AWSTransfomers/Compression/GreetingsSender/GreetingsSender.csproj @@ -1,7 +1,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Common/DbMaker/DbMaker.csproj b/samples/WebAPI/WebAPI_Common/DbMaker/DbMaker.csproj index a54885ed38..d6db4cfaf2 100644 --- a/samples/WebAPI/WebAPI_Common/DbMaker/DbMaker.csproj +++ b/samples/WebAPI/WebAPI_Common/DbMaker/DbMaker.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/samples/WebAPI/WebAPI_Common/Greetings_Migrations/Greetings_Migrations.csproj b/samples/WebAPI/WebAPI_Common/Greetings_Migrations/Greetings_Migrations.csproj index 4fd1bde80f..e516e1c3ef 100644 --- a/samples/WebAPI/WebAPI_Common/Greetings_Migrations/Greetings_Migrations.csproj +++ b/samples/WebAPI/WebAPI_Common/Greetings_Migrations/Greetings_Migrations.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable disable Greetings_MySqlMigrations diff --git a/samples/WebAPI/WebAPI_Common/Salutations_Migrations/Salutations_Migrations.csproj b/samples/WebAPI/WebAPI_Common/Salutations_Migrations/Salutations_Migrations.csproj index 7af82a1944..b08eb85959 100644 --- a/samples/WebAPI/WebAPI_Common/Salutations_Migrations/Salutations_Migrations.csproj +++ b/samples/WebAPI/WebAPI_Common/Salutations_Migrations/Salutations_Migrations.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable disable diff --git a/samples/WebAPI/WebAPI_Common/TransportMaker/TransportMaker.csproj b/samples/WebAPI/WebAPI_Common/TransportMaker/TransportMaker.csproj index 87571c3ef0..73c38b9793 100644 --- a/samples/WebAPI/WebAPI_Common/TransportMaker/TransportMaker.csproj +++ b/samples/WebAPI/WebAPI_Common/TransportMaker/TransportMaker.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs index cf3d750338..d39584a91d 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Greeting.cs @@ -4,7 +4,7 @@ public class Greeting { public long Id { get; set; } - public string Message { get; set; } + public string? Message { get; set; } public long RecipientId { get; set; } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs index 0c7115cedb..45353b7e16 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Entities/Person.cs @@ -7,7 +7,7 @@ public class Person { public DateTime TimeStamp { get; set; } public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public IList Greetings { get; set; } = new List(); public Person() diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/GreetingsApp.csproj b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/GreetingsApp.csproj index 7bde62a7ad..4aeb7ac2c2 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/GreetingsApp.csproj +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/GreetingsApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs index 9ddaa8677d..eb5cab8446 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; @@ -33,7 +34,10 @@ public override async Task ExecuteAsync(FindPersonByName query await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); IEnumerable people = await connection.QueryAsync("select * from Person where name = @name", new { name = query.Name }); - Person person = people.SingleOrDefault(); + Person? person = people.SingleOrDefault(); + + if (person == null) + throw new InvalidOperationException($"Could not find person named {query.Name}"); return new FindPersonResult(person); } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs index e6f1544dc1..94b4816c3f 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Requests/GreetingMade.cs @@ -5,7 +5,7 @@ namespace GreetingsApp.Requests; public class GreetingMade : Event { - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs index 30d7b5851c..5a5c5511a2 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsApp/Responses/FindPersonsGreetings.cs @@ -4,8 +4,8 @@ namespace GreetingsApp.Responses; public class FindPersonsGreetings { - public string Name { get; set; } - public IEnumerable Greetings { get; set; } + public string? Name { get; set; } + public IEnumerable? Greetings { get; set; } } public class Salutation diff --git a/samples/WebAPI/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index db84d46a44..b6c7075c2a 100644 --- a/samples/WebAPI/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dapper/Greetings_Sweeper/Greetings_Sweeper.csproj b/samples/WebAPI/WebAPI_Dapper/Greetings_Sweeper/Greetings_Sweeper.csproj index c5a75957a4..c8442a023b 100644 --- a/samples/WebAPI/WebAPI_Dapper/Greetings_Sweeper/Greetings_Sweeper.csproj +++ b/samples/WebAPI/WebAPI_Dapper/Greetings_Sweeper/Greetings_Sweeper.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj index 264b4cc002..8bd597402f 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs index baa02d99c1..d5ac0d1d0f 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/GreetingMade.cs @@ -5,7 +5,7 @@ namespace SalutationApp.Requests; public class GreetingMade : Event { - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs index b5bda2faf1..2adea17345 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_Dapper/SalutationApp/Requests/SalutationReceived.cs @@ -5,7 +5,7 @@ namespace SalutationApp.Requests; public class SalutationReceived : Event { - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) + public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid().ToString()) { ReceivedAt = receivedAt; } diff --git a/samples/WebAPI/WebAPI_Dapper/SalutationApp/SalutationApp.csproj b/samples/WebAPI/WebAPI_Dapper/SalutationApp/SalutationApp.csproj index e49585ac83..c8a5c82a84 100644 --- a/samples/WebAPI/WebAPI_Dapper/SalutationApp/SalutationApp.csproj +++ b/samples/WebAPI/WebAPI_Dapper/SalutationApp/SalutationApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dapper/Salutation_Sweeper/Salutation_Sweeper.csproj b/samples/WebAPI/WebAPI_Dapper/Salutation_Sweeper/Salutation_Sweeper.csproj index f30caad653..c77d888008 100644 --- a/samples/WebAPI/WebAPI_Dapper/Salutation_Sweeper/Salutation_Sweeper.csproj +++ b/samples/WebAPI/WebAPI_Dapper/Salutation_Sweeper/Salutation_Sweeper.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true diff --git a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/GreetingsApp.csproj b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/GreetingsApp.csproj index a9e66b0457..5cd7652924 100644 --- a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/GreetingsApp.csproj +++ b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/GreetingsApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs index 56dfc2f215..ba8b0b7ff8 100644 --- a/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dynamo/GreetingsApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dynamo/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI/WebAPI_Dynamo/GreetingsWeb/GreetingsWeb.csproj index 957a6509ef..b857267f04 100644 --- a/samples/WebAPI/WebAPI_Dynamo/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI/WebAPI_Dynamo/GreetingsWeb/GreetingsWeb.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dynamo/Greetings_Sweeper/Greetings_Sweeper.csproj b/samples/WebAPI/WebAPI_Dynamo/Greetings_Sweeper/Greetings_Sweeper.csproj index d320e2c793..74cd4fb3f4 100644 --- a/samples/WebAPI/WebAPI_Dynamo/Greetings_Sweeper/Greetings_Sweeper.csproj +++ b/samples/WebAPI/WebAPI_Dynamo/Greetings_Sweeper/Greetings_Sweeper.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/SalutationAnalytics.csproj index 660cbe711a..33d9676345 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationAnalytics/SalutationAnalytics.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs index 26ba345c0c..6dc1849a0a 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs index 1c00ad06c1..e6c70d73dd 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/Requests/SalutationReceived.cs @@ -7,7 +7,7 @@ public class SalutationReceived : Event { public DateTimeOffset ReceivedAt { get; } - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) + public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid().ToString()) { ReceivedAt = receivedAt; } diff --git a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/SalutationApp.csproj b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/SalutationApp.csproj index e59cff1e9b..53c678b77c 100644 --- a/samples/WebAPI/WebAPI_Dynamo/SalutationApp/SalutationApp.csproj +++ b/samples/WebAPI/WebAPI_Dynamo/SalutationApp/SalutationApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/GreetingsApp.csproj b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/GreetingsApp.csproj index 87420e5a89..09673bee7a 100644 --- a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/GreetingsApp.csproj +++ b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/GreetingsApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs index 56dfc2f215..ba8b0b7ff8 100644 --- a/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_EFCore/GreetingsApp/Requests/GreetingMade.cs @@ -7,7 +7,7 @@ public class GreetingMade : Event { public string Greeting { get; set; } - public GreetingMade(string greeting) : base(Guid.NewGuid()) + public GreetingMade(string greeting) : base(Guid.NewGuid().ToString()) { Greeting = greeting; } diff --git a/samples/WebAPI/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj index 2a83dc0092..faefc47426 100644 --- a/samples/WebAPI/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/samples/WebAPI/WebAPI_EFCore/Greetings_Sweeper/Greetings_Sweeper.csproj b/samples/WebAPI/WebAPI_EFCore/Greetings_Sweeper/Greetings_Sweeper.csproj index d320e2c793..74cd4fb3f4 100644 --- a/samples/WebAPI/WebAPI_EFCore/Greetings_Sweeper/Greetings_Sweeper.csproj +++ b/samples/WebAPI/WebAPI_EFCore/Greetings_Sweeper/Greetings_Sweeper.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable true diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj index 34bfd4d854..9c7e7a473d 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs index 26ba345c0c..35ad4bfcac 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs +++ b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/GreetingMade.cs @@ -3,13 +3,8 @@ namespace SalutationApp.Requests { - public class GreetingMade : Event + public class GreetingMade(string greeting) : Event(Guid.NewGuid().ToString()) { - public string Greeting { get; set; } - - public GreetingMade(string greeting) : base(Guid.NewGuid()) - { - Greeting = greeting; - } + public string Greeting { get; init; } = greeting; } } diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs index 1c00ad06c1..355c437f75 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs +++ b/samples/WebAPI/WebAPI_EFCore/SalutationApp/Requests/SalutationReceived.cs @@ -3,13 +3,8 @@ namespace SalutationApp.Requests { - public class SalutationReceived : Event + public class SalutationReceived(DateTimeOffset receivedAt) : Event(Guid.NewGuid().ToString()) { - public DateTimeOffset ReceivedAt { get; } - - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) - { - ReceivedAt = receivedAt; - } + public DateTimeOffset ReceivedAt { get; } = receivedAt; } } diff --git a/samples/WebAPI/WebAPI_EFCore/SalutationApp/SalutationApp.csproj b/samples/WebAPI/WebAPI_EFCore/SalutationApp/SalutationApp.csproj index 2cc4f0c82a..3d46ff2973 100644 --- a/samples/WebAPI/WebAPI_EFCore/SalutationApp/SalutationApp.csproj +++ b/samples/WebAPI/WebAPI_EFCore/SalutationApp/SalutationApp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 diff --git a/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj b/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj index 7080416e0e..358f52d0ac 100644 --- a/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj +++ b/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable Paramore.Brighter.Storage.Azure diff --git a/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs new file mode 100644 index 0000000000..37d3798c68 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/IAmAJobChannel.cs @@ -0,0 +1,63 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// Represents a channel for job processing in a workflow. +/// +/// The type of the workflow data. +public interface IAmAJobChannel +{ + /// + /// Enqueues a job to the channel. + /// + /// The job to enqueue. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous enqueue operation. + Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Dequeues a job from the channel. + /// + /// + /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. + Task?> DequeueJobAsync(CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Streams jobs from the channel. + /// + /// An asynchronous enumerable of jobs. + IAsyncEnumerable> Stream(); + + /// + /// Determines whether the channel is closed. + /// + /// true if the channel is closed; otherwise, false. + bool IsClosed(); +} diff --git a/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs new file mode 100644 index 0000000000..67840cccb1 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/IAmAStateStoreAsync.cs @@ -0,0 +1,58 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// Used to store the state of a workflow +/// +public interface IAmAStateStoreAsync +{ + /// + /// Saves the job + /// + /// The job + /// + Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Retrieves a job via its Id + /// + /// The id of the job + /// if found, the job, otherwise null + Task GetJobAsync(string? id) ; + + /// + /// + /// + /// The time before now at which becomes scheduled + /// + /// + Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)); +} diff --git a/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs new file mode 100644 index 0000000000..4b3314ab2b --- /dev/null +++ b/src/Paramore.Brighter.Mediator/InMemoryJobChannel.cs @@ -0,0 +1,134 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.Mediator; + +/// +/// Specifies the strategy to use when the channel is full. +/// +public enum FullChannelStrategy +{ + /// + /// Wait for space to become available in the channel. + /// + Wait, + + /// + /// Drop the oldest item in the channel to make space. + /// + Drop +} + +/// +/// Represents an in-memory job channel for processing jobs. +/// +/// The type of the job data. +public class InMemoryJobChannel : IAmAJobChannel +{ + private readonly Channel> _channel; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of jobs the channel can hold. + /// The strategy to use when the channel is full. + /// Thrown when the bounded capacity is less than or equal to 0. + public InMemoryJobChannel(int boundedCapacity = 100, FullChannelStrategy fullChannelStrategy = FullChannelStrategy.Wait) + { + if (boundedCapacity <= 0) + throw new System.ArgumentOutOfRangeException(nameof(boundedCapacity), "Bounded capacity must be greater than 0"); + + _channel = System.Threading.Channels.Channel.CreateBounded>(new BoundedChannelOptions(boundedCapacity) + { + SingleWriter = true, + SingleReader = false, + AllowSynchronousContinuations = true, + FullMode = fullChannelStrategy == FullChannelStrategy.Wait ? + BoundedChannelFullMode.Wait : + BoundedChannelFullMode.DropOldest + }); + } + + /// + /// Dequeues a job from the channel. + /// + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous dequeue operation. The task result contains the dequeued job. + public async Task?> DequeueJobAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + Job? item = null; + while (await _channel.Reader.WaitToReadAsync(cancellationToken)) + while (_channel.Reader.TryRead(out item)) + return item; + + return item; + } + + /// + /// Enqueues a job to the channel. + /// + /// The job to enqueue. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous enqueue operation. + public async Task EnqueueJobAsync(Job job, CancellationToken cancellationToken = default(CancellationToken)) + { + await _channel.Writer.WriteAsync(job, cancellationToken); + } + + /// + /// Determines whether the channel is closed. + /// + /// true if the channel is closed; otherwise, false. + public bool IsClosed() + { + return _channel.Reader.Completion.IsCompleted; + } + + /// + /// This is mainly useful for help with testing, to stop the channel + /// + public void Stop() + { + _channel.Writer.Complete(); + } + + /// + /// Streams jobs from the channel. + /// + /// An asynchronous enumerable of jobs. + public IAsyncEnumerable> Stream() + { + return _channel.Reader.ReadAllAsync(); + } +} diff --git a/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs new file mode 100644 index 0000000000..91ea05ae1e --- /dev/null +++ b/src/Paramore.Brighter.Mediator/InMemoryStateStoreAsync.cs @@ -0,0 +1,118 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.Mediator; + +/// +/// Represents an in-memory store for jobs. +/// +public class InMemoryStateStoreAsync : IAmAStateStoreAsync +{ + private readonly ConcurrentDictionary _jobs = new(); + private readonly TimeProvider _timeProvider; + private DateTimeOffset _sinceTime; + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + /// + /// Represents an in-memory store for jobs. + /// + public InMemoryStateStoreAsync(TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + } + + /// + /// + /// + /// A job is due now, less the jobAge span + /// A cancellation token to end the ongoing operation + /// + public Task> GetDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)) + { + var dueJobs = _jobs.Values + .Where(job => + { + if (job is null || !job.IsScheduled) return false; + _sinceTime = _timeProvider.GetUtcNow().Subtract(jobAge); + return job.DueTime > _sinceTime; + }) + .ToList(); + + return Task.FromResult((IEnumerable)dueJobs); + } + + /// + /// Retrieves a job asynchronously by its Id. + /// + /// The Id of the job. + /// A task that represents the asynchronous retrieve operation. The task result contains the job if found; otherwise, null. + public Task GetJobAsync(string? id) + { + var tcs = new TaskCompletionSource(); + if (id is null) + { + tcs.SetResult(null); + return tcs.Task; + } + + var job = _jobs.TryGetValue(id, out var state) ? state : null; + tcs.SetResult(job); + return tcs.Task; + } + + /// + /// Saves the job asynchronously. + /// + /// The type of the job data. + /// The job to save. + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous save operation. + public Task SaveJobAsync(Job? job, CancellationToken cancellationToken = default(CancellationToken)) + { + if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); + + if (job is null) return Task.CompletedTask; + + try + { + _jobs[job.Id] = job; + return Task.FromResult(true); + } + catch (Exception e) + { + s_logger.LogError($"Error saving job {job.Id} to in-memory store: {e.Message}"); + return Task.FromException(e); + } + } +} diff --git a/src/Paramore.Brighter.Mediator/Job.cs b/src/Paramore.Brighter.Mediator/Job.cs new file mode 100644 index 0000000000..2f5db8a3df --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Job.cs @@ -0,0 +1,185 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Paramore.Brighter.Mediator; + +/// +/// What state is the workflow in +/// +public enum JobState +{ + Ready, + Running, + Waiting, + Done, + Faulted +} + +/// +/// empty class, used as marker for the branch data +/// +public abstract class Job +{ + /// Used to manage access to state, as the job may be updated from multiple threads + protected readonly object LockObject = new(); + + /// The time the job is due to run + public DateTimeOffset? DueTime { get; set; } + + /// The id of the workflow, used to save-retrieve it from storage + public string Id { get; private set; } = Guid.NewGuid().ToString(); + + /// Is the job scheduled to run? + public bool IsScheduled => DueTime.HasValue; + + /// The id of the parent job, if this is a child job + public string? ParentId { get; set; } + + /// Is the job waiting to be run, running, waiting for a response or finished + public JobState State { get; set; } +} + +/// +/// Job represents the current state of the workflow and tracks if it’s awaiting a response. +/// +/// The user defined data for the workflow +public class Job : Job +{ + + /// If we are awaiting a response, we store the type of the response and the action to take when it arrives + private readonly ConcurrentDictionary?> _pendingResponses = new(); + + /// The next step. Steps are a linked list. The final step in the list has null for it's next step. + private Step? _step; + + private ConcurrentDictionary _children = new(); + + /// The data that is passed between steps of the workflow + public TData Data { get; private set; } + + + /// + /// Constructs a new Job + /// State which is passed between steps of the workflow + /// + public Job(TData data) + { + Data = data; + State = JobState.Ready; + } + + /// + /// Initializes the steps of the workflow. + /// + /// The first step of the workflow to execute. + public void InitSteps(Step? firstStep) + { + NextStep(firstStep); + } + + /// + /// Gets the current step of the workflow. + /// + /// The current step of the workflow. + public Step? CurrentStep() + { + return _step; + } + + /// + /// Adds a pending response to the job. + /// + /// The expected type of the response + /// The task response to add. + public void AddPendingResponse(Type responseType, TaskResponse? taskResponse) + { + lock (LockObject) + { + State = JobState.Waiting; + if (!_pendingResponses.TryAdd(responseType, taskResponse)) + throw new InvalidOperationException($"A pending response for {responseType} already exists"); + } + } + + /// + /// Finds a pending response by its type. + /// + /// The type of the event. + /// The task response if found. + public bool FindPendingResponse(Type eventType, out TaskResponse? taskResponse) + { + return _pendingResponses.TryGetValue(eventType, out taskResponse); + } + + /// + /// Sets the next step of the workflow. + /// + /// The next step to set. + public void NextStep(Step? nextStep) + { + lock (LockObject) + { + _step = nextStep; + if (_step is not null) + _step.AddToJob(this); + else if (State != JobState.Waiting) + State = JobState.Done; + } + } + + /// + /// Removes a pending response from the job, and resets the state to running. + /// + /// The type of event that we expect + /// + public bool ResumeAfterEvent(Type eventType) + { + if (_step is null) return false; + + lock (LockObject) + { + var success = _pendingResponses.Remove(eventType, out _); + _step.OnCompletion?.Invoke(); + _step = _step.Next; + if (success) State = JobState.Running; + return success; + } + } + + /// + /// Sets an identifier on each child to indicate the parent id + /// Adds the child to a hashtable of children + /// + /// The job we want to add as a child + public void AddChildJob(Job child) + { + child.ParentId = Id; + _children.TryAdd(child.Id, child); + } + +} diff --git a/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj new file mode 100644 index 0000000000..093a5405aa --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Paramore.Brighter.Mediator.csproj @@ -0,0 +1,23 @@ + + + The Command Dispatcher pattern is an addition to the Command design pattern that decouples the dispatcher for a service from its execution. A Command Dispatcher component maps commands to handlers. A Command Processor pattern provides a framework for handling orthogonal concerns such as logging, timeouts, or circuit breakers + Ian Cooper + net8.0;net9.0 + Command;Event;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability + latest + enable + + + + + + + + + + + + + + + diff --git a/src/Paramore.Brighter.Mediator/Runner.cs b/src/Paramore.Brighter.Mediator/Runner.cs new file mode 100644 index 0000000000..1ea7ea2082 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Runner.cs @@ -0,0 +1,143 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.Mediator; + +/// +/// The class processes jobs from a job channel and executes them. +/// +/// The type of the workflow data. +public class Runner +{ + private readonly IAmAJobChannel _channel; + private readonly IAmAStateStoreAsync _stateStore; + private readonly IAmACommandProcessor _commandProcessor; + private readonly Scheduler _scheduler; + private readonly string _runnerName = Guid.NewGuid().ToString("N"); + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); + + /// + /// Initializes a new instance of the class. + /// + /// The job channel to process jobs from. + /// The job store to save job states. + /// The command processor to handle commands. + /// The scheduler which allows us to queue work that should be deferred + public Runner(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, IAmACommandProcessor commandProcessor, Scheduler scheduler) + { + _channel = channel; + _stateStore = stateStore; + _commandProcessor = commandProcessor; + _scheduler = scheduler; + } + + /// + /// Runs the job processing loop. + /// + /// A token to monitor for cancellation requests. + public void RunAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + s_logger.LogInformation("Starting runner {RunnerName}", _runnerName); + + var task = Task.Factory.StartNew(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + await ProcessJobs(cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + }, cancellationToken); + + Task.WaitAll([task], cancellationToken); + + s_logger.LogInformation("Finished runner {RunnerName}", _runnerName); + } + + private async Task Execute(Job? job, CancellationToken cancellationToken = default(CancellationToken)) + { + if (job is null) + return; + + s_logger.LogInformation("Executing job {JobId} on runner {RunnerName}", job.Id, _runnerName); + + job.State = JobState.Running; + await _stateStore.SaveJobAsync(job, cancellationToken); + + var step = job.CurrentStep(); + while (step is not null) + { + s_logger.LogInformation("Step is {StepName} with state {StepStste}", step.Name, step.State); + if (step.State == StepState.Queued) + { + await step.ExecuteAsync(_stateStore, _commandProcessor, _scheduler, cancellationToken); + } + + //if the job has a pending step, finish execution of this job. + if (job.State == JobState.Waiting) + break; + + //assume execute has advanced he step, if you your step loops endlessly it has not advanced the step!! + step = job.CurrentStep(); + s_logger.LogInformation( + "Next step is {StepName} with state {StepState}", + step is not null ? step.Name : "flow ends", + step is not null ? step.State : StepState.Done); + } + + if (job.State != JobState.Waiting) + job.State = JobState.Done; + + s_logger.LogInformation("Finished executing job {JobId} on {RunnerName}", job.Id, _runnerName); + } + + private async Task ProcessJobs(CancellationToken cancellationToken = default(CancellationToken)) + { + while (true) + { + if (cancellationToken.IsCancellationRequested) + break; + + if (_channel.IsClosed()) + break; + + s_logger.LogInformation("Looking for jobs on {RunnerName}", _runnerName); + var job = await _channel.DequeueJobAsync(cancellationToken); + if (job is null) + continue; + + s_logger.LogInformation("Executing job {JobId} on {RunnerName}", job.Id, _runnerName); + await Execute(job, cancellationToken); + s_logger.LogInformation("Finished job {JobId} on {RunnerName}", job.Id, _runnerName); + } + } +} diff --git a/src/Paramore.Brighter.Mediator/Scheduler.cs b/src/Paramore.Brighter.Mediator/Scheduler.cs new file mode 100644 index 0000000000..0cddc7028a --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Scheduler.cs @@ -0,0 +1,149 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// The class orchestrates a workflow by executing each step in a sequence. +/// It uses a command processor and a workflow store to manage the workflow's state and actions. +/// +/// The type of the workflow data. +public class Scheduler +{ + private readonly IAmAJobChannel _channel; + private readonly IAmAStateStoreAsync _stateStore; + private readonly TimeProvider _timeProvider; + + + /// + /// Initializes a new instance of the class. + /// + /// The over which jobs flow. The is a producer + /// and the is the consumer from the channel + /// A store for pending jobs + /// Provides the time for scheduling, defaults to TimeProvider.System + public Scheduler(IAmAJobChannel channel, IAmAStateStoreAsync stateStore, TimeProvider? timeProvider = null) + { + _timeProvider = timeProvider ?? TimeProvider.System; + _channel = channel; + _stateStore = stateStore; + } + + /// + /// Call this method from a RequestHandler that listens for an expected event. This will process that event if there is a pending response for the event type. + /// + /// The event to process. + /// Thrown when the workflow has not been initialized. + public async Task ResumeAfterEvent(Event @event) + { + if (@event.CorrelationId is null) + throw new InvalidOperationException("CorrelationId should not be null; needed to retrieve state of workflow"); + + var w = await _stateStore.GetJobAsync(@event.CorrelationId); + + if (w is not Job job) + throw new InvalidOperationException("Branch has not been stored"); + + var eventType = @event.GetType(); + + if (!job.FindPendingResponse(eventType, out TaskResponse? taskResponse)) + return; + + if (taskResponse is null || taskResponse.Parser is null) + throw new InvalidOperationException($"Parser for event type {eventType} should not be null"); + + if (job.CurrentStep() is null) + throw new InvalidOperationException($"Current step of workflow #{job.Id} should not be null"); + + taskResponse.Parser(@event, job); + job.ResumeAfterEvent(eventType); + + await ScheduleAsync(job); + } + + /// + /// Runs the job by executing each step in the sequence. + /// + /// The job that we want a runner to execute + /// A cancellation token to end the ongoing operation + /// Thrown when the job has not been initialized. + public async Task ScheduleAsync(Job job,CancellationToken cancellationToken = default(CancellationToken)) + { + await _channel.EnqueueJobAsync(job, cancellationToken); + job.DueTime = null; // Clear any due time after queuing + await _stateStore.SaveJobAsync(job, cancellationToken); + } + + /// + /// Schedules a list of jobs + /// + /// The jobs to schedule + /// A cancellation token to terminate the asynchronous operation + public async Task ScheduleAsync(IEnumerable> jobs, CancellationToken cancellationToken = default(CancellationToken)) + { + foreach (var job in jobs) + { + await ScheduleAsync(job, cancellationToken); + } + } + + /// + /// + /// + /// The job that we want a runner to execute + /// The delay after which to schedule the job + /// A cancellation token to end the ongoing operation + /// Thrown when the job has not been initialized. + public async Task ScheduleAtAsync(Job job, TimeSpan delay, CancellationToken cancellationToken = default(CancellationToken)) + { + job.DueTime = _timeProvider.GetUtcNow().Add(delay); + await _stateStore.SaveJobAsync(job, cancellationToken); + } + + /// + /// Finds any jobs that are due to run and schedules them + /// + /// A job is due now, less the jobAge span + /// A cancellation token to end the ongoing operation + public async Task TriggerDueJobsAsync(TimeSpan jobAge, CancellationToken cancellationToken = default(CancellationToken)) + { + var dueJobs = await _stateStore.GetDueJobsAsync(jobAge, cancellationToken); + + foreach (var j in dueJobs) + { + var job = j as Job; + + if (job is null) + continue; + + await ScheduleAsync(job, cancellationToken); + } + } + +} diff --git a/src/Paramore.Brighter.Mediator/Specification.cs b/src/Paramore.Brighter.Mediator/Specification.cs new file mode 100644 index 0000000000..b17d32fb3b --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Specification.cs @@ -0,0 +1,78 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +namespace Paramore.Brighter.Mediator; + +using System; + +public interface ISpecification +{ + bool IsSatisfiedBy(TData entity); + + ISpecification And(ISpecification other); + ISpecification Or(ISpecification other); + ISpecification Not(); + ISpecification AndNot(ISpecification other); + ISpecification OrNot(ISpecification other); +} + +public class Specification : ISpecification +{ + private readonly Func _expression; + + public Specification(Func expression) + { + _expression = expression ?? throw new ArgumentNullException(nameof(expression)); + } + + public bool IsSatisfiedBy(T entity) + { + return _expression(entity); + } + + public ISpecification And(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) && other.IsSatisfiedBy(x)); + } + + public ISpecification Or(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) || other.IsSatisfiedBy(x)); + } + + public ISpecification Not() + { + return new Specification(x => !IsSatisfiedBy(x)); + } + + public ISpecification AndNot(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) && !other.IsSatisfiedBy(x)); + } + + public ISpecification OrNot(ISpecification other) + { + return new Specification(x => IsSatisfiedBy(x) || !other.IsSatisfiedBy(x)); + } +} diff --git a/src/Paramore.Brighter.Mediator/Steps.cs b/src/Paramore.Brighter.Mediator/Steps.cs new file mode 100644 index 0000000000..9c4495de05 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Steps.cs @@ -0,0 +1,355 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.Mediator; + +public enum StepState +{ + Queued, + Running, + Done, + Faulted +} + +/// +/// The base type for a step in the workflow. +/// +/// The name of the step, used for tracing execution +/// The next step in the sequence, null if this is the last step. +/// The action to be taken with the step, null if no action +/// An optional callback to run, following completion of the step +/// The data that the step operates over +public abstract class Step( + string name, + Sequential? next, + IStepTask? stepTask = null, + Action? onCompletion = null) +{ + /// Which job is being executed by the step. + protected Job? Job ; + + /// The logger for the step. + protected static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); + + /// The name of the step, used for tracing execution + public string Name { get; init; } = name; + + /// The next step in the sequence, null if this is the last step + protected internal Step? Next { get; } = next; + + /// An optional callback to be run, following completion of the step. + protected internal Action? OnCompletion { get; } = onCompletion; + + /// The action to be taken with the step. + protected readonly IStepTask? StepTask = stepTask; + + public StepState? State { get; set; } + + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait + /// The cancellation token, to end this workflow + /// + public abstract Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default(CancellationToken) + ); + + /// + /// Sets the job that is executing us + /// + /// The job that we are executing under + public void AddToJob(Job job) + { + Job = job; + State = StepState.Queued; + } +} + +/// +/// Allows the workflow to branch on a choice, taking either a right or left path. +/// +/// The name of the step, used for tracing execution +/// A composite specification that can be evaluated to determine the path to choose +/// An optional callback to run, following completion of the step +/// The next step in the sequence, if the predicate evaluates to true, null if this is the last step. +/// The next step in the sequence, if the predicate evaluates to false, null if this is the last step. +/// The data that the step operates over +public class ExclusiveChoice( + string name, + ISpecification predicate, + Action? onCompletion, + Sequential? nextTrue, + Sequential? nextFalse +) + : Step(name, null, null, onCompletion) +{ + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait + /// The cancellation token, to end this workflow + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (Job is null) + throw new InvalidOperationException("Job is null"); + + State = StepState.Running; + + var step = predicate.IsSatisfiedBy(Job.Data) ? nextTrue : nextFalse; + + State = StepState.Done; + + if (step != null) + step.State = StepState.Queued; + + Job.NextStep(step); + OnCompletion?.Invoke(); + await stateStore.SaveJobAsync(Job, cancellationToken); + + } +} + +public class ParallelSplit( + string name, + Func>>? onMap) + : Step(name, null) +{ + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait + /// The cancellation token, to end this workflow + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (Job is null) + throw new InvalidOperationException("Job is null"); + + if (onMap is null) + throw new InvalidOperationException("onMap is null; a ParallelSplit Step must have a mapping function to map to multiple branches"); + + if (scheduler is null) + throw new InvalidOperationException("Scheduler is null; a ParallelSplit Step must have a scheduler to schedule the next step"); + + State = StepState.Running; + + //Map to multiple branches + var branches = onMap?.Invoke(Job.Data); + + if (branches is null) + return; + + foreach (Step branch in branches) + { + var childJob = new Job(Job.Data); + childJob.AddChildJob(Job); + childJob.InitSteps(branch); + await scheduler.ScheduleAsync(childJob, cancellationToken); + } + + State = StepState.Done; + + //NOTE: parallel split is a final step - this might change when we bring in merge + Job.NextStep(null); + await stateStore.SaveJobAsync(Job, cancellationToken); + } +} + +/// +/// Represents a sequential step in the workflow. Control flows to the next step in the list, or ends if next is null. +/// A set of sequential steps for a linked list. +/// +/// The name of the step, used for tracing execution +/// The action to be taken with the step. +/// An optional callback to run, following completion of the step +/// The next step in the sequence, null if this is the last step. +/// An optional callback to run, following a faulted execution of the step +/// The next step in the sequence, following a faulted execution of the step +/// The data that the step operates over +public class Sequential( + string name, + IStepTask stepTask, + Action? onCompletion, + Sequential? next, + Action? onFaulted = null, + Sequential? faultNext = null +) + : Step(name, next, stepTask, onCompletion) +{ + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait + /// The cancellation token, to end this workflow + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (Job is null) + throw new InvalidOperationException("Job is null"); + + if (StepTask is null) + { + s_logger.LogWarning("No task to execute for {Name}", Name); + State = StepState.Done; + await stateStore.SaveJobAsync(Job, cancellationToken); + return; + } + + State = StepState.Running; + + try + { + await StepTask.HandleAsync(Job, commandProcessor, stateStore, cancellationToken); + OnCompletion?.Invoke(); + State = StepState.Done; + + if(Next != null) + Next.State = StepState.Queued; + + Job.NextStep(Next); + await stateStore.SaveJobAsync(Job, cancellationToken); + } + catch (Exception) + { + Job.State = JobState.Faulted; + onFaulted?.Invoke(); + + if (faultNext != null) + faultNext.State = StepState.Queued; + + Job.NextStep(faultNext); + State = StepState.Faulted; + await stateStore.SaveJobAsync(Job, cancellationToken); + } + } +} + +/// +/// Allows the workflow to pause. This is a blocking operation that pauses the executing thread +/// +/// The data that the step operates over +public class Wait : Step +{ + private readonly TimeSpan _duration; + + /// + /// Allows the workflow to pause. This is a blocking operation that pauses the executing thread + /// + /// The name of the step, used for tracing execution + /// The period for which we pause + /// The next step in the sequence, null if this is the last step. + /// The data that the step operates over + public Wait(string name, TimeSpan duration, Sequential? next) + : base(name, next) + { + _duration = duration; + } + + /// + /// The work of the step is done here. Note that this is an abstract method, so it must be implemented by the derived class. + /// Your application logic does not live in the step. Instead, you raise a command to a handler, which will do the work. + /// The purpose of the step is to orchestrate the workflow, not to do the work. + /// + /// If the step updates the job, it needs to save its new state + /// The command processor, used to send requests to complete steps + /// The scheduler, used for queuing jobs that need to wait + /// The cancellation token, to end this workflow + /// + public override async Task ExecuteAsync( + IAmAStateStoreAsync stateStore, + IAmACommandProcessor? commandProcessor = null, + Scheduler? scheduler = null, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (Job is null) + throw new InvalidOperationException("Job is null"); + + if (scheduler is null) + throw new InvalidOperationException("Scheduler is null; a Wait Step must have a scheduler to schedule the next step"); + + if (Next == null) + throw new InvalidOperationException("Next step is empty; wait schedule the next step, so it cannot be empty"); + + State = StepState.Running; + + Job.DueTime = DateTime.UtcNow.Add(_duration); + + State = StepState.Done; + + Next.State = StepState.Queued; + + Job.NextStep(Next); + + Job.State = JobState.Waiting; + + //this call will save the state of the Job, so no need to do it twice + await scheduler.ScheduleAtAsync(Job, _duration, cancellationToken); + } +} + + diff --git a/src/Paramore.Brighter.Mediator/TaskException.cs b/src/Paramore.Brighter.Mediator/TaskException.cs new file mode 100644 index 0000000000..1bc37a2708 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/TaskException.cs @@ -0,0 +1,53 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Runtime.Serialization; + +namespace Paramore.Brighter.Mediator; + +/// +/// Represents errors that occur during task execution. +/// +[Serializable] +public class TaskException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public TaskException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public TaskException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public TaskException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/src/Paramore.Brighter.Mediator/Tasks.cs b/src/Paramore.Brighter.Mediator/Tasks.cs new file mode 100644 index 0000000000..7e4854cbfc --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Tasks.cs @@ -0,0 +1,264 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Mediator; + +/// +/// Defines an interface for workflow actions. +/// +/// The type of the workflow data. +public interface IStepTask +{ + /// + /// Handles the workflow action. + /// + /// The current job of the workflow. + /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler + /// The cancellation token for this task + Task HandleAsync(Job? job, IAmACommandProcessor? commandProcessor, IAmAStateStoreAsync stateStore, CancellationToken cancellationToken = default(CancellationToken)); +} + +/// +/// When we are awaiting a response for a workflow, we need to store information about how to continue the workflow +/// after receiving the event. +/// +/// The parser to populate our workflow from the event that forms the response +/// The type we expect a response to be - used to check the flow +/// The type we expect a fault to be - used to check the flow +/// The user-defined data, associated with a workflow +public class TaskResponse(Action> parser, Type responseType, Type? errorType) +{ + /// Parses a response to a workflow sequence step + public Action>? Parser { get; set; } = parser; + + /// The type we expect a response to be - used to check the flow + public Type? ResponseType { get; set; } = responseType; + + /// The type we expect a fault to be - used to check the flow + public Type? ErrorType { get; set; } = errorType; + + /// + /// Do we have an error + /// + /// True if we have an error, false otherwise + public bool HasError() => ErrorType is not null; +} + +/// +/// Essentially a pass through step, it alters Data property by running the transform +/// given by onChange over it +/// +/// Takes the Data property and transforms it +/// The workflow data, that we wish to transform +public class ChangeAsync( + Func onChange +) : IStepTask +{ + + /// + /// Handles the workflow action. + /// + /// The current job of the workflow. + /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler + /// The cancellation token for this task + public async Task HandleAsync( + Job? job, + IAmACommandProcessor? commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (job is null) + return; + + if (cancellationToken.IsCancellationRequested) + return; + + await onChange(job.Data); + } +} + +/// +/// Represents a fire-and-forget action in the workflow. +/// +/// The type of the request. +/// The type of the workflow data. +/// The factory method to create the request. +public class FireAndForgetAsync( + Func requestFactory + ) + : IStepTask + where TRequest : class, IRequest +{ + /// + /// Handles the fire-and-forget action. + /// + /// The current job of the workflow. + /// The command processor used to handle commands. + /// Used to store the state of a job, if it is altered in the handler + /// The cancellation token for this task + public async Task HandleAsync( + Job? job, + IAmACommandProcessor? commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (job is null) + return; + + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); + + var command = requestFactory(job.Data); + command.CorrelationId = job.Id; + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + } +} + +/// +/// Represents a request-and-reply action in the workflow. +/// +/// The type of the request. +/// The type of the reply. +/// The type of the workflow data. +/// The factory method to create the request. +/// The factory method to handle the reply. +public class RequestAndReactionAsync( + Func requestFactory, + Action replyFactory + ) + : IStepTask + where TRequest : class, IRequest + where TReply : Event +{ + /// + /// Handles the request-and-reply action. + /// + /// The logic here has to add the pending response, before the call to send the request. This is because the call to publish is not + /// over a bus, so it occurs sequentially within the Send before it exits. The event handler calls the 's + /// ResumeAfterEvent method to schedule handling the response. This will look up the pending response. So it needs to be stored prior + /// to this call completing + /// The current job of the workflow. + /// The state store, required so that we can save the job state before sending the message + /// The command processor used to handle commands. + /// The cancellation token for this task + public async Task HandleAsync( + Job? job, + IAmACommandProcessor? commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (job is null) + return; + + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); + + var command = requestFactory(job.Data); + command.CorrelationId = job.Id; + + job.AddPendingResponse( + typeof(TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply, job.Data), typeof(TReply), + null + ) + ); + await stateStore.SaveJobAsync(job, cancellationToken); + + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + } +} + +/// +/// +/// +/// +/// +/// +/// +/// +/// +public class RobustRequestAndReactionAsync( + Func requestFactory, + Action replyFactory, + Action faultFactory +) + : IStepTask + where TRequest : class, IRequest + where TReply : Event + where TFault: Event +{ + /// + /// Handles the fire-and-forget action. + /// + /// The current job of the workflow. + /// The command processor used to handle commands. + /// The state store, required so that we can save the job state before sending the message + /// The cancellation token for this task + public async Task HandleAsync( + Job? job, + IAmACommandProcessor? commandProcessor, + IAmAStateStoreAsync stateStore, + CancellationToken cancellationToken = default(CancellationToken) + ) + { + if (job is null) + return; + + if (commandProcessor is null) + throw new ArgumentNullException(nameof(commandProcessor)); + + var command = requestFactory(job.Data); + + command.CorrelationId = job.Id; + + job.AddPendingResponse( + typeof(TReply), + new TaskResponse((reply, _) => replyFactory(reply as TReply, job.Data), + typeof(TReply), + typeof(TFault) + ) + ); + job.AddPendingResponse( + typeof(TFault), + new TaskResponse((reply, _) => faultFactory(reply as TFault, job.Data), + typeof(TReply), + typeof(TFault) + ) + ); + await stateStore.SaveJobAsync(job, cancellationToken); + + await commandProcessor.SendAsync(command, cancellationToken: cancellationToken); + } +} + + diff --git a/src/Paramore.Brighter.Mediator/Waker.cs b/src/Paramore.Brighter.Mediator/Waker.cs new file mode 100644 index 0000000000..24ac2174d6 --- /dev/null +++ b/src/Paramore.Brighter.Mediator/Waker.cs @@ -0,0 +1,93 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Ian Cooper + +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. */ + +#endregion + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.Mediator; + +/// +/// The class is responsible for periodically waking up and triggering due jobs in the scheduler. +/// +/// The type of the job data. +public class Waker +{ + private readonly TimeSpan _jobAge; + private readonly Scheduler _scheduler; + private readonly string _wakerName = Guid.NewGuid().ToString("N"); + + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); + + /// + /// Initializes a new instance of the class. + /// + /// The age of the job to determine if it is due. + /// The scheduler to trigger due jobs. + public Waker(TimeSpan jobAge, Scheduler scheduler) + { + _jobAge = jobAge; + _scheduler = scheduler; + } + + /// + /// Runs the > asynchronously. + /// This will periodically wake up and trigger due jobs in the scheduler. + /// + /// A token to monitor for cancellation requests. + /// A task that represents the asynchronous run operation. + public void RunAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + s_logger.LogInformation("Starting waker {WakerName}", _wakerName); + + var task = Task.Factory.StartNew(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + await Wake(cancellationToken); + + if (cancellationToken.IsCancellationRequested) + cancellationToken.ThrowIfCancellationRequested(); + + }, cancellationToken); + + Task.WaitAll([task], cancellationToken); + + s_logger.LogInformation("Finished waker {WakerName}", _wakerName); + } + + private async Task Wake(CancellationToken cancellationToken = default(CancellationToken)) + { + while (true) + { + if (cancellationToken.IsCancellationRequested) + break; + + await _scheduler.TriggerDueJobsAsync(_jobAge, cancellationToken); + await Task.Delay(_jobAge, cancellationToken); + } + } +} diff --git a/src/Paramore.Brighter.ServiceActivator.Control.Api/Paramore.Brighter.ServiceActivator.Control.Api.csproj b/src/Paramore.Brighter.ServiceActivator.Control.Api/Paramore.Brighter.ServiceActivator.Control.Api.csproj index d6f96967cf..e834afe357 100644 --- a/src/Paramore.Brighter.ServiceActivator.Control.Api/Paramore.Brighter.ServiceActivator.Control.Api.csproj +++ b/src/Paramore.Brighter.ServiceActivator.Control.Api/Paramore.Brighter.ServiceActivator.Control.Api.csproj @@ -2,7 +2,7 @@ Library - net8.0 + net9.0 enable enable Paul Reardon diff --git a/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs b/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs index 71282dbcac..8a58b62f45 100644 --- a/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs +++ b/src/Paramore.Brighter.ServiceActivator.Control/Events/NodeStatusEvent.cs @@ -2,13 +2,8 @@ namespace Paramore.Brighter.ServiceActivator.Control.Events; -public record NodeStatusEvent : IEvent +public class NodeStatusEvent() : Event(Guid.NewGuid().ToString()) { - /// - /// The event Id - /// - public string Id { get; set; } = null!; - /// /// The Diagnostics Span /// @@ -32,7 +27,7 @@ public record NodeStatusEvent : IEvent /// /// Is this node Healthy /// - public bool IsHealthy { get; init; } = false; + public bool IsHealthy { get; init; } /// /// The Number of Performers currently running on the Node diff --git a/src/Paramore.Brighter.ServiceActivator.Control/Paramore.Brighter.ServiceActivator.Control.csproj b/src/Paramore.Brighter.ServiceActivator.Control/Paramore.Brighter.ServiceActivator.Control.csproj index 3f73c978e4..80bf786a73 100644 --- a/src/Paramore.Brighter.ServiceActivator.Control/Paramore.Brighter.ServiceActivator.Control.csproj +++ b/src/Paramore.Brighter.ServiceActivator.Control/Paramore.Brighter.ServiceActivator.Control.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable Paul Reardon diff --git a/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj b/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj index 27f505e7fd..da24ebae71 100644 --- a/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj +++ b/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable Paul Reardon diff --git a/src/Paramore.Brighter/Command.cs b/src/Paramore.Brighter/Command.cs index cc7949e516..4307b02dc9 100644 --- a/src/Paramore.Brighter/Command.cs +++ b/src/Paramore.Brighter/Command.cs @@ -31,6 +31,12 @@ namespace Paramore.Brighter /// public class Command : ICommand { + /// + /// If we are participating in a conversation, the correlation id allows us to correlate a request with other messages in the conversation + /// + public string? CorrelationId { get; set; } + + /// /// Gets or sets the identifier. /// /// The identifier. @@ -50,7 +56,7 @@ public Command(string id) /// Initializes a new instance of the class. /// /// The identifier - public Command(Guid id) + protected Command(Guid id) { Id = id.ToString(); } diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 9cee3135d1..a5f68e3a72 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -45,7 +45,10 @@ namespace Paramore.Brighter { /// /// Class CommandProcessor. - /// Implements both the Command Dispatcher + /// The `CommandProcessor` class implements both the Command Dispatcher and Command Processor design patterns. + /// It is responsible for handling commands and events, managing their execution, and ensuring reliable delivery. + /// The `CommandProcessor` class is the main entry point for the Brighter library. + /// It implements both the Command Dispatcher /// and Command Processor Design Patterns /// public class CommandProcessor : IAmACommandProcessor @@ -117,6 +120,7 @@ public class CommandProcessor : IAmACommandProcessor /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure /// What is the tracer we will use for telemetry /// When creating a span for operations how noisy should the attributes be + /// Thrown when no handler factory is set. public CommandProcessor( IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, @@ -223,8 +227,8 @@ public CommandProcessor( /// /// The command. /// The context of the request; if null we will start one via a - /// - /// + /// Thrown when no handler factory is defined. + /// Thrown when no subscriber registry is configured or when the command has more than one handler. public void Send(T command, RequestContext? requestContext = null) where T : class, IRequest { if (_handlerFactorySync == null) @@ -265,9 +269,11 @@ public void Send(T command, RequestContext? requestContext = null) where T : /// The command. /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false - /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . - public async Task SendAsync( + /// Token to cancel the request pipeline. + /// An awaitable task. + /// Thrown when no async handler factory is defined. + /// Thrown when no subscriber registry is configured. + public async Task SendAsync( T command, RequestContext? requestContext = null, bool continueOnCapturedContext = true, @@ -317,6 +323,8 @@ await handlerChain.First().HandleAsync(command, cancellationToken) /// /// The event. /// The context of the request; if null we will start one via a + /// Thrown when no handler factory is defined. + /// Thrown when no subscriber registry is configured. public void Publish(T @event, RequestContext? requestContext = null) where T : class, IRequest { if (_handlerFactorySync == null) @@ -387,7 +395,9 @@ public void Publish(T @event, RequestContext? requestContext = null) where T /// The context of the request; if null we will start one via a /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional - /// awaitable . + /// An awaitable task. + /// Thrown when no async handler factory is defined. + /// Thrown when no subscriber registry is configured. public async Task PublishAsync( T @event, RequestContext? requestContext = null, diff --git a/src/Paramore.Brighter/CommittableTransactionProvider.cs b/src/Paramore.Brighter/CommittableTransactionProvider.cs index b3753edf6f..d461bb68ba 100644 --- a/src/Paramore.Brighter/CommittableTransactionProvider.cs +++ b/src/Paramore.Brighter/CommittableTransactionProvider.cs @@ -4,23 +4,42 @@ namespace Paramore.Brighter { + /// + /// Provides a committable transaction for use in a box transaction. + /// + /// + /// This class implements the IAmABoxTransactionProvider interface for CommittableTransaction. + /// It manages the creation, commitment, and rollback of transactions, as well as handling + /// the current transaction context. + /// public class CommittableTransactionProvider : IAmABoxTransactionProvider { private CommittableTransaction? _transaction; private Transaction? _existingTransaction; + /// + /// Closes the current transaction and restores the previous transaction context. + /// public void Close() { Transaction.Current = _existingTransaction; _transaction = null; } + /// + /// Commits the current transaction and closes it. + /// public void Commit() { _transaction?.Commit(); Close(); } + /// + /// Asynchronously commits the current transaction. + /// + /// A cancellation token that can be used to cancel the commit operation. + /// A task representing the asynchronous commit operation. public Task CommitAsync(CancellationToken cancellationToken = default) { if (_transaction is null) @@ -28,6 +47,10 @@ public Task CommitAsync(CancellationToken cancellationToken = default) return Task.Factory.FromAsync(_transaction.BeginCommit, _transaction.EndCommit, null, TaskCreationOptions.RunContinuationsAsynchronously); } + /// + /// Gets the current transaction, creating a new one if necessary. + /// + /// The current CommittableTransaction. public CommittableTransaction GetTransaction() { if (_transaction == null) @@ -39,6 +62,11 @@ public CommittableTransaction GetTransaction() return _transaction; } + /// + /// Asynchronously gets the current transaction, creating a new one if necessary. + /// + /// A cancellation token that can be used to cancel the operation. + /// A task representing the asynchronous operation, which resolves to the current CommittableTransaction. public Task GetTransactionAsync(CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -46,15 +74,30 @@ public Task GetTransactionAsync(CancellationToken cancel return tcs.Task; } + /// + /// Gets a value indicating whether there is an open transaction. + /// public bool HasOpenTransaction { get { return _transaction != null; } } + + /// + /// Gets a value indicating whether this provider uses a shared connection. + /// public bool IsSharedConnection => true; + /// + /// Rolls back the current transaction and closes it. + /// public void Rollback() { _transaction?.Rollback(); Close(); } + /// + /// Asynchronously rolls back the current transaction. + /// + /// A cancellation token that can be used to cancel the rollback operation. + /// A task representing the asynchronous rollback operation. public Task RollbackAsync(CancellationToken cancellationToken = default) { Rollback(); diff --git a/src/Paramore.Brighter/Event.cs b/src/Paramore.Brighter/Event.cs index 642bf5c85c..7b3eec799f 100644 --- a/src/Paramore.Brighter/Event.cs +++ b/src/Paramore.Brighter/Event.cs @@ -33,6 +33,11 @@ namespace Paramore.Brighter /// public class Event : IEvent { + /// + /// An event may be the response to a command, in order to find the command that caused the event, we need to know the correlation id + /// + public string? CorrelationId { get; set; } + /// /// Gets or sets the identifier. /// @@ -48,14 +53,5 @@ public Event(string id) { Id = id; } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier. - public Event(Guid id) - { - Id = id.ToString(); - } } } diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index bdedb4a081..7d978e1fdf 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -31,10 +31,12 @@ namespace Paramore.Brighter { /// /// Interface IAmACommandProcessor - /// Paramore.Brighter provides the default implementation of this interface and it is unlikely you need - /// to override this for anything other than testing purposes. The usual need is that in a you intend to publish an - /// to indicate the handler has completed to other components. In this case your tests should only verify that the correct - /// event was raised by listening to calls on this interface, using a mocking framework of your choice or bespoke + /// Provides the interface for the command processor, which dispatches commands and events to handlers, invoking any required middleware. + /// Brighter provides the default implementation of this interface and it is unlikely you need + /// to override this for anything other than testing purposes. + /// The usual testing need is that in a you intend to publish an to indicate the + /// handler has completed to other components. In this case your tests should only verify that the correct event was raised by + /// listening to calls on this interface, using a mocking framework of your choice or bespoke /// Test Double. /// public interface IAmACommandProcessor diff --git a/src/Paramore.Brighter/IRequest.cs b/src/Paramore.Brighter/IRequest.cs index fd311b90df..6ef6393a2f 100644 --- a/src/Paramore.Brighter/IRequest.cs +++ b/src/Paramore.Brighter/IRequest.cs @@ -34,10 +34,16 @@ namespace Paramore.Brighter /// public interface IRequest { + /// + /// If we are participating in a conversation, the correlation id allows us to correlate a request with other messages in the conversation + /// + string? CorrelationId { get; set; } + /// /// Gets or sets the identifier. /// /// The identifier. string Id { get; set; } + } } diff --git a/src/Paramore.Brighter/IResponse.cs b/src/Paramore.Brighter/IResponse.cs index d499fade9f..7b24f1a697 100644 --- a/src/Paramore.Brighter/IResponse.cs +++ b/src/Paramore.Brighter/IResponse.cs @@ -8,9 +8,5 @@ namespace Paramore.Brighter /// public interface IResponse : IRequest { - /// - /// Allow us to correlate request and response - /// - Guid CorrelationId { get; } - } + } } diff --git a/src/Paramore.Brighter/InMemoryInbox.cs b/src/Paramore.Brighter/InMemoryInbox.cs index 658f824d9e..19b234c5c4 100644 --- a/src/Paramore.Brighter/InMemoryInbox.cs +++ b/src/Paramore.Brighter/InMemoryInbox.cs @@ -71,10 +71,12 @@ public InboxItem(Type requestType, string requestBody, DateTimeOffset writeTime, public DateTimeOffset WriteTime { get; } /// - /// The Id and the key for the context i.e. message type, that we are looking for - /// Occurs because we may service the same message in different contexts and need to - /// know they are all handled or not + /// Gets the context key for the request. This is a combination of the request ID and the context identifier. /// + /// + /// The context key is used to uniquely identify a request within a specific processing context, + /// allowing the message to be handled differently in various contexts. + /// string Key { get;} /// @@ -82,8 +84,8 @@ public InboxItem(Type requestType, string requestBody, DateTimeOffset writeTime, /// /// The Guid for the request /// The handler this is for - /// - public static string CreateKey(string id, string contextKey) + /// A composite key combining the request ID and context key. + public static string CreateKey(string id, string contextKey) { return $"{id}:{contextKey}"; } @@ -92,14 +94,17 @@ public static string CreateKey(string id, string contextKey) /// /// Class InMemoryInbox. - /// A Inbox stores s for diagnostics or replay. + /// Provides an in-memory implementation of an inbox for storing and retrieving requests. An Inbox stores s for diagnostics or replay. + /// + /// /// This class is intended to be thread-safe, so you can use one InMemoryInbox across multiple performers. However, the state is not global i.e. static /// so you can use multiple instances safely as well. /// N.B. that the primary limitation of this in-memory inbox is that it will not work across processes. So if you use the competing consumers pattern /// the consumers will not be able to determine if another consumer has already processed this command. /// It is possible to use multiple performers within one process as competing consumers, and if you want to use an InMemoryInbox this is the most /// viable strategy - otherwise use an out-of-process inbox that provides shared state to all consumers - /// + /// + /// The time provider used for timestamp operations. public class InMemoryInbox(TimeProvider timeProvider) : InMemoryBox(timeProvider), IAmAnInboxSync, IAmAnInboxAsync { private readonly TimeProvider _timeProvider = timeProvider; @@ -118,9 +123,10 @@ public class InMemoryInbox(TimeProvider timeProvider) : InMemoryBox(t /// /// /// The command. - /// - /// The timeout in milliseconds. - public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest + /// The context-specific key for this request. + /// The timeout for the operation in milliseconds. Use -1 for no timeout. + /// Thrown when the request cannot be added to the inbox. + public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); @@ -141,11 +147,10 @@ public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) /// /// /// The command. - /// + /// The context-specific key for this request. /// The timeout in milliseconds. - /// - /// Allows the sender to cancel the call, optional - /// + /// A token to cancel the asynchronous operation. + /// Allows the sender to cancel the call, optional public Task AddAsync(T command, string contextKey, int timeoutInMilliseconds = -1, CancellationToken cancellationToken = default) where T : class, IRequest { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -165,13 +170,14 @@ public Task AddAsync(T command, string contextKey, int timeoutInMilliseconds /// /// Finds the command with the specified identifier. /// - /// - /// The identifier. - /// + /// The type of the request to retrieve, which must implement IRequest. + /// The unique identifier of the request. + /// The context-specific key for this request. /// The timeout in milliseconds. - /// ICommand. - /// - public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest + /// The requested item of type T. + /// Thrown when the requested item is not found in the inbox. + /// Thrown when the deserialized request body is null. + public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); @@ -186,6 +192,14 @@ public T Get(string id, string contextKey, int timeoutInMilliseconds = -1) wh throw new RequestNotFoundException(id); } + /// + /// Checks if a request exists in the inbox. + /// + /// The type of the request, which must implement IRequest. + /// The unique identifier of the request. + /// The context-specific key for this request. + /// The timeout for the operation in milliseconds. Use -1 for no timeout. + /// True if the request exists in the inbox; otherwise, false. public bool Exists(string id, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { ClearExpiredMessages(); diff --git a/src/Paramore.Brighter/InternalBus.cs b/src/Paramore.Brighter/InternalBus.cs index d728734251..341daffc4b 100644 --- a/src/Paramore.Brighter/InternalBus.cs +++ b/src/Paramore.Brighter/InternalBus.cs @@ -38,7 +38,7 @@ namespace Paramore.Brighter; /// The maximum number of messages that can be enqueued; -1 is unbounded; default is -1 public class InternalBus(int boundedCapacity = -1) : IAmABus { - private ConcurrentDictionary> _messages = new(); + private readonly ConcurrentDictionary> _messages = new(); /// /// Enqueue a message to tbe bus diff --git a/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs b/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs index 7c860deb05..ca9ba9350a 100644 --- a/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs +++ b/src/Paramore.Brighter/Monitoring/Events/MonitorEvent.cs @@ -66,7 +66,7 @@ public class MonitorEvent( DateTime eventTime, int timeElapsedMs, Exception? exception = null) - : Event(Guid.NewGuid()) + : Event(Guid.NewGuid().ToString()) { /// /// Any exception that was thrown when processing the handler pipeline diff --git a/src/Paramore.Brighter/Reply.cs b/src/Paramore.Brighter/Reply.cs index 2805ca551c..38c8085100 100644 --- a/src/Paramore.Brighter/Reply.cs +++ b/src/Paramore.Brighter/Reply.cs @@ -11,16 +11,12 @@ namespace Paramore.Brighter /// public class Reply : Command, IResponse { - /// - /// Use this correlation id so that sender knows what we are replying to - /// - public Guid CorrelationId { get; } /// /// The channel that we should reply to the sender on. /// public ReplyAddress SendersAddress { get; private set; } - public Reply(ReplyAddress sendersAddress) + protected Reply(ReplyAddress sendersAddress) : base(Guid.NewGuid()) { SendersAddress = sendersAddress; diff --git a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj index a2039e9a67..26c52e6766 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj +++ b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj b/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj index 925e27d9cd..f91ed67055 100644 --- a/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj +++ b/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs b/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs index 194ebdadf0..780e22e7f3 100644 --- a/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs +++ b/tests/Paramore.Brighter.Azure.Tests/TestDoubles/SuperAwesomeEvent.cs @@ -1,6 +1,6 @@ namespace Paramore.Brighter.Azure.Tests.TestDoubles; -public class SuperAwesomeEvent(string announcement) : Event(Guid.NewGuid()) +public class SuperAwesomeEvent(string announcement) : Event(Guid.NewGuid().ToString()) { public string Announcement { get; set; } = announcement; } diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj b/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj index 0c590187a3..7849e4dfe6 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs b/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs index 9bf892f3a1..fa496b0a6e 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/TestDoubles/ASBTestEvent.cs @@ -4,7 +4,7 @@ namespace Paramore.Brighter.AzureServiceBus.Tests.TestDoubles { public class ASBTestEvent : Event { - public ASBTestEvent() : base(Guid.NewGuid()) + public ASBTestEvent() : base(Guid.NewGuid().ToString()) { } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs index 147e17211b..2ea8c3fbf8 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/Call/When_Calling_A_Server_Via_The_Command_Processor.cs @@ -116,7 +116,7 @@ public void When_Calling_A_Server_Via_The_Command_Processor() new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), channel) { Channel = channel, TimeOut = TimeSpan.FromMilliseconds(5000) }; - //Run the pump on a new thread + //RunAsync the pump on a new thread Task pump = Task.Factory.StartNew(() => messagePump.Run()); _commandProcessor.Call(_myRequest, timeOut: TimeSpan.FromMilliseconds(500)); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs index 381961546f..183c9b9541 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyCommandToFail.cs @@ -3,13 +3,5 @@ namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles { - internal class MyCommandToFail : ICommand - { - public string Id { get; set; } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } - } + internal class MyCommandToFail() : Command(Guid.NewGuid().ToString()); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs index cb74433155..15ae75934a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/MyEvent.cs @@ -26,22 +26,18 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles { - internal class MyEvent : Event, IEquatable + internal class MyEvent() : Event(Guid.NewGuid().ToString()), IEquatable { public int Data { get; set; } - public MyEvent() : base(Guid.NewGuid()) - { - } - - public bool Equals(MyEvent other) + public bool Equals(MyEvent? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Data == other.Data; } - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs index 91975134d0..43f47335bb 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/MyFailingMapperEvent.cs @@ -1,26 +1,12 @@ using System; -using System.Diagnostics; namespace Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; -internal class MyFailingMapperEvent : IRequest +internal class MyFailingMapperEvent : Event { - /// - /// Gets or sets the identifier. - /// - /// The identifier. - public string Id { get; set; } - + /// /// Initializes a new instance of the class. /// - public MyFailingMapperEvent() - { - Id = Guid.NewGuid().ToString(); - } - - /// - /// Gets or sets the span that this operation live within - /// - public Activity Span { get; set; } + public MyFailingMapperEvent() : base(Guid.NewGuid().ToString()) { } } diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index 3815c79100..da36fffbd2 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -5,6 +5,7 @@ + @@ -21,6 +22,7 @@ + diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs new file mode 100644 index 0000000000..1716e2e96d --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/TestDoubles/SpecificationTestState.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Paramore.Brighter.Mediator; + +namespace Paramore.Brighter.Core.Tests.Specifications.TestDoubles; + +public enum TestState +{ + Done, + Ready, + Running, + Waiting +} + +public class SpecificationTestState +{ + public TestState State { get; set; } + public Dictionary Bag { get; set; } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs new file mode 100644 index 0000000000..eefd3c36f2 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Specifications/When_evaluating_a_specification.cs @@ -0,0 +1,75 @@ +using Paramore.Brighter.Core.Tests.Specifications.TestDoubles; +using Paramore.Brighter.Mediator; +using Xunit; + +namespace Paramore.Brighter.Core.Tests.Specifications; + +public class SpecificationTests +{ + [Fact] + public void When_evaluating_a_specificaion() + { + var specification = new Specification(state => state.State == TestState.Done); + Assert.True(specification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(specification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); + } + + [Fact] + public void When_combining_specifications_with_and() + { + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); + var combinedSpecification = doneSpecification.And(runningSpecification); + + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); + } + + [Fact] + public void When_combining_specifications_with_or() + { + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); + var combinedSpecification = doneSpecification.Or(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); + } + + [Fact] + public void When_negating_a_specification() + { + var doneSpecification = new Specification(state => state.State == TestState.Done); + var notDoneSpecification = doneSpecification.Not(); + + Assert.False(notDoneSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(notDoneSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); + } + + [Fact] + public void When_combining_specifications_with_and_not() + { + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); + var combinedSpecification = doneSpecification.AndNot(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); + } + + [Fact] + public void When_combining_specifications_with_or_not() + { + var doneSpecification = new Specification(state => state.State == TestState.Done); + var runningSpecification = new Specification(state => state.State == TestState.Running); + var combinedSpecification = doneSpecification.OrNot(runningSpecification); + + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Done })); + Assert.True(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Ready })); + Assert.False(combinedSpecification.IsSatisfiedBy(new SpecificationTestState { State = TestState.Running })); + } +} + + + diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs new file mode 100644 index 0000000000..ada1447797 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommand.cs @@ -0,0 +1,33 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + public class MyCommand() : Command(Guid.NewGuid()) + { + public string Value { get; set; } = string.Empty; + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs new file mode 100644 index 0000000000..5fd95797f0 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyCommandHandlerAsync.cs @@ -0,0 +1,53 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyCommandHandlerAsync(IAmACommandProcessor? commandProcessor, bool raiseFault = false) : RequestHandlerAsync + { + public static List ReceivedCommands { get; } = []; + + + public override async Task HandleAsync(MyCommand command, CancellationToken cancellationToken = default) + { + LogCommand(command); + if (!raiseFault) + await commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + else + await commandProcessor?.PublishAsync(new MyFault(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + + + return await base.HandleAsync(command, cancellationToken); + } + + private void LogCommand(MyCommand request) + { + ReceivedCommands.Add(request); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs new file mode 100644 index 0000000000..03cbafc742 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEvent.cs @@ -0,0 +1,33 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyEvent(string? value) : Event(Guid.NewGuid().ToString()) + { + public string Value { get; set; } = value ?? string.Empty; + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs new file mode 100644 index 0000000000..6da286c4f9 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyEventHandlerAsync.cs @@ -0,0 +1,48 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Mediator; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyEventHandlerAsync(Scheduler? scheduler) : RequestHandlerAsync + { + public static List ReceivedEvents { get; } = []; + + public override async Task HandleAsync(MyEvent @event, CancellationToken cancellationToken = default) + { + LogEvent(@event); + scheduler?.ResumeAfterEvent(@event); + return await base.HandleAsync(@event, cancellationToken); + } + + private void LogEvent(MyEvent request) + { + ReceivedEvents.Add(request); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs new file mode 100644 index 0000000000..b46dfab693 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFault.cs @@ -0,0 +1,33 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyFault(string? value) : Event(Guid.NewGuid().ToString()) + { + public string Value { get; set; } = value ?? string.Empty; + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs new file mode 100644 index 0000000000..fb4161bab1 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyFaultHandlerAsync.cs @@ -0,0 +1,48 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +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. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Paramore.Brighter.Mediator; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles +{ + internal class MyFaultHandlerAsync(Scheduler? scheduler) : RequestHandlerAsync + { + public static List ReceivedFaults { get; } = []; + + public override async Task HandleAsync(MyFault @event, CancellationToken cancellationToken = default) + { + LogEvent(@event); + scheduler?.ResumeAfterEvent(@event); + return await base.HandleAsync(@event, cancellationToken); + } + + private void LogEvent(MyFault request) + { + ReceivedFaults.Add(request); + } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs new file mode 100644 index 0000000000..7d4cc6fff5 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommand.cs @@ -0,0 +1,8 @@ +using System; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class MyOtherCommand() : Command(Guid.NewGuid().ToString()) +{ + public string Value { get; set; } = string.Empty; +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs new file mode 100644 index 0000000000..650f0fb4e1 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/MyOtherCommandHandler.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class MyOtherCommandHandlerAsync(IAmACommandProcessor commandProcessor) : RequestHandlerAsync +{ + public static List ReceivedCommands { get; set; } = []; + + public override async Task HandleAsync(MyOtherCommand command, CancellationToken cancellationToken = default) + { + LogCommand(command); + commandProcessor?.PublishAsync(new MyEvent(command.Value) {CorrelationId = command.CorrelationId}, cancellationToken: cancellationToken); + return await base.HandleAsync(command, cancellationToken); + } + + private void LogCommand(MyOtherCommand request) + { + ReceivedCommands.Add(request); + } + +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs new file mode 100644 index 0000000000..3626fe9d1c --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/TestDoubles/WorkflowTestData.cs @@ -0,0 +1,8 @@ +using System.Collections.Concurrent; + +namespace Paramore.Brighter.Core.Tests.Workflows.TestDoubles; + +public class WorkflowTestData +{ + public ConcurrentDictionary Bag { get; set; } = new(); +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs new file mode 100644 index 0000000000..5b5f4eac43 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_blocking_wait_workflow.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorWaitStepFlowTests +{ + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + private readonly ITestOutputHelper _testOutputHelper; + private readonly FakeTimeProvider _timeProvider = new(); + private readonly Waker _waker; + + public MediatorWaitStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData); + + var secondStep = new Sequential( + "Test of Job", + new ChangeAsync( (_) => Task.CompletedTask), + () => { _stepCompleted = true; }, + null + ); + + var firstStep = new Wait("Test of Job", + TimeSpan.FromMilliseconds(100), + secondStep + ); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(_timeProvider); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + _waker = new Waker(TimeSpan.FromMilliseconds(100), _scheduler); + } + + [Fact] + public async Task When_running_a_wait_workflow() + { + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(3)); + + try + { + await _scheduler.ScheduleAsync(_job); + + _timeProvider.Advance(TimeSpan.FromMilliseconds(1000)); + + _runner.RunAsync(ct.Token); + _waker.RunAsync(ct.Token); + + await Task.Delay(5, ct.Token); + + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + _stepCompleted.Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs new file mode 100644 index 0000000000..07bd52c51b --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_change_workflow.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorChangeStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + + public MediatorChangeStepFlowTests (ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData { Bag = { ["MyValue"] = "Test" } }; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new ChangeAsync( (data) => + { + var tcs = new TaskCompletionSource(); + data.Bag["MyValue"] = "Altered"; + tcs.SetResult(); + return tcs.Task; + }), + () => { _stepCompleted = true; }, + null + ); + + _job.InitSteps(firstStep); + + var store = new InMemoryStateStoreAsync (); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_change_workflow() + { + await _scheduler.ScheduleAsync(_job); + + //let it run long enough to finish work, then terminate + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception ex) + { + // swallow the exception, we expect it to be cancelled + _testOutputHelper.WriteLine(ex.ToString()); + } + + _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + _job.Data.Bag["MyValue"].Should().Be("Altered"); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs new file mode 100644 index 0000000000..a5266ee806 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_failing_choice_workflow_step.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorFailingChoiceFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompletedOne; + private bool _stepCompletedTwo; + private bool _stepCompletedThree; + + public MediatorFailingChoiceFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + // arrange + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyOtherCommandHandlerAsync) => new MyOtherCommandHandlerAsync(commandProcessor), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Fail"; + + _job = new Job(workflowData) ; + + var stepThree = new Sequential( + "Test of Job SequenceStep Three", + new FireAndForgetAsync((data) => + new MyOtherCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepCompletedThree = true; }, + null); + + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + var stepOne = new ExclusiveChoice( + "Test of Job SequenceStep One", + new Specification(data => data.Bag["MyValue"] as string == "Pass"), + () => { _stepCompletedOne = true; }, + stepTwo, + stepThree); + + _job.InitSteps(stepOne); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_choice_workflow_step() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeFalse(); + _stepCompletedThree.Should().BeTrue(); + MyOtherCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Fail").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any().Should().BeFalse(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs new file mode 100644 index 0000000000..936a38b572 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_multistep_workflow_with_reply.cs @@ -0,0 +1,102 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorReplyMultiStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompletedOne; + private bool _stepCompletedTwo; + + public MediatorReplyMultiStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + Sequential stepOne = new( + "Test of Job SequenceStep One", + new RequestAndReactionAsync( + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => data.Bag["MyReply"] = ((MyEvent)reply).Value), + () => { _stepCompletedOne = true; }, + stepTwo); + + _job.InitSteps(stepOne); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_workflow_with_reply() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeTrue(); + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs new file mode 100644 index 0000000000..cdd100e1f0 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_passing_choice_workflow_step.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorPassingChoiceFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompletedOne; + private bool _stepCompletedTwo; + private bool _stepCompletedThree; + + public MediatorPassingChoiceFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + // arrange + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyOtherCommandHandlerAsync) => new (commandProcessor), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Pass"; + + _job = new Job(workflowData) ; + + var stepThree = new Sequential( + "Test of Job SequenceStep Three", + new FireAndForgetAsync((data) => + new MyOtherCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepCompletedThree = true; }, + null); + + var stepTwo = new Sequential( + "Test of Job SequenceStep Two", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepCompletedTwo = true; }, + null); + + var stepOne = new ExclusiveChoice( + "Test of Job SequenceStep One", + new Specification(x => x.Bag["MyValue"] as string == "Pass"), + () => { _stepCompletedOne = true; }, + stepTwo, + stepThree); + + _job.InitSteps(stepOne); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_choice_workflow_step() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyOtherCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + _stepCompletedOne.Should().BeTrue(); + _stepCompletedTwo.Should().BeTrue(); + _stepCompletedThree.Should().BeFalse(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Pass").Should().BeTrue(); + MyOtherCommandHandlerAsync.ReceivedCommands.Any().Should().BeFalse(); + _stepCompletedOne.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs new file mode 100644 index 0000000000..3bea73ee29 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_single_step_workflow.cs @@ -0,0 +1,83 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorOneStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + + public MediatorOneStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new FireAndForgetAsync((data) => + new MyCommand { Value = (workflowData.Bag["MyValue"] as string)!}), + () => { _stepCompleted = true; }, + null + ); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_single_step_workflow() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs new file mode 100644 index 0000000000..9f4be1cd13 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_two_step_workflow.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorTwoStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepsCompleted; + + public MediatorTwoStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var secondStep = new Sequential( + "Test of Job Two", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _stepsCompleted = true; }, + null + ); + + var firstStep = new Sequential( + "Test of Job One", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { workflowData.Bag["MyValue"] = "TestTwo"; }, + secondStep + ); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_two_step_workflow() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + _stepsCompleted.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs new file mode 100644 index 0000000000..4b3d6bf8e9 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_a_parallel_split.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorParallelSplitFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _firstBranchFinished; + private bool _secondBranchFinished; + private readonly InMemoryJobChannel _channel; + + public MediatorParallelSplitFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + + _job = new Job(workflowData) ; + + var parallelSplit = new ParallelSplit( + "Test of Job Parallel Split", + (data) => + { + data.Bag.TryAdd("MyValue", "Test"); + data.Bag.TryAdd("MyOtherValue", "TestTwo"); + + var secondBranch = new Sequential( + "Test of Job Two", + new FireAndForgetAsync((d) => + new MyCommand { Value = (d.Bag["MyOtherValue"] as string)! }), + () => { _secondBranchFinished = true; }, + null + ); + + var firstBranch = new Sequential( + "Test of Job One", + new FireAndForgetAsync((d) => + new MyCommand { Value = (d.Bag["MyValue"] as string)! }), + () => { _firstBranchFinished = true; }, + null + ); + + return [firstBranch, secondBranch]; + } + ); + + _job.InitSteps(parallelSplit); + + InMemoryStateStoreAsync store = new(); + _channel = new InMemoryJobChannel(); + + _scheduler = new Scheduler( + _channel, + store + ); + + _runner = new Runner(_channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_workflow_with_a_parallel_split() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _firstBranchFinished.Should().BeTrue(); + _secondBranchFinished.Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs new file mode 100644 index 0000000000..2c3f1f4c68 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_reply.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; +using MyCommand = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommand; +using MyCommandHandlerAsync = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyCommandHandlerAsync; +using MyEvent = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyEvent; +using MyEventHandlerAsync = Paramore.Brighter.Core.Tests.Workflows.TestDoubles.MyEventHandlerAsync; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorReplyStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + + public MediatorReplyStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor? commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new RequestAndReactionAsync( + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply,data) => { data.Bag["MyReply"] = reply!.Value; }), + () => { _stepCompleted = true; }, + null); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_workflow_with_reply() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + + await _scheduler.ScheduleAsync(_job); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(3) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + _stepCompleted.Should().BeTrue(); + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + _job.State.Should().Be(JobState.Done); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs new file mode 100644 index 0000000000..3d28de6222 --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_nofault.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime.Internal.Transform; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorRobustReplyNoFaultStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + private bool _stepFaulted; + + public MediatorRobustReplyNoFaultStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new RobustRequestAndReactionAsync( + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => { data.Bag["MyReply"] = ((MyEvent)reply).Value; }, + (fault, data) => { data.Bag["MyFault"] = ((MyFault)fault).Value; }), + () => { _stepCompleted = true; }, + null, + () => { _stepFaulted = true; }, + null); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_workflow_with_reply() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + MyFaultHandlerAsync.ReceivedFaults.Clear(); + + await _scheduler.ScheduleAsync(_job); + + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Any(e => e.Value == "Test").Should().BeTrue(); + MyFaultHandlerAsync.ReceivedFaults.Should().BeEmpty(); + _job.Data.Bag["MyValue"].Should().Be("Test"); + _job.Data.Bag["MyReply"].Should().Be("Test"); + _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + _stepFaulted.Should().BeFalse(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs new file mode 100644 index 0000000000..58edfbebbe --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_a_workflow_with_robust_reply_with_fault.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Amazon.Runtime.Internal.Transform; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorRobustReplyFaultStepFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _job; + private bool _stepCompleted; + private bool _stepFaulted; + + public MediatorRobustReplyFaultStepFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + registry.RegisterAsync(); + registry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync((handlerType) => + handlerType switch + { + _ when handlerType == typeof(MyCommandHandlerAsync) => new MyCommandHandlerAsync(commandProcessor, raiseFault: true), + _ when handlerType == typeof(MyEventHandlerAsync) => new MyEventHandlerAsync(_scheduler), + _ when handlerType == typeof(MyFaultHandlerAsync) => new MyFaultHandlerAsync(_scheduler), + _ => throw new InvalidOperationException($"The handler type {handlerType} is not supported") + }); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var workflowData= new WorkflowTestData(); + workflowData.Bag["MyValue"] = "Test"; + + _job = new Job(workflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new RobustRequestAndReactionAsync( + (data) => new MyCommand { Value = (data.Bag["MyValue"] as string)! }, + (reply, data) => { data.Bag["MyReply"] = reply!.Value; }, + (fault, data) => { data.Bag["MyFault"] = fault!.Value; }), + () => { _stepCompleted = true; }, + null, + () => { _stepFaulted = true; }, + null); + + _job.InitSteps(firstStep); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_workflow_with_reply() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + MyEventHandlerAsync.ReceivedEvents.Clear(); + MyFaultHandlerAsync.ReceivedFaults.Clear(); + + await _scheduler.ScheduleAsync(_job); + + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(1) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyFaultHandlerAsync.ReceivedFaults.Any(e => e.Value == "Test").Should().BeTrue(); + MyEventHandlerAsync.ReceivedEvents.Should().BeEmpty(); + _job.Data.Bag["MyValue"].Should().Be("Test"); + _job.Data.Bag["MyFault"].Should().Be("Test"); + _job.State.Should().Be(JobState.Done); + _stepCompleted.Should().BeTrue(); + _stepFaulted.Should().BeFalse(); + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs new file mode 100644 index 0000000000..8f3ae2e63b --- /dev/null +++ b/tests/Paramore.Brighter.Core.Tests/Workflows/When_running_multiple_workflows.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.Core.Tests.Workflows.TestDoubles; +using Paramore.Brighter.Mediator; +using Polly.Registry; +using Xunit; +using Xunit.Abstractions; + +namespace Paramore.Brighter.Core.Tests.Workflows; + +public class MediatorMultipleWorkflowFlowTests +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly Scheduler _scheduler; + private readonly Runner _runner; + private readonly Job _firstJob; + private readonly Job _secondJob; + private bool _jobOneCompleted; + private bool _jobTwoCompleted; + + public MediatorMultipleWorkflowFlowTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + var registry = new SubscriberRegistry(); + registry.RegisterAsync(); + + CommandProcessor commandProcessor = null; + var handlerFactory = new SimpleHandlerFactoryAsync(_ => new MyCommandHandlerAsync(commandProcessor)); + + commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry()); + PipelineBuilder.ClearPipelineCache(); + + var firstWorkflowData= new WorkflowTestData(); + firstWorkflowData.Bag["MyValue"] = "Test"; + + _firstJob = new Job(firstWorkflowData) ; + + var firstStep = new Sequential( + "Test of Job", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)!}), + () => { _jobOneCompleted = true; }, + null + ); + + _firstJob.InitSteps(firstStep); + + var secondWorkflowData = new WorkflowTestData(); + secondWorkflowData.Bag["MyValue"] = "TestTwo"; + _secondJob = new Job(secondWorkflowData); + + var secondStep = new Sequential( + "Second Test of Job", + new FireAndForgetAsync((data) => + new MyCommand { Value = (data.Bag["MyValue"] as string)! }), + () => { _jobTwoCompleted = true; }, + null + ); + + InMemoryStateStoreAsync store = new(); + InMemoryJobChannel channel = new(); + + _scheduler = new Scheduler( + channel, + store + ); + + _runner = new Runner(channel, store, commandProcessor, _scheduler); + } + + [Fact] + public async Task When_running_a_single_step_workflow() + { + MyCommandHandlerAsync.ReceivedCommands.Clear(); + + await _scheduler.ScheduleAsync([_firstJob, _secondJob]); + + var ct = new CancellationTokenSource(); + ct.CancelAfter( TimeSpan.FromSeconds(120) ); + + try + { + _runner.RunAsync(ct.Token); + } + catch (Exception e) + { + _testOutputHelper.WriteLine(e.ToString()); + } + + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "Test").Should().BeTrue(); + MyCommandHandlerAsync.ReceivedCommands.Any(c => c.Value == "TestTwo").Should().BeTrue(); + _firstJob.State.Should().Be(JobState.Done); + _secondJob.State.Should().Be(JobState.Done); + _jobOneCompleted.Should().BeTrue(); + _jobTwoCompleted.Should().BeTrue(); + } +} diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj index 82f2dfaf51..44db2eaeb1 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj b/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj index b7f14b70ac..734400eba7 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj +++ b/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs b/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs index 7cdbfece57..f62ba26995 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestEvent.cs @@ -3,10 +3,5 @@ namespace Tests { - public class TestEvent : Event - { - public TestEvent() : base(Guid.NewGuid()) - { - } - } -} \ No newline at end of file + public class TestEvent() : Event(Guid.NewGuid().ToString()); +} diff --git a/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj b/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj index aabd4188db..b31b68de48 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj +++ b/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs index 465b4b2421..47cc124a0c 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/MyEvent.cs @@ -2,7 +2,7 @@ namespace Paramore.Brighter.InMemory.Tests.TestDoubles; -public class MyEvent() : Event(Guid.NewGuid()) +public class MyEvent() : Event(Guid.NewGuid().ToString()) { public string Value { get; set; } } diff --git a/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj b/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj index b6726c96bd..108230452b 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj +++ b/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj b/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj index 6fb6f926f5..33a3f2e0a7 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj +++ b/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj b/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj index 4602938f34..6baf5a3e11 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj +++ b/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj index 89eb31e481..3224d7c3aa 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj b/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj index bfd3f8ae41..c5af3cf03a 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj +++ b/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs index aa2157b15a..f2d1107e7c 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/TestDoubles/MyEvent.cs @@ -24,44 +24,40 @@ THE SOFTWARE. */ using System; -namespace Paramore.Brighter.RMQ.Tests.TestDoubles; - -internal class MyEvent : Event, IEquatable +namespace Paramore.Brighter.RMQ.Tests.TestDoubles { - public int Data { get; private set; } - - public MyEvent() : base(Guid.NewGuid()) - { - Data = 7; - } - - public bool Equals(MyEvent other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Data == other.Data; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MyEvent)obj); - } - - public override int GetHashCode() - { - return Data; - } - - public static bool operator ==(MyEvent left, MyEvent right) - { - return Equals(left, right); - } - - public static bool operator !=(MyEvent left, MyEvent right) + internal class MyEvent() : Event(Guid.NewGuid().ToString()), IEquatable { - return !Equals(left, right); + public int Data { get; private set; } = 7; + + public bool Equals(MyEvent? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Data == other.Data; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((MyEvent)obj); + } + + public override int GetHashCode() + { + return Data; + } + + public static bool operator ==(MyEvent left, MyEvent right) + { + return Equals(left, right); + } + + public static bool operator !=(MyEvent left, MyEvent right) + { + return !Equals(left, right); + } } } diff --git a/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj b/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj index abac4c35f2..db12a25835 100644 --- a/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj +++ b/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj b/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj index 2081c16c46..04758329ba 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj +++ b/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false