diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln
index 837a0ba0..5a5d79b1 100644
--- a/Microsoft.DurableTask.sln
+++ b/Microsoft.DurableTask.sln
@@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\In
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "test\InProcessTestHost.Tests\InProcessTestHost.Tests.csproj", "{B894780C-338F-475E-8E84-56AFA8197A06}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsSample", "samples\EventsSample\EventsSample.csproj", "{34A3EC44-2609-A058-ED30-2F81C3F3A885}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -251,14 +253,6 @@ Global
{D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.Build.0 = Release|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -271,6 +265,18 @@ Global
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
+ {34A3EC44-2609-A058-ED30-2F81C3F3A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {34A3EC44-2609-A058-ED30-2F81C3F3A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {34A3EC44-2609-A058-ED30-2F81C3F3A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {34A3EC44-2609-A058-ED30-2F81C3F3A885}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -316,11 +322,12 @@ Global
{A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{D2779F32-A548-44F8-B60A-6AC018966C79} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
- {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{7C3ECBCE-BEFB-4982-842E-B654BB6B6285} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
+ {34A3EC44-2609-A058-ED30-2F81C3F3A885} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
diff --git a/samples/AzureFunctionsApp/ApprovalOrchestrator.cs b/samples/AzureFunctionsApp/ApprovalOrchestrator.cs
new file mode 100644
index 00000000..9ecf059a
--- /dev/null
+++ b/samples/AzureFunctionsApp/ApprovalOrchestrator.cs
@@ -0,0 +1,121 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+
+namespace AzureFunctionsApp.Approval;
+
+///
+/// HTTP-triggered function that starts the orchestration.
+///
+public static class ApprovalOrchestratorStarter
+{
+ [Function(nameof(StartApprovalOrchestrator))]
+ public static async Task StartApprovalOrchestrator(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ FunctionContext executionContext)
+ {
+ ILogger logger = executionContext.GetLogger(nameof(StartApprovalOrchestrator));
+
+ string? requestName = await req.ReadAsStringAsync();
+ if (string.IsNullOrEmpty(requestName))
+ {
+ requestName = "Sample Request";
+ }
+
+ // Use the generated type-safe extension method to start the orchestration
+ string instanceId = await client.ScheduleNewApprovalOrchestratorInstanceAsync(requestName);
+ logger.LogInformation("Started approval orchestration with instance ID = {instanceId}", instanceId);
+
+ return client.CreateCheckStatusResponse(req, instanceId);
+ }
+
+ [Function(nameof(SendApprovalEvent))]
+ public static async Task SendApprovalEvent(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "approval/{instanceId}")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ string instanceId,
+ FunctionContext executionContext)
+ {
+ ILogger logger = executionContext.GetLogger(nameof(SendApprovalEvent));
+
+ string? approverName = await req.ReadAsStringAsync();
+ bool isApproved = req.Url.Query.Contains("approve=true");
+
+ // Raise the ApprovalEvent
+ await client.RaiseEventAsync(instanceId, "ApprovalEvent", new ApprovalEvent(isApproved, approverName));
+ logger.LogInformation("Sent approval event to instance {instanceId}: approved={isApproved}, approver={approverName}",
+ instanceId, isApproved, approverName);
+
+ var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
+ await response.WriteStringAsync($"Approval event sent to instance {instanceId}");
+ return response;
+ }
+}
+
+///
+/// Example event type for approval workflows.
+/// The DurableEventAttribute generates a strongly-typed WaitForApprovalEventAsync method.
+///
+[DurableEvent(nameof(ApprovalEvent))]
+public sealed record ApprovalEvent(bool Approved, string? Approver);
+
+///
+/// Orchestrator that demonstrates strongly-typed external events.
+///
+[DurableTask(nameof(ApprovalOrchestrator))]
+public class ApprovalOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string requestName)
+ {
+ ILogger logger = context.CreateReplaySafeLogger();
+ logger.LogInformation("Approval request received for: {requestName}", requestName);
+
+ // Send a notification that approval is required
+ await context.CallNotifyApprovalRequiredAsync(requestName);
+
+ // Wait for approval event using the generated strongly-typed method
+ // This method is generated by the source generator from the DurableEventAttribute
+ ApprovalEvent approvalEvent = await context.WaitForApprovalEventAsync();
+
+ string result;
+ if (approvalEvent.Approved)
+ {
+ result = $"Request '{requestName}' was approved by {approvalEvent.Approver ?? "unknown"}";
+ logger.LogInformation("Request approved: {result}", result);
+ }
+ else
+ {
+ result = $"Request '{requestName}' was rejected by {approvalEvent.Approver ?? "unknown"}";
+ logger.LogInformation("Request rejected: {result}", result);
+ }
+
+ return result;
+ }
+}
+
+///
+/// Activity that simulates sending an approval notification.
+///
+[DurableTask(nameof(NotifyApprovalRequired))]
+public class NotifyApprovalRequired : TaskActivity
+{
+ readonly ILogger logger;
+
+ public NotifyApprovalRequired(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ public override Task RunAsync(TaskActivityContext context, string requestName)
+ {
+ this.logger.LogInformation("Approval required for: {requestName} (Instance: {instanceId})",
+ requestName, context.InstanceId);
+ return Task.FromResult("Notification sent");
+ }
+}
diff --git a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
index ad64c303..25d824fa 100644
--- a/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
+++ b/samples/AzureFunctionsApp/AzureFunctionsApp.csproj
@@ -17,6 +17,8 @@
+
+
diff --git a/samples/EventsSample/Events.cs b/samples/EventsSample/Events.cs
new file mode 100644
index 00000000..e284acae
--- /dev/null
+++ b/samples/EventsSample/Events.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.DurableTask;
+
+namespace EventsSample;
+
+///
+/// Example event type annotated with DurableEventAttribute.
+/// This generates a strongly-typed WaitForApprovalEventAsync method.
+///
+[DurableEvent(nameof(ApprovalEvent))]
+public sealed record ApprovalEvent(bool Approved, string? Approver);
+
+///
+/// Another example event type with custom name.
+/// This generates a WaitForDataReceivedAsync method that waits for "DataReceived" event.
+///
+[DurableEvent("DataReceived")]
+public sealed record DataReceivedEvent(int Id, string Data);
diff --git a/samples/EventsSample/EventsSample.csproj b/samples/EventsSample/EventsSample.csproj
new file mode 100644
index 00000000..7df90c5a
--- /dev/null
+++ b/samples/EventsSample/EventsSample.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net6.0;net8.0;net10.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/EventsSample/Program.cs b/samples/EventsSample/Program.cs
new file mode 100644
index 00000000..90487bc5
--- /dev/null
+++ b/samples/EventsSample/Program.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// This sample demonstrates the use of strongly-typed external events with DurableEventAttribute.
+
+using EventsSample;
+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;
+
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+
+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 =>
+{
+ workerBuilder.AddTasks(tasks =>
+ {
+ tasks.AddOrchestrator();
+ tasks.AddActivity();
+ tasks.AddOrchestrator();
+ tasks.AddActivity();
+ });
+
+ workerBuilder.UseDurableTaskScheduler(schedulerConnectionString);
+});
+
+IHost host = builder.Build();
+await host.StartAsync();
+
+await using DurableTaskClient client = host.Services.GetRequiredService();
+
+Console.WriteLine("=== Strongly-Typed Events Sample ===");
+Console.WriteLine();
+
+// Example 1: Approval workflow
+Console.WriteLine("Starting approval workflow...");
+string approvalInstanceId = await client.ScheduleNewOrchestrationInstanceAsync("ApprovalOrchestrator", "Important Request");
+Console.WriteLine($"Started orchestration with ID: {approvalInstanceId}");
+Console.WriteLine();
+
+// Wait a moment for the notification to be sent
+await Task.Delay(1000);
+
+// Simulate approval
+Console.WriteLine("Simulating approval event...");
+await client.RaiseEventAsync(approvalInstanceId, "ApprovalEvent", new ApprovalEvent(true, "John Doe"));
+
+// Wait for completion
+OrchestrationMetadata approvalResult = await client.WaitForInstanceCompletionAsync(
+ approvalInstanceId,
+ getInputsAndOutputs: true);
+Console.WriteLine($"Approval workflow result: {approvalResult.ReadOutputAs()}");
+Console.WriteLine();
+
+// Example 2: Data processing workflow
+Console.WriteLine("Starting data processing workflow...");
+string dataInstanceId = await client.ScheduleNewOrchestrationInstanceAsync("DataProcessingOrchestrator", "test-input");
+Console.WriteLine($"Started orchestration with ID: {dataInstanceId}");
+Console.WriteLine();
+
+// Wait a moment
+await Task.Delay(1000);
+
+// Send data event
+Console.WriteLine("Sending data event...");
+await client.RaiseEventAsync(dataInstanceId, "DataReceived", new DataReceivedEvent(123, "Sample Data"));
+
+// Wait for completion
+OrchestrationMetadata dataResult = await client.WaitForInstanceCompletionAsync(
+ dataInstanceId,
+ getInputsAndOutputs: true);
+Console.WriteLine($"Data processing result: {dataResult.ReadOutputAs()}");
+Console.WriteLine();
+
+Console.WriteLine("Sample completed successfully!");
+await host.StopAsync();
diff --git a/samples/EventsSample/README.md b/samples/EventsSample/README.md
new file mode 100644
index 00000000..a90a3c13
--- /dev/null
+++ b/samples/EventsSample/README.md
@@ -0,0 +1,88 @@
+# Strongly-Typed Events Sample
+
+This sample demonstrates the use of strongly-typed external events using the `DurableEventAttribute`.
+
+## Overview
+
+The `DurableEventAttribute` allows you to define event types that automatically generate strongly-typed extension methods for waiting on external events in orchestrations. This provides compile-time type safety and better IntelliSense support.
+
+## Key Features
+
+1. **Strongly-Typed Event Definitions**: Define event types using records or classes with the `[DurableEvent]` attribute
+2. **Generated Extension Methods**: The source generator automatically creates `WaitFor{EventName}Async` methods
+3. **Type Safety**: Event payloads are strongly-typed, reducing runtime errors
+
+## Sample Code
+
+### Defining an Event
+
+```csharp
+[DurableEvent(nameof(ApprovalEvent))]
+public sealed record ApprovalEvent(bool Approved, string? Approver);
+```
+
+This generates an extension method:
+
+```csharp
+public static Task WaitForApprovalEventAsync(
+ this TaskOrchestrationContext context,
+ CancellationToken cancellationToken = default);
+```
+
+### Using the Generated Method in an Orchestrator
+
+```csharp
+[DurableTask("ApprovalOrchestrator")]
+public class ApprovalOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string requestName)
+ {
+ // Wait for approval event using the generated strongly-typed method
+ ApprovalEvent approvalEvent = await context.WaitForApprovalEventAsync();
+
+ if (approvalEvent.Approved)
+ {
+ return $"Request approved by {approvalEvent.Approver}";
+ }
+ else
+ {
+ return $"Request rejected by {approvalEvent.Approver}";
+ }
+ }
+}
+```
+
+### Raising Events from Client Code
+
+```csharp
+await client.RaiseEventAsync(
+ instanceId,
+ "ApprovalEvent",
+ new ApprovalEvent(true, "John Doe"));
+```
+
+## Running the Sample
+
+This sample is configured to use **Durable Task Scheduler (DTS)** (no local gRPC sidecar required).
+
+1. Set the DTS connection string:
+ ```bash
+ export DURABLE_TASK_SCHEDULER_CONNECTION_STRING="..."
+ ```
+2. Run the sample:
+ ```bash
+ dotnet run
+ ```
+
+The sample will:
+1. Start an approval workflow and wait for an approval event
+2. Raise an approval event from the client
+3. Complete the workflow with the approval result
+4. Start a data processing workflow and demonstrate another event type
+
+## Benefits
+
+- **Type Safety**: Compile-time checking of event payloads
+- **IntelliSense**: Better IDE support for discovering available event methods
+- **Less Boilerplate**: No need to manually call `WaitForExternalEvent` with string literals
+- **Refactoring Support**: Renaming event types automatically updates generated code
diff --git a/samples/EventsSample/Tasks.cs b/samples/EventsSample/Tasks.cs
new file mode 100644
index 00000000..4f8756fb
--- /dev/null
+++ b/samples/EventsSample/Tasks.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.DurableTask;
+
+namespace EventsSample;
+
+///
+/// Orchestrator that demonstrates strongly-typed external events.
+///
+[DurableTask("ApprovalOrchestrator")]
+public class ApprovalOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string requestName)
+ {
+ // Send a notification requesting approval
+ await context.CallNotifyApprovalRequiredAsync(requestName);
+
+ // Wait for approval event using the generated strongly-typed method
+ // Note: WaitForApprovalEventAsync is generated by the source generator
+ ApprovalEvent approvalEvent = await context.WaitForApprovalEventAsync();
+
+ return $"Request '{requestName}' was {(approvalEvent.Approved ? "approved" : "rejected")} by {approvalEvent.Approver ?? "unknown"}";
+ }
+}
+
+///
+/// Activity that simulates sending an approval notification.
+///
+[DurableTask("NotifyApprovalRequired")]
+public class NotifyApprovalRequiredActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, string requestName)
+ {
+ Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] Approval required for: {requestName}");
+ Console.WriteLine($" Instance ID: {context.InstanceId}");
+ return Task.FromResult("Notification sent");
+ }
+}
+
+///
+/// Orchestrator that demonstrates waiting for multiple event types.
+///
+[DurableTask("DataProcessingOrchestrator")]
+public class DataProcessingOrchestrator : TaskOrchestrator
+{
+ public override async Task RunAsync(TaskOrchestrationContext context, string input)
+ {
+ // Wait for data using the generated strongly-typed method
+ DataReceivedEvent dataEvent = await context.WaitForDataReceivedAsync();
+
+ // Process the data
+ string result = await context.CallProcessDataAsync(dataEvent.Data);
+
+ return $"Processed data {dataEvent.Id}: {result}";
+ }
+}
+
+///
+/// Activity that processes data.
+///
+[DurableTask("ProcessData")]
+public class ProcessDataActivity : TaskActivity
+{
+ public override Task RunAsync(TaskActivityContext context, string data)
+ {
+ Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] Processing data: {data}");
+ return Task.FromResult($"Processed: {data.ToUpper()}");
+ }
+}
diff --git a/src/Abstractions/DurableEventAttribute.cs b/src/Abstractions/DurableEventAttribute.cs
new file mode 100644
index 00000000..f8cb18b2
--- /dev/null
+++ b/src/Abstractions/DurableEventAttribute.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Microsoft.DurableTask;
+
+///
+/// Indicates that the attributed type represents a durable event.
+///
+///
+/// This attribute is meant to be used on type definitions to generate strongly-typed
+/// external event methods for orchestration contexts.
+/// It is used specifically by build-time source generators to generate type-safe methods for waiting
+/// for external events in orchestrations.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class DurableEventAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The name of the durable event. If not specified, the type name is used as the implied name of the durable event.
+ ///
+ public DurableEventAttribute(string? name = null)
+ {
+ // This logic cannot become too complex as code-generator relies on examining the constructor arguments.
+ this.Name = string.IsNullOrEmpty(name) ? default : new TaskName(name!);
+ }
+
+ ///
+ /// Gets the name of the durable event.
+ ///
+ public TaskName Name { get; }
+}
diff --git a/src/Generators/DurableTaskSourceGenerator.cs b/src/Generators/DurableTaskSourceGenerator.cs
index 19a24cc9..0b4e717e 100644
--- a/src/Generators/DurableTaskSourceGenerator.cs
+++ b/src/Generators/DurableTaskSourceGenerator.cs
@@ -49,6 +49,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
transform: static (ctx, _) => GetDurableTaskTypeInfo(ctx))
.Where(static info => info != null)!;
+ // Create providers for DurableEvent attributes
+ IncrementalValuesProvider durableEventAttributes = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: static (node, _) => node is AttributeSyntax,
+ transform: static (ctx, _) => GetDurableEventTypeInfo(ctx))
+ .Where(static info => info != null)!;
+
// Create providers for Durable Functions
IncrementalValuesProvider durableFunctions = context.SyntaxProvider
.CreateSyntaxProvider(
@@ -57,14 +64,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Where(static func => func != null)!;
// Collect all results and check if Durable Functions is referenced
- IncrementalValueProvider<(Compilation, ImmutableArray, ImmutableArray)> compilationAndTasks =
+ IncrementalValueProvider<(Compilation, ImmutableArray, ImmutableArray, ImmutableArray)> compilationAndTasks =
durableTaskAttributes.Collect()
+ .Combine(durableEventAttributes.Collect())
.Combine(durableFunctions.Collect())
.Combine(context.CompilationProvider)
- .Select((x, _) => (x.Right, x.Left.Left, x.Left.Right));
+ // 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));
// Generate the source
- context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3));
+ context.RegisterSourceOutput(compilationAndTasks, static (spc, source) => Execute(spc, source.Item1, source.Item2, source.Item3, source.Item4));
}
static DurableTaskTypeInfo? GetDurableTaskTypeInfo(GeneratorSyntaxContext context)
@@ -161,6 +175,49 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
return new DurableTaskTypeInfo(className, taskName, inputType, outputType, kind);
}
+ static DurableEventTypeInfo? GetDurableEventTypeInfo(GeneratorSyntaxContext context)
+ {
+ AttributeSyntax attribute = (AttributeSyntax)context.Node;
+
+ ITypeSymbol? attributeType = context.SemanticModel.GetTypeInfo(attribute.Name).Type;
+ if (attributeType?.ToString() != "Microsoft.DurableTask.DurableEventAttribute")
+ {
+ return null;
+ }
+
+ // DurableEventAttribute can be applied to both class and struct (record)
+ TypeDeclarationSyntax? typeDeclaration = attribute.Parent?.Parent as TypeDeclarationSyntax;
+ if (typeDeclaration == null)
+ {
+ return null;
+ }
+
+ // Verify that the attribute is being used on a non-abstract type
+ if (typeDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
+ {
+ return null;
+ }
+
+ if (context.SemanticModel.GetDeclaredSymbol(typeDeclaration) is not ITypeSymbol eventType)
+ {
+ return null;
+ }
+
+ string eventName = eventType.Name;
+
+ if (attribute.ArgumentList?.Arguments.Count > 0)
+ {
+ ExpressionSyntax expression = attribute.ArgumentList.Arguments[0].Expression;
+ Optional