Skip to content
Open
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
11 changes: 11 additions & 0 deletions .autover/changes/durable-execution-blueprints.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Templates",
"Type": "Minor",
"ChangelogMessages": [
"Add lambda.DurableFunction and serverless.DurableFunction blueprints (vs2026) for Lambda durable execution workflows. lambda.DurableFunction uses the class-library static-wrapper model (DurableFunction.WrapAsync) and deploys via dotnet lambda deploy-function; serverless.DurableFunction uses the annotations model ([LambdaFunction] + [DurableExecution]) and deploys via CloudFormation (serverless.template). Both target the managed dotnet10 runtime and ship a sample ProcessOrder workflow plus a local test project driven by Amazon.Lambda.DurableExecution.Testing. Preview."
]
}
]
}
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. Deploys straight to Lambda with the 'dotnet lambda deploy-function' command.",
"sort-order": 130,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This controls the order the blueprint shows up in the VS blueprint dialog

Image

130 would basically put this at the bottom where users would have to scroll. Looking at the other sort orders if you set this to 124 it would be next to the PowerTools template and before all of the "Simple" templates.

"hidden-tags": [
"C#",
"LambdaProject"
],
"tags": [
"Durable"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentionally only added to vs2026. didnt feel like it was worth adding to 2024 folder since dotnet8 will be end of life soon

"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,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--
Class-library programming model: there is NO OutputType=Exe and NO hand-written Main.
The handler delegates to DurableFunction.WrapAsync (the static wrapper model), and the managed
dotnet10 runtime hosts its own bootstrap and invokes the handler 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.DurableExecution" Version="0.1.1-preview" />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will update to ga version once released

</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.Serialization.SystemTextJson;
using Microsoft.Extensions.Logging;

// The durable runtime reads this serializer off ILambdaContext.Serializer to (de)serialize the
// invocation envelope and every checkpointed step input/output.
[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

namespace BlueprintBaseName._1;

public class Function
{
/// <summary>
/// The Lambda entry point. The managed runtime invokes this method directly (via the
/// <c>Assembly::Type::Method</c> handler string in aws-lambda-tools-defaults.json) and
/// <see cref="DurableFunction.WrapAsync"/> bridges the durable invocation envelope to the
/// strongly-typed <see cref="ProcessOrder"/> workflow below.
/// </summary>
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<OrderRequest, OrderResult>(ProcessOrder, input, context);

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,88 @@
# Durable Lambda Function

This project contains a Lambda **durable execution** workflow built with the
[Amazon.Lambda.DurableExecution](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.DurableExecution)
**static wrapper** programming model, deployed straight to Lambda with `dotnet lambda deploy-function`.

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.

> Looking for the CloudFormation/Annotations variant? Use the **`serverless.DurableFunction`**
> template, which uses `[DurableExecution]` + a `serverless.template` and deploys with
> `dotnet lambda deploy-serverless`.

## How it works

`Function.Handler` is the Lambda entry point. It delegates to `DurableFunction.WrapAsync`, which
bridges the durable invocation envelope to the strongly-typed `ProcessOrder` workflow:

```csharp
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<OrderRequest, OrderResult>(ProcessOrder, input, context);
```

This is the **class-library** hosting model on the managed `dotnet10` runtime: there is no
`Main`/`LambdaBootstrap` loop and no `[DurableExecution]` annotation. The runtime hosts the
bootstrap and invokes `Handler` directly via the `Assembly::Type::Method` handler string in
`aws-lambda-tools-defaults.json`. The serializer is declared with an assembly attribute:

```csharp
[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
```

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 operations into a single logical operation. |

> **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

`aws-lambda-tools-defaults.json` sets the runtime (`dotnet10`), handler, and the durable execution
timeout (`durable-execution-timeout`). Deploy the function directly to Lambda with:

```bash
dotnet lambda deploy-function
```

When the tool creates the function's execution role for you, it automatically attaches the
`AWSLambdaBasicDurableExecutionRolePolicy` managed policy, which grants the durable-execution
checkpoint permissions the function needs at runtime. If you supply your own role
(`--function-role`), make sure that policy is attached to it.

## Invoke

Durable functions are invoked with a qualified function reference and a durable execution name:

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the --invoke-model switch for Durable functions?

```

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have this section since the test project will only exist if the create the project via Visual Studio. You could have a more general section about how to test and and pointing out the Amazon.Lambda.DurableExecution.Testing project. Just don't say there is a Test project.


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be careful calling it a runner or make it more clear that it isn't a test runner but a durable functions runner. That sounds like this is our own flavor of xUnit or mstest which are test runners.


```bash
dotnet test
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"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",
"function-runtime": "dotnet10",
"function-memory-size": 512,
"function-timeout": 30,
"function-handler": "BlueprintBaseName.1::BlueprintBaseName._1.Function::Handler",
"durable-execution-timeout": 86400
}
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" />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this pr isnt merged yet, i tested with local nuget feed.


  # 1. Pack the three required packages into a local feed
  FEED=/tmp/durable-feed && mkdir -p "$FEED"
  cd Libraries/src/Amazon.Lambda.Annotations            && dotnet pack -c Release -o "$FEED"
  cd ../Amazon.Lambda.DurableExecution                  && dotnet pack -c Release -o "$FEED"
  cd ../Amazon.Lambda.DurableExecution.Testing          && dotnet pack -c Release -o "$FEED"

  # 2. Install the template from the blueprint folder
  cd <repo>/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1
  dotnet new install .    # or install from the .template.config location

  # 3. Instantiate into a scratch dir
  mkdir /tmp/dtest && cd /tmp/dtest
  dotnet new lambda.DurableFunction --name MyOrders

  # 4. Build + test against the local feed
  dotnet test --source "$FEED" --source https://api.nuget.org/v3/index.json

<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);
}
}
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest setting sort-order to 120 to put in with the Annotations template in the VS wizard.

Image

"hidden-tags": [
"C#",
"ServerlessProject"
],
"tags": [
"Durable"
]
}
Loading