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
18 changes: 12 additions & 6 deletions Libraries/src/Amazon.Lambda.DurableExecution/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,13 @@ set `TestLambdaContext.Serializer` so `LambdaSerializerHelper.GetRequired` finds
### Integration tests (expensive, real AWS)

`Libraries/test/Amazon.Lambda.DurableExecution.IntegrationTests` deploys real Lambdas. Each test builds a
`TestFunctions/<X>/` project into a container image via **`dotnet publish` + `docker build`**, pushes to ECR,
creates an IAM role + Lambda (`DurableFunctionDeployment`), invokes it, and tears everything down on dispose.
Requires Docker, AWS creds (us-east-1), and is slow. Every behavior in `docs/` should have a paired
integration test under that project. Prefix AWS commands with `unset AWS_PROFILE` to use `[default]` creds.
`TestFunctions/<X>/` project with **`dotnet publish` (framework-dependent, linux-x64)**, zips the publish
output, and deploys it as a **zip package on the managed `dotnet10` runtime** (executable model,
`Handler=bootstrap`) — no Docker or ECR. `DurableFunctionDeployment` creates an IAM role + Lambda (with
`DurableConfig` and JSON `LoggingConfig`), invokes it, and tears everything down on dispose.
Requires only the .NET SDK + AWS creds (us-east-1); no Docker. Slow, but no container build. Every behavior
in `docs/` should have a paired integration test under that project. Prefix AWS commands with
`unset AWS_PROFILE` to use `[default]` creds.

**Run integration tests against `net10.0`.** The project multi-targets `net8.0;net10.0`; `dotnet test`
without a framework spins up one testhost per TFW and runs them concurrently, which races two processes on
Expand Down Expand Up @@ -134,8 +137,11 @@ intentionally avoid collision with `AWSSDK.Lambda` model enums.

## Conventions

- **Programming model:** preview supports only the *executable* model — `Main` builds a `LambdaBootstrap`
with a handler wrapper and an `ILambdaSerializer`. The serializer is read off `ILambdaContext.Serializer`
- **Programming model:** both the *executable* model (`Main` builds a `LambdaBootstrap` with a handler
wrapper and an `ILambdaSerializer`) and the *class-library* model on the managed `dotnet10` runtime (a
plain `Handler` method, serializer via `[assembly: LambdaSerializer(...)]`, deployed with an
`Assembly::Type::Method` handler string) are supported and have integration coverage (`ClassLibraryTest`
+ `TestFunctions/ClassLibraryFunction`). The serializer is read off `ILambdaContext.Serializer`
(a preview API; the project-wide `AWSLAMBDA001` suppression in the `.csproj` is intentional for that
reason). All step/result/payload (de)serialization flows through that one registered serializer, so AOT
and reflection callers share a single code path — there is no per-call `JsonSerializerContext` argument.
Expand Down
30 changes: 29 additions & 1 deletion Libraries/src/Amazon.Lambda.DurableExecution/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dotnet add package Amazon.Lambda.DurableExecution

### Your first durable function

