Skip to content
7 changes: 7 additions & 0 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grpc", "src\Grpc\Grpc.cspro
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "test\Benchmarks\Benchmarks.csproj", "{82C0CD7D-2764-421A-8256-7E2304D5A6E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview", "samples\Preview\Preview.csproj", "{EA7F706E-9738-4DDB-9089-F17F927E1247}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -167,6 +169,10 @@ Global
{82C0CD7D-2764-421A-8256-7E2304D5A6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82C0CD7D-2764-421A-8256-7E2304D5A6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82C0CD7D-2764-421A-8256-7E2304D5A6E7}.Release|Any CPU.Build.0 = Release|Any CPU
{EA7F706E-9738-4DDB-9089-F17F927E1247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA7F706E-9738-4DDB-9089-F17F927E1247}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA7F706E-9738-4DDB-9089-F17F927E1247}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA7F706E-9738-4DDB-9089-F17F927E1247}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -199,6 +205,7 @@ Global
{93E3B973-0FC4-4241-B7BB-064FB538FB50} = {5AD837BC-78F3-4543-8AA3-DF74D0DF94C0}
{44AD321D-96D4-481E-BD41-D0B12A619833} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{82C0CD7D-2764-421A-8256-7E2304D5A6E7} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{EA7F706E-9738-4DDB-9089-F17F927E1247} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using McMaster.Extensions.CommandLineUtils;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;

namespace Preview.MediatorPattern.ExistingTypes;

/**
* This sample shows mediator-pattern orchestrations and activities using existing types as their inputs. In this mode,
* the request object provides a distinct separate object as the input to the task. The below code has no real purpose
* nor demonstrates good ways to organize orchestrations or activities. The purpose is to demonstrate how the static
* 'CreateRequest' method way of using the mediator pattern.
*
* This is just one such way to leverage the mediator-pattern. Ultimately all the request object is all that is needed,
* how it is created is flexible.
*/

public class MediatorOrchestrator1 : TaskOrchestrator<MyInput> // Single generic means it has no output. Only input.
{
public static IOrchestrationRequest CreateRequest(string propA, string propB)
=> OrchestrationRequest.Create(nameof(MediatorOrchestrator1), new MyInput(propA, propB));

public override async Task RunAsync(TaskOrchestrationContext context, MyInput input)
{
string output = await context.RunAsync(MediatorSubOrchestrator1.CreateRequest(input.PropA));
await context.RunAsync(WriteConsoleActivity1.CreateRequest(output));

output = await context.RunAsync(ExpandActivity1.CreateRequest(input.PropB));
await context.RunAsync(WriteConsoleActivity1.CreateRequest(output));
}
}

public class MediatorSubOrchestrator1 : TaskOrchestrator<string, string>
{
public static IOrchestrationRequest<string> CreateRequest(string input)
=> OrchestrationRequest.Create<string>(nameof(MediatorSubOrchestrator1), input);

public override Task<string> RunAsync(TaskOrchestrationContext context, string input)
{
// Orchestrations create replay-safe loggers off the
ILogger logger = context.CreateReplaySafeLogger<MediatorSubOrchestrator1>();
logger.LogDebug("In MySubOrchestrator");
return context.RunAsync(ExpandActivity1.CreateRequest($"{nameof(MediatorSubOrchestrator1)}: {input}"));
}
}

public class WriteConsoleActivity1 : TaskActivity<string> // Single generic means it has no output. Only input.
{
readonly IConsole console;

public WriteConsoleActivity1(IConsole console) // Dependency injection example.
{
this.console = console;
}

public static IActivityRequest CreateRequest(string input)
=> ActivityRequest.Create(nameof(WriteConsoleActivity1), input);

public override Task RunAsync(TaskActivityContext context, string input)
{
this.console.WriteLine(input);
return Task.CompletedTask;
}
}

