# Using NServiceBus Sagas with AWS Lambda, SQS, and DynamoDB This sample shows a basic saga using AWS Lambda, SQS, and DynamoDB. ## Prerequisites The sample includes a [`CloudFormation`](https://aws.amazon.com/cloudformation/aws-cloudformation-templates/) template that deploys the Lambda function and creates the necessary queues to run the code. The [`Amazon.Lambda.Tools` CLI](https://github.com/aws/aws-lambda-dotnet) can be used to deploy the template to an AWS account. 1. Install the [`Amazon.Lambda.Tools CLI`](https://github.com/aws/aws-lambda-dotnet#amazonlambdatools) using `dotnet tool install -g Amazon.Lambda.Tools` 1. Make sure an [S3 bucket](https://aws.amazon.com/s3/) is available in the [AWS region](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html#Regions) of choice ## Running the sample > [!NOTE] > It is not possible at this stage to use the AWS .NET Mock Lambda Test Tool to run the sample locally. Run the following command from the `Sales` directory to deploy the Lambda project: `dotnet lambda deploy-serverless` The deployment will ask for a stack name and an S3 bucket name to deploy the serverless stack. After that, running the sample will launch a single console window: * **ClientUI** is a console application that will send a `PlaceOrder` command to the `Samples.DynamoDB.Lambda.Sales` endpoint, which is monitored by the AWS Lambda. * The deployed **Sales** project will receive messages from the `Samples.DynamoDB.Lambda.Sales` queue and process them using the AWS Lambda runtime. To try the AWS Lambda: 1. From the **ClientUI** window, press Enter to send a `PlaceOrder` message to the trigger queue. 2. The AWS Lambda will receive the `PlaceOrder` message and will start the `OrderSaga`. 3. The `OrderSaga` will publish an `OrderReceived` event and a business SLA message `OrderDelayed`. 4. The AWS Lambda receives the `OrderReceived` event, which is handled by the `BillCustomerHandler` and the `StageInventoryHandler`. After a delay, each handler publishes an event, `CustomerBilled` and `InventoryStaged`, respectively. 5. The AWS Lambda will receive the events. Once both events are received, the `OrderSaga` publishes an `OrderShipped` event. If it takes longer than the defined business SLA to bill and stage the order, the client is notified that the order is delayed by publishing `OrderDelayed`. 6. The **ClientUI** will handle the `OrderShipped` event and log a message to the console. It might also occasionally handle the `OrderDelayed` event and hand out 10% coupon codes. ## Code walk-through The **ClientUI** console application is an Amazon SQS endpoint that sends `PlaceOrder` commands and handles the `OrderShipped` event. The **Sales** project is hosted using AWS Lambda. The static NServiceBus endpoint must be configured with details from the AWS Lambda `ILambdaContext`. Since that is not available until a message is handled by the function, the NServiceBus endpoint instance is deferred until the first message is processed, using a lambda expression such as: ```cs static readonly AwsLambdaSQSEndpoint endpoint = new AwsLambdaSQSEndpoint(context => { var endpointConfiguration = new AwsLambdaSQSEndpointConfiguration("Samples.DynamoDB.Lambda.Sales"); var advanced = endpointConfiguration.AdvancedConfiguration; advanced.UseSerialization(); advanced.SendFailedMessagesTo("Samples-DynamoDB-Lambda-Error"); var persistence = advanced.UsePersistence(); persistence.UseSharedTable(new TableConfiguration() { TableName = "Samples.DynamoDB.Lambda", }); return endpointConfiguration; }); ``` The same class defines the AWS Lambda function that hosts the NServiceBus endpoint. The `ProcessOrder` method hands off processing of the message to NServiceBus: ```cs public async Task ProcessOrder(SQSEvent eventData, ILambdaContext context) { context.Logger.Log("ProcessOrder was called"); await endpoint.Process(eventData, context, CancellationToken.None); } ``` Meanwhile, the `OrderSaga` hosted within the AWS Lambda project is a standard NServiceBus saga that can also send and receive messages. ```cs public class OrderSaga : Saga, IAmStartedByMessages, IHandleMessages, IHandleMessages, IHandleTimeouts { protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { mapper.MapSaga(sagaData => sagaData.OrderId) .ToMessage(s => s.OrderId) .ToMessage(s => s.OrderId) .ToMessage(s => s.OrderId); } public async Task Handle(PlaceOrder message, IMessageHandlerContext context) { log.Info($"Placing order: {Data.OrderId}"); await RequestTimeout(context, TimeSpan.FromSeconds(8), new OrderDelayed { OrderId = message.OrderId }); await context.Publish(new OrderReceived { OrderId = message.OrderId }); } public async Task Handle(CustomerBilled message, IMessageHandlerContext context) { log.Info($"The customer for order {Data.OrderId} has been billed."); Data.CustomerBilled = true; await ShipItIfPossible(context); } public async Task Handle(InventoryStaged message, IMessageHandlerContext context) { log.Info($"The inventory for order {Data.OrderId} has been staged."); Data.InventoryStaged = true; await ShipItIfPossible(context); } public async Task Timeout(OrderDelayed state, IMessageHandlerContext context) { log.Info($"Order {Data.OrderId} is slightly delayed."); await context.Publish(state); } async Task ShipItIfPossible(IMessageHandlerContext context) { if (Data is { CustomerBilled: true, InventoryStaged: true }) { log.Info($"Order {Data.OrderId} has been shipped."); // Send duplicate message for outbox test. await context.Publish(new OrderShipped { OrderId = Data.OrderId }); MarkAsComplete(); } } static ILog log = LogManager.GetLogger(typeof(OrderSagaData)); } ``` The saga data is stored in the `Samples.DynamoDB.Lambda` table and can be viewed in the AWS web portal: ![Saga data in the AWS web portal](saga-data.png) ## Removing the sample stack Remove the deployed stack with the following command: `dotnet lambda delete-serverless` and provide the previously chosen stack name.