From 49084891410d086ff44f4d4fc5e956297e8cc65b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:30:27 +0000 Subject: [PATCH 1/2] Initial plan From 56c3e7927e44f91a00ad817b8bec159e375b0d06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:39:18 +0000 Subject: [PATCH 2/2] Add durable function entity source generation support and tests Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com> --- src/Generators/DurableTaskSourceGenerator.cs | 20 ++ test/Generators.Tests/AzureFunctionsTests.cs | 223 +++++++++++++++++++ 2 files changed, 243 insertions(+) diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs index c45adca1..986f7ec3 100644 --- a/src/Generators/DurableTaskSourceGenerator.cs +++ b/src/Generators/DurableTaskSourceGenerator.cs @@ -270,6 +270,15 @@ public static class GeneratedDurableTaskExtensions } } + foreach (DurableTaskTypeInfo entity in entities) + { + if (isDurableFunctions) + { + // Generate the function definition required to trigger entities in Azure Functions + AddEntityFunctionDeclaration(sourceBuilder, entity); + } + } + // Activity function triggers are supported for code-gen (but not orchestration triggers) IEnumerable activityTriggers = allFunctions.Where( df => df.Kind == DurableFunctionKind.Activity); @@ -368,6 +377,17 @@ static void AddActivityFunctionDeclaration(StringBuilder sourceBuilder, DurableT }}"); } + static void AddEntityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo entity) + { + // Generate the entity trigger function that dispatches to the entity implementation. + sourceBuilder.AppendLine($@" + [Function(nameof({entity.TaskName}))] + public static Task {entity.TaskName}([EntityTrigger] TaskEntityDispatcher dispatcher) + {{ + return dispatcher.DispatchAsync<{entity.TypeName}>(); + }}"); + } + /// /// Adds a custom ITaskActivityContext implementation used by code generated from . /// diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs index 63dd768d..6ae9523d 100644 --- a/test/Generators.Tests/AzureFunctionsTests.cs +++ b/test/Generators.Tests/AzureFunctionsTests.cs @@ -312,4 +312,227 @@ await TestHelpers.RunTestAsync( expectedOutput, isDurableFunctions: true); } + + /// + /// Verifies that using the class-based syntax for authoring entities generates + /// function triggers for Azure Functions. + /// + /// The entity state type. + [Theory] + [InlineData("int")] + [InlineData("string")] + public async Task Entities_ClassBasedSyntax(string stateType) + { + string code = $@" +#nullable enable +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +namespace MyNS +{{ + [DurableTask(nameof(MyEntity))] + public class MyEntity : TaskEntity<{stateType}> + {{ + public {stateType} Get() => this.State; + }} +}}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +[Function(nameof(MyEntity))] +public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher) +{ + return dispatcher.DispatchAsync(); +}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } + + /// + /// Verifies that using the class-based syntax for authoring entities with inheritance generates + /// function triggers for Azure Functions. + /// + /// The entity state type. + [Theory] + [InlineData("int")] + [InlineData("string")] + public async Task Entities_ClassBasedSyntax_Inheritance(string stateType) + { + string code = $@" +#nullable enable +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +namespace MyNS +{{ + [DurableTask] + public class MyEntity : MyEntityBase + {{ + public override {stateType} Get() => this.State; + }} + + public abstract class MyEntityBase : TaskEntity<{stateType}> + {{ + public abstract {stateType} Get(); + }} +}}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +[Function(nameof(MyEntity))] +public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher) +{ + return dispatcher.DispatchAsync(); +}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } + + /// + /// Verifies that using the class-based syntax for authoring entities with custom state types generates + /// function triggers for Azure Functions. + /// + [Fact] + public async Task Entities_ClassBasedSyntax_CustomStateType() + { + string code = @" +#nullable enable +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +namespace MyNS +{ + public class MyState + { + public int Value { get; set; } + } + + [DurableTask(nameof(MyEntity))] + public class MyEntity : TaskEntity + { + public MyState Get() => this.State; + } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +[Function(nameof(MyEntity))] +public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher) +{ + return dispatcher.DispatchAsync(); +}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } + + /// + /// Verifies that using the class-based syntax for authoring a mix of orchestrators, activities, + /// and entities generates the appropriate function triggers for Azure Functions. + /// + [Fact] + public async Task Mixed_OrchestratorActivityEntity_ClassBasedSyntax() + { + string code = @" +#nullable enable +using System; +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +namespace MyNS +{ + [DurableTask(nameof(MyOrchestrator))] + public class MyOrchestrator : TaskOrchestrator + { + public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty); + } + + [DurableTask(nameof(MyActivity))] + public class MyActivity : TaskActivity + { + public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty); + } + + [DurableTask(nameof(MyEntity))] + public class MyEntity : TaskEntity + { + public int Get() => this.State; + } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: $@" +static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator(); + +[Function(nameof(MyOrchestrator))] +public static Task MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return singletonMyOrchestrator.RunAsync(context, context.GetInput()) + .ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously); +}} + +/// +public static Task ScheduleNewMyOrchestratorInstanceAsync( + this IOrchestrationSubmitter client, int input, StartOrchestrationOptions? options = null) +{{ + return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options); +}} + +/// +public static Task CallMyOrchestratorAsync( + this TaskOrchestrationContext context, int input, TaskOptions? options = null) +{{ + return context.CallSubOrchestratorAsync(""MyOrchestrator"", input, options); +}} + +public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null) +{{ + return ctx.CallActivityAsync(""MyActivity"", input, options); +}} + +[Function(nameof(MyActivity))] +public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext) +{{ + ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices); + TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId); + object? result = await activity.RunAsync(context, input); + return (string)result!; +}} + +[Function(nameof(MyEntity))] +public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher) +{{ + return dispatcher.DispatchAsync(); +}} +{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}", + isDurableFunctions: true); + + await TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: true); + } }