45

I have created Lambda functions using AWS.Net SDK, .net core version 1.0. I want to implement dependency injection. Since lambda functions triggered and run independently in AWS environment, there is no such class like Startup present. How and Where can I configure my containers to achieve this implementation?

Yaduraj
  • 605
  • 1
  • 7
  • 11

6 Answers6

55

I know I am way late to the game but I am adding this because I believe there are some bad/lacking examples out on the internet. @Erndob is right about the accepted answer. You'll just be creating more instances.

Depending on what registrations you are making in your DI container you need to keep in mind:

  1. What registrations are you making that implement IDisposable
  2. How long does AWS keep the instance of your object around. I have not been able to find any documentation on this.

Ended up going with something like this:

public class Function
{
    private ServiceCollection _serviceCollection;

    public Function()
    {
        ConfigureServices();
    }

    public string FunctionHandler(string input, ILambdaContext context)
    {
        using (ServiceProvider serviceProvider = _serviceCollection.BuildServiceProvider())
        {
            // entry to run app.
            return serviceProvider.GetService<App>().Run(input);
        }
    }

    private void ConfigureServices()
    {
        // add dependencies here
        _serviceCollection = new ServiceCollection();
        _serviceCollection.AddTransient<App>();
    }
}

With this pattern each lambda invocation will get a new ServiceProvider and dispose of it when finished.

Chris Dargis
  • 5,891
  • 4
  • 39
  • 63
  • Thank you to much chris, It's never too late. Accepted answer solves the DI problem but definitely we should also consider the singleton issues. So I will take this as the extension of that answer. Up-voting it. – Yaduraj Sep 23 '19 at 07:13
  • How would you test this in a unit test? How could we mock the `ServiceCollection`? – Jose Mar 18 '21 at 14:33
  • 1
    Ok I answered my own question! I just add another constructor that takes in a mocked `ServiceCollection`. – Jose Mar 18 '21 at 14:36
  • Thanks a lot, this really helps me improving the project a lot! Regards – pncsoares Feb 23 '22 at 10:25
  • 3
    This answer still could be improved as it builds DI on every invocation which doesn't let lambda do reuse of saved container context of aws lambda. I think we should only rebuild the container if its null. – A.Learn Mar 22 '22 at 09:11
  • 1
    @A.Learn incorrect. The constructor is only called once when the process first instantiates the type. The constructor is not called per invocation. – Chris Dargis Jun 03 '22 at 20:01
  • 1
    @A.Learn is actually right, the current code builds a new `ServiceProvider` on each invocation, though it reuses the ServiceCollection. That means that objects with a Singleton lifecycle aren't possible with this setup. – Nino van der Mark Oct 21 '22 at 09:25
  • @ChrisDargis - thanks for the write-up on this implementation. This is very useful. – GunWanderer Oct 22 '22 at 13:44
  • `That means that objects with a Singleton lifecycle aren't possible with this setup` - why not? – JobaDiniz Feb 26 '23 at 14:04
21

While the FunctionHandler is indeed your entry point to your application, I would actually wire up your DI in a parameterless constructor. The constructor only ever gets called once, so this purely "setup" code should really only need to be called once. We just want to take advantage of using it in every subsequent invocation that gets routed to the same container.

public class Function
{
    private static ServiceProvider ServiceProvider { get; set; }

    /// <summary>
    /// The parameterless constructor is what Lambda uses to construct your instance the first time.
    /// It will only ever be called once for the lifetime of the container that it's running on.
    /// We want to build our ServiceProvider once, and then use the same provider in all subsequent 
    /// Lambda invocations. This makes things like using local MemoryCache techniques viable (Just 
    /// remember that you can never count on a locally cached item to be there!)
    /// </summary>
    public Function()
    {
        var services = new ServiceCollection();
        ConfigureServices(services);
        ServiceProvider = services.BuildServiceProvider();
    }

    public async Task FunctionHandler(SQSEvent evnt, ILambdaContext context)
    {
        await ServiceProvider.GetService<App>().Run(evnt);
    }

    /// <summary>
    /// Configure whatever dependency injection you like here
    /// </summary>
    /// <param name="services"></param>
    private static void ConfigureServices(IServiceCollection services)
    {
        // add dependencies here ex: Logging, IMemoryCache, Interface mapping to concrete class, etc...

        // add a hook to your class that will actually do the application logic
        services.AddTransient<App>();
    }

    /// <summary>
    /// Since we don't want to dispose of the ServiceProvider in the FunctionHandler, we will
    /// at least try to clean up after ourselves in the destructor for the class.
    /// </summary>
    ~Function()
    {
        ServiceProvider.Dispose();
    }
}

public class App
{
    public async Task Run(SQSEvent evnt)
    {
        // actual business logic goes here
        await Task.CompletedTask;
    }
}
Tim Robinson
  • 445
  • 3
  • 7
  • 3
    Make the constructor static and this works. Now you have a static service provider that is shared between instances, but if AWS creates a new instance on the same micro-vm it will override the service collection again. – Erndob Apr 26 '19 at 09:23
  • 5
    With current implementation, your Scoped services will be created the same way as Singleton services (on each invocation of lambda function), because you are not controlling the scope of ServiceProvider. To address this, `FunctionHandler` should start with: ` using var scope = ServiceProvider.CreateScope(); scope.ServiceProvider.GetRequiredService().Run(); //rest of the code below ` – Valera Mar 08 '21 at 16:37
  • So the constructor of your Function is only called once during a cold start, is that right? – Jim Aho May 04 '21 at 14:38
  • @Valera is right. Also want to add that without the scope all transient disposable objects will not be disposed – Vladimir Makarov Nov 09 '21 at 11:21
