Dependency Injection with the Lambda Annotations Library for .NET - Part 2, Lambda Functions

Want to learn more about AWS Lambda and .NET? Check out my A Cloud Guru course on ASP.NET Web API and Lambda.

Download full source code.

Note, these examples are deployed using dotnet lambda deploy-function, and do not use a serverless.template, if you are looking for an example that is deployed using dotnet lambda deploy-serverless, see this post.

If you want to jump to the code -

Introduction

The new Amazon.Lambda.Annotations library makes dependency injection (DI) for .NET AWS Lambda functions easier. The Lambda .NET project templates now include a serverless project template with annotations and DI configured for you. This template setups up a project with multiple function handlers, and triggers them via an API Gateway.

But this post will show you how to use DI with the simpler lambda.EmptyFunction template, which is invoked directly (though you can easily change it to be invoked by another AWS service). You will see how to configure both scoped services and singleton services.

If you don’t want to use the Amazon.Lambda.Annotations library, take a look at another post on doing DI with .NET Lambda functions.

Singleton and scoped services

In traditional .NET API applications where multiple requests are handled simultaneously by the same process, scoped services create a new instance of the service for each request. This prevents an instance of a service from being shared between requests.

Lambda functions run inside execution environments, each execution environment handles a single request at a time. Therefore two requests can’t use a single instance of a service at the same time.

As long are you are not storing any state in the service, a singleton might be a better choice as there will be less overhead. But if you are storing state (or not clearing state) in the service, then with a singleton, the state will be available to the next request (if the request uses an execution environment that already exists).

Scoped services, should generally be injected directly into the function handler method. Remember, for a given Lambda execution environment, the function constructor is only called once, at initialization time. If you passed a scoped service into the constructor, you would be using the same instance of the service for every request.

Singleton services should generally be injected via the constructor of the function handler class.

Using scoped dependency injection with lambda.EmptyFunction templates

In this example, you are going to use the simplest Lambda function template, lambda.EmptyFunction.

Create a new project

Create a new Lambda function using the lambda.EmptyFunction template -

dotnet new lambda.EmptyFunction -n LambdaDIAnnotationsScoped 

Create the service and add it to the dependency injection container

Create a file called Startup.cs, this is where you will configure register services with the dependency injection container. Add the following code to the file -

 1using Amazon.Lambda.Annotations;
 2using Microsoft.Extensions.DependencyInjection;
 3
 4namespace LambdaDIAnnotationsScoped;
 5
 6[LambdaStartup]
 7public class Startup
 8{
 9    public void ConfigureServices(IServiceCollection services)
10    {
11        services.AddScoped<ISomeService, SomeService>();
12    }
13} 

Create a file called SomeService.cs, it will contain the interface and the class. Add the following code to the file -

 1namespace LambdaDIAnnotationsScoped;
 2
 3public class SomeService : ISomeService
 4{
 5    private int counter = 0;
 6    public string ToUpperCase(string text)
 7    {
 8        Console.WriteLine($"In ToUpperCase method. Invocation count: {++counter}"); 
 9        return text.ToUpper();
10    }
11}
12
13public interface ISomeService
14{
15    string ToUpperCase(string text);
16}

As you can see, this is very simple, all it does is convert a string to uppercase.

Update the function

