Skip to content
Draft
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
365 changes: 355 additions & 10 deletions Microsoft.DurableTask.sln

Large diffs are not rendered by default.

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/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>
75 changes: 75 additions & 0 deletions samples/EventsSample/Program.cs
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();
83 changes: 83 additions & 0 deletions samples/EventsSample/README.md
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
77 changes: 77 additions & 0 deletions samples/EventsSample/Tasks.cs
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"}";
}
Comment on lines +23 to +30
}
}

/// <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()}");
}
}
34 changes: 34 additions & 0 deletions src/Abstractions/DurableEventAttribute.cs
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.
/// </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; }
}
Loading
Loading