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

Design Doc: Lambda Annotation

Open normj opened this issue 2 years ago • 51 comments

The AWS .NET team is working on a new library for constructing .NET Lambda functions. The design doc can be viewed and commented on in this PR https://github.com/aws/aws-lambda-dotnet/pull/961.

The high level overview of the new library is to use .NET source generator support to translating from the low level single event object programming model of Lambda to an experience similar to ASP.NET Core but with minimal overhead. The initial work is focused on REST API Lambda functions but the technology is applicable for other Lambda event types.

For example developers will be able to write a Lambda function like the following with the ICalculator service being injected by dependency injection and and the LambdaFunction and HttpApi attributes directing the source generator to generator the compatible Lambda boiler plate code and sync with the CloudFormation template.

public class Functions
{
	ICalculator _calulator;

	public Functions(ICalculator calculator)
	{
		_calulator = calculator;
	}

	[LambdaFunction]
	[HttpApi(HttpMethod.Get, HttpApiVersion.V2, "/add/{x}/{y}")]
	public int Add(int x, int y)
	{
		return _calulator.Add(x, y);
	}
}

We would appreciate feedback on the design. What use cases what you like this library to help solve and what boiler plate code can we remove from your applications to make it simple to write Lambda functions.


Update 12/21/2021

We are excited to announce that Lambda Annotations first preview is here. Developers using the preview version of Amazon.Lambda.Annotations NuGet package can start using the simplified programming model to write REST and HTTP API Lambda functions on .NET 6. (Only Image package type is supported as of now)

Example (sample project)

[LambdaFunction(Name = "CalculatorAdd", PackageType = LambdaPackageType.Image)]
[RestApi(HttpMethod.Get, "/Calculator/Add/{x}/{y}")]
public int Add(int x, int y, [FromServices]ICalculatorService calculatorService)
{
    return calculatorService.Add(x, y);
}
[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<ICalculatorService, CalculatorService>();
    }
}

Learn more about the Lambda Annotations and supported feature set here.

We cannot overstate how critical your feedback is as we move forward in the development. Let us know your experience working with Lambda Annotations in GitHub issues.

normj avatar Nov 17 '21 01:11 normj

Dependency Injection is a huge plus, to me. I've had to roll that myself to support it - same with logging and configuration / environmental configuration.

EDIT: Why confused, @normj ?

CalvinAllen avatar Nov 17 '21 01:11 CalvinAllen

Source generators is not dotnet, but csharp technology, so will be applicable only to csharp, with fsharp it won't be usable.

Lanayx avatar Nov 17 '21 07:11 Lanayx

Would it also generate the code needed for manual invocation via Lambda Test Tool?

 public static async Task Main()
  {
      var func = new Startup().FunctionHandler;
      using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(func, new DefaultLambdaJsonSerializer());
      using var bootstrap = new LambdaBootstrap(handlerWrapper);
      await bootstrap.RunAsync();
  }

mtschneiders avatar Nov 17 '21 16:11 mtschneiders

Dependency Injection is a huge plus, to me. I've had to roll that myself to support it - same with logging and configuration / environmental configuration.

EDIT: Why confused, @normj ?

@CalvinAllen Sorry clicked the wrong emoji

normj avatar Nov 17 '21 17:11 normj

@normj Ha, okay. I was a little confused myself, trying to figure out what I said, lol.

CalvinAllen avatar Nov 17 '21 17:11 CalvinAllen

It would be great to see more of the plumbing, which happened to be the weakest point of Lambda development.

As @CalvinAllen wrote, logging, configuration and service registration are all concerns that required some time to work in a vanilla function.

But I generally love the idea of seeing multi-function packages becoming first class citizen of the new toolkit.

Kralizek avatar Nov 17 '21 17:11 Kralizek

why is this being introduced ? what problems it solves ? what are expected benefits over the current approach? what are the downsides ?

petarrepac avatar Nov 17 '21 18:11 petarrepac

why is this being introduced ? what problems it solves ? what are expected benefits over the current approach? what are the downsides ?

@petarrepac Are you saying these questions are not answered in the design doc?

normj avatar Nov 18 '21 07:11 normj

It would be great to design DI container setup in such a way so it would be possible to replace some dependencies for testing after initial container setup. I mean not unit tests, but more like functional tests, similar to ASP.NET Core's WebApplicationFactory. https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0

Dreamescaper avatar Nov 18 '21 11:11 Dreamescaper

Would it be possible to customize parsing the request somehow? For example, if I have not a JSON body, but application/x-www-form-urlencoded ?

