Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design Doc: Lambda Annotation #979

Closed
normj opened this issue Nov 17, 2021 · 59 comments
Closed

Design Doc: Lambda Annotation #979

normj opened this issue Nov 17, 2021 · 59 comments

Comments

@normj
Copy link
Member

normj commented Nov 17, 2021

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 #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 normj added guidance Question that needs advice or information. needs-triage This issue or PR still needs to be triaged. labels Nov 17, 2021
@normj normj pinned this issue Nov 17, 2021
@normj normj added Announcement and removed guidance Question that needs advice or information. needs-triage This issue or PR still needs to be triaged. labels Nov 17, 2021
@CalvinAllen
Copy link

CalvinAllen commented Nov 17, 2021

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 ?

@Lanayx
Copy link

Lanayx commented Nov 17, 2021

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

@mtschneiders
Copy link

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();
  }

@normj
Copy link
Member Author

normj commented Nov 17, 2021

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

@CalvinAllen
Copy link

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

@Kralizek
Copy link

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.

@petarrepac
Copy link

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

@normj
Copy link
Member Author

normj commented Nov 18, 2021

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?

@Dreamescaper
Copy link
Contributor

Dreamescaper commented Nov 18, 2021

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
Copy link
Contributor

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?

@petarrepac
Copy link

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?

@ganeshnj
Copy link
Contributor

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.

@normj
Copy link
Member Author

normj commented Nov 19, 2021

@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.

@petarrepac
Copy link

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

@BoundedChenn31
Copy link

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.

@normj
Copy link
Member Author

normj commented Nov 22, 2021

@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?

@BoundedChenn31
Copy link

@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 😊

@RichiCoder1
Copy link

RichiCoder1 commented Dec 13, 2021

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.

@DerekBeattieCG
Copy link

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

@normj
Copy link
Member Author

normj commented Dec 13, 2021

@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.

@ganeshnj
Copy link
Contributor

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.

@genifycom
Copy link

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

@ganeshnj
Copy link
Contributor

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.

@normj
Copy link
Member Author

normj commented Apr 27, 2022

@jaknor The CodeUri properties in the template should be relative to the location of the template file. If that is not what you are seeing then can you file a separate issue so we don't lose that bug in this thread?

@jwarhurst
Copy link

Are you forced into CloudFormation template? I love the built in DI, but would prefer to use CDK.

@normj
Copy link
Member Author

normj commented Apr 28, 2022

We want to support more then CloudFormation. CDK is tricky because then we have to figure out how to generate the CDK code within project, so we might have to make some assumptions. One of things we have thought of is to to generate a metadata JSON file that would contain all of the function handler strings generated. Then you could use that metadata to update your CDK code with the generated function handler strings to tie the source generation with the deployment tech.

@RichiCoder1
Copy link

RichiCoder1 commented Apr 28, 2022

A nice MVP would just be a simple way/package that maps generated handler and function information to something easily consumuble by the CDK. Agree that full on generating CDK code would be very hard, and possibly undesirable as I imagine if you're using the CDK you're doing it for full control.

In theory you could probably do this today by reading the mangled handle and src that are generated from CDK, but it's not a super nice dev UX.

And for the question "why annotations if you're handling everything in CDK", the handler code generation experience is still super nice and desireable from a CDK perspective.

@normj
Copy link
Member Author

normj commented Apr 28, 2022

Agree that Annotations library should be useful users to more then just CloudFormation users. We also want to use the Annotations and source generators to make it easier for coding event handling as well. We have been doing that for API based functions but we should be able to do this for other event types.

I thought if we wrote the metadata in JSON you could read that JSON in your CDK code and do the appropriate work based on that.

@bjorg
Copy link
Contributor

bjorg commented Apr 29, 2022

The problem with CDK is that it is not language agnostic. So, in what language should be used to generated it? If it's C# then you will not be able to compose with most of the other CDK constructs. Sadly, this situation could have been avoided if the CloudFormation team had defined a way to compose templates.

