Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add request types to allow for mediator pattern invocation of tasks #121

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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