Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion samples/AzureFunctionsApp/AzureFunctionsApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<!-- Disable SDK's source generation to allow reflection-based discovery of source-generated functions -->
<FunctionsEnableExecutorSourceGen>false</FunctionsEnableExecutorSourceGen>
<FunctionsEnableWorkerIndexing>false</FunctionsEnableWorkerIndexing>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.Functions.Worker" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" OutputItemType="Analyzer" />
<PackageReference Include="Microsoft.DurableTask.Generators" OutputItemType="Analyzer" />
<!-- Reference the source generator project directly for local development -->
<ProjectReference Include="..\..\src\Generators\Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 15 additions & 9 deletions samples/AzureFunctionsApp/Entities/Counter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ namespace AzureFunctionsApp.Entities;
/// Example on how to dispatch to an entity which directly implements TaskEntity<TState>. Using TaskEntity<TState> gives
/// the added benefit of being able to use DI. When using TaskEntity<TState>, 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).
/// </summary>
[DurableTask(nameof(Counter))]
public class Counter : TaskEntity<int>
{
readonly ILogger logger;
Expand All @@ -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<TState> 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<Counter>();
// }

[Function("Counter_Alt")]
public static Task DispatchStaticAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
// Can also dispatch to a TaskEntity<TState> 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<Counter>();
}
}
Expand Down
19 changes: 11 additions & 8 deletions samples/AzureFunctionsApp/Entities/Lifetime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,7 +17,10 @@ namespace AzureFunctionsApp.Entities;
/// is considered deleted when <see cref="TaskEntity{TState}.State"/> is <c>null</c> at the end of an operation. It
/// is also possible to design an entity which remains stateless by always returning <c>null</c> from
/// <see cref="InitializeState"/> and never assigning a non-null state.
///
/// Source generators are used to generate the [Function] method with [EntityTrigger] binding automatically.
/// </summary>
[DurableTask(nameof(Lifetime))]
public class Lifetime : TaskEntity<MyState>
{
readonly ILogger logger;
Expand All @@ -34,14 +38,13 @@ public Lifetime(ILogger<Lifetime> logger)
/// </summary>
protected override bool AllowStateDispatch => base.AllowStateDispatch;

// NOTE: when using TaskEntity<TState>, 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<Lifetime>();
// }

public MyState Get() => this.State;

Expand Down
17 changes: 11 additions & 6 deletions samples/AzureFunctionsApp/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ public record UserUpdate(string? Name, int? Age);

/// <summary>
/// This sample demonstrates how to bind to <see cref="TaskEntityContext"/> 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).
/// </summary>
[DurableTask(nameof(User))]
public class UserEntity : TaskEntity<User>
{
readonly ILogger logger;
Expand Down Expand Up @@ -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<TState> 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<UserEntity>();
// }

protected override User InitializeState(TaskEntityOperation entityOperation)
{
Expand Down
20 changes: 20 additions & 0 deletions src/Generators/DurableTaskSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DurableFunction> activityTriggers = allFunctions.Where(
df => df.Kind == DurableFunctionKind.Activity);
Expand Down Expand Up @@ -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}>();
}}");
}

/// <summary>
/// Adds a custom ITaskActivityContext implementation used by code generated from <see cref="AddActivityFunctionDeclaration"/>.
/// </summary>
Expand Down
223 changes: 223 additions & 0 deletions test/Generators.Tests/AzureFunctionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,227 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
expectedOutput,
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities generates
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
/// </summary>
/// <param name="stateType">The entity state type.</param>
[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<MyNS.MyEntity>();
}",
isDurableFunctions: true);

await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities with inheritance generates
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
/// </summary>
/// <param name="stateType">The entity state type.</param>
[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<MyNS.MyEntity>();
}",
isDurableFunctions: true);

await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring entities with custom state types generates
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
/// </summary>
[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<MyState>
{
public MyState Get() => this.State;
}
}";

string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: @"
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync<MyNS.MyEntity>();
}",
isDurableFunctions: true);

await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
isDurableFunctions: true);
}

/// <summary>
/// Verifies that using the class-based syntax for authoring a mix of orchestrators, activities,
/// and entities generates the appropriate function triggers for Azure Functions.
/// </summary>
[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<int, string>
{
public override Task<string> RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
}

[DurableTask(nameof(MyActivity))]
public class MyActivity : TaskActivity<int, string>
{
public override Task<string> RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
}

[DurableTask(nameof(MyEntity))]
public class MyEntity : TaskEntity<int>
{
public int Get() => this.State;
}
}";

string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
methodList: $@"
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();

[Function(nameof(MyOrchestrator))]
public static Task<string> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
{{
return singletonMyOrchestrator.RunAsync(context, context.GetInput<int>())
.ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
}}

/// <inheritdoc cref=""IOrchestrationSubmitter.ScheduleNewOrchestrationInstanceAsync""/>
public static Task<string> ScheduleNewMyOrchestratorInstanceAsync(
this IOrchestrationSubmitter client, int input, StartOrchestrationOptions? options = null)
{{
return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options);
}}

/// <inheritdoc cref=""TaskOrchestrationContext.CallSubOrchestratorAsync(TaskName, object?, TaskOptions?)""/>
public static Task<string> CallMyOrchestratorAsync(
this TaskOrchestrationContext context, int input, TaskOptions? options = null)
{{
return context.CallSubOrchestratorAsync<string>(""MyOrchestrator"", input, options);
}}

public static Task<string> CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
{{
return ctx.CallActivityAsync<string>(""MyActivity"", input, options);
}}

[Function(nameof(MyActivity))]
public static async Task<string> MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
{{
ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<MyNS.MyActivity>(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<MyNS.MyEntity>();
}}
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
isDurableFunctions: true);

await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
GeneratedFileName,
code,
expectedOutput,
isDurableFunctions: true);
}
}
Loading