diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/blueprint-manifest.json b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/blueprint-manifest.json new file mode 100644 index 000000000..61c4ee179 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/blueprint-manifest.json @@ -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" + ] +} diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/.template.config/template.json b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/.template.config/template.json new file mode 100644 index 000000000..7a656edaa --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/.template.config/template.json @@ -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" + } + ] +} diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/BlueprintBaseName.1.csproj b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/BlueprintBaseName.1.csproj new file mode 100644 index 000000000..1b3199030 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/BlueprintBaseName.1.csproj @@ -0,0 +1,26 @@ + + + + Library + net10.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Function.cs b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Function.cs new file mode 100644 index 000000000..ef5829193 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Function.cs @@ -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; + +/// +/// 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. +/// +public class Function +{ + /// + /// The durable workflow entry point. The method signature is + /// (TInput, IDurableContext) -> Task<TOutput>; 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. + /// + [LambdaFunction] + [DurableExecution] + public async Task 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, + }; + } +} + +/// Input payload for the workflow. +public class OrderRequest +{ + public string? OrderId { get; set; } + public string[]? Items { get; set; } +} + +/// Output payload returned when the workflow completes. +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; } +} diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Readme.md b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Readme.md new file mode 100644 index 000000000..632b2de1a --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/Readme.md @@ -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 +``` diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/aws-lambda-tools-defaults.json b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..153ca2264 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/aws-lambda-tools-defaults.json @@ -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": "" +} diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/serverless.template b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/serverless.template new file mode 100644 index 000000000..6f4319827 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/src/BlueprintBaseName.1/serverless.template @@ -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": {} + } + } + } +} diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/BlueprintBaseName.1.Tests.csproj b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/BlueprintBaseName.1.Tests.csproj new file mode 100644 index 000000000..1c2ed1bdc --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/BlueprintBaseName.1.Tests.csproj @@ -0,0 +1,19 @@ + + + net10.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/FunctionTest.cs b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/FunctionTest.cs new file mode 100644 index 000000000..ac09b1b27 --- /dev/null +++ b/Blueprints/BlueprintDefinitions/vs2026/DurableFunction/template/test/BlueprintBaseName.1.Tests/FunctionTest.cs @@ -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( + 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( + handler: function.ProcessOrder, + options: new TestRunnerOptions { SkipTime = true }); + + var input = new OrderRequest { OrderId = "order-456", Items = System.Array.Empty() }; + + var result = await runner.RunAsync(input, cancellationToken: TestContext.Current.CancellationToken); + + Assert.True(result.IsFailed); + } +} diff --git a/Blueprints/README.md b/Blueprints/README.md index f5dd6fda7..aafaa77ba 100644 --- a/Blueprints/README.md +++ b/Blueprints/README.md @@ -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