Skip to content

C# SDK

The .NET SDK (Amazon.Lambda.DurableExecution) runs in your Lambda functions and provides IDurableContext, the durable operations, replay-aware logging, and pluggable serialization. The durable operations and execution model match the other SDKs. What differs is the host surface: your handler receives the service envelope, you call DurableFunction.WrapAsync yourself, and every operation body takes a CancellationToken.

Installation

Add the package to your Lambda function project:

dotnet add package Amazon.Lambda.DurableExecution

Usage

Your Lambda handler receives a DurableExecutionInvocationInput and returns a DurableExecutionInvocationOutput. These are the service envelope: the durable execution service sends the first, and reads the second to learn whether the workflow succeeded, failed, or suspended. Delegate to DurableFunction.WrapAsync<TInput, TOutput>, which unpacks your input from the envelope, runs your workflow with an IDurableContext, and packs the result back into the envelope.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;

namespace OrderProcessor;

public class OrderProcessor
{
    public static async Task Main()
    {
        var handler = new OrderProcessor();
        var serializer = new DefaultLambdaJsonSerializer();
        using var wrapper = HandlerWrapper.GetHandlerWrapper<DurableExecutionInvocationInput, DurableExecutionInvocationOutput>(
            handler.Handler, serializer);
        using var bootstrap = new LambdaBootstrap(wrapper);
        await bootstrap.RunAsync();
    }

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

    private async Task<OrderResult> Workflow(Order order, IDurableContext ctx)
    {
        var reservation = await ctx.StepAsync(
            async (_, ct) => await InventoryService.ReserveAsync(order.Items, ct),
            name: "reserve-inventory");

        await ctx.WaitAsync(TimeSpan.FromHours(2), name: "warehouse-processing");

        var shipment = await ctx.StepAsync(
            async (_, ct) => await ShippingService.ShipAsync(reservation, order.Address, ct),
            name: "confirm-shipment");

        return new OrderResult(order.Id, shipment.TrackingNumber);
    }
}

WrapAsync has a void overload, WrapAsync<TInput>(Func<TInput, IDurableContext, Task>, ...), for workflows that return nothing, and overloads that accept an explicit IAmazonLambda client when you need to customize the client used for the durable-execution service calls (checkpointing and state hydration).

Operation names are optional. When you omit name, the SDK infers one from the call site and derives the deterministic operation ID from it. Pass stable, descriptive names so replay matches new execution to checkpointed state and so operations read clearly in logs and traces.

Programming models

The managed .NET runtime supports three ways to host a durable function.

The executable model shown above owns its entry point: Main builds a LambdaBootstrap and calls RunAsync().

The class-library model drops the Main loop. Declare the serializer with an assembly attribute and deploy with an Assembly::Namespace.Type::Method handler string:

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 the executable model
    }
}

Lambda Annotations removes the boilerplate. Add Amazon.Lambda.Annotations and annotate your workflow with [LambdaFunction] and [DurableExecution]. The source generator emits the WrapAsync wrapper and writes the DurableConfig block and checkpoint-API IAM permissions into the generated serverless.template.

using Amazon.Lambda.Annotations;
using Amazon.Lambda.DurableExecution;

public class OrderProcessor
{
    [LambdaFunction]
    [DurableExecution(300, RetentionPeriodInDays = 7)]
    public async Task<OrderResult> Workflow(Order order, IDurableContext ctx)
    {
        var reservation = await ctx.StepAsync(
            async (_, ct) => await InventoryService.ReserveAsync(order.Items, ct),
            name: "reserve-inventory");
        // remaining steps
        return new OrderResult(order.Id, reservation.TrackingNumber);
    }
}

Set [assembly: LambdaGlobalProperties(GenerateMain = true)] for the executable model, or omit it for a class library. The generated serverless.template handler adapts to each. The generator validates the (TInput, IDurableContext) -> Task or Task<TOutput> signature and Zip packaging, and reports a diagnostic otherwise.

Serialization

WrapAsync reads the ILambdaSerializer off ILambdaContext.Serializer and uses that single serializer for every payload: your workflow input and output, and each step, child context, callback, invoke, and condition-check result that gets checkpointed. You register the serializer once, at the host boundary. In the executable model you pass it to HandlerWrapper.GetHandlerWrapper; in the class-library model you declare it with [assembly: LambdaSerializer(...)].

For reflection-based serialization, use DefaultLambdaJsonSerializer. For trimmed or Native AOT functions, register SourceGeneratorLambdaJsonSerializer<TContext> with your JsonSerializerContext. Every operation uses the serializer you register at the host boundary.

Cancellation

Every user function that IDurableContext accepts, StepAsync, RunInChildContextAsync, WaitForCallbackAsync, WaitForConditionAsync, and the branch and item functions of ParallelAsync and MapAsync, receives a CancellationToken. The token is a linked source that combines the token you passed to the operation with an SDK-owned workflow-shutdown signal. The shutdown signal fires when the workflow is being torn down, for example when a sibling operation suspends or a checkpoint fails.

Pass the token to cancellation-aware APIs inside the body so the body unwinds cleanly when either trigger fires:

var user = await ctx.StepAsync(
    async (_, ct) => await httpClient.GetAsync(url, ct),
    name: "fetch");

When the token fires and an OperationCanceledException propagates out of the body, the SDK treats it as cancellation: it writes no failure checkpoint and consults no retry strategy. An OperationCanceledException thrown for unrelated reasons, when the token never fired, is a normal step failure that checkpoints and retries per the configured RetryStrategy. The SDK's own writes, checkpoints and the runtime API response, never observe the shutdown signal, so completed work is never lost to teardown.

Do not branch workflow logic on IsCancellationRequested. Cancellation is a runtime concern, not a workflow concern, and branching on it makes replay non-deterministic. Do not catch OperationCanceledException and continue. Either let it propagate, or catch and rethrow.

Logging

IDurableContext.Logger is a replay-safe Microsoft.Extensions.Logging.ILogger. It suppresses messages emitted while the workflow re-derives prior operations from checkpointed state, so a 30-step workflow re-invoked 30 times still emits each line once. Use it instead of Console.WriteLine, which repeats on every replay. Call ConfigureLogger(LoggerConfig) to swap the underlying logger (Serilog, AWS Lambda Powertools) or set ModeAware = false to see every line on every replay while debugging.

Source and reference

The source is in aws/aws-lambda-dotnet.