@RichiCoder1
Copy link

RichiCoder1 commented Apr 29, 2022

Agree that Annotations library should be useful users to more then just CloudFormation users. We also want to use the Annotations and source generators to make it easier for coding event handling as well. We have been doing that for API based functions but we should be able to do this for other event types.

I thought if we wrote the metadata in JSON you could read that JSON in your CDK code and do the appropriate work based on that.

That would def be a path forward! It'd be pretty easy to make (and I could PoC as a community contribution) a package which knows how to consume that and pass it to Function from the lambda lib. And maybe do some verification that there are constructs present in the current stack that align with what's attribute'd, if that makes sense.

The problem with CDK is that it is not language agnostic. So, in what language should be used to generated it? If it's C# then you will not be able to compose with most of the other CDK constructs. Sadly, this situation could have been avoided if the CloudFormation team had defined a way to compose templates.

You sorta can with CDK include, but it's pretty hacky for sure.

@cchen-ownit
Copy link

Are there plans for more types of annotations?

This starts to resemble Azure Functions in the best way possible given how easy it is to "snap together" Azure Blob Storage to Azure Functions to Azure CosmosDB to Azure Service Bus, etc.

@Simonl9l
Copy link

@normj et al beyond the CDK there is potentially another similar issue if one wants to deploy the lambda in a docker container as it seems the Dockerfile CMD requires the generated lambda name per the CF template.

@normj
Copy link
Member Author

normj commented Aug 2, 2022

Version 0.6.0-preview has been released. It contains a breaking change to move the API Gateway .NET attributes to a new Amazon.Lambda.Annotations.APIGateway namespace. We wanted to make sure when we add more services that we would not make the root namespace Amazon.Lambda.Annotations too confusing or run into conflicts.

Due to the breaking change and the way source generators work if you update your project you might need to restart Visual Studio after updating. Otherwise the events section in the CloudFormation template might disappear.

Note, we avoid making breaking changes as much as possible but since this is in preview and it is important we set our selves up for the future we think this breaking change is warranted.

There are also a couple other fixes dealing with functions that return null and making sure the CloudFormation is synced when all of the LambdaFunctionAttribute are removed.

@Simonl9l
Copy link

Simonl9l commented Aug 2, 2022

@normj thanks for the update.

For the time being we’re sticking with the dotnet 6 minimal API setup given need for ALB bindings, and related issues below.

We figure it’s minimal effort to switch over to the annotations library as it progresses…

Our main issues as it stand is not the annotations library itself but the tool support either via the lambda test tools - not supporting the lambda lifecycle, or the Rider AWS toolkit support at all in a reliable way we can use access the dev team.

We have issues opened on both.

We’re also interested to see where any CDK integration goes. Including Docker and Extensions support. We’ve been experimenting with Local Stack such that our dev env fully replicates our eventual production deployment stack, and have been able to deploy and test lambdas locally.

We realize you have limited capacity/resources in the dotnet space, but are exited at your continued efforts and your ongoing commitment to make it’s support equivalent of other languages.

@ryanpsm
Copy link

ryanpsm commented Aug 17, 2022

@normj If I want to return different HTTP status codes and error messages depending on the issue that arose during processing the request, is that possible with the Annotations API?

The regular Lambda model has returns such as:

return new APIGatewayHttpApiV2ProxyResponse {
    StatusCode = (int)HttpStatusCode.BadRequest,
    Body = "Failed to process request due to invalid type parameter."
    Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
};

return new APIGatewayHttpApiV2ProxyResponse {
        StatusCode = (int)HttpStatusCode.OK,
        Body = "Successfully sent message.",
        Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
};

And the new version from the examples is as far as I can tell just:

return "Successfully sent message.";

@normj
Copy link
Member Author

normj commented Aug 29, 2022

