-
Notifications
You must be signed in to change notification settings - Fork 53
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b739a9e
Initial plan
Copilot f76990a
Add strongly-typed events with DurableEventAttribute and generator su…
Copilot e063765
Add EventsSample demonstrating strongly-typed events
Copilot fd94810
Fix null safety in event name extraction
Copilot 5233ff4
Potential fix for pull request finding 'Missed ternary opportunity'
YunchuWang 8a12d09
remove sln file change
YunchuWang 53e48ab
Merge branch 'main' of https://github.com/microsoft/durabletask-dotne…
YunchuWang 5f6823a
update sln
YunchuWang 3bea221
Merge branch 'main' into copilot/add-strongly-typed-events
YunchuWang 56c0e32
use dts
YunchuWang 810c98f
cleanup
YunchuWang b495a87
Add strongly-typed events to AzureFunctionsApp sample
Copilot 6897cce
cleanuip
YunchuWang a356142
only register if any tasks to register
YunchuWang 6c4db99
Add strongly-typed SendEvent methods for DurableEventAttribute
Copilot fc99572
Add explanatory comments for Roslyn tuple unpacking syntax
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// HTTP-triggered function that starts the <see cref="ApprovalOrchestrator"/> orchestration. | ||
| /// </summary> | ||
| public static class ApprovalOrchestratorStarter | ||
| { | ||
| [Function(nameof(StartApprovalOrchestrator))] | ||
| public static async Task<HttpResponseData> 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<HttpResponseData> 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; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Example event type for approval workflows. | ||
| /// The DurableEventAttribute generates a strongly-typed WaitForApprovalEventAsync method. | ||
| /// </summary> | ||
| [DurableEvent(nameof(ApprovalEvent))] | ||
| public sealed record ApprovalEvent(bool Approved, string? Approver); | ||
|
|
||
| /// <summary> | ||
| /// Orchestrator that demonstrates strongly-typed external events. | ||
| /// </summary> | ||
| [DurableTask(nameof(ApprovalOrchestrator))] | ||
| public class ApprovalOrchestrator : TaskOrchestrator<string, string> | ||
| { | ||
| public override async Task<string> RunAsync(TaskOrchestrationContext context, string requestName) | ||
| { | ||
| ILogger logger = context.CreateReplaySafeLogger<ApprovalOrchestrator>(); | ||
| 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; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Activity that simulates sending an approval notification. | ||
| /// </summary> | ||
| [DurableTask(nameof(NotifyApprovalRequired))] | ||
| public class NotifyApprovalRequired : TaskActivity<string, string> | ||
| { | ||
| readonly ILogger logger; | ||
|
|
||
| public NotifyApprovalRequired(ILogger<NotifyApprovalRequired> logger) | ||
| { | ||
| this.logger = logger; | ||
| } | ||
|
|
||
| public override Task<string> RunAsync(TaskActivityContext context, string requestName) | ||
| { | ||
| this.logger.LogInformation("Approval required for: {requestName} (Instance: {instanceId})", | ||
| requestName, context.InstanceId); | ||
| return Task.FromResult("Notification sent"); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/AzureManaged/Client.AzureManaged.csproj" /> | ||
| <ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" /> | ||
|
|
||
| <!-- Reference the source generator --> | ||
| <ProjectReference Include="$(SrcRoot)Generators/Generators.csproj" | ||
| OutputItemType="Analyzer" | ||
| ReferenceOutputAssembly="false" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string>("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<ApprovalOrchestrator>(); | ||
| tasks.AddActivity<NotifyApprovalRequiredActivity>(); | ||
| tasks.AddOrchestrator<DataProcessingOrchestrator>(); | ||
| tasks.AddActivity<ProcessDataActivity>(); | ||
| }); | ||
|
|
||
| workerBuilder.UseDurableTaskScheduler(schedulerConnectionString); | ||
| }); | ||
|
|
||
| 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(); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.