diff --git a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
index 8531f38a7..69f3ff811 100644
--- a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
+++ b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
@@ -5,6 +5,9 @@
v4
Exe
enable
+
+ false
+ false
@@ -12,7 +15,8 @@
-
+
+
diff --git a/samples/AzureFunctionsApp/Entities/Counter.cs b/samples/AzureFunctionsApp/Entities/Counter.cs
index b960d3865..38697da98 100644
--- a/samples/AzureFunctionsApp/Entities/Counter.cs
+++ b/samples/AzureFunctionsApp/Entities/Counter.cs
@@ -29,7 +29,11 @@ namespace AzureFunctionsApp.Entities;
/// Example on how to dispatch to an entity which directly implements TaskEntity. Using TaskEntity gives
/// the added benefit of being able to use DI. When using TaskEntity, state is deserialized to the "State"
/// property. No other properties on this type will be serialized/deserialized.
+///
+/// Source generators are used to generate the [Function] method with [EntityTrigger] binding automatically.
+/// The generated function will be named "Counter" (based on the class name or the DurableTask attribute value).
///
+[DurableTask(nameof(Counter))]
public class Counter : TaskEntity
{
readonly ILogger logger;
@@ -49,19 +53,21 @@ public int Add(int input)
public void Reset() => this.State = 0;
- [Function("Counter")]
- public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
- {
- // Can dispatch to a TaskEntity by passing a instance.
- return dispatcher.DispatchAsync(this);
- }
+ // Note: The [Function("Counter")] method is now auto-generated by the source generator.
+ // The generated code will look like:
+ // [Function(nameof(Counter))]
+ // public static Task Counter([EntityTrigger] TaskEntityDispatcher dispatcher)
+ // {
+ // return dispatcher.DispatchAsync();
+ // }
[Function("Counter_Alt")]
public static Task DispatchStaticAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
- // Can also dispatch to a TaskEntity by using a static method.
- // However, this is a different entity ID - "counter_alt" and not "counter". Even though it uses the same
- // entity implementation, the function attribute has a different name, which determines the entity ID.
+ // This is kept as a manual example showing how to create an alternative entity function
+ // with a different name. This creates a separate entity ID "counter_alt" vs "counter".
+ // Even though it uses the same entity implementation, the function attribute has a different name,
+ // which determines the entity ID.
return dispatcher.DispatchAsync();
}
}
diff --git a/samples/AzureFunctionsApp/Entities/Lifetime.cs b/samples/AzureFunctionsApp/Entities/Lifetime.cs
index ed6c1b095..5a219bac2 100644
--- a/samples/AzureFunctionsApp/Entities/Lifetime.cs
+++ b/samples/AzureFunctionsApp/Entities/Lifetime.cs
@@ -4,6 +4,7 @@
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Entities;
@@ -16,7 +17,10 @@ namespace AzureFunctionsApp.Entities;
/// is considered deleted when is null at the end of an operation. It
/// is also possible to design an entity which remains stateless by always returning null from
/// and never assigning a non-null state.
+///
+/// Source generators are used to generate the [Function] method with [EntityTrigger] binding automatically.
///
+[DurableTask(nameof(Lifetime))]
public class Lifetime : TaskEntity
{
readonly ILogger logger;
@@ -34,14 +38,13 @@ public Lifetime(ILogger logger)
///
protected override bool AllowStateDispatch => base.AllowStateDispatch;
- // NOTE: when using TaskEntity, you cannot use "RunAsync" as the entity trigger name as this conflicts
- // with the base class method 'RunAsync'.
- [Function(nameof(Lifetime))]
- public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
- {
- this.logger.LogInformation("Dispatching entity");
- return dispatcher.DispatchAsync(this);
- }
+ // Note: The [Function(nameof(Lifetime))] method is now auto-generated by the source generator.
+ // The generated code will look like:
+ // [Function(nameof(Lifetime))]
+ // public static Task Lifetime([EntityTrigger] TaskEntityDispatcher dispatcher)
+ // {
+ // return dispatcher.DispatchAsync();
+ // }
public MyState Get() => this.State;
diff --git a/samples/AzureFunctionsApp/Entities/User.cs b/samples/AzureFunctionsApp/Entities/User.cs
index c66630a9c..3fb822457 100644
--- a/samples/AzureFunctionsApp/Entities/User.cs
+++ b/samples/AzureFunctionsApp/Entities/User.cs
@@ -18,7 +18,11 @@ public record UserUpdate(string? Name, int? Age);
///
/// This sample demonstrates how to bind to as well as dispatch to orchestrations.
+///
+/// Source generators are used to generate the [Function] method with [EntityTrigger] binding automatically.
+/// The generated function will be named "User" (based on the class name or the DurableTask attribute value).
///
+[DurableTask(nameof(User))]
public class UserEntity : TaskEntity
{
readonly ILogger logger;
@@ -68,12 +72,13 @@ public void Greet(TaskEntityContext context, string? message = null)
// this.Context.ScheduleNewOrchestration(nameof(Greeting.GreetingOrchestration), input);
}
- [Function(nameof(User))]
- public Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
- {
- // Can dispatch to a TaskEntity by passing a instance.
- return dispatcher.DispatchAsync(this);
- }
+ // Note: The [Function(nameof(User))] method is now auto-generated by the source generator.
+ // The generated code will look like:
+ // [Function(nameof(User))]
+ // public static Task User([EntityTrigger] TaskEntityDispatcher dispatcher)
+ // {
+ // return dispatcher.DispatchAsync();
+ // }
protected override User InitializeState(TaskEntityOperation entityOperation)
{
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index c45adca13..986f7ec3e 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 63dd768de..6ae9523d6 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);
+ }
}