AWS Developer Tools Blog
.NET Lambda Annotations Framework is now generally available
We are happy to announce the general availability of the Lambda Annotations Framework for .NET. This new programming model makes the experience of writing Lambda in C# feel more natural for .NET developers by using C# Source Generators. In this post we’ll show how to use framework to simplifying writing .NET Lambda functions that are more idiomatic for .NET developers.
What is the Lambda Annotations Framework?
The Lambda Annotations Framework provides a natural programming model for .NET developers to create AWS Lambda functions. The new framework uses C# custom attributes and Source Generators to translate annotated Lambda functions to the regular Lambda programming model. Source Generators create new C# source code and incorporate that code during compilation. The Lambda Annotations Framework does not impact Lambda startup times because it translates your code at compile time.
Source generators integrate into the C# compiler to do code generation when the .NET project is compiled. This means no additional tooling is required to use Lambda Annotations other than the Lambda Annotations NuGet package. Any Lambda deployment tooling using CloudFormation can use Lambda Annotations. This includes the AWS Toolkit for Visual Studio, Lambda .NET CLI or SAM.
The following C# Lambda function is written using the regular Lambda programming model. It processes REST API requests from Amazon API Gateway:
public class Functions
{
public APIGatewayProxyResponse LambdaMathPlus(APIGatewayProxyRequest request, ILambdaContext context)
{
if (!request.PathParameters.TryGetValue("x", out var xs))
{
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.BadRequest
};
}
if (!request.PathParameters.TryGetValue("y", out var ys))
{
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.BadRequest
};
}
var x = int.Parse(xs);
var y = int.Parse(ys);
return new APIGatewayProxyResponse
{
StatusCode = (int)HttpStatusCode.OK,
Body = (x + y).ToString(),
Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
};
}
}
The same Lambda function can be rewritten using the Lambda Annotations Framework like this:
public class Functions
{
[LambdaFunction]
[RestApi("/plus/{x}/{y}")]
public int Plus(int x, int y)
{
return x + y;
}
}
The Lambda Annotations Framework incorporates the following features:
- Dependency injection
- AWS CloudFormation syncing
- Code generation
- JSON and YAML CloudFormation template support
How to use the Lambda Annotations Framework
In this section we will create an AWS Serverless Application built with the Lambda Annotations Framework. To follow along you will need to install and configure the following:
- Visual Studio 2022
- The latest version of the AWS Toolkit for Visual Studio
- An IAM user with permissions to create APIs using Amazon API Gateway, AWS Lambda Functions, and Amazon Simple Storage Service (Amazon S3) buckets.
If you are not using Visual Studio, then the same project template shown below can be created from the Amazon.Lambda.Templates NuGet package.
dotnet new install Amazon.Lambda.Templates
dotnet new serverless.Annotations --output LambdaAnnotations
Getting Started
Lambda Annotations is now the default project template for AWS Serverless Applications, both in the AWS Toolkit for Visual Studio and also from the .NET CLI. Let’s start by creating a new AWS Serverless Application with Lambda annotations via the AWS Toolkit for Visual Studio:
- Open Visual Studio and create a new AWS Serverless Application.
- Name the project LambdaAnnotations.
- On the Select Blueprint page select Annotations Framework and then select Finish.
The created project contains a collection of Lambda functions in the Function.cs file that simulate a calculator as a REST API. Looking at the Add
Lambda function the LambdaFunction
attribute identifies the C# method as a Lambda function that is synchronized with the CloudFormation template. The HttpApi
attribute adds the API Gateway event configuration in the CloudFormation template.
[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
public int Add(int x, int y, ILambdaContext context)
{
var sum = _calculatorService.Add(x, y);
context.Logger.LogInformation($"{x} plus {y} is {sum}");
return sum;
}
NOTE: If you are writing Lambda functions that are not for API Gateway you can still use Lambda Annotations with the LambdaFunction attribute and configure the event source in the CloudFormation template. In the future we would like to add more event attributes, similar to that of API Gateway, for other services like SQS and S3.
The serverless.template file in the project is the CloudFormation template used for deploying the Lambda function. Each C# method that is decorated with the LambdaFunction attribute will have a corresponding declaration in the template. Notice the Handler field in the screenshot below is set to a method generated by the Lambda Annotation’s source generator.
Building an application from scratch
To better understand Lambda Annotations let’s build a new serverless application. The serverless application we are going to build will use the Amazon Translate service to translate text into other languages. Before we do this we need to remove the calculator APIs. Open the Functions.cs file and delete all of the code inside Functions class. After you recompile the .NET project, serverless.template no longer declares any Lambda functions.
Dependency Injection
Dependency injection is a first-class citizen in the Lambda Annotations Framework. You can wire up dependency injection in your Lambda function the same way you would wire up dependency injection in an ASP.NET Core application. Dependency injection is implemented for your Lambda functions through LambdaStartup
and LambdaFunction
attributes. Let’s take a look at how to leverage dependency injection with the Lambda Annotations framework.
Open Startup.cs. The ConfigureServices method is used to wire up objects for dependency injection. For our application we need to inject the AWS SDK service client for Amazon Translate.
- Add the following NuGet packages to your LambdaAnnotations project:
- AWSSDK.Translate
- AWSSDK.Extensions.NETCore.Setup
- Add the following line of code to the ConfigureServices method in the Startup class:
services.AddAWSService<Amazon.Translate.IAmazonTranslate>();
Your Startup.cs file should look like this:
using Microsoft.Extensions.DependencyInjection;
namespace LambdaAnnotations
{
[Amazon.Lambda.Annotations.LambdaStartup]
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAWSService<Amazon.Translate.IAmazonTranslate>();
}
}
}
Notice that the Startup class has been decorated with the LambdaStartup
attribute. This tells the Lambda Annotations Framework that the Startup class is used to wire up dependencies.
That’s it! You have configured your AWS services for dependency injection and can now inject it into your Lambda functions rather than having to instantiate it directly.
Once you have wired up your function’s dependencies there are two ways you can inject a dependency into your Lambda function: via your Lambda function’s constructor, or directly into your Lambda function’s method. Constructor injection is ideal for services that can be shared amongst function invocations and/or are heavy to instantiate. Method injection is better suited for services that should be instantiated for each method invocation.
In our application, the Translate service client can be shared across function invocations so we can inject the service client into the constructor. To do this, go back to the Functions class and add a constructor that declares an IAmazonTranslate
parameter.
public class Functions
{
private IAmazonTranslate _translateClient;
public Functions(IAmazonTranslate translateClient)
{
_translateClient = translateClient;
}
}
NOTE For our application we will not use method injection, but that can be done by using the FromServices attribute on a parameter for the Lambda function.
Writing the translate Lambda function
The actual work of the translation function is done within the TranslateFromEnglish
method. It uses the IAmazonTranslate
client that was injected into the function’s constructor to translate the text provided in the string text
parameter into the language specified in the string targetLanguageCode
parameter. Copy and paste the code below into your Functions class.
public async Task<string> TranslateFromEnglish(string targetLanguageCode, string text)
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return response.TranslatedText;
}
Now we need to tell the Annotations Framework that the TranslateFromEnglish
method is a Lambda function by decorating the method with a LambdaFunction
attribute. LambdaFunctionAttribute
exposes a property called Policies
that enable us to define the IAM policies required by our Lambda function. Our function requires two IAM policies: TranslateReadOnly and AWSLambdaBasicExecutionRole.
We also need to tell the Annotations Framework how to wire up our Lambda function with API Gateway by decorating the TranslateFromEnglish
method with a HttpApi
attribute. HttpApi
attribute accepts a LambdaHttpMethod
parameter and a string template parameter that defines the function’s route. Our function will process POST
requests and will be triggered when those requests are sent to /translate/{targetLanguageCode}
.
The text that will be translated by the function is contained within the body of the POST request so you need to add the FromBody
attribute on the string text parameter.
The updated TranslateFromEnglish method should now look like this:
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<string> TranslateFromEnglish(string targetLanguageCode, [FromBody] string text)
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return response.TranslatedText;
}
This is a good start, but it is probably not ready for production. For example, what happens if targetLanguageCode
is not a valid language code, or the text to be translated is missing or invalid, or some other unexpected error occurs? We probably want some way to signal that something has gone wrong.
You can communicate errors by changing the method’s return type to IHttpResult
and using the HttpResults
utility class to create an IHttpResult
for the desired status code. In the code below, if translation was successful an Ok
response is returned. If an UnsupportedLanguagePairException
exception is caught that means the targetLanguageCode
was invalid and a BadRequest
response is returned. For all other exceptions an InternalServerError
response is returned.
Notice for generating Ok
response the AddHeader
is used to demonstrate how to include headers as part of the response.
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<IHttpResult> TranslateFromEnglish(string targetLanguageCode, [FromBody] string text)
{
try
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
var response = await _translateClient.TranslateTextAsync(request);
return HttpResults.Ok(response.TranslatedText)
.AddHeader("target-language", targetLanguageCode);
}
catch (UnsupportedLanguagePairException)
{
return HttpResults.BadRequest($"Translating from English to {targetLanguageCode} is not supported");
}
catch (Exception ex)
{
return HttpResults.InternalServerError();
}
}
Now that exceptions are being handled gracefully we need to make sure there is plenty of logging available to debug any issues. When using Lambda Annotations, ILambdaContext
can be added to the method signature just like the traditional Lambda function programming model. As the actual text being translated could be large the text is logged as debug. The LogDebug
methods won’t write to CloudWatch Logs unless the AWS_LAMBDA_HANDLER_LOG_LEVEL
environment variable is set to debug.
[LambdaFunction(Policies = "TranslateReadOnly, AWSLambdaBasicExecutionRole")]
[HttpApi(LambdaHttpMethod.Post, "/translate/{targetLanguageCode}")]
public async Task<IHttpResult> TranslateFromEnglish(string targetLanguageCode,
[FromBody] string text, ILambdaContext context)
{
try
{
var request = new TranslateTextRequest
{
SourceLanguageCode = "en-US",
TargetLanguageCode = targetLanguageCode,
Text = text
};
context.Logger.LogDebug("Text to translate:");
context.Logger.LogDebug(text);
var response = await _translateClient.TranslateTextAsync(request);
context.Logger.LogDebug("Translation:");
context.Logger.LogDebug(response.TranslatedText);
return HttpResults.Ok(response.TranslatedText)
.AddHeader("target-language", targetLanguageCode);
}
catch(UnsupportedLanguagePairException)
{
context.Logger.LogWarning($"Invalid target language code: {targetLanguageCode}");
return HttpResults.BadRequest($"Translating from English to {targetLanguageCode} is not supported");
}
catch (Exception ex)
{
context.Logger.LogError("Unknown error performing translate");
context.Logger.LogError(ex.ToString());
return HttpResults.InternalServerError();
}
}
At this point the Lambda function is in good shape and is ready to be pushed up the AWS Lambda. You can do this by right-clicking the .NET project and selecting Published to AWS Lambda… menu option. The AWS Toolkit for Visual Studio will initiate a deployment through CloudFormation with the CloudFormation template that Lambda Annotations synchronized with the C# code.
As Lambda Annotations synchronized the CloudFormation template as part of the C# compiler other tools such as SAM can also be used for deployment.
Code Generation
The Lambda Annotations Framework leverages Source Generators to translate your annotated Lambda functions to the regular Lambda programming model. This is a great “quality of life” feature of the Lambda Annotations framework that significantly reduces the amount of boilerplate code you need to write.
Let’s take a look at the generated Lambda functions. In the Solution Explorer window in Visual Studio:
- Navigate to your LambdaAnnotations project
- Expand Dependencies
- Expand Analyzers
- Expand Amazon.Lambda.Annotations.SourceGenerator
- Expand Amazon.Lambda.Annotations.SourceGenerator.Generator
- Open Functions_TranslateFromEnglish_Generated.g.cs
The Lambda Annotations Framework has taken care of:
- Updating the method signature to comply with an Amazon API Gateway event source
- Parameter checking and exception handling
- Updating the return type to comply with Amazon API Gateway
Conclusion
The easiest way to start developing with the Lambda Annotations Framework is by downloading Visual studio 2022 and installing the AWS Toolkit for Visual Studio extension. You can then use the AWS Serverless Application project template that ships with the AWS Toolkit for Visual Studio to get started. You can also use the annotations framework by manually pulling down the Amazon.Lambda.Annotations NuGet package into your Serverless applications. Try it and let us know what you think on our github repo.
In this post we highlighted creating an API Gateway based Lambda function using Lambda Annotations framework. Other types of Lambda functions can also take advantage of the dependency injection and CloudFormation synchronization by using the LambdaFunction
attribute. The event source can still be configured in the CloudFormation template.
Here are some useful links:
- All the framework related work is part of the main dotnet lambda repo, aws/aws-lambda-dotnet, on github.
- The Annotations source code library is located here inside the main repo.
- The above folder contains a README.md that lists various Event and Parameter attributes currently supported.