Skip to content

Commit

Permalink
Add type-safe SignalWithStart (#193)
Browse files Browse the repository at this point in the history
  • Loading branch information
cretz authored Feb 28, 2024
1 parent 66abe70 commit c703da9
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ Some things to note about the above code:
`StartWorkflowAsync` would be a compile-time failure.
* The `handle` is also typed with the workflow result, so `GetResultAsync()` returns a `string` as expected.
* A shortcut extension `ExecuteWorkflowAsync` is available that is just `StartWorkflowAsync` + `GetResultAsync`.
* `SignalWithStart` method is present on the workflow options to make the workflow call a signal-with-start call which
means it will only start the workflow if it's not running, but send a signal to it regardless.

#### Invoking Activities

Expand Down
28 changes: 28 additions & 0 deletions src/Temporalio/Client/WorkflowOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Temporalio.Api.Enums.V1;
using Temporalio.Common;

Expand Down Expand Up @@ -105,6 +107,32 @@ public WorkflowOptions(string id, string taskQueue)
/// </summary>
public RpcOptions? Rpc { get; set; }

/// <summary>
/// Perform a signal-with-start which will only start the workflow if it's not already
/// running, but send a signal to it regardless. This is just sugar for manually setting
/// <see cref="StartSignal" /> and <see cref="StartSignalArgs" /> directly.
/// </summary>
/// <typeparam name="TWorkflow">Workflow class type.</typeparam>
/// <param name="signalCall">Invocation or a workflow signal method.</param>
public void SignalWithStart<TWorkflow>(Expression<Func<TWorkflow, Task>> signalCall)
{
var (method, args) = ExpressionUtil.ExtractCall(signalCall);
SignalWithStart(Workflows.WorkflowSignalDefinition.NameFromMethodForCall(method), args);
}

/// <summary>
/// Perform a signal-with-start which will only start the workflow if it's not already
/// running, but send a signal to it regardless. This is just sugar for manually setting
/// <see cref="StartSignal" /> and <see cref="StartSignalArgs" /> directly.
/// </summary>
/// <param name="signal">Signal name.</param>
/// <param name="args">Signal args.</param>
public void SignalWithStart(string signal, IReadOnlyCollection<object?> args)
{
StartSignal = signal;
StartSignalArgs = args;
}

/// <summary>
/// Create a shallow copy of these options.
/// </summary>
Expand Down
45 changes: 43 additions & 2 deletions tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,43 @@ await handle.SignalAsync(
});
}

[Fact]
public async Task ExecuteWorkflowAsync_Signals_SignalWithStart()
{
await ExecuteWorkerAsync<SignalWorkflow>(async worker =>
{
// Start the workflow with a signal
var options = new WorkflowOptions(
id: $"workflow-{Guid.NewGuid()}",
taskQueue: worker.Options.TaskQueue!);
options.SignalWithStart((SignalWorkflow wf) => wf.Signal1Async("signalval1"));
var handle = await Env.Client.StartWorkflowAsync(
(SignalWorkflow wf) => wf.RunAsync(),
options);
// Confirm signal received
Assert.Equal(
new List<string>
{
"Signal1: signalval1",
},
await handle.QueryAsync(wf => wf.Events()));
// Do it again, confirm signal received on same workflow
options.SignalWithStart((SignalWorkflow wf) => wf.Signal1Async("signalval2"));
var newHandle = await Env.Client.StartWorkflowAsync(
(SignalWorkflow wf) => wf.RunAsync(),
options);
Assert.Equal(handle.ResultRunId, newHandle.ResultRunId);
// Confirm signal received
Assert.Equal(
new List<string>
{
"Signal1: signalval1",
"Signal1: signalval2",
},
await handle.QueryAsync(wf => wf.Events()));
});
}

[Workflow]
public class BadSignalArgsDroppedWorkflow
{
Expand Down Expand Up @@ -3939,8 +3976,12 @@ private async Task ExecuteWorkerAsync<TWf>(
TemporalWorkerOptions? options = null,
IWorkerClient? client = null)
{
await ExecuteWorkerAsyncReturning<TWf, bool>(
(w) => action(w).ContinueWith(t => true),
await ExecuteWorkerAsyncReturning<TWf, ValueTuple>(
async (w) =>
{
await action(w);
return ValueTuple.Create();
},
options,
client);
}
Expand Down

0 comments on commit c703da9

Please sign in to comment.