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:

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:

  1. Open Visual Studio and create a new AWS Serverless Application.
  2. Name the project LambdaAnnotations.
  3. On the Select Blueprint page select Annotations Framework and then select Finish.

Lambda template wizard in Visual Studio

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.

CloudFormation template

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.

  1. Add the following NuGet packages to your LambdaAnnotations project:
    • AWSSDK.Translate
    • AWSSDK.Extensions.NETCore.Setup
  2. 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.

Publish to Lambda in Visual Studio Solution Explorer

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:

  1. Navigate to your LambdaAnnotations project
  2. Expand Dependencies
  3. Expand Analyzers
  4. Expand Amazon.Lambda.Annotations.SourceGenerator
  5. Expand Amazon.Lambda.Annotations.SourceGenerator.Generator
  6. Open Functions_TranslateFromEnglish_Generated.g.cs

Analyzers in Visual Studio Solution Explorer

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.
Brad Webber

Brad Webber

Brad is a Senior Microsoft Solutions Architect at Amazon Web Services (AWS). He started writing code when Visual Basic 6 was still a thing. These days he spends most of his time helping organizations modernize their .NET workloads and migrate them to the cloud.

Norm Johanson

Norm Johanson

Norm Johanson has been a software developer for more than 20 years developing all types of applications. Since 2010 he has been working for AWS focusing on the .NET developer experience at AWS. You can find him on Twitter @socketnorm and GitHub @normj.