AWS Developer Tools Blog
Updates for serverless ASP.NET Core
Elastic Load Balancing recently added support for routing requests from an Application Load Balancer to AWS Lambda functions. This enables developers who are already using an Application Load Balancer to easily add serverless functionality to their existing applications.
Amazon API Gateway also updated the requests and responses sent to Lambda functions to include multivalue support for headers and query string parameters.
The event objects for API Gateway that the Amazon.Lambda.APIGatewayEvents NuGet package provides have been updated, and we’ve released a new Amazon.Lambda.ApplicationLoadBalancerEvents package for requests coming from an Application Load Balancer.
Serverless ASP.NET Core
One of our most popular packages is the Amazon.Lambda.AspNetCoreServer NuGet package, which provides the capabilities of running an ASP.NET Core application as a serverless function. Today we have released version 3.0 of the Amazon.Lambda.AspNetCoreServer package. This version adds support for Application Load Balancers and takes advantage of the new multivalue support that API Gateway provides.
A great feature of the Amazon.Lambda.AspNetCoreServer package is how little Lambda-specific code you have to put in your ASP.NET Core project. For most cases all you need to do is add a simple class, like the following LambdaEntryPoint.cs, to handle requests coming in from API Gateway. The base class APIGatewayProxyFunction provides the FunctionHandlerAsync method that will be the Lambda function handler.
public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
{
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<Startup>();
}
}
To have the project handle an Application Load Balancer request, all you need to do is change the base class of LambdaEntryPoint from APIGatewayProxyFunction to ApplicationLoadBalancerFunction.
public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction
{
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<Startup>();
}
}
Once you change the base class and deploy the Lambda function, you can follow this walkthrough on connecting a Lambda function to an Application Load Balancer. https://thinkwithwp.com/blogs/networking-and-content-delivery/lambda-functions-as-targets-for-application-load-balancers/
To take advantage of multivalue header and query string support, you must enable it on the ELB target group that targets the Lambda function.
For functions fronted by API Gateway, multivalue support is automatically enabled and supported after you update your Lambda functions to the new 3.0 version.
Encoding fix
The 3.0 version of Amazon.Lambda.AspNetCoreServer contains a fix that I wanted to give special attention.
During the process of translating the API Gateway request into an ASP.NET Core request, query string parameters were incorrectly URL encoded. This caused ASP.NET Core controllers to possibly receive values from the query string with encoded characters in the value. This incorrect behavior has been fixed so query string parameters will come into controllers correctly.
If you had been working around this bug in your existing code by decoding the query string values in your controller, when you upgrade to 3.0 you should remove the extra decoding logic. Otherwise, it’s possible to have a value be decoded twice and cause incorrect behavior.
ASP.NET Core path base
ASP.NET Core applications have what is called the path base. It’s what tells ASP.NET Core where in the URI to look when figuring out how to route requests. By default, a serverless ASP.NET Core function treats the path base of a request coming in as either the host (that is, “/”) or API Gateway stage (that is, “/Prod/”).
The reason for the default is you might want to deploy the serverless ASP.NET Core application but have different Lambda, API Gateway, or security settings for different parts of the application.
For example, in this serverless.template, I have one controller named IntenseController that needs extra memory. I make a specific AWS::Serverless::Function routing to that controller with extra memory, and then allow the rest of the requests to fall to the default AWS::Serverless::Function with lower memory.
"Resources" : {
"Intense" : {
"Type" : "AWS::Serverless::Function",
"Properties": {
"Handler": "APIGatewayExample::APIGatewayExample.LambdaEntryPoint::FunctionHandlerAsync",
"Runtime": "dotnetcore2.1",
"CodeUri": "",
"MemorySize": 1024,
"Timeout": 30,
"Policies": [ "AWSLambdaFullAccess" ],
"Events": {
"PutResource": {
"Type": "Api",
"Properties": {
"Path": "/api/intense",
"Method": "ANY"
}
}
}
}
},
"Default" : {
"Type" : "AWS::Serverless::Function",
"Properties": {
"Handler": "APIGatewayExample::APIGatewayExample.LambdaEntryPoint::FunctionHandlerAsync",
"Runtime": "dotnetcore2.1",
"CodeUri": "",
"MemorySize": 256,
"Timeout": 30,
"Policies": [ "AWSLambdaFullAccess" ],
"Events": {
"PutResource": {
"Type": "Api",
"Properties": {
"Path": "/{proxy+}",
"Method": "ANY"
}
}
}
}
}
},
The 2 Lambda functions will use the same API Gateway REST API to expose my ASP.NET Core application. This can reduce my cost by not having every request require the maximum memory that any of my controllers need.
An alternative approach I’ve been asked about is to use a subresource in the resource path to be the path base. For example, if the listener of an Application Load Balancer points to a Lambda target group for requests starting with ‘/webapp/*’, and you want to call a controller ‘api/values’, ASP.NET Core will think the resource you want to access is ‘/webapp/api/values’, which will return a 404 NotFound.
In the ‘LambdaEntryPoint’ class you can override the ‘PostMarshallRequestFeature’ method to add custom logic to how the path base is computed. In the example below, it configures the path base to be ‘/webapp/’. When the Application Load balancer sends in a request with the resource path set to /webapp/api/values, this code configures the ASP.NET Core request to have the path base set to /webapp/ and the path to /api/values.
public class LambdaEntryPoint : ApplicationLoadBalancerFunction
{
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<Startup>();
}
protected override void PostMarshallRequestFeature(IHttpRequestFeature aspNetCoreRequestFeature, ApplicationLoadBalancerRequest lambdaRequest, ILambdaContext lambdaContext)
{
aspNetCoreRequestFeature.PathBase = "/webapp/";
// The minus one is ensure path is always at least set to `/`
aspNetCoreRequestFeature.Path =
aspNetCoreRequestFeature.Path.Substring(aspNetCoreRequestFeature.PathBase.Length - 1);
lambdaContext.Logger.LogLine($"Path: {aspNetCoreRequestFeature.Path}, PathBase: {aspNetCoreRequestFeature.PathBase}");
}
}
Migrating to Amazon.Lambda.AspNetCoreServer 3.0
The code for Amazon.Lambda.AspNetCoreServer, which can be found on GitHub, was refactored significantly to support requests coming from either API Gateway or an Application Load Balancer. This was the reason for the major version bump along with the new Application Load Balancer support.
Other than the encoding fix mentioned previously, you should see no difference in existing applications when migrating to version 3.0. If you had automated tests that were inspecting the JSON or the APIGatewayProxyResponse object returned from the ASP.NET Core function, you also might need to update your asserts to use the MultiValueHeaders property instead of the Headers property.
Conclusion
Now with version 3.0 of Amazon.Lambda.AspNetCoreServer you can use either API Gateway or an Application Load Balancer to expose your serverless ASP.NET Core applications. As always, please feel free to reach out on GitHub, https://github.com/aws/aws-lambda-dotnet for any feedback on our .NET Core Lambda support. I think 2019 is going to be an exciting year for .NET Core and Lambda.
— Norm