public class ExpandActivity1 : TaskActivity<string, string>
{
readonly ILogger logger;

public ExpandActivity1(ILogger<ExpandActivity1> logger) // Activities get logger from DI.
{
this.logger = logger;
}

public static IActivityRequest<string> CreateRequest(string input)
=> ActivityRequest.Create<string>(nameof(ExpandActivity1), input);

public override Task<string> RunAsync(TaskActivityContext context, string input)
{
this.logger.LogDebug("In ExpandActivity");
return Task.FromResult($"Input received: {input}");
}
}

public record MyInput(string PropA, string PropB);
27 changes: 27 additions & 0 deletions samples/Preview/MediatorPattern/Mediator1/Mediator1Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using McMaster.Extensions.CommandLineUtils;
using Microsoft.DurableTask;
using Preview.MediatorPattern.ExistingTypes;

namespace Preview.MediatorPattern;

[Command(Description = "Runs the first mediator sample")]
public class Mediator1Command : SampleCommandBase
{
public static void Register(DurableTaskRegistry tasks)
{
tasks.AddActivity<ExpandActivity1>();
tasks.AddActivity<WriteConsoleActivity1>();
tasks.AddOrchestrator<MediatorOrchestrator1>();
tasks.AddOrchestrator<MediatorSubOrchestrator1>();
}

protected override IBaseOrchestrationRequest GetRequest()
{
return MediatorOrchestrator1.CreateRequest("PropInputA", "PropInputB");
}
}


27 changes: 27 additions & 0 deletions samples/Preview/MediatorPattern/Mediator2/Mediator2Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using McMaster.Extensions.CommandLineUtils;
using Microsoft.DurableTask;
using Preview.MediatorPattern.NewTypes;

namespace Preview.MediatorPattern;

[Command(Description = "Runs the second mediator sample")]
public class Mediator2Command : SampleCommandBase
{
public static void Register(DurableTaskRegistry tasks)
{
tasks.AddActivity<ExpandActivity2>();
tasks.AddActivity<WriteConsoleActivity2>();
tasks.AddOrchestrator<MediatorOrchestrator2>();
tasks.AddOrchestrator<MediatorSubOrchestrator2>();
}

protected override IBaseOrchestrationRequest GetRequest()
{
return new MediatorOrchestratorRequest("PropA", "PropB");
}
}


90 changes: 90 additions & 0 deletions samples/Preview/MediatorPattern/Mediator2/NewTypesOrchestration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using McMaster.Extensions.CommandLineUtils;
using Microsoft.DurableTask;
using Microsoft.Extensions.Logging;

namespace Preview.MediatorPattern.NewTypes;

/**
* This sample shows mediator-pattern orchestrations and activities using newly defined request types as their input. In
* this mode, the request object is the input to the task itself. The below code has no real purpose nor demonstrates
* good ways to organize orchestrations or activities. The purpose is to demonstrate how request objects can be defined
* manually and provided directly to RunAsync method.
*
* This is just one such way to leverage the mediator-pattern. Ultimately all the request object is all that is needed,
* how it is created is flexible.
*/

public record MediatorOrchestratorRequest(string PropA, string PropB) : IOrchestrationRequest
{
public TaskName GetTaskName() => nameof(MediatorOrchestrator2);
}

public class MediatorOrchestrator2 : TaskOrchestrator<MediatorOrchestratorRequest> // Single generic means it has no output. Only input.
{
public override async Task RunAsync(TaskOrchestrationContext context, MediatorOrchestratorRequest input)
{
string output = await context.RunAsync(new MediatorSubOrchestratorRequest(input.PropA));
await context.RunAsync(new WriteConsoleActivityRequest(output));
}
}

public record MediatorSubOrchestratorRequest(string Value) : IOrchestrationRequest<string>
{
public TaskName GetTaskName() => nameof(MediatorSubOrchestrator2);
}

