Add .NET Core DI and Config Goodness to AWS Lambda Functions

This is Part 1 in a 3 part series:

  1. Add .NET Core DI and Config Goodness to AWS Lambda Functions (this post)
  2. IDesignTimeDbContextFactory and Dependency Injection: A Love Story
  3. Use EF Core with AWS Lambda Functions

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.

Note: 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.

aws-lambda-templates.png

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.

private void ConfigureServices(IServiceCollection serviceCollection)
{
// TODO: Register services with DI system
}

Next, add a parameterless constructor that creates a new ServiceCollection, calls ConfigureServices, then calls BuildServiceProvider to create a service provider you can use to get services via DI.

public Function()
{
// Set up Dependency Injection
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
var serviceProvider = serviceCollection.BuildServiceProvider();
// TODO: Get service from DI system
}

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:

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json

Then create an IConfigurationService interface with a GetConfiguration method that returns an IConfiguration.

public interface IConfigurationService
{
IConfiguration GetConfiguration();
}

Because some of the configuration settings will be for specific environments, you’ll also want to add an IEnvironmentService interface with an EnvironmentName property.

public interface IEnvironmentService
{
string EnvironmentName { get; set; }
}

To implement IEnvironmentService create an EnvironmentService class that checks for the presence of a special ASPNETCORE_ENVIRONMENT variable, defaulting to a value of “Production”.

public class EnvironmentService : IEnvironmentService
{
public EnvironmentService()
{
EnvironmentName = Environment.GetEnvironmentVariable(EnvironmentVariables.AspnetCoreEnvironment)
?? Environments.Production;
}
public string EnvironmentName { get; set; }
}

To avoid the use of magic strings, you can employ a static Constants class.

public static class Constants
{
public static class EnvironmentVariables
{
public const string AspnetCoreEnvironment = "ASPNETCORE_ENVIRONMENT";
}
public static class Environments
{
public const string Production = "Production";
}
}
view raw Constants1.cs hosted with ❤ by GitHub

The 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.

public class ConfigurationService : IConfigurationService
{
public IEnvironmentService EnvService { get; }
public ConfigurationService(IEnvironmentService envService)
{
EnvService = envService;
}
public IConfiguration GetConfiguration()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{EnvService.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables()
.Build();
}
}

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 Function class.

private void ConfigureServices(IServiceCollection serviceCollection)
{
// Register services with DI system
serviceCollection.AddTransient<IEnvironmentService, EnvironmentService>();
serviceCollection.AddTransient<IConfigurationService, ConfigurationService>();
}

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:

// Configuration Service
public IConfigurationService ConfigService { get; }
public Function()
{
// Set up Dependency Injection
var serviceCollection = new ServiceCollection();
ConfigureServices(serviceCollection);
var serviceProvider = serviceCollection.BuildServiceProvider();
// Get Configuration Service from DI system
ConfigService = serviceProvider.GetService<IConfigurationService>();
}

Lastly, add code to the FunctionHandler method to get a value from the ConfigService using the input parameter as a key.

public string FunctionHandler(string input, ILambdaContext context)
{
// Get config setting using input as a key
return ConfigService.GetConfiguration()[input] ?? "None";
}

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.

{
"env1": "val1",
"env2": "val2",
"env3": "val3"
}

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.

{
"env1": "dev-val1",
"env2": "dev-val2",
"env3": "dev-val3"
}

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.

appsettings-props

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:

"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

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.)

"environment-variables" : "\"ASPNETCORE_ENVIRONMENT\"=\"Development\";\"env1\"=\"val1\";\"env2\"=\"val2\"",

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.

netcore-lambda

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 "env1".

mock-lambda-test-tool1.png

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 val1 with foo.  Click Apply Changes, then invoke the function again to return a value of foo.

environment-foo

Unit Testing

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 IConfigurationService.

// Use this ctor from unit tests that can mock IConfigurationService
public Function(IConfigurationService configService)
{
ConfigService = configService;
}

In the NetCoreLambda.Test project add a package dependency for Moq.  Then add a unit test which mocks both IConfiguration and IConfigurationService, passing the mock IConfigurationService to the Function class constructor.  Calling the FunctionHandler method will then return the expected value.

public class FunctionTest
{
[Fact]
public void Function_Should_Return_Config_Variable()
{
// Mock IConfiguration
var expected = "val1";
var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(p => p[It.IsAny<string>()]).Returns(expected);
// Mock IConfigurationService
var mockConfigService = new Mock<IConfigurationService>();
mockConfigService.Setup(p => p.GetConfiguration()).Returns(mockConfig.Object);
// Invoke the lambda function and confirm config value is returned
var function = new Function(mockConfigService.Object);
var result = function.FunctionHandler("env1", new TestLambdaContext());
Assert.Equal(expected, result);
}
}

Conclusion

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.

Happy Coding!

About Tony Sneed

Sr. Software Solutions Architect, Hilti Global Application Software
This entry was posted in Technical and tagged , , . Bookmark the permalink.

15 Responses to Add .NET Core DI and Config Goodness to AWS Lambda Functions

  1. Royi Namir says:

    Thanks. But how do you use `ihostingenvironment` ?

    • Tony Sneed says:

      You don’t use IHostingEnvironment, because the ASPNET Core runtime is not there to set it up, which is why my sample reads the environment variable for the hosting environment directly.

  2. NeilS says:

    How does the extra DI configuration effect initial startup times?

  3. Raymond says:

    You Sir are my Hero. Very well explained, great snippets, easy to follow. All I had to do is copy and paste to get it working. Couldn’t thank you more. BRAVO!! Keep up the Great Work!!

  4. Russ Waltz says:

    Hi, do you have any idea how to configure separate settings for test and prod versions of a function running in Lambda? For example, consider a Prod alias pointing to version 1 and a Test alias pointing to version 2. I would like to be able to “promote” version 2 to prod by updating the Prod alias from 1 to 2, which would cause the newer version to be invoked when the prod API Gateway is called. I understood your explanation of how to configure dev configuration settings for local development and prod configuration settings in Lambda, but I’m at a loss for a clean way to configure different settings for test and prod versions of a function running in Lambda. It seems like the alias level would be the perfect place to configure environment variables, but the alias details screen will not let me edit the environment variables. Thanks!

  5. Royi says:

    BTW , is .net core 2.2 supported?

  6. mid787 says:

    Is .net core 2.2 supported ?

  7. John Harcourt says:

    Good job putting this together! As this is over a year old, are there any updates to this due to the release of.NET Core 3.1?

  8. Rohin Tak says:

    This is a great article and life saving for people wanting to develop only lambda without the WebAPI.
    This documentation is nowhere to be found.
    Thanks

  9. Pingback: A Simple .Net Core DI in AWS Lambda | Jian Huang

  10. David C. Joy says:

    Thanks! Great article.
    In this case, do we need to use ‘ConfigService.GetConfiguration()’ in all places to fetch environment specific values/parameters?
    I guess, we need to inject ‘IConfigurationService’ in all classes. Is that correct?

Leave a Reply to Tony Sneed Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.