diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln
index 9336d0846..0b8ef9359 100644
--- a/Microsoft.DurableTask.sln
+++ b/Microsoft.DurableTask.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32901.215
@@ -111,20 +111,35 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistory.Tests", "test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DistributedTracingSample", "samples\DistributedTracingSample\DistributedTracingSample.csproj", "{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NamespaceGenerationSample", "samples\NamespaceGenerationSample\NamespaceGenerationSample.csproj", "{5A69FD28-D814-490E-A76B-B0A5F88C25B2}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReplaySafeLoggerFactorySample", "samples\ReplaySafeLoggerFactorySample\ReplaySafeLoggerFactorySample.csproj", "{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x64.Build.0 = Debug|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Debug|x86.Build.0 = Debug|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x64.ActiveCfg = Release|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x64.Build.0 = Release|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x86.ActiveCfg = Release|Any CPU
+ {B12489CB-B7E5-497B-8F0C-F87F678947C3}.Release|x86.Build.0 = Release|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x64.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x64.Build.0 = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x86.ActiveCfg = Debug|Any CPU
{B0EB48BE-E4F7-4F50-B8BD-5C6172A7A584}.Debug|x86.Build.0 = Debug|Any CPU
@@ -662,6 +677,18 @@ Global
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x64.Build.0 = Release|Any CPU
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x86.ActiveCfg = Release|Any CPU
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8}.Release|x86.Build.0 = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|x64.Build.0 = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Debug|x86.Build.0 = Debug|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|x64.ActiveCfg = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|x64.Build.0 = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|x86.ActiveCfg = Release|Any CPU
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2}.Release|x86.Build.0 = Release|Any CPU
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -674,6 +701,7 @@ Global
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Release|x64.Build.0 = Release|Any CPU
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Release|x86.ActiveCfg = Release|Any CPU
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4}.Release|x86.Build.0 = Release|Any CPU
+
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -729,7 +757,9 @@ Global
{354CE69B-78DB-9B29-C67E-0DBB862C7A65} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{05C9EBA6-7221-D458-47D6-DA457C2F893B} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{4A7305AE-AAAE-43AE-AAB2-DA58DACC6FA8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
+ {5A69FD28-D814-490E-A76B-B0A5F88C25B2} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{8E7BECBC-7226-4778-B8F2-8EBDFF0D3BA4} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
+
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
diff --git a/samples/NamespaceGenerationSample/NamespaceGenerationSample.csproj b/samples/NamespaceGenerationSample/NamespaceGenerationSample.csproj
new file mode 100644
index 000000000..495a9a9fc
--- /dev/null
+++ b/samples/NamespaceGenerationSample/NamespaceGenerationSample.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net8.0;net10.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/NamespaceGenerationSample/Program.cs b/samples/NamespaceGenerationSample/Program.cs
new file mode 100644
index 000000000..1572e6fa9
--- /dev/null
+++ b/samples/NamespaceGenerationSample/Program.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// This sample demonstrates how the source generator places extension methods into the same
+// namespace as the orchestrator/activity classes, keeping IDE suggestions clean and scoped.
+// Tasks in different namespaces get their own GeneratedDurableTaskExtensions class.
+
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.AzureManaged;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+// The generated AddAllGeneratedTasks() method is always in Microsoft.DurableTask namespace.
+// Extension methods like ScheduleNewApprovalOrchestratorInstanceAsync() are in the
+// NamespaceGenerationSample.Approvals namespace, and CallRegistrationActivityAsync() is in
+// NamespaceGenerationSample.Registrations namespace.
+using NamespaceGenerationSample.Approvals;
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+
+// Read the DTS connection string from configuration
+string schedulerConnectionString = builder.Configuration.GetValue("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
+ ?? throw new InvalidOperationException("DURABLE_TASK_SCHEDULER_CONNECTION_STRING is not set.");
+
+builder.Services.AddDurableTaskClient(clientBuilder => clientBuilder.UseDurableTaskScheduler(schedulerConnectionString));
+
+builder.Services.AddDurableTaskWorker(workerBuilder =>
+{
+ // Use the generated AddAllGeneratedTasks() to register all orchestrators and activities
+ workerBuilder.AddTasks(tasks => tasks.AddAllGeneratedTasks());
+ workerBuilder.UseDurableTaskScheduler(schedulerConnectionString);
+});
+
+IHost host = builder.Build();
+await host.StartAsync();
+
+await using DurableTaskClient client = host.Services.GetRequiredService();
+
+// Use the generated typed extension method (in the Approvals namespace)
+string instanceId = await client.ScheduleNewApprovalOrchestratorInstanceAsync("request-123");
+Console.WriteLine($"Started approval orchestration: {instanceId}");
+
+// Wait for completion
+OrchestrationMetadata? result = await client.WaitForInstanceCompletionAsync(
+ instanceId, getInputsAndOutputs: true);
+Console.WriteLine($"Orchestration completed with status: {result?.RuntimeStatus}");
+Console.WriteLine($"Output: {result?.ReadOutputAs()}");
+
+await host.StopAsync();
diff --git a/samples/NamespaceGenerationSample/README.md b/samples/NamespaceGenerationSample/README.md
new file mode 100644
index 000000000..299ec0e13
--- /dev/null
+++ b/samples/NamespaceGenerationSample/README.md
@@ -0,0 +1,72 @@
+# Namespace Generation Sample
+
+This sample demonstrates how the DurableTask source generator places extension methods into the same namespace as the orchestrator/activity classes.
+
+## What it shows
+
+When using the `[DurableTask]` attribute on classes in different namespaces, the source generator will:
+
+1. Place extension methods (e.g., `ScheduleNewApprovalOrchestratorInstanceAsync()`, `CallRegistrationActivityAsync()`) into the **same namespace** as the task class
+2. Keep the `AddAllGeneratedTasks()` registration method in the `Microsoft.DurableTask` namespace
+3. Simplify type names within the same namespace (e.g., `MyClass` instead of `MyNS.MyClass`)
+
+This results in cleaner IDE suggestions — you only see extension methods for tasks that are imported via `using` statements.
+
+## Project structure
+
+- `Tasks.cs` - Defines an orchestrator in `NamespaceGenerationSample.Approvals` and an activity in `NamespaceGenerationSample.Registrations`
+- `Program.cs` - Shows how to use the generated extension methods with explicit `using` statements
+
+## How to run
+
+1. Start the DTS emulator:
+ ```bash
+ docker run --name durabletask-emulator -d -p 8080:8080 -e ASPNETCORE_URLS=http://+:8080 mcr.microsoft.com/dts/dts-emulator:latest
+ ```
+
+2. Set the connection string environment variable:
+ ```bash
+ export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="Endpoint=http://localhost:8080;TaskHub=default;Authentication=None"
+ ```
+
+3. Run the sample:
+ ```bash
+ dotnet run
+ ```
+
+## Generated code
+
+The source generator produces code like this:
+
+```csharp
+// Extension methods in the task's namespace
+namespace NamespaceGenerationSample.Approvals
+{
+ public static class GeneratedDurableTaskExtensions
+ {
+ public static Task ScheduleNewApprovalOrchestratorInstanceAsync(
+ this IOrchestrationSubmitter client, string input, StartOrchestrationOptions? options = null) { ... }
+
+ public static Task CallApprovalOrchestratorAsync(
+ this TaskOrchestrationContext context, string input, TaskOptions? options = null) { ... }
+ }
+}
+
+namespace NamespaceGenerationSample.Registrations
+{
+ public static class GeneratedDurableTaskExtensions
+ {
+ public static Task CallRegistrationActivityAsync(
+ this TaskOrchestrationContext ctx, int input, TaskOptions? options = null) { ... }
+ }
+}
+
+// Registration method stays in Microsoft.DurableTask
+namespace Microsoft.DurableTask
+{
+ public static class GeneratedDurableTaskExtensions
+ {
+ internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder) { ... }
+ }
+}
+```
diff --git a/samples/NamespaceGenerationSample/Tasks.cs b/samples/NamespaceGenerationSample/Tasks.cs
new file mode 100644
index 000000000..404a81aad
--- /dev/null
+++ b/samples/NamespaceGenerationSample/Tasks.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Tasks are organized into separate namespaces. The source generator will place
+// each task's extension methods into its own namespace instead of Microsoft.DurableTask.
+
+using Microsoft.DurableTask;
+using NamespaceGenerationSample.Registrations;
+
+// Approval-related tasks live in their own namespace.
+// The generated ScheduleNewApprovalOrchestratorInstanceAsync() extension method
+// will be generated in this namespace. The CallRegistrationActivityAsync() extension
+// method is generated in the NamespaceGenerationSample.Registrations namespace.
+namespace NamespaceGenerationSample.Approvals
+{
+ ///
+ /// An orchestrator that runs an approval workflow.
+ /// The generated extension method ScheduleNewApprovalOrchestratorInstanceAsync()
+ /// will be in the NamespaceGenerationSample.Approvals namespace.
+ ///
+ [DurableTask(nameof(ApprovalOrchestrator))]
+ public class ApprovalOrchestrator : TaskOrchestrator
+ {
+ public override async Task RunAsync(TaskOrchestrationContext context, string requestId)
+ {
+ // Use the generated typed extension method (in the Registrations namespace)
+ // By importing the Registrations namespace, we get access to CallRegistrationActivityAsync().
+ string registrationResult = await context.CallRegistrationActivityAsync(42);
+
+ return $"Approved request '{requestId}' with registration: {registrationResult}";
+ }
+ }
+}
+
+// Registration-related tasks in a separate namespace.
+// The generated CallRegistrationActivityAsync() extension method will be in this namespace.
+namespace NamespaceGenerationSample.Registrations
+{
+ ///
+ /// An activity that performs registration.
+ /// The generated extension method CallRegistrationActivityAsync()
+ /// will be in the NamespaceGenerationSample.Registrations namespace.
+ ///
+ [DurableTask(nameof(RegistrationActivity))]
+ public class RegistrationActivity : TaskActivity
+ {
+ public override Task RunAsync(TaskActivityContext context, int registrationId)
+ {
+ return Task.FromResult($"Registration-{registrationId} completed");
+ }
+ }
+}
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 0116e568c..b4b2de97c 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -145,6 +145,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
string className = classType.ToDisplayString();
+ string classNamespace = classType.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : classType.ContainingNamespace.ToDisplayString();
INamedTypeSymbol? taskType = null;
DurableTaskKind kind = DurableTaskKind.Orchestrator;
@@ -211,7 +214,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
taskNameLocation = expression.GetLocation();
}
- return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind, taskNameLocation);
+ return new DurableTaskTypeInfo(className, classNamespace, taskName, inputType, outputType, kind, taskNameLocation);
}
static DurableEventTypeInfo? GetDurableEventTypeInfo(GeneratorSyntaxContext context)
@@ -256,7 +259,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
}
}
- return new DurableEventTypeInfo(eventName, eventType, eventNameLocation);
+ string eventNamespace = eventType.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : eventType.ContainingNamespace.ToDisplayString();
+
+ return new DurableEventTypeInfo(eventName, eventNamespace, eventType, eventNameLocation);
}
static DurableFunction? GetDurableFunction(GeneratorSyntaxContext context)
@@ -358,6 +365,56 @@ static void Execute(
return;
}
+ // Group tasks by namespace. Tasks in the global namespace use "Microsoft.DurableTask" for backward compatibility.
+ Dictionary> tasksByNamespace = new();
+ foreach (DurableTaskTypeInfo task in orchestrators.Concat(activities).Concat(entities))
+ {
+ string targetNamespace = string.IsNullOrEmpty(task.Namespace) ? "Microsoft.DurableTask" : task.Namespace;
+ if (!tasksByNamespace.TryGetValue(targetNamespace, out List? list))
+ {
+ list = new List();
+ tasksByNamespace[targetNamespace] = list;
+ }
+
+ list.Add(task);
+ }
+
+ // Group events by namespace. Events in the global namespace use "Microsoft.DurableTask".
+ Dictionary> eventsByNamespace = new();
+ foreach (DurableEventTypeInfo eventInfo in validEvents)
+ {
+ string targetNamespace = string.IsNullOrEmpty(eventInfo.Namespace) ? "Microsoft.DurableTask" : eventInfo.Namespace;
+ if (!eventsByNamespace.TryGetValue(targetNamespace, out List? list))
+ {
+ list = new List();
+ eventsByNamespace[targetNamespace] = list;
+ }
+
+ list.Add(eventInfo);
+ }
+
+ // Collect all distinct namespaces
+ HashSet allNamespaces = new(tasksByNamespace.Keys);
+ foreach (string ns in eventsByNamespace.Keys)
+ {
+ allNamespaces.Add(ns);
+ }
+
+ // Activity function triggers from DurableFunction go into Microsoft.DurableTask namespace
+ List activityTriggers = allFunctions.Where(
+ df => df.Kind == DurableFunctionKind.Activity).ToList();
+ if (activityTriggers.Count > 0)
+ {
+ allNamespaces.Add("Microsoft.DurableTask");
+ }
+
+ // Registration method always goes in Microsoft.DurableTask
+ bool needsRegistrationMethod = !isDurableFunctions && (orchestrators.Count > 0 || activities.Count > 0 || entities.Count > 0);
+ if (needsRegistrationMethod)
+ {
+ allNamespaces.Add("Microsoft.DurableTask");
+ }
+
StringBuilder sourceBuilder = new(capacity: found * 1024);
sourceBuilder.Append(@"//
#nullable enable
@@ -365,6 +422,7 @@ static void Execute(
using System;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.DurableTask;
using Microsoft.DurableTask.Internal;");
if (isDurableFunctions)
@@ -374,93 +432,120 @@ static void Execute(
using Microsoft.Extensions.DependencyInjection;");
}
- sourceBuilder.Append(@"
+ // Sort namespaces so "Microsoft.DurableTask" comes last for consistent output
+ List sortedNamespaces = allNamespaces
+ .OrderBy(ns => ns == "Microsoft.DurableTask" ? 1 : 0)
+ .ThenBy(ns => ns, StringComparer.Ordinal)
+ .ToList();
-namespace Microsoft.DurableTask
-{
- public static class GeneratedDurableTaskExtensions
- {");
- if (isDurableFunctions)
+ foreach (string targetNamespace in sortedNamespaces)
{
- // Generate a singleton orchestrator object instance that can be reused for all invocations.
- foreach (DurableTaskTypeInfo orchestrator in orchestrators)
+ tasksByNamespace.TryGetValue(targetNamespace, out List? tasksInNamespace);
+ eventsByNamespace.TryGetValue(targetNamespace, out List? eventsInNamespace);
+
+ List orchestratorsInNs = tasksInNamespace?.Where(t => t.IsOrchestrator).ToList() ?? new();
+ List activitiesInNs = tasksInNamespace?.Where(t => t.IsActivity).ToList() ?? new();
+ List entitiesInNs = tasksInNamespace?.Where(t => t.IsEntity).ToList() ?? new();
+ bool isMicrosoftDurableTask = targetNamespace == "Microsoft.DurableTask";
+
+ // Check if there's any content to generate for this namespace
+ bool hasOrchestratorMethods = orchestratorsInNs.Count > 0;
+ bool hasActivityMethods = activitiesInNs.Count > 0;
+ bool hasEntityFunctions = isDurableFunctions && entitiesInNs.Count > 0;
+ bool hasActivityTriggers = isMicrosoftDurableTask && activityTriggers.Count > 0;
+ bool hasEvents = eventsInNamespace != null && eventsInNamespace.Count > 0;
+ bool hasRegistration = isMicrosoftDurableTask && needsRegistrationMethod;
+
+ if (!hasOrchestratorMethods && !hasActivityMethods && !hasEntityFunctions
+ && !hasActivityTriggers && !hasEvents && !hasRegistration)
{
- sourceBuilder.AppendLine($@"
- static readonly ITaskOrchestrator singleton{orchestrator.TaskName} = new {orchestrator.TypeName}();");
+ continue;
}
- }
- foreach (DurableTaskTypeInfo orchestrator in orchestrators)
- {
+ sourceBuilder.Append($@"
+
+namespace {targetNamespace}
+{{
+ public static class GeneratedDurableTaskExtensions
+ {{");
if (isDurableFunctions)
{
- // Generate the function definition required to trigger orchestrators in Azure Functions
- AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator);
+ // Generate a singleton orchestrator object instance that can be reused for all invocations.
+ foreach (DurableTaskTypeInfo orchestrator in orchestratorsInNs)
+ {
+ sourceBuilder.AppendLine($@"
+ static readonly ITaskOrchestrator singleton{orchestrator.TaskName} = new {SimplifyTypeName(orchestrator.TypeName, targetNamespace)}();");
+ }
}
- AddOrchestratorCallMethod(sourceBuilder, orchestrator);
- AddSubOrchestratorCallMethod(sourceBuilder, orchestrator);
- }
+ foreach (DurableTaskTypeInfo orchestrator in orchestratorsInNs)
+ {
+ if (isDurableFunctions)
+ {
+ AddOrchestratorFunctionDeclaration(sourceBuilder, orchestrator, targetNamespace);
+ }
- foreach (DurableTaskTypeInfo activity in activities)
- {
- AddActivityCallMethod(sourceBuilder, activity);
+ AddOrchestratorCallMethod(sourceBuilder, orchestrator, targetNamespace);
+ AddSubOrchestratorCallMethod(sourceBuilder, orchestrator, targetNamespace);
+ }
- if (isDurableFunctions)
+ foreach (DurableTaskTypeInfo activity in activitiesInNs)
{
- // Generate the function definition required to trigger activities in Azure Functions
- AddActivityFunctionDeclaration(sourceBuilder, activity);
+ AddActivityCallMethod(sourceBuilder, activity, targetNamespace);
+
+ if (isDurableFunctions)
+ {
+ AddActivityFunctionDeclaration(sourceBuilder, activity, targetNamespace);
+ }
}
- }
- foreach (DurableTaskTypeInfo entity in entities)
- {
- if (isDurableFunctions)
+ foreach (DurableTaskTypeInfo entity in entitiesInNs)
{
- // Generate the function definition required to trigger entities in Azure Functions
- AddEntityFunctionDeclaration(sourceBuilder, entity);
+ if (isDurableFunctions)
+ {
+ AddEntityFunctionDeclaration(sourceBuilder, entity, targetNamespace);
+ }
}
- }
- // Activity function triggers are supported for code-gen (but not orchestration triggers)
- IEnumerable activityTriggers = allFunctions.Where(
- df => df.Kind == DurableFunctionKind.Activity);
- foreach (DurableFunction function in activityTriggers)
- {
- AddActivityCallMethod(sourceBuilder, function);
- }
+ // Activity function triggers from DurableFunction always go in Microsoft.DurableTask
+ if (isMicrosoftDurableTask)
+ {
+ foreach (DurableFunction function in activityTriggers)
+ {
+ AddActivityCallMethod(sourceBuilder, function);
+ }
+ }
- // Generate WaitFor{EventName}Async methods for each event type
- foreach (DurableEventTypeInfo eventInfo in validEvents)
- {
- AddEventWaitMethod(sourceBuilder, eventInfo);
- AddEventSendMethod(sourceBuilder, eventInfo);
- }
+ // Generate WaitFor/Send methods for events in this namespace
+ if (eventsInNamespace != null)
+ {
+ foreach (DurableEventTypeInfo eventInfo in eventsInNamespace)
+ {
+ AddEventWaitMethod(sourceBuilder, eventInfo, targetNamespace);
+ AddEventSendMethod(sourceBuilder, eventInfo, targetNamespace);
+ }
+ }
- if (isDurableFunctions)
- {
- if (activities.Count > 0)
+ if (isDurableFunctions)
{
- // Functions-specific helper class, which is only needed when
- // using the class-based syntax.
- AddGeneratedActivityContextClass(sourceBuilder);
+ if (activitiesInNs.Count > 0)
+ {
+ AddGeneratedActivityContextClass(sourceBuilder);
+ }
}
- }
- else
- {
- // ASP.NET Core-specific service registration methods
- // Only generate if there are actually tasks to register
- if (orchestrators.Count > 0 || activities.Count > 0 || entities.Count > 0)
+
+ // Registration method goes in Microsoft.DurableTask namespace only
+ if (isMicrosoftDurableTask && needsRegistrationMethod)
{
AddRegistrationMethodForAllTasks(
sourceBuilder,
orchestrators,
activities,
- entities);
+ entities);
}
- }
- sourceBuilder.AppendLine(" }").AppendLine("}");
+ sourceBuilder.AppendLine(" }").AppendLine("}");
+ }
context.AddSource("GeneratedDurableTaskExtensions.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8, SourceHashAlgorithm.Sha256));
}
@@ -508,55 +593,106 @@ static bool DetermineIsDurableFunctions(Compilation compilation, ImmutableArray<
assembly => assembly.Name.Equals("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", StringComparison.OrdinalIgnoreCase));
}
- static void AddOrchestratorFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator)
+ ///
+ /// Simplifies a fully qualified type name relative to a target namespace.
+ /// Types in the same namespace are returned without the namespace prefix.
+ ///
+ static string SimplifyTypeName(string fullyQualifiedTypeName, string targetNamespace)
+ {
+ if (string.IsNullOrEmpty(targetNamespace))
+ {
+ return fullyQualifiedTypeName;
+ }
+
+ if (fullyQualifiedTypeName.StartsWith(targetNamespace + ".", StringComparison.Ordinal))
+ {
+ return fullyQualifiedTypeName.Substring(targetNamespace.Length + 1);
+ }
+
+ return fullyQualifiedTypeName;
+ }
+
+ static void AddOrchestratorFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator, string targetNamespace)
{
+ string inputType = orchestrator.GetInputTypeForNamespace(targetNamespace);
+ string outputType = orchestrator.GetOutputTypeForNamespace(targetNamespace);
+
sourceBuilder.AppendLine($@"
[Function(nameof({orchestrator.TaskName}))]
- public static Task<{orchestrator.OutputType}> {orchestrator.TaskName}([OrchestrationTrigger] TaskOrchestrationContext context)
+ public static Task<{outputType}> {orchestrator.TaskName}([OrchestrationTrigger] TaskOrchestrationContext context)
{{
- return singleton{orchestrator.TaskName}.RunAsync(context, context.GetInput<{orchestrator.InputType}>())
- .ContinueWith(t => ({orchestrator.OutputType})(t.Result ?? default({orchestrator.OutputType})!), TaskContinuationOptions.ExecuteSynchronously);
+ return singleton{orchestrator.TaskName}.RunAsync(context, context.GetInput<{inputType}>())
+ .ContinueWith(t => ({outputType})(t.Result ?? default({outputType})!), TaskContinuationOptions.ExecuteSynchronously);
}}");
}
- static void AddOrchestratorCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator)
+ static void AddOrchestratorCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator, string targetNamespace)
{
+ string inputType = orchestrator.GetInputTypeForNamespace(targetNamespace);
+ string inputParameter = inputType + " input";
+ if (inputType.EndsWith("?", StringComparison.Ordinal))
+ {
+ inputParameter += " = default";
+ }
+
+ string simplifiedTypeName = SimplifyTypeName(orchestrator.TypeName, targetNamespace);
+
sourceBuilder.AppendLine($@"
///
- /// Schedules a new instance of the orchestrator.
+ /// Schedules a new instance of the orchestrator.
///
///
public static Task ScheduleNew{orchestrator.TaskName}InstanceAsync(
- this IOrchestrationSubmitter client, {orchestrator.InputParameter}, StartOrchestrationOptions? options = null)
+ this IOrchestrationSubmitter client, {inputParameter}, StartOrchestrationOptions? options = null)
{{
return client.ScheduleNewOrchestrationInstanceAsync(""{orchestrator.TaskName}"", input, options);
}}");
}
- static void AddSubOrchestratorCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator)
+ static void AddSubOrchestratorCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo orchestrator, string targetNamespace)
{
+ string inputType = orchestrator.GetInputTypeForNamespace(targetNamespace);
+ string outputType = orchestrator.GetOutputTypeForNamespace(targetNamespace);
+ string inputParameter = inputType + " input";
+ if (inputType.EndsWith("?", StringComparison.Ordinal))
+ {
+ inputParameter += " = default";
+ }
+
+ string simplifiedTypeName = SimplifyTypeName(orchestrator.TypeName, targetNamespace);
+
sourceBuilder.AppendLine($@"
///
- /// Calls the sub-orchestrator.
+ /// Calls the sub-orchestrator.
///
///
- public static Task<{orchestrator.OutputType}> Call{orchestrator.TaskName}Async(
- this TaskOrchestrationContext context, {orchestrator.InputParameter}, TaskOptions? options = null)
+ public static Task<{outputType}> Call{orchestrator.TaskName}Async(
+ this TaskOrchestrationContext context, {inputParameter}, TaskOptions? options = null)
{{
- return context.CallSubOrchestratorAsync<{orchestrator.OutputType}>(""{orchestrator.TaskName}"", input, options);
+ return context.CallSubOrchestratorAsync<{outputType}>(""{orchestrator.TaskName}"", input, options);
}}");
}
- static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo activity)
+ static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableTaskTypeInfo activity, string targetNamespace)
{
+ string inputType = activity.GetInputTypeForNamespace(targetNamespace);
+ string outputType = activity.GetOutputTypeForNamespace(targetNamespace);
+ string inputParameter = inputType + " input";
+ if (inputType.EndsWith("?", StringComparison.Ordinal))
+ {
+ inputParameter += " = default";
+ }
+
+ string simplifiedTypeName = SimplifyTypeName(activity.TypeName, targetNamespace);
+
sourceBuilder.AppendLine($@"
///
- /// Calls the activity.
+ /// Calls the activity.
///
///
- public static Task<{activity.OutputType}> Call{activity.TaskName}Async(this TaskOrchestrationContext ctx, {activity.InputParameter}, TaskOptions? options = null)
+ public static Task<{outputType}> Call{activity.TaskName}Async(this TaskOrchestrationContext ctx, {inputParameter}, TaskOptions? options = null)
{{
- return ctx.CallActivityAsync<{activity.OutputType}>(""{activity.TaskName}"", input, options);
+ return ctx.CallActivityAsync<{outputType}>(""{activity.TaskName}"", input, options);
}}");
}
@@ -588,55 +724,70 @@ static void AddActivityCallMethod(StringBuilder sourceBuilder, DurableFunction a
}
}
- static void AddEventWaitMethod(StringBuilder sourceBuilder, DurableEventTypeInfo eventInfo)
+ static void AddEventWaitMethod(StringBuilder sourceBuilder, DurableEventTypeInfo eventInfo, string targetNamespace)
{
+ string typeName = SimplifyTypeName(eventInfo.TypeName, targetNamespace);
+
sourceBuilder.AppendLine($@"
///
- /// Waits for an external event of type .
+ /// Waits for an external event of type .
///
///
- public static Task<{eventInfo.TypeName}> WaitFor{eventInfo.EventName}Async(this TaskOrchestrationContext context, CancellationToken cancellationToken = default)
+ public static Task<{typeName}> WaitFor{eventInfo.EventName}Async(this TaskOrchestrationContext context, CancellationToken cancellationToken = default)
{{
- return context.WaitForExternalEvent<{eventInfo.TypeName}>(""{eventInfo.EventName}"", cancellationToken);
+ return context.WaitForExternalEvent<{typeName}>(""{eventInfo.EventName}"", cancellationToken);
}}");
}
- static void AddEventSendMethod(StringBuilder sourceBuilder, DurableEventTypeInfo eventInfo)
+ static void AddEventSendMethod(StringBuilder sourceBuilder, DurableEventTypeInfo eventInfo, string targetNamespace)
{
+ string typeName = SimplifyTypeName(eventInfo.TypeName, targetNamespace);
+
sourceBuilder.AppendLine($@"
///
- /// Sends an external event of type to another orchestration instance.
+ /// Sends an external event of type to another orchestration instance.
///
///
- public static void Send{eventInfo.EventName}(this TaskOrchestrationContext context, string instanceId, {eventInfo.TypeName} eventData)
+ public static void Send{eventInfo.EventName}(this TaskOrchestrationContext context, string instanceId, {typeName} eventData)
{{
context.SendEvent(instanceId, ""{eventInfo.EventName}"", eventData);
}}");
}
- static void AddActivityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo activity)
+ static void AddActivityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo activity, string targetNamespace)
{
+ string inputType = activity.GetInputTypeForNamespace(targetNamespace);
+ string outputType = activity.GetOutputTypeForNamespace(targetNamespace);
+ string inputParameter = inputType + " input";
+ if (inputType.EndsWith("?", StringComparison.Ordinal))
+ {
+ inputParameter += " = default";
+ }
+
+ string simplifiedActivityTypeName = SimplifyTypeName(activity.TypeName, targetNamespace);
// GeneratedActivityContext is a generated class that we use for each generated activity trigger definition.
// Note that the second "instanceId" parameter is populated via the Azure Functions binding context.
sourceBuilder.AppendLine($@"
[Function(nameof({activity.TaskName}))]
- public static async Task<{activity.OutputType}> {activity.TaskName}([ActivityTrigger] {activity.InputParameter}, string instanceId, FunctionContext executionContext)
+ public static async Task<{outputType}> {activity.TaskName}([ActivityTrigger] {inputParameter}, string instanceId, FunctionContext executionContext)
{{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<{activity.TypeName}>(executionContext.InstanceServices);
+ ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance<{simplifiedActivityTypeName}>(executionContext.InstanceServices);
TaskActivityContext context = new GeneratedActivityContext(""{activity.TaskName}"", instanceId);
object? result = await activity.RunAsync(context, input);
- return ({activity.OutputType})result!;
+ return ({outputType})result!;
}}");
}
- static void AddEntityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo entity)
+ static void AddEntityFunctionDeclaration(StringBuilder sourceBuilder, DurableTaskTypeInfo entity, string targetNamespace)
{
+ string simplifiedEntityTypeName = SimplifyTypeName(entity.TypeName, targetNamespace);
+
// Generate the entity trigger function that dispatches to the entity implementation.
sourceBuilder.AppendLine($@"
[Function(nameof({entity.TaskName}))]
public static Task {entity.TaskName}([EntityTrigger] TaskEntityDispatcher dispatcher)
{{
- return dispatcher.DispatchAsync<{entity.TypeName}>();
+ return dispatcher.DispatchAsync<{simplifiedEntityTypeName}>();
}}");
}
@@ -712,6 +863,7 @@ class DurableTaskTypeInfo
{
public DurableTaskTypeInfo(
string taskType,
+ string taskNamespace,
string taskName,
ITypeSymbol? inputType,
ITypeSymbol? outputType,
@@ -719,37 +871,21 @@ public DurableTaskTypeInfo(
Location? taskNameLocation = null)
{
this.TypeName = taskType;
+ this.Namespace = taskNamespace;
this.TaskName = taskName;
this.Kind = kind;
this.TaskNameLocation = taskNameLocation;
-
- // Entities only have a state type parameter, not input/output
- if (kind == DurableTaskKind.Entity)
- {
- this.InputType = string.Empty;
- this.InputParameter = string.Empty;
- this.OutputType = string.Empty;
- }
- else
- {
- this.InputType = GetRenderedTypeExpression(inputType);
- this.InputParameter = this.InputType + " input";
- if (this.InputType[this.InputType.Length - 1] == '?')
- {
- this.InputParameter += " = default";
- }
-
- this.OutputType = GetRenderedTypeExpression(outputType);
- }
+ this.InputTypeSymbol = inputType;
+ this.OutputTypeSymbol = outputType;
}
public string TypeName { get; }
+ public string Namespace { get; }
public string TaskName { get; }
- public string InputType { get; }
- public string InputParameter { get; }
- public string OutputType { get; }
public DurableTaskKind Kind { get; }
public Location? TaskNameLocation { get; }
+ ITypeSymbol? InputTypeSymbol { get; }
+ ITypeSymbol? OutputTypeSymbol { get; }
public bool IsActivity => this.Kind == DurableTaskKind.Activity;
@@ -757,19 +893,43 @@ public DurableTaskTypeInfo(
public bool IsEntity => this.Kind == DurableTaskKind.Entity;
- static string GetRenderedTypeExpression(ITypeSymbol? symbol)
+ ///
+ /// Gets the rendered input type expression relative to the specified namespace.
+ ///
+ public string GetInputTypeForNamespace(string targetNamespace)
+ {
+ return GetRenderedTypeExpressionForNamespace(this.InputTypeSymbol, targetNamespace);
+ }
+
+ ///
+ /// Gets the rendered output type expression relative to the specified namespace.
+ ///
+ public string GetOutputTypeForNamespace(string targetNamespace)
+ {
+ return GetRenderedTypeExpressionForNamespace(this.OutputTypeSymbol, targetNamespace);
+ }
+
+ static string GetRenderedTypeExpressionForNamespace(ITypeSymbol? symbol, string targetNamespace)
{
if (symbol == null)
{
return "object";
}
- string expression = symbol.ToString();
+ string expression = symbol.ToDisplayString();
+
+ // Simplify System types (e.g., System.String -> String, System.Int32 -> int)
if (expression.StartsWith("System.", StringComparison.Ordinal)
- && symbol.ContainingNamespace.Name == "System")
+ && symbol.ContainingNamespace.ToDisplayString() == "System")
{
expression = expression.Substring("System.".Length);
}
+ // Simplify types in the same namespace
+ else if (!string.IsNullOrEmpty(targetNamespace)
+ && symbol.ContainingNamespace.ToDisplayString() == targetNamespace)
+ {
+ expression = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+ }
return expression;
}
@@ -777,14 +937,16 @@ static string GetRenderedTypeExpression(ITypeSymbol? symbol)
class DurableEventTypeInfo
{
- public DurableEventTypeInfo(string eventName, ITypeSymbol eventType, Location? eventNameLocation = null)
+ public DurableEventTypeInfo(string eventName, string eventNamespace, ITypeSymbol eventType, Location? eventNameLocation = null)
{
this.TypeName = GetRenderedTypeExpression(eventType);
+ this.Namespace = eventNamespace;
this.EventName = eventName;
this.EventNameLocation = eventNameLocation;
}
public string TypeName { get; }
+ public string Namespace { get; }
public string EventName { get; }
public Location? EventNameLocation { get; }
diff --git a/test/Generators.Tests/AzureFunctionsTests.cs b/test/Generators.Tests/AzureFunctionsTests.cs
index 3a02eeee2..ac2d81992 100644
--- a/test/Generators.Tests/AzureFunctionsTests.cs
+++ b/test/Generators.Tests/AzureFunctionsTests.cs
@@ -293,8 +293,9 @@ public class MyOrchestrator : TaskOrchestrator<{inputType}, {outputType}>
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
+static readonly ITaskOrchestrator singletonMyOrchestrator = new MyOrchestrator();
[Function(nameof(MyOrchestrator))]
public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
@@ -304,7 +305,7 @@ public class MyOrchestrator : TaskOrchestrator<{inputType}, {outputType}>
}}
///
-/// Schedules a new instance of the orchestrator.
+/// Schedules a new instance of the orchestrator.
///
///
public static Task ScheduleNewMyOrchestratorInstanceAsync(
@@ -314,7 +315,7 @@ public static Task ScheduleNewMyOrchestratorInstanceAsync(
}}
///
-/// Calls the sub-orchestrator.
+/// Calls the sub-orchestrator.
///
///
public static Task<{outputType}> CallMyOrchestratorAsync(
@@ -376,8 +377,9 @@ public abstract class MyOrchestratorBase : TaskOrchestrator<{inputType}, {output
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
+static readonly ITaskOrchestrator singletonMyOrchestrator = new MyOrchestrator();
[Function(nameof(MyOrchestrator))]
public static Task<{outputType}> MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
@@ -387,7 +389,7 @@ public abstract class MyOrchestratorBase : TaskOrchestrator<{inputType}, {output
}}
///
-/// Schedules a new instance of the orchestrator.
+/// Schedules a new instance of the orchestrator.
///
///
public static Task ScheduleNewMyOrchestratorInstanceAsync(
@@ -397,7 +399,7 @@ public static Task ScheduleNewMyOrchestratorInstanceAsync(
}}
///
-/// Calls the sub-orchestrator.
+/// Calls the sub-orchestrator.
///
///
public static Task<{outputType}> CallMyOrchestratorAsync(
@@ -441,11 +443,12 @@ public class MyEntity : TaskEntity<{stateType}>
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: @"
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{
- return dispatcher.DispatchAsync();
+ return dispatcher.DispatchAsync();
}",
isDurableFunctions: true);
@@ -488,11 +491,12 @@ public abstract class MyEntityBase : TaskEntity<{stateType}>
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: @"
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{
- return dispatcher.DispatchAsync();
+ return dispatcher.DispatchAsync();
}",
isDurableFunctions: true);
@@ -532,11 +536,12 @@ public class MyEntity : TaskEntity
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: @"
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{
- return dispatcher.DispatchAsync();
+ return dispatcher.DispatchAsync();
}",
isDurableFunctions: true);
@@ -584,8 +589,9 @@ public class MyEntity : TaskEntity
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: $@"
-static readonly ITaskOrchestrator singletonMyOrchestrator = new MyNS.MyOrchestrator();
+static readonly ITaskOrchestrator singletonMyOrchestrator = new MyOrchestrator();
[Function(nameof(MyOrchestrator))]
public static Task MyOrchestrator([OrchestrationTrigger] TaskOrchestrationContext context)
@@ -595,7 +601,7 @@ public static Task MyOrchestrator([OrchestrationTrigger] TaskOrchestrati
}}
///
-/// Schedules a new instance of the orchestrator.
+/// Schedules a new instance of the orchestrator.
///
///
public static Task ScheduleNewMyOrchestratorInstanceAsync(
@@ -605,7 +611,7 @@ public static Task ScheduleNewMyOrchestratorInstanceAsync(
}}
///
-/// Calls the sub-orchestrator.
+/// Calls the sub-orchestrator.
///
///
public static Task CallMyOrchestratorAsync(
@@ -615,7 +621,7 @@ public static Task CallMyOrchestratorAsync(
}}
///
-/// Calls the activity.
+/// Calls the activity.
///
///
public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
@@ -626,7 +632,7 @@ public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx
[Function(nameof(MyActivity))]
public static async Task MyActivity([ActivityTrigger] int input, string instanceId, FunctionContext executionContext)
{{
- ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
+ ITaskActivity activity = ActivatorUtilities.GetServiceOrCreateInstance(executionContext.InstanceServices);
TaskActivityContext context = new GeneratedActivityContext(""MyActivity"", instanceId);
object? result = await activity.RunAsync(context, input);
return (string)result!;
@@ -635,7 +641,7 @@ public static async Task MyActivity([ActivityTrigger] int input, string
[Function(nameof(MyEntity))]
public static Task MyEntity([EntityTrigger] TaskEntityDispatcher dispatcher)
{{
- return dispatcher.DispatchAsync();
+ return dispatcher.DispatchAsync();
}}
{TestHelpers.DeIndent(DurableTaskSourceGenerator.GetGeneratedActivityContextCode(), spacesToRemove: 8)}",
isDurableFunctions: true);
diff --git a/test/Generators.Tests/ClassBasedSyntaxTests.cs b/test/Generators.Tests/ClassBasedSyntaxTests.cs
index 638d5a267..aaed02098 100644
--- a/test/Generators.Tests/ClassBasedSyntaxTests.cs
+++ b/test/Generators.Tests/ClassBasedSyntaxTests.cs
@@ -228,23 +228,25 @@ class MyActivityImpl : TaskActivity
public class MyClass { }
}";
- string expectedOutput = TestHelpers.WrapAndFormat(
+ string expectedOutput = TestHelpers.WrapAndFormatMultiNamespace(
GeneratedClassName,
- methodList: @"
+ isDurableFunctions: false,
+ ("MyNS", @"
///
-/// Calls the activity.
+/// Calls the activity.
///
///
-public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, MyNS.MyClass input, TaskOptions? options = null)
+public static Task CallMyActivityAsync(this TaskOrchestrationContext ctx, MyClass input, TaskOptions? options = null)
{
- return ctx.CallActivityAsync(""MyActivity"", input, options);
-}
-
+ return ctx.CallActivityAsync(""MyActivity"", input, options);
+}"),
+ ("Microsoft.DurableTask", @"
internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder)
{
builder.AddActivity();
return builder;
-}");
+}")
+ );
return TestHelpers.RunTestAsync(
GeneratedFileName,
@@ -608,21 +610,22 @@ public record DataReceivedEvent(int Id, string Data);
string expectedOutput = TestHelpers.WrapAndFormat(
GeneratedClassName,
+ "MyNS",
methodList: @"
///
-/// Waits for an external event of type .
+/// Waits for an external event of type .
///
///
-public static Task WaitForDataReceivedEventAsync(this TaskOrchestrationContext context, CancellationToken cancellationToken = default)
+public static Task WaitForDataReceivedEventAsync(this TaskOrchestrationContext context, CancellationToken cancellationToken = default)
{
- return context.WaitForExternalEvent(""DataReceivedEvent"", cancellationToken);
+ return context.WaitForExternalEvent(""DataReceivedEvent"", cancellationToken);
}
///
-/// Sends an external event of type to another orchestration instance.
+/// Sends an external event of type to another orchestration instance.
///
///
-public static void SendDataReceivedEvent(this TaskOrchestrationContext context, string instanceId, MyNS.DataReceivedEvent eventData)
+public static void SendDataReceivedEvent(this TaskOrchestrationContext context, string instanceId, DataReceivedEvent eventData)
{
context.SendEvent(instanceId, ""DataReceivedEvent"", eventData);
}");
@@ -633,4 +636,149 @@ public static void SendDataReceivedEvent(this TaskOrchestrationContext context,
expectedOutput,
isDurableFunctions: false);
}
+
+ [Fact]
+ public Task MultiNamespace_OrchestratorAndActivityInDifferentNamespaces()
+ {
+ // Verify that tasks in different namespaces generate extension methods in their respective namespaces
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+namespace Approvals
+{
+ [DurableTask(nameof(ApprovalOrchestrator))]
+ public class ApprovalOrchestrator : TaskOrchestrator
+ {
+ public override Task RunAsync(TaskOrchestrationContext ctx, string input) => Task.FromResult(string.Empty);
+ }
+}
+
+namespace Registrations
+{
+ [DurableTask(nameof(RegistrationActivity))]
+ public class RegistrationActivity : TaskActivity
+ {
+ public override Task RunAsync(TaskActivityContext context, int input) => Task.FromResult(string.Empty);
+ }
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormatMultiNamespace(
+ GeneratedClassName,
+ isDurableFunctions: false,
+ ("Approvals", @"
+///
+/// Schedules a new instance of the orchestrator.
+///
+///
+public static Task ScheduleNewApprovalOrchestratorInstanceAsync(
+ this IOrchestrationSubmitter client, string input, StartOrchestrationOptions? options = null)
+{
+ return client.ScheduleNewOrchestrationInstanceAsync(""ApprovalOrchestrator"", input, options);
+}
+
+///
+/// Calls the sub-orchestrator.
+///
+///
+public static Task CallApprovalOrchestratorAsync(
+ this TaskOrchestrationContext context, string input, TaskOptions? options = null)
+{
+ return context.CallSubOrchestratorAsync(""ApprovalOrchestrator"", input, options);
+}"),
+ ("Registrations", @"
+///
+/// Calls the activity.
+///
+///
+public static Task CallRegistrationActivityAsync(this TaskOrchestrationContext ctx, int input, TaskOptions? options = null)
+{
+ return ctx.CallActivityAsync(""RegistrationActivity"", input, options);
+}"),
+ ("Microsoft.DurableTask", @"
+internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder)
+{
+ builder.AddOrchestrator();
+ builder.AddActivity();
+ return builder;
+}")
+ );
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false);
+ }
+
+ [Fact]
+ public Task MultiNamespace_CustomTypesSimplifiedPerNamespace()
+ {
+ // Verify that custom types are simplified when in the same namespace as the generated code,
+ // but remain fully qualified when referenced from a different namespace
+ string code = @"
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+
+namespace OrderNS
+{
+ public class OrderInput { }
+ public class OrderOutput { }
+
+ [DurableTask(nameof(OrderActivity))]
+ public class OrderActivity : TaskActivity
+ {
+ public override Task RunAsync(TaskActivityContext context, OrderInput input) => Task.FromResult(new OrderOutput());
+ }
+}
+
+namespace ShippingNS
+{
+ public class ShippingRequest { }
+ public class ShippingResult { }
+
+ [DurableTask(nameof(ShippingActivity))]
+ public class ShippingActivity : TaskActivity
+ {
+ public override Task RunAsync(TaskActivityContext context, ShippingRequest input) => Task.FromResult(new ShippingResult());
+ }
+}";
+
+ string expectedOutput = TestHelpers.WrapAndFormatMultiNamespace(
+ GeneratedClassName,
+ isDurableFunctions: false,
+ ("OrderNS", @"
+///
+/// Calls the activity.
+///
+///
+public static Task CallOrderActivityAsync(this TaskOrchestrationContext ctx, OrderInput input, TaskOptions? options = null)
+{
+ return ctx.CallActivityAsync(""OrderActivity"", input, options);
+}"),
+ ("ShippingNS", @"
+///
+/// Calls the activity.
+///
+///
+public static Task CallShippingActivityAsync(this TaskOrchestrationContext ctx, ShippingRequest input, TaskOptions? options = null)
+{
+ return ctx.CallActivityAsync(""ShippingActivity"", input, options);
+}"),
+ ("Microsoft.DurableTask", @"
+internal static DurableTaskRegistry AddAllGeneratedTasks(this DurableTaskRegistry builder)
+{
+ builder.AddActivity();
+ builder.AddActivity();
+ return builder;
+}")
+ );
+
+ return TestHelpers.RunTestAsync(
+ GeneratedFileName,
+ code,
+ expectedOutput,
+ isDurableFunctions: false);
+ }
+
}
diff --git a/test/Generators.Tests/Utils/TestHelpers.cs b/test/Generators.Tests/Utils/TestHelpers.cs
index 92f3db86f..786033a76 100644
--- a/test/Generators.Tests/Utils/TestHelpers.cs
+++ b/test/Generators.Tests/Utils/TestHelpers.cs
@@ -82,12 +82,18 @@ public static Task RunTestAsync(
}
public static string WrapAndFormat(string generatedClassName, string methodList, bool isDurableFunctions = false)
+ {
+ return WrapAndFormat(generatedClassName, "Microsoft.DurableTask", methodList, isDurableFunctions);
+ }
+
+ public static string WrapAndFormat(string generatedClassName, string targetNamespace, string methodList, bool isDurableFunctions = false)
{
string formattedMethodList = IndentLines(spaces: 8, methodList);
string usings = @"
using System;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.DurableTask;
using Microsoft.DurableTask.Internal;";
if (isDurableFunctions)
@@ -102,7 +108,7 @@ public static string WrapAndFormat(string generatedClassName, string methodList,
#nullable enable
{usings}
-namespace Microsoft.DurableTask
+namespace {targetNamespace}
{{
public static class {generatedClassName}
{{
@@ -112,6 +118,51 @@ public static class {generatedClassName}
".TrimStart();
}
+ ///
+ /// Wraps content in multiple namespace blocks, producing the complete generated file structure.
+ ///
+ public static string WrapAndFormatMultiNamespace(
+ string generatedClassName,
+ bool isDurableFunctions,
+ params (string Namespace, string MethodList)[] namespaceBlocks)
+ {
+ string usings = @"
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Internal;";
+
+ if (isDurableFunctions)
+ {
+ usings += @"
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;";
+ }
+
+ StringBuilder sb = new();
+ sb.Append($@"//
+#nullable enable
+{usings}");
+
+ foreach ((string ns, string methodList) in namespaceBlocks)
+ {
+ string formattedMethodList = IndentLines(spaces: 8, methodList);
+ sb.Append($@"
+
+namespace {ns}
+{{
+ public static class {generatedClassName}
+ {{
+ {formattedMethodList.TrimStart()}
+ }}
+}}
+");
+ }
+
+ return sb.ToString().TrimStart();
+ }
+
static string IndentLines(int spaces, string multilineText)
{
string indent = new(' ', spaces);