@ryanpsm With the Annotations library you can still return the APIGatewayHttpApiV2ProxyResponse to customize the status codes and headers returned. I have thought about creating an optional base class that could have the familiar Ok, NotFound and other base methods for a better experience. Curious what others thoughts are on that.

@normj
Copy link
Member Author

normj commented Aug 29, 2022

Version 0.7.0-preview is out that adds YAML support and fixes a code generation issue.

@danielsunnerberg
Copy link

@ryanpsm With the Annotations library you can still return the APIGatewayHttpApiV2ProxyResponse to customize the status codes and headers returned. I have thought about creating an optional base class that could have the familiar Ok, NotFound and other base methods for a better experience. Curious what others thoughts are on that.

This is my exact use-case (related discussion thread). Preferably I'd like to avoid building the response body myself (serialization etc). Perhaps throwing an exception could be an option?

@Simonl9l
Copy link

Simonl9l commented Jan 28, 2023

Per the comments above from @ganeshnj related to application/x-www-form-urlencoded we're currently using the Minimal API and AddAWSLambdaHosting that is fine when debugging as its using Kestrel locally; but fails miserably once we put the Lambda behind and API Gateway and it swaps in the Lambda bootstrapping process.

From an API Gateway perspective it seems a common patterns related to this to the use a Request Transform to convert the Form payload onto a JSON one, however that then makes local debugging a pain as one does not have the transform in place.

We've opened an issue (#1423) more related to AddAWSLambdaHosting in this regard, as It's not clearly documented as to what happens with custom middleware related to the Lambda Hosting, or one could implement the equivalent transform there.

We figured any builtin support in the Annotation library to support this context type more directly would definitely be valuable.

As a related use case the Stipe payments system uses Forms for its hosted Checkout and Customer Portals - as it uses redirects from the web client to achieve the workflow. Surely others either do, or might plan to, host Lambdas to support their Stripe integration if this is more easily supported.

@normj
Copy link
Member Author

normj commented Feb 8, 2023

We released version 0.11.0 of the library today that provides greater control over the response for API Gateway based events. Checkout below for an example and please let us know what you think.

[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/{id}")]
public IHttpResult GetOrder(string id)
{
    var order = FetchOrderInfo(id);
    if(order == null)
    {
        return HttpResults.NotFound()
                            .AddHeader("custom-header", "value");
    }

    return HttpResults.Ok(order);
}

@danielsunnerberg
Copy link

We released version 0.11.0 of the library today that provides greater control over the response for API Gateway based events. Checkout below for an example and please let us know what you think.

[LambdaFunction]
[HttpApi(LambdaHttpMethod.Get, "/{id}")]
public IHttpResult GetOrder(string id)
{
    var order = FetchOrderInfo(id);
    if(order == null)
    {
        return HttpResults.NotFound()
                            .AddHeader("custom-header", "value");
    }

    return HttpResults.Ok(order);
}

Exactly what I was looking for, many thanks!

@NickBittar
Copy link

Really love what you're doing with this library. I noticed in the design document there are mentions of adding middleware and a configure method in the Startup.cs here. I haven't seen it mentioned much outside of this document. I was wondering, is this still a planned feature to add?

@normj
Copy link
Member Author

normj commented Mar 16, 2023

@NickBittar thanks for the feedback. It is still the plan to add middleware support. Not sure if it will be in the 1.0 version because we would really like to get the library past the preview state.

@normj
Copy link
Member Author

normj commented Jul 17, 2023

We released the 1.0 GA version of Lambda Annotations: https://aws.amazon.com/blogs/developer/net-lambda-annotations-framework/

I'm going to close this design issue now that we have reached GA status. This does not mean we are done and there is more feature we want to implement like the middleware abstraction and other events. That work will be tracked in separate issues and PRs.

Please keep the feedback coming and feel free to open issues for feature requests or any issues you run in.

@normj normj closed this as completed Jul 17, 2023
@normj normj unpinned this issue Jul 17, 2023
@github-actions
Copy link
Contributor

⚠️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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests