From 494a759906a9b6c6b80275c634a75f216a98898a Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Mon, 15 Jun 2026 13:47:34 -0700 Subject: [PATCH 01/13] Add system Nexus signal-with-start workflow API --- .../Program.cs | 634 ++++++++++++++++++ .../Temporalio.SystemNexus.Generator.csproj | 23 + .../dotnet/TemporalSupport.cs | 279 ++++++++ .../wit/deps/nexus-temporal-types/model.wit | 162 +++++ .../SystemNexus/wit/workflow-service.wit | 123 ++++ src/Temporalio/Temporalio.csproj | 46 ++ .../Worker/IWorkflowCodecHelperInstance.cs | 16 +- src/Temporalio/Worker/WorkflowCodecHelper.cs | 40 +- src/Temporalio/Worker/WorkflowInstance.cs | 20 +- src/Temporalio/Workflows/Workflow.cs | 2 +- .../Worker/WorkflowWorkerTests.cs | 83 +++ tests/Temporalio.Tests/WorkflowEnvironment.cs | 4 +- 12 files changed, 1419 insertions(+), 13 deletions(-) create mode 100644 src/Temporalio.SystemNexus.Generator/Program.cs create mode 100644 src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj create mode 100644 src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs create mode 100644 src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit create mode 100644 src/Temporalio/SystemNexus/wit/workflow-service.wit diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs new file mode 100644 index 00000000..f4c726b0 --- /dev/null +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -0,0 +1,634 @@ +using System.Text; +using System.Text.RegularExpressions; +using Google.Protobuf.Reflection; + +if (args.Length != 2) +{ + Console.Error.WriteLine( + "Usage: Temporalio.SystemNexus.Generator "); + return 2; +} + +var operationsPath = args[0]; +var descriptorPath = args[1]; + +TransformOperationsFile(operationsPath); +GeneratePayloadVisitorRegistry(operationsPath, descriptorPath); +return 0; + +static void TransformOperationsFile(string operationsPath) +{ + var source = File.ReadAllText(operationsPath); + var namespaceMatch = Regex.Match( + source, + @"namespace\s+(?[A-Za-z0-9_.]+)\s*\{", + RegexOptions.Multiline); + if (!namespaceMatch.Success) + { + throw new InvalidOperationException($"No generated namespace found in {operationsPath}"); + } + + var generatedNamespace = namespaceMatch.Groups["namespace"].Value; + source = EnsureUsing(source, generatedNamespace); + source = Regex.Replace( + source, + @"namespace\s+" + Regex.Escape(generatedNamespace) + @"\s*\{", + "namespace Temporalio.Workflows\n{", + RegexOptions.Multiline, + TimeSpan.FromSeconds(5)); + source = Regex.Replace( + source, + @"public\s+static\s+class\s+\w+Operations\b", + "public static partial class Workflow", + RegexOptions.Multiline, + TimeSpan.FromSeconds(5)); + + if (!source.Contains("public static partial class Workflow", StringComparison.Ordinal)) + { + throw new InvalidOperationException($"No generated operations class found in {operationsPath}"); + } + + File.WriteAllText(operationsPath, source); +} + +static void GeneratePayloadVisitorRegistry(string operationsPath, string descriptorPath) +{ + var generatedDir = Path.GetDirectoryName(operationsPath) ?? + throw new InvalidOperationException($"No directory for {operationsPath}"); + var servicePath = Path.Combine(generatedDir, "Service.cs"); + var source = File.ReadAllText(servicePath); + var namespaceMatch = Regex.Match( + source, + @"namespace\s+(?[A-Za-z0-9_.]+)\s*\{", + RegexOptions.Multiline); + if (!namespaceMatch.Success) + { + throw new InvalidOperationException($"No generated namespace found in {servicePath}"); + } + + var generatedNamespace = namespaceMatch.Groups["namespace"].Value; + var operations = ParseOperations(source); + if (operations.Count == 0) + { + throw new InvalidOperationException($"No generated operations found in {servicePath}"); + } + + var messages = LoadMessages(descriptorPath); + var messagesByCsharpType = messages.Values.ToDictionary( + message => NormalizeTypeName(message.CsharpType), + message => message); + var operationMessages = operations.Select(operation => + new OperationMessages( + operation, + GetMessage(messagesByCsharpType, operation.InputType), + GetMessage(messagesByCsharpType, operation.OutputType))).ToList(); + var containsPayloadMemo = new Dictionary(); + var emittedMethods = new HashSet(); + var builder = new StringBuilder(); + + builder.AppendLine("// "); + builder.AppendLine("// Generated by Temporalio.SystemNexus.Generator. DO NOT EDIT!"); + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + builder.AppendLine("using System;"); + builder.AppendLine("using System.Threading.Tasks;"); + builder.AppendLine("using Google.Protobuf;"); + builder.AppendLine("using Google.Protobuf.Collections;"); + builder.AppendLine("using Temporalio.Api.Common.V1;"); + builder.AppendLine("using Temporalio.Converters;"); + builder.AppendLine($"using {generatedNamespace};"); + builder.AppendLine(); + builder.AppendLine("namespace Temporalio.Worker"); + builder.AppendLine("{"); + builder.AppendLine(" internal static class SystemNexusPayloadVisitorRegistry"); + builder.AppendLine(" {"); + builder.AppendLine(" internal delegate Task PayloadVisitor(Payload payload);"); + builder.AppendLine(" internal delegate Task PayloadsVisitor(RepeatedField payloads);"); + builder.AppendLine(" internal delegate Task EnvelopeVisitor(Payload payload);"); + builder.AppendLine(); + builder.AppendLine(" private static readonly BinaryProtoConverter ProtoPayloadConverter = new();"); + builder.AppendLine(); + builder.AppendLine(" internal static bool TryToInputPayload("); + builder.AppendLine(" string service,"); + builder.AppendLine(" string operation,"); + builder.AppendLine(" object? value,"); + builder.AppendLine(" out Payload payload)"); + builder.AppendLine(" {"); + builder.AppendLine(" payload = null!;"); + builder.AppendLine(" if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation)))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + foreach (var operation in operationMessages) + { + var serviceLiteral = CsharpLiteral(operation.Operation.Service); + var operationLiteral = CsharpLiteral(operation.Operation.Operation); + builder.AppendLine( + $" if (service == {serviceLiteral} && operation == {operationLiteral})"); + builder.AppendLine(" {"); + builder.AppendLine($" if (value is not {operation.Input.CsharpType} typedValue)"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" ProtoPayloadConverter.TryToPayload(typedValue, out var converted);"); + builder.AppendLine(" payload = converted!;"); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" internal static Task TryVisitInputAsync("); + builder.AppendLine(" string service,"); + builder.AppendLine(" string operation,"); + builder.AppendLine(" Payload payload,"); + builder.AppendLine(" PayloadVisitor visitPayload,"); + builder.AppendLine(" PayloadsVisitor visitPayloads,"); + builder.AppendLine(" EnvelopeVisitor? visitEnvelope = null) =>"); + builder.AppendLine(" TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope);"); + builder.AppendLine(); + builder.AppendLine(" internal static Task TryVisitOutputAsync("); + builder.AppendLine(" string service,"); + builder.AppendLine(" string operation,"); + builder.AppendLine(" Payload payload,"); + builder.AppendLine(" PayloadVisitor visitPayload,"); + builder.AppendLine(" PayloadsVisitor visitPayloads,"); + builder.AppendLine(" EnvelopeVisitor? visitEnvelope = null) =>"); + builder.AppendLine(" TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope);"); + builder.AppendLine(); + builder.AppendLine(" private static async Task TryVisitAsync("); + builder.AppendLine(" string service,"); + builder.AppendLine(" string operation,"); + builder.AppendLine(" bool input,"); + builder.AppendLine(" Payload payload,"); + builder.AppendLine(" PayloadVisitor visitPayload,"); + builder.AppendLine(" PayloadsVisitor visitPayloads,"); + builder.AppendLine(" EnvelopeVisitor? visitEnvelope)"); + builder.AppendLine(" {"); + builder.AppendLine(" if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation)))"); + builder.AppendLine(" {"); + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + + foreach (var operation in operationMessages) + { + var serviceLiteral = CsharpLiteral(operation.Operation.Service); + var operationLiteral = CsharpLiteral(operation.Operation.Operation); + builder.AppendLine( + $" if (service == {serviceLiteral} && operation == {operationLiteral})"); + builder.AppendLine(" {"); + builder.AppendLine(" if (input)"); + builder.AppendLine(" {"); + builder.AppendLine( + $" await VisitEnvelopeAsync<{operation.Input.CsharpType}>(payload, {VisitMethodName(operation.Input)}, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false);"); + builder.AppendLine(" }"); + builder.AppendLine(" else"); + builder.AppendLine(" {"); + builder.AppendLine( + $" await VisitEnvelopeAsync<{operation.Output.CsharpType}>(payload, {VisitMethodName(operation.Output)}, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false);"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" return true;"); + builder.AppendLine(" }"); + builder.AppendLine(); + } + + builder.AppendLine(" return false;"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" private static async Task VisitEnvelopeAsync("); + builder.AppendLine(" Payload payload,"); + builder.AppendLine(" Func visitMessage,"); + builder.AppendLine(" PayloadVisitor visitPayload,"); + builder.AppendLine(" PayloadsVisitor visitPayloads,"); + builder.AppendLine(" EnvelopeVisitor? visitEnvelope)"); + builder.AppendLine(" where T : IMessage, new()"); + builder.AppendLine(" {"); + builder.AppendLine(" BinaryProtoConverter.AssertProtoPayload(payload, typeof(T));"); + builder.AppendLine(" var message = new T();"); + builder.AppendLine(" message.MergeFrom(payload.Data);"); + builder.AppendLine(" await visitMessage(message, visitPayload, visitPayloads).ConfigureAwait(false);"); + builder.AppendLine(" payload.Metadata.Clear();"); + builder.AppendLine(" payload.Metadata[\"encoding\"] = ByteString.CopyFromUtf8(\"binary/protobuf\");"); + builder.AppendLine(" payload.Metadata[\"messageType\"] = ByteString.CopyFromUtf8(message.Descriptor.FullName);"); + builder.AppendLine(" payload.Data = message.ToByteString();"); + builder.AppendLine(" if (visitEnvelope != null)"); + builder.AppendLine(" {"); + builder.AppendLine(" await visitEnvelope(payload).ConfigureAwait(false);"); + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine(); + + foreach (var operation in operationMessages) + { + EmitVisitMethod(builder, operation.Input, messages, containsPayloadMemo, emittedMethods); + EmitVisitMethod(builder, operation.Output, messages, containsPayloadMemo, emittedMethods); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + + File.WriteAllText(Path.Combine(generatedDir, "SystemNexusPayloadVisitorRegistry.cs"), builder.ToString()); +} + +static List ParseOperations(string serviceSource) +{ + var operations = new List(); + var serviceMatches = Regex.Matches( + serviceSource, + @"\[NexusService\(""(?[^""]+)""\)\]\s+internal\s+interface\s+\w+\s*\{(?.*?)^\s*\}", + RegexOptions.Multiline | RegexOptions.Singleline, + TimeSpan.FromSeconds(5)); + foreach (Match serviceMatch in serviceMatches) + { + var service = serviceMatch.Groups["service"].Value; + var operationMatches = Regex.Matches( + serviceMatch.Groups["body"].Value, + @"\[NexusOperation\(""(?[^""]+)""\)\]\s+(?[A-Za-z0-9_.:]+)\s+\w+\((?[A-Za-z0-9_.:]+)\s+\w+\);", + RegexOptions.Multiline, + TimeSpan.FromSeconds(5)); + foreach (Match operationMatch in operationMatches) + { + operations.Add(new OperationInfo( + service, + operationMatch.Groups["operation"].Value, + NormalizeTypeName(operationMatch.Groups["input"].Value), + NormalizeTypeName(operationMatch.Groups["output"].Value))); + } + } + + return operations; +} + +static IReadOnlyDictionary LoadMessages(string descriptorPath) +{ + var descriptorSet = FileDescriptorSet.Parser.ParseFrom(File.ReadAllBytes(descriptorPath)); + var messages = new Dictionary(); + foreach (var file in descriptorSet.File) + { + var csharpNamespace = file.Options?.CsharpNamespace; + if (string.IsNullOrEmpty(csharpNamespace)) + { + continue; + } + + foreach (var message in file.MessageType) + { + AddMessage(messages, file.Package, $"global::{csharpNamespace}", message); + } + } + + return messages; +} + +static void AddMessage( + Dictionary messages, + string protoPrefix, + string csharpPrefix, + DescriptorProto descriptor) +{ + var fullName = string.IsNullOrEmpty(protoPrefix) ? + descriptor.Name : + $"{protoPrefix}.{descriptor.Name}"; + var csharpType = $"{csharpPrefix}.{descriptor.Name}"; + messages[fullName] = new MessageInfo(fullName, csharpType, descriptor); + foreach (var nested in descriptor.NestedType) + { + AddMessage(messages, fullName, $"{csharpType}.Types", nested); + } +} + +static MessageInfo GetMessage( + IReadOnlyDictionary messagesByCsharpType, + string csharpType) +{ + if (!messagesByCsharpType.TryGetValue(NormalizeTypeName(csharpType), out var message)) + { + throw new InvalidOperationException($"No protobuf message descriptor found for {csharpType}"); + } + + return message; +} + +static void EmitVisitMethod( + StringBuilder builder, + MessageInfo message, + IReadOnlyDictionary messages, + Dictionary containsPayloadMemo, + HashSet emittedMethods) +{ + if (!emittedMethods.Add(message.FullName)) + { + return; + } + + foreach (var referenced in ReferencedMessagesWithPayloads(message, messages, containsPayloadMemo)) + { + EmitVisitMethod(builder, referenced, messages, containsPayloadMemo, emittedMethods); + } + + builder.AppendLine($" private static async Task {VisitMethodName(message)}("); + builder.AppendLine($" {message.CsharpType} value,"); + builder.AppendLine(" PayloadVisitor visitPayload,"); + builder.AppendLine(" PayloadsVisitor visitPayloads)"); + builder.AppendLine(" {"); + foreach (var line in VisitFieldLines(message, messages, containsPayloadMemo)) + { + builder.AppendLine($" {line}"); + } + + builder.AppendLine(" await Task.CompletedTask.ConfigureAwait(false);"); + builder.AppendLine(" }"); + builder.AppendLine(); +} + +static IEnumerable ReferencedMessagesWithPayloads( + MessageInfo message, + IReadOnlyDictionary messages, + Dictionary containsPayloadMemo) +{ + foreach (var field in message.Descriptor.Field) + { + foreach (var referenced in FieldReferencedMessagesWithPayloads(field, messages, containsPayloadMemo)) + { + yield return referenced; + } + } +} + +static IEnumerable FieldReferencedMessagesWithPayloads( + FieldDescriptorProto field, + IReadOnlyDictionary messages, + Dictionary containsPayloadMemo) +{ + if (!TryGetMessage(field.TypeName, messages, out var fieldMessage) || + IsPayload(fieldMessage) || + IsSearchAttributes(fieldMessage)) + { + yield break; + } + + if (fieldMessage.Descriptor.Options?.MapEntry == true) + { + var valueField = fieldMessage.Descriptor.Field.FirstOrDefault(field => field.Number == 2); + if (valueField != null && + TryGetMessage(valueField.TypeName, messages, out var valueMessage) && + !IsPayload(valueMessage) && + !IsSearchAttributes(valueMessage) && + ContainsPayload(valueMessage, messages, containsPayloadMemo)) + { + yield return valueMessage; + } + + yield break; + } + + if (ContainsPayload(fieldMessage, messages, containsPayloadMemo)) + { + yield return fieldMessage; + } +} + +static IEnumerable VisitFieldLines( + MessageInfo message, + IReadOnlyDictionary messages, + Dictionary containsPayloadMemo) +{ + foreach (var field in message.Descriptor.Field) + { + if (!TryGetMessage(field.TypeName, messages, out var fieldMessage) || + IsSearchAttributes(fieldMessage)) + { + continue; + } + + var propertyName = PropertyName(field.Name, message.Descriptor.Name); + if (fieldMessage.Descriptor.Options?.MapEntry == true) + { + var valueField = fieldMessage.Descriptor.Field.FirstOrDefault(field => field.Number == 2); + if (valueField == null || + !TryGetMessage(valueField.TypeName, messages, out var valueMessage) || + IsSearchAttributes(valueMessage) || + !ContainsPayload(valueMessage, messages, containsPayloadMemo)) + { + continue; + } + + var valueName = UniqueLocalName(message, field, "item"); + yield return $"foreach (var {valueName} in value.{propertyName}.Values)"; + yield return "{"; + if (IsPayload(valueMessage)) + { + yield return $" if ({valueName} != null)"; + yield return " {"; + yield return $" await visitPayload({valueName}).ConfigureAwait(false);"; + yield return " }"; + } + else + { + yield return $" if ({valueName} != null)"; + yield return " {"; + yield return $" await {VisitMethodName(valueMessage)}({valueName}, visitPayload, visitPayloads).ConfigureAwait(false);"; + yield return " }"; + } + + yield return "}"; + continue; + } + + if (!ContainsPayload(fieldMessage, messages, containsPayloadMemo)) + { + continue; + } + + if (IsPayload(fieldMessage)) + { + if (field.Label == FieldDescriptorProto.Types.Label.Repeated) + { + var valueName = UniqueLocalName(message, field, "payload"); + yield return $"foreach (var {valueName} in value.{propertyName})"; + yield return "{"; + yield return $" await visitPayload({valueName}).ConfigureAwait(false);"; + yield return "}"; + } + else + { + yield return $"if (value.{propertyName} != null)"; + yield return "{"; + yield return $" await visitPayload(value.{propertyName}).ConfigureAwait(false);"; + yield return "}"; + } + } + else if (IsPayloads(fieldMessage)) + { + if (field.Label == FieldDescriptorProto.Types.Label.Repeated) + { + var valueName = UniqueLocalName(message, field, "payloads"); + yield return $"foreach (var {valueName} in value.{propertyName})"; + yield return "{"; + yield return $" await visitPayloads({valueName}.Payloads_).ConfigureAwait(false);"; + yield return "}"; + } + else + { + yield return $"if (value.{propertyName} != null)"; + yield return "{"; + yield return $" await visitPayloads(value.{propertyName}.Payloads_).ConfigureAwait(false);"; + yield return "}"; + } + } + else if (field.Label == FieldDescriptorProto.Types.Label.Repeated) + { + var valueName = UniqueLocalName(message, field, "item"); + yield return $"foreach (var {valueName} in value.{propertyName})"; + yield return "{"; + yield return $" await {VisitMethodName(fieldMessage)}({valueName}, visitPayload, visitPayloads).ConfigureAwait(false);"; + yield return "}"; + } + else + { + yield return $"if (value.{propertyName} != null)"; + yield return "{"; + yield return $" await {VisitMethodName(fieldMessage)}(value.{propertyName}, visitPayload, visitPayloads).ConfigureAwait(false);"; + yield return "}"; + } + } +} + +static bool ContainsPayload( + MessageInfo message, + IReadOnlyDictionary messages, + Dictionary memo) => + ContainsPayloadCore(message, messages, memo, new HashSet()); + +static bool ContainsPayloadCore( + MessageInfo message, + IReadOnlyDictionary messages, + Dictionary memo, + HashSet visiting) +{ + if (IsSearchAttributes(message)) + { + return false; + } + + if (IsPayload(message) || IsPayloads(message)) + { + return true; + } + + if (memo.TryGetValue(message.FullName, out var result)) + { + return result; + } + + if (!visiting.Add(message.FullName)) + { + return false; + } + + result = false; + foreach (var field in message.Descriptor.Field) + { + if (!TryGetMessage(field.TypeName, messages, out var fieldMessage) || + IsSearchAttributes(fieldMessage)) + { + continue; + } + + if (fieldMessage.Descriptor.Options?.MapEntry == true) + { + var valueField = fieldMessage.Descriptor.Field.FirstOrDefault(field => field.Number == 2); + if (valueField != null && + TryGetMessage(valueField.TypeName, messages, out var valueMessage) && + ContainsPayloadCore(valueMessage, messages, memo, visiting)) + { + result = true; + break; + } + } + else if (ContainsPayloadCore(fieldMessage, messages, memo, visiting)) + { + result = true; + break; + } + } + + visiting.Remove(message.FullName); + memo[message.FullName] = result; + return result; +} + +static bool TryGetMessage( + string typeName, + IReadOnlyDictionary messages, + out MessageInfo message) => + messages.TryGetValue(typeName.TrimStart('.'), out message!); + +static bool IsPayload(MessageInfo message) => message.FullName == "temporal.api.common.v1.Payload"; + +static bool IsPayloads(MessageInfo message) => message.FullName == "temporal.api.common.v1.Payloads"; + +static bool IsSearchAttributes(MessageInfo message) => + message.FullName == "temporal.api.common.v1.SearchAttributes"; + +static string VisitMethodName(MessageInfo message) => + "Visit_" + Regex.Replace(message.FullName, @"[^A-Za-z0-9_]", "_"); + +static string PropertyName(string protoName, string messageName) +{ + var builder = new StringBuilder(); + var capitalizeNext = true; + foreach (var ch in protoName) + { + if (ch == '_') + { + capitalizeNext = true; + continue; + } + + builder.Append(capitalizeNext ? char.ToUpperInvariant(ch) : ch); + capitalizeNext = false; + } + + var propertyName = builder.ToString(); + return propertyName == messageName ? propertyName + "_" : propertyName; +} + +static string UniqueLocalName(MessageInfo message, FieldDescriptorProto field, string prefix) => + $"{prefix}_{Regex.Replace(message.FullName, @"[^A-Za-z0-9_]", "_")}_{field.Number}"; + +static string NormalizeTypeName(string typeName) => + typeName.StartsWith("global::", StringComparison.Ordinal) ? typeName["global::".Length..] : typeName; + +static string CsharpLiteral(string value) => "@\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; + +static string EnsureUsing(string source, string namespaceName) +{ + if (Regex.IsMatch(source, @"using\s+" + Regex.Escape(namespaceName) + @"\s*;")) + { + return source; + } + + var lastUsing = Regex.Matches(source, @"^using\s+[A-Za-z0-9_.]+;\s*$", RegexOptions.Multiline) + .Cast() + .LastOrDefault(); + if (lastUsing == null) + { + throw new InvalidOperationException("No using directives found in generated operations file"); + } + + return source.Insert(lastUsing.Index + lastUsing.Length, Environment.NewLine + "using " + namespaceName + ";"); +} + +internal sealed record OperationInfo(string Service, string Operation, string InputType, string OutputType); + +internal sealed record OperationMessages(OperationInfo Operation, MessageInfo Input, MessageInfo Output); + +internal sealed record MessageInfo(string FullName, string CsharpType, DescriptorProto Descriptor); diff --git a/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj new file mode 100644 index 00000000..6f5f3920 --- /dev/null +++ b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + false + false + false + enable + false + enable + + false + false + + + + + + + + + diff --git a/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs b/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs new file mode 100644 index 00000000..c30d26fa --- /dev/null +++ b/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Google.Protobuf.WellKnownTypes; +using Temporalio.Common; +using Temporalio.Converters; +using Temporalio.Workflows; +using ApiCommon = Temporalio.Api.Common.V1; +using ApiDeployment = Temporalio.Api.Deployment.V1; +using ApiTaskQueue = Temporalio.Api.TaskQueue.V1; +using ApiWorkflow = Temporalio.Api.Workflow.V1; + +namespace NexGen.Support +{ + internal static class TemporalWorkflowContext + { + internal static string WorkflowNamespace() => Workflow.Info.Namespace; + } + + internal static class TemporalFunctionNames + { + internal static string WorkflowName(MethodInfo method) + { + if (method.GetCustomAttribute() == null) + { + throw new ArgumentException($"{method} missing WorkflowRun attribute"); + } + var definition = WorkflowDefinition.Create(method.ReflectedType ?? + throw new ArgumentException($"{method} has no reflected type")); + return definition.Name ?? + throw new ArgumentException( + $"{method} cannot be used directly since it is a dynamic workflow"); + } + + internal static string SignalName(MethodInfo method) + { + var definition = WorkflowSignalDefinition.FromMethod(method); + return definition.Name ?? + throw new ArgumentException( + $"{method} cannot be used directly since it is a dynamic signal"); + } + } + + internal static class ProtoExtensions + { + internal static ApiCommon.WorkflowType ToProto(this string value, ApiCommon.WorkflowType _) => + new() { Name = value }; + + internal static ApiTaskQueue.TaskQueue ToProto(this string value, ApiTaskQueue.TaskQueue _) => + new() { Name = value }; + + internal static ApiCommon.Payload ToProto(this object? value) => + Workflow.PayloadConverter.ToPayload(value); + + internal static ApiCommon.Payloads ToProto(this IEnumerable value) => + ToPayloads(value); + + internal static Duration ToProto(this TimeSpan value) => + Duration.FromTimeSpan(value); + + internal static ApiCommon.RetryPolicy ToProto(this Temporalio.Common.RetryPolicy value) => + ToRetryPolicy(value); + + internal static ApiCommon.Memo ToProto(this IReadOnlyDictionary value) => + ToMemo(value); + + internal static ApiCommon.SearchAttributes ToProto(this SearchAttributeCollection value) => + value.ToProto(); + + internal static ApiCommon.Priority ToProto(this Temporalio.Common.Priority value) => + ToPriority(value); + + internal static ApiWorkflow.VersioningOverride ToProto(this Temporalio.Common.VersioningOverride value) => + ToVersioningOverride(value); + + internal static string FromProto(this ApiCommon.WorkflowType proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Name; + } + + internal static string FromProto(this ApiTaskQueue.TaskQueue proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Name; + } + + internal static TimeSpan FromProto(this Duration proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.ToTimeSpan(); + } + + internal static object? FromProto(this ApiCommon.Payload proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return Workflow.PayloadConverter.ToValue(proto); + } + + internal static IReadOnlyCollection FromProto(this ApiCommon.Payloads proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return PayloadsToValues(proto); + } + + internal static Temporalio.Common.RetryPolicy FromProto(this ApiCommon.RetryPolicy proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return FromRetryPolicy(proto); + } + + internal static IReadOnlyDictionary FromProto(this ApiCommon.Memo proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Fields.ToDictionary(item => item.Key, item => Workflow.PayloadConverter.ToValue(item.Value)); + } + + internal static SearchAttributeCollection FromProto(this ApiCommon.SearchAttributes proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return SearchAttributeCollection.FromProto(proto); + } + + internal static Temporalio.Common.Priority FromProto(this ApiCommon.Priority proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return new Temporalio.Common.Priority( + proto.PriorityKey == 0 ? null : proto.PriorityKey, + string.IsNullOrEmpty(proto.FairnessKey) ? null : proto.FairnessKey, + proto.FairnessWeight == 0f ? null : proto.FairnessWeight); + } + + internal static Temporalio.Common.VersioningOverride FromProto(this ApiWorkflow.VersioningOverride proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + if (proto.Pinned is { } pinned) + { + return new Temporalio.Common.VersioningOverride.Pinned( + new WorkerDeploymentVersion(pinned.Version.DeploymentName, pinned.Version.BuildId), + (Temporalio.Common.VersioningOverride.PinnedOverrideBehavior)pinned.Behavior); + } + if (proto.AutoUpgrade) + { + return new Temporalio.Common.VersioningOverride.AutoUpgrade(); + } + + throw new NotSupportedException("Unsupported versioning override proto"); + } + + private static ApiCommon.Payloads ToPayloads(IEnumerable values) + { + var payloads = new ApiCommon.Payloads(); + payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); + return payloads; + } + + private static IReadOnlyCollection PayloadsToValues(ApiCommon.Payloads payloads) => + payloads.Payloads_.Select(payload => Workflow.PayloadConverter.ToValue(payload)).ToArray(); + + private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy policy) + { + var proto = new ApiCommon.RetryPolicy + { + InitialInterval = Duration.FromTimeSpan(policy.InitialInterval), + BackoffCoefficient = policy.BackoffCoefficient, + MaximumAttempts = policy.MaximumAttempts, + }; + if (policy.MaximumInterval is { } maximumInterval) + { + proto.MaximumInterval = Duration.FromTimeSpan(maximumInterval); + } + if (policy.NonRetryableErrorTypes is { Count: > 0 } nonRetryableErrorTypes) + { + proto.NonRetryableErrorTypes.AddRange(nonRetryableErrorTypes); + } + return proto; + } + + private static Temporalio.Common.RetryPolicy FromRetryPolicy(ApiCommon.RetryPolicy proto) + { + var retryPolicy = new Temporalio.Common.RetryPolicy + { + BackoffCoefficient = (float)proto.BackoffCoefficient, + MaximumAttempts = proto.MaximumAttempts, + NonRetryableErrorTypes = proto.NonRetryableErrorTypes.ToArray(), + }; + if (proto.InitialInterval is { } initialInterval) + { + retryPolicy.InitialInterval = initialInterval.ToTimeSpan(); + } + if (proto.MaximumInterval is { } maximumInterval) + { + retryPolicy.MaximumInterval = maximumInterval.ToTimeSpan(); + } + return retryPolicy; + } + + private static ApiCommon.Memo ToMemo(IReadOnlyDictionary memo) + { + var proto = new ApiCommon.Memo(); + foreach (var item in memo) + { + if (item.Value == null) + { + throw new ArgumentException($"Memo value for {item.Key} is null", nameof(memo)); + } + proto.Fields.Add(item.Key, Workflow.PayloadConverter.ToPayload(item.Value)); + } + return proto; + } + + private static ApiCommon.Priority ToPriority(Temporalio.Common.Priority priority) => new() + { + PriorityKey = priority.PriorityKey ?? 0, + FairnessKey = priority.FairnessKey ?? string.Empty, + FairnessWeight = priority.FairnessWeight ?? 0f, + }; + + private static ApiWorkflow.VersioningOverride ToVersioningOverride(Temporalio.Common.VersioningOverride versioningOverride) => + versioningOverride switch + { + Temporalio.Common.VersioningOverride.Pinned pinned => new ApiWorkflow.VersioningOverride + { +#pragma warning disable CS0612 + Behavior = Temporalio.Api.Enums.V1.VersioningBehavior.Pinned, + PinnedVersion = pinned.Version.ToCanonicalString(), +#pragma warning restore CS0612 + Pinned = new ApiWorkflow.VersioningOverride.Types.PinnedOverride + { + Version = new ApiDeployment.WorkerDeploymentVersion + { + DeploymentName = pinned.Version.DeploymentName, + BuildId = pinned.Version.BuildId, + }, + Behavior = (ApiWorkflow.VersioningOverride.Types.PinnedOverrideBehavior)pinned.Behavior, + }, + }, + Temporalio.Common.VersioningOverride.AutoUpgrade _ => new ApiWorkflow.VersioningOverride + { +#pragma warning disable CS0612 + Behavior = Temporalio.Api.Enums.V1.VersioningBehavior.AutoUpgrade, +#pragma warning restore CS0612 + AutoUpgrade = true, + }, + _ => throw new ArgumentException("Unknown versioning override type", nameof(versioningOverride)), + }; + } +} diff --git a/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit b/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit new file mode 100644 index 00000000..d995d038 --- /dev/null +++ b/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit @@ -0,0 +1,162 @@ +/// @nexus.support +/// dotnet="dotnet/TemporalSupport.cs" +/// @nexus.support-namespace dotnet="NexGen.Support" +package nexus:temporal-types@1.0.0; + +interface model { + /// String-shaped placeholder for semantic types that generators reinterpret. + type placeholder = string; + + /// @nexus.proto "temporal.api.common.v1.Payload" typescript-package="@temporalio/proto" + /// @nexus.type python="typing.Any" typescript="common.Payload" dotnet="object?" typescript-package="@temporalio/common" + type payload = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Payloads" + /// typescript-package="@temporalio/proto" + type payloads = list; + + /// Callable result annotation for workflow functions. + /// @nexus.type + /// python="collections.abc.Awaitable[WorkflowResult]" + /// typescript="Promise" + /// dotnet="System.Threading.Tasks.Task" + type workflow-result = placeholder; + + /// Receiver/context argument for workflow callable method forms. + /// @nexus.type python="typing.Any" typescript="any" dotnet="object" + type callable-prefix = placeholder; + + /// @nexus.function-args + /// varargs=true + /// param="args" + /// typescript-drop-prefix=true + workflow-call: async func(callable-prefix: callable-prefix, args: payloads) -> workflow-result; + + /// Callable result annotation for signal functions. + /// @nexus.type python="None | collections.abc.Awaitable[None]" typescript="void" dotnet="void" + type signal-result = placeholder; + + /// @nexus.function-args + /// varargs=true + /// param="signal-args" + /// typescript-drop-prefix=true + signal-call: func(callable-prefix: callable-prefix, signal-args: payloads) -> signal-result; + + /// @nexus.proto "temporal.api.common.v1.WorkflowType" typescript-package="@temporalio/proto" + /// @nexus.type python="str" typescript="string" dotnet="string" + type workflow-type = placeholder; + + /// @nexus.function + /// primary=true + /// signature="workflow-call" + /// args-field="input" + /// result-type-parameter="WorkflowResult" + /// alternate-type="workflow-type" + /// dotnet-name-extractor="TemporalFunctionNames.WorkflowName" + /// @nexus.add-rpc-compatible-with "workflow-type" + type workflow-function = placeholder; + + /// @nexus.function + /// signature="signal-call" + /// args-field="signal-input" + /// alternate-type="string" + /// python-converter="signal_function_to_proto" + /// typescript-converter="signalFunctionToProto" + /// dotnet-name-extractor="TemporalFunctionNames.SignalName" + /// @nexus.add-rpc-compatible-with "string" + /// @nexus.typescript-with-arguments + /// signature="signal-call" + /// args-field="signal-input" + /// alternate-type="string" + /// value-type="workflow.SignalDefinition" + /// args-type="Value extends workflow.SignalDefinition ? Args : never" + /// name-expr="value.name" + /// typescript-package="@temporalio/workflow" + type signal-function = placeholder; + + /// @nexus.proto "temporal.api.common.v1.RetryPolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.RetryPolicy" + /// typescript="common.RetryPolicy" + /// dotnet="Temporalio.Common.RetryPolicy" + /// typescript-package="@temporalio/common" + type retry-policy = placeholder; + + /// @nexus.proto "temporal.api.taskqueue.v1.TaskQueue" typescript-package="@temporalio/proto" + /// @nexus.type python="str" typescript="string" dotnet="string" + type task-queue = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Memo" typescript-package="@temporalio/proto" + /// @nexus.type python="collections.abc.Mapping[str, typing.Any]" typescript="Record" dotnet="IReadOnlyDictionary" + type memo = placeholder; + + /// @nexus.proto "temporal.api.common.v1.SearchAttributes" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.TypedSearchAttributes" + /// typescript="common.TypedSearchAttributes" + /// dotnet="Temporalio.Common.SearchAttributeCollection" + /// typescript-package="@temporalio/common" + type search-attributes = placeholder; + + /// @nexus.proto "temporal.api.common.v1.Priority" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.Priority" + /// typescript="common.Priority" + /// dotnet="Temporalio.Common.Priority" + /// typescript-package="@temporalio/common" + type priority = placeholder; + + /// @nexus.proto "temporal.api.workflow.v1.VersioningOverride" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.VersioningOverride" + /// typescript="common.VersioningOverride" + /// dotnet="Temporalio.Common.VersioningOverride" + /// typescript-package="@temporalio/common" + type versioning-override = placeholder; + + /// @nexus.proto "google.protobuf.Duration" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="datetime.timedelta" + /// typescript="common.Duration" + /// dotnet="System.TimeSpan" + /// typescript-package="@temporalio/common" + type duration = placeholder; + + /// @nexus.proto "temporal.api.enums.v1.WorkflowIdReusePolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.WorkflowIDReusePolicy" + /// typescript="common.WorkflowIdReusePolicy" + /// dotnet="Temporalio.Api.Enums.V1.WorkflowIdReusePolicy" + /// typescript-package="@temporalio/common" + enum workflow-id-reuse-policy { + allow-duplicate, + allow-duplicate-failed-only, + reject-duplicate, + terminate-if-running, + } + + /// @nexus.proto "temporal.api.enums.v1.WorkflowIdConflictPolicy" typescript-package="@temporalio/proto" + /// @nexus.type + /// python="temporalio.common.WorkflowIDConflictPolicy" + /// typescript="common.WorkflowIdConflictPolicy" + /// dotnet="Temporalio.Api.Enums.V1.WorkflowIdConflictPolicy" + /// typescript-package="@temporalio/common" + enum workflow-id-conflict-policy { + fail, + use-existing, + terminate-existing, + } + + /// @nexus.proto "temporal.api.sdk.v1.UserMetadata" typescript-package="@temporalio/proto" + /// @nexus.flatten-in-api + record user-metadata { + /// @nexus.doc "Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format." + /// @nexus.proto-field "summary" + /// @nexus.flattened-type python="str" typescript="string" dotnet="string" + static-summary: option, + /// @nexus.doc "General fixed details for the workflow execution that may appear in UI and CLI. This can be in Temporal Markdown format and can span multiple lines. This value is fixed on the workflow execution and cannot be updated." + /// @nexus.proto-field "details" + /// @nexus.flattened-type python="str" typescript="string" dotnet="string" + static-details: option, + } +} diff --git a/src/Temporalio/SystemNexus/wit/workflow-service.wit b/src/Temporalio/SystemNexus/wit/workflow-service.wit new file mode 100644 index 00000000..3ff23004 --- /dev/null +++ b/src/Temporalio/SystemNexus/wit/workflow-service.wit @@ -0,0 +1,123 @@ +package temporal:nexus@1.0.0; + +world system { + export workflow-service; +} + +/// @nexus.endpoint "__temporal_system" +/// @nexus.service-name "temporal.api.workflowservice.v1.WorkflowService" +/// @nexus.delay-load-temporalio-workflow +/// @nexus.experimental +interface workflow-service { + use nexus:temporal-types/model@1.0.0.{ + duration, + memo, + payloads, + placeholder, + priority, + retry-policy, + search-attributes, + signal-function, + task-queue, + user-metadata, + versioning-override, + workflow-function, + workflow-id-conflict-policy, + workflow-id-reuse-policy, + }; + + /// @nexus.doc "Request fields for signaling a workflow, starting it first if needed." + /// @nexus.experimental + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" typescript-package="@temporalio/proto" + record signal-with-start-workflow-request { + /// @nexus.doc + /// python="Workflow type name or callable identifying the workflow to start." + /// typescript="Workflow type name or workflow function identifying the workflow to start." + /// dotnet="Workflow type name or workflow expression identifying the workflow to start." + /// @nexus.proto-field "workflow_type" + workflow: workflow-function, + /// @nexus.doc "Unique identifier for the workflow execution." + /// @nexus.proto-field "workflow_id" + id: string, + /// @nexus.doc "Task queue to run the workflow on." + task-queue: task-queue, + /// @nexus.doc + /// python="Signal name or callable to send with the start request." + /// typescript="Signal name or signal definition to send with the start request." + /// dotnet="Signal name or signal expression to send with the start request." + /// @nexus.proto-field "signal_name" + signal: signal-function, + /// @nexus.doc "Total workflow execution timeout, including retries and continue-as-new." + /// @nexus.proto-field "workflow_execution_timeout" + execution-timeout: option, + /// @nexus.doc "Timeout of a single workflow run." + /// @nexus.proto-field "workflow_run_timeout" + run-timeout: option, + /// @nexus.doc "Timeout of a single workflow task." + /// @nexus.proto-field "workflow_task_timeout" + task-timeout: option, + /// @nexus.omit + identity: placeholder, + /// @nexus.doc "Request ID used to deduplicate workflow start requests." + request-id: option, + /// @nexus.doc "Behavior when a closed workflow with the same ID exists. Default is allow-duplicate." + /// @nexus.proto-field "workflow_id_reuse_policy" + /// @nexus.default "allow-duplicate" + id-reuse-policy: workflow-id-reuse-policy, + /// @nexus.doc "Behavior when a workflow is currently running with the same ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot be set if id-reuse-policy is terminate-if-running." + /// @nexus.proto-field "workflow_id_conflict_policy" + id-conflict-policy: option, + /// @nexus.doc "Retry policy for the workflow." + retry-policy: option, + /// @nexus.doc "Cron schedule for recurring workflow executions. See https://docs.temporal.io/cron-job." + cron-schedule: option, + /// @nexus.doc "Memo for the workflow." + memo: option, + /// @nexus.doc "Typed search attributes for the workflow." + search-attributes: option, + /// @nexus.doc "Priority of the workflow execution." + priority: option, + /// @nexus.doc "Override for workflow versioning behavior." + versioning-override: option, + /// @nexus.doc "Amount of time to wait before starting the workflow. This does not work with cron-schedule." + /// @nexus.proto-field "workflow_start_delay" + start-delay: option, + user-metadata: option, + /// @nexus.source python="workflow_namespace" typescript="workflowNamespace" dotnet="TemporalWorkflowContext.WorkflowNamespace" + namespace: string, + /// @nexus.omit + control: placeholder, + /// @nexus.omit + header: placeholder, + /// @nexus.omit + links: placeholder, + /// @nexus.omit + time-skipping-config: placeholder, + } + + /// @nexus.experimental + /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" typescript-package="@temporalio/proto" + record signal-with-start-workflow-response { + run-id: option, + started: option, + /// @nexus.omit + signal-link: placeholder, + } + + /// @nexus.doc + /// "Signal a workflow, starting it first if needed." + /// returns="A workflow handle to the started workflow." + /// @nexus.output-transform + /// python-type="temporalio.workflow.ExternalWorkflowHandle[WorkflowResult]" + /// python="temporalio.workflow.get_external_workflow_handle(request.id, run_id=result.run_id)" + /// typescript-type="workflow.ExternalWorkflowHandle" + /// typescript="workflow.getExternalWorkflowHandle(request.id, result.runId ?? undefined)" + /// typescript-package="@temporalio/workflow" + /// dotnet-type="Temporalio.Workflows.ExternalWorkflowHandle" + /// dotnet="Temporalio.Workflows.Workflow.GetExternalWorkflowHandle(request.Id, result.RunId)" + /// @nexus.operation name="SignalWithStartWorkflowExecution" + /// @nexus.experimental + signal-with-start-workflow: func( + request: signal-with-start-workflow-request, + ) -> signal-with-start-workflow-response; +} diff --git a/src/Temporalio/Temporalio.csproj b/src/Temporalio/Temporalio.csproj index 817fe019..071f8fa4 100644 --- a/src/Temporalio/Temporalio.csproj +++ b/src/Temporalio/Temporalio.csproj @@ -5,6 +5,17 @@ Temporal SDK for .NET true 9.0 + $(MSBuildProjectDirectory)/obj/$(Configuration)/$(TargetFramework)/SystemNexus/ + $(SystemNexusIntermediateDir)temporal_api.bin + $(SystemNexusIntermediateDir)generated/ + $(SystemNexusGeneratedDir)Operations.cs + $(SystemNexusIntermediateDir)generated.stamp + $(MSBuildProjectDirectory)/../Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj + 0.1.4 + $(NEX_GEN_BIN) + nex-gen + $(DotNetHostPath) + dotnet README.md true snupkg @@ -30,6 +41,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs b/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs index bcb39ed9..28dc6335 100644 --- a/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs +++ b/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs @@ -40,5 +40,19 @@ internal interface IWorkflowCodecHelperInstance /// Sequence. /// Context. ISerializationContext.Workflow? GetPendingExternalSignalSerializationContext(uint seq); + + /// + /// Gets the pending Nexus operation info for the given sequence. + /// + /// Sequence. + /// Info. + NexusOperationInfo? GetPendingNexusOperationInfo(uint seq); + + /// + /// Pending Nexus operation info. + /// + /// Service name. + /// Operation name. + internal record NexusOperationInfo(string Service, string Operation); } -} \ No newline at end of file +} diff --git a/src/Temporalio/Worker/WorkflowCodecHelper.cs b/src/Temporalio/Worker/WorkflowCodecHelper.cs index bc92db9a..013f85b2 100644 --- a/src/Temporalio/Worker/WorkflowCodecHelper.cs +++ b/src/Temporalio/Worker/WorkflowCodecHelper.cs @@ -118,31 +118,44 @@ await childCodec2.DecodeFailureAsync( break; case WorkflowActivationJob.VariantOneofCase.ResolveNexusOperation: // TODO(cretz): Support Nexus serialization context - if (context.CodecNoContext == null) + var nexusCodec = context.CodecNoContext; + if (nexusCodec == null) { break; } if (job.ResolveNexusOperation.Result.Completed != null) { - await DecodeAsync( - context.CodecNoContext, job.ResolveNexusOperation.Result.Completed). - ConfigureAwait(false); + var operationInfo = context.Instance?.GetPendingNexusOperationInfo( + job.ResolveNexusOperation.Seq); + if (operationInfo == null || + !await SystemNexusPayloadVisitorRegistry.TryVisitOutputAsync( + operationInfo.Service, + operationInfo.Operation, + job.ResolveNexusOperation.Result.Completed, + payload => DecodeAsync(nexusCodec, payload), + payloads => DecodeAsync(nexusCodec, payloads)). + ConfigureAwait(false)) + { + await DecodeAsync( + nexusCodec, job.ResolveNexusOperation.Result.Completed). + ConfigureAwait(false); + } } else if (job.ResolveNexusOperation.Result.Failed != null) { - await context.CodecNoContext.DecodeFailureAsync( + await nexusCodec.DecodeFailureAsync( job.ResolveNexusOperation.Result.Failed). ConfigureAwait(false); } else if (job.ResolveNexusOperation.Result.Cancelled != null) { - await context.CodecNoContext.DecodeFailureAsync( + await nexusCodec.DecodeFailureAsync( job.ResolveNexusOperation.Result.Cancelled). ConfigureAwait(false); } else if (job.ResolveNexusOperation.Result.TimedOut != null) { - await context.CodecNoContext.DecodeFailureAsync( + await nexusCodec.DecodeFailureAsync( job.ResolveNexusOperation.Result.TimedOut). ConfigureAwait(false); } @@ -335,8 +348,17 @@ await EncodeAsync( codec = context.CodecNoContext; if (cmd.ScheduleNexusOperation.Input != null && codec != null) { - await EncodeAsync( - codec, cmd.ScheduleNexusOperation.Input).ConfigureAwait(false); + if (!await SystemNexusPayloadVisitorRegistry.TryVisitInputAsync( + cmd.ScheduleNexusOperation.Service, + cmd.ScheduleNexusOperation.Operation, + cmd.ScheduleNexusOperation.Input, + payload => EncodeAsync(codec, payload), + payloads => EncodeAsync(codec, payloads)). + ConfigureAwait(false)) + { + await EncodeAsync( + codec, cmd.ScheduleNexusOperation.Input).ConfigureAwait(false); + } } break; case WorkflowCommand.VariantOneofCase.StartChildWorkflowExecution: diff --git a/src/Temporalio/Worker/WorkflowInstance.cs b/src/Temporalio/Worker/WorkflowInstance.cs index 253eaee6..e675ae20 100644 --- a/src/Temporalio/Worker/WorkflowInstance.cs +++ b/src/Temporalio/Worker/WorkflowInstance.cs @@ -820,6 +820,13 @@ public WorkflowActivationCompletion Activate(WorkflowActivation act) return pending?.SerializationContext; } + /// + public IWorkflowCodecHelperInstance.NexusOperationInfo? GetPendingNexusOperationInfo(uint seq) + { + nexusOperationsPending.TryGetValue(seq, out var pending); + return pending == null ? null : new(pending.Service, pending.Operation); + } + /// protected override IEnumerable? GetScheduledTasks() => scheduledTasks; @@ -2578,13 +2585,20 @@ public override Task> ScheduleNexusOperati var payloadConverter = instance.payloadConverterNoContext; var seq = ++instance.nexusOperationCounter; + var inputPayload = SystemNexusPayloadVisitorRegistry.TryToInputPayload( + input.Service, + input.OperationName, + input.Arg, + out var systemNexusInputPayload) ? + systemNexusInputPayload : + payloadConverter.ToPayload(input.Arg); var cmd = new ScheduleNexusOperation() { Seq = seq, Endpoint = input.ClientOptions.Endpoint, Service = input.Service, Operation = input.OperationName, - Input = payloadConverter.ToPayload(input.Arg), + Input = inputPayload, CancellationType = (Bridge.Api.Nexus.NexusOperationCancellationType)input.Options.CancellationType, }; if (input.Options.ScheduleToCloseTimeout is TimeSpan schedToCloseTimeout) @@ -2612,6 +2626,8 @@ public override Task> ScheduleNexusOperati var handleSource = new TaskCompletionSource>(); var pending = new PendingNexusOperationInfo( + Service: input.Service, + Operation: input.OperationName, StartCompletionSource: new(), ResultCompletionSource: new()); instance.nexusOperationsPending[seq] = pending; @@ -3074,6 +3090,8 @@ private record PendingExternalCancel( TaskCompletionSource CompletionSource); private record PendingNexusOperationInfo( + string Service, + string Operation, TaskCompletionSource StartCompletionSource, TaskCompletionSource ResultCompletionSource); diff --git a/src/Temporalio/Workflows/Workflow.cs b/src/Temporalio/Workflows/Workflow.cs index 5afff73c..d1301d10 100644 --- a/src/Temporalio/Workflows/Workflow.cs +++ b/src/Temporalio/Workflows/Workflow.cs @@ -16,7 +16,7 @@ namespace Temporalio.Workflows /// class cannot be used outside of a workflow (with the obvious exception of /// ). /// - public static class Workflow + public static partial class Workflow { /// /// Gets a value indicating whether all update and signal handlers have finished executing. diff --git a/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs b/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs index c3eeea25..43b7fe77 100644 --- a/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs +++ b/tests/Temporalio.Tests/Worker/WorkflowWorkerTests.cs @@ -985,6 +985,55 @@ async Task HandleSignalAsync(string arg) => public IList Events() => events; } + [Workflow] + public class SystemNexusSignalWithStartTargetWorkflow + { + private readonly List events = new(); + + [WorkflowRun] + public async Task> RunAsync(string value) + { + events.Add($"Started: {value}"); + await Workflow.WaitConditionAsync(() => events.Count >= 3); + return events; + } + + [WorkflowSignal] + public Task SignalAsync(string value) + { + events.Add($"Signal: {value}"); + return Task.CompletedTask; + } + } + + [Workflow] + public class SystemNexusSignalWithStartCallerWorkflow + { + [WorkflowRun] + public async Task RunAsync(string workflowId, string taskQueue) + { + var handle = await Workflow.SignalWithStartWorkflowAsync( + (SystemNexusSignalWithStartTargetWorkflow workflow) => + workflow.RunAsync("start-value"), + workflow => workflow.SignalAsync("signal-one"), + new(workflowId, taskQueue) + { + IdConflictPolicy = WorkflowIdConflictPolicy.UseExisting, + }); + + await Workflow.SignalWithStartWorkflowAsync( + (SystemNexusSignalWithStartTargetWorkflow workflow) => + workflow.RunAsync("unused-start-value"), + workflow => workflow.SignalAsync("signal-two"), + new(workflowId, taskQueue) + { + IdConflictPolicy = WorkflowIdConflictPolicy.UseExisting, + }); + + return handle.Id; + } + } + [Fact] public async Task ExecuteWorkflowAsync_Signals_ProperlyHandled() { @@ -1066,6 +1115,40 @@ await ExecuteWorkerAsync(async worker => }); } + [Fact] + public async Task ExecuteWorkflowAsync_SignalWithStartFromWorkflow_Succeeds() + { + var newOptions = (TemporalClientOptions)Client.Options.Clone(); + newOptions.DataConverter = DataConverter.Default with + { + PayloadCodec = new Base64PayloadCodec(), + }; + var codecClient = new TemporalClient(Client.Connection, newOptions); + var workerOptions = new TemporalWorkerOptions($"tq-{Guid.NewGuid()}"). + AddWorkflow(); + await ExecuteWorkerAsync( + async worker => + { + var targetWorkflowId = $"workflow-{Guid.NewGuid()}"; + var resultWorkflowId = await codecClient.ExecuteWorkflowAsync( + (SystemNexusSignalWithStartCallerWorkflow workflow) => + workflow.RunAsync(targetWorkflowId, worker.Options.TaskQueue!), + new(id: $"workflow-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!)); + Assert.Equal(targetWorkflowId, resultWorkflowId); + + var targetHandle = codecClient.GetWorkflowHandle< + SystemNexusSignalWithStartTargetWorkflow, + IReadOnlyCollection>(targetWorkflowId); + var events = await targetHandle.GetResultAsync(); + Assert.Equal(3, events.Count); + Assert.Contains("Started: start-value", events); + Assert.Contains("Signal: signal-one", events); + Assert.Contains("Signal: signal-two", events); + }, + workerOptions, + codecClient); + } + [Workflow] public class BadSignalArgsDroppedWorkflow { diff --git a/tests/Temporalio.Tests/WorkflowEnvironment.cs b/tests/Temporalio.Tests/WorkflowEnvironment.cs index 14b34bcf..5d163dd3 100644 --- a/tests/Temporalio.Tests/WorkflowEnvironment.cs +++ b/tests/Temporalio.Tests/WorkflowEnvironment.cs @@ -67,7 +67,7 @@ public async Task InitializeAsync() { DevServerOptions = new() { - DownloadVersion = "v1.7.1-standalone-nexus-operations", + DownloadVersion = "v1.7.1-system-nexus-operations", ExtraArgs = new List { // Disable search attribute cache @@ -86,6 +86,8 @@ public async Task InitializeAsync() "--dynamic-config-value", "frontend.enableExecuteMultiOperation=true", "--dynamic-config-value", + "history.enableSignalWithStartFromWorkflow=true", + "--dynamic-config-value", "system.enableDeploymentVersions=true", // Enable standalone activities "--dynamic-config-value", "frontend.activityAPIsEnabled=true", From 5b04510674b278d85c4d917d897a1de33f75bd98 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 17 Jun 2026 10:48:06 -0700 Subject: [PATCH 02/13] Check in system Nexus generated sources --- .github/workflows/ci.yml | 1 + .../Program.cs | 107 ++++++- .../Temporalio.SystemNexus.Generator.csproj | 3 +- .../packages.lock.json | 13 + .../dotnet/TemporalSupport.cs | 0 .../wit/deps/nexus-temporal-types/model.wit | 0 .../wit/workflow-service.wit | 0 .../SystemNexus/Generated/Models.cs | 208 +++++++++++++ .../SystemNexus/Generated/Operations.cs | 261 ++++++++++++++++ .../SystemNexus/Generated/Service.cs | 40 +++ .../Generated/Support/TemporalSupport.cs | 282 ++++++++++++++++++ .../SystemNexusPayloadVisitorRegistry.cs | 216 ++++++++++++++ src/Temporalio/Temporalio.csproj | 46 --- 13 files changed, 1121 insertions(+), 56 deletions(-) create mode 100644 src/Temporalio.SystemNexus.Generator/packages.lock.json rename src/{Temporalio/SystemNexus => Temporalio.SystemNexus.Generator}/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs (100%) rename src/{Temporalio/SystemNexus => Temporalio.SystemNexus.Generator}/wit/deps/nexus-temporal-types/model.wit (100%) rename src/{Temporalio/SystemNexus => Temporalio.SystemNexus.Generator}/wit/workflow-service.wit (100%) create mode 100644 src/Temporalio/SystemNexus/Generated/Models.cs create mode 100644 src/Temporalio/SystemNexus/Generated/Operations.cs create mode 100644 src/Temporalio/SystemNexus/Generated/Service.cs create mode 100644 src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs create mode 100644 src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f945ee02..39054ea0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,7 @@ jobs: dotnet tool install --global --version '[20.1.2.1]' ClangSharpPInvokeGenerator ClangSharpPInvokeGenerator @src/Temporalio/Bridge/GenerateInterop.rsp dotnet run --project src/Temporalio.Api.Generator + dotnet run --project src/Temporalio.SystemNexus.Generator git add . git config --global core.safecrlf false git diff --cached > generator.diff diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index f4c726b0..98b2ec74 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -1,20 +1,80 @@ +using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Google.Protobuf.Reflection; -if (args.Length != 2) +var currFile = new StackTrace(true).GetFrame(0)?.GetFileName(); +var projectDir = Path.GetFullPath(Path.Join(currFile, "../../../")); +var generatorDir = Path.Join(projectDir, "src/Temporalio.SystemNexus.Generator"); +var protoDir = Path.Join(projectDir, "src/Temporalio/Bridge/sdk-core/crates/protos/protos"); +var apiProtoDir = Path.Join(protoDir, "api_upstream"); +var descriptorPath = Path.Join(generatorDir, "obj/SystemNexus/temporal_api.bin"); +var outputDir = Path.Join(projectDir, "src/Temporalio/SystemNexus/Generated"); +var operationsPath = Path.Join(outputDir, "Operations.cs"); + +EnsureNexGen(); +BuildDescriptor(); +GenerateNexusApi(); +TransformOperationsFile(operationsPath); +GeneratePayloadVisitorRegistry(operationsPath, descriptorPath); +return 0; + +void EnsureNexGen() { - Console.Error.WriteLine( - "Usage: Temporalio.SystemNexus.Generator "); - return 2; + var nexGenCommand = NexGenCommand(); + if (RunProcess(nexGenCommand, new[] { "help" }, ignoreExitCode: true) == 0) + { + return; + } + + if (Environment.GetEnvironmentVariable("NEX_GEN_BIN") is { Length: > 0 }) + { + throw new InvalidOperationException($"Unable to run nex-gen command {nexGenCommand}"); + } + + RunProcess("cargo", new[] { "install", "--locked", "nex-gen", "--force" }); } -var operationsPath = args[0]; -var descriptorPath = args[1]; +void BuildDescriptor() +{ + Directory.CreateDirectory(Path.GetDirectoryName(descriptorPath)!); + RunProcess( + "protoc", + new[] + { + "-I=" + apiProtoDir, + "-I=" + protoDir, + "--include_imports", + "--descriptor_set_out=" + descriptorPath, + Path.Join(apiProtoDir, "temporal/api/workflowservice/v1/request_response.proto"), + }); +} -TransformOperationsFile(operationsPath); -GeneratePayloadVisitorRegistry(operationsPath, descriptorPath); -return 0; +void GenerateNexusApi() +{ + if (Directory.Exists(outputDir)) + { + new DirectoryInfo(outputDir).Delete(true); + } + + Directory.CreateDirectory(outputDir); + RunProcess( + NexGenCommand(), + new[] + { + "generate", + "--lang", + "dotnet", + "--input", + Path.Join(generatorDir, "wit/workflow-service.wit"), + "--input", + Path.Join(generatorDir, "wit/deps"), + "--descriptors", + descriptorPath, + "--output", + outputDir, + }); +} static void TransformOperationsFile(string operationsPath) { @@ -601,6 +661,35 @@ static string PropertyName(string protoName, string messageName) return propertyName == messageName ? propertyName + "_" : propertyName; } +static int RunProcess(string fileName, IEnumerable arguments, bool ignoreExitCode = false) +{ + var process = new Process + { + StartInfo = + { + FileName = fileName, + UseShellExecute = false, + }, + }; + foreach (var argument in arguments) + { + process.StartInfo.ArgumentList.Add(argument); + } + + Console.WriteLine("Running {0} {1}", fileName, string.Join(" ", arguments)); + process.Start(); + process.WaitForExit(); + if (!ignoreExitCode && process.ExitCode != 0) + { + throw new InvalidOperationException($"{fileName} failed with exit code {process.ExitCode}"); + } + + return process.ExitCode; +} + +static string NexGenCommand() => + Environment.GetEnvironmentVariable("NEX_GEN_BIN") is { Length: > 0 } value ? value : "nex-gen"; + static string UniqueLocalName(MessageInfo message, FieldDescriptorProto field, string prefix) => $"{prefix}_{Regex.Replace(message.FullName, @"[^A-Za-z0-9_]", "_")}_{field.Number}"; diff --git a/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj index 6f5f3920..0200095c 100644 --- a/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj +++ b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj @@ -10,11 +10,12 @@ false enable - false false + + diff --git a/src/Temporalio.SystemNexus.Generator/packages.lock.json b/src/Temporalio.SystemNexus.Generator/packages.lock.json new file mode 100644 index 00000000..4a20ae08 --- /dev/null +++ b/src/Temporalio.SystemNexus.Generator/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Google.Protobuf": { + "type": "Direct", + "requested": "[3.26.1, )", + "resolved": "3.26.1", + "contentHash": "CHZX8zXqhF/fdUtd+AYzew8T2HFkAoe5c7lbGxZY/qryAlQXckDvM5BfOJjXlMS7kyICqQTMszj4w1bX5uBJ/w==" + } + } + } +} \ No newline at end of file diff --git a/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs similarity index 100% rename from src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs rename to src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs diff --git a/src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit similarity index 100% rename from src/Temporalio/SystemNexus/wit/deps/nexus-temporal-types/model.wit rename to src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit diff --git a/src/Temporalio/SystemNexus/wit/workflow-service.wit b/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit similarity index 100% rename from src/Temporalio/SystemNexus/wit/workflow-service.wit rename to src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit diff --git a/src/Temporalio/SystemNexus/Generated/Models.cs b/src/Temporalio/SystemNexus/Generated/Models.cs new file mode 100644 index 00000000..72aaafeb --- /dev/null +++ b/src/Temporalio/SystemNexus/Generated/Models.cs @@ -0,0 +1,208 @@ +// +// Generated by nex-gen. DO NOT EDIT! +#nullable enable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using NexGen.Support; + +namespace NexGen.WorkflowService +{ + + /// + /// Request fields for signaling a workflow, starting it first if needed. + /// + internal class SignalWithStartWorkflowRequest + { + internal SignalWithStartWorkflowRequest(string workflow, string id, string taskQueue, string signal) + { + Workflow = workflow; + Id = id; + TaskQueue = taskQueue; + Signal = signal; + } + + /// + /// Workflow type name or workflow expression identifying the workflow to start. + /// + public string Workflow { get; init; } + /// + /// Arguments for the workflow. + /// + public IReadOnlyCollection? Args { get; init; } + /// + /// Unique identifier for the workflow execution. + /// + public string Id { get; init; } + /// + /// Task queue to run the workflow on. + /// + public string TaskQueue { get; init; } + /// + /// Signal name or signal expression to send with the start request. + /// + public string Signal { get; init; } + /// + /// Arguments for the signal. + /// + public IReadOnlyCollection? SignalArgs { get; init; } + /// + /// Total workflow execution timeout, including retries and continue-as-new. + /// + public System.TimeSpan? ExecutionTimeout { get; init; } + /// + /// Timeout of a single workflow run. + /// + public System.TimeSpan? RunTimeout { get; init; } + /// + /// Timeout of a single workflow task. + /// + public System.TimeSpan? TaskTimeout { get; init; } + /// + /// Request ID used to deduplicate workflow start requests. + /// + public string? RequestId { get; init; } + /// + /// Behavior when a closed workflow with the same ID exists. Default is allow-duplicate. + /// + public Temporalio.Api.Enums.V1.WorkflowIdReusePolicy? IdReusePolicy { get; init; } + /// + /// Behavior when a workflow is currently running with the same ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot be set if id-reuse-policy is terminate-if-running. + /// + public Temporalio.Api.Enums.V1.WorkflowIdConflictPolicy? IdConflictPolicy { get; init; } + /// + /// Retry policy for the workflow. + /// + public Temporalio.Common.RetryPolicy? RetryPolicy { get; init; } + /// + /// Cron schedule for recurring workflow executions. See https://docs.temporal.io/cron-job. + /// + public string? CronSchedule { get; init; } + /// + /// Memo for the workflow. + /// + public IReadOnlyDictionary? Memo { get; init; } + /// + /// Typed search attributes for the workflow. + /// + public Temporalio.Common.SearchAttributeCollection? SearchAttributes { get; init; } + /// + /// Priority of the workflow execution. + /// + public Temporalio.Common.Priority? Priority { get; init; } + /// + /// Override for workflow versioning behavior. + /// + public Temporalio.Common.VersioningOverride? VersioningOverride { get; init; } + /// + /// Amount of time to wait before starting the workflow. This does not work with cron-schedule. + /// + public System.TimeSpan? StartDelay { get; init; } + public UserMetadata? UserMetadata { get; init; } + + public Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest ToProto() + { + var proto = new Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest(); + proto.Namespace = NexGen.Support.TemporalWorkflowContext.WorkflowNamespace(); + proto.WorkflowType = Workflow.ToProto(default(Temporalio.Api.Common.V1.WorkflowType)!); + if (Args is { } args) + { + proto.Input = args.ToProto(); + } + proto.WorkflowId = Id; + proto.TaskQueue = TaskQueue.ToProto(default(Temporalio.Api.TaskQueue.V1.TaskQueue)!); + proto.SignalName = Signal; + if (SignalArgs is { } signalArgs) + { + proto.SignalInput = signalArgs.ToProto(); + } + if (ExecutionTimeout is { } executionTimeout) + { + proto.WorkflowExecutionTimeout = executionTimeout.ToProto(); + } + if (RunTimeout is { } runTimeout) + { + proto.WorkflowRunTimeout = runTimeout.ToProto(); + } + if (TaskTimeout is { } taskTimeout) + { + proto.WorkflowTaskTimeout = taskTimeout.ToProto(); + } + if (RequestId is { } requestId) + { + proto.RequestId = requestId; + } + if (IdReusePolicy is { } idReusePolicy) + { + proto.WorkflowIdReusePolicy = idReusePolicy; + } + if (IdConflictPolicy is { } idConflictPolicy) + { + proto.WorkflowIdConflictPolicy = idConflictPolicy; + } + if (RetryPolicy is { } retryPolicy) + { + proto.RetryPolicy = retryPolicy.ToProto(); + } + if (CronSchedule is { } cronSchedule) + { + proto.CronSchedule = cronSchedule; + } + if (Memo is { } memo) + { + proto.Memo = memo.ToProto(); + } + if (SearchAttributes is { } searchAttributes) + { + proto.SearchAttributes = searchAttributes.ToProto(); + } + if (Priority is { } priority) + { + proto.Priority = priority.ToProto(); + } + if (VersioningOverride is { } versioningOverride) + { + proto.VersioningOverride = versioningOverride.ToProto(); + } + if (StartDelay is { } startDelay) + { + proto.WorkflowStartDelay = startDelay.ToProto(); + } + if (UserMetadata is { } userMetadata) + { + proto.UserMetadata = userMetadata.ToProto(); + } + return proto; + } + + } + + public class UserMetadata + { + /// + /// Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format. + /// + public object? StaticSummary { get; init; } + /// + /// General fixed details for the workflow execution that may appear in UI and CLI. This can be in Temporal Markdown format and can span multiple lines. This value is fixed on the workflow execution and cannot be updated. + /// + public object? StaticDetails { get; init; } + + public Temporalio.Api.Sdk.V1.UserMetadata ToProto() + { + var proto = new Temporalio.Api.Sdk.V1.UserMetadata(); + if (StaticSummary is { } staticSummary) + { + proto.Summary = staticSummary.ToProto(); + } + if (StaticDetails is { } staticDetails) + { + proto.Details = staticDetails.ToProto(); + } + return proto; + } + + } + +} diff --git a/src/Temporalio/SystemNexus/Generated/Operations.cs b/src/Temporalio/SystemNexus/Generated/Operations.cs new file mode 100644 index 00000000..4d85b814 --- /dev/null +++ b/src/Temporalio/SystemNexus/Generated/Operations.cs @@ -0,0 +1,261 @@ +// +// Generated by nex-gen. DO NOT EDIT! +#nullable enable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Google.Protobuf.WellKnownTypes; +using Temporalio.Converters; +using Temporalio.Workflows; + +using NexGen.WorkflowService; +namespace Temporalio.Workflows +{ + + /// + /// Signal a workflow, starting it first if needed. + /// + public class SignalWithStartWorkflowOptions + { + public SignalWithStartWorkflowOptions(string id, string taskQueue) + { + Id = id; + TaskQueue = taskQueue; + } + + /// + /// Unique identifier for the workflow execution. + /// + public string Id { get; set; } + /// + /// Task queue to run the workflow on. + /// + public string TaskQueue { get; set; } + /// + /// Total workflow execution timeout, including retries and continue-as-new. + /// + public System.TimeSpan? ExecutionTimeout { get; set; } + /// + /// Timeout of a single workflow run. + /// + public System.TimeSpan? RunTimeout { get; set; } + /// + /// Timeout of a single workflow task. + /// + public System.TimeSpan? TaskTimeout { get; set; } + /// + /// Request ID used to deduplicate workflow start requests. + /// + public string? RequestId { get; set; } + /// + /// Behavior when a closed workflow with the same ID exists. Default is allow-duplicate. + /// + public Temporalio.Api.Enums.V1.WorkflowIdReusePolicy? IdReusePolicy { get; set; } + /// + /// Behavior when a workflow is currently running with the same ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot be set if id-reuse-policy is terminate-if-running. + /// + public Temporalio.Api.Enums.V1.WorkflowIdConflictPolicy? IdConflictPolicy { get; set; } + /// + /// Retry policy for the workflow. + /// + public Temporalio.Common.RetryPolicy? RetryPolicy { get; set; } + /// + /// Cron schedule for recurring workflow executions. See https://docs.temporal.io/cron-job. + /// + public string? CronSchedule { get; set; } + /// + /// Memo for the workflow. + /// + public IReadOnlyDictionary? Memo { get; set; } + /// + /// Typed search attributes for the workflow. + /// + public Temporalio.Common.SearchAttributeCollection? SearchAttributes { get; set; } + /// + /// Priority of the workflow execution. + /// + public Temporalio.Common.Priority? Priority { get; set; } + /// + /// Override for workflow versioning behavior. + /// + public Temporalio.Common.VersioningOverride? VersioningOverride { get; set; } + /// + /// Amount of time to wait before starting the workflow. This does not work with cron-schedule. + /// + public System.TimeSpan? StartDelay { get; set; } + /// + /// Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format. + /// + public string? StaticSummary { get; set; } + /// + /// General fixed details for the workflow execution that may appear in UI and CLI. This can be in Temporal Markdown format and can span multiple lines. This value is fixed on the workflow execution and cannot be updated. + /// + public string? StaticDetails { get; set; } + } + + public static partial class Workflow + { + /// + /// Signal a workflow, starting it first if needed. + /// + /// Request fields for signaling a workflow, starting it first if needed. + /// A workflow handle to the started workflow. + private static async Task SignalWithStartWorkflowAsync(SignalWithStartWorkflowRequest request) + { + var client = Workflow.CreateNexusWorkflowClient("__temporal_system"); + var protoRequest = request.ToProto(); + var result = await client.ExecuteNexusOperationAsync(svc => svc.SignalWithStartWorkflow(protoRequest)).ConfigureAwait(true); + return Temporalio.Workflows.Workflow.GetExternalWorkflowHandle(request.Id, result.RunId); + } + + /// + /// Signal a workflow, starting it first if needed. + /// + /// Workflow type name or workflow expression identifying the workflow to start. + /// Arguments for the workflow. + /// Signal name or signal expression to send with the start request. + /// Arguments for the signal. + /// Request fields for signaling a workflow, starting it first if needed. + /// A workflow handle to the started workflow. + public static Task SignalWithStartWorkflowAsync(string workflow, IReadOnlyCollection? args, string signal, IReadOnlyCollection? signalArgs, SignalWithStartWorkflowOptions options) + { + var request = new SignalWithStartWorkflowRequest(workflow, options.Id, options.TaskQueue, signal) + { + Args = args, + SignalArgs = signalArgs, + ExecutionTimeout = options.ExecutionTimeout, + RunTimeout = options.RunTimeout, + TaskTimeout = options.TaskTimeout, + RequestId = options.RequestId, + IdReusePolicy = options.IdReusePolicy, + IdConflictPolicy = options.IdConflictPolicy, + RetryPolicy = options.RetryPolicy, + CronSchedule = options.CronSchedule, + Memo = options.Memo, + SearchAttributes = options.SearchAttributes, + Priority = options.Priority, + VersioningOverride = options.VersioningOverride, + StartDelay = options.StartDelay, + UserMetadata = options.StaticSummary != null || options.StaticDetails != null ? new UserMetadata() { StaticSummary = options.StaticSummary, StaticDetails = options.StaticDetails } : null, + }; + return SignalWithStartWorkflowAsync(request); + } + + /// + /// Signal a workflow, starting it first if needed. + /// + /// Workflow type name or workflow expression identifying the workflow to start. + /// Signal name or signal expression to send with the start request. + /// Arguments for the signal. + /// Request fields for signaling a workflow, starting it first if needed. + /// A workflow handle to the started workflow. + public static Task SignalWithStartWorkflowAsync(Expression>> workflow, string signal, IReadOnlyCollection? signalArgs, SignalWithStartWorkflowOptions options) + { + var (workflowMethod, workflowArgs) = ExtractCall(workflow); + var request = new SignalWithStartWorkflowRequest(NexGen.Support.TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, signal) + { + Args = workflowArgs, + SignalArgs = signalArgs, + ExecutionTimeout = options.ExecutionTimeout, + RunTimeout = options.RunTimeout, + TaskTimeout = options.TaskTimeout, + RequestId = options.RequestId, + IdReusePolicy = options.IdReusePolicy, + IdConflictPolicy = options.IdConflictPolicy, + RetryPolicy = options.RetryPolicy, + CronSchedule = options.CronSchedule, + Memo = options.Memo, + SearchAttributes = options.SearchAttributes, + Priority = options.Priority, + VersioningOverride = options.VersioningOverride, + StartDelay = options.StartDelay, + UserMetadata = options.StaticSummary != null || options.StaticDetails != null ? new UserMetadata() { StaticSummary = options.StaticSummary, StaticDetails = options.StaticDetails } : null, + }; + return SignalWithStartWorkflowAsync(request); + } + + /// + /// Signal a workflow, starting it first if needed. + /// + /// Workflow type name or workflow expression identifying the workflow to start. + /// Arguments for the workflow. + /// Signal name or signal expression to send with the start request. + /// Request fields for signaling a workflow, starting it first if needed. + /// A workflow handle to the started workflow. + public static Task SignalWithStartWorkflowAsync(string workflow, IReadOnlyCollection? args, Expression> signal, SignalWithStartWorkflowOptions options) + { + var (signalMethod, signalArgs) = ExtractCall(signal); + var request = new SignalWithStartWorkflowRequest(workflow, options.Id, options.TaskQueue, NexGen.Support.TemporalFunctionNames.SignalName(signalMethod)) + { + Args = args, + SignalArgs = signalArgs, + ExecutionTimeout = options.ExecutionTimeout, + RunTimeout = options.RunTimeout, + TaskTimeout = options.TaskTimeout, + RequestId = options.RequestId, + IdReusePolicy = options.IdReusePolicy, + IdConflictPolicy = options.IdConflictPolicy, + RetryPolicy = options.RetryPolicy, + CronSchedule = options.CronSchedule, + Memo = options.Memo, + SearchAttributes = options.SearchAttributes, + Priority = options.Priority, + VersioningOverride = options.VersioningOverride, + StartDelay = options.StartDelay, + UserMetadata = options.StaticSummary != null || options.StaticDetails != null ? new UserMetadata() { StaticSummary = options.StaticSummary, StaticDetails = options.StaticDetails } : null, + }; + return SignalWithStartWorkflowAsync(request); + } + + /// + /// Signal a workflow, starting it first if needed. + /// + /// Workflow type name or workflow expression identifying the workflow to start. + /// Signal name or signal expression to send with the start request. + /// Request fields for signaling a workflow, starting it first if needed. + /// A workflow handle to the started workflow. + public static Task SignalWithStartWorkflowAsync(Expression>> workflow, Expression> signal, SignalWithStartWorkflowOptions options) + { + var (workflowMethod, workflowArgs) = ExtractCall(workflow); + var (signalMethod, signalArgs) = ExtractCall(signal); + var request = new SignalWithStartWorkflowRequest(NexGen.Support.TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, NexGen.Support.TemporalFunctionNames.SignalName(signalMethod)) + { + Args = workflowArgs, + SignalArgs = signalArgs, + ExecutionTimeout = options.ExecutionTimeout, + RunTimeout = options.RunTimeout, + TaskTimeout = options.TaskTimeout, + RequestId = options.RequestId, + IdReusePolicy = options.IdReusePolicy, + IdConflictPolicy = options.IdConflictPolicy, + RetryPolicy = options.RetryPolicy, + CronSchedule = options.CronSchedule, + Memo = options.Memo, + SearchAttributes = options.SearchAttributes, + Priority = options.Priority, + VersioningOverride = options.VersioningOverride, + StartDelay = options.StartDelay, + UserMetadata = options.StaticSummary != null || options.StaticDetails != null ? new UserMetadata() { StaticSummary = options.StaticSummary, StaticDetails = options.StaticDetails } : null, + }; + return SignalWithStartWorkflowAsync(request); + } + + private static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) + { + if (expression.Body is not MethodCallExpression call) + { + throw new ArgumentException("Expression must be a single method call", nameof(expression)); + } + var method = call.Method; + var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); + return (method, args); + } + + } + +} diff --git a/src/Temporalio/SystemNexus/Generated/Service.cs b/src/Temporalio/SystemNexus/Generated/Service.cs new file mode 100644 index 00000000..d016c69a --- /dev/null +++ b/src/Temporalio/SystemNexus/Generated/Service.cs @@ -0,0 +1,40 @@ +// +// Generated by nex-gen. DO NOT EDIT! +#nullable enable +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using NexusRpc; + +namespace NexGen.WorkflowService +{ + + [NexusService("temporal.api.workflowservice.v1.WorkflowService")] + internal interface IWorkflowService + { + /// + /// Signal a workflow, starting it first if needed. + /// + /// A workflow handle to the started workflow. + [NexusOperation("SignalWithStartWorkflowExecution")] + Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse SignalWithStartWorkflow(Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest request); + + } + + public static class NexGenOperationRegistry + { + internal static IReadOnlyDictionary Services { get; } = + new Dictionary + { + ["temporal.api.workflowservice.v1.WorkflowService"] = ServiceDefinition.FromType(), + }; + + public static IReadOnlyDictionary<(string Service, string Operation), OperationDefinition> Operations { get; } = + new Dictionary<(string Service, string Operation), OperationDefinition> + { + [("temporal.api.workflowservice.v1.WorkflowService", "SignalWithStartWorkflowExecution")] = Services["temporal.api.workflowservice.v1.WorkflowService"].Operations["SignalWithStartWorkflowExecution"], + }; + } + +} diff --git a/src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs b/src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs new file mode 100644 index 00000000..e2a5a27d --- /dev/null +++ b/src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs @@ -0,0 +1,282 @@ +// +// Generated by nex-gen. DO NOT EDIT! +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Google.Protobuf.WellKnownTypes; +using Temporalio.Common; +using Temporalio.Converters; +using Temporalio.Workflows; +using ApiCommon = Temporalio.Api.Common.V1; +using ApiDeployment = Temporalio.Api.Deployment.V1; +using ApiTaskQueue = Temporalio.Api.TaskQueue.V1; +using ApiWorkflow = Temporalio.Api.Workflow.V1; + +namespace NexGen.Support +{ + internal static class TemporalWorkflowContext + { + internal static string WorkflowNamespace() => Workflow.Info.Namespace; + } + + internal static class TemporalFunctionNames + { + internal static string WorkflowName(MethodInfo method) + { + if (method.GetCustomAttribute() == null) + { + throw new ArgumentException($"{method} missing WorkflowRun attribute"); + } + var definition = WorkflowDefinition.Create(method.ReflectedType ?? + throw new ArgumentException($"{method} has no reflected type")); + return definition.Name ?? + throw new ArgumentException( + $"{method} cannot be used directly since it is a dynamic workflow"); + } + + internal static string SignalName(MethodInfo method) + { + var definition = WorkflowSignalDefinition.FromMethod(method); + return definition.Name ?? + throw new ArgumentException( + $"{method} cannot be used directly since it is a dynamic signal"); + } + } + + internal static class ProtoExtensions + { + internal static ApiCommon.WorkflowType ToProto(this string value, ApiCommon.WorkflowType _) => + new() { Name = value }; + + internal static ApiTaskQueue.TaskQueue ToProto(this string value, ApiTaskQueue.TaskQueue _) => + new() { Name = value }; + + internal static ApiCommon.Payload ToProto(this object? value) => + Workflow.PayloadConverter.ToPayload(value); + + internal static ApiCommon.Payloads ToProto(this IEnumerable value) => + ToPayloads(value); + + internal static Duration ToProto(this TimeSpan value) => + Duration.FromTimeSpan(value); + + internal static ApiCommon.RetryPolicy ToProto(this Temporalio.Common.RetryPolicy value) => + ToRetryPolicy(value); + + internal static ApiCommon.Memo ToProto(this IReadOnlyDictionary value) => + ToMemo(value); + + internal static ApiCommon.SearchAttributes ToProto(this SearchAttributeCollection value) => + value.ToProto(); + + internal static ApiCommon.Priority ToProto(this Temporalio.Common.Priority value) => + ToPriority(value); + + internal static ApiWorkflow.VersioningOverride ToProto(this Temporalio.Common.VersioningOverride value) => + ToVersioningOverride(value); + + internal static string FromProto(this ApiCommon.WorkflowType proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Name; + } + + internal static string FromProto(this ApiTaskQueue.TaskQueue proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Name; + } + + internal static TimeSpan FromProto(this Duration proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.ToTimeSpan(); + } + + internal static object? FromProto(this ApiCommon.Payload proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return Workflow.PayloadConverter.ToValue(proto); + } + + internal static IReadOnlyCollection FromProto(this ApiCommon.Payloads proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return PayloadsToValues(proto); + } + + internal static Temporalio.Common.RetryPolicy FromProto(this ApiCommon.RetryPolicy proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return FromRetryPolicy(proto); + } + + internal static IReadOnlyDictionary FromProto(this ApiCommon.Memo proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return proto.Fields.ToDictionary(item => item.Key, item => Workflow.PayloadConverter.ToValue(item.Value)); + } + + internal static SearchAttributeCollection FromProto(this ApiCommon.SearchAttributes proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return SearchAttributeCollection.FromProto(proto); + } + + internal static Temporalio.Common.Priority FromProto(this ApiCommon.Priority proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + return new Temporalio.Common.Priority( + proto.PriorityKey == 0 ? null : proto.PriorityKey, + string.IsNullOrEmpty(proto.FairnessKey) ? null : proto.FairnessKey, + proto.FairnessWeight == 0f ? null : proto.FairnessWeight); + } + + internal static Temporalio.Common.VersioningOverride FromProto(this ApiWorkflow.VersioningOverride proto) + { + if (proto is null) + { + throw new ArgumentNullException(nameof(proto)); + } + if (proto.Pinned is { } pinned) + { + return new Temporalio.Common.VersioningOverride.Pinned( + new WorkerDeploymentVersion(pinned.Version.DeploymentName, pinned.Version.BuildId), + (Temporalio.Common.VersioningOverride.PinnedOverrideBehavior)pinned.Behavior); + } + if (proto.AutoUpgrade) + { + return new Temporalio.Common.VersioningOverride.AutoUpgrade(); + } + + throw new NotSupportedException("Unsupported versioning override proto"); + } + + private static ApiCommon.Payloads ToPayloads(IEnumerable values) + { + var payloads = new ApiCommon.Payloads(); + payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); + return payloads; + } + + private static IReadOnlyCollection PayloadsToValues(ApiCommon.Payloads payloads) => + payloads.Payloads_.Select(payload => Workflow.PayloadConverter.ToValue(payload)).ToArray(); + + private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy policy) + { + var proto = new ApiCommon.RetryPolicy + { + InitialInterval = Duration.FromTimeSpan(policy.InitialInterval), + BackoffCoefficient = policy.BackoffCoefficient, + MaximumAttempts = policy.MaximumAttempts, + }; + if (policy.MaximumInterval is { } maximumInterval) + { + proto.MaximumInterval = Duration.FromTimeSpan(maximumInterval); + } + if (policy.NonRetryableErrorTypes is { Count: > 0 } nonRetryableErrorTypes) + { + proto.NonRetryableErrorTypes.AddRange(nonRetryableErrorTypes); + } + return proto; + } + + private static Temporalio.Common.RetryPolicy FromRetryPolicy(ApiCommon.RetryPolicy proto) + { + var retryPolicy = new Temporalio.Common.RetryPolicy + { + BackoffCoefficient = (float)proto.BackoffCoefficient, + MaximumAttempts = proto.MaximumAttempts, + NonRetryableErrorTypes = proto.NonRetryableErrorTypes.ToArray(), + }; + if (proto.InitialInterval is { } initialInterval) + { + retryPolicy.InitialInterval = initialInterval.ToTimeSpan(); + } + if (proto.MaximumInterval is { } maximumInterval) + { + retryPolicy.MaximumInterval = maximumInterval.ToTimeSpan(); + } + return retryPolicy; + } + + private static ApiCommon.Memo ToMemo(IReadOnlyDictionary memo) + { + var proto = new ApiCommon.Memo(); + foreach (var item in memo) + { + if (item.Value == null) + { + throw new ArgumentException($"Memo value for {item.Key} is null", nameof(memo)); + } + proto.Fields.Add(item.Key, Workflow.PayloadConverter.ToPayload(item.Value)); + } + return proto; + } + + private static ApiCommon.Priority ToPriority(Temporalio.Common.Priority priority) => new() + { + PriorityKey = priority.PriorityKey ?? 0, + FairnessKey = priority.FairnessKey ?? string.Empty, + FairnessWeight = priority.FairnessWeight ?? 0f, + }; + + private static ApiWorkflow.VersioningOverride ToVersioningOverride(Temporalio.Common.VersioningOverride versioningOverride) => + versioningOverride switch + { + Temporalio.Common.VersioningOverride.Pinned pinned => new ApiWorkflow.VersioningOverride + { +#pragma warning disable CS0612 + Behavior = Temporalio.Api.Enums.V1.VersioningBehavior.Pinned, + PinnedVersion = pinned.Version.ToCanonicalString(), +#pragma warning restore CS0612 + Pinned = new ApiWorkflow.VersioningOverride.Types.PinnedOverride + { + Version = new ApiDeployment.WorkerDeploymentVersion + { + DeploymentName = pinned.Version.DeploymentName, + BuildId = pinned.Version.BuildId, + }, + Behavior = (ApiWorkflow.VersioningOverride.Types.PinnedOverrideBehavior)pinned.Behavior, + }, + }, + Temporalio.Common.VersioningOverride.AutoUpgrade _ => new ApiWorkflow.VersioningOverride + { +#pragma warning disable CS0612 + Behavior = Temporalio.Api.Enums.V1.VersioningBehavior.AutoUpgrade, +#pragma warning restore CS0612 + AutoUpgrade = true, + }, + _ => throw new ArgumentException("Unknown versioning override type", nameof(versioningOverride)), + }; + } +} diff --git a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs new file mode 100644 index 00000000..2d601dce --- /dev/null +++ b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs @@ -0,0 +1,216 @@ +// +// Generated by Temporalio.SystemNexus.Generator. DO NOT EDIT! +#nullable enable + +using System; +using System.Threading.Tasks; +using Google.Protobuf; +using Google.Protobuf.Collections; +using Temporalio.Api.Common.V1; +using Temporalio.Converters; +using NexGen.WorkflowService; + +namespace Temporalio.Worker +{ + internal static class SystemNexusPayloadVisitorRegistry + { + internal delegate Task PayloadVisitor(Payload payload); + internal delegate Task PayloadsVisitor(RepeatedField payloads); + internal delegate Task EnvelopeVisitor(Payload payload); + + private static readonly BinaryProtoConverter ProtoPayloadConverter = new(); + + internal static bool TryToInputPayload( + string service, + string operation, + object? value, + out Payload payload) + { + payload = null!; + if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation))) + { + return false; + } + + if (service == @"temporal.api.workflowservice.v1.WorkflowService" && operation == @"SignalWithStartWorkflowExecution") + { + if (value is not global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest typedValue) + { + return false; + } + + ProtoPayloadConverter.TryToPayload(typedValue, out var converted); + payload = converted!; + return true; + } + + return false; + } + + internal static Task TryVisitInputAsync( + string service, + string operation, + Payload payload, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope = null) => + TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope); + + internal static Task TryVisitOutputAsync( + string service, + string operation, + Payload payload, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope = null) => + TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope); + + private static async Task TryVisitAsync( + string service, + string operation, + bool input, + Payload payload, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope) + { + if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation))) + { + return false; + } + + if (service == @"temporal.api.workflowservice.v1.WorkflowService" && operation == @"SignalWithStartWorkflowExecution") + { + if (input) + { + await VisitEnvelopeAsync(payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false); + } + else + { + await VisitEnvelopeAsync(payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false); + } + + return true; + } + + return false; + } + + private static async Task VisitEnvelopeAsync( + Payload payload, + Func visitMessage, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope) + where T : IMessage, new() + { + BinaryProtoConverter.AssertProtoPayload(payload, typeof(T)); + var message = new T(); + message.MergeFrom(payload.Data); + await visitMessage(message, visitPayload, visitPayloads).ConfigureAwait(false); + payload.Metadata.Clear(); + payload.Metadata["encoding"] = ByteString.CopyFromUtf8("binary/protobuf"); + payload.Metadata["messageType"] = ByteString.CopyFromUtf8(message.Descriptor.FullName); + payload.Data = message.ToByteString(); + if (visitEnvelope != null) + { + await visitEnvelope(payload).ConfigureAwait(false); + } + } + + private static async Task Visit_temporal_api_common_v1_Payloads( + global::Temporalio.Api.Common.V1.Payloads value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + foreach (var payload_temporal_api_common_v1_Payloads_1 in value.Payloads_) + { + await visitPayload(payload_temporal_api_common_v1_Payloads_1).ConfigureAwait(false); + } + await Task.CompletedTask.ConfigureAwait(false); + } + + private static async Task Visit_temporal_api_common_v1_Memo( + global::Temporalio.Api.Common.V1.Memo value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + foreach (var item_temporal_api_common_v1_Memo_1 in value.Fields.Values) + { + if (item_temporal_api_common_v1_Memo_1 != null) + { + await visitPayload(item_temporal_api_common_v1_Memo_1).ConfigureAwait(false); + } + } + await Task.CompletedTask.ConfigureAwait(false); + } + + private static async Task Visit_temporal_api_common_v1_Header( + global::Temporalio.Api.Common.V1.Header value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + foreach (var item_temporal_api_common_v1_Header_1 in value.Fields.Values) + { + if (item_temporal_api_common_v1_Header_1 != null) + { + await visitPayload(item_temporal_api_common_v1_Header_1).ConfigureAwait(false); + } + } + await Task.CompletedTask.ConfigureAwait(false); + } + + private static async Task Visit_temporal_api_sdk_v1_UserMetadata( + global::Temporalio.Api.Sdk.V1.UserMetadata value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + if (value.Summary != null) + { + await visitPayload(value.Summary).ConfigureAwait(false); + } + if (value.Details != null) + { + await visitPayload(value.Details).ConfigureAwait(false); + } + await Task.CompletedTask.ConfigureAwait(false); + } + + private static async Task Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest( + global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + if (value.Input != null) + { + await visitPayloads(value.Input.Payloads_).ConfigureAwait(false); + } + if (value.SignalInput != null) + { + await visitPayloads(value.SignalInput.Payloads_).ConfigureAwait(false); + } + if (value.Memo != null) + { + await Visit_temporal_api_common_v1_Memo(value.Memo, visitPayload, visitPayloads).ConfigureAwait(false); + } + if (value.Header != null) + { + await Visit_temporal_api_common_v1_Header(value.Header, visitPayload, visitPayloads).ConfigureAwait(false); + } + if (value.UserMetadata != null) + { + await Visit_temporal_api_sdk_v1_UserMetadata(value.UserMetadata, visitPayload, visitPayloads).ConfigureAwait(false); + } + await Task.CompletedTask.ConfigureAwait(false); + } + + private static async Task Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse( + global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse value, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads) + { + await Task.CompletedTask.ConfigureAwait(false); + } + + } +} diff --git a/src/Temporalio/Temporalio.csproj b/src/Temporalio/Temporalio.csproj index 071f8fa4..817fe019 100644 --- a/src/Temporalio/Temporalio.csproj +++ b/src/Temporalio/Temporalio.csproj @@ -5,17 +5,6 @@ Temporal SDK for .NET true 9.0 - $(MSBuildProjectDirectory)/obj/$(Configuration)/$(TargetFramework)/SystemNexus/ - $(SystemNexusIntermediateDir)temporal_api.bin - $(SystemNexusIntermediateDir)generated/ - $(SystemNexusGeneratedDir)Operations.cs - $(SystemNexusIntermediateDir)generated.stamp - $(MSBuildProjectDirectory)/../Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj - 0.1.4 - $(NEX_GEN_BIN) - nex-gen - $(DotNetHostPath) - dotnet README.md true snupkg @@ -41,41 +30,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 565795f3d221e041a10f2b1854811fbd8d621fc5 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 17 Jun 2026 11:16:43 -0700 Subject: [PATCH 03/13] Use nex-gen annotations for workflow system Nexus --- .../Program.cs | 54 ------------------- .../wit/workflow-service.wit | 2 + .../SystemNexus/Generated/Models.cs | 2 +- .../SystemNexus/Generated/Operations.cs | 1 - .../SystemNexus/Generated/Service.cs | 2 +- .../SystemNexusPayloadVisitorRegistry.cs | 2 +- 6 files changed, 5 insertions(+), 58 deletions(-) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index 98b2ec74..b4c8784d 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -15,7 +15,6 @@ EnsureNexGen(); BuildDescriptor(); GenerateNexusApi(); -TransformOperationsFile(operationsPath); GeneratePayloadVisitorRegistry(operationsPath, descriptorPath); return 0; @@ -76,41 +75,6 @@ void GenerateNexusApi() }); } -static void TransformOperationsFile(string operationsPath) -{ - var source = File.ReadAllText(operationsPath); - var namespaceMatch = Regex.Match( - source, - @"namespace\s+(?[A-Za-z0-9_.]+)\s*\{", - RegexOptions.Multiline); - if (!namespaceMatch.Success) - { - throw new InvalidOperationException($"No generated namespace found in {operationsPath}"); - } - - var generatedNamespace = namespaceMatch.Groups["namespace"].Value; - source = EnsureUsing(source, generatedNamespace); - source = Regex.Replace( - source, - @"namespace\s+" + Regex.Escape(generatedNamespace) + @"\s*\{", - "namespace Temporalio.Workflows\n{", - RegexOptions.Multiline, - TimeSpan.FromSeconds(5)); - source = Regex.Replace( - source, - @"public\s+static\s+class\s+\w+Operations\b", - "public static partial class Workflow", - RegexOptions.Multiline, - TimeSpan.FromSeconds(5)); - - if (!source.Contains("public static partial class Workflow", StringComparison.Ordinal)) - { - throw new InvalidOperationException($"No generated operations class found in {operationsPath}"); - } - - File.WriteAllText(operationsPath, source); -} - static void GeneratePayloadVisitorRegistry(string operationsPath, string descriptorPath) { var generatedDir = Path.GetDirectoryName(operationsPath) ?? @@ -698,24 +662,6 @@ static string NormalizeTypeName(string typeName) => static string CsharpLiteral(string value) => "@\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; -static string EnsureUsing(string source, string namespaceName) -{ - if (Regex.IsMatch(source, @"using\s+" + Regex.Escape(namespaceName) + @"\s*;")) - { - return source; - } - - var lastUsing = Regex.Matches(source, @"^using\s+[A-Za-z0-9_.]+;\s*$", RegexOptions.Multiline) - .Cast() - .LastOrDefault(); - if (lastUsing == null) - { - throw new InvalidOperationException("No using directives found in generated operations file"); - } - - return source.Insert(lastUsing.Index + lastUsing.Length, Environment.NewLine + "using " + namespaceName + ";"); -} - internal sealed record OperationInfo(string Service, string Operation, string InputType, string OutputType); internal sealed record OperationMessages(OperationInfo Operation, MessageInfo Input, MessageInfo Output); diff --git a/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit b/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit index 3ff23004..a4547ab2 100644 --- a/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit +++ b/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit @@ -6,6 +6,8 @@ world system { /// @nexus.endpoint "__temporal_system" /// @nexus.service-name "temporal.api.workflowservice.v1.WorkflowService" +/// @nexus.namespace dotnet="Temporalio.Workflows" +/// @nexus.operations-class dotnet="Workflow" /// @nexus.delay-load-temporalio-workflow /// @nexus.experimental interface workflow-service { diff --git a/src/Temporalio/SystemNexus/Generated/Models.cs b/src/Temporalio/SystemNexus/Generated/Models.cs index 72aaafeb..a29256a7 100644 --- a/src/Temporalio/SystemNexus/Generated/Models.cs +++ b/src/Temporalio/SystemNexus/Generated/Models.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using NexGen.Support; -namespace NexGen.WorkflowService +namespace Temporalio.Workflows { /// diff --git a/src/Temporalio/SystemNexus/Generated/Operations.cs b/src/Temporalio/SystemNexus/Generated/Operations.cs index 4d85b814..6cf67a1a 100644 --- a/src/Temporalio/SystemNexus/Generated/Operations.cs +++ b/src/Temporalio/SystemNexus/Generated/Operations.cs @@ -13,7 +13,6 @@ using Temporalio.Converters; using Temporalio.Workflows; -using NexGen.WorkflowService; namespace Temporalio.Workflows { diff --git a/src/Temporalio/SystemNexus/Generated/Service.cs b/src/Temporalio/SystemNexus/Generated/Service.cs index d016c69a..89f82e97 100644 --- a/src/Temporalio/SystemNexus/Generated/Service.cs +++ b/src/Temporalio/SystemNexus/Generated/Service.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using NexusRpc; -namespace NexGen.WorkflowService +namespace Temporalio.Workflows { [NexusService("temporal.api.workflowservice.v1.WorkflowService")] diff --git a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs index 2d601dce..0936f4c4 100644 --- a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs +++ b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs @@ -8,7 +8,7 @@ using Google.Protobuf.Collections; using Temporalio.Api.Common.V1; using Temporalio.Converters; -using NexGen.WorkflowService; +using Temporalio.Workflows; namespace Temporalio.Worker { From 1845cbf8920e241fb5a7b09a4c494dc508028434 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 17 Jun 2026 11:45:38 -0700 Subject: [PATCH 04/13] Rename system Nexus payload visitor --- src/Temporalio.SystemNexus.Generator/Program.cs | 4 ++-- src/Temporalio/SystemNexus/Generated/Models.cs | 2 +- ...PayloadVisitorRegistry.cs => SystemNexusPayloadVisitor.cs} | 2 +- src/Temporalio/Worker/WorkflowCodecHelper.cs | 4 ++-- src/Temporalio/Worker/WorkflowInstance.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/Temporalio/SystemNexus/Generated/{SystemNexusPayloadVisitorRegistry.cs => SystemNexusPayloadVisitor.cs} (99%) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index b4c8784d..edd1bc46 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -124,7 +124,7 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip builder.AppendLine(); builder.AppendLine("namespace Temporalio.Worker"); builder.AppendLine("{"); - builder.AppendLine(" internal static class SystemNexusPayloadVisitorRegistry"); + builder.AppendLine(" internal static class SystemNexusPayloadVisitor"); builder.AppendLine(" {"); builder.AppendLine(" internal delegate Task PayloadVisitor(Payload payload);"); builder.AppendLine(" internal delegate Task PayloadsVisitor(RepeatedField payloads);"); @@ -258,7 +258,7 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip builder.AppendLine(" }"); builder.AppendLine("}"); - File.WriteAllText(Path.Combine(generatedDir, "SystemNexusPayloadVisitorRegistry.cs"), builder.ToString()); + File.WriteAllText(Path.Combine(generatedDir, "SystemNexusPayloadVisitor.cs"), builder.ToString()); } static List ParseOperations(string serviceSource) diff --git a/src/Temporalio/SystemNexus/Generated/Models.cs b/src/Temporalio/SystemNexus/Generated/Models.cs index a29256a7..ed4d0da1 100644 --- a/src/Temporalio/SystemNexus/Generated/Models.cs +++ b/src/Temporalio/SystemNexus/Generated/Models.cs @@ -178,7 +178,7 @@ public Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest } - public class UserMetadata + internal class UserMetadata { /// /// Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format. diff --git a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs similarity index 99% rename from src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs rename to src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs index 0936f4c4..100c78f7 100644 --- a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitorRegistry.cs +++ b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs @@ -12,7 +12,7 @@ namespace Temporalio.Worker { - internal static class SystemNexusPayloadVisitorRegistry + internal static class SystemNexusPayloadVisitor { internal delegate Task PayloadVisitor(Payload payload); internal delegate Task PayloadsVisitor(RepeatedField payloads); diff --git a/src/Temporalio/Worker/WorkflowCodecHelper.cs b/src/Temporalio/Worker/WorkflowCodecHelper.cs index 013f85b2..1c325cd8 100644 --- a/src/Temporalio/Worker/WorkflowCodecHelper.cs +++ b/src/Temporalio/Worker/WorkflowCodecHelper.cs @@ -128,7 +128,7 @@ await childCodec2.DecodeFailureAsync( var operationInfo = context.Instance?.GetPendingNexusOperationInfo( job.ResolveNexusOperation.Seq); if (operationInfo == null || - !await SystemNexusPayloadVisitorRegistry.TryVisitOutputAsync( + !await SystemNexusPayloadVisitor.TryVisitOutputAsync( operationInfo.Service, operationInfo.Operation, job.ResolveNexusOperation.Result.Completed, @@ -348,7 +348,7 @@ await EncodeAsync( codec = context.CodecNoContext; if (cmd.ScheduleNexusOperation.Input != null && codec != null) { - if (!await SystemNexusPayloadVisitorRegistry.TryVisitInputAsync( + if (!await SystemNexusPayloadVisitor.TryVisitInputAsync( cmd.ScheduleNexusOperation.Service, cmd.ScheduleNexusOperation.Operation, cmd.ScheduleNexusOperation.Input, diff --git a/src/Temporalio/Worker/WorkflowInstance.cs b/src/Temporalio/Worker/WorkflowInstance.cs index e675ae20..0f984a88 100644 --- a/src/Temporalio/Worker/WorkflowInstance.cs +++ b/src/Temporalio/Worker/WorkflowInstance.cs @@ -2585,7 +2585,7 @@ public override Task> ScheduleNexusOperati var payloadConverter = instance.payloadConverterNoContext; var seq = ++instance.nexusOperationCounter; - var inputPayload = SystemNexusPayloadVisitorRegistry.TryToInputPayload( + var inputPayload = SystemNexusPayloadVisitor.TryToInputPayload( input.Service, input.OperationName, input.Arg, From 53b4d37c223bbb5bb5db666a5d4e7a9a3b101526 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 17 Jun 2026 12:30:28 -0700 Subject: [PATCH 05/13] Use operation registry for system Nexus payload visiting --- .../Program.cs | 149 +++++------------- .../Generated/SystemNexusPayloadVisitor.cs | 112 +++---------- .../Worker/SystemNexusPayloadVisitor.cs | 85 ++++++++++ 3 files changed, 146 insertions(+), 200 deletions(-) create mode 100644 src/Temporalio/Worker/SystemNexusPayloadVisitor.cs diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index edd1bc46..2e7dc6fc 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -103,10 +103,10 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip message => message); var operationMessages = operations.Select(operation => new OperationMessages( - operation, GetMessage(messagesByCsharpType, operation.InputType), GetMessage(messagesByCsharpType, operation.OutputType))).ToList(); var containsPayloadMemo = new Dictionary(); + var emittedVisitors = new HashSet(); var emittedMethods = new HashSet(); var builder = new StringBuilder(); @@ -115,75 +115,26 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip builder.AppendLine("#nullable enable"); builder.AppendLine(); builder.AppendLine("using System;"); + builder.AppendLine("using System.Collections.Generic;"); builder.AppendLine("using System.Threading.Tasks;"); - builder.AppendLine("using Google.Protobuf;"); - builder.AppendLine("using Google.Protobuf.Collections;"); builder.AppendLine("using Temporalio.Api.Common.V1;"); - builder.AppendLine("using Temporalio.Converters;"); builder.AppendLine($"using {generatedNamespace};"); builder.AppendLine(); builder.AppendLine("namespace Temporalio.Worker"); builder.AppendLine("{"); - builder.AppendLine(" internal static class SystemNexusPayloadVisitor"); + builder.AppendLine(" internal static partial class SystemNexusPayloadVisitor"); builder.AppendLine(" {"); - builder.AppendLine(" internal delegate Task PayloadVisitor(Payload payload);"); - builder.AppendLine(" internal delegate Task PayloadsVisitor(RepeatedField payloads);"); - builder.AppendLine(" internal delegate Task EnvelopeVisitor(Payload payload);"); - builder.AppendLine(); - builder.AppendLine(" private static readonly BinaryProtoConverter ProtoPayloadConverter = new();"); - builder.AppendLine(); - builder.AppendLine(" internal static bool TryToInputPayload("); - builder.AppendLine(" string service,"); - builder.AppendLine(" string operation,"); - builder.AppendLine(" object? value,"); - builder.AppendLine(" out Payload payload)"); - builder.AppendLine(" {"); - builder.AppendLine(" payload = null!;"); - builder.AppendLine(" if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation)))"); + builder.AppendLine(" private static readonly IReadOnlyDictionary> EnvelopeVisitors ="); + builder.AppendLine(" new Dictionary>"); builder.AppendLine(" {"); - builder.AppendLine(" return false;"); - builder.AppendLine(" }"); - builder.AppendLine(); foreach (var operation in operationMessages) { - var serviceLiteral = CsharpLiteral(operation.Operation.Service); - var operationLiteral = CsharpLiteral(operation.Operation.Operation); - builder.AppendLine( - $" if (service == {serviceLiteral} && operation == {operationLiteral})"); - builder.AppendLine(" {"); - builder.AppendLine($" if (value is not {operation.Input.CsharpType} typedValue)"); - builder.AppendLine(" {"); - builder.AppendLine(" return false;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" ProtoPayloadConverter.TryToPayload(typedValue, out var converted);"); - builder.AppendLine(" payload = converted!;"); - builder.AppendLine(" return true;"); - builder.AppendLine(" }"); - builder.AppendLine(); + EmitEnvelopeVisitor(builder, operation.Input, emittedVisitors); + EmitEnvelopeVisitor(builder, operation.Output, emittedVisitors); } - builder.AppendLine(" return false;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" internal static Task TryVisitInputAsync("); - builder.AppendLine(" string service,"); - builder.AppendLine(" string operation,"); - builder.AppendLine(" Payload payload,"); - builder.AppendLine(" PayloadVisitor visitPayload,"); - builder.AppendLine(" PayloadsVisitor visitPayloads,"); - builder.AppendLine(" EnvelopeVisitor? visitEnvelope = null) =>"); - builder.AppendLine(" TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope);"); - builder.AppendLine(); - builder.AppendLine(" internal static Task TryVisitOutputAsync("); - builder.AppendLine(" string service,"); - builder.AppendLine(" string operation,"); - builder.AppendLine(" Payload payload,"); - builder.AppendLine(" PayloadVisitor visitPayload,"); - builder.AppendLine(" PayloadsVisitor visitPayloads,"); - builder.AppendLine(" EnvelopeVisitor? visitEnvelope = null) =>"); - builder.AppendLine(" TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope);"); + builder.AppendLine(" };"); builder.AppendLine(); builder.AppendLine(" private static async Task TryVisitAsync("); builder.AppendLine(" string service,"); @@ -194,58 +145,18 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip builder.AppendLine(" PayloadsVisitor visitPayloads,"); builder.AppendLine(" EnvelopeVisitor? visitEnvelope)"); builder.AppendLine(" {"); - builder.AppendLine(" if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation)))"); + builder.AppendLine(" if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition))"); builder.AppendLine(" {"); builder.AppendLine(" return false;"); builder.AppendLine(" }"); builder.AppendLine(); - - foreach (var operation in operationMessages) - { - var serviceLiteral = CsharpLiteral(operation.Operation.Service); - var operationLiteral = CsharpLiteral(operation.Operation.Operation); - builder.AppendLine( - $" if (service == {serviceLiteral} && operation == {operationLiteral})"); - builder.AppendLine(" {"); - builder.AppendLine(" if (input)"); - builder.AppendLine(" {"); - builder.AppendLine( - $" await VisitEnvelopeAsync<{operation.Input.CsharpType}>(payload, {VisitMethodName(operation.Input)}, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false);"); - builder.AppendLine(" }"); - builder.AppendLine(" else"); - builder.AppendLine(" {"); - builder.AppendLine( - $" await VisitEnvelopeAsync<{operation.Output.CsharpType}>(payload, {VisitMethodName(operation.Output)}, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false);"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" return true;"); - builder.AppendLine(" }"); - builder.AppendLine(); - } - - builder.AppendLine(" return false;"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" private static async Task VisitEnvelopeAsync("); - builder.AppendLine(" Payload payload,"); - builder.AppendLine(" Func visitMessage,"); - builder.AppendLine(" PayloadVisitor visitPayload,"); - builder.AppendLine(" PayloadsVisitor visitPayloads,"); - builder.AppendLine(" EnvelopeVisitor? visitEnvelope)"); - builder.AppendLine(" where T : IMessage, new()"); - builder.AppendLine(" {"); - builder.AppendLine(" BinaryProtoConverter.AssertProtoPayload(payload, typeof(T));"); - builder.AppendLine(" var message = new T();"); - builder.AppendLine(" message.MergeFrom(payload.Data);"); - builder.AppendLine(" await visitMessage(message, visitPayload, visitPayloads).ConfigureAwait(false);"); - builder.AppendLine(" payload.Metadata.Clear();"); - builder.AppendLine(" payload.Metadata[\"encoding\"] = ByteString.CopyFromUtf8(\"binary/protobuf\");"); - builder.AppendLine(" payload.Metadata[\"messageType\"] = ByteString.CopyFromUtf8(message.Descriptor.FullName);"); - builder.AppendLine(" payload.Data = message.ToByteString();"); - builder.AppendLine(" if (visitEnvelope != null)"); + builder.AppendLine(" if (!EnvelopeVisitors.TryGetValue(input ? definition.InputType : definition.OutputType, out var visit))"); builder.AppendLine(" {"); - builder.AppendLine(" await visitEnvelope(payload).ConfigureAwait(false);"); + builder.AppendLine(" return false;"); builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" await visit(payload, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false);"); + builder.AppendLine(" return true;"); builder.AppendLine(" }"); builder.AppendLine(); @@ -266,22 +177,19 @@ static List ParseOperations(string serviceSource) var operations = new List(); var serviceMatches = Regex.Matches( serviceSource, - @"\[NexusService\(""(?[^""]+)""\)\]\s+internal\s+interface\s+\w+\s*\{(?.*?)^\s*\}", + @"\[NexusService\(""[^""]+""\)\]\s+internal\s+interface\s+\w+\s*\{(?.*?)^\s*\}", RegexOptions.Multiline | RegexOptions.Singleline, TimeSpan.FromSeconds(5)); foreach (Match serviceMatch in serviceMatches) { - var service = serviceMatch.Groups["service"].Value; var operationMatches = Regex.Matches( serviceMatch.Groups["body"].Value, - @"\[NexusOperation\(""(?[^""]+)""\)\]\s+(?[A-Za-z0-9_.:]+)\s+\w+\((?[A-Za-z0-9_.:]+)\s+\w+\);", + @"\[NexusOperation\(""[^""]+""\)\]\s+(?[A-Za-z0-9_.:]+)\s+\w+\((?[A-Za-z0-9_.:]+)\s+\w+\);", RegexOptions.Multiline, TimeSpan.FromSeconds(5)); foreach (Match operationMatch in operationMatches) { operations.Add(new OperationInfo( - service, - operationMatch.Groups["operation"].Value, NormalizeTypeName(operationMatch.Groups["input"].Value), NormalizeTypeName(operationMatch.Groups["output"].Value))); } @@ -340,6 +248,25 @@ static MessageInfo GetMessage( return message; } +static void EmitEnvelopeVisitor( + StringBuilder builder, + MessageInfo message, + HashSet emittedVisitors) +{ + if (!emittedVisitors.Add(message.FullName)) + { + return; + } + + builder.AppendLine($" [typeof({message.CsharpType})] = (payload, visitPayload, visitPayloads, visitEnvelope) =>"); + builder.AppendLine($" VisitEnvelopeAsync<{message.CsharpType}>("); + builder.AppendLine(" payload,"); + builder.AppendLine($" {VisitMethodName(message)},"); + builder.AppendLine(" visitPayload,"); + builder.AppendLine(" visitPayloads,"); + builder.AppendLine(" visitEnvelope),"); +} + static void EmitVisitMethod( StringBuilder builder, MessageInfo message, @@ -660,10 +587,8 @@ static string UniqueLocalName(MessageInfo message, FieldDescriptorProto field, s static string NormalizeTypeName(string typeName) => typeName.StartsWith("global::", StringComparison.Ordinal) ? typeName["global::".Length..] : typeName; -static string CsharpLiteral(string value) => "@\"" + value.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; - -internal sealed record OperationInfo(string Service, string Operation, string InputType, string OutputType); +internal sealed record OperationInfo(string InputType, string OutputType); -internal sealed record OperationMessages(OperationInfo Operation, MessageInfo Input, MessageInfo Output); +internal sealed record OperationMessages(MessageInfo Input, MessageInfo Output); internal sealed record MessageInfo(string FullName, string CsharpType, DescriptorProto Descriptor); diff --git a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs index 100c78f7..23487c47 100644 --- a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs +++ b/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs @@ -3,67 +3,33 @@ #nullable enable using System; +using System.Collections.Generic; using System.Threading.Tasks; -using Google.Protobuf; -using Google.Protobuf.Collections; using Temporalio.Api.Common.V1; -using Temporalio.Converters; using Temporalio.Workflows; namespace Temporalio.Worker { - internal static class SystemNexusPayloadVisitor + internal static partial class SystemNexusPayloadVisitor { - internal delegate Task PayloadVisitor(Payload payload); - internal delegate Task PayloadsVisitor(RepeatedField payloads); - internal delegate Task EnvelopeVisitor(Payload payload); - - private static readonly BinaryProtoConverter ProtoPayloadConverter = new(); - - internal static bool TryToInputPayload( - string service, - string operation, - object? value, - out Payload payload) - { - payload = null!; - if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation))) + private static readonly IReadOnlyDictionary> EnvelopeVisitors = + new Dictionary> { - return false; - } - - if (service == @"temporal.api.workflowservice.v1.WorkflowService" && operation == @"SignalWithStartWorkflowExecution") - { - if (value is not global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest typedValue) - { - return false; - } - - ProtoPayloadConverter.TryToPayload(typedValue, out var converted); - payload = converted!; - return true; - } - - return false; - } - - internal static Task TryVisitInputAsync( - string service, - string operation, - Payload payload, - PayloadVisitor visitPayload, - PayloadsVisitor visitPayloads, - EnvelopeVisitor? visitEnvelope = null) => - TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope); - - internal static Task TryVisitOutputAsync( - string service, - string operation, - Payload payload, - PayloadVisitor visitPayload, - PayloadsVisitor visitPayloads, - EnvelopeVisitor? visitEnvelope = null) => - TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope); + [typeof(global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest)] = (payload, visitPayload, visitPayloads, visitEnvelope) => + VisitEnvelopeAsync( + payload, + Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest, + visitPayload, + visitPayloads, + visitEnvelope), + [typeof(global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse)] = (payload, visitPayload, visitPayloads, visitEnvelope) => + VisitEnvelopeAsync( + payload, + Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse, + visitPayload, + visitPayloads, + visitEnvelope), + }; private static async Task TryVisitAsync( string service, @@ -74,48 +40,18 @@ private static async Task TryVisitAsync( PayloadsVisitor visitPayloads, EnvelopeVisitor? visitEnvelope) { - if (!NexGenOperationRegistry.Operations.ContainsKey((service, operation))) + if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition)) { return false; } - if (service == @"temporal.api.workflowservice.v1.WorkflowService" && operation == @"SignalWithStartWorkflowExecution") + if (!EnvelopeVisitors.TryGetValue(input ? definition.InputType : definition.OutputType, out var visit)) { - if (input) - { - await VisitEnvelopeAsync(payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false); - } - else - { - await VisitEnvelopeAsync(payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false); - } - - return true; + return false; } - return false; - } - - private static async Task VisitEnvelopeAsync( - Payload payload, - Func visitMessage, - PayloadVisitor visitPayload, - PayloadsVisitor visitPayloads, - EnvelopeVisitor? visitEnvelope) - where T : IMessage, new() - { - BinaryProtoConverter.AssertProtoPayload(payload, typeof(T)); - var message = new T(); - message.MergeFrom(payload.Data); - await visitMessage(message, visitPayload, visitPayloads).ConfigureAwait(false); - payload.Metadata.Clear(); - payload.Metadata["encoding"] = ByteString.CopyFromUtf8("binary/protobuf"); - payload.Metadata["messageType"] = ByteString.CopyFromUtf8(message.Descriptor.FullName); - payload.Data = message.ToByteString(); - if (visitEnvelope != null) - { - await visitEnvelope(payload).ConfigureAwait(false); - } + await visit(payload, visitPayload, visitPayloads, visitEnvelope).ConfigureAwait(false); + return true; } private static async Task Visit_temporal_api_common_v1_Payloads( diff --git a/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs b/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs new file mode 100644 index 00000000..0f1e139e --- /dev/null +++ b/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs @@ -0,0 +1,85 @@ +#pragma warning disable SA1600 // Internal implementation plumbing. + +using System; +using System.Threading.Tasks; +using Google.Protobuf; +using Google.Protobuf.Collections; +using Temporalio.Api.Common.V1; +using Temporalio.Converters; +using Temporalio.Workflows; + +namespace Temporalio.Worker +{ + internal static partial class SystemNexusPayloadVisitor + { + private static readonly BinaryProtoConverter ProtoPayloadConverter = new(); + + internal delegate Task PayloadVisitor(Payload payload); + + internal delegate Task PayloadsVisitor(RepeatedField payloads); + + internal delegate Task EnvelopeVisitor(Payload payload); + + internal static bool TryToInputPayload( + string service, + string operation, + object? value, + out Payload payload) + { + payload = null!; + if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition) || + !definition.InputType.IsInstanceOfType(value)) + { + return false; + } + + if (!ProtoPayloadConverter.TryToPayload(value, out var converted) || converted == null) + { + return false; + } + + payload = converted; + return true; + } + + internal static Task TryVisitInputAsync( + string service, + string operation, + Payload payload, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope = null) => + TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope); + + internal static Task TryVisitOutputAsync( + string service, + string operation, + Payload payload, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope = null) => + TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope); + + private static async Task VisitEnvelopeAsync( + Payload payload, + Func visitMessage, + PayloadVisitor visitPayload, + PayloadsVisitor visitPayloads, + EnvelopeVisitor? visitEnvelope) + where T : IMessage, new() + { + BinaryProtoConverter.AssertProtoPayload(payload, typeof(T)); + var message = new T(); + message.MergeFrom(payload.Data); + await visitMessage(message, visitPayload, visitPayloads).ConfigureAwait(false); + payload.Metadata.Clear(); + payload.Metadata["encoding"] = ByteString.CopyFromUtf8("binary/protobuf"); + payload.Metadata["messageType"] = ByteString.CopyFromUtf8(message.Descriptor.FullName); + payload.Data = message.ToByteString(); + if (visitEnvelope != null) + { + await visitEnvelope(payload).ConfigureAwait(false); + } + } + } +} From d9fd783e2f393330b69d3c8907bcd183e90bd20d Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Mon, 22 Jun 2026 08:20:17 -0700 Subject: [PATCH 06/13] Pass system Nexus support as generator input --- src/Temporalio.SystemNexus.Generator/Program.cs | 2 ++ .../wit/deps/nexus-temporal-types/model.wit | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index 2e7dc6fc..a71e600f 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -68,6 +68,8 @@ void GenerateNexusApi() Path.Join(generatorDir, "wit/workflow-service.wit"), "--input", Path.Join(generatorDir, "wit/deps"), + "--support-file", + Path.Join(generatorDir, "wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs"), "--descriptors", descriptorPath, "--output", diff --git a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit index d995d038..73905570 100644 --- a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit +++ b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit @@ -1,6 +1,3 @@ -/// @nexus.support -/// dotnet="dotnet/TemporalSupport.cs" -/// @nexus.support-namespace dotnet="NexGen.Support" package nexus:temporal-types@1.0.0; interface model { From c7ad99f1d8477ecdadc2757d398c7f0edf39f876 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Mon, 22 Jun 2026 08:42:06 -0700 Subject: [PATCH 07/13] Exclude delegates from cloneable type test --- tests/Temporalio.Tests/GeneralTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Temporalio.Tests/GeneralTests.cs b/tests/Temporalio.Tests/GeneralTests.cs index 596342be..1852d9a8 100644 --- a/tests/Temporalio.Tests/GeneralTests.cs +++ b/tests/Temporalio.Tests/GeneralTests.cs @@ -21,6 +21,7 @@ t.Namespace is { } ns && ns.StartsWith("Temporalio.") && !ns.StartsWith("Temporalio.Api.") && !ns.StartsWith("Temporalio.Bridge.") && + !typeof(Delegate).IsAssignableFrom(t) && t.GetMethod("Clone", BindingFlags.Public | BindingFlags.Instance) != null)); // Ensure at least one we know of is there Assert.Contains(typeof(Temporalio.Client.Schedules.ScheduleListOptions), types); @@ -43,4 +44,4 @@ t.Namespace is { } ns && Assert.IsType(type, clone); } } -} \ No newline at end of file +} From bf284ae62f98673895549605d05cada82064af9d Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Mon, 22 Jun 2026 15:27:21 -0700 Subject: [PATCH 08/13] Regenerate system Nexus output layout --- .github/workflows/ci.yml | 4 + .../Program.cs | 99 +++++++++-- .../Temporalio.SystemNexus.Generator.csproj | 3 - .../dotnet/TemporalSupport.cs | 162 +++-------------- .../wit/deps/nexus-temporal-types/model.wit | 22 ++- .../Generated/SystemNexusPayloadVisitor.cs | 20 +-- .../Generated/Models.cs | 23 ++- .../Generated/Operations.cs | 30 ++-- .../Generated/Service.cs | 2 +- .../Generated}/TemporalSupport.cs | 165 +++--------------- 10 files changed, 174 insertions(+), 356 deletions(-) rename src/Temporalio/{SystemNexus => Worker}/Generated/SystemNexusPayloadVisitor.cs (85%) rename src/Temporalio/{SystemNexus => Workflows}/Generated/Models.cs (91%) rename src/Temporalio/{SystemNexus => Workflows}/Generated/Operations.cs (90%) rename src/Temporalio/{SystemNexus => Workflows}/Generated/Service.cs (96%) rename src/Temporalio/{SystemNexus/Generated/Support => Workflows/Generated}/TemporalSupport.cs (52%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b537de30..9d9d20b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,10 @@ jobs: version: "23.x" repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install nex-gen + if: ${{ matrix.checkTarget }} + run: cargo install --locked --version 0.1.4 --force nex-gen + - name: Regen confirm unchanged if: ${{ matrix.checkTarget }} run: | diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index a71e600f..5cea6411 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -9,19 +9,24 @@ var protoDir = Path.Join(projectDir, "src/Temporalio/Bridge/sdk-core/crates/protos/protos"); var apiProtoDir = Path.Join(protoDir, "api_upstream"); var descriptorPath = Path.Join(generatorDir, "obj/SystemNexus/temporal_api.bin"); -var outputDir = Path.Join(projectDir, "src/Temporalio/SystemNexus/Generated"); -var operationsPath = Path.Join(outputDir, "Operations.cs"); +var stagingOutputDir = Path.Join(generatorDir, "obj/SystemNexus/Generated"); +var workflowsGeneratedDir = Path.Join(projectDir, "src/Temporalio/Workflows/Generated"); +var workerGeneratedDir = Path.Join(projectDir, "src/Temporalio/Worker/Generated"); +var obsoleteOutputDir = Path.Join(projectDir, "src/Temporalio/SystemNexus/Generated"); +var operationsPath = Path.Join(workflowsGeneratedDir, "Operations.cs"); EnsureNexGen(); BuildDescriptor(); GenerateNexusApi(); -GeneratePayloadVisitorRegistry(operationsPath, descriptorPath); +PostProcessGeneratedNexusApi(); +GeneratePayloadVisitorRegistry(operationsPath, descriptorPath, workerGeneratedDir); return 0; void EnsureNexGen() { var nexGenCommand = NexGenCommand(); - if (RunProcess(nexGenCommand, new[] { "help" }, ignoreExitCode: true) == 0) + var helpArgs = new[] { "help" }; + if (RunProcess(nexGenCommand, helpArgs, ignoreExitCode: true) == 0) { return; } @@ -31,7 +36,8 @@ void EnsureNexGen() throw new InvalidOperationException($"Unable to run nex-gen command {nexGenCommand}"); } - RunProcess("cargo", new[] { "install", "--locked", "nex-gen", "--force" }); + var installArgs = new[] { "install", "--locked", "nex-gen", "--force" }; + RunProcess("cargo", installArgs); } void BuildDescriptor() @@ -51,12 +57,12 @@ void BuildDescriptor() void GenerateNexusApi() { - if (Directory.Exists(outputDir)) + if (Directory.Exists(stagingOutputDir)) { - new DirectoryInfo(outputDir).Delete(true); + new DirectoryInfo(stagingOutputDir).Delete(true); } - Directory.CreateDirectory(outputDir); + Directory.CreateDirectory(stagingOutputDir); RunProcess( NexGenCommand(), new[] @@ -73,11 +79,54 @@ void GenerateNexusApi() "--descriptors", descriptorPath, "--output", - outputDir, + stagingOutputDir, }); } -static void GeneratePayloadVisitorRegistry(string operationsPath, string descriptorPath) +void PostProcessGeneratedNexusApi() +{ + RecreateDirectory(workflowsGeneratedDir); + RecreateDirectory(workerGeneratedDir); + + if (Directory.Exists(obsoleteOutputDir)) + { + new DirectoryInfo(obsoleteOutputDir).Delete(true); + } + + WriteWorkflowGeneratedFile("Models.cs"); + WriteWorkflowGeneratedFile("Operations.cs"); + WriteWorkflowGeneratedFile("Service.cs"); + WriteWorkflowGeneratedFile(Path.Join("Support", "TemporalSupport.cs"), "TemporalSupport.cs"); +} + +void WriteWorkflowGeneratedFile(string stagingRelativePath, string? outputFileName = null) +{ + var sourcePath = Path.Join(stagingOutputDir, stagingRelativePath); + var destinationPath = Path.Join( + workflowsGeneratedDir, + outputFileName ?? Path.GetFileName(stagingRelativePath)); + var contents = File.ReadAllText(sourcePath); + contents = contents.Replace("using NexGen.Support;\n", string.Empty); + contents = contents.Replace("using Temporalio.Workflows;\n", string.Empty); + contents = contents.Replace("namespace NexGen.Support", "namespace Temporalio.Workflows"); + contents = contents.Replace("NexGen.Support.", string.Empty); + File.WriteAllText(destinationPath, contents); +} + +static void RecreateDirectory(string path) +{ + if (Directory.Exists(path)) + { + new DirectoryInfo(path).Delete(true); + } + + Directory.CreateDirectory(path); +} + +static void GeneratePayloadVisitorRegistry( + string operationsPath, + string descriptorPath, + string outputDir) { var generatedDir = Path.GetDirectoryName(operationsPath) ?? throw new InvalidOperationException($"No directory for {operationsPath}"); @@ -171,7 +220,7 @@ static void GeneratePayloadVisitorRegistry(string operationsPath, string descrip builder.AppendLine(" }"); builder.AppendLine("}"); - File.WriteAllText(Path.Combine(generatedDir, "SystemNexusPayloadVisitor.cs"), builder.ToString()); + File.WriteAllText(Path.Combine(outputDir, "SystemNexusPayloadVisitor.cs"), builder.ToString()); } static List ParseOperations(string serviceSource) @@ -286,17 +335,29 @@ static void EmitVisitMethod( EmitVisitMethod(builder, referenced, messages, containsPayloadMemo, emittedMethods); } - builder.AppendLine($" private static async Task {VisitMethodName(message)}("); + var fieldLines = VisitFieldLines(message, messages, containsPayloadMemo).ToList(); + var hasAwaits = fieldLines.Any(line => line.Contains("await ", StringComparison.Ordinal)); + builder.Append(" private static "); + if (hasAwaits) + { + builder.Append("async "); + } + + builder.AppendLine($"Task {VisitMethodName(message)}("); builder.AppendLine($" {message.CsharpType} value,"); builder.AppendLine(" PayloadVisitor visitPayload,"); builder.AppendLine(" PayloadsVisitor visitPayloads)"); builder.AppendLine(" {"); - foreach (var line in VisitFieldLines(message, messages, containsPayloadMemo)) + foreach (var line in fieldLines) { builder.AppendLine($" {line}"); } - builder.AppendLine(" await Task.CompletedTask.ConfigureAwait(false);"); + if (!hasAwaits) + { + builder.AppendLine(" return Task.CompletedTask;"); + } + builder.AppendLine(" }"); builder.AppendLine(); } @@ -322,6 +383,7 @@ static IEnumerable FieldReferencedMessagesWithPayloads( { if (!TryGetMessage(field.TypeName, messages, out var fieldMessage) || IsPayload(fieldMessage) || + IsPayloads(fieldMessage) || IsSearchAttributes(fieldMessage)) { yield break; @@ -556,11 +618,12 @@ static string PropertyName(string protoName, string messageName) static int RunProcess(string fileName, IEnumerable arguments, bool ignoreExitCode = false) { - var process = new Process + using var process = new Process { StartInfo = { FileName = fileName, + RedirectStandardError = true, UseShellExecute = false, }, }; @@ -571,7 +634,13 @@ static int RunProcess(string fileName, IEnumerable arguments, bool ignor Console.WriteLine("Running {0} {1}", fileName, string.Join(" ", arguments)); process.Start(); + var stderr = process.StandardError.ReadToEnd(); process.WaitForExit(); + if (stderr.Length > 0) + { + Console.Error.Write(stderr); + } + if (!ignoreExitCode && process.ExitCode != 0) { throw new InvalidOperationException($"{fileName} failed with exit code {process.ExitCode}"); diff --git a/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj index 0200095c..0bbbdf05 100644 --- a/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj +++ b/src/Temporalio.SystemNexus.Generator/Temporalio.SystemNexus.Generator.csproj @@ -3,14 +3,11 @@ Exe net10.0 - false - false false enable false enable - false diff --git a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs index c30d26fa..715c346e 100644 --- a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs +++ b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using Google.Protobuf.WellKnownTypes; using Temporalio.Common; @@ -20,6 +21,17 @@ internal static class TemporalWorkflowContext internal static class TemporalFunctionNames { + internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) + { + if (expression.Body is not MethodCallExpression call) + { + throw new ArgumentException("Expression must be a single method call", nameof(expression)); + } + var method = call.Method; + var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); + return (method, args); + } + internal static string WorkflowName(MethodInfo method) { if (method.GetCustomAttribute() == null) @@ -44,17 +56,21 @@ internal static string SignalName(MethodInfo method) internal static class ProtoExtensions { - internal static ApiCommon.WorkflowType ToProto(this string value, ApiCommon.WorkflowType _) => + internal static ApiCommon.WorkflowType ToWorkflowTypeProto(this string value) => new() { Name = value }; - internal static ApiTaskQueue.TaskQueue ToProto(this string value, ApiTaskQueue.TaskQueue _) => + internal static ApiTaskQueue.TaskQueue ToTaskQueueProto(this string value) => new() { Name = value }; - internal static ApiCommon.Payload ToProto(this object? value) => + internal static ApiCommon.Payload ToPayload(object? value) => Workflow.PayloadConverter.ToPayload(value); - internal static ApiCommon.Payloads ToProto(this IEnumerable value) => - ToPayloads(value); + internal static ApiCommon.Payloads ToPayloads(IEnumerable values) + { + var payloads = new ApiCommon.Payloads(); + payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); + return payloads; + } internal static Duration ToProto(this TimeSpan value) => Duration.FromTimeSpan(value); @@ -65,129 +81,12 @@ internal static ApiCommon.RetryPolicy ToProto(this Temporalio.Common.RetryPolicy internal static ApiCommon.Memo ToProto(this IReadOnlyDictionary value) => ToMemo(value); - internal static ApiCommon.SearchAttributes ToProto(this SearchAttributeCollection value) => - value.ToProto(); - internal static ApiCommon.Priority ToProto(this Temporalio.Common.Priority value) => ToPriority(value); internal static ApiWorkflow.VersioningOverride ToProto(this Temporalio.Common.VersioningOverride value) => ToVersioningOverride(value); - internal static string FromProto(this ApiCommon.WorkflowType proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Name; - } - - internal static string FromProto(this ApiTaskQueue.TaskQueue proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Name; - } - - internal static TimeSpan FromProto(this Duration proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.ToTimeSpan(); - } - - internal static object? FromProto(this ApiCommon.Payload proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return Workflow.PayloadConverter.ToValue(proto); - } - - internal static IReadOnlyCollection FromProto(this ApiCommon.Payloads proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return PayloadsToValues(proto); - } - - internal static Temporalio.Common.RetryPolicy FromProto(this ApiCommon.RetryPolicy proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return FromRetryPolicy(proto); - } - - internal static IReadOnlyDictionary FromProto(this ApiCommon.Memo proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Fields.ToDictionary(item => item.Key, item => Workflow.PayloadConverter.ToValue(item.Value)); - } - - internal static SearchAttributeCollection FromProto(this ApiCommon.SearchAttributes proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return SearchAttributeCollection.FromProto(proto); - } - - internal static Temporalio.Common.Priority FromProto(this ApiCommon.Priority proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return new Temporalio.Common.Priority( - proto.PriorityKey == 0 ? null : proto.PriorityKey, - string.IsNullOrEmpty(proto.FairnessKey) ? null : proto.FairnessKey, - proto.FairnessWeight == 0f ? null : proto.FairnessWeight); - } - - internal static Temporalio.Common.VersioningOverride FromProto(this ApiWorkflow.VersioningOverride proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - if (proto.Pinned is { } pinned) - { - return new Temporalio.Common.VersioningOverride.Pinned( - new WorkerDeploymentVersion(pinned.Version.DeploymentName, pinned.Version.BuildId), - (Temporalio.Common.VersioningOverride.PinnedOverrideBehavior)pinned.Behavior); - } - if (proto.AutoUpgrade) - { - return new Temporalio.Common.VersioningOverride.AutoUpgrade(); - } - - throw new NotSupportedException("Unsupported versioning override proto"); - } - - private static ApiCommon.Payloads ToPayloads(IEnumerable values) - { - var payloads = new ApiCommon.Payloads(); - payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); - return payloads; - } - - private static IReadOnlyCollection PayloadsToValues(ApiCommon.Payloads payloads) => - payloads.Payloads_.Select(payload => Workflow.PayloadConverter.ToValue(payload)).ToArray(); - private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy policy) { var proto = new ApiCommon.RetryPolicy @@ -207,25 +106,6 @@ private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy return proto; } - private static Temporalio.Common.RetryPolicy FromRetryPolicy(ApiCommon.RetryPolicy proto) - { - var retryPolicy = new Temporalio.Common.RetryPolicy - { - BackoffCoefficient = (float)proto.BackoffCoefficient, - MaximumAttempts = proto.MaximumAttempts, - NonRetryableErrorTypes = proto.NonRetryableErrorTypes.ToArray(), - }; - if (proto.InitialInterval is { } initialInterval) - { - retryPolicy.InitialInterval = initialInterval.ToTimeSpan(); - } - if (proto.MaximumInterval is { } maximumInterval) - { - retryPolicy.MaximumInterval = maximumInterval.ToTimeSpan(); - } - return retryPolicy; - } - private static ApiCommon.Memo ToMemo(IReadOnlyDictionary memo) { var proto = new ApiCommon.Memo(); diff --git a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit index 73905570..8d771b78 100644 --- a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit +++ b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit @@ -5,11 +5,17 @@ interface model { type placeholder = string; /// @nexus.proto "temporal.api.common.v1.Payload" typescript-package="@temporalio/proto" - /// @nexus.type python="typing.Any" typescript="common.Payload" dotnet="object?" typescript-package="@temporalio/common" + /// @nexus.type + /// python="typing.Any" + /// typescript="common.Payload" + /// dotnet="object?" + /// dotnet-to="ProtoExtensions.ToPayload" + /// typescript-package="@temporalio/common" type payload = placeholder; /// @nexus.proto "temporal.api.common.v1.Payloads" /// typescript-package="@temporalio/proto" + /// @nexus.type dotnet="IReadOnlyCollection" dotnet-to="ProtoExtensions.ToPayloads" type payloads = list; /// Callable result annotation for workflow functions. @@ -40,7 +46,11 @@ interface model { signal-call: func(callable-prefix: callable-prefix, signal-args: payloads) -> signal-result; /// @nexus.proto "temporal.api.common.v1.WorkflowType" typescript-package="@temporalio/proto" - /// @nexus.type python="str" typescript="string" dotnet="string" + /// @nexus.type + /// python="str" + /// typescript="string" + /// dotnet="string" + /// dotnet-to="ProtoExtensions.ToWorkflowTypeProto" type workflow-type = placeholder; /// @nexus.function @@ -50,6 +60,7 @@ interface model { /// result-type-parameter="WorkflowResult" /// alternate-type="workflow-type" /// dotnet-name-extractor="TemporalFunctionNames.WorkflowName" + /// dotnet-call-extractor="TemporalFunctionNames.ExtractCall" /// @nexus.add-rpc-compatible-with "workflow-type" type workflow-function = placeholder; @@ -60,6 +71,7 @@ interface model { /// python-converter="signal_function_to_proto" /// typescript-converter="signalFunctionToProto" /// dotnet-name-extractor="TemporalFunctionNames.SignalName" + /// dotnet-call-extractor="TemporalFunctionNames.ExtractCall" /// @nexus.add-rpc-compatible-with "string" /// @nexus.typescript-with-arguments /// signature="signal-call" @@ -80,7 +92,11 @@ interface model { type retry-policy = placeholder; /// @nexus.proto "temporal.api.taskqueue.v1.TaskQueue" typescript-package="@temporalio/proto" - /// @nexus.type python="str" typescript="string" dotnet="string" + /// @nexus.type + /// python="str" + /// typescript="string" + /// dotnet="string" + /// dotnet-to="ProtoExtensions.ToTaskQueueProto" type task-queue = placeholder; /// @nexus.proto "temporal.api.common.v1.Memo" typescript-package="@temporalio/proto" diff --git a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs similarity index 85% rename from src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs rename to src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs index 23487c47..11d12fc6 100644 --- a/src/Temporalio/SystemNexus/Generated/SystemNexusPayloadVisitor.cs +++ b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs @@ -54,18 +54,6 @@ private static async Task TryVisitAsync( return true; } - private static async Task Visit_temporal_api_common_v1_Payloads( - global::Temporalio.Api.Common.V1.Payloads value, - PayloadVisitor visitPayload, - PayloadsVisitor visitPayloads) - { - foreach (var payload_temporal_api_common_v1_Payloads_1 in value.Payloads_) - { - await visitPayload(payload_temporal_api_common_v1_Payloads_1).ConfigureAwait(false); - } - await Task.CompletedTask.ConfigureAwait(false); - } - private static async Task Visit_temporal_api_common_v1_Memo( global::Temporalio.Api.Common.V1.Memo value, PayloadVisitor visitPayload, @@ -78,7 +66,6 @@ private static async Task Visit_temporal_api_common_v1_Memo( await visitPayload(item_temporal_api_common_v1_Memo_1).ConfigureAwait(false); } } - await Task.CompletedTask.ConfigureAwait(false); } private static async Task Visit_temporal_api_common_v1_Header( @@ -93,7 +80,6 @@ private static async Task Visit_temporal_api_common_v1_Header( await visitPayload(item_temporal_api_common_v1_Header_1).ConfigureAwait(false); } } - await Task.CompletedTask.ConfigureAwait(false); } private static async Task Visit_temporal_api_sdk_v1_UserMetadata( @@ -109,7 +95,6 @@ private static async Task Visit_temporal_api_sdk_v1_UserMetadata( { await visitPayload(value.Details).ConfigureAwait(false); } - await Task.CompletedTask.ConfigureAwait(false); } private static async Task Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest( @@ -137,15 +122,14 @@ private static async Task Visit_temporal_api_workflowservice_v1_SignalWithStartW { await Visit_temporal_api_sdk_v1_UserMetadata(value.UserMetadata, visitPayload, visitPayloads).ConfigureAwait(false); } - await Task.CompletedTask.ConfigureAwait(false); } - private static async Task Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse( + private static Task Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse( global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse value, PayloadVisitor visitPayload, PayloadsVisitor visitPayloads) { - await Task.CompletedTask.ConfigureAwait(false); + return Task.CompletedTask; } } diff --git a/src/Temporalio/SystemNexus/Generated/Models.cs b/src/Temporalio/Workflows/Generated/Models.cs similarity index 91% rename from src/Temporalio/SystemNexus/Generated/Models.cs rename to src/Temporalio/Workflows/Generated/Models.cs index ed4d0da1..2e3983c6 100644 --- a/src/Temporalio/SystemNexus/Generated/Models.cs +++ b/src/Temporalio/Workflows/Generated/Models.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using NexGen.Support; namespace Temporalio.Workflows { @@ -26,7 +25,7 @@ internal SignalWithStartWorkflowRequest(string workflow, string id, string taskQ /// /// Workflow type name or workflow expression identifying the workflow to start. /// - public string Workflow { get; init; } + public string Workflow { get; } /// /// Arguments for the workflow. /// @@ -34,15 +33,15 @@ internal SignalWithStartWorkflowRequest(string workflow, string id, string taskQ /// /// Unique identifier for the workflow execution. /// - public string Id { get; init; } + public string Id { get; } /// /// Task queue to run the workflow on. /// - public string TaskQueue { get; init; } + public string TaskQueue { get; } /// /// Signal name or signal expression to send with the start request. /// - public string Signal { get; init; } + public string Signal { get; } /// /// Arguments for the signal. /// @@ -104,18 +103,18 @@ internal SignalWithStartWorkflowRequest(string workflow, string id, string taskQ public Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest ToProto() { var proto = new Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest(); - proto.Namespace = NexGen.Support.TemporalWorkflowContext.WorkflowNamespace(); - proto.WorkflowType = Workflow.ToProto(default(Temporalio.Api.Common.V1.WorkflowType)!); + proto.Namespace = TemporalWorkflowContext.WorkflowNamespace(); + proto.WorkflowType = ProtoExtensions.ToWorkflowTypeProto(Workflow); if (Args is { } args) { - proto.Input = args.ToProto(); + proto.Input = ProtoExtensions.ToPayloads(args); } proto.WorkflowId = Id; - proto.TaskQueue = TaskQueue.ToProto(default(Temporalio.Api.TaskQueue.V1.TaskQueue)!); + proto.TaskQueue = ProtoExtensions.ToTaskQueueProto(TaskQueue); proto.SignalName = Signal; if (SignalArgs is { } signalArgs) { - proto.SignalInput = signalArgs.ToProto(); + proto.SignalInput = ProtoExtensions.ToPayloads(signalArgs); } if (ExecutionTimeout is { } executionTimeout) { @@ -194,11 +193,11 @@ public Temporalio.Api.Sdk.V1.UserMetadata ToProto() var proto = new Temporalio.Api.Sdk.V1.UserMetadata(); if (StaticSummary is { } staticSummary) { - proto.Summary = staticSummary.ToProto(); + proto.Summary = ProtoExtensions.ToPayload(staticSummary); } if (StaticDetails is { } staticDetails) { - proto.Details = staticDetails.ToProto(); + proto.Details = ProtoExtensions.ToPayload(staticDetails); } return proto; } diff --git a/src/Temporalio/SystemNexus/Generated/Operations.cs b/src/Temporalio/Workflows/Generated/Operations.cs similarity index 90% rename from src/Temporalio/SystemNexus/Generated/Operations.cs rename to src/Temporalio/Workflows/Generated/Operations.cs index 6cf67a1a..6db436a3 100644 --- a/src/Temporalio/SystemNexus/Generated/Operations.cs +++ b/src/Temporalio/Workflows/Generated/Operations.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Temporalio.Converters; -using Temporalio.Workflows; namespace Temporalio.Workflows { @@ -99,6 +98,8 @@ public SignalWithStartWorkflowOptions(string id, string taskQueue) public static partial class Workflow { + private const string WorkflowServiceEndpoint = "__temporal_system"; + /// /// Signal a workflow, starting it first if needed. /// @@ -106,7 +107,7 @@ public static partial class Workflow /// A workflow handle to the started workflow. private static async Task SignalWithStartWorkflowAsync(SignalWithStartWorkflowRequest request) { - var client = Workflow.CreateNexusWorkflowClient("__temporal_system"); + var client = Workflow.CreateNexusWorkflowClient(WorkflowServiceEndpoint); var protoRequest = request.ToProto(); var result = await client.ExecuteNexusOperationAsync(svc => svc.SignalWithStartWorkflow(protoRequest)).ConfigureAwait(true); return Temporalio.Workflows.Workflow.GetExternalWorkflowHandle(request.Id, result.RunId); @@ -155,8 +156,8 @@ public static partial class Workflow /// A workflow handle to the started workflow. public static Task SignalWithStartWorkflowAsync(Expression>> workflow, string signal, IReadOnlyCollection? signalArgs, SignalWithStartWorkflowOptions options) { - var (workflowMethod, workflowArgs) = ExtractCall(workflow); - var request = new SignalWithStartWorkflowRequest(NexGen.Support.TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, signal) + var (workflowMethod, workflowArgs) = TemporalFunctionNames.ExtractCall(workflow); + var request = new SignalWithStartWorkflowRequest(TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, signal) { Args = workflowArgs, SignalArgs = signalArgs, @@ -188,8 +189,8 @@ public static partial class Workflow /// A workflow handle to the started workflow. public static Task SignalWithStartWorkflowAsync(string workflow, IReadOnlyCollection? args, Expression> signal, SignalWithStartWorkflowOptions options) { - var (signalMethod, signalArgs) = ExtractCall(signal); - var request = new SignalWithStartWorkflowRequest(workflow, options.Id, options.TaskQueue, NexGen.Support.TemporalFunctionNames.SignalName(signalMethod)) + var (signalMethod, signalArgs) = TemporalFunctionNames.ExtractCall(signal); + var request = new SignalWithStartWorkflowRequest(workflow, options.Id, options.TaskQueue, TemporalFunctionNames.SignalName(signalMethod)) { Args = args, SignalArgs = signalArgs, @@ -220,9 +221,9 @@ public static partial class Workflow /// A workflow handle to the started workflow. public static Task SignalWithStartWorkflowAsync(Expression>> workflow, Expression> signal, SignalWithStartWorkflowOptions options) { - var (workflowMethod, workflowArgs) = ExtractCall(workflow); - var (signalMethod, signalArgs) = ExtractCall(signal); - var request = new SignalWithStartWorkflowRequest(NexGen.Support.TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, NexGen.Support.TemporalFunctionNames.SignalName(signalMethod)) + var (workflowMethod, workflowArgs) = TemporalFunctionNames.ExtractCall(workflow); + var (signalMethod, signalArgs) = TemporalFunctionNames.ExtractCall(signal); + var request = new SignalWithStartWorkflowRequest(TemporalFunctionNames.WorkflowName(workflowMethod), options.Id, options.TaskQueue, TemporalFunctionNames.SignalName(signalMethod)) { Args = workflowArgs, SignalArgs = signalArgs, @@ -244,17 +245,6 @@ public static partial class Workflow return SignalWithStartWorkflowAsync(request); } - private static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) - { - if (expression.Body is not MethodCallExpression call) - { - throw new ArgumentException("Expression must be a single method call", nameof(expression)); - } - var method = call.Method; - var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); - return (method, args); - } - } } diff --git a/src/Temporalio/SystemNexus/Generated/Service.cs b/src/Temporalio/Workflows/Generated/Service.cs similarity index 96% rename from src/Temporalio/SystemNexus/Generated/Service.cs rename to src/Temporalio/Workflows/Generated/Service.cs index 89f82e97..5951f3c0 100644 --- a/src/Temporalio/SystemNexus/Generated/Service.cs +++ b/src/Temporalio/Workflows/Generated/Service.cs @@ -22,7 +22,7 @@ internal interface IWorkflowService } - public static class NexGenOperationRegistry + internal static class NexGenOperationRegistry { internal static IReadOnlyDictionary Services { get; } = new Dictionary diff --git a/src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs b/src/Temporalio/Workflows/Generated/TemporalSupport.cs similarity index 52% rename from src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs rename to src/Temporalio/Workflows/Generated/TemporalSupport.cs index e2a5a27d..fe5a54d2 100644 --- a/src/Temporalio/SystemNexus/Generated/Support/TemporalSupport.cs +++ b/src/Temporalio/Workflows/Generated/TemporalSupport.cs @@ -4,17 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using Google.Protobuf.WellKnownTypes; using Temporalio.Common; using Temporalio.Converters; -using Temporalio.Workflows; using ApiCommon = Temporalio.Api.Common.V1; using ApiDeployment = Temporalio.Api.Deployment.V1; using ApiTaskQueue = Temporalio.Api.TaskQueue.V1; using ApiWorkflow = Temporalio.Api.Workflow.V1; -namespace NexGen.Support +namespace Temporalio.Workflows { internal static class TemporalWorkflowContext { @@ -23,6 +23,17 @@ internal static class TemporalWorkflowContext internal static class TemporalFunctionNames { + internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) + { + if (expression.Body is not MethodCallExpression call) + { + throw new ArgumentException("Expression must be a single method call", nameof(expression)); + } + var method = call.Method; + var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); + return (method, args); + } + internal static string WorkflowName(MethodInfo method) { if (method.GetCustomAttribute() == null) @@ -47,17 +58,21 @@ internal static string SignalName(MethodInfo method) internal static class ProtoExtensions { - internal static ApiCommon.WorkflowType ToProto(this string value, ApiCommon.WorkflowType _) => + internal static ApiCommon.WorkflowType ToWorkflowTypeProto(this string value) => new() { Name = value }; - internal static ApiTaskQueue.TaskQueue ToProto(this string value, ApiTaskQueue.TaskQueue _) => + internal static ApiTaskQueue.TaskQueue ToTaskQueueProto(this string value) => new() { Name = value }; - internal static ApiCommon.Payload ToProto(this object? value) => + internal static ApiCommon.Payload ToPayload(object? value) => Workflow.PayloadConverter.ToPayload(value); - internal static ApiCommon.Payloads ToProto(this IEnumerable value) => - ToPayloads(value); + internal static ApiCommon.Payloads ToPayloads(IEnumerable values) + { + var payloads = new ApiCommon.Payloads(); + payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); + return payloads; + } internal static Duration ToProto(this TimeSpan value) => Duration.FromTimeSpan(value); @@ -68,129 +83,12 @@ internal static ApiCommon.RetryPolicy ToProto(this Temporalio.Common.RetryPolicy internal static ApiCommon.Memo ToProto(this IReadOnlyDictionary value) => ToMemo(value); - internal static ApiCommon.SearchAttributes ToProto(this SearchAttributeCollection value) => - value.ToProto(); - internal static ApiCommon.Priority ToProto(this Temporalio.Common.Priority value) => ToPriority(value); internal static ApiWorkflow.VersioningOverride ToProto(this Temporalio.Common.VersioningOverride value) => ToVersioningOverride(value); - internal static string FromProto(this ApiCommon.WorkflowType proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Name; - } - - internal static string FromProto(this ApiTaskQueue.TaskQueue proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Name; - } - - internal static TimeSpan FromProto(this Duration proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.ToTimeSpan(); - } - - internal static object? FromProto(this ApiCommon.Payload proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return Workflow.PayloadConverter.ToValue(proto); - } - - internal static IReadOnlyCollection FromProto(this ApiCommon.Payloads proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return PayloadsToValues(proto); - } - - internal static Temporalio.Common.RetryPolicy FromProto(this ApiCommon.RetryPolicy proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return FromRetryPolicy(proto); - } - - internal static IReadOnlyDictionary FromProto(this ApiCommon.Memo proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return proto.Fields.ToDictionary(item => item.Key, item => Workflow.PayloadConverter.ToValue(item.Value)); - } - - internal static SearchAttributeCollection FromProto(this ApiCommon.SearchAttributes proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return SearchAttributeCollection.FromProto(proto); - } - - internal static Temporalio.Common.Priority FromProto(this ApiCommon.Priority proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - return new Temporalio.Common.Priority( - proto.PriorityKey == 0 ? null : proto.PriorityKey, - string.IsNullOrEmpty(proto.FairnessKey) ? null : proto.FairnessKey, - proto.FairnessWeight == 0f ? null : proto.FairnessWeight); - } - - internal static Temporalio.Common.VersioningOverride FromProto(this ApiWorkflow.VersioningOverride proto) - { - if (proto is null) - { - throw new ArgumentNullException(nameof(proto)); - } - if (proto.Pinned is { } pinned) - { - return new Temporalio.Common.VersioningOverride.Pinned( - new WorkerDeploymentVersion(pinned.Version.DeploymentName, pinned.Version.BuildId), - (Temporalio.Common.VersioningOverride.PinnedOverrideBehavior)pinned.Behavior); - } - if (proto.AutoUpgrade) - { - return new Temporalio.Common.VersioningOverride.AutoUpgrade(); - } - - throw new NotSupportedException("Unsupported versioning override proto"); - } - - private static ApiCommon.Payloads ToPayloads(IEnumerable values) - { - var payloads = new ApiCommon.Payloads(); - payloads.Payloads_.AddRange(Workflow.PayloadConverter.ToPayloads(values as IReadOnlyCollection ?? new List(values))); - return payloads; - } - - private static IReadOnlyCollection PayloadsToValues(ApiCommon.Payloads payloads) => - payloads.Payloads_.Select(payload => Workflow.PayloadConverter.ToValue(payload)).ToArray(); - private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy policy) { var proto = new ApiCommon.RetryPolicy @@ -210,25 +108,6 @@ private static ApiCommon.RetryPolicy ToRetryPolicy(Temporalio.Common.RetryPolicy return proto; } - private static Temporalio.Common.RetryPolicy FromRetryPolicy(ApiCommon.RetryPolicy proto) - { - var retryPolicy = new Temporalio.Common.RetryPolicy - { - BackoffCoefficient = (float)proto.BackoffCoefficient, - MaximumAttempts = proto.MaximumAttempts, - NonRetryableErrorTypes = proto.NonRetryableErrorTypes.ToArray(), - }; - if (proto.InitialInterval is { } initialInterval) - { - retryPolicy.InitialInterval = initialInterval.ToTimeSpan(); - } - if (proto.MaximumInterval is { } maximumInterval) - { - retryPolicy.MaximumInterval = maximumInterval.ToTimeSpan(); - } - return retryPolicy; - } - private static ApiCommon.Memo ToMemo(IReadOnlyDictionary memo) { var proto = new ApiCommon.Memo(); From c76eb2cd895f5f29124e551337f4a1db64bfab22 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Wed, 24 Jun 2026 12:22:34 -0700 Subject: [PATCH 09/13] Remove system Nexus operation registry usage --- .../Program.cs | 34 +++++++------------ .../Generated/SystemNexusPayloadVisitor.cs | 22 ++++++------ .../Worker/IWorkflowCodecHelperInstance.cs | 5 ++- .../Worker/SystemNexusPayloadVisitor.cs | 17 ++++------ src/Temporalio/Worker/WorkflowCodecHelper.cs | 6 ++-- src/Temporalio/Worker/WorkflowInstance.cs | 11 +++--- src/Temporalio/Workflows/Generated/Service.cs | 16 --------- 7 files changed, 39 insertions(+), 72 deletions(-) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index 5cea6411..b89a28c8 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -19,7 +19,7 @@ BuildDescriptor(); GenerateNexusApi(); PostProcessGeneratedNexusApi(); -GeneratePayloadVisitorRegistry(operationsPath, descriptorPath, workerGeneratedDir); +GeneratePayloadVisitor(operationsPath, descriptorPath, workerGeneratedDir); return 0; void EnsureNexGen() @@ -123,7 +123,7 @@ static void RecreateDirectory(string path) Directory.CreateDirectory(path); } -static void GeneratePayloadVisitorRegistry( +static void GeneratePayloadVisitor( string operationsPath, string descriptorPath, string outputDir) @@ -132,16 +132,6 @@ static void GeneratePayloadVisitorRegistry( throw new InvalidOperationException($"No directory for {operationsPath}"); var servicePath = Path.Combine(generatedDir, "Service.cs"); var source = File.ReadAllText(servicePath); - var namespaceMatch = Regex.Match( - source, - @"namespace\s+(?[A-Za-z0-9_.]+)\s*\{", - RegexOptions.Multiline); - if (!namespaceMatch.Success) - { - throw new InvalidOperationException($"No generated namespace found in {servicePath}"); - } - - var generatedNamespace = namespaceMatch.Groups["namespace"].Value; var operations = ParseOperations(source); if (operations.Count == 0) { @@ -169,14 +159,15 @@ static void GeneratePayloadVisitorRegistry( builder.AppendLine("using System.Collections.Generic;"); builder.AppendLine("using System.Threading.Tasks;"); builder.AppendLine("using Temporalio.Api.Common.V1;"); - builder.AppendLine($"using {generatedNamespace};"); builder.AppendLine(); builder.AppendLine("namespace Temporalio.Worker"); builder.AppendLine("{"); builder.AppendLine(" internal static partial class SystemNexusPayloadVisitor"); builder.AppendLine(" {"); - builder.AppendLine(" private static readonly IReadOnlyDictionary> EnvelopeVisitors ="); - builder.AppendLine(" new Dictionary>"); + builder.AppendLine(" private const string TemporalSystemEndpoint = \"__temporal_system\";"); + builder.AppendLine(); + builder.AppendLine(" private static readonly IReadOnlyDictionary> EnvelopeVisitors ="); + builder.AppendLine(" new Dictionary>"); builder.AppendLine(" {"); foreach (var operation in operationMessages) @@ -188,20 +179,19 @@ static void GeneratePayloadVisitorRegistry( builder.AppendLine(" };"); builder.AppendLine(); builder.AppendLine(" private static async Task TryVisitAsync("); - builder.AppendLine(" string service,"); - builder.AppendLine(" string operation,"); - builder.AppendLine(" bool input,"); + builder.AppendLine(" string? endpoint,"); builder.AppendLine(" Payload payload,"); builder.AppendLine(" PayloadVisitor visitPayload,"); builder.AppendLine(" PayloadsVisitor visitPayloads,"); builder.AppendLine(" EnvelopeVisitor? visitEnvelope)"); builder.AppendLine(" {"); - builder.AppendLine(" if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition))"); + builder.AppendLine(" if (!IsSystemNexusEndpoint(endpoint))"); builder.AppendLine(" {"); builder.AppendLine(" return false;"); builder.AppendLine(" }"); builder.AppendLine(); - builder.AppendLine(" if (!EnvelopeVisitors.TryGetValue(input ? definition.InputType : definition.OutputType, out var visit))"); + builder.AppendLine(" if (!payload.Metadata.TryGetValue(\"messageType\", out var messageType) ||"); + builder.AppendLine(" !EnvelopeVisitors.TryGetValue(messageType.ToStringUtf8(), out var visit))"); builder.AppendLine(" {"); builder.AppendLine(" return false;"); builder.AppendLine(" }"); @@ -210,6 +200,8 @@ static void GeneratePayloadVisitorRegistry( builder.AppendLine(" return true;"); builder.AppendLine(" }"); builder.AppendLine(); + builder.AppendLine(" private static bool IsSystemNexusEndpoint(string? endpoint) => endpoint == TemporalSystemEndpoint;"); + builder.AppendLine(); foreach (var operation in operationMessages) { @@ -309,7 +301,7 @@ static void EmitEnvelopeVisitor( return; } - builder.AppendLine($" [typeof({message.CsharpType})] = (payload, visitPayload, visitPayloads, visitEnvelope) =>"); + builder.AppendLine($" [\"{message.FullName}\"] = (payload, visitPayload, visitPayloads, visitEnvelope) =>"); builder.AppendLine($" VisitEnvelopeAsync<{message.CsharpType}>("); builder.AppendLine(" payload,"); builder.AppendLine($" {VisitMethodName(message)},"); diff --git a/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs index 11d12fc6..413be4b8 100644 --- a/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs +++ b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs @@ -6,23 +6,24 @@ using System.Collections.Generic; using System.Threading.Tasks; using Temporalio.Api.Common.V1; -using Temporalio.Workflows; namespace Temporalio.Worker { internal static partial class SystemNexusPayloadVisitor { - private static readonly IReadOnlyDictionary> EnvelopeVisitors = - new Dictionary> + private const string TemporalSystemEndpoint = "__temporal_system"; + + private static readonly IReadOnlyDictionary> EnvelopeVisitors = + new Dictionary> { - [typeof(global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest)] = (payload, visitPayload, visitPayloads, visitEnvelope) => + ["temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest"] = (payload, visitPayload, visitPayloads, visitEnvelope) => VisitEnvelopeAsync( payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionRequest, visitPayload, visitPayloads, visitEnvelope), - [typeof(global::Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse)] = (payload, visitPayload, visitPayloads, visitEnvelope) => + ["temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse"] = (payload, visitPayload, visitPayloads, visitEnvelope) => VisitEnvelopeAsync( payload, Visit_temporal_api_workflowservice_v1_SignalWithStartWorkflowExecutionResponse, @@ -32,20 +33,19 @@ internal static partial class SystemNexusPayloadVisitor }; private static async Task TryVisitAsync( - string service, - string operation, - bool input, + string? endpoint, Payload payload, PayloadVisitor visitPayload, PayloadsVisitor visitPayloads, EnvelopeVisitor? visitEnvelope) { - if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition)) + if (!IsSystemNexusEndpoint(endpoint)) { return false; } - if (!EnvelopeVisitors.TryGetValue(input ? definition.InputType : definition.OutputType, out var visit)) + if (!payload.Metadata.TryGetValue("messageType", out var messageType) || + !EnvelopeVisitors.TryGetValue(messageType.ToStringUtf8(), out var visit)) { return false; } @@ -54,6 +54,8 @@ private static async Task TryVisitAsync( return true; } + private static bool IsSystemNexusEndpoint(string? endpoint) => endpoint == TemporalSystemEndpoint; + private static async Task Visit_temporal_api_common_v1_Memo( global::Temporalio.Api.Common.V1.Memo value, PayloadVisitor visitPayload, diff --git a/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs b/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs index 28dc6335..31a51ad3 100644 --- a/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs +++ b/src/Temporalio/Worker/IWorkflowCodecHelperInstance.cs @@ -51,8 +51,7 @@ internal interface IWorkflowCodecHelperInstance /// /// Pending Nexus operation info. /// - /// Service name. - /// Operation name. - internal record NexusOperationInfo(string Service, string Operation); + /// Endpoint name. + internal record NexusOperationInfo(string? Endpoint); } } diff --git a/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs b/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs index 0f1e139e..0ca62f7d 100644 --- a/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs +++ b/src/Temporalio/Worker/SystemNexusPayloadVisitor.cs @@ -6,7 +6,6 @@ using Google.Protobuf.Collections; using Temporalio.Api.Common.V1; using Temporalio.Converters; -using Temporalio.Workflows; namespace Temporalio.Worker { @@ -21,14 +20,12 @@ internal static partial class SystemNexusPayloadVisitor internal delegate Task EnvelopeVisitor(Payload payload); internal static bool TryToInputPayload( - string service, - string operation, + string? endpoint, object? value, out Payload payload) { payload = null!; - if (!NexGenOperationRegistry.Operations.TryGetValue((service, operation), out var definition) || - !definition.InputType.IsInstanceOfType(value)) + if (!IsSystemNexusEndpoint(endpoint)) { return false; } @@ -43,22 +40,20 @@ internal static bool TryToInputPayload( } internal static Task TryVisitInputAsync( - string service, - string operation, + string? endpoint, Payload payload, PayloadVisitor visitPayload, PayloadsVisitor visitPayloads, EnvelopeVisitor? visitEnvelope = null) => - TryVisitAsync(service, operation, input: true, payload, visitPayload, visitPayloads, visitEnvelope); + TryVisitAsync(endpoint, payload, visitPayload, visitPayloads, visitEnvelope); internal static Task TryVisitOutputAsync( - string service, - string operation, + string? endpoint, Payload payload, PayloadVisitor visitPayload, PayloadsVisitor visitPayloads, EnvelopeVisitor? visitEnvelope = null) => - TryVisitAsync(service, operation, input: false, payload, visitPayload, visitPayloads, visitEnvelope); + TryVisitAsync(endpoint, payload, visitPayload, visitPayloads, visitEnvelope); private static async Task VisitEnvelopeAsync( Payload payload, diff --git a/src/Temporalio/Worker/WorkflowCodecHelper.cs b/src/Temporalio/Worker/WorkflowCodecHelper.cs index 1c325cd8..95804a39 100644 --- a/src/Temporalio/Worker/WorkflowCodecHelper.cs +++ b/src/Temporalio/Worker/WorkflowCodecHelper.cs @@ -129,8 +129,7 @@ await childCodec2.DecodeFailureAsync( job.ResolveNexusOperation.Seq); if (operationInfo == null || !await SystemNexusPayloadVisitor.TryVisitOutputAsync( - operationInfo.Service, - operationInfo.Operation, + operationInfo.Endpoint, job.ResolveNexusOperation.Result.Completed, payload => DecodeAsync(nexusCodec, payload), payloads => DecodeAsync(nexusCodec, payloads)). @@ -349,8 +348,7 @@ await EncodeAsync( if (cmd.ScheduleNexusOperation.Input != null && codec != null) { if (!await SystemNexusPayloadVisitor.TryVisitInputAsync( - cmd.ScheduleNexusOperation.Service, - cmd.ScheduleNexusOperation.Operation, + cmd.ScheduleNexusOperation.Endpoint, cmd.ScheduleNexusOperation.Input, payload => EncodeAsync(codec, payload), payloads => EncodeAsync(codec, payloads)). diff --git a/src/Temporalio/Worker/WorkflowInstance.cs b/src/Temporalio/Worker/WorkflowInstance.cs index fef8cfa0..ce78caff 100644 --- a/src/Temporalio/Worker/WorkflowInstance.cs +++ b/src/Temporalio/Worker/WorkflowInstance.cs @@ -824,7 +824,7 @@ public WorkflowActivationCompletion Activate(WorkflowActivation act) public IWorkflowCodecHelperInstance.NexusOperationInfo? GetPendingNexusOperationInfo(uint seq) { nexusOperationsPending.TryGetValue(seq, out var pending); - return pending == null ? null : new(pending.Service, pending.Operation); + return pending == null ? null : new(pending.Endpoint); } /// @@ -2591,8 +2591,7 @@ public override Task> ScheduleNexusOperati var seq = ++instance.nexusOperationCounter; var inputPayload = SystemNexusPayloadVisitor.TryToInputPayload( - input.Service, - input.OperationName, + input.ClientOptions.Endpoint, input.Arg, out var systemNexusInputPayload) ? systemNexusInputPayload : @@ -2631,8 +2630,7 @@ public override Task> ScheduleNexusOperati var handleSource = new TaskCompletionSource>(); var pending = new PendingNexusOperationInfo( - Service: input.Service, - Operation: input.OperationName, + Endpoint: input.ClientOptions.Endpoint, StartCompletionSource: new(), ResultCompletionSource: new()); instance.nexusOperationsPending[seq] = pending; @@ -3095,8 +3093,7 @@ private record PendingExternalCancel( TaskCompletionSource CompletionSource); private record PendingNexusOperationInfo( - string Service, - string Operation, + string? Endpoint, TaskCompletionSource StartCompletionSource, TaskCompletionSource ResultCompletionSource); diff --git a/src/Temporalio/Workflows/Generated/Service.cs b/src/Temporalio/Workflows/Generated/Service.cs index 5951f3c0..3d2f19cd 100644 --- a/src/Temporalio/Workflows/Generated/Service.cs +++ b/src/Temporalio/Workflows/Generated/Service.cs @@ -4,7 +4,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using NexusRpc; namespace Temporalio.Workflows @@ -22,19 +21,4 @@ internal interface IWorkflowService } - internal static class NexGenOperationRegistry - { - internal static IReadOnlyDictionary Services { get; } = - new Dictionary - { - ["temporal.api.workflowservice.v1.WorkflowService"] = ServiceDefinition.FromType(), - }; - - public static IReadOnlyDictionary<(string Service, string Operation), OperationDefinition> Operations { get; } = - new Dictionary<(string Service, string Operation), OperationDefinition> - { - [("temporal.api.workflowservice.v1.WorkflowService", "SignalWithStartWorkflowExecution")] = Services["temporal.api.workflowservice.v1.WorkflowService"].Operations["SignalWithStartWorkflowExecution"], - }; - } - } From 2e8b7addcab41a1ed6da099c3f23f8f3bacf512e Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Fri, 26 Jun 2026 09:36:35 -0700 Subject: [PATCH 10/13] Regenerate system Nexus API annotations --- src/Temporalio.SystemNexus.Generator/Program.cs | 2 ++ .../nexus-temporal-types/dotnet/TemporalSupport.cs | 13 ++----------- .../Worker/Generated/SystemNexusPayloadVisitor.cs | 2 ++ src/Temporalio/Workflows/Generated/Models.cs | 4 ++++ src/Temporalio/Workflows/Generated/Operations.cs | 14 ++++++++++++++ src/Temporalio/Workflows/Generated/Service.cs | 5 +++++ .../Workflows/Generated/TemporalSupport.cs | 13 ++----------- 7 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index b89a28c8..dcfca9e6 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -156,12 +156,14 @@ static void GeneratePayloadVisitor( builder.AppendLine("#nullable enable"); builder.AppendLine(); builder.AppendLine("using System;"); + builder.AppendLine("using System.CodeDom.Compiler;"); builder.AppendLine("using System.Collections.Generic;"); builder.AppendLine("using System.Threading.Tasks;"); builder.AppendLine("using Temporalio.Api.Common.V1;"); builder.AppendLine(); builder.AppendLine("namespace Temporalio.Worker"); builder.AppendLine("{"); + builder.AppendLine(" [GeneratedCode(\"Temporalio.SystemNexus.Generator\", null)]"); builder.AppendLine(" internal static partial class SystemNexusPayloadVisitor"); builder.AppendLine(" {"); builder.AppendLine(" private const string TemporalSystemEndpoint = \"__temporal_system\";"); diff --git a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs index 715c346e..ffe8d311 100644 --- a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs +++ b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Google.Protobuf.WellKnownTypes; @@ -21,16 +20,8 @@ internal static class TemporalWorkflowContext internal static class TemporalFunctionNames { - internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) - { - if (expression.Body is not MethodCallExpression call) - { - throw new ArgumentException("Expression must be a single method call", nameof(expression)); - } - var method = call.Method; - var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); - return (method, args); - } + internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall( + Expression> expression) => ExpressionUtil.ExtractCall(expression); internal static string WorkflowName(MethodInfo method) { diff --git a/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs index 413be4b8..e0479232 100644 --- a/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs +++ b/src/Temporalio/Worker/Generated/SystemNexusPayloadVisitor.cs @@ -3,12 +3,14 @@ #nullable enable using System; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.Threading.Tasks; using Temporalio.Api.Common.V1; namespace Temporalio.Worker { + [GeneratedCode("Temporalio.SystemNexus.Generator", null)] internal static partial class SystemNexusPayloadVisitor { private const string TemporalSystemEndpoint = "__temporal_system"; diff --git a/src/Temporalio/Workflows/Generated/Models.cs b/src/Temporalio/Workflows/Generated/Models.cs index 2e3983c6..66327efa 100644 --- a/src/Temporalio/Workflows/Generated/Models.cs +++ b/src/Temporalio/Workflows/Generated/Models.cs @@ -4,6 +4,7 @@ #pragma warning disable CS1591 using System; +using System.CodeDom.Compiler; using System.Collections.Generic; namespace Temporalio.Workflows @@ -12,6 +13,8 @@ namespace Temporalio.Workflows /// /// Request fields for signaling a workflow, starting it first if needed. /// + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] internal class SignalWithStartWorkflowRequest { internal SignalWithStartWorkflowRequest(string workflow, string id, string taskQueue, string signal) @@ -177,6 +180,7 @@ public Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest } + [GeneratedCode("nex-gen", null)] internal class UserMetadata { /// diff --git a/src/Temporalio/Workflows/Generated/Operations.cs b/src/Temporalio/Workflows/Generated/Operations.cs index 6db436a3..01eb9297 100644 --- a/src/Temporalio/Workflows/Generated/Operations.cs +++ b/src/Temporalio/Workflows/Generated/Operations.cs @@ -4,6 +4,7 @@ #pragma warning disable CS1591 using System; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -18,6 +19,8 @@ namespace Temporalio.Workflows /// /// Signal a workflow, starting it first if needed. /// + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] public class SignalWithStartWorkflowOptions { public SignalWithStartWorkflowOptions(string id, string taskQueue) @@ -98,6 +101,7 @@ public SignalWithStartWorkflowOptions(string id, string taskQueue) public static partial class Workflow { + [GeneratedCode("nex-gen", null)] private const string WorkflowServiceEndpoint = "__temporal_system"; /// @@ -105,6 +109,8 @@ public static partial class Workflow /// /// Request fields for signaling a workflow, starting it first if needed. /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] private static async Task SignalWithStartWorkflowAsync(SignalWithStartWorkflowRequest request) { var client = Workflow.CreateNexusWorkflowClient(WorkflowServiceEndpoint); @@ -122,6 +128,8 @@ public static partial class Workflow /// Arguments for the signal. /// Request fields for signaling a workflow, starting it first if needed. /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] public static Task SignalWithStartWorkflowAsync(string workflow, IReadOnlyCollection? args, string signal, IReadOnlyCollection? signalArgs, SignalWithStartWorkflowOptions options) { var request = new SignalWithStartWorkflowRequest(workflow, options.Id, options.TaskQueue, signal) @@ -154,6 +162,8 @@ public static partial class Workflow /// Arguments for the signal. /// Request fields for signaling a workflow, starting it first if needed. /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] public static Task SignalWithStartWorkflowAsync(Expression>> workflow, string signal, IReadOnlyCollection? signalArgs, SignalWithStartWorkflowOptions options) { var (workflowMethod, workflowArgs) = TemporalFunctionNames.ExtractCall(workflow); @@ -187,6 +197,8 @@ public static partial class Workflow /// Signal name or signal expression to send with the start request. /// Request fields for signaling a workflow, starting it first if needed. /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] public static Task SignalWithStartWorkflowAsync(string workflow, IReadOnlyCollection? args, Expression> signal, SignalWithStartWorkflowOptions options) { var (signalMethod, signalArgs) = TemporalFunctionNames.ExtractCall(signal); @@ -219,6 +231,8 @@ public static partial class Workflow /// Signal name or signal expression to send with the start request. /// Request fields for signaling a workflow, starting it first if needed. /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] public static Task SignalWithStartWorkflowAsync(Expression>> workflow, Expression> signal, SignalWithStartWorkflowOptions options) { var (workflowMethod, workflowArgs) = TemporalFunctionNames.ExtractCall(workflow); diff --git a/src/Temporalio/Workflows/Generated/Service.cs b/src/Temporalio/Workflows/Generated/Service.cs index 3d2f19cd..e2a201e3 100644 --- a/src/Temporalio/Workflows/Generated/Service.cs +++ b/src/Temporalio/Workflows/Generated/Service.cs @@ -4,11 +4,14 @@ #pragma warning disable CS1591 using System; +using System.CodeDom.Compiler; using NexusRpc; namespace Temporalio.Workflows { + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] [NexusService("temporal.api.workflowservice.v1.WorkflowService")] internal interface IWorkflowService { @@ -16,6 +19,8 @@ internal interface IWorkflowService /// Signal a workflow, starting it first if needed. /// /// A workflow handle to the started workflow. + /// WARNING: This API is experimental and may change in the future. + [GeneratedCode("nex-gen", null)] [NexusOperation("SignalWithStartWorkflowExecution")] Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionResponse SignalWithStartWorkflow(Temporalio.Api.WorkflowService.V1.SignalWithStartWorkflowExecutionRequest request); diff --git a/src/Temporalio/Workflows/Generated/TemporalSupport.cs b/src/Temporalio/Workflows/Generated/TemporalSupport.cs index fe5a54d2..84c3a7eb 100644 --- a/src/Temporalio/Workflows/Generated/TemporalSupport.cs +++ b/src/Temporalio/Workflows/Generated/TemporalSupport.cs @@ -3,7 +3,6 @@ #nullable enable using System; using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Google.Protobuf.WellKnownTypes; @@ -23,16 +22,8 @@ internal static class TemporalWorkflowContext internal static class TemporalFunctionNames { - internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall(Expression expression) - { - if (expression.Body is not MethodCallExpression call) - { - throw new ArgumentException("Expression must be a single method call", nameof(expression)); - } - var method = call.Method; - var args = call.Arguments.Select(arg => Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile()()).ToArray(); - return (method, args); - } + internal static (MethodInfo Method, IReadOnlyCollection Args) ExtractCall( + Expression> expression) => ExpressionUtil.ExtractCall(expression); internal static string WorkflowName(MethodInfo method) { From 348d96b27dbcc34fec75c8449e461a1bbc11a565 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Thu, 9 Jul 2026 11:56:33 -0700 Subject: [PATCH 11/13] chore: use sdk-core system Nexus WIT --- .github/workflows/ci.yml | 2 +- .../Program.cs | 13 +- .../wit/deps/nexus-temporal-types/model.wit | 175 ------------------ .../wit/workflow-service.wit | 125 ------------- 4 files changed, 5 insertions(+), 310 deletions(-) delete mode 100644 src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit delete mode 100644 src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f60cac36..ec3f014d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: - name: Install nex-gen if: ${{ matrix.checkTarget }} - run: cargo install --locked --version 0.1.4 --force nex-gen + run: cargo install --locked --version 0.1.5 --force nex-gen - name: Regen confirm unchanged if: ${{ matrix.checkTarget }} diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index dcfca9e6..06296c4a 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -8,6 +8,7 @@ var generatorDir = Path.Join(projectDir, "src/Temporalio.SystemNexus.Generator"); var protoDir = Path.Join(projectDir, "src/Temporalio/Bridge/sdk-core/crates/protos/protos"); var apiProtoDir = Path.Join(protoDir, "api_upstream"); +var nexusWitDir = Path.Join(apiProtoDir, "nexus"); var descriptorPath = Path.Join(generatorDir, "obj/SystemNexus/temporal_api.bin"); var stagingOutputDir = Path.Join(generatorDir, "obj/SystemNexus/Generated"); var workflowsGeneratedDir = Path.Join(projectDir, "src/Temporalio/Workflows/Generated"); @@ -31,13 +32,7 @@ void EnsureNexGen() return; } - if (Environment.GetEnvironmentVariable("NEX_GEN_BIN") is { Length: > 0 }) - { - throw new InvalidOperationException($"Unable to run nex-gen command {nexGenCommand}"); - } - - var installArgs = new[] { "install", "--locked", "nex-gen", "--force" }; - RunProcess("cargo", installArgs); + throw new InvalidOperationException($"Unable to run nex-gen command {nexGenCommand}"); } void BuildDescriptor() @@ -71,9 +66,9 @@ void GenerateNexusApi() "--lang", "dotnet", "--input", - Path.Join(generatorDir, "wit/workflow-service.wit"), + Path.Join(nexusWitDir, "workflow-service.wit"), "--input", - Path.Join(generatorDir, "wit/deps"), + Path.Join(nexusWitDir, "deps"), "--support-file", Path.Join(generatorDir, "wit/deps/nexus-temporal-types/dotnet/TemporalSupport.cs"), "--descriptors", diff --git a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit b/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit deleted file mode 100644 index 8d771b78..00000000 --- a/src/Temporalio.SystemNexus.Generator/wit/deps/nexus-temporal-types/model.wit +++ /dev/null @@ -1,175 +0,0 @@ -package nexus:temporal-types@1.0.0; - -interface model { - /// String-shaped placeholder for semantic types that generators reinterpret. - type placeholder = string; - - /// @nexus.proto "temporal.api.common.v1.Payload" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="typing.Any" - /// typescript="common.Payload" - /// dotnet="object?" - /// dotnet-to="ProtoExtensions.ToPayload" - /// typescript-package="@temporalio/common" - type payload = placeholder; - - /// @nexus.proto "temporal.api.common.v1.Payloads" - /// typescript-package="@temporalio/proto" - /// @nexus.type dotnet="IReadOnlyCollection" dotnet-to="ProtoExtensions.ToPayloads" - type payloads = list; - - /// Callable result annotation for workflow functions. - /// @nexus.type - /// python="collections.abc.Awaitable[WorkflowResult]" - /// typescript="Promise" - /// dotnet="System.Threading.Tasks.Task" - type workflow-result = placeholder; - - /// Receiver/context argument for workflow callable method forms. - /// @nexus.type python="typing.Any" typescript="any" dotnet="object" - type callable-prefix = placeholder; - - /// @nexus.function-args - /// varargs=true - /// param="args" - /// typescript-drop-prefix=true - workflow-call: async func(callable-prefix: callable-prefix, args: payloads) -> workflow-result; - - /// Callable result annotation for signal functions. - /// @nexus.type python="None | collections.abc.Awaitable[None]" typescript="void" dotnet="void" - type signal-result = placeholder; - - /// @nexus.function-args - /// varargs=true - /// param="signal-args" - /// typescript-drop-prefix=true - signal-call: func(callable-prefix: callable-prefix, signal-args: payloads) -> signal-result; - - /// @nexus.proto "temporal.api.common.v1.WorkflowType" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="str" - /// typescript="string" - /// dotnet="string" - /// dotnet-to="ProtoExtensions.ToWorkflowTypeProto" - type workflow-type = placeholder; - - /// @nexus.function - /// primary=true - /// signature="workflow-call" - /// args-field="input" - /// result-type-parameter="WorkflowResult" - /// alternate-type="workflow-type" - /// dotnet-name-extractor="TemporalFunctionNames.WorkflowName" - /// dotnet-call-extractor="TemporalFunctionNames.ExtractCall" - /// @nexus.add-rpc-compatible-with "workflow-type" - type workflow-function = placeholder; - - /// @nexus.function - /// signature="signal-call" - /// args-field="signal-input" - /// alternate-type="string" - /// python-converter="signal_function_to_proto" - /// typescript-converter="signalFunctionToProto" - /// dotnet-name-extractor="TemporalFunctionNames.SignalName" - /// dotnet-call-extractor="TemporalFunctionNames.ExtractCall" - /// @nexus.add-rpc-compatible-with "string" - /// @nexus.typescript-with-arguments - /// signature="signal-call" - /// args-field="signal-input" - /// alternate-type="string" - /// value-type="workflow.SignalDefinition" - /// args-type="Value extends workflow.SignalDefinition ? Args : never" - /// name-expr="value.name" - /// typescript-package="@temporalio/workflow" - type signal-function = placeholder; - - /// @nexus.proto "temporal.api.common.v1.RetryPolicy" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.RetryPolicy" - /// typescript="common.RetryPolicy" - /// dotnet="Temporalio.Common.RetryPolicy" - /// typescript-package="@temporalio/common" - type retry-policy = placeholder; - - /// @nexus.proto "temporal.api.taskqueue.v1.TaskQueue" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="str" - /// typescript="string" - /// dotnet="string" - /// dotnet-to="ProtoExtensions.ToTaskQueueProto" - type task-queue = placeholder; - - /// @nexus.proto "temporal.api.common.v1.Memo" typescript-package="@temporalio/proto" - /// @nexus.type python="collections.abc.Mapping[str, typing.Any]" typescript="Record" dotnet="IReadOnlyDictionary" - type memo = placeholder; - - /// @nexus.proto "temporal.api.common.v1.SearchAttributes" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.TypedSearchAttributes" - /// typescript="common.TypedSearchAttributes" - /// dotnet="Temporalio.Common.SearchAttributeCollection" - /// typescript-package="@temporalio/common" - type search-attributes = placeholder; - - /// @nexus.proto "temporal.api.common.v1.Priority" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.Priority" - /// typescript="common.Priority" - /// dotnet="Temporalio.Common.Priority" - /// typescript-package="@temporalio/common" - type priority = placeholder; - - /// @nexus.proto "temporal.api.workflow.v1.VersioningOverride" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.VersioningOverride" - /// typescript="common.VersioningOverride" - /// dotnet="Temporalio.Common.VersioningOverride" - /// typescript-package="@temporalio/common" - type versioning-override = placeholder; - - /// @nexus.proto "google.protobuf.Duration" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="datetime.timedelta" - /// typescript="common.Duration" - /// dotnet="System.TimeSpan" - /// typescript-package="@temporalio/common" - type duration = placeholder; - - /// @nexus.proto "temporal.api.enums.v1.WorkflowIdReusePolicy" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.WorkflowIDReusePolicy" - /// typescript="common.WorkflowIdReusePolicy" - /// dotnet="Temporalio.Api.Enums.V1.WorkflowIdReusePolicy" - /// typescript-package="@temporalio/common" - enum workflow-id-reuse-policy { - allow-duplicate, - allow-duplicate-failed-only, - reject-duplicate, - terminate-if-running, - } - - /// @nexus.proto "temporal.api.enums.v1.WorkflowIdConflictPolicy" typescript-package="@temporalio/proto" - /// @nexus.type - /// python="temporalio.common.WorkflowIDConflictPolicy" - /// typescript="common.WorkflowIdConflictPolicy" - /// dotnet="Temporalio.Api.Enums.V1.WorkflowIdConflictPolicy" - /// typescript-package="@temporalio/common" - enum workflow-id-conflict-policy { - fail, - use-existing, - terminate-existing, - } - - /// @nexus.proto "temporal.api.sdk.v1.UserMetadata" typescript-package="@temporalio/proto" - /// @nexus.flatten-in-api - record user-metadata { - /// @nexus.doc "Single-line fixed summary for the workflow execution that may appear in UI and CLI. This can be in single-line Temporal Markdown format." - /// @nexus.proto-field "summary" - /// @nexus.flattened-type python="str" typescript="string" dotnet="string" - static-summary: option, - /// @nexus.doc "General fixed details for the workflow execution that may appear in UI and CLI. This can be in Temporal Markdown format and can span multiple lines. This value is fixed on the workflow execution and cannot be updated." - /// @nexus.proto-field "details" - /// @nexus.flattened-type python="str" typescript="string" dotnet="string" - static-details: option, - } -} diff --git a/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit b/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit deleted file mode 100644 index a4547ab2..00000000 --- a/src/Temporalio.SystemNexus.Generator/wit/workflow-service.wit +++ /dev/null @@ -1,125 +0,0 @@ -package temporal:nexus@1.0.0; - -world system { - export workflow-service; -} - -/// @nexus.endpoint "__temporal_system" -/// @nexus.service-name "temporal.api.workflowservice.v1.WorkflowService" -/// @nexus.namespace dotnet="Temporalio.Workflows" -/// @nexus.operations-class dotnet="Workflow" -/// @nexus.delay-load-temporalio-workflow -/// @nexus.experimental -interface workflow-service { - use nexus:temporal-types/model@1.0.0.{ - duration, - memo, - payloads, - placeholder, - priority, - retry-policy, - search-attributes, - signal-function, - task-queue, - user-metadata, - versioning-override, - workflow-function, - workflow-id-conflict-policy, - workflow-id-reuse-policy, - }; - - /// @nexus.doc "Request fields for signaling a workflow, starting it first if needed." - /// @nexus.experimental - /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionRequest" typescript-package="@temporalio/proto" - record signal-with-start-workflow-request { - /// @nexus.doc - /// python="Workflow type name or callable identifying the workflow to start." - /// typescript="Workflow type name or workflow function identifying the workflow to start." - /// dotnet="Workflow type name or workflow expression identifying the workflow to start." - /// @nexus.proto-field "workflow_type" - workflow: workflow-function, - /// @nexus.doc "Unique identifier for the workflow execution." - /// @nexus.proto-field "workflow_id" - id: string, - /// @nexus.doc "Task queue to run the workflow on." - task-queue: task-queue, - /// @nexus.doc - /// python="Signal name or callable to send with the start request." - /// typescript="Signal name or signal definition to send with the start request." - /// dotnet="Signal name or signal expression to send with the start request." - /// @nexus.proto-field "signal_name" - signal: signal-function, - /// @nexus.doc "Total workflow execution timeout, including retries and continue-as-new." - /// @nexus.proto-field "workflow_execution_timeout" - execution-timeout: option, - /// @nexus.doc "Timeout of a single workflow run." - /// @nexus.proto-field "workflow_run_timeout" - run-timeout: option, - /// @nexus.doc "Timeout of a single workflow task." - /// @nexus.proto-field "workflow_task_timeout" - task-timeout: option, - /// @nexus.omit - identity: placeholder, - /// @nexus.doc "Request ID used to deduplicate workflow start requests." - request-id: option, - /// @nexus.doc "Behavior when a closed workflow with the same ID exists. Default is allow-duplicate." - /// @nexus.proto-field "workflow_id_reuse_policy" - /// @nexus.default "allow-duplicate" - id-reuse-policy: workflow-id-reuse-policy, - /// @nexus.doc "Behavior when a workflow is currently running with the same ID. Set to use-existing for idempotent deduplication on workflow ID. Cannot be set if id-reuse-policy is terminate-if-running." - /// @nexus.proto-field "workflow_id_conflict_policy" - id-conflict-policy: option, - /// @nexus.doc "Retry policy for the workflow." - retry-policy: option, - /// @nexus.doc "Cron schedule for recurring workflow executions. See https://docs.temporal.io/cron-job." - cron-schedule: option, - /// @nexus.doc "Memo for the workflow." - memo: option, - /// @nexus.doc "Typed search attributes for the workflow." - search-attributes: option, - /// @nexus.doc "Priority of the workflow execution." - priority: option, - /// @nexus.doc "Override for workflow versioning behavior." - versioning-override: option, - /// @nexus.doc "Amount of time to wait before starting the workflow. This does not work with cron-schedule." - /// @nexus.proto-field "workflow_start_delay" - start-delay: option, - user-metadata: option, - /// @nexus.source python="workflow_namespace" typescript="workflowNamespace" dotnet="TemporalWorkflowContext.WorkflowNamespace" - namespace: string, - /// @nexus.omit - control: placeholder, - /// @nexus.omit - header: placeholder, - /// @nexus.omit - links: placeholder, - /// @nexus.omit - time-skipping-config: placeholder, - } - - /// @nexus.experimental - /// @nexus.proto "temporal.api.workflowservice.v1.SignalWithStartWorkflowExecutionResponse" typescript-package="@temporalio/proto" - record signal-with-start-workflow-response { - run-id: option, - started: option, - /// @nexus.omit - signal-link: placeholder, - } - - /// @nexus.doc - /// "Signal a workflow, starting it first if needed." - /// returns="A workflow handle to the started workflow." - /// @nexus.output-transform - /// python-type="temporalio.workflow.ExternalWorkflowHandle[WorkflowResult]" - /// python="temporalio.workflow.get_external_workflow_handle(request.id, run_id=result.run_id)" - /// typescript-type="workflow.ExternalWorkflowHandle" - /// typescript="workflow.getExternalWorkflowHandle(request.id, result.runId ?? undefined)" - /// typescript-package="@temporalio/workflow" - /// dotnet-type="Temporalio.Workflows.ExternalWorkflowHandle" - /// dotnet="Temporalio.Workflows.Workflow.GetExternalWorkflowHandle(request.Id, result.RunId)" - /// @nexus.operation name="SignalWithStartWorkflowExecution" - /// @nexus.experimental - signal-with-start-workflow: func( - request: signal-with-start-workflow-request, - ) -> signal-with-start-workflow-response; -} From 7581e61a8f110940c51ff828bcad4bc3166fa433 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Thu, 9 Jul 2026 12:04:19 -0700 Subject: [PATCH 12/13] fix: normalize system Nexus support imports --- src/Temporalio.SystemNexus.Generator/Program.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Temporalio.SystemNexus.Generator/Program.cs b/src/Temporalio.SystemNexus.Generator/Program.cs index 06296c4a..d4ec9407 100644 --- a/src/Temporalio.SystemNexus.Generator/Program.cs +++ b/src/Temporalio.SystemNexus.Generator/Program.cs @@ -101,8 +101,11 @@ void WriteWorkflowGeneratedFile(string stagingRelativePath, string? outputFileNa workflowsGeneratedDir, outputFileName ?? Path.GetFileName(stagingRelativePath)); var contents = File.ReadAllText(sourcePath); - contents = contents.Replace("using NexGen.Support;\n", string.Empty); - contents = contents.Replace("using Temporalio.Workflows;\n", string.Empty); + contents = Regex.Replace( + contents, + @"^using (NexGen\.Support|Temporalio\.Workflows);\r?\n", + string.Empty, + RegexOptions.Multiline); contents = contents.Replace("namespace NexGen.Support", "namespace Temporalio.Workflows"); contents = contents.Replace("NexGen.Support.", string.Empty); File.WriteAllText(destinationPath, contents); From 762cb9ac0b050324b8c418e453539ce7f9dcf554 Mon Sep 17 00:00:00 2001 From: Tim Conley Date: Thu, 9 Jul 2026 14:08:46 -0700 Subject: [PATCH 13/13] chore: add signal-with-start changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54089ad2..ea5f4037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ to docs, or any other relevant information. ### Added - Added AWS Lambda worker support packages, including OpenTelemetry helpers for Lambda workers. +- Added experimental workflow-side `Workflow.SignalWithStartWorkflowAsync` support. ### Fixed