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

Use expression trees for all type-safe remote calls (💥 BREAKING CHANGES) #75

Merged
merged 3 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ jobs:
name: test-fail-${{ matrix.os }}
path: tests/Temporalio.Tests/TestResults

- name: Confirm bench works
run: dotnet run --project tests/Temporalio.SimpleBench/Temporalio.SimpleBench.csproj -- --workflow-count 5 --max-cached-workflows 100 --max-concurrent 100

- name: Test cloud
# Only supported in non-fork runs, since secrets are not available in forks
if: ${{ matrix.cloudTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-dotnet') }}
Expand Down
92 changes: 39 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ using Temporalio.Activities;

public class MyActivities
{
// We need a "ref" so we can reference instance methods from workflows in a type-safe way
public static readonly MyActivities Ref = ActivityRefs.Create<MyActivities>();

// Activities can be async and/or static too! We just demonstrate instance
// methods since many will use them that way.
[Activity]
Expand All @@ -117,20 +114,16 @@ using Temporalio.Workflows;
[Workflow]
public class SayHelloWorkflow
{
// A "ref" is needed to access methods on this class in a type-safe way from
// the client without instantiating the class
public static readonly SayHelloWorkflow Ref = WorkflowRefs.Create<SayHelloWorkflow>();

[WorkflowRun]
public async Task<string> RunAsync(string name)
{
// This workflow just runs a simple activity to completion.
// StartActivityAsync could be used to just start and there are many
// other things that you can do inside a workflow.
return await Workflow.ExecuteActivityAsync(
// If the activity is static, we don't need Ref
MyActivities.Ref.SayHello,
name,
// This is a lambda expression where the instance is typed. If this
// were static, you wouldn't need a parameter.
(MyActivities act) => act.SayHello(name),
new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });
}
}
Expand Down Expand Up @@ -197,8 +190,7 @@ var client = await TemporalClient.ConnectAsync(new("localhost:7233"));

// Run workflow
var result = await client.ExecuteWorkflowAsync(
SayHelloWorkflow.Ref.RunAsync,
"Temporal",
(SayHelloWorkflow wf) => wf.RunAsync("Temporal"),
new(id: "my-workflow-id", taskQueue: "my-task-queue"));

Console.WriteLine("Workflow result: {0}", result);
Expand Down Expand Up @@ -227,8 +219,7 @@ var client = await TemporalClient.ConnectAsync(new()

// Start a workflow
var handle = await client.StartWorkflowAsync(
MyWorkflow.Ref.RunAsync,
"some workflow argument",
(MyWorkflow wf) => wf.RunAsync("some workflow argument"),
new() { ID = "my-workflow-id", TaskQueue = "my-task-queue" });

// Wait for a result
Expand All @@ -242,7 +233,9 @@ Notes about the above code:
* To enable TLS, the `Tls` option can be set to a non-null `TlsOptions` instance.
* Instead of `StartWorkflowAsync` + `GetResultAsync` above, there is an `ExecuteWorkflowAsync` extension method that is
clearer if the handle is not needed.
* Non-typesafe forms of `StartWorkflowAsync` and `ExecuteWorkflowAsync` exist when there is no workflow definition or
* The type-safe forms of `StartWorkflowAsync` and `ExecuteWorkflowAsync` accept a lambda expression that take the
workflow as the first parameter and must only call the run method in the lambda.
* Non-type-safe forms of `StartWorkflowAsync` and `ExecuteWorkflowAsync` exist when there is no workflow definition or
the workflow may take more than one argument or some other dynamic need. These simply take string workflow type names
and an object array for arguments.
* The `handle` above represents a `WorkflowHandle` which has specific workflow operations on it. For existing workflows,
Expand Down Expand Up @@ -274,7 +267,7 @@ app.MapGet("/", async (Task<TemporalClient> clientTask) =>
{
var client = await clientTask;
return await client.ExecuteWorkflowAsync(
MyWorkflow.Ref.RunAsync,
(MyWorkflow wf) => wf.RunAsync(),
new(id: "my-workflow-id", taskQueue: "my-task-queue"));
});
app.Run();
Expand Down Expand Up @@ -338,7 +331,7 @@ var client = await TemporalClient.ConnectAsync(new()

### Workers

Workers host workflows and/or activites. Here's how to run a worker::
Workers host workflows and/or activities. Here's how to run a worker:

```csharp
using MyNamespace;
Expand Down Expand Up @@ -433,8 +426,6 @@ public record GreetingParams(string Salutation = "Hello", string Name = "<unknow
[Workflow]
public class GreetingWorkflow
{
public static readonly GreetingWorkflow Ref = WorkflowRefs.Create<GreetingWorkflow>();

private string? currentGreeting;
private GreetingParams? greetingParamsUpdate;
private bool complete;
Expand All @@ -447,8 +438,9 @@ public class GreetingWorkflow
{
// Call activity to create greeting and store as field
currentGreeting = await Workflow.ExecuteActivityAsync(
GreetingActivities.CreateGreeting,
greetingParams,
// This is a static activity method. If it were an instance
// method, a typed parameter can be accepted in the lambda call.
() => GreetingActivities.CreateGreeting(greetingParams),
new() { ScheduleToCloseTimeout = TimeSpan.FromMinutes(5) });
Workflow.Logger.LogDebug("Greeting set to {Greeting}", currentGreeting);

Expand Down Expand Up @@ -482,20 +474,14 @@ public class GreetingWorkflow

Notes about the above code:

* The workflow client needs the ability to reference these instance methods, but C# doesn't allow referencing instance
methods without an instance. Therefore we add a readonly `Ref` instance which is a proxy instance just for method
references.
* This is backed by either `GetUninitializedObject` for classes or a dynamic proxy generator for interfaces, but
method invocations should never be made on it. It is only for referencing methods.
* This is technically not needed. Any way that the method can be referenced for a client is acceptable.
* Source generators will provide an additional, alternative way to use workflows in a typed way in the future.
* Interfaces and abstract methods can have these attributes. This is helpful for defining a workflow implemented
elsewhere. But if/when implemented, all pieces of the implementation should have the attributes too.
* This workflow continually updates the greeting params when signalled and can complete with the greeting when given a
different signal
* Workflow code must be deterministic. See the "Workflow Logic Constraints" section below.
* `Workflow.ExecuteActivityAsync` is strongly typed, so if the parameter was not a `GreetingParams` instance,
compilation would fail.
* `Workflow.ExecuteActivityAsync` is strongly typed and accepts a lambda expression. This activity call can be a sync or
async function, return a value or not, and invoked statically or on an instance (which would require accepting the
instance as the only lambda parameter).

Attributes that can be applied:

Expand Down Expand Up @@ -524,24 +510,24 @@ Attributes that can be applied:

#### Running Workflows

To start a workflow from a client, you can `StartWorkflowAsync` and then use the resulting handle:
To start a workflow from a client, you can `StartWorkflowAsync` with a lambda expression and then use the resulting
handle:

```csharp
// Start the workflow
var arg = new GreetingParams(Name: "Temporal");
var handle = await client.StartWorkflowAsync(
GreetingWorkflow.Ref.RunAsync,
new GreetingParams(Name: "Temporal"),
(GreetingWorkflow wf) => wf.RunAsync(arg),
new(id: "my-workflow-id", taskQueue: "my-task-queue"));
// Check current greeting via query
Console.WriteLine(
"Current greeting: {0}",
await handle.QueryWorkflowAsync(GreetingWorkflow.Ref.CurrentGreeting));
await handle.QueryWorkflowAsync(wf => wf.CurrentGreeting()));
// Change the params via signal
await handle.SignalWorkflowAsync(
GreetingWorkflow.Ref.UpdateGreetingParamsAsync,
new GreetingParams(Salutation: "Aloha", Name: "John"));
var signalArg = new GreetingParams(Salutation: "Aloha", Name: "John");
await handle.SignalWorkflowAsync(wf => wf.UpdateGreetingParamsAsync(signalArg));
// Tell it to complete via signal
await handle.SignalWorkflowAsync(GreetingWorkflow.Ref.CompleteWithGreetingAsync);
await handle.SignalWorkflowAsync(wf => wf.CompleteWithGreetingAsync());
// Wait for workflow result
Console.WriteLine(
"Final greeting: {0}",
Expand All @@ -560,9 +546,11 @@ Some things to note about the above code:

#### Invoking Activities

* Activities are executed with `Workflow.ExecuteActivityAsync` which accepts an activity method delegate + argument if
required, or a string name + object array of arguments.
* Activity options are a simple class set after any activity and its argument(s).
* Activities are executed with `Workflow.ExecuteActivityAsync` which accepts a lambda expression that invokes the
activity with its arguments. The activity method can be sync or async, return a result or not, and be static or an
instance method (which would require the parameter of the lambda to be the instance type).
* A non-type-safe form of `ExecuteActivityAsync` exists that just accepts a string activity name.
* Activity options are a simple class set after the lambda expression or name.
* These options must be present and either `ScheduleToCloseTimeout` or `StartToCloseTimeout` must be present.
* Retry policy, cancellation type, etc can also be set on the options.
* Cancellation token is defaulted as the workflow cancellation token, but an alternative can be given in options.
Expand All @@ -573,9 +561,10 @@ Some things to note about the above code:

#### Invoking Child Workflows

* Child workflows are started with `Workflow.StartChildWorkflowAsync` which accepts a workflow run method delegate +
argument if required, or a string name + object array of arguments.
* Child workflow options are a simple class set after any child workflow run method and its argument(s).
* Child workflows are started with `Workflow.StartChildWorkflowAsync` which accepts a lambda expression whose parameter
is the child workflow to call and the expression is a call to its run method with arguments.
* A non-type-safe form of `StartChildWorkflowAsync` exists that just accepts a string workflow name.
* Child workflow options are a simple class set after after the lambda expression or name.
* These options are optional.
* Retry policy, ID, etc can also be set on the options.
* Cancellation token is defaulted as the workflow cancellation token, but an alternative can be given in options.
Expand Down Expand Up @@ -641,7 +630,8 @@ can be used from workflows including:
* `Unsafe.IsReplaying` - For advanced users to know whether the workflow is replaying. This is rarely needed.
* Methods:
* `CreateContinueAsNewException` - Create exception that can be thrown to perform a continue-as-new on the workflow.
There are several overloads to properly type workflow arguments similar to start/execute workflow calls elsewhere.
There are several overloads to properly accept a lambda expression for the workflow similar to start/execute
workflow calls elsewhere.
* `GetExternalWorkflowHandle` - Get a handle to an external workflow to issue cancellation requests and signals.
* `NewGuid` - Create a deterministically random UUIDv4 GUID.
* `Patched` and `DeprecatePatch` - Support for patch-based versioning inside the workflow.
Expand Down Expand Up @@ -803,8 +793,6 @@ using Temporalio.Workflows;
[Workflow]
public class WaitADayWorkflow
{
public static readonly WaitADayWorkflow Ref = WorkflowRefs.Create<WaitADayWorkflow>();

[WorkflowRun]
public async Task<string> RunAsync()
{
Expand All @@ -830,7 +818,7 @@ public async Task WaitADayWorkflow_SimpleRun_Succeeds()
new TemporalWorkerOptions($"task-queue-{Guid.NewGuid()}").
AddWorkflow<WaitADayWorkflow>());
var result = await env.Client.ExecuteWorkflowAsync(
WaitADayWorkflow.Ref.RunAsync,
(WaitADayWorkflow wf) => wf.RunAsync(),
new(id: $"wf-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
Assert.Equal("all done", result);
}
Expand All @@ -856,8 +844,6 @@ using Temporalio.Workflows;
[Workflow]
public class SignalWorkflow
{
public static readonly SignalWorkflow Ref = WorkflowRefs.Create<SignalWorkflow>();

private bool signalReceived = false;

[WorkflowRun]
Expand Down Expand Up @@ -891,9 +877,9 @@ public async Task SignalWorkflow_SendSignal_HasExpectedResult()
new TemporalWorkerOptions($"task-queue-{Guid.NewGuid()}").
AddWorkflow<SignalWorkflow>());
var handle = await env.Client.StartWorkflowAsync(
SignalWorkflow.Ref.RunAsync,
(SignalWorkflow wf) => wf.RunAsync(),
new(id: $"wf-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
await handle.SignalAsync(SignalWorkflow.Ref.SomeSignalAsync);
await handle.SignalAsync(wf => wf.SomeSignalAsync());
Assert.Equal("got signal", await handle.GetResultAsync());
}
```
Expand All @@ -913,7 +899,7 @@ public async Task SignalWorkflow_SignalTimeout_HasExpectedResult()
new TemporalWorkerOptions($"task-queue-{Guid.NewGuid()}").
AddWorkflow<SignalWorkflow>());
var handle = await env.Client.StartWorkflowAsync(
SignalWorkflow.Ref.RunAsync,
(SignalWorkflow wf) => wf.RunAsync(),
new(id: $"wf-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
await env.DelayAsync(TimeSpan.FromSeconds(50));
Assert.Equal("got timeout", await handle.GetResultAsync());
Expand Down
17 changes: 16 additions & 1 deletion src/Temporalio/Activities/ActivityDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public static ActivityDefinition Create(
/// <param name="cache">True if each definition should be cached.</param>
/// <returns>Collection of activity definitions on the type.</returns>
public static IReadOnlyCollection<ActivityDefinition> CreateAll<T>(
T? instance, bool cache = true) => CreateAll(typeof(T), cache);
T? instance, bool cache = true) => CreateAll(typeof(T), instance, cache);

/// <summary>
/// Create all applicable activity definitions for the given type. At least one activity
Expand Down Expand Up @@ -153,6 +153,21 @@ public static IReadOnlyCollection<ActivityDefinition> CreateAll(
return ret;
}

/// <summary>
/// Gets the activity name from the given ActivityAttribute method.
/// </summary>
/// <param name="method">Method to get name from.</param>
/// <returns>Name.</returns>
/// <exception cref="ArgumentException">
/// If method does not have ActivityAttribute.
/// </exception>
public static string NameFromMethod(MethodInfo method)
{
var attr = method.GetCustomAttribute<ActivityAttribute>(false) ??
throw new ArgumentException($"{method} missing Activity attribute");
return NameFromAttributed(method, attr);
}

/// <summary>
/// Invoke this activity with the given parameters. Before calling this, callers should
/// have already validated that the parameters match <see cref="ParameterTypes" /> and there
Expand Down
16 changes: 0 additions & 16 deletions src/Temporalio/Activities/ActivityRefs.cs

This file was deleted.

Loading