> **Programming model:** the preview only supports the **executable programming model** — your function is an executable assembly that hosts its own bootstrap loop and passes the serializer to the runtime in code. Class-library handlers on the managed runtime will be supported once the changes made to Amazon.Lambda.RuntimeSupport to support durable functions has been deployed to the managed runtime. This README will be updated then.
> **Programming model:** durable functions support both the **executable programming model** (shown below) and the **class-library programming model** on the managed `dotnet10` runtime. See [the class-library variant](#class-library-programming-model) below.

@GarrettBeatty GarrettBeatty Jun 22, 2026

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.

runtime support changes for #2378 is deployed in all regions for people making new lambda functions

with using managed runtime - its deployed to some regions but not all.

i was thinking we can update docs to say its available and then in the github issue just mention its still rolling out to some regions. i dont want to wait to update the docs is why


A complete order-processing workflow with two steps and a wait, deployed as an executable assembly on the `dotnet10` runtime. `Main` builds a `LambdaBootstrap` with your handler and an `ILambdaSerializer`, and `DurableFunction.WrapAsync` uses that serializer to checkpoint step inputs and outputs.

Expand Down Expand Up @@ -89,6 +89,34 @@ public record OrderResult(string OrderId, string TrackingNumber);

For AOT or trim-friendly serialization, swap `DefaultLambdaJsonSerializer` for `SourceGeneratorLambdaJsonSerializer<TContext>` and register your `JsonSerializerContext`.

### Class-library programming model

On the managed `dotnet10` runtime you can skip the `Main`/`LambdaBootstrap` loop entirely and deploy a plain class-library handler — the same model used by non-durable Lambda functions. Declare the serializer with an assembly attribute and deploy with the `Assembly::Namespace.Type::Method` handler string (e.g. `OrderProcessor::OrderProcessor.OrderProcessor::Handler`):

```csharp
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.Serialization.SystemTextJson;

[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]

namespace OrderProcessor;

public class OrderProcessor
{
public Task<DurableExecutionInvocationOutput> Handler(
DurableExecutionInvocationInput input, ILambdaContext context)
=> DurableFunction.WrapAsync<Order, OrderResult>(Workflow, input, context);

private async Task<OrderResult> Workflow(Order order, IDurableContext ctx)
{
// ...same workflow body as above...
}
}
```

The project is a normal Lambda class library; the managed runtime supplies the bootstrap loop and invokes `Handler` directly.

## Documentation

**Core operations**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@

<ItemGroup>
<PackageReference Include="AWSSDK.IdentityManagement" Version="4.0.9.22" />
<PackageReference Include="AWSSDK.ECR" Version="4.0.7" />
<PackageReference Include="AWSSDK.Lambda" Version="4.0.13.1" />
<PackageReference Include="AWSSDK.SecurityToken" Version="4.0.6.3" />
<PackageReference Include="AWSSDK.CloudWatchLogs" Version="4.0.20" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Linq;
using System.Text;
using Amazon.Lambda.Model;
using Xunit;
using Xunit.Abstractions;

namespace Amazon.Lambda.DurableExecution.IntegrationTests;

/// <summary>
/// Proves a durable function works on the managed <c>dotnet10</c> runtime using the
/// <b>class-library programming model</b> — a plain <c>Handler</c> method with no
/// <c>Main</c>/<c>LambdaBootstrap</c> loop, deployed via an <c>Assembly::Type::Method</c>
/// handler string. Confirms the RuntimeSupport durable-execution changes are live in
/// the managed runtime, so the executable model is no longer required.
/// </summary>
public class ClassLibraryTest
{
private readonly ITestOutputHelper _output;
public ClassLibraryTest(ITestOutputHelper output) => _output = output;

[Fact]
public async Task ClassLibrary_TwoSteps_Checkpointed()
{
await using var deployment = await DurableFunctionDeployment.CreateAsync(
DurableFunctionDeployment.FindTestFunctionDir("ClassLibraryFunction"),
"classlib", _output,
handler: "ClassLibraryFunction::ClassLibraryFunction.Function::Handler");

var (invokeResponse, executionName) = await deployment.InvokeAsync("""{"orderId": "chain"}""");
Assert.Equal(200, invokeResponse.StatusCode);

var responsePayload = Encoding.UTF8.GetString(invokeResponse.Payload.ToArray());
_output.WriteLine($"Response: {responsePayload}");

var arn = await deployment.FindDurableExecutionArnByNameAsync(executionName, TimeSpan.FromSeconds(60));
Assert.NotNull(arn);

var status = await deployment.PollForCompletionAsync(arn!, TimeSpan.FromSeconds(60));
Assert.Equal("SUCCEEDED", status, ignoreCase: true);

// History is eventually consistent — wait until both steps are indexed.
var history = await deployment.WaitForHistoryAsync(
arn!,
h => (h.Events?.Count(e => e.EventType == EventType.StepStarted) ?? 0) >= 2
&& (h.Events?.Count(e => e.StepSucceededDetails != null) ?? 0) >= 2,
TimeSpan.FromSeconds(60));
var events = history.Events ?? new List<Event>();

// Both steps ran exactly once, in declaration order, each chaining from the
// previous one's output — same checkpointing behavior as the executable model.
Assert.Equal(2, events.Count(e => e.EventType == EventType.StepStarted));

var stepResults = events
.Where(e => e.StepSucceededDetails != null)
.Select(e => $"{e.Name}={e.StepSucceededDetails.Result?.Payload?.Trim('"')}")
.ToList();
Assert.Equal(
new[]
{
"step_1=a-chain",
"step_2=a-chain-b",
},
stepResults);
}
}
Loading