Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
Expand Down
121 changes: 121 additions & 0 deletions samples/AzureFunctionsApp/ApprovalOrchestrator.cs
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");
}
}
2 changes: 2 additions & 0 deletions samples/AzureFunctionsApp/AzureFunctionsApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
<PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" OutputItemType="Analyzer" />
<!-- Reference the source generator project directly for local development -->
<ProjectReference Include="..\..\src\Generators\Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<!-- Reference Abstractions for latest DurableEventAttribute -->
<ProjectReference Include="..\..\src\Abstractions\Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions samples/EventsSample/Events.cs
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);
24 changes: 24 additions & 0 deletions samples/EventsSample/EventsSample.csproj
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>
85 changes: 85 additions & 0 deletions samples/EventsSample/Program.cs
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();
Loading
Loading