Open the Function.cs file and replace the code with the following -

 1using Amazon.Lambda.Annotations;
 2using Amazon.Lambda.Core;
 3
 4[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
 5
 6namespace LambdaDIAnnotationsScoped;
 7
 8public class Function
 9{    
10    [LambdaFunction] 
11    public string FunctionHandler([FromServices] someService, // inject the service into the method
12        string input,
13        ILambdaContext context) 
14    {
15        return someService.ToUpperCase(input);
16    }
17}

The function handler takes the service as a parameter.

You’re almost done, one more small change to the function handler.

Setting the function handler and deploying the function

Right now the function handler is aws-lambda-tools-defaults.json is set to LambdaDIAnnotationsScoped::LambdaDIAnnotationsScoped.Function::FunctionHandler.

Change it to -

LambdaDIAnnotationsScoped::LambdaDIAnnotationsScoped.Function_FunctionHandler_Generated::FunctionHandler

This handler points to a class the annotations library generated for you.

Deploy the function using the following command -

dotnet lambda deploy-function LambdaDIAnnotationsScoped

When asked, give the role a name like LambdaDIAnnotationsScopedRole, and choose AWSLambdaBasicExecutionRole as the policy to attach.

Invoke the function

Use the following command to invoke the function -

dotnet lambda invoke-function LambdaDIAnnotationsScoped --payload "convert to upper"

You will see the following output -

Payload:
"CONVERT TO UPPER"

Log Tail:
START RequestId: 8679f86c-7565-4ce3-95e7-34ee946d543c Version: $LATEST
2022-10-15T00:25:18.700Z        8679f86c-7565-4ce3-95e7-34ee946d543c    info    In ToUpperCase method. Invocation count: 1

But no matter how many times you invoke it, the invocation count will always be 1. This is because the service is scoped, and a new instance of the service is created for each request.

Using singleton dependency injection with the lambda.EmptyFunction template

In this example, you are going to use the simplest Lambda function template, lambda.EmptyFunction.

Create a new project

Create a new Lambda function using the lambda.EmptyFunction template -

dotnet new lambda.EmptyFunction -n LambdaDIAnnotationsSingleton 

Create the service and add it to the dependency injection container

Create a file called Startup.cs, this is where you will configure register services with the dependency injection container. Add the following code to the file -

 1using Amazon.Lambda.Annotations;
 2using Microsoft.Extensions.DependencyInjection;
 3
 4namespace LambdaDIAnnotationsSingleton;
 5
 6[LambdaStartup]
 7public class Startup
 8{
 9    public void ConfigureServices(IServiceCollection services)
10    {
11        services.AddSingleton<ISomeService, SomeService>();
12    }
13} 

Create a file called SomeService.cs, it will contain the interface and the class. Add the following code to the file -

 1namespace LambdaDIAnnotationsSingleton;
 2
 3public class SomeService : ISomeService
 4{
 5    private int counter = 0;
 6    public string ToUpperCase(string text)
 7    {
 8        Console.WriteLine($"In ToUpperCase method. Invocation count: {++counter}"); 
 9        return text.ToUpper();
10    }
11}
12
13public interface ISomeService
14{
15    string ToUpperCase(string text);
16}

As you can see, this is very simple, all it does is convert a string to uppercase.

Update the function

Open the Function.cs file and replace the code with the following -

 1using Amazon.Lambda.Annotations;
 2using Amazon.Lambda.Core;
 3
 4[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
 5
 6namespace LambdaDIAnnotationsSingleton;
 7
 8public class Function
 9{
10    private readonly ISomeService _someService; 
11    public Function(ISomeService someService)
12    {
13        _someService = someService;
14    }   
15
16    [LambdaFunction]
17    public string FunctionHandler(string input, ILambdaContext context)
18    {
19        return _someService.ToUpperCase(input);
20    }
21}

You’re almost done, one more small change to the function handler.

Setting the function handler and deploying the function

Right now the function handler is aws-lambda-tools-defaults.json is set to LambdaDIAnnotationsSingleton::LambdaDIAnnotationsSingleton.Function::FunctionHandler.

Change it to -

LambdaDIAnnotationsSingleton::LambdaDIAnnotationsSingleton.Function_FunctionHandler_Generated::FunctionHandler

This handler points to a class the annotations library generated for you.

Deploy the function using the following command -

dotnet lambda deploy-function LambdaDIAnnotationsSingleton

When asked, give the role a name like LambdaDIAnnotationsSingleton, and choose AWSLambdaBasicExecutionRole as the policy to attach.

Invoke the function

Use the following command to invoke the function -

dotnet lambda invoke-function LambdaDIAnnotationsSingleton --payload "convert to upper"

You will see the following output -

Payload:
"CONVERT TO UPPER"

Log Tail:
START RequestId: 8bf5a8e0-bdd7-44a0-a6c5-bb0bb0ecd933 Version: $LATEST
2022-10-15T00:32:40.171Z        8bf5a8e0-bdd7-44a0-a6c5-bb0bb0ecd933    info    In ToUpperCase method. Invocation count: 3

This time the invocation count will increase each time you invoke the function. This is because the service is passed to the constructor which is invoked only once, and the same instance of the service is used for each request.

Download full source code.

comments powered by Disqus

Related