-
Notifications
You must be signed in to change notification settings - Fork 61
Generic Temporal Nexus Operation Handler #690
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
Merged
jmaeagle99
merged 10 commits into
temporalio:main
from
Quinn-With-Two-Ns:nexus-generic-handler
Jun 3, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
2c4d250
Add generic Temporal operation handler
Quinn-With-Two-Ns c57034b
/
Quinn-With-Two-Ns d8751d1
Add more test coverage
Quinn-With-Two-Ns ea6c135
Review
Quinn-With-Two-Ns a29fdb1
Respond to PR comments
Quinn-With-Two-Ns 5d2b4d7
Refactor some context usage
Quinn-With-Two-Ns 14e3844
Fix tests
Quinn-With-Two-Ns e9a53f7
Rename to match what we decided to call them in Python and Go
Quinn-With-Two-Ns 1514ea4
Remove client from StartWorkflowAndGetTokenAsync
Quinn-With-Two-Ns c354d2a
Respond to PR comments
Quinn-With-Two-Ns File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq.Expressions; | ||
| using System.Threading.Tasks; | ||
| using NexusRpc.Handlers; | ||
| using Temporalio.Client; | ||
|
|
||
| namespace Temporalio.Nexus | ||
| { | ||
| /// <summary> | ||
| /// Nexus-aware client wrapping the Temporal client. Provides methods for starting workflows | ||
| /// from within a Nexus operation handler. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// <para>WARNING: Nexus support is experimental.</para> | ||
| /// <para>Obtained via the <see cref="TemporalOperationHandler.FromHandleFactory{TInput, TResult}"/> | ||
| /// start function parameter.</para> | ||
| /// <para>Example usage — starting a workflow from an operation handler:</para> | ||
| /// <code> | ||
| /// await client.StartWorkflowAsync<MyWorkflow, MyResult>( | ||
| /// wf => wf.RunAsync(input), | ||
| /// new(id: "my-workflow-id", taskQueue: "my-task-queue")); | ||
| /// </code> | ||
| /// <para>To perform a synchronous operation (e.g., sending a signal), use the underlying | ||
| /// <see cref="TemporalClient"/> and return a sync result:</para> | ||
| /// <code> | ||
| /// await client.TemporalClient | ||
| /// .GetWorkflowHandle($"order-{input.OrderId}") | ||
| /// .SignalAsync("requestCancellation", new[] { input }); | ||
| /// return TemporalOperationResult<NoValue>.SyncResult(default); | ||
| /// </code> | ||
| /// </remarks> | ||
| public interface ITemporalNexusClient | ||
| { | ||
| /// <summary> | ||
| /// Gets the underlying Temporal client for advanced use cases such as sending signals | ||
| /// or queries. | ||
| /// </summary> | ||
| ITemporalClient TemporalClient { get; } | ||
|
|
||
| /// <summary> | ||
| /// Start a workflow via a lambda invoking the run method. Always returns an async result | ||
| /// with a workflow-run operation token. | ||
| /// </summary> | ||
| /// <typeparam name="TWorkflow">Workflow class type.</typeparam> | ||
| /// <typeparam name="TResult">Workflow result type.</typeparam> | ||
| /// <param name="workflowRunCall">Invocation of workflow run method with a result.</param> | ||
| /// <param name="options">Start workflow options. ID and TaskQueue are required.</param> | ||
| /// <returns>An async operation result containing the workflow-run token.</returns> | ||
| Task<TemporalOperationResult<TResult>> StartWorkflowAsync<TWorkflow, TResult>( | ||
| Expression<Func<TWorkflow, Task<TResult>>> workflowRunCall, WorkflowOptions options); | ||
|
|
||
| /// <summary> | ||
| /// Start a workflow via a lambda invoking the run method with no return value. Always | ||
| /// returns an async result with a workflow-run operation token. | ||
| /// </summary> | ||
| /// <typeparam name="TWorkflow">Workflow class type.</typeparam> | ||
| /// <param name="workflowRunCall">Invocation of workflow run method with no result.</param> | ||
| /// <param name="options">Start workflow options. ID and TaskQueue are required.</param> | ||
| /// <returns>An async operation result containing the workflow-run token.</returns> | ||
| Task<TemporalOperationResult<NoValue>> StartWorkflowAsync<TWorkflow>( | ||
| Expression<Func<TWorkflow, Task>> workflowRunCall, WorkflowOptions options); | ||
|
|
||
| /// <summary> | ||
| /// Start a workflow by name. Always returns an async result with a workflow-run operation | ||
| /// token. | ||
| /// </summary> | ||
| /// <typeparam name="TResult">Workflow result type.</typeparam> | ||
| /// <param name="workflow">Workflow type name.</param> | ||
| /// <param name="args">Arguments for the workflow.</param> | ||
| /// <param name="options">Start workflow options. ID and TaskQueue are required.</param> | ||
| /// <returns>An async operation result containing the workflow-run token.</returns> | ||
| Task<TemporalOperationResult<TResult>> StartWorkflowAsync<TResult>( | ||
| string workflow, IReadOnlyCollection<object?> args, WorkflowOptions options); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.Extensions.Logging; | ||
| using NexusRpc.Handlers; | ||
| using Temporalio.Api.Common.V1; | ||
| using Temporalio.Api.Enums.V1; | ||
| using Temporalio.Client; | ||
|
|
||
| namespace Temporalio.Nexus | ||
| { | ||
| /// <summary> | ||
| /// Internal helper for starting workflows from Nexus operations and managing operation tokens. | ||
| /// Shared by both <see cref="WorkflowRunOperationContext"/> and <see cref="TemporalNexusClient"/>. | ||
| /// </summary> | ||
| internal static class NexusWorkflowStartHelper | ||
| { | ||
| private const string NexusOperationTokenHeader = "Nexus-Operation-Token"; | ||
|
|
||
| /// <summary> | ||
| /// Start a workflow and return the workflow-run handle. This handles all Nexus plumbing: | ||
| /// cloning options, setting task queue, processing links, injecting callbacks, and | ||
| /// adding outbound links. | ||
| /// </summary> | ||
| /// <param name="nexusStartContext">Nexus start context for callbacks and links.</param> | ||
| /// <param name="temporalContext">Temporal operation context for client, info, and logging.</param> | ||
| /// <param name="workflow">Workflow type name.</param> | ||
| /// <param name="args">Workflow arguments.</param> | ||
| /// <param name="options">Workflow start options. ID and TaskQueue are required.</param> | ||
| /// <returns>Workflow-run handle for the started workflow.</returns> | ||
| internal static async Task<NexusWorkflowRunHandle> StartWorkflowAsync( | ||
| OperationStartContext nexusStartContext, | ||
| NexusOperationExecutionContext temporalContext, | ||
| string workflow, | ||
| IReadOnlyCollection<object?> args, | ||
| WorkflowOptions options) | ||
| { | ||
| var client = temporalContext.TemporalClient; | ||
| var namespace_ = client.Options.Namespace; | ||
| var workflowId = options.Id ?? string.Empty; | ||
|
|
||
| // Generate the handle and token before starting the workflow (token is needed for the | ||
| // callback header). | ||
| var handle = new NexusWorkflowRunHandle(namespace_, workflowId, 0); | ||
| var token = handle.ToToken(); | ||
|
|
||
| // Shallow clone the options so we can mutate them. We just overwrite any of these | ||
| // internal options since they cannot be user set at this time. | ||
| options = (WorkflowOptions)options.Clone(); | ||
| options.TaskQueue ??= temporalContext.Info.TaskQueue; | ||
| if (options.IdConflictPolicy == WorkflowIdConflictPolicy.UseExisting) | ||
| { | ||
| options.OnConflictOptions = new() | ||
| { | ||
| AttachLinks = true, | ||
| AttachCompletionCallbacks = true, | ||
| AttachRequestId = true, | ||
| }; | ||
| } | ||
| if (nexusStartContext.InboundLinks.Count > 0) | ||
| { | ||
| options.Links = nexusStartContext.InboundLinks.Select(link => | ||
| { | ||
| try | ||
| { | ||
| return new Link { WorkflowEvent = link.ToWorkflowEvent() }; | ||
| } | ||
| catch (ArgumentException e) | ||
| { | ||
| temporalContext.Logger.LogWarning(e, "Invalid Nexus link: {Url}", link.Uri); | ||
| return null; | ||
| } | ||
| }).OfType<Link>().ToList(); | ||
| } | ||
| if (nexusStartContext.CallbackUrl is { } callbackUrl) | ||
| { | ||
| var callback = new Callback() { Nexus = new() { Url = callbackUrl } }; | ||
| var callbackHeadersHasToken = false; | ||
| if (nexusStartContext.CallbackHeaders is { } callbackHeaders) | ||
| { | ||
| foreach (var kv in callbackHeaders) | ||
| { | ||
| callback.Nexus.Header.Add(kv.Key, kv.Value); | ||
| if (string.Equals( | ||
| kv.Key, NexusOperationTokenHeader, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| callbackHeadersHasToken = true; | ||
| } | ||
| } | ||
| } | ||
| // Set operation token if not already present (header is case-insensitive) | ||
| if (!callbackHeadersHasToken) | ||
| { | ||
| callback.Nexus.Header[NexusOperationTokenHeader] = token; | ||
| } | ||
| if (options.Links is { } links) | ||
| { | ||
| callback.Links.AddRange(links); | ||
| } | ||
| options.CompletionCallbacks = new[] { callback }; | ||
| } | ||
| options.RequestId = nexusStartContext.RequestId; | ||
|
|
||
| // Do the start call | ||
| var wfHandle = await client.StartWorkflowAsync( | ||
| workflow, args, options).ConfigureAwait(false); | ||
|
|
||
| // Add the outbound link | ||
| nexusStartContext.OutboundLinks.Add(new Link.Types.WorkflowEvent | ||
| { | ||
| Namespace = namespace_, | ||
| WorkflowId = workflowId, | ||
| RunId = wfHandle.FirstExecutionRunId ?? | ||
| throw new InvalidOperationException("Handle unexpectedly missing run ID"), | ||
| EventRef = new() { EventId = 1, EventType = EventType.WorkflowExecutionStarted }, | ||
| }.ToNexusLink()); | ||
|
|
||
| return handle; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq.Expressions; | ||
| using System.Threading.Tasks; | ||
| using NexusRpc.Handlers; | ||
| using Temporalio.Client; | ||
|
|
||
| namespace Temporalio.Nexus | ||
| { | ||
| /// <summary> | ||
| /// Nexus-aware client wrapping the Temporal client. Provides methods for starting workflows | ||
| /// from within Nexus operation handlers, handling all Nexus plumbing (links, callbacks, token | ||
| /// generation) internally. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// WARNING: Nexus support is experimental. | ||
| /// This client is created by <see cref="TemporalOperationHandler"/> and passed to the | ||
| /// user's start function. It should not be instantiated directly. | ||
| /// </remarks> | ||
| public class TemporalNexusClient : ITemporalNexusClient | ||
| { | ||
| private readonly OperationStartContext nexusStartContext; | ||
| private readonly NexusOperationExecutionContext temporalContext; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="TemporalNexusClient"/> class. | ||
| /// </summary> | ||
| /// <param name="nexusStartContext">Nexus start context for callbacks and links.</param> | ||
| /// <param name="temporalContext">Temporal operation context. </param> | ||
| internal TemporalNexusClient( | ||
| OperationStartContext nexusStartContext, | ||
| NexusOperationExecutionContext temporalContext) | ||
| { | ||
| this.nexusStartContext = nexusStartContext; | ||
| this.temporalContext = temporalContext; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ITemporalClient TemporalClient => temporalContext.TemporalClient; | ||
|
|
||
| /// <inheritdoc/> | ||
| public Task<TemporalOperationResult<TResult>> StartWorkflowAsync<TWorkflow, TResult>( | ||
| Expression<Func<TWorkflow, Task<TResult>>> workflowRunCall, WorkflowOptions options) | ||
| { | ||
| var (runMethod, args) = Common.ExpressionUtil.ExtractCall(workflowRunCall); | ||
| return StartWorkflowAsync<TResult>( | ||
| Workflows.WorkflowDefinition.NameFromRunMethodForCall(runMethod), | ||
| args, | ||
| options); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public async Task<TemporalOperationResult<NoValue>> StartWorkflowAsync<TWorkflow>( | ||
| Expression<Func<TWorkflow, Task>> workflowRunCall, WorkflowOptions options) | ||
| { | ||
| var (runMethod, args) = Common.ExpressionUtil.ExtractCall(workflowRunCall); | ||
| var handle = await NexusWorkflowStartHelper.StartWorkflowAsync( | ||
| nexusStartContext, | ||
| temporalContext, | ||
| Workflows.WorkflowDefinition.NameFromRunMethodForCall(runMethod), | ||
| args, | ||
| options).ConfigureAwait(false); | ||
| return TemporalOperationResult<NoValue>.AsyncResult(handle.ToToken()); | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public async Task<TemporalOperationResult<TResult>> StartWorkflowAsync<TResult>( | ||
| string workflow, IReadOnlyCollection<object?> args, WorkflowOptions options) | ||
| { | ||
| var handle = await NexusWorkflowStartHelper.StartWorkflowAsync( | ||
| nexusStartContext, | ||
| temporalContext, | ||
| workflow, | ||
| args, | ||
| options).ConfigureAwait(false); | ||
| return TemporalOperationResult<TResult>.AsyncResult(handle.ToToken()); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.