From 906d3caffe3057d518cc4131dcdc429df0ee5e41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:59:05 +0000 Subject: [PATCH 1/3] Initial plan From b8ef400160a379825028b66b61ec9d9866930b46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:10:29 +0000 Subject: [PATCH 2/3] Fix source generator to handle void-returning activity functions Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../AzureFunctions/DurableFunction.cs | 40 ++++++++-- .../AzureFunctions/TypedParameter.cs | 12 ++- src/Generators/DurableTaskSourceGenerator.cs | 17 ++++- test/Generators.Tests/AzureFunctionsTests.cs | 73 +++++++++++++++++++ 4 files changed, 134 insertions(+), 8 deletions(-) diff --git a/src/Generators/AzureFunctions/DurableFunction.cs b/src/Generators/AzureFunctions/DurableFunction.cs index 2f1af7dc4..2ec0923a8 100644 --- a/src/Generators/AzureFunctions/DurableFunction.cs +++ b/src/Generators/AzureFunctions/DurableFunction.cs @@ -22,6 +22,7 @@ public class DurableFunction public DurableFunctionKind Kind { get; } public TypedParameter Parameter { get; } public string ReturnType { get; } + public bool ReturnsVoid { get; } public DurableFunction( string fullTypeName, @@ -29,6 +30,7 @@ public DurableFunction( DurableFunctionKind kind, TypedParameter parameter, ITypeSymbol returnType, + bool returnsVoid, HashSet requiredNamespaces) { this.FullTypeName = fullTypeName; @@ -37,6 +39,7 @@ public DurableFunction( this.Kind = kind; this.Parameter = parameter; this.ReturnType = SyntaxNodeUtility.GetRenderedTypeExpression(returnType, false); + this.ReturnsVoid = returnsVoid; } public static bool TryParse(SemanticModel model, MethodDeclarationSyntax method, out DurableFunction? function) @@ -59,12 +62,37 @@ public static bool TryParse(SemanticModel model, MethodDeclarationSyntax method, return false; } - INamedTypeSymbol taskSymbol = model.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1")!; - INamedTypeSymbol returnSymbol = (INamedTypeSymbol)model.GetTypeInfo(returnType).Type!; - if (SymbolEqualityComparer.Default.Equals(returnSymbol.OriginalDefinition, taskSymbol)) + ITypeSymbol returnTypeSymbol = model.GetTypeInfo(returnType).Type!; + bool returnsVoid = false; + INamedTypeSymbol returnSymbol; + + // Check if it's a void return type + if (returnTypeSymbol.SpecialType == SpecialType.System_Void) + { + returnsVoid = true; + // For void, we'll use object as a placeholder since it won't be used + returnSymbol = model.Compilation.GetSpecialType(SpecialType.System_Object); + } + // Check if it's Task (non-generic) + else if (returnTypeSymbol is INamedTypeSymbol namedReturn && + namedReturn.ContainingNamespace.ToString() == "System.Threading.Tasks" && + namedReturn.Name == "Task" && + namedReturn.TypeArguments.Length == 0) + { + returnsVoid = true; + // For Task with no return, we'll use object as a placeholder since it won't be used + returnSymbol = model.Compilation.GetSpecialType(SpecialType.System_Object); + } + // Check if it's Task + else { - // this is a Task return value, lets pull out the generic. - returnSymbol = (INamedTypeSymbol)returnSymbol.TypeArguments[0]; + INamedTypeSymbol taskSymbol = model.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1")!; + returnSymbol = (INamedTypeSymbol)returnTypeSymbol; + if (SymbolEqualityComparer.Default.Equals(returnSymbol.OriginalDefinition, taskSymbol)) + { + // this is a Task return value, lets pull out the generic. + returnSymbol = (INamedTypeSymbol)returnSymbol.TypeArguments[0]; + } } if (!SyntaxNodeUtility.TryGetParameter(model, method, kind, out TypedParameter? parameter) || parameter == null) @@ -93,7 +121,7 @@ public static bool TryParse(SemanticModel model, MethodDeclarationSyntax method, requiredNamespaces!.UnionWith(GetRequiredGlobalNamespaces()); - function = new DurableFunction(fullTypeName!, name, kind, parameter, returnSymbol, requiredNamespaces); + function = new DurableFunction(fullTypeName!, name, kind, parameter, returnSymbol, returnsVoid, requiredNamespaces); return true; } diff --git a/src/Generators/AzureFunctions/TypedParameter.cs b/src/Generators/AzureFunctions/TypedParameter.cs index f6bc7ed83..c79666759 100644 --- a/src/Generators/AzureFunctions/TypedParameter.cs +++ b/src/Generators/AzureFunctions/TypedParameter.cs @@ -19,7 +19,17 @@ public TypedParameter(INamedTypeSymbol type, string name) public override string ToString() { - return $"{SyntaxNodeUtility.GetRenderedTypeExpression(this.Type, false)} {this.Name}"; + // Use the type as-is, preserving the nullability annotation from the source + string typeExpression = SyntaxNodeUtility.GetRenderedTypeExpression(this.Type, false); + + // Special case: if the type is exactly "object" (not a nullable object), make it nullable + // This is because object parameters are typically nullable in the context of Durable Functions + if (typeExpression == "object" && this.Type.NullableAnnotation != NullableAnnotation.Annotated) + { + typeExpression = "object?"; + } + + return $"{typeExpression} {this.Name}"; } } } diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs index 19a24cc90..6318622b2 100644 --- a/src/Generators/DurableTaskSourceGenerator.cs +++ b/src/Generators/DurableTaskSourceGenerator.cs @@ -365,7 +365,21 @@ static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableTaskTypeIn static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableFunction activity) { - sourceBuilder.AppendLine($@" + if (activity.ReturnsVoid) + { + sourceBuilder.AppendLine($@" + /// + /// Calls the activity. + /// + /// + public static Task Call{activity.Name}Async(this TaskOrchestrationContext ctx, {activity.Parameter}, TaskOptions? options = null) + {{ + return ctx.CallActivityAsync(""{activity.Name}"", {activity.Parameter.Name}, options); + }}"); + } + else + { + sourceBuilder.AppendLine($@" /// /// Calls the activity. /// @@ -374,6 +388,7 @@ static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableFunction a {{ return ctx.CallActivityAsync<{activity.ReturnType}>(""{activity.Name}"", {activity.Parameter.Name}, options); }}"); + } } static void AddActivityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo activity) diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs index d9d7fad01..3a02eeee2 100644 --- a/test/Generators.Tests/AzureFunctionsTests.cs +++ b/test/Generators.Tests/AzureFunctionsTests.cs @@ -117,6 +117,79 @@ await TestHelpers.RunTestAsync( isDurableFunctions: true); } + [Fact] + public async Task Activities_SimpleFunctionTrigger_VoidReturn() + { + string code = @" +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +public class Activities +{ + [Function(nameof(FlakeyActivity))] + public static void FlakeyActivity([ActivityTrigger] object _) + { + throw new System.ApplicationException(""Kah-BOOOOM!!!""); + } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +/// +/// Calls the activity. +/// +/// +public static Task CallFlakeyActivityAsync(this TaskOrchestrationContext ctx, object? _, TaskOptions? options = null) +{ + return ctx.CallActivityAsync(""FlakeyActivity"", _, options); +}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } + + [Fact] + public async Task Activities_SimpleFunctionTrigger_TaskReturn() + { + string code = @" +using System.Threading.Tasks; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +public class Activities +{ + [Function(nameof(FlakeyActivity))] + public static Task FlakeyActivity([ActivityTrigger] object _) + { + throw new System.ApplicationException(""Kah-BOOOOM!!!""); + } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +/// +/// Calls the activity. +/// +/// +public static Task CallFlakeyActivityAsync(this TaskOrchestrationContext ctx, object? _, TaskOptions? options = null) +{ + return ctx.CallActivityAsync(""FlakeyActivity"", _, options); +}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } + /// /// Verifies that using the class-based activity syntax generates a /// extension method as well as an function definition. From 4dc963ac525751549eed1d689b37887711fe7f81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:17:43 +0000 Subject: [PATCH 3/3] Address code review feedback - use robust type comparisons Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- .../AzureFunctions/DurableFunction.cs | 33 +++++++++++-------- .../AzureFunctions/TypedParameter.cs | 4 +-- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Generators/AzureFunctions/DurableFunction.cs b/src/Generators/AzureFunctions/DurableFunction.cs index 2ec0923a8..93dc5d890 100644 --- a/src/Generators/AzureFunctions/DurableFunction.cs +++ b/src/Generators/AzureFunctions/DurableFunction.cs @@ -74,25 +74,30 @@ public static bool TryParse(SemanticModel model, MethodDeclarationSyntax method, returnSymbol = model.Compilation.GetSpecialType(SpecialType.System_Object); } // Check if it's Task (non-generic) - else if (returnTypeSymbol is INamedTypeSymbol namedReturn && - namedReturn.ContainingNamespace.ToString() == "System.Threading.Tasks" && - namedReturn.Name == "Task" && - namedReturn.TypeArguments.Length == 0) + else if (returnTypeSymbol is INamedTypeSymbol namedReturn) { - returnsVoid = true; - // For Task with no return, we'll use object as a placeholder since it won't be used - returnSymbol = model.Compilation.GetSpecialType(SpecialType.System_Object); + INamedTypeSymbol? nonGenericTaskSymbol = model.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task"); + if (nonGenericTaskSymbol != null && SymbolEqualityComparer.Default.Equals(namedReturn, nonGenericTaskSymbol)) + { + returnsVoid = true; + // For Task with no return, we'll use object as a placeholder since it won't be used + returnSymbol = model.Compilation.GetSpecialType(SpecialType.System_Object); + } + // Check if it's Task + else + { + INamedTypeSymbol? taskSymbol = model.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1"); + returnSymbol = namedReturn; + if (taskSymbol != null && SymbolEqualityComparer.Default.Equals(returnSymbol.OriginalDefinition, taskSymbol)) + { + // this is a Task return value, lets pull out the generic. + returnSymbol = (INamedTypeSymbol)returnSymbol.TypeArguments[0]; + } + } } - // Check if it's Task else { - INamedTypeSymbol taskSymbol = model.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1")!; returnSymbol = (INamedTypeSymbol)returnTypeSymbol; - if (SymbolEqualityComparer.Default.Equals(returnSymbol.OriginalDefinition, taskSymbol)) - { - // this is a Task return value, lets pull out the generic. - returnSymbol = (INamedTypeSymbol)returnSymbol.TypeArguments[0]; - } } if (!SyntaxNodeUtility.TryGetParameter(model, method, kind, out TypedParameter? parameter) || parameter == null) diff --git a/src/Generators/AzureFunctions/TypedParameter.cs b/src/Generators/AzureFunctions/TypedParameter.cs index c79666759..4860905af 100644 --- a/src/Generators/AzureFunctions/TypedParameter.cs +++ b/src/Generators/AzureFunctions/TypedParameter.cs @@ -22,9 +22,9 @@ public override string ToString() // Use the type as-is, preserving the nullability annotation from the source string typeExpression = SyntaxNodeUtility.GetRenderedTypeExpression(this.Type, false); - // Special case: if the type is exactly "object" (not a nullable object), make it nullable + // Special case: if the type is exactly System.Object (not a nullable object), make it nullable // This is because object parameters are typically nullable in the context of Durable Functions - if (typeExpression == "object" && this.Type.NullableAnnotation != NullableAnnotation.Annotated) + if (this.Type.SpecialType == SpecialType.System_Object && this.Type.NullableAnnotation != NullableAnnotation.Annotated) { typeExpression = "object?"; }