Skip to content

Commit 58a169d

Browse files
CopilotYunchuWangCopilot
authored
Improve DurableTask source generator detection and add optional project type configuration (#575)
* Initial plan * Add DurableTaskGeneratorProjectType configuration option - Add support for MSBuild property DurableTaskGeneratorProjectType - Support values: DurableFunctions, Worker, DurableTaskScheduler, Auto - Add comprehensive unit tests for all configuration scenarios - All 46 generator tests passing Co-authored-by: YunchuWang <[email protected]> * Add documentation for DurableTaskGeneratorProjectType configuration Co-authored-by: YunchuWang <[email protected]> * Refactor project type checking to use HashSet for better maintainability Co-authored-by: YunchuWang <[email protected]> * Simplify project type configuration to use Functions and Standalone modes only - Changed from multiple naming variations to two clear modes: "Functions" and "Standalone" - Updated generator code to only accept "Functions" or "Standalone" (plus "Auto") - Updated documentation to reflect simplified configuration - Updated all tests to use new naming convention - Reduced from 8 to 7 tests by consolidating similar scenarios Co-authored-by: YunchuWang <[email protected]> * Resolve merge conflicts with main branch - Added DurableEventTypeInfo support from main branch - Updated Initialize method to include durableEventAttributes provider - Updated Execute method signature to include allEvents parameter - Added GetDurableEventTypeInfo method - Added DurableEventTypeInfo class - Preserved projectType parameter functionality - All 45 tests passing Co-authored-by: YunchuWang <[email protected]> * Improve project type detection to check for Azure Functions trigger attributes - Changed primary detection mechanism from assembly references to actual usage of trigger attributes - Now checks if any methods use OrchestrationTrigger, ActivityTrigger, or EntityTrigger - Solves the original issue: projects with transitive Functions dependencies now correctly generate standalone code - Assembly reference check kept as fallback for edge cases - MSBuild property still available as manual override when needed - Updated documentation to explain the improved detection logic - All 50 tests passing Co-authored-by: YunchuWang <[email protected]> * Update src/Generators/DurableTaskSourceGenerator.cs Co-authored-by: Copilot <[email protected]> * Add tests for null projectType (default behavior) - Added test for null projectType without Functions reference (generates Standalone) - Added test for null projectType with Functions reference (detects Functions) - These tests verify default behavior when no MSBuild property is set - All 52 tests passing (50 original + 2 new) Co-authored-by: YunchuWang <[email protected]> * Add test for auto-detection via trigger attributes - Added AutoDetect_WithTriggerAttributes_GeneratesFunctionsCode test - Validates that presence of [ActivityTrigger] causes auto-detection as Functions project - Tests the allFunctions.IsDefaultOrEmpty check in DetermineIsDurableFunctions - All 53 tests passing (52 original + 1 new) Co-authored-by: YunchuWang <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: YunchuWang <[email protected]> Co-authored-by: wangbill <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent c96a6bf commit 58a169d

File tree

4 files changed

+663
-12
lines changed

4 files changed

+663
-12
lines changed

src/Generators/DurableTaskSourceGenerator.cs

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6363
transform: static (ctx, _) => GetDurableFunction(ctx))
6464
.Where(static func => func != null)!;
6565

