DEV Community

Gary Woodfine
Gary Woodfine

Posted on • Originally published at garywoodfine.com on

Simple Dependency Injection In AWS Lambda .net core

I’ve previously discussed how to develop a .net core AWS Lambda with the serverless frameworkto provide an abstraction over the underlying operating system and execution runtime so that you can more easily achieve economies of scale using Cloud computing paradigms.

In this post, I will expand on these concepts and provide an introduction on how you can introduce concepts like dependency injection and configuration to your lambda functions.

Serverless Framework enables you to easily create and manage resources used by your Lambda components and creates a simple stub function enabling you to focus on developing your function.

The one problem with this is that developers may perceive that they can’t implement all the usual good things you would in any other development project and judging from some of the code I have had to deal with out in the wild this certainly is true!

GitHub logo garywoodfine / HelloConfiguration

AWS Lambda and .net core dependency injection

Hello Configuration

Source Code to support a series of blog posts to detailing how to use Dotnet Core Configuration and Dependency Injection in AWS Dotnet core lambdas

Donate

Hello Configuration is a FREE tutorial developed and supported by Threenine.co.uk

If you would like to make a donation to keep the developers stocked with coffee and pizza's would be greatly appreciated.

Donate via PayPal

threenine logo




AWS Lambda and .net core

AWS Lambda supports a number of programming languages and runtimes, well custom runtimes which enable the 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.

When generating a project using the serverless framework template is that it lacks the usual hooks for setting up configuration and dependency injection. You’re on your own for adding the necessary code. I will walk you through the process of implementing all the boilerplate code required.

To generate a new serverless lambda project we’ll use the following Serverless command:

sls create -t  aws-csharp -p HelloConfiguration -n Threenine.ConfigTest

Enter fullscreen mode Exit fullscreen mode

Information

To learn how to install the Serverless Framework and what these commands do I urge you to read Getting started with .NET Core and the Serverless Framework

Once the project has been generated we can set to work clearing out the Handler.cs and remove most of the code there so we’re only left with the following

using Amazon.Lambda.Core;
using System;

[assembly:LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AwsDotnetCsharp
{
    public class Handler
    {

    }
}

Enter fullscreen mode Exit fullscreen mode

Add .net core Dependency Injection to lambda

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 Controlpattern and .NET Core Dependency Injection goodness to your AWS Lambda Functions as you would in a standard ASP.NET Core Web API project.

The additional benefit of making use of IOC and Dependency Injection code in an AWS Lambda project is that you can eventually make use of AWS Lambda Layers which enable you to configure your Lambda function to pull in additional code and content in the form of layers.

A layer is a ZIP archive that contains libraries, a custom runtime, or other dependencies. With layers, you can use libraries in your function without needing to include them in your deployment package.

First, we need to wire up dependency injection, so in order to do that add the following references to our project

dotnet add package Microsoft.Extensions.Hosting –version 2.1.0
dotnet add package Microsoft.Extensions.Configuration –version 2.1.0
dotnet add package Microsoft.Extensions.Configuration.Json –version 2.1.0

Enter fullscreen mode Exit fullscreen mode

Note the version number, this is due to fact AWS lambda currently only supports .net core 2.1.0.

In our next step lets add a new Interface which we’ll name ILambdaConfiguration.cs

using Microsoft.Extensions.Configuration;

namespace AwsDotnetCsharp
{
    public interface  ILambdaConfiguration
    {
        IConfiguration Configuration { get; }
    }
}

Enter fullscreen mode Exit fullscreen mode

Implement Lambda Configuration

We can now add a class that will implement the interface we’ll imaginatively call this LambdaConfiguration.cs

Initially, we are going to enable our Lambda to read configuration settings from an appsettings.json file so before we add the code to do so add the file to your project. Just add an empty json file, by convention .net developers name this file appsettings.json but you can call it whatever you want.

Your aws-csharp.csproj should now look something similar to this


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
    <AssemblyName>CsharpHandlers</AssemblyName>
    <PackageId>aws-csharp</PackageId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="1.0.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.Json" Version="1.3.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.1.0" />
       <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup>
    <DotNetCliToolReference Include="Amazon.Lambda.Tools" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <None Remove="appsettings.json" />
    <Content Include="appsettings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

</Project>

Enter fullscreen mode Exit fullscreen mode

We can edit the LambdaConfiguration to get data from our App Settings

using System.IO;
using Microsoft.Extensions.Configuration;

namespace AwsDotnetCsharp
{
     public class LambdaConfiguration : ILambdaConfiguration
    {
        public IConfiguration Configuration  => new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .Build();


    }
    }
}

