AWS Developer Tools Blog

Building and Debugging .NET Lambda applications with .NET Aspire (Part 1)

In a recent post we gave some background on .NET Aspire and introduced our AWS integrations with .NET Aspire that integrate AWS into the .NET dev inner loop for building applications. The integrations included how to provision application resources with AWS CloudFormation or AWS Cloud Development Kit (AWS CDK) and using Amazon DynamoDB local for local development. These features can keep .NET developers in the development environments they are most productive in.

AWS Lambda has it’s own programming model for responding to events, and we are frequently asked how to run and debug Lambda functions locally; that is, essentially, how to F5 debug a collection of Lambda functions that make up a serverless application. Today we have released a developer preview for local Lambda development support that provides this feature in a .NET idiomatic experience.

With the new Lambda support, you can use the .NET Aspire app host to orchestrate the Lambda functions that make up your serverless applications. When the app host is launched from Visual Studio, all of the Lambda functions are connected to the debugger. In addition to connecting the Lambda functions to the app host, the functions can also be orchestrated through an API Gateway emulator to provide the full experience of invoking Lambda functions through a REST API.

Developer Preview

Our Lambda features are currently in developer preview. To signify the preview status, the Lambda APIs in our Aspire.Hosting.AWS package have been marked with the RequiresPreviewFeatures .NET attribute. In order to use the Lambda API you need to opt-in to preview features; otherwise, a compilation error will be triggered.

You can opt-in to the Lambda preview features by adding the following line at the startup of the app host’s Program.cs file.



#pragma warning disable CA2252 // Opt in to preview features

C#

One known issue with the developer preview is that the Rider IDE from JetBrains is not able to start Lambda functions that are defined in a class library. Lambda functions written as an executable, also referred to as using top level statements, will work with no issue in Rider. The Rider team has made a fix that address the Lambda class-library issue, which will be available in an upcoming release.

Getting Started

To get started using .NET Aspire with Lambda, let’s walk through how you can enable .NET Aspire for an existing .NET Lambda project. In this scenario, say that I have a .NET Lambda project called “LambdaWebCalculator”, which is a collection of Lambda functions that are exposed as a REST API through Amazon API Gateway. The steps used for enabling .NET Aspire for this function can be used for any .NET Lambda project, but for context, here is a sample of the code in the aforementioned .NET Lambda project.


using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaWebCalculator;

public class Functions
{
    public APIGatewayHttpApiV2ProxyResponse AddFunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
    {
        context.Logger.LogDebug("Starting adding operation");    
        var x = (int)Convert.ChangeType(request.PathParameters["x"], typeof(int));
        var y = (int)Convert.ChangeType(request.PathParameters["y"], typeof(int));
        var sum = x + y;
        context.Logger.LogInformation("Adding {x} with {y} is {sum}", x, y, sum);
        var response = new APIGatewayHttpApiV2ProxyResponse
        {
            StatusCode = 200,
            Headers = new Dictionary&t;string, string>
                    {
                        {"Content-Type", "application/json" }
                    },
            Body = sum.ToString()
        };

        return response;
    }

... // Similar functions for minus, multiply, and divide

}

C#

Setting up the .NET Aspire app host project

To get started using .NET Aspire to run and debug our Lambda functions, we need to create a .NET Aspire app host project. This project represents our local orchestration for running the pieces of the distributed application. In our Lambda scenario, the app host orchestrates running all of the Lambda functions.

Use the following steps to create an app host for Lambda.

  1. In the solution, add a new project using the .NET Aspire App Host project template. For the .NET Aspire version you should choose the latest version. As of this writing the latest .NET Aspire version is 9.1. Common convention is to name the app host project <solution-name>.AppHost.
  2. On the AppHost, add a NuGet reference to Aspire.Hosting.AWS version 9.1.4 or above.
  3. On the AppHost project, add a project reference to the Lambda project.
  4. In the Program.cs file of the AppHost, we need to add the #pragma to enable using the preview Lambda APIs from Aspire.Hosting.AWS. Then use the AddAWSLambdaFunction method to register all the Lambda functions we want to run and debug locally. In my example I have the four Lambda functions for the Add, Minus, Multiple, and Divide features of my REST API calculator.

    
    #pragma warning disable CA2252
    
    var builder = DistributedApplication.CreateBuilder(args);
    
    builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("AddFunction",
            lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::AddFunctionHandler");
    builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("MinusFunction",
            lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::MinusFunctionHandler");
    builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("MultipyFunction",
            lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::MultiplyFunctionHandler");
    builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("DivideFunction",
            lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::DivideFunctionHandler");
    
    builder.Build().Run();
    
    C#
  5. Make the AppHost project the startup project and push F5 to launch the Visual Studio debugger.
  6. From the Aspire dashboard, the Lambda functions are added as a resource as well as the Lambda service emulator. To debug a Lambda function, click the debug icon in the Actions column for the Lambda project.
  7. The debug icon launches the Lambda test page where Lambda events can be invoked. Breakpoints can be set in the Lambda function for debugging.

API Gateway Emulation

In my existing .NET Lambda project, all my Lambda functions were meant to be called from an Amazon API Gateway endpoint. This means that I want my web calculator to be called as a REST API through HTTP clients like web browsers. With our Lambda integration, we have added the ability to orchestrate your Lambda functions in the app host as if they were running in API Gateway.

To enable the API Gateway emulator, use the AddAWSAPIGatewayEmulator method, and include Lambda functions with routes using the WithReference method. Note that if your route uses a wildcard path, use the {proxy+} path variable, similar to the way in which you would define wildcard routes in the CloudFormation template.


#pragma warning disable CA2252

var builder = DistributedApplication.CreateBuilder(args);

var addFunction = builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("AddFunction",
        lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::AddFunctionHandler");
var minusFunction = builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("MinusFunction",
        lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::MinusFunctionHandler");
var multiplyFunction = builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("MultipyFunction",
        lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::MultiplyFunctionHandler");
var divideFunction = builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("DivideFunction",
        lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::DivideFunctionHandler");

builder.AddAWSAPIGatewayEmulator("APIGatewayEmulator", Aspire.Hosting.AWS.Lambda.APIGatewayType.HttpV2)
        .WithReference(addFunction, Aspire.Hosting.AWS.Lambda.Method.Get, "/add/{x}/{y}")
        .WithReference(minusFunction, Aspire.Hosting.AWS.Lambda.Method.Get, "/minus/{x}/{y}")
        .WithReference(multiplyFunction, Aspire.Hosting.AWS.Lambda.Method.Get, "/multiply/{x}/{y}")
        .WithReference(divideFunction, Aspire.Hosting.AWS.Lambda.Method.Get, "/divide/{x}/{y}");

builder.Build().Run();
C#

Now when the app host is launched, the API Gateway emulator resource is included in the list of resources. The endpoint shown for the API Gateway emulator can be used to make REST requests that invoke the Lambda functions.

In my example I have registered the addFunction Lambda function with the HTTP verb GET and the resource path /add/{x}/{y}. So if I make a request to http://localhost:<apigateway-emulator-port>/add/1/2, my add Lambda function will be invoked in the debugger where breakpoints will be honored and I’ll get back the response.

Accessing Logs

The logs generated from the Lambda function can be viewed in the .NET Aspire dashboard by clicking on the Console tab and selecting the Lambda function in the Resources dropdown list.

By default, the Lambda function will log at the INFO level in text format, matching the default behavior of functions deployed to Lambda. Using the LambdaFunctionOptions class with the AddAWSLambdaFunction method, the format and log level can be adjusted. The following code sets the log level to debug and the format to JSON.


var addFunction = builder.AddAWSLambdaFunction<Projects.LambdaWebCalculator>("AddFunction",
        lambdaHandler: "LambdaWebCalculator::LambdaWebCalculator.Functions::AddFunctionHandler",
        options: new LambdaFunctionOptions
        {
            ApplicationLogLevel = ApplicationLogLevel.DEBUG,
            LogFormat = LogFormat.JSON
        });
        
C#

The Console Logs view will now show the debug statements coming from the Lambda function, and format the messages as JSON documents.

Connecting to AWS

As mention in the previous .NET Aspire blog post for AWS, the configuration that the AWS SDK for .NET should use to connect to AWS can be created using the AddAWSSDKConfig method. The configuration can be assigned to projects by calling the WithReference method on the project. A .NET Lambda project is also a project, and the same pattern can be used for .NET Lambda projects.


var builder = DistributedApplication.CreateBuilder(args);

var awsConfig = builder.AddAWSSDKConfig()
                        .WithRegion(RegionEndpoint.USWest2);

builder.AddAWSLambdaFunction<Projects.ReadDynamoDBExecutable>("ReadDynamoDBFunction", lambdaHandler: "ReadDynamoDBExecutable")
        .WithReference(awsConfig);      
C#

Conclusion

With the Lambda integration with .NET Aspire, we hope to provide an experience for building .NET Lambda functions that is idiomatic and simple to get started. Once the app host is set up, all that team members of a project need to do is to clone or pull the latest changes from the repo and start debugging immediately. The Lambda integration can be used in combination with our previous integrations for provisioning resources with AWS CloudFormation and having Lambda functions that use Amazon DynamoDB local for the backend.

In this blog post, which is Part 1, you learned how to use .NET Aspire to run and debug .NET Lambda functions locally. In the next blog post, which is Part 2, you will learn how to incorporate .NET Aspire’s programming model for connecting additional components and sharing best practices across Lambda functions.

The development of this feature is happening in our aws/integrations-on-dotnet-aspire-for-aws repository. The following GitHub issue is being used as the main tracker issue for the Lambda integration: https://github.com/aws/integrations-on-dotnet-aspire-for-aws/issues/17. We ask that .NET developers who are building Lambda functions try out this preview and let us know on our repository your success stories and any issues with our Lambda integration.

Norm Johanson

Norm Johanson

Norm Johanson has been a software developer for more than 25 years developing all types of applications. Since 2010 he has been working for AWS focusing on the .NET developer experience at AWS. You can find him on Twitter @socketnorm and GitHub @normj.