How To Easily Make Your .NET AWS Lambda Function Idempotent
Tags: dotnet, aws, lambda, dotnetcore
Published: 2024-02-02 00:45:31

Idempotency refers to the property of a function that produces the same result regardless of how many times it is executed with the same input parameters.

Idempotency is particularly important in distributed systems, where messages may be retried due to failures, timeouts, or other issues. Idempotency ensures the system remains consistent without unintended side effects, even if a request is duplicated or repeated.

{% embed https://youtu.be/v-A8iP0hSLQ %}

The [Powertools Idempotency utility](https://docs.powertools.aws.dev/lambda/dotnet/utilities/idempotency/) makes it easy to convert your Lambda functions into idempotent operations that are safe to retry.

In this blog post, let's learn how to get started using the Powertools Idempotency package, some key features, and how it easily fits into your existing Lambda Functions.

## .NET Lambda Function and Powertools Idempotency Package

The Lambda Powertools Idempotency utility prevents the function handler from executing more than once for the same event payload during a specified period while ensuring it returns the same result.

The utility package is highly customizable, making it very easy to be tuned to work for your application-specific needs.

### Setting Up Lambda Function Idempotency

To start using the Idempotency utility, let's first install the NuGet package - `AWS.Lambda.Powertools.Idempotency`

Once installed, we only need to add the `Idempotent` attribute to our Lambda Function handler endpoint, as shown below.

```csharp
[LambdaFunction(Role = "@WeatherApiLambdaExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/weather-forecast")]
[Idempotent]
public async Task AddWeatherData([FromBody] WeatherForecast weatherForecast)
{
    Console.WriteLine("Running time consuming process");
    await Task.Delay(TimeSpan.FromSeconds(5));
    await dynamoDBContext.SaveAsync(weatherForecast);
}
```

The Lambda Function sample uses [Lambda Annotations Framework](https://www.rahulpnath.com/blog/lambda-annotation-framework-crud-api/) , which makes it extremely easy to build Lambda Functions in .NET.

The function is an API endpoint that takes in a `WeatherForecast` data from the body of the API Gateway API request object.

In the Function constructor, you can initialize the Idempotency configuration. The code below sets up the package to use the DynamoDB table `IdempotencyTable` , as it's storage mechanism to maintain request/response information.

```csharp
public Functions(IDynamoDBContext dynamoDBContext)
{
    this.dynamoDBContext = dynamoDBContext;
    Idempotency.Configure(builder => builder.UseDynamoDb("IdempotencyTable"));
}
```

*Ensure that the Lambda Function has appropriate rights to talk to the* `IdempotencyTable`*.*

When making requests against the API endpoint, it creates a hash value based on the request object (in this case, the `WeatherForecast` object) and stores it in the DynamoDB table.

If the same request is made, it will find a matching item in the DynamoDB table with the hash key and return the recorded response from the earlier successful request.

The default timeout period is 1 hour, which means any request with the same payload during that period will get the recorded response. After the timeout, the record is deleted from the DynamoDB table, and the first new request with the same payload will invoke the Lambda Function again.


![DynamoDB Table for Idempotency data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/te22klh24nd9o43wnsoc.png)

Idempotency DynamoDB Table with records populated as we make requests in the API endpoint.

Since the sample function above does not return any value, the data property in the DynamoDB table is empty.

## Customizing Idempotency Key

The Idempotency package automatically used the first parameter In the above function, it automatically used the first parameter of the Lambda Function as the Idempotency key.

If there is more than one parameter for the Lambda Function, you can explicitly mark the item to use as the Idempotency Key using the `IdempotencyKey` attribute.

```csharp
[LambdaFunction(Role = "@WeatherApiLambdaExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/weather-forecast")]
[Idempotent]
public async Task AddWeatherData(APIGatewayHttpApiV2ProxyRequest request, [IdempotencyKey][FromBody] WeatherForecast weatherForecast)
{
    Console.WriteLine("Running time consuming process");
    await Task.Delay(TimeSpan.FromSeconds(5));
    await dynamoDBContext.SaveAsync(weatherForecast);
}
```

You can further customize the IdempotencyKey to use specific properties from the object payload.

This helps to ensure that the idempotency key is not affected by properties in the request object that might change over multiple requests (e.g., HTTP headers, etc.).

The code below specifies the `CityName` and `Date` properties to be used for the Idempotency key, ignoring all other properties on the request object (`WeatherForecast`).

```csharp
public Functions(IDynamoDBContext dynamoDBContext)
{
    this.dynamoDBContext = dynamoDBContext;
    Idempotency.Configure(builder =>
        builder
        .WithOptions(optionsBuilder => 
              optionsBuilder.WithEventKeyJmesPath("[CityName, Date]"))
        .UseDynamoDb("IdempotencyTable"));
}
```

Any requests with the same `CityName` and `Date` property will be treated as an idempotent request during the timeout period.

## Adding Payload Validation for Idempotency

Now, in the case of the WeatherForecast payload, what if the Temperature amount is different between two consecutive requests?

Since we chose not to include that as part of the Idempotency key, it will respond with the response from the initial request for any subsequent request.

In these cases, you can either add the `Temperature` property to the Idempotency key or add in request validation to ensure that the Temperature property has the same value as the initial request.

The initialization code below sets up the property validation`TemperatureC` using the `WithPayloadValidationJmesPath` function.

```csharp
public Functions(IDynamoDBContext dynamoDBContext)
{
    this.dynamoDBContext = dynamoDBContext;
    Idempotency.Configure(builder =>
        builder
        .WithOptions(optionsBuilder => optionsBuilder
            .WithEventKeyJmesPath("[CityName, Date]")
            .WithPayloadValidationJmesPath("TemperatureC"))
        .UseDynamoDb("IdempotencyTable"));
}
```

Any subsequent requests with the same `CityName` and `Date` property, with a different `TemperatureC` property will throw an `IdempotencyValidationException.`

I hope this helps you to get started using the Idempotency Lambda package.