Enter fullscreen mode Exit fullscreen mode

We can now complete the code from our simple lambda. So edit the Handler.cs to contain the following code.

using Amazon.Lambda.Core;
using Microsoft.Extensions.DependencyInjection;

[assembly:LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace AwsDotnetCsharp
{
    public class Handler
    {
        private ILambdaConfiguration Configuration { get; }

        public Handler()
        {
            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);
            var serviceProvider = serviceCollection.BuildServiceProvider();
            Configuration = serviceProvider.GetService<ILambdaConfiguration>();
        }

        public string Hello( ILambdaContext context)
        {
            return LambdaConfiguration.Configuration["hello"] ?? "None";
        }

        private void ConfigureServices(IServiceCollection serviceCollection)
        {
            serviceCollection.AddTransient<ILambdaConfiguration, LambdaConfiguration>();
        }

    }
}


Enter fullscreen mode Exit fullscreen mode

All this code is basically doing is wiring up the dependency injection and configuring the application. Online 20 we have created a simple Hello lambda method and the only thing it does is read a value from the app settings file, a key which we have called hello.

Our App Settings file is rather simple and looks like

{
    "hello": "This is from the configuration"
}


Enter fullscreen mode Exit fullscreen mode

Build and Deploy the Lambda

We can now build and deploy our lambda using Serverless Framework. I typically work on my Linux box so I will build the application making use of the build.sh. I have kept the yaml file really simple and it contains the following

service: Hello-Configuration # NOTE: update this with your service name

provider:
  name: aws
  runtime: dotnetcore2.1

package:
  individually: true

functions:
  hello:
    handler: CsharpHandlers::AwsDotnetCsharp.Handler::Hello


    package:
      artifact: bin/release/netcoreapp2.1/hello.zip


Enter fullscreen mode Exit fullscreen mode

Using the build command

sudo ./build.sh

Enter fullscreen mode Exit fullscreen mode

Then once the build has completed we can simply deploy our application to AWS using


sls deploy

Enter fullscreen mode Exit fullscreen mode

after the deploy has completed we can simply test our function by invoking it as follows


sls invoke -f hello

Enter fullscreen mode Exit fullscreen mode

We should then see the response printed of whatever the value you placed in the appsettings file

Summary

In this simple tutorial, we have discovered how to implement a basic implementation of Dependency Injection and we have also provided an example of how you can make use of application configuration to make use of appsettings to store additional configuration information for your AWS Lambda and read this information at run time.

If you want to learn more advanced techniques on how to implement dependency injection in AWS Lambda with .net core check out Serverless AWS Lambda Dependency Injection

Top comments (16)

Collapse
 
yaser profile image
Yaser Al-Najjar

Inversion of control is meant to have a loosely coupled architecture...

Why would I want to have that in a lambda function that has one purpose and should do only one thing cuz it's a function after all?

Why don't I instantiate objects directly from the classes in my lambda layers? ain't DI adding more extra work and complexity?

Collapse
 
gary_woodfine profile image
Gary Woodfine • Edited

Firstly, I think you're completely misunderstanding of the Lambda, in that you have taken the meaning of Function as a literal term, to mean that a Lambda should be a Simple 1 Function. In reality, Lambdas provide Functionality as a Service.

Typically may need the interactivity of Several classes or components. Also when you get further down the road in Micro-Services architecture and using Lambda's you may need to make use of Shared components which are typically stored in Layers, so you can make use of them in several Lambdas.

IOC and DI are also vitally important when it comes TDD, because after all you should be Unit testing your Lambdas after all. SOLID principles also dictate that you shouldn't bury complex functionality in 1 Function, that is just bad code and design.

Lambdas are also typically used to migrate Services and daemons to CLoud Architecture, often Daemons abstract complex functionality, after all if it wasn't complex why would you need to wrap it into a service?

Collapse
 
jamsidedown profile image
Rob Anderson • Edited

How would a lambda like this be unit tested?

Say I want to use a mocked ILambdaConfiguration instance; with the DI set-up inside the class using a private method, there's no easy way to mock the configuration and test behaviour.

Thread Thread
 
gary_woodfine profile image
Gary Woodfine

Not sure I a entirely understand your question.

You can change the method to public if you want too! There is no hard approach to this. This is only an approach or pattern to use.

Besides all the classes or dependencies that would be wired up in the private method would more than likely already be unit tested.

Essentially you're just wiring up the dependencies, but if you want to unit test you've wired up your dependencies correctly then by all means change the method to public or internal and make your internals visible to your test projects.

Thread Thread
 
jamsidedown profile image
Rob Anderson

You mentioned that DI is "vitally important" to unit testing, but given that the lambda is responsible for setting up its own service provider, there's no easy way to pass a mocked ILambdaConfiguration instance into this class.

In a monolithic application, you'd normally rely on a global service provider, and a controller constructor would take an interface that could be easily mocked.

Using serverless functions removes the global environment, and I can't see why a lambda having its own internal dependency injection adds any value. It just seems to add bloat.

My question was how would you unit test a serverless function like the one in your example?

Thread Thread
 
yaser profile image
Yaser Al-Najjar

The one thing I've learned after moving to Python, is that you can do testing without mocking... just do it right not "mocked"!

Thread Thread
 
gary_woodfine profile image
Gary Woodfine

Doesn't add bloat at all. Especially when working with layers or shared assemblies.

Introducing concepts like DI actually helps quite a lot with Lambda's.

The sentence is correct, DI is vitally important to Unit Testing.

