-
Notifications
You must be signed in to change notification settings - Fork 56
Add strongly-typed external events with DurableEventAttribute #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
b739a9e
f76990a
e063765
fd94810
5233ff4
8a12d09
53e48ab
5f6823a
3bea221
56c0e32
810c98f
b495a87
6897cce
a356142
6c4db99
fc99572
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.DurableTask; | ||
|
|
||
| namespace EventsSample; | ||
|
|
||
| /// <summary> | ||
| /// Example event type annotated with DurableEventAttribute. | ||
| /// This generates a strongly-typed WaitForApprovalEventAsync method. | ||
| /// </summary> | ||
| [DurableEvent(nameof(ApprovalEvent))] | ||
| public sealed record ApprovalEvent(bool Approved, string? Approver); | ||
|
|
||
| /// <summary> | ||
| /// Another example event type with custom name. | ||
| /// This generates a WaitForDataReceivedAsync method that waits for "DataReceived" event. | ||
| /// </summary> | ||
| [DurableEvent("DataReceived")] | ||
| public sealed record DataReceivedEvent(int Id, string Data); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <!-- Using p2p references so we can show latest changes in samples. --> | ||
| <ProjectReference Include="$(SrcRoot)Client/Grpc/Client.Grpc.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Worker/Grpc/Worker.Grpc.csproj" /> | ||
|
|
||
| <!-- Reference the source generator --> | ||
| <ProjectReference Include="$(SrcRoot)Generators/Generators.csproj" | ||
| OutputItemType="Analyzer" | ||
| ReferenceOutputAssembly="false" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| // 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.Worker; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Hosting; | ||
|
|
||
| HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); | ||
|
|
||
| builder.Services.AddDurableTaskClient().UseGrpc(); | ||
| builder.Services.AddDurableTaskWorker() | ||
| .AddTasks(tasks => | ||
| { | ||
| tasks.AddOrchestrator<ApprovalOrchestrator>(); | ||
| tasks.AddActivity<NotifyApprovalRequiredActivity>(); | ||
| tasks.AddOrchestrator<DataProcessingOrchestrator>(); | ||
| tasks.AddActivity<ProcessDataActivity>(); | ||
| }) | ||
| .UseGrpc(); | ||
|
|
||
| IHost host = builder.Build(); | ||
| await host.StartAsync(); | ||
|
|
||
| await using DurableTaskClient client = host.Services.GetRequiredService<DurableTaskClient>(); | ||
|
|
||
| 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<string>()}"); | ||
| 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<string>()}"); | ||
| Console.WriteLine(); | ||
|
|
||
| Console.WriteLine("Sample completed successfully!"); | ||
| await host.StopAsync(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # 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<ApprovalEvent> WaitForApprovalEventAsync( | ||
| this TaskOrchestrationContext context, | ||
| CancellationToken cancellationToken = default); | ||
| ``` | ||
|
|
||
| ### Using the Generated Method in an Orchestrator | ||
|
|
||
| ```csharp | ||
| [DurableTask("ApprovalOrchestrator")] | ||
| public class ApprovalOrchestrator : TaskOrchestrator<string, string> | ||
| { | ||
| public override async Task<string> 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 | ||
|
|
||
| 1. Ensure you have the Durable Task sidecar running (if using gRPC mode) | ||
| 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<T>` with string literals | ||
| - **Refactoring Support**: Renaming event types automatically updates generated code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.DurableTask; | ||
|
|
||
| namespace EventsSample; | ||
|
|
||
| /// <summary> | ||
| /// Orchestrator that demonstrates strongly-typed external events. | ||
| /// </summary> | ||
| [DurableTask("ApprovalOrchestrator")] | ||
| public class ApprovalOrchestrator : TaskOrchestrator<string, string> | ||
| { | ||
| public override async Task<string> 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(); | ||
|
|
||
| if (approvalEvent.Approved) | ||
| { | ||
| return $"Request '{requestName}' was approved by {approvalEvent.Approver ?? "unknown"}"; | ||
| } | ||
| else | ||
| { | ||
| return $"Request '{requestName}' was rejected by {approvalEvent.Approver ?? "unknown"}"; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that simulates sending an approval notification. | ||
| /// </summary> | ||
| [DurableTask("NotifyApprovalRequired")] | ||
| public class NotifyApprovalRequiredActivity : TaskActivity<string, string> | ||
| { | ||
| public override Task<string> 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"); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Orchestrator that demonstrates waiting for multiple event types. | ||
| /// </summary> | ||
| [DurableTask("DataProcessingOrchestrator")] | ||
| public class DataProcessingOrchestrator : TaskOrchestrator<string, string> | ||
| { | ||
| public override async Task<string> 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}"; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that processes data. | ||
| /// </summary> | ||
| [DurableTask("ProcessData")] | ||
| public class ProcessDataActivity : TaskActivity<string, string> | ||
| { | ||
| public override Task<string> RunAsync(TaskActivityContext context, string data) | ||
| { | ||
| Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] Processing data: {data}"); | ||
| return Task.FromResult($"Processed: {data.ToUpper()}"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| namespace Microsoft.DurableTask; | ||
|
|
||
| /// <summary> | ||
| /// Indicates that the attributed type represents a durable event. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// 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. | ||
|
Comment on lines
+12
to
+13
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can also call
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot do it for sendevent as well
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added strongly-typed SendEvent methods in commit 6c4db99. For each event annotated with
Example for context.SendApprovalEvent(targetInstanceId, new ApprovalEvent(true, "John")); |
||
| /// </remarks> | ||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] | ||
| public sealed class DurableEventAttribute : Attribute | ||
| { | ||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="DurableEventAttribute"/> class. | ||
| /// </summary> | ||
| /// <param name="name"> | ||
| /// The name of the durable event. If not specified, the type name is used as the implied name of the durable event. | ||
| /// </param> | ||
| 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!); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets the name of the durable event. | ||
| /// </summary> | ||
| public TaskName Name { get; } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.