66+
// Get the project type configuration from MSBuild properties
67+
IncrementalValueProvider<string?> projectTypeProvider = context.AnalyzerConfigOptionsProvider
68+
.Select(static (provider, _) =>
69+
{
70+
provider.GlobalOptions.TryGetValue("build_property.DurableTaskGeneratorProjectType", out string? projectType);
71+
return projectType;
72+
});
73+
6674
// Collect all results and check if Durable Functions is referenced
67-
IncrementalValueProvider<(Compilation, ImmutableArray<DurableTaskTypeInfo>, ImmutableArray<DurableEventTypeInfo>, ImmutableArray<DurableFunction>)> compilationAndTasks =
75+
IncrementalValueProvider<(Compilation, ImmutableArray<DurableTaskTypeInfo>, ImmutableArray<DurableEventTypeInfo>, ImmutableArray<DurableFunction>, string?)> compilationAndTasks =
6876
durableTaskAttributes.Collect()
6977
.Combine(durableEventAttributes.Collect())
7078
.Combine(durableFunctions.Collect())
7179
.Combine(context.CompilationProvider)
80+
.Combine(projectTypeProvider)
7281
// Roslyn's IncrementalValueProvider.Combine creates nested tuple pairs: ((Left, Right), Right)
7382
// After multiple .Combine() calls, we unpack the nested structure:
74-
// x.Right = Compilation
75-
// x.Left.Left.Left = DurableTaskAttributes (orchestrators, activities, entities)
76-
// x.Left.Left.Right = DurableEventAttributes (events)
77-
// x.Left.Right = DurableFunctions (Azure Functions metadata)
78-
.Select((x, _) => (x.Right, x.Left.Left.Left, x.Left.Left.Right, x.Left.Right));
83+
// x.Right = projectType (string?)
84+
// x.Left.Right = Compilation
85+
// x.Left.Left.Left.Left = DurableTaskAttributes (orchestrators, activities, entities)
86+
// x.Left.Left.Left.Right = DurableEventAttributes (events)
87+
// x.Left.Left.Right = DurableFunctions (Azure Functions metadata)
88+
.Select((x, _) => (x.Left.Right, x.Left.Left.Left.Left, x.Left.Left.Left.Right, x.Left.Left.Right, x.Right));
7989

8090
// Generate the source
81-
context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3, source.Item4));
91+
context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3, source.Item4, source.Item5));
8292
}
8393

