aws-lambda-dotnet icon indicating copy to clipboard operation
aws-lambda-dotnet copied to clipboard

Feature Request: Dependency Injection via constructor for handler classes

Open Itamaram opened this issue 4 years ago • 13 comments

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.

  • [✅] :wave: I may be able to implement this feature request
  • [❎ ] :warning: This feature might incur a breaking change

This is a :rocket: Feature Request

Itamaram avatar Jun 22 '21 12:06 Itamaram

As author of the aforementioned Kralizek Lambda Template, I'd love if there was a built-in approach for defining lambda functions without the need of external libraries :)

Kralizek avatar Jun 22 '21 12:06 Kralizek

I would definitely like to see this - I feel it may be enabled by implementing https://github.com/aws/aws-lambda-dotnet/issues/709

acraven avatar Sep 25 '21 20:09 acraven

Possibly a breaking change. Also refer https://github.com/aws/aws-lambda-dotnet/issues/800#issuecomment-770014058 for workaround.

This needs review with the team.

ashishdhingra avatar Jan 28 '22 17:01 ashishdhingra

I disagree with this being a breaking change. There are many ways in which this can be implemented which would be very much backwards compatible, I've even proposed one above.

Itamaram avatar Jan 28 '22 20:01 Itamaram

@normj It would be good to get an AWS response on this. I would like to take advantage of dotnet Core 6 startup code to initialize a lambda that continues to be loaded (Provisioned Concurrency) then then process subsequent events from Kinesis but this seem to only be implementable as stranded Function Handler.

Simonl9l avatar Jun 05 '22 21:06 Simonl9l

The problem with baking this logic directly into Amazon.Lambda.RuntimeSupport is the dependency injection packages are not part of the base class library (BCL) unless your project using the Microsoft.NET.Sdk.Web SDK like an ASP.NET Core stack. For Amazon.Lambda.RuntimeSupport to implement it directly it would have to take a dependency on Microsoft.Extensions.DependencyInjection. Whenever Amazon.Lambda.RuntimeSupport takes dependency it causes havoc with the dependencies functions take in.

We are working on a new library called Amazon.Lambda.Annotations that has support for .NET dependency injection. The library is currently in preview and here is the design doc for it. https://github.com/aws/aws-lambda-dotnet/issues/979. It uses the Startup class pattern where you can configure services and uses .NET source generators to generate the correct code needed to match constructor or functional handler parameter requirements. I think that should meet the original authors intent, I would love to hear feedback where we need to improve that library to solve more use cases.

normj avatar Jun 05 '22 23:06 normj

Wow. Okay. I didn't realise all I had to do to get a response was to tag @normj, would've done that a year ago if I knew that was the case.

Avoiding adding Microsoft.Extensions.DependencyInjection as a dependency is a reasonable restriction, and the proposed Amazon.Lambda.Annotations lib is exactly what we are after. The auto generation of code from attributes is also something we've tackled internally, and having an official solution would be a huge timesaver.

There are a couple of points I would like to raise:

  • We are currently using the serverless framework, can we expect support for code generation from attribute for it?
  • It appears this initiative has been on going for a while ( > 6 months). I would love to contribute to this project to ensure its timely progress. Are external contributions welcome at this time?

Itamaram avatar Jun 06 '22 00:06 Itamaram

Ha @Itamaram, I do try and engage as much as I can with GitHub issues but I know I'm far from perfect.

External contributions are very welcome on Amazon.Lambda.Annotations. I would suggest opening a feature request first so we can discuss the feature to make sure it fits before you spend a lot of time on it. Feel free to tag me in the feature request 😄

Our GA requirement is both JSON and YAML support for CloudFormation and I would also like to have it generate a JSON metadata file that contains all of the generated function handler strings that could be set in other frameworks like CDK and Serverless Framework. I don't think we want to directly support generating Serverless Framework templates at least till we get some of the other table stacks done. If you wouldn't mind could you add a comment in that main tracking issue #979 asking for Serverless Framework template support? I'm curious what how many others would like that. With GA you should be able use annotations with serverless to take advantage of the dependency injection and other code generating attributes but you would have to sync the Serverless Framework template.

