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