AWS Developer Tools Blog
One Month Update to .NET Core 3.1 Lambda
One Month Update to .NET Core 3.1 Lambda
About one month ago we released the .NET Core 3.1 Lambda runtime. Since then we have seen a lot excitement for creating new .NET Core 3.1 Lambda functions or porting existing Lambda functions to .NET Core 3.1. We have also received some great feedback and as a result made some updates to our .NET Core Lambda client libraries. In this post I want to talk about these updates, as well as mention a few other features that came out with .NET Core 3.1 Lambda that didn't make it into the original announcement blog post.
Using IHostBuilder for ASP.NET Core 3.1 Lambda functions
The Amazon.Lambda.AspNetCoreServer NuGet package allows ASP.NET Core applications to run as a Lambda function. This library supports both ASP.NET Core 2.1 and ASP.NET Core 3.1. In ASP.NET Core 2.1 the pattern for bootstrapping an ASP.NET Core application is to use an IWebHostBuilder
. You can see this by taking a look at a typical Program.cs
file.
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
For Lambda you usually interact with the IWebHostBuilder
in your LambdaEntryPoint.cs
file, by overriding the Init
method and setting the startup class:
public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
{
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<Startup>();
}
}
For ASP.NET Core 3.1 the pattern shifted to use the more generic host builder, IHostBuilder
, to bootstrap the application. Below you can see how the bootstrapping changed for ASP.NET Core 3.1 using IHostBuilder
. Notice how the IWebHostBuilder is still used as part of the ConfigureWebHostDefaults
call:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
When we launched the .NET Core 3.1 support for Lambda, we received feedback that we needed to switch Amazon.Lambda.AspNetCoreServer
to use the IHostBuilder
pattern. This was important when using libraries like AutoFac, a popular Inversion of Control container, that changed its extension methods to use IHostBuilder
. Starting with version 5.1.0 of Amazon.Lambda.AspNetCoreServer
, when targeting .NET Core 3.1, IHostBuilder
will now be used. The switch to IHostBuilder
will not affect how existing Lambda functions are written. The Init
method to customize the IWebHostBuilder
still exists. An additional Init
method is now available that you override to customize the IHostBuilder
. The example below shows how to use both Init
methods to configure AutoFac on the IHostBuilder
and the startup class on the IWebHostBuilder
:
public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
{
protected override void Init(IHostBuilder builder)
{
builder
.UseServiceProviderFactory(new AutofacServiceProviderFactory());
}
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<Startup>();
}
}
Our approach for this change enabled us to maintain compatibility with ASP.NET Core 2.1 and not affect existing Lambda functions. The base class of the Lambda function has several methods that can be overridden like the Init
method. If your ASP.NET Core 3.1 Lambda function was overriding the CreateWebHostBuilder
method, which is usually done to have complete control how the IWebHostBuilder
is created, then Amazon.Lambda.AspNetCoreServer
will not switch to using IHostBuilder. To have complete control of the bootstrapping and switch to IHostBuilder
, override CreateHostBuilder
instead of overriding CreateWebHostBuilder
.
For more information about how the IHostBuilder
bootstrapping works checkout Amazon.Lambda.AspNetCoreServer’s README file.
JSON casing issues with Amazon.Lambda.Serialization.SystemTextJson
As part of the .NET Core 3.1 Lambda release we released a new JSON serialization library Amazon.Lambda.Serialization.SystemTextJson, which is based on .NET Core's new System.Text.Json
library for parsing JSON. The new serializer gives significant improvements for Lambda cold starts compared to the original Amazon.Lambda.Serialization.Json
library that was based on Newtonsoft.Json.
An issue discovered with Amazon.Lambda.Serialization.SystemTextJson
is that it inconsistently cases JSON properties when serializing .NET objects to JSON. Amazon.Lambda.Serialization.Json
and System.Text.Json
use PascalCase when serializing objects by default. Amazon.Lambda.Serialization.SystemTextJson
incorrectly used camelCase by default. This would affect Lambda functions returning custom response objects, for example in functions used by state machines in AWS Step Functions.
Since we have already shipped Amazon.Lambda.Serialization.SystemTextJson
, changing the LambdaJsonSerializer
class to now use PascalCase might break Lambda functions that have already compensated for the change in casing. We decided instead to create a new class inside Amazon.Lambda.Serialization.SystemTextJson
called DefaultLambdaJsonSerializer
which will act consistently with how System.Text.Json
works by default. That means the casing of the .NET properties in the .NET object is what will be used in the output JSON document. If you want the camelCase behavior you can use CamelCaseLambdaJsonSerializer
instead of DefaultLambdaJsonSerializer
. The LambdaJsonSerializer
class has been marked as Obsolete
and should not be used going forward.
To migrate to the new behavior update your references to Amazon.Lambda.Serialization.SystemTextJson
to version 2.0.0 and update your LambdaSerializer
attribute to use the new DefaultLambdaJsonSerializer
class:
[assembly:LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
Using Amazon API Gateway's HTTP APIs
Amazon API Gateway has announced general availability of the their new HTTP APIs feature. HTTP APIs provide a faster and lower cost alternative to API Gateway's original REST APIs. Check out this post for more details about how API Gateway's HTTP APIs work and how they are different than REST APIs.
The format for the request and responses for HTTP APIs can come in either of 2 formats. The version 1.0 format is the same as REST API but the 2.0 format, which is the default, simplifies the request object. In version 2.0.0 of the NuGet package Amazon.Lambda.APIGatewayEvents we added APIGatewayHttpApiV2ProxyRequest
and APIGatewayHttpApiV2ProxyResponse
classes to represent HTTP API 2.0-formatted requests and responses.
public APIGatewayHttpApiV2ProxyResponse Get(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
ProcessBody(request.Body);
var response = new APIGatewayHttpApiV2ProxyResponse
{
StatusCode = (int)HttpStatusCode.OK,
Body = "Request processed",
Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
};
return response;
}
ASP.NET Core and API Gateway HTTP APIs
The Amazon.Lambda.AspNetCoreServer NuGet package allow you to run your ASP.NET Core projects as a serverless application. Starting with version 5.0.0 of the package you can now configure the project to use API Gateway's HTTP APIs with your ASP.NET Core project. To configure an ASP.NET Core Lambda project to use HTTP APIs there are two changes you have to do. First the LambdaEntryPoint
class, or whichever class you are using that extends from APIGatewayProxyFunction
, needs to have its base class changed to APIGatewayHttpApiV2ProxyFunction
:
public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction
{
protected override void Init(IWebHostBuilder builder)
{
builder
.UseStartup<string, string>();
}
}
The second change is to update the CloudFormation template file, serverless.template
, to declare that an HTTP API should be created. In the Events
section of the AWS::Serverless::Function
resource change the type from Api to HttpApi. Also, if you are using the variable ${ServerlessRestApi} to create the URL of the REST API then change it to ${ServerlessHttpApi} and remove the Prod stage name from the URL since HTTP APIs by default deploy to the root of the URL. Here is a full example of a modified serverless.template
:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"AspNetCoreFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "HttpApiExample::HttpApiExample.LambdaEntryPoint::FunctionHandlerAsync",
"Runtime": "dotnetcore3.1",
"CodeUri": "",
"MemorySize": 256,
"Timeout": 30,
"Role": null,
"Policies": [
"AWSLambdaFullAccess"
],
"Events": {
"ProxyResource": {
"Type": "HttpApi",
"Properties": {
"Path": "/{proxy+}",
"Method": "ANY"
}
},
"RootResource": {
"Type": "HttpApi",
"Properties": {
"Path": "/",
"Method": "ANY"
}
}
}
}
}
},
"Outputs": {
"ApiURL": {
"Description": "API endpoint URL for Prod environment",
"Value": {
"Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
}
}
}
}
WebSocket API with .NET Core Lambda functions
Amazon API Gateway also supports WebSockets, which can backed by Lambda. You can read more about WebSocket support here. The post describes how the WebSocket support works and contains a tutorial using Node.js. In version 1.17.0.0 of the AWS Toolkit for Visual Studio, released as part of the .NET Core 3.1 Lambda update, we took the Node.js tutorial and turned it into a .NET Core 3.1 Lambda blueprint. By using this blueprint, it is simple to start building your .NET Core serverless real-time communication applications.
If you are unfamiliar with how to access .NET Core Lambda blueprints in the AWS Toolkit for Visual Studio follow these steps after installing the tooling.
- In Visual Studio go to File -> New -> Project
- Search for Visual Studio project template in the search box by typing "AWS Serverless"
- Select "AWS Serverless Application (.NET Core – C#)" and click Next.
- Set a project name and click Create.
- In the Lambda blueprint dialog, select "WebSocket API", then click Finish.
For .NET developers not using Visual Studio you can also create a Lambda WebSocket project from the dotnet new
command using the following steps.
- Install the AWS Lambda template package
dotnet new -i Amazon.Lambda.Templates
- Create project
dotnet new serverless.WebSocketAPI -o ExampleWebSocketProject
Once you create the project check out the README file in the project for more information about the code in the blueprint and how to deploy and test the WebSocket.
Deploying multiple .NET Core Lambda projects in Visual Studio
It is common for serverless applications to consist of multiple Lambda functions. This is usually done by writing multiple functions in a single .NET Core Lambda project and then defining AWS::Serverless::Function
resources in serverless.template
for each Lambda function you want to expose from the project.
The downside of this approach is that it can cause the .NET Lambda project to become large, with all of the dependencies for all the Lambda functions defined in the project. An alternative is to put the Lambda functions in separate .NET Core Lambda projects but that raises the question of how to deploy all of the projects as a single unit.
Our .NET Core Global tool Amazon.Lambda.Tools has had support for deploying multiple projects with a single dotnet lambda deploy-serverless
or dotnet lambda package-ci
command for a while. It works by setting the CodeUri
property of a AWS::Serverless::Function
resource or Code/S3Key
property of a AWS::Lambda::Function
resource to a relative location from the serverless.template
file to the .NET Core Lambda project directory. Here is an example of a serverless.template that deploys 2 Lambda functions, defined in 2 separate projects. One project is called FrontendAPI and the other is called S3EventProcessor. The serverless.template
file is located in the parent directory of the projects, and each CodeUri
property points to the appropriate child directory:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Globals" : {
"Function" : {
"Runtime" : "dotnetcore3.1",
"MemorySize" : 256,
"Timeout": 30
}
},
"Resources": {
"AspNetCoreFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "FrontendAPI::FrontendAPI.LambdaEntryPoint::FunctionHandlerAsync",
"CodeUri": "./FrontendAPI",
"Policies": [
"AWSLambdaFullAccess"
],
"Environment": {
"Variables": {
"AppS3Bucket": {"Ref": "Bucket"}
}
},
"Events": {
"ProxyResource": {
"Type": "Api",
"Properties": {
"Path": "/{proxy+}",
"Method": "ANY"
}
},
"RootResource": {
"Type": "Api",
"Properties": {
"Path": "/",
"Method": "ANY"
}
}
}
}
},
"S3Function": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "S3EventProcessor::S3EventProcessor.Function::FunctionHandler",
"CodeUri": "./S3EventProcessor",
"Policies": [
"AWSLambdaFullAccess"
],
"Events": {
"NewObjects": {
"Type": "S3",
"Properties": {
"Bucket": {"Ref": "Bucket"},
"Events": ["s3:ObjectCreated:*"]
}
}
}
}
},
"Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
}
}
},
"Outputs": {
"ApiURL": {
"Description": "API endpoint URL for Prod environment",
"Value": {
"Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
}
},
"AppBucket": {
"Value": {"Ref": "Bucket"}
}
}
}
In version 1.17.0.0 of the AWS Toolkit for Visual Studio we added the ability to right click and deploy a serverless.template
that was created as a Solution Item. That means in my Visual Studio solution, containing the FrontendAPI and S3EventProcessor projects, I can define my serverless.template
in the same directory as the solution file and add it as a Solution Item. Then I can easily right click and deploy both projects together from Visual Studio.
Change in exception handling in .NET Core 3.1 Lambda runtime
When exceptions are thrown from Lambda functions the Lambda runtime catches the exceptions and writes them to CloudWatch Logs, before returning the exceptions to calling services. In previous .NET Core Lambda runtimes, if a Lambda function was async and returned a Task
then an AggregateException
would be written to the logs and returned to the calling service. This made exception handling hard especially in services like AWS Step Functions, which allows you to branch your state machine based on the type of exception thrown.
In .NET Core 2.1 we added a work around to this problem. You could set the environment variable UNWRAP_AGGREGATE_EXCEPTIONS
to true
and the Lambda runtime would unwrap the AggregateException
and rethrow the inner exception.
Now with .NET Core 3.1 we have changed the behavior for AggregateExceptions
so that they are always unwrapped. This makes it easy to have exception handling with your .NET Core Lambda functions and Step Function's state machine.
Summary
Thanks for the all the feedback and we hope you all have enjoyed the first month of building .NET Core 3.1 Lambda functions. If you haven't tried out .NET Core Lambda support, now is a great time to try them out! And please, keep the feedback coming on our .NET Core Lambda repository.