Skip to content
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
91 changes: 50 additions & 41 deletions src/NexusMessaging/CallerPattern/Handler/NexusGreetingService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace TemporalioSamples.NexusMessaging.CallerPattern.Handler;

using NexusRpc.Handlers;
using Temporalio.Client;
using Temporalio.Nexus;
using TemporalioSamples.NexusMessaging.CallerPattern;
using TemporalioSamples.NexusMessaging.Common;
Expand All @@ -11,55 +12,63 @@ namespace TemporalioSamples.NexusMessaging.CallerPattern.Handler;
[NexusServiceHandler(typeof(INexusGreetingService))]
public class NexusGreetingService
{
// OperationHandler.Sync means the result is returned inline to the Nexus caller
// (as opposed to WorkflowRunOperationHandler, which returns an async operation token).
// The lambda may still be async internally.
#pragma warning disable VSTHRD200 // Names must match the INexusGreetingService operations, which can't take the Async suffix

// Query: read-only, no state mutation — uses workflow query
[NexusOperationHandler]
public IOperationHandler<INexusGreetingService.GetLanguagesInput, INexusGreetingService.GetLanguagesOutput> GetLanguages() =>
OperationHandler.Sync<INexusGreetingService.GetLanguagesInput, INexusGreetingService.GetLanguagesOutput>(
async (ctx, input) =>
{
// Access the Temporal client from the Nexus operation context
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(WorkflowIdForUser(input.UserId));
return await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported));
});
[TemporalOperation]
public async Task<TemporalOperationResult<INexusGreetingService.GetLanguagesOutput>> GetLanguages(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusGreetingService.GetLanguagesInput input)
{
// Access the Temporal client from the Nexus client passed to the handler
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
WorkflowIdForUser(input.UserId));
return TemporalOperationResult<INexusGreetingService.GetLanguagesOutput>.SyncResult(
await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported)));
}

// Query: read-only — returns the workflow's current language
[NexusOperationHandler]
public IOperationHandler<INexusGreetingService.GetLanguageInput, Language> GetLanguage() =>
OperationHandler.Sync<INexusGreetingService.GetLanguageInput, Language>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(WorkflowIdForUser(input.UserId));
return await handle.QueryAsync(wf => wf.QueryLanguage());
});
[TemporalOperation]
public async Task<TemporalOperationResult<Language>> GetLanguage(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusGreetingService.GetLanguageInput input)
{
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
WorkflowIdForUser(input.UserId));
return TemporalOperationResult<Language>.SyncResult(
await handle.QueryAsync(wf => wf.QueryLanguage()));
}

// Update: mutates state and returns the previous value — uses workflow update
[NexusOperationHandler]
public IOperationHandler<INexusGreetingService.SetLanguageInput, Language> SetLanguage() =>
OperationHandler.Sync<INexusGreetingService.SetLanguageInput, Language>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(WorkflowIdForUser(input.UserId));
return await handle.ExecuteUpdateAsync(wf => wf.SetLanguageAsync(input.Language));
});
[TemporalOperation]
public Task<TemporalOperationResult<Language>> SetLanguage(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusGreetingService.SetLanguageInput input) =>
client.StartWorkflowUpdateAsync<GreetingWorkflow, Language>(
WorkflowIdForUser(input.UserId),
wf => wf.SetLanguageAsync(input.Language),
// An Update-backed Operation must wait for the Accepted stage. Any other stage is
// rejected with "nexus op workflow updates only support WorkflowUpdateStageAccepted
// for async updates".
new(WorkflowUpdateStage.Accepted));

// Signal: fire-and-forget, no return value needed — uses workflow signal
[NexusOperationHandler]
public IOperationHandler<INexusGreetingService.ApproveInput, NoValue> Approve() =>
OperationHandler.Sync<INexusGreetingService.ApproveInput, NoValue>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(WorkflowIdForUser(input.UserId));
await handle.SignalAsync(wf => wf.ApproveAsync(input.Name));
return default;
});
[TemporalOperation]
public async Task<TemporalOperationResult<NoValue>> Approve(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusGreetingService.ApproveInput input)
{
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
WorkflowIdForUser(input.UserId));
await handle.SignalAsync(wf => wf.ApproveAsync(input.Name));
return TemporalOperationResult<NoValue>.SyncResult(default);
}