public class MediatorSubOrchestrator2 : TaskOrchestrator<MediatorSubOrchestratorRequest, string>
{
public override Task<string> RunAsync(TaskOrchestrationContext context, MediatorSubOrchestratorRequest input)
{
// Orchestrations create replay-safe loggers off the
ILogger logger = context.CreateReplaySafeLogger<MediatorSubOrchestrator2>();
logger.LogDebug("In MySubOrchestrator");
return context.RunAsync(new ExpandActivityRequest($"{nameof(MediatorSubOrchestrator2)}: {input.Value}"));
}
}

public record WriteConsoleActivityRequest(string Value) : IActivityRequest<string>
{
public TaskName GetTaskName() => nameof(WriteConsoleActivity2);
}

public class WriteConsoleActivity2 : TaskActivity<WriteConsoleActivityRequest> // Single generic means it has no output. Only input.
{
readonly IConsole console;

public WriteConsoleActivity2(IConsole console) // Dependency injection example.
{
this.console = console;
}

public override Task RunAsync(TaskActivityContext context, WriteConsoleActivityRequest input)
{
this.console.WriteLine(input.Value);
return Task.CompletedTask;
}
}

public record ExpandActivityRequest(string Value) : IActivityRequest<string>
{
public TaskName GetTaskName() => nameof(ExpandActivity2);
}

public class ExpandActivity2 : TaskActivity<ExpandActivityRequest, string>
{
readonly ILogger logger;

public ExpandActivity2(ILogger<ExpandActivity2> logger) // Activities get logger from DI.
{
this.logger = logger;
}

public override Task<string> RunAsync(TaskActivityContext context, ExpandActivityRequest input)
{
this.logger.LogDebug("In ExpandActivity");
return Task.FromResult($"Input received: {input.Value}");
}
}
56 changes: 56 additions & 0 deletions samples/Preview/MediatorPattern/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Mediator Pattern

## Running this sample

First sample:
``` cli
dotnet run Preview.csproj -- mediator1
```

Second sample:
``` cli
dotnet run Preview.csproj -- mediator2
```

**NOTE**: see [dotnet run](https://learn.microsoft.com/dotnet/core/tools/dotnet-run). The `--` with a space following it is important.

## What is the mediator pattern?

> In software engineering, the mediator pattern defines an object that encapsulates how a set of objects interact. This pattern is considered to be a behavioral pattern due to the way it can alter the program's running behavior.
>
> -- [wikipedia](https://en.wikipedia.org/wiki/Mediator_pattern)

Specifically to Durable Task, this means using objects to assist with enqueueing of orchestrations, sub-orchestrations, and activities. These objects handle all of the following:

1. Defining which `TaskOrchestrator` or `TaskActivity` to run.
2. Providing the input for the task to be ran.
3. Defining the output type of the task.

The end result is the ability to invoke orchestrations and activities in a type-safe manner.

## What does it look like?

Instead of supplying the name, input, and return type of an orchestration or activity separately, instead a 'request' object is used to do all of these at once.

Example: enqueueing an activity.

Raw API:
``` CSharp
string result = await context.RunActivityAsync<string>(nameof(MyActivity), input);
```

Explicit extension method [1]:
``` csharp
string result = await context.RunMyActivityAsync(input);
```

Mediator
``` csharp
string result = await context.RunAsync(MyActivity.CreateRequest(input));

// OR - it is up to individual developers which style they prefer. Can also be mixed and matched as seen fit.

string result = await context.RunAsync(new MyActivityRequest(input));
```

[1] - while the extension method is concise, having many extension methods off the same type can make intellisense a bit unwieldy.
20 changes: 20 additions & 0 deletions samples/Preview/Preview.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="McMaster.Extensions.Hosting.CommandLine" Version="4.0.2" />
<PackageReference Include="Microsoft.DurableTask.Sidecar" Version="0.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" />
<ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" />
</ItemGroup>

</Project>
Loading