18

You can do this. Your FunctionHandler is your entry point to your application.. so you have to wire up the service collection from there.

public class Function
{
    public string FunctionHandler(string input, ILambdaContext context)
    {
        var serviceCollection = new ServiceCollection();
        ConfigureServices(serviceCollection);

        // create service provider
        var serviceProvider = serviceCollection.BuildServiceProvider();

        // entry to run app.
        return serviceProvider.GetService<App>().Run(input);
    }

    private static void ConfigureServices(IServiceCollection serviceCollection)
    {
        // add dependencies here

        // here is where you're adding the actual application logic to the collection
        serviceCollection.AddTransient<App>();
    }
}

public class App
{
    // if you put a constructor here with arguments that are wired up in your services collection, they will be injected.

    public string Run(string input)
    {
        return "This is a test";
    }
}

If you want to wire up logging, have a look here: https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Logging.AspNetCore

Donuts
  • 2,185
  • 3
  • 20
  • 31
  • 2
    Helpful post @Donuts - thanks. In your example, it may help others to show how you execute "App::Run" from the ConfigureServices. e.g. `serviceCollection.BuildServiceProvider().GetService().Run()` – Aaron Hudon Jun 13 '18 at 19:36
  • 21
    This is incorrect and dangerous. You will create a new service collection on each request. So your "Singletons" won't be actual singletons that are shared between requests. They will be singletons only in the scope of the request itself. And under heavy loads, if you use certain type of resources, it can have very big consequences. – Erndob Apr 26 '19 at 09:20
  • 1
    @Erndob Thanks for pointing it out. Chris Dargis has refactored it and I think that we should follow while implementing. – Yaduraj Sep 23 '19 at 07:10
  • Thanks a lot, this really helps me improving the project a lot! Regards – pncsoares Feb 23 '22 at 10:25
4

With the regular Lambda projects template, it seems that Scoped lifetime app didn't exist, you had to create workarounds as the answers above.

With new ".NET Annotations Lambda Framework", AWS is trying to solve that issue.

Here the explanation: https://aws.amazon.com/blogs/developer/introducing-net-annotations-lambda-framework-preview/

  • 1
    As one of the authors of this library I would appreciate any feedback on it. https://github.com/aws/aws-lambda-dotnet – Norm Johanson May 10 '22 at 04:55
  • While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - [From Review](/review/late-answers/31733817) – Kuro Neko May 14 '22 at 08:01
  • 1
    @NormJohanson I'd like to be able to take advantage of the DI wiring without the need for any CloudFormation tooling since I'm using the AWS CDK to handle that. Am I locked into using CF with Annotations? – Dave Black Oct 10 '22 at 21:39
  • 1
    For now to use the library it does require CloudFormation. We would like to expand it to support other use deployment scenarios then CloudFormation. – Norm Johanson Oct 12 '22 at 05:07
2

Here is another way to handle it. This is a simplified version of some code from here https://aws.amazon.com/blogs/compute/migrating-a-monolithic-net-rest-api-to-aws-lambda/. One notable difference is that BuildServiceProvider() is only called once when the Lambda is created.

public class Function
{
    private static IServiceProvider services;
    private readonly IBookingRepository bookingRepository;
    private readonly ILogger<Function> logger;

    public Function()
    {
        ConfigureServices();
        
        this.bookingRepository = services.GetRequiredService<IBookingRepository>();
        this.logger = services.GetRequiredService<ILogger<Function>>();
    }

    public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
    {
        if (!apigProxyEvent.PathParameters.ContainsKey("customerId"))
        {
            return new APIGatewayProxyResponse
            {
                StatusCode = 400,
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };
        }

        var customerId = apigProxyEvent.PathParameters["customerId"];

        this.logger.LogInformation($"Received request to list bookings for: {customerId}");

        var customerBookings = await this.bookingRepository.ListForCustomer(customerId);

        return new APIGatewayProxyResponse
        {
            Body = JsonSerializer.Serialize(customerBookings),
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    private void ConfigureServices()
    {
        // Add dependencies here.
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddDbContext<BookingContext>(options =>
            options.UseMySQL("ConnectionString..."));
        serviceCollection.AddTransient<IBookingRepository, BookingRepository>();
        serviceCollection.AddLogging(logging =>
        {
            logging.AddLambdaLogger();
            logging.SetMinimumLevel(LogLevel.Debug);
        });
        services = serviceCollection.BuildServiceProvider();
    }
}
phillhutt
  • 183
  • 1
  • 5
1

Based on the comments of the answers above, shouldn't the following be the correct answer?

public class Function
{
    private readonly ServiceProvider serviceProvider;

    public Function()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddTransient<FunctionHandler>(); //your 'app' or actual handler
        //add your services...
        //serviceCollection.AddSingleton(...);
        //serviceCollection.AddScoped(...);

        serviceProvider = serviceCollection.BuildServiceProvider();
    }

    public Task<APIGatewayProxyResponse> Handler(APIGatewayProxyRequest request, ILambdaContext context)
    {
        //create a scope for each function execution, respecting the DI singletons and scoped configurations
        using var scope = serviceProvider.CreateScope();
        return scope.ServiceProvider.GetService<FunctionHandler>()!.Run(request, context);
    }

    ~Function()
    {
        serviceProvider.Dispose();
    }
}
JobaDiniz
  • 862
  • 1
  • 14
  • 32