#pragma warning restore VSTHRD200

private static string WorkflowIdForUser(string userId) => $"GreetingWorkflow_for_{userId}";
}
28 changes: 21 additions & 7 deletions src/NexusMessaging/CallerPattern/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ The caller Workflow:

### Running

Start a Temporal server:
This sample requires a Temporal dev server build that supports Workflow Update callbacks. Download
the compatible binary from the [Temporal CLI pre-release instructions](https://docs.temporal.io/standalone-nexus-operation#temporal-cli-support).

Start the Temporal dev server with the required namespaces pre-created and Workflow Update
callbacks enabled:

```bash
temporal server start-dev
./temporal server start-dev \
--dynamic-config-value history.enableUpdateCallbacks=true \
--dynamic-config-value history.enableCHASMSignalBacklinks=true \
--namespace nexus-messaging-handler-namespace \
--namespace nexus-messaging-caller-namespace
```

Create the namespaces and Nexus endpoint:
Create the Nexus endpoint:

```bash
temporal operator namespace create --namespace nexus-messaging-handler-namespace
temporal operator namespace create --namespace nexus-messaging-caller-namespace

temporal operator nexus endpoint create \
./temporal operator nexus endpoint create \
--name nexus-messaging-caller-pattern-endpoint \
--target-namespace nexus-messaging-handler-namespace \
--target-task-queue nexus-messaging-handler-sample
Expand All @@ -49,3 +54,12 @@ In a third terminal, run the following command to start the example:
```bash
dotnet run --project src/NexusMessaging -- caller-workflow
```

Expected output:

```
Supported languages: Chinese, English
Current language: English
Set language from English to Chinese
Approved workflow
```
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ public async Task<string[]> RunAsync()
var userIdOne = "user-one";
var userIdTwo = "user-two";

// Attach information before the Workflow exists. Since AttachApprovalContext is backed
// by Signal-with-Start on the handler, this call creates the Workflow and delivers the
// note to it.
await client.ExecuteNexusOperationAsync(
svc => svc.AttachApprovalContext(new INexusRemoteGreetingService.AttachApprovalContextInput(
"queued for localization review by the nightly batch", userIdOne)));
log.Add($"Attached approval context before the workflow existed: {userIdOne}");

// The Workflow for this user is already running due to AttachApprovalContext. The handler
// sets the conflict policy to UseExisting, so this call attaches the Operation's
// completion callback to the running execution.
var handleOne = await client.StartNexusOperationAsync(
svc => svc.RunFromRemote(new INexusRemoteGreetingService.RunFromRemoteInput(userIdOne)));
log.Add($"Started remote workflow for user: {userIdOne}");
Expand All @@ -26,6 +37,13 @@ public async Task<string[]> RunAsync()
svc => svc.RunFromRemote(new INexusRemoteGreetingService.RunFromRemoteInput(userIdTwo)));
log.Add($"Started remote workflow for user: {userIdTwo}");

// This user's Workflow was created by RunFromRemote just above, so here Signal-with-Start
// skips the start and only delivers the Signal.
await client.ExecuteNexusOperationAsync(
svc => svc.AttachApprovalContext(new INexusRemoteGreetingService.AttachApprovalContextInput(
"translation approved by the localization team", userIdTwo)));
log.Add($"Attached approval context to the running workflow: {userIdTwo}");

// Interact with workflow one: get languages, set language, approve
var languagesOne = await client.ExecuteNexusOperationAsync(
svc => svc.GetLanguages(new INexusRemoteGreetingService.GetLanguagesInput(false, userIdOne)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class GreetingWorkflow
private Language currentLanguage = Language.English;
private bool approved;
private string approvedBy = string.Empty;
private string? approvalContext;

[WorkflowRun]
public async Task<string> RunAsync(string userId)
Expand Down Expand Up @@ -79,4 +80,11 @@ public Task ApproveAsync(string name)
approvedBy = name;
return Task.CompletedTask;
}

[WorkflowSignal]
public Task AttachApprovalContextAsync(string note)
{
approvalContext = note;
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace TemporalioSamples.NexusMessaging.OnDemandPattern.Handler;

using NexusRpc.Handlers;
using Temporalio.Api.Enums.V1;
using Temporalio.Client;
using Temporalio.Nexus;
using TemporalioSamples.NexusMessaging.Common;
using TemporalioSamples.NexusMessaging.OnDemandPattern;
Expand All @@ -11,66 +13,100 @@ namespace TemporalioSamples.NexusMessaging.OnDemandPattern.Handler;
[NexusServiceHandler(typeof(INexusRemoteGreetingService))]
public class NexusRemoteGreetingService
{
// WorkflowRunOperationHandler starts a backing workflow and returns its handle to the
// Nexus infrastructure. The caller receives an async operation token and can poll for
// the workflow result later via GetResultAsync.
[NexusOperationHandler]
public IOperationHandler<INexusRemoteGreetingService.RunFromRemoteInput, string> RunFromRemote() =>
WorkflowRunOperationHandler.FromHandleFactory(
(WorkflowRunOperationContext context, INexusRemoteGreetingService.RunFromRemoteInput input) =>
context.StartWorkflowAsync(
(GreetingWorkflow wf) => wf.RunAsync(input.UserId),
new() { Id = GetWorkflowId(input.UserId) }));
#pragma warning disable VSTHRD200 // Names must match the INexusRemoteGreetingService operations, which can't take the Async suffix

// OperationHandler.Sync means the result is returned inline to the Nexus caller
// (as opposed to WorkflowRunOperationHandler, which returns an async operation token).
// The lambda may still be async internally.

// Query: read-only, no state mutation — uses workflow query
[NexusOperationHandler]
public IOperationHandler<INexusRemoteGreetingService.GetLanguagesInput, INexusRemoteGreetingService.GetLanguagesOutput> GetLanguages() =>
OperationHandler.Sync<INexusRemoteGreetingService.GetLanguagesInput, INexusRemoteGreetingService.GetLanguagesOutput>(
async (ctx, input) =>
// Starts the GreetingWorkflow for the given user, or attaches to one already running.
// StartWorkflowAsync returns an async result, which attaches a completion callback,
// so the Operation completes when the Workflow returns.
[TemporalOperation]
public Task<TemporalOperationResult<string>> RunFromRemote(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.RunFromRemoteInput input) =>
client.StartWorkflowAsync(
(GreetingWorkflow wf) => wf.RunAsync(input.UserId),
new()
{
// Access the Temporal client from the Nexus operation context
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(GetWorkflowId(input.UserId));
return await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported));
Id = GetWorkflowId(input.UserId),

// By default, starting a Workflow whose ID is already running fails the Operation.
// Since AttachApprovalContext below can create the GreetingWorkflow first, this
// Operation needs to attach to the running execution rather than fail.
IdConflictPolicy = WorkflowIdConflictPolicy.UseExisting,
});

// Query: read-only, no state mutation — uses workflow query
[TemporalOperation]
public async Task<TemporalOperationResult<INexusRemoteGreetingService.GetLanguagesOutput>> GetLanguages(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.GetLanguagesInput input)
{
// Access the Temporal client from the Nexus client passed to the handler
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
GetWorkflowId(input.UserId));
return TemporalOperationResult<INexusRemoteGreetingService.GetLanguagesOutput>.SyncResult(
await handle.QueryAsync(wf => wf.QueryLanguages(input.IncludeUnsupported)));
}

// Query: read-only — returns the workflow's current language
[NexusOperationHandler]
public IOperationHandler<INexusRemoteGreetingService.GetLanguageInput, Language> GetLanguage() =>
OperationHandler.Sync<INexusRemoteGreetingService.GetLanguageInput, Language>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(GetWorkflowId(input.UserId));
return await handle.QueryAsync(wf => wf.QueryLanguage());
});
[TemporalOperation]
public async Task<TemporalOperationResult<Language>> GetLanguage(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.GetLanguageInput input)
{
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
GetWorkflowId(input.UserId));
return TemporalOperationResult<Language>.SyncResult(
await handle.QueryAsync(wf => wf.QueryLanguage()));
}

// Update: mutates state and returns the previous value — uses workflow update
[NexusOperationHandler]
public IOperationHandler<INexusRemoteGreetingService.SetLanguageInput, Language> SetLanguage() =>
OperationHandler.Sync<INexusRemoteGreetingService.SetLanguageInput, Language>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(GetWorkflowId(input.UserId));
return await handle.ExecuteUpdateAsync(wf => wf.SetLanguageAsync(input.Language));
});
[TemporalOperation]
public Task<TemporalOperationResult<Language>> SetLanguage(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.SetLanguageInput input) =>
client.StartWorkflowUpdateAsync<GreetingWorkflow, Language>(
GetWorkflowId(input.UserId),
wf => wf.SetLanguageAsync(input.Language),
// An Update-backed Operation must wait for the Accepted stage. Any other stage is
// rejected with "nexus op workflow updates only support WorkflowUpdateStageAccepted
// for async updates".
new(WorkflowUpdateStage.Accepted));

// Signal: fire-and-forget, no return value needed — uses workflow signal
[NexusOperationHandler]
public IOperationHandler<INexusRemoteGreetingService.ApproveInput, NoValue> Approve() =>
OperationHandler.Sync<INexusRemoteGreetingService.ApproveInput, NoValue>(
async (ctx, input) =>
{
var client = NexusOperationExecutionContext.Current.TemporalClient;
var handle = client.GetWorkflowHandle<GreetingWorkflow>(GetWorkflowId(input.UserId));
await handle.SignalAsync(wf => wf.ApproveAsync(input.Name));
return default;
});
[TemporalOperation]
public async Task<TemporalOperationResult<NoValue>> Approve(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.ApproveInput input)
{
var handle = client.TemporalClient.GetWorkflowHandle<GreetingWorkflow>(
GetWorkflowId(input.UserId));
await handle.SignalAsync(wf => wf.ApproveAsync(input.Name));
return TemporalOperationResult<NoValue>.SyncResult(default);
}

// Signal-with-Start: starts the Workflow first if it is not already running.
// When the Workflow already exists, only the Signal is delivered.
[TemporalOperation]
public async Task<TemporalOperationResult<NoValue>> AttachApprovalContext(
TemporalOperationStartContext ctx,
ITemporalNexusClient client,
INexusRemoteGreetingService.AttachApprovalContextInput input)
{
var options = new WorkflowOptions(
id: GetWorkflowId(input.UserId),
taskQueue: NexusOperationExecutionContext.Current.Info.TaskQueue);
options.SignalWithStart((GreetingWorkflow wf) => wf.AttachApprovalContextAsync(input.Note));
await client.TemporalClient.StartWorkflowAsync(
(GreetingWorkflow wf) => wf.RunAsync(input.UserId), options);
return TemporalOperationResult<NoValue>.SyncResult(default);
}

#pragma warning restore VSTHRD200

private static string GetWorkflowId(string userId) => $"GreetingWorkflow_for_{userId}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public interface INexusRemoteGreetingService
[NexusOperation]
void Approve(ApproveInput input);

[NexusOperation]
void AttachApprovalContext(AttachApprovalContextInput input);

public record RunFromRemoteInput(string UserId);

public record GetLanguagesInput(bool IncludeUnsupported, string UserId);
Expand All @@ -32,4 +35,6 @@ public record GetLanguageInput(string UserId);
public record SetLanguageInput(Language Language, string UserId);

public record ApproveInput(string Name, string UserId);

public record AttachApprovalContextInput(string Note, string UserId);
}
Loading
Loading