After some time developing Lambda functions with .NET one can notice that the programming model could be considered low-level, especially if we compare it with ASP.NET Core. Let's check the following code:
public async Task<APIGatewayHttpApiV2ProxyResponse> RegisterNews(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
try
{
var body = System.Text.Json.JsonSerializer.Deserialize<RegisterNewsRequest>(request.Body)!;
var news = new News() { Id = Guid.NewGuid(), Description = body.Description, Title = body.Title };
await _dbContext.SaveAsync(news);
return new APIGatewayHttpApiV2ProxyResponse
{
Body = System.Text.Json.JsonSerializer.Serialize(new RegisterNewsResponse(news.Id)),
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"}
},
StatusCode = 200
};
}
catch (Exception e)
{
return new APIGatewayHttpApiV2ProxyResponse
{
Body = @$"{{""message"": ""{e.Message}""}}",
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
},
StatusCode = 400
};
}
}
The developer deals with a lot of boilerplate code that usually is hidden from us. A similar code but using ASP.NET Core could be:
[HttpPost()]
public async Task<RegisterNewsResponse> RegisterNews(RegisterNewsRequest request)
{
var news = new News() { Id = Guid.NewGuid(), Description = request.Description, Title = request.Title };
await _dbContext.SaveAsync(news);
return new RegisterNewsResponse(news.Id);
}
As an option, we can decide to use ASP.NET Core to develop our Lambda functions, but there are drawbacks around it:
There is an increment in the cold start time.
There are features not supported by Lambda functions (SignalR, Blazor, etc).
API Gateway is replaced by the routing mechanism present in ASP.NET.
So, Lambda Annotations are an effort to improve the development experience in writing Lambda functions that is similar to what we have with ASP.NET Core.
Lambda Annotations is a programming model for writing .NET Lambda functions. At a high level, the programming model allows idiomatic .NET coding patterns. C# Source Generators are used to bridge the gap between the Lambda programming model to the Lambda Annotations programming model without adding any performance penalty.
To start using Lambda Annotations, we have a few pre-requisites:
Have an IAM User with programmatic access.
Install the Amazon Lambda Templates (
dotnet new -i Amazon.Lambda.Templates
)Install the Amazon Lambda Tools (
dotnet tool install -g
Amazon.Lambda.Tools
)Install AWS SAM CLI.
Run the following commands:
dotnet new serverless.Annotations -n JournalApi -o .
dotnet new sln -n aws-lambda-annotations
dotnet sln add --in-root src/JournalApi
dotnet add src/JournalApi package AWSSDK.DynamoDBv2
By default, Lambda Annotations use JSON for the AWS SAM configuration file. The following steps are oriented to change that to use YAML. Open the solution and add a file named template.yml
with the following content:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Open the aws-lambda-tools-defaults.json
file and edit the template
property. The new value should be template.yml
. Delete the file serverless.template
. Open the Functions.cs
file and replace the content with:
using Amazon.Lambda.Core;
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.DynamoDBv2.DataModel;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace JournalApi;
public record RegisterNewsRequest(string Description, string Title);
public record RegisterNewsResponse(Guid Id);
[DynamoDBTable("newstable")]
public class News
{
[DynamoDBHashKey("id")]
public Guid Id { get; set; }
[DynamoDBProperty("description")]
public string? Description { get; set; }
[DynamoDBProperty("title")]
public string? Title { get; set; }
}
public class Functions
{
private readonly DynamoDBContext _dbContext;
public Functions(DynamoDBContext dbContext)
{
_dbContext = dbContext;
}
[LambdaFunction(MemorySize = 512)]
[HttpApi(LambdaHttpMethod.Post, "/")]
public async Task<RegisterNewsResponse> RegisterNews([FromBody]RegisterNewsRequest request)
{
var news = new News() { Id = Guid.NewGuid(), Description = request.Description, Title = request.Title };
await _dbContext.SaveAsync(news);
return new RegisterNewsResponse(news.Id);
}
[LambdaFunction(MemorySize = 512)]
[HttpApi(LambdaHttpMethod.Get, "/")]
public async Task<List<News>> ListNews()
{
return await _dbContext.ScanAsync<News>(default).GetRemainingAsync();
}
[LambdaFunction(MemorySize = 512)]
[HttpApi(LambdaHttpMethod.Get, "/{id}")]
public async Task<News> GetNews(string id)
{
return await _dbContext.LoadAsync<News>(Guid.Parse(id));
}
}
Open the Startup.cs
file and replace the content with:
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2;
using Microsoft.Extensions.DependencyInjection;
namespace JournalApi;
[Amazon.Lambda.Annotations.LambdaStartup]
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton(new DynamoDBContext(new AmazonDynamoDBClient()));
}
}
Compile the project and watch the magic of code generation by writing all the boilerplate code required by our Lambda functions:
We suggest reviewing those classes to understand what is doing Lambda Annotations by us. But not only is generating code, open the template.yml
to see all the configurations needed to deploy our Lambda functions. We are going to customize the file to include the creation of the DynamoDB table and the permissions needed by the Lambda functions:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
NewsTable:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: id
Type: String
TableName: newstable
JournalApiFunctionsRegisterNewsGenerated:
Type: AWS::Serverless::Function
Metadata:
Tool: Amazon.Lambda.Annotations
SyncedEvents:
- RootPost
Properties:
Runtime: dotnet6
CodeUri: .
MemorySize: 512
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
- DynamoDBCrudPolicy:
TableName: newstable
PackageType: Zip
Handler: JournalApi::JournalApi.Functions_RegisterNews_Generated::RegisterNews
Events:
RootPost:
Type: HttpApi
Properties:
Path: /
Method: POST
JournalApiFunctionsListNewsGenerated:
Type: AWS::Serverless::Function
Metadata:
Tool: Amazon.Lambda.Annotations
SyncedEvents:
- RootGet
Properties:
Runtime: dotnet6
CodeUri: .
MemorySize: 512
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
- DynamoDBCrudPolicy:
TableName: newstable
PackageType: Zip
Handler: JournalApi::JournalApi.Functions_ListNews_Generated::ListNews
Events:
RootGet:
Type: HttpApi
Properties:
Path: /
Method: GET
JournalApiFunctionsGetNewsGenerated:
Type: AWS::Serverless::Function
Metadata:
Tool: Amazon.Lambda.Annotations
SyncedEvents:
- RootGet
Properties:
Runtime: dotnet6
CodeUri: .
MemorySize: 512
Timeout: 30
Policies:
- AWSLambdaBasicExecutionRole
- DynamoDBCrudPolicy:
TableName: newstable
PackageType: Zip
Handler: JournalApi::JournalApi.Functions_GetNews_Generated::GetNews
Events:
RootGet:
Type: HttpApi
Properties:
Path: /{id}
Method: GET
Go to the template.yml
level and run the following commands to deploy it to AWS:
sam build
sam deploy --guided
And that's it. Our Lambda functions are up and running using Lambda Annotation. Sadly Lambda Annotations are still on preview, but we see a lot of potential for this initiative that wants to make easier the development experience for the .NET community. All the code is available here. Thanks, and happy coding.