normj avatar Jun 06 '22 06:06 normj

@normj Thanks so much for chiming in, (@Itamaram Norm has be v. helpful before so I took a gamble and tagged him! - I also appreciate he being largely the .Net one man show mean he probably pulled in 1001 directions!)

Norm Im trying to come up to speed with al the dotnet 6 changes so apologies if I'm not reading enough first. I will dong into the Amazon.Lambda.Annotations auto be honest Code Generation is far better than runtime DI and it's great y'all are following this approach!

So I planning to use Provisioned Concurrency to have a lambda consume events from Kinesis, unit need to initialize some background services so I it can then process/handle events, and was figuring I could do this with serverless, but assume I can use the Amazon.Lambda.Annotations approach?

Do you have any recommendations in this regard?

Simonl9l avatar Jun 06 '22 17:06 Simonl9l

With Amazon.Lambda.Annotations the Startup class where you configure services you could also start any other background services. That code will run as part of the Provisioned Concurrency initialization stage so by the time it is handling Lambda events the code will already be warmed up.

normj avatar Jun 07 '22 00:06 normj

@normj thanks, it it correct that Amazon.Lambda.Annotations only supports REST events. What's plan on the other Handler types, specifically Kinesis Data Analytics?

Simonl9l avatar Jun 07 '22 01:06 Simonl9l

@Simonl9l You can use Amazon.Lambda.Annotations for any event type by just adding the LambdaFunction attribute. For REST events we have additional attributes you can use to configure the event in code. Otherwise in your case you would configure the Kinesis event type in the CloudFormation template. But you can still use the LambdaFunction and have the DI integration.

normj avatar Jun 07 '22 06:06 normj

@normj thanks again, trying to get for her to a small to us this, with no clue on tooling support or template support for Jetbrains Rider, I put an issue here and hope the Rider Tools support this soon https://github.com/aws/aws-toolkit-jetbrains/issues/3175

in the meantime is there a suitable dotnet new template for this?

Thanks again!

Simonl9l avatar Jun 14 '22 05:06 Simonl9l

@Simonl9l You can run dotnet new serverless.Annotations --output MySampleAnnotationsProject to create a project using Lambda Annotations. This will create a project in a sub directory of the current director called MySampleAnnotationsProject. The directory will contain both a Lambda project using Annotations as well as a unit test project.

Is it ok to close this FR now?

ik130 avatar Nov 17 '22 17:11 ik130

@ik130 not my feature request to agree to close.

I have been doing as suggested and we have multiple lambdas in production albeit build via docker with custom runtime to give AoT support - with .Net 7!

this eliminated the need for provisioned capacity as cold start is v fast. So from my perspective I’m good.

However given AoT we’re wanting to not use the dotnet DI container and use one of the code generation equivalents out there. As such (prob a different feature request) would be a way to make the DI container pluggable.

this then raises other issues with the AWSSDK in general. Many of the .Add<service> implementations use reflection that are not very AoT-able… prob a second additional feature request.

AoT is the future - so more will find this to be an issue!

Simonl9l avatar Nov 17 '22 21:11 Simonl9l

@Simonl9l I agree that I'm imagining more source generators and other library updates in our future to cut our reflection usage and make all of our .NET libraries be more AOT friendly. It will take time but we are all excited about the potential for AOT.

normj avatar Nov 18 '22 07:11 normj

I'm closing this because are direction to better support Microsoft.Extensions.DependencyInjection is our Amazon.Lambda.Annotations framework.

normj avatar Apr 21 '23 21:04 normj

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

github-actions[bot] avatar Apr 21 '23 21:04 github-actions[bot]