diff --git a/Directory.Packages.props b/Directory.Packages.props index 4918cef1..b36897e3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,28 +9,28 @@ - - - - - - - - - + + + + + + + + + - + - + @@ -80,10 +80,10 @@ - + - + - + \ No newline at end of file diff --git a/src/Abstractions/DurableTaskAttribute.cs b/src/Abstractions/DurableTaskAttribute.cs index c7a6edce..1b7caa4f 100644 --- a/src/Abstractions/DurableTaskAttribute.cs +++ b/src/Abstractions/DurableTaskAttribute.cs @@ -8,9 +8,10 @@ namespace Microsoft.DurableTask; /// /// /// This attribute is meant to be used on class definitions that derive from -/// or . +/// , , +/// or TaskEntity{TState} from the Microsoft.DurableTask.Entities namespace. /// It is used specifically by build-time source generators to generate type-safe methods for invoking -/// orchestrations or activities. +/// orchestrations, activities, or registering entities. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class DurableTaskAttribute : Attribute diff --git a/src/Generators/AzureFunctions/DurableFunction.cs b/src/Generators/AzureFunctions/DurableFunction.cs index 94925ff8..2f1af7dc 100644 --- a/src/Generators/AzureFunctions/DurableFunction.cs +++ b/src/Generators/AzureFunctions/DurableFunction.cs @@ -10,7 +10,8 @@ public enum DurableFunctionKind { Unknown, Orchestration, - Activity + Activity, + Entity } public class DurableFunction diff --git a/src/Generators/AzureFunctions/SyntaxNodeUtility.cs b/src/Generators/AzureFunctions/SyntaxNodeUtility.cs index 25d98de5..e5bbdf3f 100644 --- a/src/Generators/AzureFunctions/SyntaxNodeUtility.cs +++ b/src/Generators/AzureFunctions/SyntaxNodeUtility.cs @@ -61,6 +61,12 @@ public static bool TryGetFunctionKind(MethodDeclarationSyntax method, out Durabl kind = DurableFunctionKind.Activity; return true; } + + if (attribute.ToString().Equals("EntityTrigger", StringComparison.Ordinal)) + { + kind = DurableFunctionKind.Entity; + return true; + } } } @@ -125,7 +131,8 @@ public static bool TryGetParameter( { string attributeName = attribute.Name.ToString(); if ((kind == DurableFunctionKind.Activity && attributeName == "ActivityTrigger") || - (kind == DurableFunctionKind.Orchestration && attributeName == "OrchestratorTrigger")) + (kind == DurableFunctionKind.Orchestration && attributeName == "OrchestratorTrigger") || + (kind == DurableFunctionKind.Entity && attributeName == "EntityTrigger")) { TypeInfo info = model.GetTypeInfo(methodParam.Type); if (info.Type is INamedTypeSymbol named) diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs index 32821c93..c45adca1 100644 --- a/src/Generators/DurableTaskSourceGenerator.cs +++ b/src/Generators/DurableTaskSourceGenerator.cs @@ -95,7 +95,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) string className = classType.ToDisplayString(); INamedTypeSymbol? taskType = null; - bool isActivity = false; + DurableTaskKind kind = DurableTaskKind.Orchestrator; INamedTypeSymbol? baseType = classType.BaseType; while (baseType != null) @@ -105,13 +105,19 @@ public void Initialize(IncrementalGeneratorInitializationContext context) if (baseType.Name == "TaskActivity") { taskType = baseType; - isActivity = true; + kind = DurableTaskKind.Activity; break; } else if (baseType.Name == "TaskOrchestrator") { taskType = baseType; - isActivity = false; + kind = DurableTaskKind.Orchestrator; + break; + } + else if (baseType.Name == "TaskEntity") + { + taskType = baseType; + kind = DurableTaskKind.Entity; break; } } @@ -119,13 +125,31 @@ public void Initialize(IncrementalGeneratorInitializationContext context) baseType = baseType.BaseType; } - if (taskType == null || taskType.TypeParameters.Length <= 1) + // TaskEntity has 1 type parameter (TState), while TaskActivity and TaskOrchestrator have 2 (TInput, TOutput) + if (taskType == null) { return null; } - ITypeSymbol inputType = taskType.TypeArguments.First(); - ITypeSymbol outputType = taskType.TypeArguments.Last(); + if (kind == DurableTaskKind.Entity) + { + // Entity only has a single TState type parameter + if (taskType.TypeParameters.Length < 1) + { + return null; + } + } + else + { + // Orchestrator and Activity have TInput and TOutput type parameters + if (taskType.TypeParameters.Length <= 1) + { + return null; + } + } + + ITypeSymbol? inputType = kind == DurableTaskKind.Entity ? null : taskType.TypeArguments.First(); + ITypeSymbol? outputType = kind == DurableTaskKind.Entity ? null : taskType.TypeArguments.Last(); string taskName = classType.Name; if (attribute.ArgumentList?.Arguments.Count > 0) @@ -134,7 +158,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) taskName = context.SemanticModel.GetConstantValue(expression).ToString(); } - return new DurableTaskTypeInfo(className, taskName, inputType, outputType, isActivity); + return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind); } static DurableFunction? GetDurableFunction(GeneratorSyntaxContext context) @@ -165,9 +189,10 @@ static void Execute( bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any( assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase)); - // Separate tasks into orchestrators and activities + // Separate tasks into orchestrators, activities, and entities List orchestrators = new(); List activities = new(); + List entities = new(); foreach (DurableTaskTypeInfo task in allTasks) { @@ -175,13 +200,17 @@ static void Execute( { activities.Add(task); } + else if (task.IsEntity) + { + entities.Add(task); + } else { orchestrators.Add(task); } } - int found = activities.Count + orchestrators.Count + allFunctions.Length; + int found = activities.Count + orchestrators.Count + entities.Count + allFunctions.Length; if (found == 0) { return; @@ -264,7 +293,8 @@ public static class GeneratedDurableTaskExtensions AddRegistrationMethodForAllTasks( sourceBuilder, orchestrators, - activities); + activities, + entities); } sourceBuilder.AppendLine(" }").AppendLine("}"); @@ -368,7 +398,8 @@ public GeneratedActivityContext(TaskName name, string instanceId) static void AddRegistrationMethodForAllTasks( StringBuilder sourceBuilder, IEnumerable orchestrators, - IEnumerable activities) + IEnumerable activities, + IEnumerable entities) { // internal so it does not conflict with other projects with this generated file. sourceBuilder.Append($@" @@ -387,11 +418,24 @@ internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistr builder.AddActivity<{taskInfo.TypeName}>();"); } + foreach (DurableTaskTypeInfo taskInfo in entities) + { + sourceBuilder.Append($@" + builder.AddEntity<{taskInfo.TypeName}>();"); + } + sourceBuilder.AppendLine($@" return builder; }}"); } + enum DurableTaskKind + { + Orchestrator, + Activity, + Entity + } + class DurableTaskTypeInfo { public DurableTaskTypeInfo( @@ -399,19 +443,30 @@ public DurableTaskTypeInfo( string taskName, ITypeSymbol? inputType, ITypeSymbol? outputType, - bool isActivity) + DurableTaskKind kind) { this.TypeName = taskType; this.TaskName = taskName; - this.InputType = GetRenderedTypeExpression(inputType); - this.InputParameter = this.InputType + " input"; - if (this.InputType[this.InputType.Length - 1] == '?') + this.Kind = kind; + + // Entities only have a state type parameter, not input/output + if (kind == DurableTaskKind.Entity) { - this.InputParameter += " = default"; + this.InputType = string.Empty; + this.InputParameter = string.Empty; + this.OutputType = string.Empty; } + else + { + this.InputType = GetRenderedTypeExpression(inputType); + this.InputParameter = this.InputType + " input"; + if (this.InputType[this.InputType.Length - 1] == '?') + { + this.InputParameter += " = default"; + } - this.OutputType = GetRenderedTypeExpression(outputType); - this.IsActivity = isActivity; + this.OutputType = GetRenderedTypeExpression(outputType); + } } public string TypeName { get; } @@ -419,7 +474,13 @@ public DurableTaskTypeInfo( public string InputType { get; } public string InputParameter { get; } public string OutputType { get; } - public bool IsActivity { get; } + public DurableTaskKind Kind { get; } + + public bool IsActivity => this.Kind == DurableTaskKind.Activity; + + public bool IsOrchestrator => this.Kind == DurableTaskKind.Orchestrator; + + public bool IsEntity => this.Kind == DurableTaskKind.Entity; static string GetRenderedTypeExpression(ITypeSymbol? symbol) { diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs index 5cfa9d8a..63dd768d 100644 --- a/test/Generators.Tests/AzureFunctionsTests.cs +++ b/test/Generators.Tests/AzureFunctionsTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker; using Microsoft.DurableTask.Generators.Tests.Utils; namespace Microsoft.DurableTask.Generators.Tests; @@ -11,7 +11,7 @@ public class AzureFunctionsTests const string GeneratedClassName = "GeneratedDurableTaskExtensions"; const string GeneratedFileName = $"{GeneratedClassName}.cs"; - [Fact(Skip = "Durable Functions Extension out of date")] + [Fact] public async Task Activities_SimpleFunctionTrigger() { string code = @" @@ -40,7 +40,7 @@ await TestHelpers.RunTestAsync( isDurableFunctions: true); } - [Fact(Skip = "Durable Functions Extension out of date")] + [Fact] public async Task Activities_SimpleFunctionTrigger_TaskReturning() { string code = @" @@ -70,7 +70,7 @@ await TestHelpers.RunTestAsync( isDurableFunctions: true); } - [Fact(Skip = "Durable Functions Extension out of date")] + [Fact] public async Task Activities_SimpleFunctionTrigger_CustomType() { string code = @" @@ -111,7 +111,7 @@ await TestHelpers.RunTestAsync( /// /// The activity input type. /// The activity output type. - [Theory(Skip = "Durable Functions Extension out of date")] + [Theory] [InlineData("int", "string")] [InlineData("string", "int")] [InlineData("Guid", "TimeSpan")] @@ -126,11 +126,18 @@ public async Task Activities_ClassBasedSyntax(string inputType, string outputTyp using Microsoft.DurableTask; [DurableTask(nameof(MyActivity))] -public class MyActivity : TaskActivityBase<{inputType}, {outputType}> +public class MyActivity : TaskActivity<{inputType}, {outputType}> {{ - protected override {outputType} OnRun(TaskActivityContext context, {inputType} input) => default!; + public override Task<{outputType}> RunAsync(TaskActivityContext context, {inputType} input) => Task.FromResult<{outputType}>(default!); }}"; + // Build the expected InputParameter format (matches generator logic) + string expectedInputParameter = inputType + " input"; + if (inputType.EndsWith('?')) + { + expectedInputParameter += " = default"; + } + string expectedOutput = TestHelpers.WrapAndFormat( GeneratedClassName, methodList: $@" @@ -140,9 +147,9 @@ public class MyActivity : TaskActivityBase<{inputType}, {outputType}> }} [Function(nameof(MyActivity))] -public static async Task<{outputType}> MyActivity([ActivityTrigger] {defaultInputType} input, string instanceId, FunctionContext executionContext) +public static async Task<{outputType}> MyActivity([ActivityTrigger] {expectedInputParameter}, string instanceId, FunctionContext executionContext) {{ - ITaskActivity activity = ActivatorUtilities.CreateInstance(executionContext.InstanceServices); + ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices); TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId); object? result = await activity.RunAsync(context, input); return ({outputType})result!; @@ -164,7 +171,7 @@ await TestHelpers.RunTestAsync( /// /// The activity input type. /// The activity output type. - [Theory(Skip = "Durable Functions Extension out of date")] + [Theory] [InlineData("int", "string?")] [InlineData("string", "int")] [InlineData("(int, int)", "(double, double)")] @@ -183,11 +190,18 @@ public async Task Orchestrators_ClassBasedSyntax(string inputType, string output namespace MyNS {{ [DurableTask(nameof(MyOrchestrator))] - public class MyOrchestrator : TaskOrchestratorBase<{inputType}, {outputType}> + public class MyOrchestrator : TaskOrchestrator<{inputType}, {outputType}> {{ - protected override Task<{outputType}> OnRunAsync(TaskOrchestrationContext ctx, {defaultInputType} input) => throw new NotImplementedException(); + public override Task<{outputType}> RunAsync(TaskOrchestrationContext ctx, {defaultInputType} input) => throw new NotImplementedException(); }} }}"; + // Build the expected InputParameter format (matches generator logic) + string expectedInputParameter = inputType + " input"; + if (inputType.EndsWith('?')) + { + expectedInputParameter += " = default"; + } + string expectedOutput = TestHelpers.WrapAndFormat( GeneratedClassName, methodList: $@" @@ -200,32 +214,18 @@ public class MyOrchestrator : TaskOrchestratorBase<{inputType}, {outputType}> .ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously); }} -/// +/// public static Task ScheduleNewMyOrchestratorInstanceAsync( - this DurableTaskClient client, - string? instanceId = null, - {defaultInputType} input = default, - DateTimeOffset? startTime = null) + this IOrchestrationSubmitter client, {expectedInputParameter}, StartOrchestrationOptions? options = null) {{ - return client.ScheduleNewOrchestrationInstanceAsync( - ""MyOrchestrator"", - instanceId, - input, - startTime); + return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options); }} -/// +/// public static Task<{outputType}> CallMyOrchestratorAsync( - this TaskOrchestrationContext context, - string? instanceId = null, - {defaultInputType} input = default, - TaskOptions? options = null) + this TaskOrchestrationContext context, {expectedInputParameter}, TaskOptions? options = null) {{ - return context.CallSubOrchestratorAsync<{outputType}>( - ""MyOrchestrator"", - instanceId, - input, - options); + return context.CallSubOrchestratorAsync<{outputType}>(""MyOrchestrator"", input, options); }}", isDurableFunctions: true); @@ -243,7 +243,7 @@ await TestHelpers.RunTestAsync( /// /// The activity input type. /// The activity output type. - [Theory(Skip = "Durable Functions Extension out of date")] + [Theory] [InlineData("int", "string?")] [InlineData("string", "int")] [InlineData("(int, int)", "(double, double)")] @@ -264,14 +264,21 @@ namespace MyNS [DurableTask] public class MyOrchestrator : MyOrchestratorBase {{ - protected override Task<{outputType}> OnRunAsync(TaskOrchestrationContext ctx, {defaultInputType} input) => throw new NotImplementedException(); + public override Task<{outputType}> RunAsync(TaskOrchestrationContext ctx, {defaultInputType} input) => throw new NotImplementedException(); }} - public abstract class MyOrchestratorBase : TaskOrchestratorBase<{inputType}, {outputType}> + public abstract class MyOrchestratorBase : TaskOrchestrator<{inputType}, {outputType}> {{ }} }}"; // Same output as Orchestrators_ClassBasedSyntax + // Build the expected InputParameter format (matches generator logic) + string expectedInputParameter = inputType + " input"; + if (inputType.EndsWith('?')) + { + expectedInputParameter += " = default"; + } + string expectedOutput = TestHelpers.WrapAndFormat( GeneratedClassName, methodList: $@" @@ -284,32 +291,18 @@ public abstract class MyOrchestratorBase : TaskOrchestratorBase<{inputType}, {ou .ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously); }} -/// +/// public static Task ScheduleNewMyOrchestratorInstanceAsync( - this DurableTaskClient client, - string? instanceId = null, - {defaultInputType} input = default, - DateTimeOffset? startTime = null) + this IOrchestrationSubmitter client, {expectedInputParameter}, StartOrchestrationOptions? options = null) {{ - return client.ScheduleNewOrchestrationInstanceAsync( - ""MyOrchestrator"", - instanceId, - input, - startTime); + return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options); }} -/// +/// public static Task<{outputType}> CallMyOrchestratorAsync( - this TaskOrchestrationContext context, - string? instanceId = null, - {defaultInputType} input = default, - TaskOptions? options = null) + this TaskOrchestrationContext context, {expectedInputParameter}, TaskOptions? options = null) {{ - return context.CallSubOrchestratorAsync<{outputType}>( - ""MyOrchestrator"", - instanceId, - input, - options); + return context.CallSubOrchestratorAsync<{outputType}>(""MyOrchestrator"", input, options); }}", isDurableFunctions: true); diff --git a/test/Generators.Tests/ClassBasedSyntaxTests.cs b/test/Generators.Tests/ClassBasedSyntaxTests.cs index 18bd9d32..d0cf51d5 100644 --- a/test/Generators.Tests/ClassBasedSyntaxTests.cs +++ b/test/Generators.Tests/ClassBasedSyntaxTests.cs @@ -267,4 +267,210 @@ internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistr expectedOutput, isDurableFunctions: false); } + + [Theory] + [InlineData("int")] + [InlineData("string")] + public Task Entities_PrimitiveStateTypes(string type) + { + string code = $@" +#nullable enable +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +[DurableTask(nameof(MyEntity))] +class MyEntity : TaskEntity<{type}> +{{ + public {type} Get() => this.State; +}}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) +{ + builder.AddEntity(); + return builder; +}"); + + return TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: false); + } + + [Fact] + public Task Entities_CustomStateTypes() + { + string code = @" +using System.Threading.Tasks; +using MyNS; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +[DurableTask(nameof(MyEntity))] +class MyEntity : TaskEntity +{ + public MyClass Get() => this.State; +} + +namespace MyNS +{ + public class MyClass { } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + generatedClassName: "GeneratedDurableTaskExtensions", + methodList: @" +internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) +{ + builder.AddEntity(); + return builder; +}"); + + return TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: false); + } + + [Fact] + public Task Entities_ExplicitNaming() + { + // The [DurableTask] attribute is expected to override the entity class name + string code = @" +using System.Threading.Tasks; +using MyNS; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +namespace MyNS +{ + [DurableTask(""MyEntity"")] + class MyEntityImpl : TaskEntity + { + public MyClass Get() => this.State; + } + + public class MyClass { } +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) +{ + builder.AddEntity(); + return builder; +}"); + + return TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: false); + } + + [Fact] + public Task Entities_Inheritance() + { + string code = @" +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +[DurableTask(nameof(MyEntity))] +class MyEntity : MyEntityBase +{ + public override int Get() => this.State; +} + +abstract class MyEntityBase : TaskEntity +{ + public abstract int Get(); +}"; + + // NOTE: Same output as Entities_PrimitiveStateTypes + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) +{ + builder.AddEntity(); + return builder; +}"); + + return TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: false); + } + + [Fact] + public Task Mixed_OrchestratorActivityEntity() + { + // Test that the generator handles a mix of orchestrators, activities and entities + string code = @" +using System.Threading.Tasks; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Entities; + +[DurableTask(nameof(MyOrchestrator))] +class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty); +} + +[DurableTask(nameof(MyActivity))] +class MyActivity : TaskActivity +{ + public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty); +} + +[DurableTask(nameof(MyEntity))] +class MyEntity : TaskEntity +{ + public int Get() => this.State; +}"; + + string expectedOutput = TestHelpers.WrapAndFormat( + GeneratedClassName, + methodList: @" +/// +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); +} + +internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) +{ + builder.AddOrchestrator(); + builder.AddActivity(); + builder.AddEntity(); + return builder; +}"); + + return TestHelpers.RunTestAsync( + GeneratedFileName, + code, + expectedOutput, + isDurableFunctions: false); + } } diff --git a/test/Generators.Tests/Generators.Tests.csproj b/test/Generators.Tests/Generators.Tests.csproj index 61797770..409b69c2 100644 --- a/test/Generators.Tests/Generators.Tests.csproj +++ b/test/Generators.Tests/Generators.Tests.csproj @@ -6,6 +6,8 @@ + + diff --git a/test/Generators.Tests/Utils/TestHelpers.cs b/test/Generators.Tests/Utils/TestHelpers.cs index 8b53462d..67f030d9 100644 --- a/test/Generators.Tests/Utils/TestHelpers.cs +++ b/test/Generators.Tests/Utils/TestHelpers.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Text; +using Microsoft.Azure.Functions.Worker; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; @@ -38,14 +39,15 @@ public static Task RunTestAsync( { // Durable Functions code generation is triggered by the presence of the // Durable Functions worker extension for .NET Isolated. - // Assembly functionsWorkerAbstractions = typeof(TriggerBindingAttribute).Assembly; - // test.TestState.AdditionalReferences.Add(functionsWorkerAbstractions); + Assembly functionsWorkerAbstractions = typeof(FunctionAttribute).Assembly; + test.TestState.AdditionalReferences.Add(functionsWorkerAbstractions); - // Assembly functionsWorkerCore = typeof(FunctionContext).Assembly; - // test.TestState.AdditionalReferences.Add(functionsWorkerCore); + Assembly functionsWorkerCore = typeof(FunctionContext).Assembly; + test.TestState.AdditionalReferences.Add(functionsWorkerCore); - // Assembly durableExtension = typeof(OrchestrationTriggerAttribute).Assembly; - // test.TestState.AdditionalReferences.Add(durableExtension); + // OrchestrationTriggerAttribute and ActivityTriggerAttribute are in the DurableTask extension + Assembly durableExtension = typeof(OrchestrationTriggerAttribute).Assembly; + test.TestState.AdditionalReferences.Add(durableExtension); Assembly dependencyInjection = typeof(ActivatorUtilities).Assembly; test.TestState.AdditionalReferences.Add(dependencyInjection);