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));
+}
+
+///