Skip to content

Commit 56c3e79

Browse files
CopilotYunchuWang
andcommitted
Add durable function entity source generation support and tests
Co-authored-by: YunchuWang <[email protected]>
1 parent 4908489 commit 56c3e79

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

src/Generators/DurableTaskSourceGenerator.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,15 @@ public static class GeneratedDurableTaskExtensions
270270
}
271271
}
272272

273+
foreach (DurableTaskTypeInfo entity in entities)
274+
{
275+
if (isDurableFunctions)
276+
{
277+
// Generate the function definition required to trigger entities in Azure Functions
278+
AddEntityFunctionDeclaration(sourceBuilder, entity);
279+
}
280+
}
281+
273282
// Activity function triggers are supported for code-gen (but not orchestration triggers)
274283
IEnumerable<DurableFunction> activityTriggers = allFunctions.Where(
275284
df => df.Kind == DurableFunctionKind.Activity);
@@ -368,6 +377,17 @@ static void AddActivityFunctionDeclaration(StringBuilder sourceBuilder, DurableT
368377
}}");
369378
}
370379

380+
static void AddEntityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo entity)
381+
{
382+
// Generate the entity trigger function that dispatches to the entity implementation.
383+
sourceBuilder.AppendLine($@"
384+
[Function(nameof({entity.TaskName}))]
385+
public static Task {entity.TaskName}([EntityTrigger] TaskEntityDispatcher dispatcher)
386+
{{
387+
return dispatcher.DispatchAsync<{entity.TypeName}>();
388+
}}");
389+
}
390+
371391
/// <summary>
372392
/// Adds a custom ITaskActivityContext implementation used by code generated from <see cref="AddActivityFunctionDeclaration"/>.
373393
/// </summary>

test/Generators.Tests/AzureFunctionsTests.cs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,227 @@ await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
312312
expectedOutput,
313313
isDurableFunctions: true);
314314
}
315+
316+
/// <summary>
317+
/// Verifies that using the class-based syntax for authoring entities generates
318+
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
319+
/// </summary>
320+
/// <param name="stateType">The entity state type.</param>
321+
[Theory]
322+
[InlineData("int")]
323+
[InlineData("string")]
324+
public async Task Entities_ClassBasedSyntax(string stateType)
325+
{
326+
string code = $@"
327+
#nullable enable
328+
using System.Threading.Tasks;
329+
using Microsoft.DurableTask;
330+
using Microsoft.DurableTask.Entities;
331+
332+
namespace MyNS
333+
{{
334+
[DurableTask(nameof(MyEntity))]
335+
public class MyEntity : TaskEntity<{stateType}>
336+
{{
337+
public {stateType} Get() => this.State;
338+
}}
339+
}}";
340+
341+
string expectedOutput = TestHelpers.WrapAndFormat(
342+
GeneratedClassName,
343+
methodList: @"
344+
[Function(nameof(MyEntity))]
345+
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
346+
{
347+
return dispatcher.DispatchAsync<MyNS.MyEntity>();
348+
}",
349+
isDurableFunctions: true);
350+
351+
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
352+
GeneratedFileName,
353+
code,
354+
expectedOutput,
355+
isDurableFunctions: true);
356+
}
357+
358+
/// <summary>
359+
/// Verifies that using the class-based syntax for authoring entities with inheritance generates
360+
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
361+
/// </summary>
362+
/// <param name="stateType">The entity state type.</param>
363+
[Theory]
364+
[InlineData("int")]
365+
[InlineData("string")]
366+
public async Task Entities_ClassBasedSyntax_Inheritance(string stateType)
367+
{
368+
string code = $@"
369+
#nullable enable
370+
using System.Threading.Tasks;
371+
using Microsoft.DurableTask;
372+
using Microsoft.DurableTask.Entities;
373+
374+
namespace MyNS
375+
{{
376+
[DurableTask]
377+
public class MyEntity : MyEntityBase
378+
{{
379+
public override {stateType} Get() => this.State;
380+
}}
381+
382+
public abstract class MyEntityBase : TaskEntity<{stateType}>
383+
{{
384+
public abstract {stateType} Get();
385+
}}
386+
}}";
387+
388+
string expectedOutput = TestHelpers.WrapAndFormat(
389+
GeneratedClassName,
390+
methodList: @"
391+
[Function(nameof(MyEntity))]
392+
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
393+
{
394+
return dispatcher.DispatchAsync<MyNS.MyEntity>();
395+
}",
396+
isDurableFunctions: true);
397+
398+
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
399+
GeneratedFileName,
400+
code,
401+
expectedOutput,
402+
isDurableFunctions: true);
403+
}
404+
405+
/// <summary>
406+
/// Verifies that using the class-based syntax for authoring entities with custom state types generates
407+
/// <see cref="EntityTriggerAttribute"/> function triggers for Azure Functions.
408+
/// </summary>
409+
[Fact]
410+
public async Task Entities_ClassBasedSyntax_CustomStateType()
411+
{
412+
string code = @"
413+
#nullable enable
414+
using System.Threading.Tasks;
415+
using Microsoft.DurableTask;
416+
using Microsoft.DurableTask.Entities;
417+
418+
namespace MyNS
419+
{
420+
public class MyState
421+
{
422+
public int Value { get; set; }
423+
}
424+
425+
[DurableTask(nameof(MyEntity))]
426+
public class MyEntity : TaskEntity<MyState>
427+
{
428+
public MyState Get() => this.State;
429+
}
430+
}";
431+
432+
string expectedOutput = TestHelpers.WrapAndFormat(
433+
GeneratedClassName,
434+
methodList: @"
435+
[Function(nameof(MyEntity))]
436+
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
437+
{
438+
return dispatcher.DispatchAsync<MyNS.MyEntity>();
439+
}",
440+
isDurableFunctions: true);
441+
442+
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
443+
GeneratedFileName,
444+
code,
445+
expectedOutput,
446+
isDurableFunctions: true);
447+
}
448+
449+
/// <summary>
450+
/// Verifies that using the class-based syntax for authoring a mix of orchestrators, activities,
451+
/// and entities generates the appropriate function triggers for Azure Functions.
452+
/// </summary>
453+
[Fact]
454+
public async Task Mixed_OrchestratorActivityEntity_ClassBasedSyntax()
455+
{
456+
string code = @"
457+
#nullable enable
458+
using System;
459+
using System.Threading.Tasks;
460+
using Microsoft.DurableTask;
461+
using Microsoft.DurableTask.Entities;
462+
463+
namespace MyNS
464+
{
465+
[DurableTask(nameof(MyOrchestrator))]
466+
public class MyOrchestrator : TaskOrchestrator<int, string>
467+
{
468+
public override Task<string> RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
469+
}
470+
471+
[DurableTask(nameof(MyActivity))]
472+
public class MyActivity : TaskActivity<int, string>
473+
{
474+
public override Task<string> RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
475+
}
476+
477+
[DurableTask(nameof(MyEntity))]
478+
public class MyEntity : TaskEntity<int>
479+
{
480+
public int Get() => this.State;
481+
}
482+
}";
483+
484+
string expectedOutput = TestHelpers.WrapAndFormat(
485+
GeneratedClassName,
486+
methodList: $@"
487+
static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
488+
489+
[Function(nameof(MyOrchestrator))]
490+
public static Task<string> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
491+
{{
492+
return singletonMyOrchestrator.RunAsync(context, context.GetInput<int>())
493+
.ContinueWith(t => (string)(t.Result ?? default(string)!), TaskContinuationOptions.ExecuteSynchronously);
494+
}}
495+
496+
/// <inheritdoc cref=""IOrchestrationSubmitter.ScheduleNewOrchestrationInstanceAsync""/>
497+
public static Task<string> ScheduleNewMyOrchestratorInstanceAsync(
498+
this IOrchestrationSubmitter client, int input, StartOrchestrationOptions? options = null)
499+
{{
500+
return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options);
501+
}}
502+
503+
/// <inheritdoc cref=""TaskOrchestrationContext.CallSubOrchestratorAsync(TaskName, object?, TaskOptions?)""/>
504+
public static Task<string> CallMyOrchestratorAsync(
505+
this TaskOrchestrationContext context, int input, TaskOptions? options = null)
506+
{{
507+
return context.CallSubOrchestratorAsync<string>(""MyOrchestrator"", input, options);
508+
}}
509+
510+
public static Task<string> CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
511+
{{
512+
return ctx.CallActivityAsync<string>(""MyActivity"", input, options);
513+
}}
514+
515+
[Function(nameof(MyActivity))]
516+
public static async Task<string> MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
517+
{{
518+
ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<MyNS.MyActivity>(executionContext.InstanceServices);
519+
TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
520+
object? result = await activity.RunAsync(context, input);
521+
return (string)result!;
522+
}}
523+
524+
[Function(nameof(MyEntity))]
525+
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
526+
{{
527+
return dispatcher.DispatchAsync<MyNS.MyEntity>();
528+
}}
529+
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
530+
isDurableFunctions: true);
531+
532+
await TestHelpers.RunTestAsync<DurableTaskSourceGenerator>(
533+
GeneratedFileName,
534+
code,
535+
expectedOutput,
536+
isDurableFunctions: true);
537+
}
315538
}

0 commit comments

Comments
 (0)