Skip to content

Commit f7732a6

Browse files
authored
Add strongly-typed external events with DurableEventAttribute (#549)
1 parent 61274c2 commit f7732a6

File tree

12 files changed

+719
-20
lines changed

12 files changed

+719
-20
lines changed

Microsoft.DurableTask.sln

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\In
101101
EndProject
102102
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "test\InProcessTestHost.Tests\InProcessTestHost.Tests.csproj", "{B894780C-338F-475E-8E84-56AFA8197A06}"
103103
EndProject
104+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsSample", "samples\EventsSample\EventsSample.csproj", "{34A3EC44-2609-A058-ED30-2F81C3F3A885}"
105+
EndProject
104106
Global
105107
GlobalSection(SolutionConfigurationPlatforms) = preSolution
106108
Debug|Any CPU = Debug|Any CPU
@@ -251,14 +253,6 @@ Global
251253
{D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.Build.0 = Debug|Any CPU
252254
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.ActiveCfg = Release|Any CPU
253255
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.Build.0 = Release|Any CPU
254-
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
255-
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
256-
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
257-
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
258-
{B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
259-
{B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
260-
{B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
261-
{B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
262256
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
263257
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
264258
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -271,6 +265,18 @@ Global
271265
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.Build.0 = Debug|Any CPU
272266
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.ActiveCfg = Release|Any CPU
273267
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.Build.0 = Release|Any CPU
268+
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
269+
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
270+
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
271+
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
272+
{B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
273+
{B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
274+
{B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
275+
{B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
276+
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
277+
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
278+
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
279+
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Release|Any CPU.Build.0 = Release|Any CPU
274280
EndGlobalSection
275281
GlobalSection(SolutionProperties) = preSolution
276282
HideSolutionNode = FALSE
@@ -316,11 +322,12 @@ Global
316322
{A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
317323
{100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
318324
{D2779F32-A548-44F8-B60A-6AC018966C79} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
319-
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
320-
{B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
321325
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
322326
{7C3ECBCE-BEFB-4982-842E-B654BB6B6285} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
323327
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
328+
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
329+
{B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
330+
{34A3EC44-2609-A058-ED30-2F81C3F3A885} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
324331
EndGlobalSection
325332
GlobalSection(ExtensibilityGlobals) = postSolution
326333
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Azure.Functions.Worker;
5+
using Microsoft.Azure.Functions.Worker.Http;
6+
using Microsoft.DurableTask;
7+
using Microsoft.DurableTask.Client;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace AzureFunctionsApp.Approval;
11+
12+
/// <summary>
13+
/// HTTP-triggered function that starts the <see cref="ApprovalOrchestrator"/> orchestration.
14+
/// </summary>
15+
public static class ApprovalOrchestratorStarter
16+
{
17+
[Function(nameof(StartApprovalOrchestrator))]
18+
public static async Task<HttpResponseData> StartApprovalOrchestrator(
19+
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
20+
[DurableClient] DurableTaskClient client,
21+
FunctionContext executionContext)
22+
{
23+
ILogger logger = executionContext.GetLogger(nameof(StartApprovalOrchestrator));
24+
25+
string? requestName = await req.ReadAsStringAsync();
26+
if (string.IsNullOrEmpty(requestName))
27+
{
28+
requestName = "Sample Request";
29+
}
30+
31+
// Use the generated type-safe extension method to start the orchestration
32+
string instanceId = await client.ScheduleNewApprovalOrchestratorInstanceAsync(requestName);
33+
logger.LogInformation("Started approval orchestration with instance ID = {instanceId}", instanceId);
34+
35+
return client.CreateCheckStatusResponse(req, instanceId);
36+
}
37+
38+
[Function(nameof(SendApprovalEvent))]
39+
public static async Task<HttpResponseData> SendApprovalEvent(
40+
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "approval/{instanceId}")] HttpRequestData req,
41+
[DurableClient] DurableTaskClient client,
42+
string instanceId,
43+
FunctionContext executionContext)
44+
{
45+
ILogger logger = executionContext.GetLogger(nameof(SendApprovalEvent));
46+
47+
string? approverName = await req.ReadAsStringAsync();
48+
bool isApproved = req.Url.Query.Contains("approve=true");
49+
50+
// Raise the ApprovalEvent
51+
await client.RaiseEventAsync(instanceId, "ApprovalEvent", new ApprovalEvent(isApproved, approverName));
52+
logger.LogInformation("Sent approval event to instance {instanceId}: approved={isApproved}, approver={approverName}",
53+
instanceId, isApproved, approverName);
54+
55+
var response = req.CreateResponse(System.Net.HttpStatusCode.Accepted);
56+
await response.WriteStringAsync($"Approval event sent to instance {instanceId}");
57+
return response;
58+
}
59+
}
60+
61+
/// <summary>
62+
/// Example event type for approval workflows.
63+
/// The DurableEventAttribute generates a strongly-typed WaitForApprovalEventAsync method.
64+
/// </summary>
65+
[DurableEvent(nameof(ApprovalEvent))]
66+
public sealed record ApprovalEvent(bool Approved, string? Approver);
67+
68+
/// <summary>
69+
/// Orchestrator that demonstrates strongly-typed external events.
70+
/// </summary>
71+
[DurableTask(nameof(ApprovalOrchestrator))]
72+
public class ApprovalOrchestrator : TaskOrchestrator<string, string>
73+
{
74+
public override async Task<string> RunAsync(TaskOrchestrationContext context, string requestName)
75+
{
76+
ILogger logger = context.CreateReplaySafeLogger<ApprovalOrchestrator>();
77+
logger.LogInformation("Approval request received for: {requestName}", requestName);
78+
79+
// Send a notification that approval is required
80+
await context.CallNotifyApprovalRequiredAsync(requestName);
81+
82+
// Wait for approval event using the generated strongly-typed method
83+
// This method is generated by the source generator from the DurableEventAttribute
84+
ApprovalEvent approvalEvent = await context.WaitForApprovalEventAsync();
85+
86+
string result;
87+
if (approvalEvent.Approved)
88+
{
89+
result = $"Request '{requestName}' was approved by {approvalEvent.Approver ?? "unknown"}";
90+
logger.LogInformation("Request approved: {result}", result);
91+
}
92+
else
93+
{
94+
result = $"Request '{requestName}' was rejected by {approvalEvent.Approver ?? "unknown"}";
95+
logger.LogInformation("Request rejected: {result}", result);
96+
}
97+
98+
return result;
99+
}
100+
}
101+
102+
/// <summary>
103+
/// Activity that simulates sending an approval notification.
104+
/// </summary>
105+
[DurableTask(nameof(NotifyApprovalRequired))]
106+
public class NotifyApprovalRequired : TaskActivity<string, string>
107+
{
108+
readonly ILogger logger;
109+
110+
public NotifyApprovalRequired(ILogger<NotifyApprovalRequired> logger)
111+
{
112+
this.logger = logger;
113+
}
114+
115+
public override Task<string> RunAsync(TaskActivityContext context, string requestName)
116+
{
117+
this.logger.LogInformation("Approval required for: {requestName} (Instance: {instanceId})",
118+
requestName, context.InstanceId);
119+
return Task.FromResult("Notification sent");
120+
}
121+
}

samples/AzureFunctionsApp/AzureFunctionsApp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" OutputItemType="Analyzer" />
1818
<!-- Reference the source generator project directly for local development -->
1919
<ProjectReference Include="..\..\src\Generators\Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
20+
<!-- Reference Abstractions for latest DurableEventAttribute -->
21+
<ProjectReference Include="..\..\src\Abstractions\Abstractions.csproj" />
2022
</ItemGroup>
2123

2224
<ItemGroup>

samples/EventsSample/Events.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DurableTask;
5+
6+
namespace EventsSample;
7+
8+
/// <summary>
9+
/// Example event type annotated with DurableEventAttribute.
10+
/// This generates a strongly-typed WaitForApprovalEventAsync method.
11+
/// </summary>
12+
[DurableEvent(nameof(ApprovalEvent))]
13+
public sealed record ApprovalEvent(bool Approved, string? Approver);
14+
15+
/// <summary>
16+
/// Another example event type with custom name.
17+
/// This generates a WaitForDataReceivedAsync method that waits for "DataReceived" event.
18+
/// </summary>
19+
[DurableEvent("DataReceived")]
20+
public sealed record DataReceivedEvent(int Id, string Data);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFrameworks>net6.0;net8.0;net10.0</TargetFrameworks>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Hosting" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<!-- Using p2p references so we can show latest changes in samples. -->
15+
<ProjectReference Include="$(SrcRoot)Client/AzureManaged/Client.AzureManaged.csproj" />
16+
<ProjectReference Include="$(SrcRoot)Worker/AzureManaged/Worker.AzureManaged.csproj" />
17+
18+
<!-- Reference the source generator -->
19+
<ProjectReference Include="$(SrcRoot)Generators/Generators.csproj"
20+
OutputItemType="Analyzer"
21+
ReferenceOutputAssembly="false" />
22+
</ItemGroup>
23+
24+
</Project>

samples/EventsSample/Program.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// This sample demonstrates the use of strongly-typed external events with DurableEventAttribute.
5+
6+
using EventsSample;
7+
using Microsoft.DurableTask;
8+
using Microsoft.DurableTask.Client;
9+
using Microsoft.DurableTask.Client.AzureManaged;
10+
using Microsoft.DurableTask.Worker;
11+
using Microsoft.DurableTask.Worker.AzureManaged;
12+
using Microsoft.Extensions.Configuration;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Hosting;
15+
16+
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
17+
18+
string schedulerConnectionString = builder.Configuration.GetValue<string>("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
19+
?? throw new InvalidOperationException("DURABLE_TASK_SCHEDULER_CONNECTION_STRING is not set.");
20+
21+
builder.Services.AddDurableTaskClient(clientBuilder => clientBuilder.UseDurableTaskScheduler(schedulerConnectionString));
22+
23+
builder.Services.AddDurableTaskWorker(workerBuilder =>
24+
{
25+
workerBuilder.AddTasks(tasks =>
26+
{
27+
tasks.AddOrchestrator<ApprovalOrchestrator>();
28+
tasks.AddActivity<NotifyApprovalRequiredActivity>();
29+
tasks.AddOrchestrator<DataProcessingOrchestrator>();
30+
tasks.AddActivity<ProcessDataActivity>();
31+
});
32+
33+
workerBuilder.UseDurableTaskScheduler(schedulerConnectionString);
34+
});
35+
36+
IHost host = builder.Build();
37+
await host.StartAsync();
38+
39+
await using DurableTaskClient client = host.Services.GetRequiredService<DurableTaskClient>();
40+
41+
Console.WriteLine("=== Strongly-Typed Events Sample ===");
42+
Console.WriteLine();
43+
44+
// Example 1: Approval workflow
45+
Console.WriteLine("Starting approval workflow...");
46+
string approvalInstanceId = await client.ScheduleNewOrchestrationInstanceAsync("ApprovalOrchestrator", "Important Request");
47+
Console.WriteLine($"Started orchestration with ID: {approvalInstanceId}");
48+
Console.WriteLine();
49+
50+
// Wait a moment for the notification to be sent
51+
await Task.Delay(1000);
52+
53+
// Simulate approval
54+
Console.WriteLine("Simulating approval event...");
55+
await client.RaiseEventAsync(approvalInstanceId, "ApprovalEvent", new ApprovalEvent(true, "John Doe"));
56+
57+
// Wait for completion
58+
OrchestrationMetadata approvalResult = await client.WaitForInstanceCompletionAsync(
59+
approvalInstanceId,
60+
getInputsAndOutputs: true);
61+
Console.WriteLine($"Approval workflow result: {approvalResult.ReadOutputAs<string>()}");
62+
Console.WriteLine();
63+
64+
// Example 2: Data processing workflow
65+
Console.WriteLine("Starting data processing workflow...");
66+
string dataInstanceId = await client.ScheduleNewOrchestrationInstanceAsync("DataProcessingOrchestrator", "test-input");
67+
Console.WriteLine($"Started orchestration with ID: {dataInstanceId}");
68+
Console.WriteLine();
69+
70+
// Wait a moment
71+
await Task.Delay(1000);
72+
73+
// Send data event
74+
Console.WriteLine("Sending data event...");
75+
await client.RaiseEventAsync(dataInstanceId, "DataReceived", new DataReceivedEvent(123, "Sample Data"));
76+
77+
// Wait for completion
78+
OrchestrationMetadata dataResult = await client.WaitForInstanceCompletionAsync(
79+
dataInstanceId,
80+
getInputsAndOutputs: true);
81+
Console.WriteLine($"Data processing result: {dataResult.ReadOutputAs<string>()}");
82+
Console.WriteLine();
83+
84+
Console.WriteLine("Sample completed successfully!");
85+
await host.StopAsync();

0 commit comments

Comments
 (0)