8494
static DurableTaskTypeInfo? GetDurableTaskTypeInfo(GeneratorSyntaxContext context)
@@ -235,17 +245,16 @@ static void Execute(
235245
Compilation compilation,
236246
ImmutableArray<DurableTaskTypeInfo> allTasks,
237247
ImmutableArray<DurableEventTypeInfo> allEvents,
238-
ImmutableArray<DurableFunction> allFunctions)
248+
ImmutableArray<DurableFunction> allFunctions,
249+
string? projectType)
239250
{
240251
if (allTasks.IsDefaultOrEmpty && allEvents.IsDefaultOrEmpty && allFunctions.IsDefaultOrEmpty)
241252
{
242253
return;
243254
}
244255

245-
// This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
246-
// code if we find the Durable Functions extension listed in the set of referenced assembly names.
247-
bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any(
248-
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
256+
// Determine if we should generate Durable Functions specific code
257+
bool isDurableFunctions = DetermineIsDurableFunctions(compilation, allFunctions, projectType);
249258

250259
// Separate tasks into orchestrators, activities, and entities
251260
List<DurableTaskTypeInfo> orchestrators = new();
@@ -381,6 +390,49 @@ public static class GeneratedDurableTaskExtensions
381390
context.AddSource("GeneratedDurableTaskExtensions.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
382391
}
383392

393+
/// <summary>
394+
/// Determines whether the current project should be treated as an Azure Functions-based Durable Functions project.
395+
/// </summary>
396+
/// <param name="compilation">The Roslyn compilation for the project, used to inspect referenced assemblies.</param>
397+
/// <param name="allFunctions">The collection of discovered Durable Functions triggers in the project.</param>
398+
/// <param name="projectType">
399+
/// An optional project type hint. When set to <c>"Functions"</c> or <c>"Standalone"</c>, this value takes precedence
400+
/// over automatic detection. Any other value (including <c>"Auto"</c>) falls back to auto-detection.
401+
/// </param>
402+
/// <returns>
403+
/// <c>true</c> if the project is determined to be a Durable Functions (Azure Functions) project; otherwise, <c>false</c>.
404+
/// </returns>
405+
static bool DetermineIsDurableFunctions(Compilation compilation, ImmutableArray<DurableFunction> allFunctions, string? projectType)
406+
{
407+
// Check if the user has explicitly configured the project type
408+
if (!string.IsNullOrWhiteSpace(projectType))
409+
{
410+
// Explicit configuration takes precedence
411+
if (projectType!.Equals("Functions", StringComparison.OrdinalIgnoreCase))
412+
{
413+
return true;
414+
}
415+
else if (projectType.Equals("Standalone", StringComparison.OrdinalIgnoreCase))
416+
{
417+
return false;
418+
}
419+
// If "Auto" or unrecognized value, fall through to auto-detection
420+
}
421+
422+
// Auto-detect based on the presence of Azure Functions trigger attributes
423+
// If we found any methods with OrchestrationTrigger, ActivityTrigger, or EntityTrigger attributes,
424+
// then this is a Durable Functions project
425+
if (!allFunctions.IsDefaultOrEmpty)
426+
{
427+
return true;
428+
}
429+
430+
// Fallback: check if Durable Functions assembly is referenced
431+
// This handles edge cases where the project references the assembly but hasn't defined triggers yet
432+
return compilation.ReferencedAssemblyNames.Any(
433+
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
434+
}
435+
384436
static void AddOrchestratorFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator)
385437
{
386438
sourceBuilder.AppendLine($@"

src/Generators/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
11
Source generators for `Microsoft.DurableTask`
22

3+
## Overview
4+
5+
The `Microsoft.DurableTask.Generators` package provides source generators that automatically generate type-safe extension methods for orchestrators and activities. The generator automatically detects whether you're using Azure Functions or the Durable Task Scheduler and generates appropriate code for your environment.
6+
7+
## Configuration
8+
9+
### Project Type Detection
10+
11+
The generator uses intelligent automatic detection to determine the project type:
12+
13+
1. **Primary Detection**: Checks for Azure Functions trigger attributes (`OrchestrationTrigger`, `ActivityTrigger`, `EntityTrigger`) in your code
14+
- If any methods use these trigger attributes, it generates Azure Functions-specific code
15+
- Otherwise, it generates standalone Durable Task Worker code
16+
17+
2. **Fallback Detection**: If no trigger attributes are found, checks if `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` is referenced
18+
- This handles projects that reference the Functions package but haven't defined triggers yet
19+
20+
This automatic detection solves the common issue where transitive dependencies on Functions packages would incorrectly trigger Functions mode even when not using Azure Functions.
21+
22+
### Explicit Project Type Configuration (Optional)
23+
24+
In rare scenarios where you need to override the automatic detection, you can explicitly configure the project type using the `DurableTaskGeneratorProjectType` MSBuild property in your `.csproj` file:
25+
26+
```xml
27+
<PropertyGroup>
28+
<DurableTaskGeneratorProjectType>Standalone</DurableTaskGeneratorProjectType>
29+
</PropertyGroup>
30+
```
31+
32+
#### Supported Values
33+
34+
- `Auto` (default): Automatically detects project type using the intelligent detection described above
35+
- `Functions`: Forces generation of Azure Functions-specific code
36+
- `Standalone`: Forces generation of standalone Durable Task Worker code (includes `AddAllGeneratedTasks` method)
37+
38+
#### Example: Force Standalone Mode
39+
40+
```xml
41+
<Project Sdk="Microsoft.NET.Sdk.Web">
42+
<PropertyGroup>
43+
<TargetFramework>net8.0</TargetFramework>
44+
<DurableTaskGeneratorProjectType>Standalone</DurableTaskGeneratorProjectType>
45+
</PropertyGroup>
46+
47+
<ItemGroup>
48+
<PackageReference Include="Microsoft.DurableTask.Generators" OutputItemType="Analyzer" />
49+
<!-- Your other package references -->
50+
</ItemGroup>
51+
</Project>
52+
```
53+
54+
With standalone mode, the generator produces the `AddAllGeneratedTasks` extension method for worker registration:
55+
56+
```csharp
57+
builder.Services.AddDurableTaskWorker(builder =>
58+
{
59+
builder.AddTasks(r => r.AddAllGeneratedTasks());
60+
});
61+
```
62+
363
For more information, see https://github.com/microsoft/durabletask-dotnet

0 commit comments

Comments
 (0)