Dependency Injection with the Lambda Annotations Library for .NET - Part 1, Lambda Applications
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-serverless
, and use a serverless.template
, if you are looking for an example that is deployed using dotnet lambda deploy-function
, 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 a function that 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.
Prerequisites
Install the latest tooling, this lets you deploy and run Lambda functions.
dotnet tool install -g Amazon.Lambda.Tools
Install the latest templates to get the annotations sample project.
dotnet new --install Amazon.Lambda.Templates
Create an S3 bucket for the the Cloud Formation stacks, you can do this with the AWS CLI or via the AWS Console.
Using scoped dependency injection
In this example, you will use a scoped service that is injected in the function handler method as opposed to the constructor.
Create a new project
Create a new Lambda function using the serverless.Annotations
template -
dotnet new serverless.Annotations -n LambdaDIAnnotationsScoped
Create the service and add it to the dependency injection container
Open the file Startup.cs
, this is where you will configure register services with the dependency injection container. Replace the content the following code -
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 Functions.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 Functions
9{
10 [LambdaFunction]
11 public string FunctionHandler([FromServices]ISomeService 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 name and deploying the function
Open the serverless.template
file in the project directory.
Look for the Handler
line. It should be -
"Handler": "LambdaDIAnnotationsScoped::LambdaDIAnnotationsScoped.Functions_FunctionHandler_Generated::FunctionHandler"
This handler points to a class the annotations library generated for you.
One more change, under properties, add the following -
"FunctionName": "LambdaDIAnnotationsScoped",
Check the serverless.template
file for a section called Outputs
, if it is there, delete it.
Deploy the function using the following command -
dotnet lambda deploy-serverless --stack-name LambdaDIAnnotationsScoped --s3-bucket cloudformation-templates-2022-personal
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 passed into the invoked function. A new instance of the service is created for each request.
Using singleton dependency injection
In this example, you will use a singleton service that is injected in the function handler constructor as opposed to the function handler method.
Create a new project
Create a new Lambda function using the serverless.Annotations
template -
dotnet new serverless.Annotations -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 Functions.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 Functions
9{
10 private readonly ISomeService _someService;
11 public Functions(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 name and deploying the function
Open the serverless.template
file in the project directory.
Look for the Handler
line. It should be -
"Handler": "LambdaDIAnnotationsSingleton::LambdaDIAnnotationsSingleton.Functions_FunctionHandler_Generated::FunctionHandler"
This handler points to a class the annotations library generated for you.
One more change, under properties, add the following -
"FunctionName": "LambdaDIAnnotationsSingleton",
Check the serverless.template
file for a section called Outputs
, if it is there, delete it.
Deploy the function using the following command -
dotnet lambda deploy-serverless --stack-name LambdaDIAnnotationsSingleton --s3-bucket cloudformation-templates-2022-personal
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.