From 70c741b2818d3b2ee12aeca7864146c2ee4bbcf2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 18 Dec 2025 23:18:23 +0000
Subject: [PATCH 01/10] Initial plan
From c87aac05a35ca2f39e671d2fe383fc56494daa4d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 18 Dec 2025 23:31:29 +0000
Subject: [PATCH 02/10] Add validation for C# identifier names in source
generator
- Add DiagnosticDescriptors for invalid task and event names
- Add IsValidCSharpIdentifier helper using SyntaxFacts.IsValidIdentifier
- Update DurableTaskTypeInfo and DurableEventTypeInfo to track name locations
- Report diagnostics for invalid identifiers and skip code generation
- Add comprehensive tests for various invalid identifier scenarios
- All 59 tests passing
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 102 +++++++-
.../InvalidIdentifierTests.cs | 238 ++++++++++++++++++
2 files changed, 334 insertions(+), 6 deletions(-)
create mode 100644 test/Generators.Tests/InvalidIdentifierTests.cs
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 0b4e717e..891bec4c 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -39,6 +39,32 @@ public class DurableTaskSourceGenerator : IIncrementalGenerator
* }
*/
+ ///
+ /// Diagnostic ID for invalid task names.
+ ///
+ const string InvalidTaskNameDiagnosticId = "DURABLE1001";
+
+ ///
+ /// Diagnostic ID for invalid event names.
+ ///
+ const string InvalidEventNameDiagnosticId = "DURABLE1002";
+
+ static readonly DiagnosticDescriptor InvalidTaskNameRule = new(
+ InvalidTaskNameDiagnosticId,
+ title: "Invalid task name",
+ messageFormat: "The task name '{0}' is not a valid C# identifier. Task names must start with a letter or underscore and contain only letters, digits, and underscores.",
+ category: "DurableTask.Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ static readonly DiagnosticDescriptor InvalidEventNameRule = new(
+ InvalidEventNameDiagnosticId,
+ title: "Invalid event name",
+ messageFormat: "The event name '{0}' is not a valid C# identifier. Event names must start with a letter or underscore and contain only letters, digits, and underscores.",
+ category: "DurableTask.Design",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
@@ -166,13 +192,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
ITypeSymbol? outputType = kind == DurableTaskKind.Entity ? null : taskType.TypeArguments.Last();
string taskName = classType.Name;
+ Location? taskNameLocation = null;
if (attribute.ArgumentList?.Arguments.Count > 0)
{
ExpressionSyntax expression = attribute.ArgumentList.Arguments[0].Expression;
taskName = context.SemanticModel.GetConstantValue(expression).ToString();
+ taskNameLocation = expression.GetLocation();
}
- return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind);
+ return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind, taskNameLocation);
}
static DurableEventTypeInfo? GetDurableEventTypeInfo(GeneratorSyntaxContext context)
@@ -204,6 +232,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
string eventName = eventType.Name;
+ Location? eventNameLocation = null;
if (attribute.ArgumentList?.Arguments.Count > 0)
{
@@ -212,10 +241,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
if (constantValue.HasValue && constantValue.Value is string value)
{
eventName = value;
+ eventNameLocation = expression.GetLocation();
}
}
- return new DurableEventTypeInfo(eventName, eventType);
+ return new DurableEventTypeInfo(eventName, eventType, eventNameLocation);
}
static DurableFunction? GetDurableFunction(GeneratorSyntaxContext context)
@@ -230,6 +260,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return null;
}
+ ///
+ /// Checks if a name is a valid C# identifier.
+ ///
+ /// The name to validate.
+ /// True if the name is a valid C# identifier, false otherwise.
+ static bool IsValidCSharpIdentifier(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return false;
+ }
+
+ // Use Roslyn's built-in identifier validation
+ return SyntaxFacts.IsValidIdentifier(name);
+ }
+
static void Execute(
SourceProductionContext context,
Compilation compilation,
@@ -242,18 +288,47 @@ static void Execute(
return;
}
+ // Validate task names and report diagnostics for invalid identifiers
+ foreach (DurableTaskTypeInfo task in allTasks)
+ {
+ if (!IsValidCSharpIdentifier(task.TaskName))
+ {
+ Location location = task.TaskNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ // Validate event names and report diagnostics for invalid identifiers
+ foreach (DurableEventTypeInfo eventInfo in allEvents)
+ {
+ if (!IsValidCSharpIdentifier(eventInfo.EventName))
+ {
+ Location location = eventInfo.EventNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
// This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
// code if we find the Durable Functions extension listed in the set of referenced assembly names.
bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any(
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
// Separate tasks into orchestrators, activities, and entities
+ // Skip tasks with invalid names to avoid generating invalid code
List orchestrators = new();
List activities = new();
List entities = new();
foreach (DurableTaskTypeInfo task in allTasks)
{
+ // Skip tasks with invalid names
+ if (!IsValidCSharpIdentifier(task.TaskName))
+ {
+ continue;
+ }
+
if (task.IsActivity)
{
activities.Add(task);
@@ -268,7 +343,17 @@ static void Execute(
}
}
- int found = activities.Count + orchestrators.Count + entities.Count + allEvents.Length + allFunctions.Length;
+ // Filter out events with invalid names
+ List validEvents = new();
+ foreach (DurableEventTypeInfo eventInfo in allEvents)
+ {
+ if (IsValidCSharpIdentifier(eventInfo.EventName))
+ {
+ validEvents.Add(eventInfo);
+ }
+ }
+
+ int found = activities.Count + orchestrators.Count + entities.Count + validEvents.Count + allFunctions.Length;
if (found == 0)
{
return;
@@ -347,7 +432,7 @@ public static class GeneratedDurableTaskExtensions
}
// Generate WaitFor{EventName}Async methods for each event type
- foreach (DurableEventTypeInfo eventInfo in allEvents)
+ foreach (DurableEventTypeInfo eventInfo in validEvents)
{
AddEventWaitMethod(sourceBuilder, eventInfo);
AddEventSendMethod(sourceBuilder, eventInfo);
@@ -573,11 +658,13 @@ public DurableTaskTypeInfo(
string taskName,
ITypeSymbol? inputType,
ITypeSymbol? outputType,
- DurableTaskKind kind)
+ DurableTaskKind kind,
+ Location? taskNameLocation = null)
{
this.TypeName = taskType;
this.TaskName = taskName;
this.Kind = kind;
+ this.TaskNameLocation = taskNameLocation;
// Entities only have a state type parameter, not input/output
if (kind == DurableTaskKind.Entity)
@@ -605,6 +692,7 @@ public DurableTaskTypeInfo(
public string InputParameter { get; }
public string OutputType { get; }
public DurableTaskKind Kind { get; }
+ public Location? TaskNameLocation { get; }
public bool IsActivity => this.Kind == DurableTaskKind.Activity;
@@ -632,14 +720,16 @@ static string GetRenderedTypeExpression(ITypeSymbol? symbol)
class DurableEventTypeInfo
{
- public DurableEventTypeInfo(string eventName, ITypeSymbol eventType)
+ public DurableEventTypeInfo(string eventName, ITypeSymbol eventType, Location? eventNameLocation = null)
{
this.TypeName = GetRenderedTypeExpression(eventType);
this.EventName = eventName;
+ this.EventNameLocation = eventNameLocation;
}
public string TypeName { get; }
public string EventName { get; }
+ public Location? EventNameLocation { get; }
static string GetRenderedTypeExpression(ITypeSymbol? symbol)
{
diff --git a/test/Generators.Tests/InvalidIdentifierTests.cs b/test/Generators.Tests/InvalidIdentifierTests.cs
new file mode 100644
index 00000000..4abcb55e
--- /dev/null
+++ b/test/Generators.Tests/InvalidIdentifierTests.cs
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.DurableTask.Generators.Tests.Utils;
+
+namespace Microsoft.DurableTask.Generators.Tests;
+
+public class InvalidIdentifierTests
+{
+ const string GeneratedClassName = "GeneratedDurableTaskExtensions";
+ const string GeneratedFileName = $"{GeneratedClassName}.cs";
+
+ [Theory]
+ [InlineData("Foo.Bar")]
+ [InlineData("Foo-Bar")]
+ [InlineData("Foo Bar")]
+ [InlineData("123Invalid")]
+ [InlineData("My-Task")]
+ [InlineData("Task.Name")]
+ [InlineData("@InvalidName")]
+ [InlineData("Task#Name")]
+ public Task Activity_InvalidName_ReportsDiagnostic(string invalidName)
+ {
+ string code = $@"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(""{invalidName}"")]
+class MyActivity : TaskActivity
+{{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}}";
+
+ // The test framework automatically verifies that the expected diagnostic is reported
+ DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ .WithSpan("/0/Test0.cs", 5, 14, 5, 14 + invalidName.Length + 2)
+ .WithArguments(invalidName);
+
+ CSharpSourceGeneratorVerifier.Test test = new()
+ {
+ TestState =
+ {
+ Sources = { code },
+ ExpectedDiagnostics = { expected },
+ AdditionalReferences =
+ {
+ typeof(TaskActivityContext).Assembly,
+ },
+ },
+ };
+
+ return test.RunAsync();
+ }
+
+ [Theory]
+ [InlineData("Foo.Bar")]
+ [InlineData("Foo-Bar")]
+ [InlineData("Foo Bar")]
+ [InlineData("123Invalid")]
+ public Task Orchestrator_InvalidName_ReportsDiagnostic(string invalidName)
+ {
+ string code = $@"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(""{invalidName}"")]
+class MyOrchestrator : TaskOrchestrator
+{{
+ public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
+}}";
+
+ DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ .WithSpan("/0/Test0.cs", 5, 14, 5, 14 + invalidName.Length + 2)
+ .WithArguments(invalidName);
+
+ CSharpSourceGeneratorVerifier.Test test = new()
+ {
+ TestState =
+ {
+ Sources = { code },
+ ExpectedDiagnostics = { expected },
+ AdditionalReferences =
+ {
+ typeof(TaskActivityContext).Assembly,
+ },
+ },
+ };
+
+ return test.RunAsync();
+ }
+
+ [Theory]
+ [InlineData("Foo.Bar")]
+ [InlineData("Foo-Bar")]
+ [InlineData("Event Name")]
+ public Task Event_InvalidName_ReportsDiagnostic(string invalidName)
+ {
+ string code = $@"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableEvent(""{invalidName}"")]
+public sealed record MyEvent(bool Approved);";
+
+ DiagnosticResult expected = new DiagnosticResult("DURABLE1002", DiagnosticSeverity.Error)
+ .WithSpan("/0/Test0.cs", 5, 15, 5, 15 + invalidName.Length + 2)
+ .WithArguments(invalidName);
+
+ CSharpSourceGeneratorVerifier.Test test = new()
+ {
+ TestState =
+ {
+ Sources = { code },
+ ExpectedDiagnostics = { expected },
+ AdditionalReferences =
+ {
+ typeof(TaskActivityContext).Assembly,
+ },
+ },
+ };
+
+ return test.RunAsync();
+ }
+
+ [Fact]
+ public Task Activity_ValidName_NoDiagnostic()
+ {
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(""MyActivity"")]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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.AddActivity();
+ return builder;
+}");
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false);
+ }
+
+ [Fact]
+ public Task Activity_ValidNameWithUnderscore_NoDiagnostic()
+ {
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(""My_Activity"")]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+public static Task CallMy_ActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
+{
+ return ctx.CallActivityAsync(""My_Activity"", input, options);
+}
+
+internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder)
+{
+ builder.AddActivity();
+ return builder;
+}");
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false);
+ }
+
+ [Fact]
+ public Task Activity_InvalidName_NoCodeGenerated()
+ {
+ // When a task has an invalid name, we should report a diagnostic
+ // but NOT generate any code for it (to avoid compilation errors)
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(""Foo.Bar"")]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ .WithSpan("/0/Test0.cs", 5, 14, 5, 23)
+ .WithArguments("Foo.Bar");
+
+ CSharpSourceGeneratorVerifier.Test test = new()
+ {
+ TestState =
+ {
+ Sources = { code },
+ ExpectedDiagnostics = { expected },
+ AdditionalReferences =
+ {
+ typeof(TaskActivityContext).Assembly,
+ },
+ // Don't expect any generated sources since the name is invalid
+ },
+ };
+
+ return test.RunAsync();
+ }
+}
From c83ab737c0f9337bc8bc234e9afbede6e4b26743 Mon Sep 17 00:00:00 2001
From: wangbill
Date: Wed, 31 Dec 2025 17:17:08 -0800
Subject: [PATCH 03/10] Potential fix for pull request finding 'Missed
opportunity to use Where'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 781d467a..b705a17d 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
+using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@@ -344,14 +345,9 @@ static void Execute(
}
// Filter out events with invalid names
- List validEvents = new();
- foreach (DurableEventTypeInfo eventInfo in allEvents)
- {
- if (IsValidCSharpIdentifier(eventInfo.EventName))
- {
- validEvents.Add(eventInfo);
- }
- }
+ List validEvents = allEvents
+ .Where(eventInfo => IsValidCSharpIdentifier(eventInfo.EventName))
+ .ToList();
int found = activities.Count + orchestrators.Count + entities.Count + validEvents.Count + allFunctions.Length;
if (found == 0)
From 59e801114e27030fd834be568f57041498f96b76 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 1 Jan 2026 01:23:21 +0000
Subject: [PATCH 04/10] Fix diagnostic ID conflicts and add analyzer release
tracking
- Change diagnostic IDs from DURABLE1001/1002 to DURABLE3001/3002 to avoid conflicts with existing analyzer IDs
- Add AnalyzerReleases.Shipped.md and AnalyzerReleases.Unshipped.md for source generator diagnostics
- Update tests to use new diagnostic IDs
- Resolves RS2008 warnings about analyzer release tracking
- All 61 tests passing
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/AnalyzerReleases.Shipped.md | 3 +++
src/Generators/AnalyzerReleases.Unshipped.md | 9 +++++++++
src/Generators/DurableTaskSourceGenerator.cs | 4 ++--
test/Generators.Tests/InvalidIdentifierTests.cs | 8 ++++----
4 files changed, 18 insertions(+), 6 deletions(-)
create mode 100644 src/Generators/AnalyzerReleases.Shipped.md
create mode 100644 src/Generators/AnalyzerReleases.Unshipped.md
diff --git a/src/Generators/AnalyzerReleases.Shipped.md b/src/Generators/AnalyzerReleases.Shipped.md
new file mode 100644
index 00000000..d027c512
--- /dev/null
+++ b/src/Generators/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/src/Generators/AnalyzerReleases.Unshipped.md b/src/Generators/AnalyzerReleases.Unshipped.md
new file mode 100644
index 00000000..bee547b6
--- /dev/null
+++ b/src/Generators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,9 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+DURABLE3001 | DurableTask.Design | Error | **DurableTaskSourceGenerator**: Reports when a task name in [DurableTask] attribute is not a valid C# identifier. Task names must start with a letter or underscore and contain only letters, digits, and underscores.
+DURABLE3002 | DurableTask.Design | Error | **DurableTaskSourceGenerator**: Reports when an event name in [DurableEvent] attribute is not a valid C# identifier. Event names must start with a letter or underscore and contain only letters, digits, and underscores.
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index b705a17d..b08c457d 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -43,12 +43,12 @@ public class DurableTaskSourceGenerator : IIncrementalGenerator
///
/// Diagnostic ID for invalid task names.
///
- const string InvalidTaskNameDiagnosticId = "DURABLE1001";
+ const string InvalidTaskNameDiagnosticId = "DURABLE3001";
///
/// Diagnostic ID for invalid event names.
///
- const string InvalidEventNameDiagnosticId = "DURABLE1002";
+ const string InvalidEventNameDiagnosticId = "DURABLE3002";
static readonly DiagnosticDescriptor InvalidTaskNameRule = new(
InvalidTaskNameDiagnosticId,
diff --git a/test/Generators.Tests/InvalidIdentifierTests.cs b/test/Generators.Tests/InvalidIdentifierTests.cs
index 4abcb55e..f0f07722 100644
--- a/test/Generators.Tests/InvalidIdentifierTests.cs
+++ b/test/Generators.Tests/InvalidIdentifierTests.cs
@@ -34,7 +34,7 @@ class MyActivity : TaskActivity
}}";
// The test framework automatically verifies that the expected diagnostic is reported
- DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ DiagnosticResult expected = new DiagnosticResult("DURABLE3001", DiagnosticSeverity.Error)
.WithSpan("/0/Test0.cs", 5, 14, 5, 14 + invalidName.Length + 2)
.WithArguments(invalidName);
@@ -71,7 +71,7 @@ class MyOrchestrator : TaskOrchestrator
public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
}}";
- DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ DiagnosticResult expected = new DiagnosticResult("DURABLE3001", DiagnosticSeverity.Error)
.WithSpan("/0/Test0.cs", 5, 14, 5, 14 + invalidName.Length + 2)
.WithArguments(invalidName);
@@ -104,7 +104,7 @@ public Task Event_InvalidName_ReportsDiagnostic(string invalidName)
[DurableEvent(""{invalidName}"")]
public sealed record MyEvent(bool Approved);";
- DiagnosticResult expected = new DiagnosticResult("DURABLE1002", DiagnosticSeverity.Error)
+ DiagnosticResult expected = new DiagnosticResult("DURABLE3002", DiagnosticSeverity.Error)
.WithSpan("/0/Test0.cs", 5, 15, 5, 15 + invalidName.Length + 2)
.WithArguments(invalidName);
@@ -215,7 +215,7 @@ class MyActivity : TaskActivity
public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
}";
- DiagnosticResult expected = new DiagnosticResult("DURABLE1001", DiagnosticSeverity.Error)
+ DiagnosticResult expected = new DiagnosticResult("DURABLE3001", DiagnosticSeverity.Error)
.WithSpan("/0/Test0.cs", 5, 14, 5, 23)
.WithArguments("Foo.Bar");
From dc69325af3dde8b1266c7a4f17b2635c6610c692 Mon Sep 17 00:00:00 2001
From: wangbill
Date: Wed, 31 Dec 2025 17:35:19 -0800
Subject: [PATCH 05/10] Potential fix for pull request finding 'Missed
opportunity to use Where'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index b08c457d..386aa219 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -322,14 +322,11 @@ static void Execute(
List activities = new();
List entities = new();
- foreach (DurableTaskTypeInfo task in allTasks)
- {
- // Skip tasks with invalid names
- if (!IsValidCSharpIdentifier(task.TaskName))
- {
- continue;
- }
+ IEnumerable validTasks = allTasks
+ .Where(task => IsValidCSharpIdentifier(task.TaskName));
+ foreach (DurableTaskTypeInfo task in validTasks)
+ {
if (task.IsActivity)
{
activities.Add(task);
From 4dce20852f73db8bf1be937a0d4179b842faa2d2 Mon Sep 17 00:00:00 2001
From: wangbill
Date: Wed, 31 Dec 2025 17:37:38 -0800
Subject: [PATCH 06/10] Update src/Generators/DurableTaskSourceGenerator.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 386aa219..ccd1e41e 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -3,8 +3,8 @@
using System.Collections.Immutable;
using System.Diagnostics;
-using System.Text;
using System.Linq;
+using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
From 392ca5e58c2aa608c13a57d9521f81595545b157 Mon Sep 17 00:00:00 2001
From: wangbill
Date: Wed, 31 Dec 2025 17:37:52 -0800
Subject: [PATCH 07/10] Update src/Generators/DurableTaskSourceGenerator.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 28 ++++++++++----------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index ccd1e41e..9211d383 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -290,25 +290,25 @@ static void Execute(
}
// Validate task names and report diagnostics for invalid identifiers
- foreach (DurableTaskTypeInfo task in allTasks)
+ IEnumerable invalidTasks = allTasks
+ .Where(task => !IsValidCSharpIdentifier(task.TaskName));
+
+ foreach (DurableTaskTypeInfo task in invalidTasks)
{
- if (!IsValidCSharpIdentifier(task.TaskName))
- {
- Location location = task.TaskNameLocation ?? Location.None;
- Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
- context.ReportDiagnostic(diagnostic);
- }
+ Location location = task.TaskNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
+ context.ReportDiagnostic(diagnostic);
}
// Validate event names and report diagnostics for invalid identifiers
- foreach (DurableEventTypeInfo eventInfo in allEvents)
+ IEnumerable invalidEvents = allEvents
+ .Where(eventInfo => !IsValidCSharpIdentifier(eventInfo.EventName));
+
+ foreach (DurableEventTypeInfo eventInfo in invalidEvents)
{
- if (!IsValidCSharpIdentifier(eventInfo.EventName))
- {
- Location location = eventInfo.EventNameLocation ?? Location.None;
- Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
- context.ReportDiagnostic(diagnostic);
- }
+ Location location = eventInfo.EventNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
+ context.ReportDiagnostic(diagnostic);
}
// This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
From ebd73f9bc50a27ac7768b44a704f2bb0033d4e75 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 1 Jan 2026 01:40:50 +0000
Subject: [PATCH 08/10] Simplify validation logic per code review feedback
- Changed foreach loops to use simple if statements instead of extracting .Where() filters
- This makes the code more straightforward and easier to read
- All 61 tests passing
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 28 ++++++++++----------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 9211d383..ccd1e41e 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -290,25 +290,25 @@ static void Execute(
}
// Validate task names and report diagnostics for invalid identifiers
- IEnumerable invalidTasks = allTasks
- .Where(task => !IsValidCSharpIdentifier(task.TaskName));
-
- foreach (DurableTaskTypeInfo task in invalidTasks)
+ foreach (DurableTaskTypeInfo task in allTasks)
{
- Location location = task.TaskNameLocation ?? Location.None;
- Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
- context.ReportDiagnostic(diagnostic);
+ if (!IsValidCSharpIdentifier(task.TaskName))
+ {
+ Location location = task.TaskNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidTaskNameRule, location, task.TaskName);
+ context.ReportDiagnostic(diagnostic);
+ }
}
// Validate event names and report diagnostics for invalid identifiers
- IEnumerable invalidEvents = allEvents
- .Where(eventInfo => !IsValidCSharpIdentifier(eventInfo.EventName));
-
- foreach (DurableEventTypeInfo eventInfo in invalidEvents)
+ foreach (DurableEventTypeInfo eventInfo in allEvents)
{
- Location location = eventInfo.EventNameLocation ?? Location.None;
- Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
- context.ReportDiagnostic(diagnostic);
+ if (!IsValidCSharpIdentifier(eventInfo.EventName))
+ {
+ Location location = eventInfo.EventNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
+ context.ReportDiagnostic(diagnostic);
+ }
}
// This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
From 62d3af454645434efe95140b5baf05a0ee7c8289 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 6 Jan 2026 00:07:43 +0000
Subject: [PATCH 09/10] Merge main branch and resolve conflicts
- Resolved merge conflict in DurableTaskSourceGenerator.cs
- Kept validation logic for invalid identifiers
- Adopted new DetermineIsDurableFunctions method from main
- All 71 tests passing (61 original + 10 new from main)
Co-authored-by: YunchuWang <12449837+YunchuWang@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 76 ++-
src/Generators/README.md | 60 ++
.../ProjectTypeConfigurationTests.cs | 514 ++++++++++++++++++
test/Generators.Tests/Utils/TestHelpers.cs | 25 +
4 files changed, 663 insertions(+), 12 deletions(-)
create mode 100644 test/Generators.Tests/ProjectTypeConfigurationTests.cs
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index ccd1e41e..84fd0373 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -90,22 +90,32 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
transform: static (ctx, _) => GetDurableFunction(ctx))
.Where(static func => func != null)!;
+ // Get the project type configuration from MSBuild properties
+ IncrementalValueProvider projectTypeProvider = context.AnalyzerConfigOptionsProvider
+ .Select(static (provider, _) =>
+ {
+ provider.GlobalOptions.TryGetValue("build_property.DurableTaskGeneratorProjectType", out string? projectType);
+ return projectType;
+ });
+
// Collect all results and check if Durable Functions is referenced
- IncrementalValueProvider<(Compilation, ImmutableArray, ImmutableArray, ImmutableArray)> compilationAndTasks =
+ IncrementalValueProvider<(Compilation, ImmutableArray, ImmutableArray, ImmutableArray, string?)> compilationAndTasks =
durableTaskAttributes.Collect()
.Combine(durableEventAttributes.Collect())
.Combine(durableFunctions.Collect())
.Combine(context.CompilationProvider)
+ .Combine(projectTypeProvider)
// Roslyn's IncrementalValueProvider.Combine creates nested tuple pairs: ((Left, Right), Right)
// After multiple .Combine() calls, we unpack the nested structure:
- // x.Right = Compilation
- // x.Left.Left.Left = DurableTaskAttributes (orchestrators, activities, entities)
- // x.Left.Left.Right = DurableEventAttributes (events)
- // x.Left.Right = DurableFunctions (Azure Functions metadata)
- .Select((x, _) => (x.Right, x.Left.Left.Left, x.Left.Left.Right, x.Left.Right));
+ // x.Right = projectType (string?)
+ // x.Left.Right = Compilation
+ // x.Left.Left.Left.Left = DurableTaskAttributes (orchestrators, activities, entities)
+ // x.Left.Left.Left.Right = DurableEventAttributes (events)
+ // x.Left.Left.Right = DurableFunctions (Azure Functions metadata)
+ .Select((x, _) => (x.Left.Right, x.Left.Left.Left.Left, x.Left.Left.Left.Right, x.Left.Left.Right, x.Right));
// Generate the source
- context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3, source.Item4));
+ context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3, source.Item4, source.Item5));
}
static DurableTaskTypeInfo? GetDurableTaskTypeInfo(GeneratorSyntaxContext context)
@@ -282,7 +292,8 @@ static void Execute(
Compilation compilation,
ImmutableArray allTasks,
ImmutableArray allEvents,
- ImmutableArray allFunctions)
+ ImmutableArray allFunctions,
+ string? projectType)
{
if (allTasks.IsDefaultOrEmpty && allEvents.IsDefaultOrEmpty && allFunctions.IsDefaultOrEmpty)
{
@@ -311,10 +322,8 @@ static void Execute(
}
}
- // This generator also supports Durable Functions for .NET isolated, but we only generate Functions-specific
- // code if we find the Durable Functions extension listed in the set of referenced assembly names.
- bool isDurableFunctions = compilation.ReferencedAssemblyNames.Any(
- assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
+ // Determine if we should generate Durable Functions specific code
+ bool isDurableFunctions = DetermineIsDurableFunctions(compilation, allFunctions, projectType);
// Separate tasks into orchestrators, activities, and entities
// Skip tasks with invalid names to avoid generating invalid code
@@ -459,6 +468,49 @@ public static class GeneratedDurableTaskExtensions
context.AddSource("GeneratedDurableTaskExtensions.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
}
+ ///
+ /// Determines whether the current project should be treated as an Azure Functions-based Durable Functions project.
+ ///
+ /// The Roslyn compilation for the project, used to inspect referenced assemblies.
+ /// The collection of discovered Durable Functions triggers in the project.
+ ///
+ /// An optional project type hint. When set to "Functions" or "Standalone", this value takes precedence
+ /// over automatic detection. Any other value (including "Auto") falls back to auto-detection.
+ ///
+ ///
+ /// true if the project is determined to be a Durable Functions (Azure Functions) project; otherwise, false.
+ ///
+ static bool DetermineIsDurableFunctions(Compilation compilation, ImmutableArray allFunctions, string? projectType)
+ {
+ // Check if the user has explicitly configured the project type
+ if (!string.IsNullOrWhiteSpace(projectType))
+ {
+ // Explicit configuration takes precedence
+ if (projectType!.Equals("Functions", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ else if (projectType.Equals("Standalone", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ // If "Auto" or unrecognized value, fall through to auto-detection
+ }
+
+ // Auto-detect based on the presence of Azure Functions trigger attributes
+ // If we found any methods with OrchestrationTrigger, ActivityTrigger, or EntityTrigger attributes,
+ // then this is a Durable Functions project
+ if (!allFunctions.IsDefaultOrEmpty)
+ {
+ return true;
+ }
+
+ // Fallback: check if Durable Functions assembly is referenced
+ // This handles edge cases where the project references the assembly but hasn't defined triggers yet
+ return compilation.ReferencedAssemblyNames.Any(
+ assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
+ }
+
static void AddOrchestratorFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator)
{
sourceBuilder.AppendLine($@"
diff --git a/src/Generators/README.md b/src/Generators/README.md
index 5c7f145b..9c253760 100644
--- a/src/Generators/README.md
+++ b/src/Generators/README.md
@@ -1,3 +1,63 @@
Source generators for `Microsoft.DurableTask`
+## Overview
+
+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.
+
+## Configuration
+
+### Project Type Detection
+
+The generator uses intelligent automatic detection to determine the project type:
+
+1. **Primary Detection**: Checks for Azure Functions trigger attributes (`OrchestrationTrigger`, `ActivityTrigger`, `EntityTrigger`) in your code
+ - If any methods use these trigger attributes, it generates Azure Functions-specific code
+ - Otherwise, it generates standalone Durable Task Worker code
+
+2. **Fallback Detection**: If no trigger attributes are found, checks if `Microsoft.Azure.Functions.Worker.Extensions.DurableTask` is referenced
+ - This handles projects that reference the Functions package but haven't defined triggers yet
+
+This automatic detection solves the common issue where transitive dependencies on Functions packages would incorrectly trigger Functions mode even when not using Azure Functions.
+
+### Explicit Project Type Configuration (Optional)
+
+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:
+
+```xml
+
+ Standalone
+
+```
+
+#### Supported Values
+
+- `Auto` (default): Automatically detects project type using the intelligent detection described above
+- `Functions`: Forces generation of Azure Functions-specific code
+- `Standalone`: Forces generation of standalone Durable Task Worker code (includes `AddAllGeneratedTasks` method)
+
+#### Example: Force Standalone Mode
+
+```xml
+
+
+ net8.0
+ Standalone
+
+
+
+
+
+
+
+```
+
+With standalone mode, the generator produces the `AddAllGeneratedTasks` extension method for worker registration:
+
+```csharp
+builder.Services.AddDurableTaskWorker(builder =>
+{
+ builder.AddTasks(r => r.AddAllGeneratedTasks());
+});
+```
+
For more information, see https://github.com/microsoft/durabletask-dotnet
\ No newline at end of file
diff --git a/test/Generators.Tests/ProjectTypeConfigurationTests.cs b/test/Generators.Tests/ProjectTypeConfigurationTests.cs
new file mode 100644
index 00000000..bcac0a3d
--- /dev/null
+++ b/test/Generators.Tests/ProjectTypeConfigurationTests.cs
@@ -0,0 +1,514 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.DurableTask.Generators.Tests.Utils;
+
+namespace Microsoft.DurableTask.Generators.Tests;
+
+public class ProjectTypeConfigurationTests
+{
+ const string GeneratedClassName = "GeneratedDurableTaskExtensions";
+ const string GeneratedFileName = $"{GeneratedClassName}.cs";
+
+ [Fact]
+ public Task ExplicitStandaloneMode_WithFunctionsReference_GeneratesStandaloneCode()
+ {
+ // Test that explicit "Standalone" configuration overrides the Functions reference
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ // Even though we have Functions references, we should get Standalone code (AddAllGeneratedTasks)
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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.AddActivity();
+ return builder;
+}",
+ isDurableFunctions: false);
+
+ // Pass isDurableFunctions: true to add Functions references, but projectType: "Standalone" to override
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "Standalone");
+ }
+
+ [Fact]
+ public Task ExplicitStandaloneMode_WithFunctionsReference_OrchestratorTest()
+ {
+ // Test that explicit "Standalone" configuration overrides the Functions reference
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyOrchestrator))]
+class MyOrchestrator : TaskOrchestrator
+{
+ public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Schedules a new instance of the orchestrator.
+///
+///
+public static Task ScheduleNewMyOrchestratorInstanceAsync(
+ this IOrchestrationSubmitter client, int input, StartOrchestrationOptions? options = null)
+{
+ return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options);
+}
+
+///
+/// Calls the sub-orchestrator.
+///
+///
+public static Task CallMyOrchestratorAsync(
+ this TaskOrchestrationContext context, int input, TaskOptions? options = null)
+{
+ return context.CallSubOrchestratorAsync(""MyOrchestrator"", input, options);
+}
+
+internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder)
+{
+ builder.AddOrchestrator();
+ return builder;
+}",
+ isDurableFunctions: false);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "Standalone");
+ }
+
+ [Fact]
+ public Task ExplicitFunctionsMode_WithoutFunctionsReference_GeneratesFunctionsCode()
+ {
+ // Test that explicit "Functions" configuration generates Functions code
+ // even without Functions references
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ // With explicit "Functions", we should get Functions code (Activity trigger function)
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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!;
+}
+
+sealed class GeneratedActivityContext : TaskActivityContext
+{
+ public GeneratedActivityContext(TaskName name, string instanceId)
+ {
+ this.Name = name;
+ this.InstanceId = instanceId;
+ }
+
+ public override TaskName Name { get; }
+
+ public override string InstanceId { get; }
+}",
+ isDurableFunctions: true);
+
+ // Pass isDurableFunctions: true for expected output, but don't add references
+ // Instead rely on projectType: "Functions" to force Functions mode
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "Functions");
+ }
+
+ [Fact]
+ public Task ExplicitFunctionsMode_OrchestratorTest()
+ {
+ // Test that "Functions" mode generates orchestrator Functions code
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyOrchestrator))]
+class MyOrchestrator : TaskOrchestrator
+{
+ public override Task RunAsync(TaskOrchestrationContext ctx, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+static readonly ITaskOrchestrator singletonMyOrchestrator = new 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);
+}
+
+///
+/// Schedules a new instance of the orchestrator.
+///
+///
+public static Task ScheduleNewMyOrchestratorInstanceAsync(
+ this IOrchestrationSubmitter client, int input, StartOrchestrationOptions? options = null)
+{
+ return client.ScheduleNewOrchestrationInstanceAsync(""MyOrchestrator"", input, options);
+}
+
+///
+/// Calls the sub-orchestrator.
+///
+///
+public static Task CallMyOrchestratorAsync(
+ this TaskOrchestrationContext context, int input, TaskOptions? options = null)
+{
+ return context.CallSubOrchestratorAsync(""MyOrchestrator"", input, options);
+}",
+ isDurableFunctions: true);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "Functions");
+ }
+
+ [Fact]
+ public Task AutoMode_WithFunctionsReference_GeneratesFunctionsCode()
+ {
+ // Test that "Auto" mode falls back to auto-detection
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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!;
+}
+
+sealed class GeneratedActivityContext : TaskActivityContext
+{
+ public GeneratedActivityContext(TaskName name, string instanceId)
+ {
+ this.Name = name;
+ this.InstanceId = instanceId;
+ }
+
+ public override TaskName Name { get; }
+
+ public override string InstanceId { get; }
+}",
+ isDurableFunctions: true);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "Auto");
+ }
+
+ [Fact]
+ public Task AutoMode_WithoutFunctionsReference_GeneratesStandaloneCode()
+ {
+ // Test that "Auto" mode falls back to auto-detection
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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.AddActivity();
+ return builder;
+}",
+ isDurableFunctions: false);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false,
+ projectType: "Auto");
+ }
+
+ [Fact]
+ public Task UnrecognizedMode_WithFunctionsReference_FallsBackToAutoDetection()
+ {
+ // Test that unrecognized values fall back to auto-detection
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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!;
+}
+
+sealed class GeneratedActivityContext : TaskActivityContext
+{
+ public GeneratedActivityContext(TaskName name, string instanceId)
+ {
+ this.Name = name;
+ this.InstanceId = instanceId;
+ }
+
+ public override TaskName Name { get; }
+
+ public override string InstanceId { get; }
+}",
+ isDurableFunctions: true);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: "UnrecognizedValue");
+ }
+
+ [Fact]
+ public Task NullProjectType_WithoutFunctionsReference_GeneratesStandaloneCode()
+ {
+ // Test that null projectType (default) falls back to auto-detection
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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.AddActivity();
+ return builder;
+}",
+ isDurableFunctions: false);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false,
+ projectType: null);
+ }
+
+ [Fact]
+ public Task NullProjectType_WithFunctionsReference_GeneratesFunctionsCode()
+ {
+ // Test that null projectType (default) with Functions reference falls back to auto-detection
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+[DurableTask(nameof(MyActivity))]
+class MyActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+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!;
+}
+
+sealed class GeneratedActivityContext : TaskActivityContext
+{
+ public GeneratedActivityContext(TaskName name, string instanceId)
+ {
+ this.Name = name;
+ this.InstanceId = instanceId;
+ }
+
+ public override TaskName Name { get; }
+
+ public override string InstanceId { get; }
+}",
+ isDurableFunctions: true);
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: null);
+ }
+
+ [Fact]
+ public Task AutoDetect_WithTriggerAttributes_GeneratesFunctionsCode()
+ {
+ // Test that presence of Azure Functions trigger attributes auto-detects as Functions project
+ // This validates the allFunctions.IsDefaultOrEmpty check in DetermineIsDurableFunctions
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.DurableTask;
+
+public class MyFunctions
+{
+ [Function(nameof(MyActivity))]
+ public int MyActivity([ActivityTrigger] int input) => input;
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormat(
+ GeneratedClassName,
+ methodList: @"
+///
+/// Calls the activity.
+///
+///
+public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
+{
+ return ctx.CallActivityAsync(""MyActivity"", input, options);
+}",
+ isDurableFunctions: true);
+
+ // No explicit projectType - should auto-detect based on [ActivityTrigger] attribute
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: true,
+ projectType: null);
+ }
+}
diff --git a/test/Generators.Tests/Utils/TestHelpers.cs b/test/Generators.Tests/Utils/TestHelpers.cs
index 960a525f..92f3db86 100644
--- a/test/Generators.Tests/Utils/TestHelpers.cs
+++ b/test/Generators.Tests/Utils/TestHelpers.cs
@@ -17,6 +17,21 @@ public static Task RunTestAsync(
string inputSource,
string expectedOutputSource,
bool isDurableFunctions) where TSourceGenerator : IIncrementalGenerator, new()
+ {
+ return RunTestAsync(
+ expectedFileName,
+ inputSource,
+ expectedOutputSource,
+ isDurableFunctions,
+ projectType: null);
+ }
+
+ public static Task RunTestAsync(
+ string expectedFileName,
+ string inputSource,
+ string expectedOutputSource,
+ bool isDurableFunctions,
+ string? projectType) where TSourceGenerator : IIncrementalGenerator, new()
{
CSharpSourceGeneratorVerifier.Test test = new()
{
@@ -53,6 +68,16 @@ public static Task RunTestAsync(
test.TestState.AdditionalReferences.Add(dependencyInjection);
}
+ // Set the project type configuration if specified
+ if (projectType != null)
+ {
+ test.TestState.AnalyzerConfigFiles.Add(
+ ("/.globalconfig", $"""
+ is_global = true
+ build_property.DurableTaskGeneratorProjectType = {projectType}
+ """));
+ }
+
return test.RunAsync();
}
From bf3d673bfe1cb01f0ba1279c2259e9821feb202a Mon Sep 17 00:00:00 2001
From: wangbill
Date: Tue, 6 Jan 2026 08:24:40 -0800
Subject: [PATCH 10/10] Potential fix for pull request finding 'Missed
opportunity to use Where'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
---
src/Generators/DurableTaskSourceGenerator.cs | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 84fd0373..0116e568 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -312,14 +312,11 @@ static void Execute(
}
// Validate event names and report diagnostics for invalid identifiers
- foreach (DurableEventTypeInfo eventInfo in allEvents)
+ foreach (DurableEventTypeInfo eventInfo in allEvents.Where(e => !IsValidCSharpIdentifier(e.EventName)))
{
- if (!IsValidCSharpIdentifier(eventInfo.EventName))
- {
- Location location = eventInfo.EventNameLocation ?? Location.None;
- Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
- context.ReportDiagnostic(diagnostic);
- }
+ Location location = eventInfo.EventNameLocation ?? Location.None;
+ Diagnostic diagnostic = Diagnostic.Create(InvalidEventNameRule, location, eventInfo.EventName);
+ context.ReportDiagnostic(diagnostic);
}
// Determine if we should generate Durable Functions specific code