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
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,
"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"]}'
```

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,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,
"hidden-tags": [
"C#",
"ServerlessProject"
],
"tags": [
"Durable"
]
}
Loading