When Amazon introduced AWS Lambda in 2014, it helped kick off the revolution in serverless computing that is now taking the software development world by storm. In a nutshell, AWS Lambda (and equivalents such as Azure Functions and Google Cloud Functions) provides an abstraction over the underlying operating system and execution runtime, so that you can more easily achieve economies of scale that are the driving force of Cloud computing paradigms.
You can download or clone the GitHub repository for this post here: https://github.com/tonysneed/net-core-lambda-di-config
AWS Lambda with .NET Core
AWS Lambda supports a number of programming languages and runtimes, well as custom runtimes which enable use of any language and execution environment. Included in the list of standard runtimes is Microsoft .NET Core, an open-source cross-platform runtime, which you can build apps on using the C# programming language. To get started with AWS Lambda for .NET Core, you can install the AWS Toolkit for Visual Studio, which provides a number of project templates.
The first thing you’ll notice is that you’re presented with a choice between two different kinds of projects: a) AWS Lambda Project, and b) AWS Serverless Application. An AWS Lambda Project will provide a standalone function that can be invoked in response to a number of different events, such as notifications from a queue service. The Serverless Application template, on the other hand, will allow you to group several functions and deploy them as a separate application. This includes a
Startup class with a
ConfigureServices method for registering types with the built-in dependency injection system, as well as local and remote entry points which leverage the default configuration system using values from an appsettings.json file that is included with the project.
The purpose of the Serverless Application approach is to create a collection of functions that will be exposed as an HTTP API using the Amazon API Gateway. This may suit your needs, but often you will want to write standalone functions that respond to various events and represent a flexible microservices architecture with granular services that can be deployed and scaled independently. In this case the AWS Lambda Project template will fill the bill.
The drawback, however, of the AWS Lambda Project template is that is lacks the usual hooks for setting up configuration and dependency injection. You’re on your own for adding the necessary code. But never fear — I will now show you step-by-step instructions for accomplishing this feat.
Add Dependency Injection Code
One of my favorite things to tell developers is that employing the
new keyword to instantiate types is an anti-pattern. Anytime you directly create an object you are coupling yourself to a specific implementation. So it stands to reason that you’d want to apply the same Inversion of Control pattern and .NET Core Dependency Injection goodness to your AWS Lambda Functions as you would in a standard ASP.NET Core Web API project.
Start by adding a package reference to Microsoft.Extensions.DependencyInjection version 2.1.0.
Note: Versions of NuGet packages you install need to match the version of .NET Core supported by AWS Lambda for the project you created. In this example it is 2.1.0.
Then add a
ConfigureServices function that accepts an
IServiceCollection and uses it to register services with the .NET Core dependency injection system.
Next, add a parameterless constructor that creates a new
ConfigureServices, then calls
BuildServiceProvider to create a service provider you can use to get services via DI.
Add Configuration Code
Following the DI playbook, you’re going to want to abstract configuration behind an interface so that you can mock it for unit tests. Start by adding version 2.1.0 of the following package references:
Then create an
IConfigurationService interface with a
GetConfiguration method that returns an
Because some of the configuration settings will be for specific environments, you’ll also want to add an
IEnvironmentService interface with an
IEnvironmentService create an
EnvironmentService class that checks for the presence of a special
ASPNETCORE_ENVIRONMENT variable, defaulting to a value of “Production”.
To avoid the use of magic strings, you can employ a static
ConfigurationService class should have a constructor that accepts an
IEnvironmentService and implements the
GetConfiguration method by creating a new
ConfigurationBuilder and calling methods to add appsettings JSON files and environment variables. Because the last config entry wins, it is possible to add values to an appsettings file which are then overridden by environment variables that are set when the AWS Lambda Function is deployed.
Now that you’ve defined the environment and configuration service interfaces and implementations, it’s time to register them with the DI system inside the
ConfigureServices method of the
Then edit the
Function class constructor to get
IConfigurationService from the DI service provider and set a read-only
ConfigService property on the class. The top of the
Function class should now look like this:
Lastly, add code to the
FunctionHandler method to get a value from the
ConfigService using the
input parameter as a key.
Add App Settings JSON Files
In .NET Core it is customary to add an appsettings.json file to the project and to store varioius configuration settings there. Typically this might include database connection strings (without passwords and other user secrets!). This is so that developers can just press F5 and values can be fed from appsettings.json into the .NET Core configuration system. For example, the following appsettings.json file contains three key-value pairs.
App settings will often vary from one environment to another, so you’ll usually see multiple JSON files added to a project, with the environment name included in the file name (for example, appsettings.Development.json, appsettings.Staging.json, etc). Config values for a production environment can then come from the primary appsettings.json file, or from environment variables set at deployment time. For example, the following appsettings.Development.json file has values which take the place of those in appsettings.json when the
ASPNETCORE_ENVIRONMENT environment variable is set to Development.
However, in order for these files to be deployed to AWS Lambda, they need to be included in the output directory when the application is published. For this to take place, you need open the Properties window to set the
Build Action property to Content and the
Copy to Output Directory property to Copy always.
Set Environment Variables
There are two places where you’ll need to set environment variables: development-time and deployment-time. At development time, the one variable you’ll need to set is the
ASPNETCORE_ENVIRONMENT environment variable, which you’ll want to set to Development. To do this, open the launchSettings.json file from under the Properties folder in the Solution Explorer. Then add the following JSON property:
To set environment variables at deployment-time, you can add these to the aws-lambda-tools-defaults.json file. (Just remember to escape the double quote marks.)
Then, when you publish your project to AWS Lambda, these environment variables will be shown in the Upload dialog, where you get set their values. They will also be shown on the Configuration tab of the Lambda Function after publication, where you can set them and click Apply Changes to deploy the new values.
Try It Out
To execute and debug your AWS Lambda Function locally, simply by press F5 to launch the Mock Lambda Test Tool with the Visual Studio debugger attached. For Function Input you can enter one of the keys from your appsettings.json file, such as
Press the Execute Function button and you should see a response of
"dev-val1" retrieved from the appsettings.Development.json file.
To try out configuration at deployment-time, right-click on the NetCoreLambda project in the Solution Explorer and select Publish to AWS Lambda. In the wizard you can set the
ASPNETCORE_ENVIRONMENT environment variable to something other than Development, such as Production or Staging. When you enter “env1” for the Sample input and click Invoke, you should get a response of
"val1" from the appsettings.json file. Then, to test overriding JSON file settings with environment variables, you can click on the Configuration tab and replace
foo. Click Apply Changes, then invoke the function again to return a value of
One of the reasons for using dependency injection in the first place is to make your AWS Lambda Functions testable by adding constructors to your types which accept interfaces for service dependencies. To this end, you can add a constructor to the
Function class that accepts an
In the NetCoreLambda.Test project add a package dependency for Moq. Then add a unit test which mocks both
IConfigurationService, passing the mock
IConfigurationService to the
Function class constructor. Calling the
FunctionHandler method will then return the expected value.
AWS Lambda Functions offer a powerful and flexible mechanism for developing and deploying single-function, event-driven microservices based on a serverless architecture. In this post I have demonstrated how you can leverage the powerful capabilities of .NET Core to add dependency injection and configuration to your C# Lambda functions, so that you can make them more testable and insulate them from specific platform implementations. For example, using this approach you could use a Repository pattern with .NET Core DI and Config systems to easily substitute a relational data store with a NoSQL database service such as Amazon DynamoDB.