Another question - would it be possible to add [From...] attributes not on method parameters, but on type properties? So I'd be able to have single MyEndpointRequest parameter, and add [FromQuery], [FromHeader], etc on corresponding properties in MyEndpointRequest type?

Dreamescaper avatar Nov 18 '21 11:11 Dreamescaper

why is this being introduced ? what problems it solves ? what are expected benefits over the current approach? what are the downsides ?

@petarrepac Are you saying these questions are not answered in the design doc?

took a look at one of our latest projects. Found this:

public async Task FunctionHandlerAsync(Stream stream, ILambdaContext context)

and also this

namespace MyProject.Api.Http
{
    public class LambdaEntryPoint : ApplicationLoadBalancerFunction
    {
        protected override void Init(IWebHostBuilder builder)
        {
            builder.ConfigureLogging((hostingContext, logging) =>
                {
                    logging.SetMinimumLevel(LogLevel.Warning);
                })
                .UseStartup<Startup>();
        }
    }
}

This are the entry points to 2 different lambdas. The 2nd one being a full REST API with many endpoints.

The nice thing is that only this code is "lambda specific". Everything else is the same code as you would have when running as a standalone app. So, you can run it locally, run integration tests, and so on.

The new approach seems more opinionated (for example CloudFormation and ApiGateway are mentioned, we use CDK and ALB instead). So, in REST API case what is the benefit? The amount of code is already very small. Is it faster with cold starts? Will the current way still work or it will be left behind going forward?

petarrepac avatar Nov 18 '21 17:11 petarrepac

Would it be possible to customize parsing the request somehow? For example, if I have not a JSON body, but application/x-www-form-urlencoded ?

Another question - would it be possible to add [From...] attributes not on method parameters, but on type properties? So I'd be able to have single MyEndpointRequest parameter, and add [FromQuery], [FromHeader], etc on corresponding properties in MyEndpointRequest type?

That's a cool idea, thanks for the feedback.

ganeshnj avatar Nov 18 '21 17:11 ganeshnj

@petarrepac The approach of running ASP.NET Core application's as Lambda functions will still be fully supported. I even did some work to support ASP.NET Core .NET 6 minimal style although that will be more useful once we get the .NET 6 managed runtime out. The ASP.NET Core approach does make development more familiar and keeps your code abstracted from Lambda.

The con of using that approach is there is a slower cold start due to initializing ASP.NET Core and you kind of miss out some of the API Gateway features because you are relying on ASP.NET Core to do the work. That isn't an issue for you because you are using ALB. We also want this new library to support more than just REST API scenarios even through that is our initial focus.

So if you are happy with ASP.NET Core approach keeping doing and we will continue to support you with it. This library is for developers that want something lighter and covers more scenarios than REST API.

normj avatar Nov 19 '21 03:11 normj

Thanks. I'm more clear on this development now.

petarrepac avatar Nov 19 '21 16:11 petarrepac

Would it be possible to provide a fallback to reflection to make this API available for any .NET language? As described in design doc performance isn't the only goal. Experience of writing Lambda functions would still be improved though at cost of worse cold start.

BoundedChenn31 avatar Nov 22 '21 18:11 BoundedChenn31

@Lanayx You are right. We are using a C# specific technology. I'll update the doc to reflect that. I have wondered if F# type providers could provide a similar experience.

@BoundedChenn31 I'm not sure how reflection would help. The Lambda programming model isn't changing. Lambda is still executing a parameterless constructor, it an instance method, and then invoking the method that takes in the event object. If we don't have the source generator to generate the translation layer from the Lambda programming model to this libraries programming model at compile time then both the Lambda programming model and the this library programming model have to be coded. Also at compile time is the syncing with CloudFormation that couldn't be done at runtime with reflection. Is F# your main concern when it comes to other .NET languages?

normj avatar Nov 22 '21 21:11 normj

@normj Ah, sorry, I overlooked these implementation details. In this case reflection doesn't really make sense. And yes, I'm mostly worried about F#. Well, in worst scenario F#-community can come up with alternative implementation using it's own source generation tool — Myriad 😊

BoundedChenn31 avatar Nov 22 '21 23:11 BoundedChenn31

Would it also generate the code needed for manual invocation via Lambda Test Tool?

 public static async Task Main()
  {
      var func = new Startup().FunctionHandler;
      using var handlerWrapper = HandlerWrapper.GetHandlerWrapper(func, new DefaultLambdaJsonSerializer());
      using var bootstrap = new LambdaBootstrap(handlerWrapper);
      await bootstrap.RunAsync();
  }

