Skip to content

Commit b495a87

Browse files
CopilotYunchuWang
andcommitted
Add strongly-typed events to AzureFunctionsApp sample
- Added ApprovalOrchestrator.cs demonstrating DurableEventAttribute usage in Azure Functions - Includes ApprovalEvent record with [DurableEvent] attribute - ApprovalOrchestrator shows waiting for events with generated WaitForApprovalEventAsync - Added HTTP triggers to start orchestration and send approval events - Added NotifyApprovalRequired activity - Included APPROVAL_README.md with usage documentation - Added Abstractions project reference to access DurableEventAttribute - Fixed generator to not emit AddAllGeneratedTasks when only events are present - All 41 generator tests passing Co-authored-by: YunchuWang <[email protected]>
1 parent 810c98f commit b495a87

File tree

4 files changed

+218
-5
lines changed

4 files changed

+218
-5
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Approval Orchestrator Sample
2+
3+
This sample demonstrates the use of strongly-typed external events with Azure Functions using the `DurableEventAttribute`.
4+
5+
## Overview
6+
7+
The Approval Orchestrator showcases how to:
8+
1. Define an event type with `[DurableEvent]` attribute
9+
2. Use the generated strongly-typed `WaitFor{EventName}Async` method
10+
3. Raise events from HTTP triggers
11+
12+
## Files
13+
14+
- **ApprovalOrchestrator.cs**: Contains the orchestrator, activity, and event definitions
15+
16+
## Event Definition
17+
18+
```csharp
19+
[DurableEvent(nameof(ApprovalEvent))]
20+
public sealed record ApprovalEvent(bool Approved, string? Approver);
21+
```
22+
23+
The source generator automatically creates:
24+
```csharp
25+
public static Task<ApprovalEvent> WaitForApprovalEventAsync(
26+
this TaskOrchestrationContext context,
27+
CancellationToken cancellationToken = default)
28+
```
29+
30+
## Usage in Orchestrator
31+
32+
```csharp
33+
[DurableTask(nameof(ApprovalOrchestrator))]
34+
public class ApprovalOrchestrator : TaskOrchestrator<string, string>
35+
{
36+
public override async Task<string> RunAsync(TaskOrchestrationContext context, string requestName)
37+
{
38+
// Send notification
39+
await context.CallNotifyApprovalRequiredAsync(requestName);
40+
41+
// Wait for approval using strongly-typed method
42+
ApprovalEvent approvalEvent = await context.WaitForApprovalEventAsync();
43+
44+
if (approvalEvent.Approved)
45+
{
46+
return $"Request '{requestName}' was approved by {approvalEvent.Approver}";
47+
}
48+
else
49+
{
50+
return $"Request '{requestName}' was rejected by {approvalEvent.Approver}";
51+
}
52+
}
53+
}
54+
```
55+
56+
## Testing the Sample
57+
58+
1. Start the orchestration:
59+
```bash
60+
curl -X POST http://localhost:7071/api/StartApprovalOrchestrator \
61+
-H "Content-Type: text/plain" \
62+
-d "My Important Request"
63+
```
64+
65+
2. Send an approval event:
66+
```bash
67+
curl -X POST "http://localhost:7071/api/approval/{instanceId}?approve=true" \
68+
-H "Content-Type: text/plain" \
69+
-d "John Doe"
70+
```
71+
72+
Or reject:
73+
```bash
74+
curl -X POST "http://localhost:7071/api/approval/{instanceId}?approve=false" \
75+
-H "Content-Type: text/plain" \
76+
-d "Jane Smith"
77+
```
78+
79+
3. Check the orchestration status using the links returned from the start request.
80+
81+
## Benefits
82+
83+
- **Type Safety**: Compile-time checking of event payloads
84+
- **IntelliSense**: IDE support for discovering available event methods
85+
- **Less Boilerplate**: No need for string literals and explicit generic types
86+
- **Refactoring Support**: Renaming event types updates generated code automatically
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>

src/Generators/DurableTaskSourceGenerator.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,15 @@ public static class GeneratedDurableTaskExtensions
358358
else
359359
{
360360
// ASP.NET Core-specific service registration methods
361-
AddRegistrationMethodForAllTasks(
362-
sourceBuilder,
363-
orchestrators,
364-
activities,
365-
entities);
361+
// Only generate if there are actually tasks to register
362+
if (orchestrators.Count > 0 || activities.Count > 0 || entities.Count > 0)
363+
{
364+
AddRegistrationMethodForAllTasks(
365+
sourceBuilder,
366+
orchestrators,
367+
activities,
368+
entities);
369+
}
366370
}
367371

368372
sourceBuilder.AppendLine(" }").AppendLine("}");

0 commit comments

Comments
 (0)