So for context. I could have a Lambda consisting of several functions listening for events on SQS Queues,

  public class EventParser
    {
        private readonly IServiceProvider _serviceProvider;

        public EventParser(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public EventParser() : this(StartUp.Container.BuildServiceProvider())
        {
        }

        [LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
        public async Task Parse(SQSEvent qEvent, ILambdaContext context)
        {
            foreach (var record in qEvent.Records)
            {
                // DO some thing here
            }
        }
    }

public class OtherEventParser
    {
        private readonly IServiceProvider _serviceProvider;

        public OtherEventParser(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public OtherEventParser() : this(StartUp.Container.BuildServiceProvider())
        {
        }

        [LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
        public async Task Parse(SQSEvent qEvent, ILambdaContext context)
        {
            foreach (var record in qEvent.Records)
            {
                // DO some thing here
            }
        }
    }

My Lambda Configuration may be configured to not only retrieve some settings from the appsettings file but also retrieve environment variables
defined in the serverless.yml

 public class LambdaConfiguration
    {
          public static IConfigurationRoot Configuration => new ConfigurationBuilder()
                   .SetBasePath(Directory.GetCurrentDirectory())
                   .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                   .AddEnvironmentVariables()
                   .Build();
    }

  public class StartUp
    {
        public static IServiceCollection Container => ConfigureServices(LambdaConfiguration.Configuration);

        private static IServiceCollection ConfigureServices(IConfigurationRoot root)
        {
            var services = new ServiceCollection();
            MapConfigurationFactory.Scan<StartUp>();


            services.AddTransient<IStreamReader, DocxStreamReader>();
            services.AddHttpClient<ApiClient>( client =>
            {
                client.BaseAddress = new Uri("http://someurl.com");
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            });

            services.AddTransient<IService<SomeServiceEntity>, SomeService>();
            services.AddTransient<IPostService<OtherServiceEntity>, OtherService>();

            return services;
        }
    }

What restrictions are there in me writing Unit Tests for these Lambda functions ?

Thread Thread
 
gary_woodfine profile image
Gary Woodfine

In Python Mocks are still heavily used and there are a number of Mock frameworks.

I write Lambda's in Python, and I can still implement the same unit testing strategy.

Unit Tests and Mocks are still pretty relevant in most programming frameworks. Engineering and Quality Assurance discipline remains the same irrelevant of software development language used.

Collapse
 
yaser profile image
Yaser Al-Najjar

mean that a Lambda should be a Simple 1 Function

I didn't say that, I said it should have one purpose and should do only one thing.

And as you mentioned, this could be by utilizing different layers.

IOC and DI are also vitally important when it comes TDD

This doesn't change the fact that you can do DI inside the layer (to have a loosely coupled code base) and have it tested without necessarily doing DI in the lambda.

I see the lambda as a facade consumer (where you have a simplified interface to your complex layer).

after all if it wasn't complex why would you need to wrap it into a service?

Of course the example you mentioned wasn't complex, but my problem with DI is when things get hairy you will see lots of dependencies getting injected into a lambda function and that's when you realize "the oops moment".

Thread Thread
 
gary_woodfine profile image
Gary Woodfine • Edited

Define what you mean by Lambda doing only one thing?

The one thing being defined by a lambda could be an entirely complex transaction involving many components. i.e. Responding to a SQS Event, retrieving the message, then taking specific actions on that message making use of the Mediator pattern as part of an AI data pipeline.

Just in that simple sentence there are many dependencies involving many interactions with several components. Some of which may be stored in a layer. Due in part the Size restrictions of lambdas, it's better to make use of layers.

There are many instances where DI and IOC in lambda function are needed. Even though your Function is going to do 1 thing!

We realise no oops moments with our lambdas, primarily because me make use of TDD and IOC and DI.

Instantiate hard references to classes in a Lambda, is madness IMO, we package our Lambdas with nothing more than the interface contracts. However, at run-time these components are wired up to their correspending component class in the layers. Which is made possible by DI.

If we need to update a component in the layer, it only means updating the component in the layer. No need to redeploy the Lambda!

Following your logic, any time we need to make a change to component we need to re-deploy the entire lambda doesn't make sense!

Collapse
 
hagatorn profile image
Hagatorn • Edited

Have you a typo in the example above?

You have a private ILambdaConfiguration property named Configuration, but you seem to try and access it using the LambdaConfiguration.Configuration["hello"] in the Hello method?
Nice article by the way, I was looking for a way not to instantiate a bunch of shared classes, for example dynamo db clients, in the constructor of my Lambda function and DI is a good alternative. Can find out about layers once I've got my head around the above.

Collapse
 
clwandling profile image
Cory Wandling

Doesn't this run the configuration for every single request? Why would you not run the config once?

Collapse
 
gary_woodfine profile image
Gary Woodfine

Lambda don't send more than one invocation at a time to the same container.

Collapse
 
clwandling profile image
Cory Wandling

I'm shocked, but thank you.

Collapse
 
deexter profile image
deexter

When do you decide to choose (serverless) lambda and when to create API?
I am not sure if it is good idea to have all API endpoint as lambda.

Collapse
 
gary_woodfine profile image
Gary Woodfine • Edited

Going for the consultant answer here, it depends.

It is quite possible to develop a small CRUD based API, using Lambda, which is a fairly common approach. I have attempted to document an approach How To Build Serverless With Netlify, FaunaDB And Gridsome. The concepts etc can be applied to any programming language. Implementing such an approach can be extremely cost effective and help to implement features quickly.

Obviously API's vary in complexity and functionality. So it would entirely depend on the objectives of and use case of API.