Was this question answered? I'm extremely bullish on this, but would want to make sure there's still some way to locally invoke and/or E2E test functions.

Edit: on the note of minimal apis, is there a world where you can do something like the below? Pardon if should be a separate discussion or thread.

var lambda = AwsLambdaBuilder.Create(args);

lambda.HandleApiGatewayV2(async (event) => {
  // do function stuff
 return obj;
});

lambda.Run();

Would be a neat way to have sorta parity with JavaScript while not having to opt too deeply into the CDK.

RichiCoder1 avatar Dec 13 '21 03:12 RichiCoder1

This is interesting, I thought about doing something similar, generating pulumi C# apigateway code based on attributes on a function.

DerekBeattieCG avatar Dec 13 '21 14:12 DerekBeattieCG

@RichiCoder1 The CloudFormation template will still have the correct function handler value for the source generated method. So SAM and the .NET Lambda Test Tool would work just as they do today.

normj avatar Dec 13 '21 21:12 normj

We are excited to announce that Lambda Annotations first preview is here. Developers using the preview version of Amazon.Lambda.Annotations NuGet package can start using the simplified programming model to write REST and HTTP API Lambda functions on .NET 6. (Only Image package type is supported as of now)

Example (sample project)

[LambdaFunction(Name = "CalculatorAdd", PackageType = LambdaPackageType.Image)]
[RestApi(HttpMethod.Get, "/Calculator/Add/{x}/{y}")]
public int Add(int x, int y, [FromServices]ICalculatorService calculatorService)
{
    return calculatorService.Add(x, y);
}
[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<ICalculatorService, CalculatorService>();
    }
}

Learn more about the Lambda Annotations and supported feature set here.

We cannot overstate how critical your feedback is as we move forward in the development. Let us know your experience working with Lambda Annotations in GitHub issues.

ganeshnj avatar Dec 22 '21 04:12 ganeshnj

For many of our current workloads, we run the same code locally and in Lambda. Our clients can then run either on an on-premise machine or in the cloud (e.g. via a tablet).

Is this going to run in both environments or Lambda only?

Thanks

genifycom avatar Dec 23 '21 20:12 genifycom

It depends on the how you are calling the target method in your local environment.

For example an attributed Lambda Function

[LambdaFunction(Name = "SimpleCalculatorAdd", PackageType = LambdaPackageType.Image)]
[RestApi(HttpMethod.Get, "/SimpleCalculator/Add")]
public int Add([FromQuery]int x, [FromQuery]int y)
{
    return _simpleCalculatorService.Add(x, y);
}

The generated code is https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SimpleCalculator_Add_Generated.g.cs

In short, you can write tests, local debug using Lambda Test Tool or instantiate (your class or generated class) & call the target method.

In short, yes assuming your local environment have ability to instantiate and call.

ganeshnj avatar Dec 23 '21 20:12 ganeshnj

Will there be a way to customize the Configuration section like it seems to be possible to customize the services?

Kralizek avatar Dec 23 '21 23:12 Kralizek

@ganeshnj I see you've been working on the lambda annotations to build the serverless.template. Trying to figure out where we'd be able to inject environment variables for which stage it is in (i.e. prod, staging, dev) so that we can control which dynamodb tables we connect to.

Any guidance here?

lukeemery avatar Jan 01 '22 16:01 lukeemery

@Kralizek If understood it correctly, we plan to support Startup.Configure method which will be used to configure the request pipeline.

ganeshnj avatar Jan 01 '22 20:01 ganeshnj

@lukeemery serverless.template writer only updates the configuration that are updatable from C# code, rest of the of the configuration is not altered. Anything added outside the scope, will stay as part of template.

You can setup environment variable using existing Environment property

"Environment": {
  "Variables": {
    "Foo": "Bar"
  }
}

ganeshnj avatar Jan 01 '22 20:01 ganeshnj

That's great to hear but I was referring to the steps necessary to customize the IConfigurationBuilder.

Use case:

  • I want to pick up values from the parameter store and/or secrets manager
  • I want to load some values from a file stored in S3

Kralizek avatar Jan 01 '22 20:01 Kralizek

I see what you mean. I have not tested it myself but this should still work. It can go to Startup.ConfigureServices method.

ganeshnj avatar Jan 01 '22 20:01 ganeshnj

I'm not sure I can see how I would be able to customize the IConfigurationBuilder from the ConfigureServices method you suggest.

Kralizek avatar Jan 01 '22 21:01 Kralizek