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
97 changes: 97 additions & 0 deletions src/Abstractions/ActivityRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask;

/// <summary>
/// Represents the base request to run a <see cref="ITaskActivity" />.
/// </summary>
public interface IBaseActivityRequest
{
/// <summary>
/// Gets the <see cref="TaskName" /> representing the <see cref="ITaskActivity" /> to run.
/// </summary>
/// <returns>A <see cref="TaskName" />.</returns>
/// <remarks>
/// This is a function instead of a property so it is excluded in serialization without needing to use a
/// serialization library specific attribute to exclude it.
/// </remarks>
TaskName GetTaskName();
}

/// <summary>
/// Represents a request to run a <see cref="ITaskActivity" /> which returns <typeparamref name="TResult" />.
/// </summary>
/// <typeparam name="TResult">The result of the orchestrator that is to be ran.</typeparam>
public interface IActivityRequest<out TResult> : IBaseActivityRequest
{
}

/// <summary>
/// Represents a request to run a <see cref="ITaskActivity" /> which has no return.
/// </summary>
public interface IActivityRequest : IActivityRequest<Unit>
{
}

/// <summary>
/// Helpers for creating activity requests.
/// </summary>
public static class ActivityRequest
{
/// <summary>
/// Gets an <see cref="IActivityRequest{TResult}" /> which has an explicitly provided input.
/// </summary>
/// <remarks>
/// This is useful when you want to use an existing type for input (like <see cref="string" />) and not derive an
/// entirely new type.
/// </remarks>
/// <typeparam name="TResult">The result type of the activity.</typeparam>
/// <param name="name">The name of the activity to run.</param>
/// <param name="input">The input for the activity.</param>
/// <returns>A request that can be used to enqueue an activity.</returns>
public static IActivityRequest<TResult> Create<TResult>(TaskName name, object? input = null)
=> new Request<TResult>(name, input);

/// <summary>
/// Gets the activity input from a <see cref="IBaseActivityRequest" />.
/// </summary>
/// <param name="request">The request to get input for.</param>
/// <returns>The input.</returns>
internal static object? GetInput(this IBaseActivityRequest request)
{
if (request is IProvidesInput provider)
{
return provider.GetInput();
}

return request;
}

/// <summary>
/// Represents an activity request where the input is not the request itself.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
class Request<TResult> : IActivityRequest<TResult>, IProvidesInput
{
readonly TaskName name;
readonly object? input;

/// <summary>
/// Initializes a new instance of the <see cref="Request{TResult}"/> class.
/// </summary>
/// <param name="name">The task name.</param>
/// <param name="input">The input.</param>
public Request(TaskName name, object? input)
{
this.name = name;
this.input = input;
}

/// <inheritdoc/>
public object? GetInput() => this.input;

/// <inheritdoc/>
public TaskName GetTaskName() => this.name;
}
}
19 changes: 19 additions & 0 deletions src/Abstractions/IProvidesInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask;

/// <summary>
/// Contract for providing input to an orchestration or activity.
/// </summary>
interface IProvidesInput
{
/// <summary>
/// Gets the input for the orchestration or activity.
/// </summary>
/// <returns>The input value.</returns>
/// <remarks>
/// This is a method and not a property to ensure it is not included in serialization.
/// </remarks>
object? GetInput();
}
39 changes: 39 additions & 0 deletions src/Abstractions/InputHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask;

/// <summary>
/// Orchestration/Activity input helpers.
/// </summary>
static class InputHelper
{
/// <summary>
/// Due to nullable reference types being static analysis only, we need to do our best efforts for validating the
/// input type, but also give control of nullability to the implementation. It is not ideal, but we do not want to
/// force 'TInput?' on the RunAsync implementation.
/// </summary>
/// <typeparam name="TInput">The input type of the orchestration or activity.</typeparam>
/// <param name="input">The input object.</param>
/// <param name="typedInput">The input converted to the desired type.</param>
public static void ValidateInput<TInput>(object? input, out TInput typedInput)
{
if (input is TInput typed)
{
// Quick pattern check.
typedInput = typed;
return;
}
else if (input is not null && typeof(TInput) != input.GetType())
{
throw new ArgumentException($"Input type '{input?.GetType()}' does not match expected type '{typeof(TInput)}'.");
}

// Input is null and did not match a nullable value type. We do not have enough information to tell if it is
// valid or not. We will have to defer this decision to the implementation. Additionally, we will coerce a null
// input to a default value type here. This is to keep the two RunAsync(context, default) overloads to have
// identical behavior.
typedInput = default!;
return;
}
}
97 changes: 97 additions & 0 deletions src/Abstractions/OrchestrationRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask;

/// <summary>
/// Represents the base request to run a <see cref="ITaskOrchestrator" />.
/// </summary>
public interface IBaseOrchestrationRequest
{
/// <summary>
/// Gets the <see cref="TaskName" /> representing the <see cref="ITaskOrchestrator" /> to run.
/// </summary>
/// <returns>A <see cref="TaskName" />.</returns>
/// <remarks>
/// This is a function instead of a property so it is excluded in serialization without needing to use a
/// serialization library specific attribute to exclude it.
/// </remarks>
TaskName GetTaskName();
}

