Skip to content

Feature Request: Dependency Injection via constructor for handler classes #882

@Itamaram

Description

@Itamaram

At the moment dependency injection is not available out of the box for lambdas. The handler must either be a method of a class with an empty constructor, or of a static class. This leads to patterns which are not in line with modern C# code.
I believe this should be rectified by changing the way the handler class is being instantiated, allowing for dependency injection via constructor arguments.

Describe the Feature

An extension point for the user to register their dependencies (with their corresponding lifecycle) will be exposed, allowing the user to configure the application. When the handler class is instantiated, instead of using Activator.CreateInstance for the generation for the instantiation, a configuration aware factory will be used. Leveraging dotnet's IServiceProvider interface seems like the most idiomatic approach.

Is your Feature Request related to a problem?

With the prevalence of standardized DI in all modern dotnet core programming paradigms, not being able to follow standardized practices when developing lambdas could be a deterrent to developers.

Proposed Solution

My proposed solution is to change the lambda runtime to allow the user to designate a factory for an instance of IServiceProvider, which will then be used to instantiate the handler classes. This can be achieved via convention, an interface or an attribute.

Here's a sample implementation:
First we define an assembly attribute LambdaServiceProviderAttribute that points to a type implementing IServiceProvider. This belongs to the Amazon.Lambda.Core dll:

    [AttributeUsage(AttributeTargets.Assembly)]
    public class LambdaServiceProviderAttribute : Attribute
    {
        public LambdaServiceProviderAttribute(Type type)
        {
            Type = type;
        }

        public Type Type { get; }
    }

Then, in the Amazon.Lambda.RunetimeSupport dll we modify the method UserCodeLoader.Init to get the type from the global attribute and instantiate it, so that we may use it for resolving our handler class:

        public void Init(Action<string> customerLoggingAction)
        {
            ...
-            var customerObject = GetCustomerObject(customerType);
+            var attr = customerAssembly.GetCustomAttribute<LambdaServiceProviderAttribute>();
+            var services = attr != null ? (IServiceProvider) GetCustomerObject(attr.Type, null) : null;          
+            var customerObject = GetCustomerObject(customerType, services);
            ...

and UserCodeLoader.GetCustomerObject to potentially accept an IServiceProvider and use it if it is present:

-        private object GetCustomerObject(Type customerType)
+        private object GetCustomerObject(Type customerType, IServiceProvider services)
        {
            ...
 
-            return Activator.CreateInstance(customerType);
+            return services?.GetService(customerType) ?? Activator.CreateInstance(customerType);
        }

A consumer can then trivially add to their lambda code a snippet such as:

[assembly: LambdaServiceProvider(typeof(SampleServiceProvider))]

public class SampleServiceProvider : IServiceProvider
{
    private readonly ServiceProvider services;

    public SampleServiceProvider()
    {
        services = new ServiceCollection()
            .AddSingleton<Handler>()
            .AddSingleton(new Foo("hello world"))
            .BuildServiceProvider();
    }
    
    public object GetService(Type serviceType) => services.GetService(serviceType);
}

As noted above, any variety of ways can be used to allow the user to use this extension point, assembly attribute is just a suggestion which goes in line with precedents such as LambdaSerializer.

Describe alternatives you've considered

There are a couple of possible approaches. One is to instantiate a DI resolution root (ie ServiceCollection) in the handler's argument-less constructor, and then use it to resolve your dependencies. This is the suggestion currently provided by google when searching for this. As an extension to it, there are a couple of libraries wrapping this in a slightly nicer way such as Kralizek Lambda Template and Tiger-Lambda, but they're effectively all functionally equivalent, and all causing unnecessary boilerplate code.

The second option is to deploy a self-contained lambda. By using lower level methods from Amazon.Lambda.RuntimeSupport, the user is able to bootstrap the lambda with an already resolved delegate through an external pipeline. This feels a little like a sledgehammer approach, customizing the entire runetime for a single minor concern.

Additional Context

Coming to lambda development from Azure Functions, the first thing I tried doing was to enable dependency injection. When I was unable to find satisfactory answers online, I was frustrated as it seemed completely unreasonable for me that this feature was missing. I thought it was more likely I couldn't find a solution, than that one didn't exist.
As someone with a considerable experience in the dotnet space, I would personally classify this feature missing as "unreasonable".

I believe that most users would need this feature, and are currently implementing some sort of a workaround to achieve it, potentially causing other issues in the process. It would be highly beneficial for the ecosystem for an idiomatic approach to not only exist, but for it to be actively supported and advocated for by the vendor.

Environment

This feature is applicable to all lambdas running dotnet code.

  • [✅] 👋 I may be able to implement this feature request
  • [❎ ] ⚠️ This feature might incur a breaking change

This is a 🚀 Feature Request

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions