AWS Developer Tools Blog

Introducing .NET Annotations Lambda Framework (Preview)

Recently we released the .NET 6 managed runtime for Lambda. Along with the new Lambda runtime we have also been working on a new framework for writing .NET 6 Lambda functions called Lambda Annotations. The Annotations framework makes the experience of writing Lambda feel more natural in C#. It also takes care of synchronizing the Lambda functions implemented in your code with your project’s CloudFormation template. This means you don’t have to worry about function handler strings not being set correctly in CloudFormation templates or lose focus while coding to also update your CloudFormation template.

Our goal with the Annotations framework is to be able to take a Lambda function, like the one shown below that uses the regular Lambda program model (which is also defined in a CloudFormation template):


public APIGatewayHttpApiV2ProxyResponse LambdaMathAdd(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
    if (!request.PathParameters.TryGetValue("x", out var xs))
    {
        return new APIGatewayHttpApiV2ProxyResponse
        {
            StatusCode = (int)HttpStatusCode.BadRequest
        };
    }
    if (!request.PathParameters.TryGetValue("y", out var ys))
    {
        return new APIGatewayHttpApiV2ProxyResponse
        {
            StatusCode = (int)HttpStatusCode.BadRequest
        };
    }
    var x = int.Parse(xs);
    var y = int.Parse(ys);
    return new APIGatewayHttpApiV2ProxyResponse
    {
        StatusCode = (int)HttpStatusCode.OK,
        Body = (x + y).ToString(),
        Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
    };
}

and replace it with the simpler version below, removing the typical Lambda function code so you can focus on just the business logic, with a couple of .NET attributes annotating the code:


[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
public int Add(int x, int y)
{
    return x + y;
}

How does Annotations work?

The Lambda programming model is to write a function that takes in at most 2 parameters. The first parameter is an object representing the event triggering the Lambda function. The second parameter is an instance of ILambdaContext which provides information and logging APIs for the running environment. This is what you can see in the first example above, and this programming model hasn’t changed with the introduction of the Annotations framework.

The second example doesn’t follow the Lambda programming model since it is taking multiple parameters that aren’t the event object. Instead, it is taking in parameters that map to the calling REST API’s resource path. The Annotations framework uses a C# feature called source generators to generate code that translates from the Lambda programming model to what we see in the second example.

At a high level, here’s what happens when the C# compiler is invoked to build the project: the Annotations framework finds the .NET Lambda attributes indicating Lambda functions and generates new code into the build that handles all of the translation. It also updates the CloudFormation template for the Lambda function, to set the Lambda function handler string to the generated code. For the Annotations example above the function handler string gets set to AnnotationsExample::AnnotationsExample.Functions_Add_Generated::Add referencing a generating class called Functions_Add_Generated.

A major benefit of using C# source generators to create the translation layer at compile time is that we avoid reflection code at runtime. Since the Annotations framework is doing all the work at compile time and only generating the code that is required for the specific Lambda function, it has minimal impact to Lambda cold start performance. However, source generators are only available for C# which means that, unfortunately, the Annotations framework cannot be used for Lambda functions written using F#.

Getting started

The easiest way to get started with the new framework is using the AWS Toolkit for Visual Studio 2022. Start by creating a project in Visual Studio using the AWS Serverless Application project template. The AWS Serverless Application project template is used for writing one or more Lambda functions and deploying them to AWS using AWS CloudFormation, along with any other required AWS resources, as a single unit of deployment.

The AWS Serverless Application project template will launch the Lambda blueprint selection wizard. The wizard has a new option called Annotations Framework that uses the new framework.

After you create the project, the project file will have a dependency on the Amazon.Lambda.Annotations NuGet package. This package contains the .NET attributes used to annotate the code for Lambda, and the C# source generator that will create the generated translation code.

The project has a Function.cs file containing the Lambda functions, and a serverless.template file which is the project’s CloudFormation template where the Lambda functions are defined. Below is a trimmed-down version of the Lambda functions defined in the Function.cs file. For example purposes, the Lambda functions implement a REST API calculator with add, subtract, multiply, and divide operations.


[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
public int Add(int x, int y, ILambdaContext context)
{
    context.Logger.LogInformation($"{x} plus {y} is {x + y}");
    return x + y;
}


[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/subtract/{x}/{y}")]
public int Subtract(int x, int y, ILambdaContext context)
{
    context.Logger.LogInformation($"{x} subtract {y} is {x - y}");
    return x - y;
}


[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/multiply/{x}/{y}")]
public int Multiply(int x, int y, ILambdaContext context)
{
    context.Logger.LogInformation($"{x} multiply {y} is {x * y}");
    return x * y;
}


[LambdaFunction()]
[HttpApi(LambdaHttpMethod.Get, "/divide/{x}/{y}")]
public int Divide(int x, int y, ILambdaContext context)
{
    context.Logger.LogInformation($"{x} divide {y} is {x / y}");
    return x / y;
}

The CloudFormation project also has all the Lambda functions defined in it so I can deploy all of these Lambda functions together as a CloudFormation stack.

Extending the sample

Let’s extend this example by adding the mod (return the remainder of a whole number division) operator. Add the following code in your Function.cs file:


[LambdaFunction(Timeout = 3, MemorySize = 128)]
[HttpApi(LambdaHttpMethod.Get, "/mod/{x}/{y}")]
public int Mod(int x, int y, ILambdaContext context)
{
    context.Logger.LogInformation($"{x} mod {y} is {x % y}");
    return x % y;
}

In this example I set the Timeout and MemorySize CloudFormation properties in the LambdaFunction attribute. The HttpApi attribute tells the source generator to configure the event source for the function to an API Gateway HTTP API. Additionally the HTTP method is configured as a HTTP GET with a resource path /mod/{x}/{y}. After the project is compiled the CloudFormation template is automatically updated with the new Lambda function:

Configuring CloudFormation properties for the functions using the .NET attributes is optional. If you prefer you can continue to configure the additional settings in the CloudFormation template.

In Visual Studio you can deploy the Lambda functions by right clicking on the project and selecting Publish to AWS Lambda.

Because the source generator used by the Annotations framework is integrated into the C# compiler, any existing tool chain used for deploying .NET Lambda functions through CloudFormation can deploy Lambda functions using the Annotations framework. Be sure to build the .NET project before deploying to ensure the CloudFormation template is up-to-date.

Configuring dependency injection

Dependency injection has become commonplace in .NET applications. .NET has its own built-in dependency injection framework available in the Microsoft.Extensions.DependencyInjection NuGet package. The Annotations framework makes it easy to configure dependency injection for Lambda functions using the LambdaStartup attribute.

In the project created in the previous section the project template created a class called Startup that has the LambdaStartup attribute applied to it:


[Amazon.Lambda.Annotations.LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }
}

In the ConfigureServices method you can register the services your application needs. For example, AWS service clients from the AWS SDK for .NET and Entity Framework Core context objects.

To demonstrate how dependency injection works with this framework, lets add an ICalculatorService service to handle the logic for our Lambda functions. Start by creating a CalculatorService.cs file containing the following code:


public interface ICalculatorService
{
    int Add(int x, int y);
    int Subtract(int x, int y);
    int Multiply(int x, int y);
    int Divide(int x, int y);
}

public class CalculatorService : ICalculatorService
{
    public int Add(int x, int y) => x + y;

    public int Subtract(int x, int y) => x - y;

    public int Multiply(int x, int y) => x * y;

    public int Divide(int x, int y) => x / y;
}

Now that we have our service we can register it in the ConfigureServices method in the Startup class. For the calculator service no state is being preserved so we’ll register the service as a singleton. If you want a separate instance of the service per Lambda invocation use the AddTransient or AddScoped operations.


[Amazon.Lambda.Annotations.LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ICalculatorService, CalculatorService>();   
    }
}

Injecting the services

There are 2 ways to inject services into Lambda function. The first and most common is constructor injection. The constructor for the containing type of the Lambda function takes in the required services. The services can be saved to class fields and used inside the Lambda functions, shown below.


public class Functions
{
    private ICalculatorService _calculatorService;

    public Functions(ICalculatorService calculatorService)
    {
        this._calculatorService = calculatorService;
    }
    
    [LambdaFunction()]
    [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
    public int Add(int x, int y, ILambdaContext context)
    {
        context.Logger.LogInformation($"{x} plus {y} is {x + y}");
        return _calculatorService.Add(x, y);
    }
    
    ...
}

This approach works best when using services that were registered as a singleton in the dependency injection container. That is because the instances of the class containing Lambda functions is only constructing once per Lambda compute environment.

If you have services that need to be recreated for each Lambda invocation then the FromServices attribute can be used, passing the service in the Lambda function’s list of parameters. The Annotations framework will create a new dependency inject scope for each Lambda invocation. So services registered with the AddScoped operation will be created once per invocation. This approach is shown below.


public class Functions
{
    [LambdaFunction()]
    [HttpApi(LambdaHttpMethod.Get, "/add/{x}/{y}")]
    public int Add([FromServices] ICalculatorService calculatorService, int x, int y, ILambdaContext context)
    {
        context.Logger.LogInformation($"{x} plus {y} is {x + y}");
        return calculatorService.Add(x, y);
    }
    
    ...
}

Conclusion

The .NET Lambda Annotations framework is currently in preview, and development of the framework is being done in the aws/aws-lambda-dotnet GitHub repository. We have a pinned GitHub issue where we have been posting updates and it also contains a link to our design doc for the framework. The README for the Annotations framework has the list of currently implemented attributes, including FromHeader, FromBody, and FromQuery.

Currently, the library is focused on Lambda functions for REST APIs. You can also write Lambda functions with just the LambdaFunction attribute to take advantage of the dependency injection integration and CloudFormation synchronization. We would like to expand the Annotation framework to simplify other event sources like S3 and DynamoDB events, and would love to hear the community’s thoughts on what the best experience would be.

Try out the new Annotations framework and let us know your thoughts on GitHub. It is easy to get started with the new Annotations framework in Visual Studio 2022 using the AWS Toolkit for Visual Studio or the .NET CLI by installing the Amazon.Lambda.Templates NuGet package.

–Norm