/// <summary>
/// Represents a request to run a <see cref="ITaskOrchestrator" /> which returns <typeparamref name="TResult" />.
/// </summary>
/// <typeparam name="TResult">The result of the orchestrator that is to be ran.</typeparam>
public interface IOrchestrationRequest<out TResult> : IBaseOrchestrationRequest
{
}

/// <summary>
/// Represents a request to run a <see cref="ITaskOrchestrator" /> which has no return.
/// </summary>
public interface IOrchestrationRequest : IOrchestrationRequest<Unit>
{
}

/// <summary>
/// Helpers for creating orchestration requests.
/// </summary>
public static class OrchestrationRequest
{
/// <summary>
/// Gets an <see cref="IOrchestrationRequest{TResult}" /> which has an explicitly provided input.
/// </summary>
/// <remarks>
/// This is useful when you want to use an existing type for input (like <see cref="string" />) and not derive an
/// entirely new type.
/// </remarks>
/// <typeparam name="TResult">The result type of the orchestration.</typeparam>
/// <param name="name">The name of the orchestration to run.</param>
/// <param name="input">The input for the orchestration.</param>
/// <returns>A request that can be used to enqueue an orchestration.</returns>
public static IOrchestrationRequest<TResult> Create<TResult>(TaskName name, object? input = null)
=> new Request<TResult>(name, input);

/// <summary>
/// Gets the orchestration input from a <see cref="IBaseOrchestrationRequest" />.
/// </summary>
/// <param name="request">The request to get input for.</param>
/// <returns>The input.</returns>
internal static object? GetInput(this IBaseOrchestrationRequest request)
{
if (request is IProvidesInput provider)
{
return provider.GetInput();
}

return request;
}

/// <summary>
/// Represents an orchestration request where the input is not the request itself.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
class Request<TResult> : IOrchestrationRequest<TResult>, IProvidesInput
{
readonly TaskName name;
readonly object? input;

/// <summary>
/// Initializes a new instance of the <see cref="Request{TResult}"/> class.
/// </summary>
/// <param name="name">The task name.</param>
/// <param name="input">The input.</param>
public Request(TaskName name, object? input)
{
this.name = name;
this.input = input;
}

/// <inheritdoc/>
public object? GetInput() => this.input;

/// <inheritdoc/>
public TaskName GetTaskName() => this.name;
}
}
56 changes: 25 additions & 31 deletions src/Abstractions/TaskActivity.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.DurableTask;

/// <summary>
Expand Down Expand Up @@ -74,11 +72,7 @@ public abstract class TaskActivity<TInput, TOutput> : ITaskActivity
async Task<object?> ITaskActivity.RunAsync(TaskActivityContext context, object? input)
{
Check.NotNull(context, nameof(context));
if (!IsValidInput(input, out TInput? typedInput))
{
throw new ArgumentException($"Input type '{input?.GetType()}' does not match expected type '{typeof(TInput)}'.");
}

InputHelper.ValidateInput(input, out TInput typedInput);
return await this.RunAsync(context, typedInput);
}

Expand All @@ -89,31 +83,31 @@ public abstract class TaskActivity<TInput, TOutput> : ITaskActivity
/// <param name="input">The deserialized activity input.</param>
/// <returns>The output of the activity as a task.</returns>
public abstract Task<TOutput> RunAsync(TaskActivityContext context, TInput input);
}

/// <summary>
/// Due to nullable reference types being static analysis only, we need to do our best efforts for validating the
/// input type, but also give control of nullability to the implementation. It is not ideal, but we do not want to
/// force 'TInput?' on the RunAsync implementation.
/// </summary>
static bool IsValidInput(object? input, [NotNullWhen(true)] out TInput? typedInput)
{
if (input is TInput typed)
{
// Quick pattern check.
typedInput = typed;
return true;
}
else if (input is not null && typeof(TInput) != input.GetType())
{
typedInput = default;
return false;
}
/// <inheritdoc cref="TaskActivity{TInput, Unit}" />
public abstract class TaskActivity<TInput> : ITaskActivity
jviau marked this conversation as resolved.
Show resolved Hide resolved
{
/// <inheritdoc/>
Type ITaskActivity.InputType => typeof(TInput);

/// <inheritdoc/>
Type ITaskActivity.OutputType => typeof(Unit);

// Input is null and did not match a nullable value type. We do not have enough information to tell if it is
// valid or not. We will have to defer this decision to the implementation. Additionally, we will coerce a null
// input to a default value type here. This is to keep the two RunAsync(context, default) overloads to have
// identical behavior.
typedInput = default!;
return true;
/// <inheritdoc/>
async Task<object?> ITaskActivity.RunAsync(TaskActivityContext context, object? input)
{
Check.NotNull(context, nameof(context));
InputHelper.ValidateInput(input, out TInput typedInput);
await this.RunAsync(context, typedInput);
return Unit.Value;
}

/// <summary>
/// Override to implement async (non-blocking) task activity logic.
/// </summary>
/// <param name="context">Provides access to additional context for the current activity execution.</param>
/// <param name="input">The deserialized activity input.</param>
/// <returns>The output of the activity as a task.</returns>
public abstract Task RunAsync(TaskActivityContext context, TInput input);
}
Loading