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.

–Norm