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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"display-name": "Durable Function",
"system-name": "DurableFunction",
"description": "A durable execution workflow that checkpoints every step, so it can be suspended during waits and resumed after a crash without re-running completed work.",
"sort-order": 130,
"hidden-tags": [
"C#",
"ServerlessProject"
],
"tags": [
"Durable"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"author": "AWS",
"classifications": [
"AWS",
"Lambda",
"Serverless"
],
"name": "Lambda Durable Function",
"identity": "AWS.Lambda.Function.Durable.CSharp",
"groupIdentity": "AWS.Lambda.Function.Durable",
"shortName": "lambda.DurableFunction",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "BlueprintBaseName.1",
"preferNameDirectory": true,
"symbols": {
"profile": {
"type": "parameter",
"description": "The AWS credentials profile set in aws-lambda-tools-defaults.json and used as the default profile when interacting with AWS.",
"datatype": "string",
"replaces": "DefaultProfile",
"defaultValue": ""
},
"region": {
"type": "parameter",
"description": "The AWS region set in aws-lambda-tools-defaults.json and used as the default region when interacting with AWS.",
"datatype": "string",
"replaces": "DefaultRegion",
"defaultValue": ""
}
},
"primaryOutputs": [
{
"path": "./BlueprintBaseName.1.csproj"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--
Class-library programming model: there is NO OutputType=Exe and NO hand-written Main.
The Amazon.Lambda.Annotations source generator emits the durable handler wrapper, and the
managed dotnet10 runtime hosts its own bootstrap and invokes that wrapper via an
Assembly::Type::Method handler string. Durable execution requires the managed dotnet10 runtime.
-->
<OutputType>Library</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<AWSProjectType>Lambda</AWSProjectType>
<!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- Generate ready to run images during publishing to improvement cold starts. -->
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="3.1.1" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="3.0.0" />
<PackageReference Include="Amazon.Lambda.Annotations" Version="2.0.1" />
<PackageReference Include="Amazon.Lambda.DurableExecution" Version="0.1.1-preview" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

// Durable execution uses the Lambda Annotations programming model in the CLASS-LIBRARY variant:
// there is no hand-written Main. The Amazon.Lambda.Annotations source generator turns the
// [LambdaFunction] + [DurableExecution] method below into a handler wrapper that delegates to
// Amazon.Lambda.DurableExecution.DurableFunction.WrapAsync. The managed dotnet10 runtime hosts its
// own bootstrap, resolves the serializer from the assembly attribute here, and invokes the
// generated wrapper directly.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace BlueprintBaseName._1;

/// <summary>
/// A durable order-processing workflow. A single invocation reads like one straight-line method,
/// but durable execution checkpoints every operation, so the function can be suspended (during the
/// settlement wait) and re-invoked without re-running completed work. If the process crashes
/// mid-flight it resumes from the last checkpoint — no lost orders, no double charges.
/// </summary>
public class Function
{
/// <summary>
/// The durable workflow entry point. The method signature is
/// <c>(TInput, IDurableContext) -&gt; Task&lt;TOutput&gt;</c>; the source generator wires it to the
/// durable runtime and emits the matching CloudFormation resource (with DurableConfig and the
/// durable IAM policy) in serverless.template.
/// </summary>
[LambdaFunction]
[DurableExecution]
public async Task<OrderResult> ProcessOrder(OrderRequest order, IDurableContext context)
{
// The durable logger is replay-aware: this line is emitted once, not once per replay.
context.Logger.LogInformation("Processing order {OrderId}", order.OrderId);

// 1) VALIDATE — a plain step. The result is checkpointed; on replay the cached value is
// returned instead of re-running the body.
var itemCount = await context.StepAsync(
async (_, _) =>
{
await Task.CompletedTask;
if (order.Items is null || order.Items.Length == 0)
throw new InvalidOperationException("Order has no items.");
return order.Items.Length;
},
name: "validate_order");

// 2) CHARGE PAYMENT — a step with a retry policy. Payment gateways are flaky, so the SDK
// transparently retries with exponential backoff and checkpoints only the successful
// attempt. AtMostOncePerRetry avoids double-charging if Lambda is re-invoked mid-attempt.
var transactionId = await context.StepAsync(
async (_, _) =>
{
await Task.CompletedTask;
return $"txn-{order.OrderId}";
},
name: "charge_payment",
config: new StepConfig
{
RetryStrategy = RetryStrategy.Exponential(
maxAttempts: 5,
initialDelay: TimeSpan.FromSeconds(2),
maxDelay: TimeSpan.FromSeconds(30),
backoffRate: 2.0),
Semantics = StepSemantics.AtMostOncePerRetry,
});

// 3) SETTLEMENT WAIT — suspend the workflow for a fixed delay. While suspended there is no
// compute charge; the runtime re-invokes the function when the timer fires.
await context.WaitAsync(TimeSpan.FromSeconds(5), name: "settlement_delay");

// 4) SHIP — group related steps into a single child context. The packing and labeling steps
// are checkpointed together as one logical operation.
var trackingId = await context.RunInChildContextAsync(
async (childContext, _) =>
{
await childContext.StepAsync(
async (_, _) => { await Task.CompletedTask; return "packed"; },
name: "pack");

return await childContext.StepAsync(
async (_, _) => { await Task.CompletedTask; return $"trk-{order.OrderId}"; },
name: "label");
},
name: "ship_order");

context.Logger.LogInformation("Order {OrderId} shipped: {TrackingId}", order.OrderId, trackingId);

return new OrderResult
{
OrderId = order.OrderId,
Status = "shipped",
ItemCount = itemCount,
TransactionId = transactionId,
TrackingId = trackingId,
};
}
}

/// <summary>Input payload for the workflow.</summary>
public class OrderRequest
{
public string? OrderId { get; set; }
public string[]? Items { get; set; }
}

/// <summary>Output payload returned when the workflow completes.</summary>
public class OrderResult
{
public string? OrderId { get; set; }
public string? Status { get; set; }
public int ItemCount { get; set; }
public string? TransactionId { get; set; }
public string? TrackingId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Durable Lambda Function

This project contains a Lambda **durable execution** workflow built with the
[Lambda Annotations](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Annotations)
programming model.

Durable execution lets you write a multi-step workflow as a single straight-line method. The
runtime checkpoints every operation, so the function can be **suspended** during waits and
**resumed after a crash** without re-running completed work.

## How it works

`Function.ProcessOrder` is the workflow entry point. It is marked with two attributes:

* `[LambdaFunction]` — registers the method with the Annotations source generator.
* `[DurableExecution]` — tells the generator to wrap the method with the durable runtime and to add
the durable configuration and IAM policy to `serverless.template`.

The workflow uses the core durable primitives on `IDurableContext`:

| Primitive | Used for |
|-----------|----------|
| `StepAsync` | A checkpointed unit of work. On replay the cached result is returned instead of re-running the body. |
| `StepAsync` + `StepConfig.RetryStrategy` | Retry a flaky step with exponential backoff; only the successful attempt is checkpointed. |
| `StepSemantics.AtMostOncePerRetry` | Avoid re-running a side-effecting step (e.g. charging a card) if Lambda is re-invoked mid-attempt. |
| `WaitAsync` | Suspend the workflow for a delay. There is no compute charge while suspended. |
| `RunInChildContextAsync` | Group related steps into a single logical operation. |

The class-library model is used (no `Main`): the managed `dotnet10` runtime hosts the bootstrap and
invokes the generated handler wrapper directly.

> **Note:** Durable execution requires the managed **`dotnet10`** runtime.

## Requirements

* [.NET 10 SDK](https://dotnet.microsoft.com/download)
* [Amazon.Lambda.Tools](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools)

```bash
dotnet tool install -g Amazon.Lambda.Tools
```

## Deploy

The `[DurableExecution]` attribute drives the source generator, which keeps the function resource in
`serverless.template` in sync (runtime, handler, `DurableConfig`, and the
`AWSLambdaBasicDurableExecutionRolePolicy` managed policy). Deploy the serverless application with:

```bash
dotnet lambda deploy-serverless
```

## Invoke

After deploying, invoke the function with a sample order payload:

```bash
dotnet lambda invoke-function BlueprintBaseName.1 --payload '{"OrderId":"order-123","Items":["sku-1","sku-2"]}'
```

The workflow validates the order, charges payment, waits out a short settlement period, ships the
order in a child context, and returns the result.

## Test

The included test project drives the workflow locally with the
`Amazon.Lambda.DurableExecution.Testing` runner — no AWS resources required:

```bash
dotnet test
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"Information": [
"This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
"To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",
"dotnet lambda help",
"All the command line options for the Lambda command can be specified in this file."
],
"profile": "DefaultProfile",
"region": "DefaultRegion",
"configuration": "Release",
"s3-prefix": "BlueprintBaseName.1/",
"template": "serverless.template",
"template-parameters": ""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Description": "An AWS Serverless Application with a durable execution function. This template is partially managed by Amazon.Lambda.Annotations.",
"Resources": {
"BlueprintBaseName1FunctionProcessOrderGenerated": {
"Type": "AWS::Serverless::Function",
"Metadata": {
"Tool": "Amazon.Lambda.Annotations",
"SyncedDurableConfig": true,
"SyncedDurablePolicy": true
},
"Properties": {
"Runtime": "dotnet10",
"CodeUri": ".",
"MemorySize": 512,
"Timeout": 30,
"Policies": [
"AWSLambdaBasicExecutionRole",
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy"
],
"PackageType": "Zip",
"Handler": "BlueprintBaseName.1::BlueprintBaseName._1.Function_ProcessOrder_Generated::ProcessOrder",
"DurableConfig": {}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="3.1.1" />
<PackageReference Include="Amazon.Lambda.TestUtilities" Version="4.1.0" />
<PackageReference Include="Amazon.Lambda.DurableExecution.Testing" Version="0.0.1-preview" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\BlueprintBaseName.1\BlueprintBaseName.1.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Amazon.Lambda.DurableExecution.Testing;
using Xunit;

namespace BlueprintBaseName._1.Tests;

public class FunctionTest
{
[Fact]
public async Task ProcessOrder_ShipsOrder()
{
var function = new Function();

// The local runner drives the workflow to completion in-process using the real durable
// runtime with an in-memory backend. SkipTime collapses the settlement WaitAsync delay so
// the test does not actually block for 5 seconds.
await using var runner = new DurableTestRunner<OrderRequest, OrderResult>(
handler: function.ProcessOrder,
options: new TestRunnerOptions { SkipTime = true });

var input = new OrderRequest
{
OrderId = "order-123",
Items = new[] { "sku-1", "sku-2" },
};

var result = await runner.RunAsync(input, cancellationToken: TestContext.Current.CancellationToken);

result.EnsureSucceeded();
Assert.NotNull(result.Result);
Assert.Equal("order-123", result.Result!.OrderId);
Assert.Equal("shipped", result.Result.Status);
Assert.Equal(2, result.Result.ItemCount);
Assert.Equal("txn-order-123", result.Result.TransactionId);
Assert.Equal("trk-order-123", result.Result.TrackingId);

// Each named operation is checkpointed and inspectable.
Assert.Equal(OperationStatus.Succeeded, result.GetStep("validate_order").Status);
Assert.Equal(OperationStatus.Succeeded, result.GetStep("charge_payment").Status);
}

[Fact]
public async Task ProcessOrder_EmptyOrder_Fails()
{
var function = new Function();

await using var runner = new DurableTestRunner<OrderRequest, OrderResult>(
handler: function.ProcessOrder,
options: new TestRunnerOptions { SkipTime = true });

var input = new OrderRequest { OrderId = "order-456", Items = System.Array.Empty<string>() };

var result = await runner.RunAsync(input, cancellationToken: TestContext.Current.CancellationToken);

Assert.True(result.IsFailed);
}
}
1 change: 1 addition & 0 deletions Blueprints/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Lambda Simple S3 Function lambda.S3 [C#]
Lambda ASP.NET Core Web API lambda.AspNetCoreWebAPI [C#] AWS/Lambda/Serverless
Lambda DynamoDB Blog API lambda.DynamoDBBlogAPI [C#] AWS/Lambda/Serverless
Lambda Empty Serverless lambda.EmptyServerless [C#] AWS/Lambda/Serverless
Lambda Durable Function lambda.DurableFunction [C#] AWS/Lambda/Serverless
Console Application console [C#], F# Common/Console
Class library classlib [C#], F# Common/Library
Unit Test Project mstest [C#], F# Test/MSTest
Expand Down
Loading