DEV Community

loading...

Serverless Microservices in Asp.net with AWS API Gateway

Kayes Islam
・6 min read

For the project I am currently working on, I have been introduced to a whole array of services provided by AWS. We are going to embark on creating serverless Web APIs in AWS Lambda and Api Gateway. There are a few ways to achieve this and we needed to evaluate which way would work the best for our solution.

Lambda Functions

One of the AWS serverless project templates provided by Amazon.Lambda.Templates has straight up lambda functions written in C# like this:

public class Functions
{
    public APIGatewayProxyResponse Get(
        APIGatewayProxyRequest request,
        ILambdaContext context
    )
    {
        context.Logger.LogLine("Get Request\n");
        var response = new APIGatewayProxyResponse
        {
            StatusCode = (int)HttpStatusCode.OK,
            Body = "Hello AWS Serverless",
            Headers = new Dictionary<string, string>
            {
                { "Content-Type", "text/plain"}
            }
        };


        return response;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is then put together with a SAM template file to define the AWS API Gateway:

 "GetLambda": {
   "Type": "AWS::Serverless::Function",
   "Properties": {
     "Handler": "AwsServerless.Lambda::AwsServerless.Lambda.Functions::Get",
     "Runtime": "dotnetcore3.1",
     "CodeUri": "",
     "MemorySize": 256,
     "Timeout": 30,
     "Role": null,
     "Policies": [
       "AWSLambdaBasicExecutionRole"
     ],
     "Events": {
       "RootGet": {
         "Type": "Api",
         "Properties": {
           "Path": "/",
           "Method": "GET"
         }
       }
     }
   }
 }
Enter fullscreen mode Exit fullscreen mode

This creates an API endpoint that invokes the Get() method of our Function class when a GET request event occurs on our API's path "/".
For local development, it comes with a Mock Lambda Test Tool. So if you hit F5 on your Visual Studio it opens this on localhost:5050:
Mock Lambda Test Tool Screenshot

The tool forwards the request to the lambda so that it could be executed/debugged with test JSON data in the local development environment. You can also invoke the tool from the command line without the UI and pass in a payload file.
One thing to say here is that I'm quite unimpressed that there's no way to debug the lambda as an API service so that requests to a localhost:port endpoint are forwarded to the designated lambda. As a full stack developer, one of the common workflows for me is to open the API project in Visual Studio, open the client app project in VS Code and run the entire stack together. I didn't easily find any way to do that with a lambda project for .net. There's step through debugging available using AWS provided sam command line tool for nodejs, python and golang, but the documentation is missing for .net.
Another issue here is that there's a lot of boilerplate code that we will need to put together for services we take for granted out of the box when we start-up a basic asp.net core project - CORS, JWT Auth, Authorization, HTTPS Redirection, Model Binding. None of this is present here. Sure how hard is it to hook it up together in a base class, but still it's work that needs to be done here.
Using the lambda template straight out of the box also means your code base is strongly tied to AWS Lambda, not platform agnostic.

Asp.net Core

Another project template that is provided by the Amazon.Lambda.Templates is an Asp.net core project configured to run as one lambda.

Project Outline Monolity

There are two notable files here: LocalEntryPoint.cs and LambdaEntryPoint.cs. LocalEntyPoint has the typical asp.net core main function for when you want to run it locally:

 public class LocalEntryPoint
 {
     public static void Main(string[] args)
     {
         CreateHostBuilder(args).Build().Run();
     }


     public static IHostBuilder CreateHostBuilder(string[] args) =>
         Host.CreateDefaultBuilder(args)
             .ConfigureWebHostDefaults(webBuilder =>
             {
                 webBuilder.UseStartup<Startup>();
             });
 }
Enter fullscreen mode Exit fullscreen mode

LambdaEntryPoint derives from APIGatewayProxyFunction which ensures that for each API gateway call the web-api's startup is invoked and the API Gateway request is marshalled into Asp.net core's request.

 public class LambdaEntryPoint : 
    Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
 {
     protected override void Init(IWebHostBuilder builder)
     {
         builder
             .UseStartup<Startup>();
     }


     protected override void Init(IHostBuilder builder)
     {
     }
 }
Enter fullscreen mode Exit fullscreen mode

The SAM template then uses a /{proxy+} path to ensure that all requests to the root of the Api Gateway just invoke the Asp.net core application.

 "AspNetCoreFunction": {
   "Type": "AWS::Serverless::Function",
   "Properties": {
     "Handler": "AWSServerless.Aspnet::AWSServerless.Aspnet.LambdaEntryPoint::FunctionHandlerAsync",
     "Runtime": "dotnetcore3.1",
     "CodeUri": "",
     "MemorySize": 256,
     "Timeout": 30,
     "Role": null,
     "Policies": [
       "AWSLambda_FullAccess"
     ],
     "Events": {
       "ProxyResource": {
         "Type": "Api",
         "Properties": {
           "Path": "/{proxy+}",
           "Method": "ANY"
         }
       },
       "RootResource": {
         "Type": "Api",
         "Properties": {
           "Path": "/",
           "Method": "ANY"
         }
       }
     }
   }
 }
Enter fullscreen mode Exit fullscreen mode

In this configuration that routing is handled by asp.net core, not Api Gateway.

Monolyth Lambda Diagram

This configuration is familiar to any asp.net developer, which means new developers assigned to the project can hit the ground running. Full stack debug workflow I mentioned earlier is also a great advantage. Comes with all the benefits of Asp.net core, microsoft driven standard framework, available middlewares, libraries, community support and all the rest of it.
The solution is also platform agnostic. All we need to do is write an entrypoint for Azure Functions or Google Cloud Function and the solution is potentially deployable on these platforms.
The problem here is that this is a lambda monolith and not quite the microservices we are opting for. The benefits of microservices are lost here. Loading a lambda monolith would also have some performance impact.
Another point to note here is that this is not a traditional asp.net core app, different environment, different lifecycle, hence some components won't work. For example, you won't be able to host SignalR, or a scheduler component as you could on your traditional asp.net core app. If you are investing into AWS however, you are probably looking at replacing these components with AWS provided services anyway, but it's just something to keep in mind.

Other Frameworks

We did have a look at some other similar frameworks that could be used.

Nancy

A web framework similar to Asp.net core, with possibly less plumbing. Although it lacks the support and familiarity as Asp.net core does, it is indeed a very promising framework for microservices that I am keeping my eye on.

Serverless Framework

Another very promising framework that enables writing serverless microservices in a platform agnostic way. At a first glance, I found most of the examples and libraries are for nodejs. I'm sure it has the libraries that provide sufficient middlewares to create a Restful API, but at a first glance, it just didn't give us the confidence for .net development.

Our Pick

We decided to stick with the Asp.net core framework, but rather than having one monolith web-api, we decided to break it up into multiple web-api projects, each having their own Startup.
Project Outline - Microservices

There's a shared Infrastructure where we can encapsulate start-up behaviour common to all services. There's a LocalDev project which is the only place we have a LocalEntryPoint. This is set as the start-up project for local development only, not deployed to AWS. This project doesn't have any controllers. The Starup may have some extra configuration to set-up local dev environment. For example you may want to use Lambda Authorizer in production environment, but for local-dev you could enable Auth middleware.
The other projects, each focused on a particular service: customers, orders and products. Each of these projects potentially deals with one type of resource (or related sub-resource). These projects have a Startup and a LambdaEntryPoint each as they will be deployed as to run as Lambda behind API Gateway.
Microservices Asp.net Diagram

The SAM templates then ensure that for a given base path, we invoke the correct LambdaEntryPoint. Below is an example for just the definition for product service as an example:

 "ProductsServiceLambda": {
   "Type": "AWS::Serverless::Function",
   "Properties": {
     "Handler": "AWSServerlessDemo.Aspnet::AWSServerlessDemo.Products.LambdaEntryPoint::FunctionHandlerAsync",
     "Runtime": "dotnetcore3.1",
     "CodeUri": "./AWSServerlessDemo.Products/",
     "MemorySize": 256,
     "Timeout": 30,
     "Role": null,
     "Policies": [
       "AWSLambda_FullAccess"
     ],
     "Events": {
       "ProductsProxyResource": {
         "Type": "Api",
         "Properties": {
           "Path": "/products/{proxy+}",
           "Method": "ANY"
         }
       },
       "ProductsRootResource": {
         "Type": "Api",
         "Properties": {
           "Path": "/products/",
           "Method": "ANY"
         }
       }
     }
   }
}
Enter fullscreen mode Exit fullscreen mode

Note here that we're setting the "CodeUri": "./AWSServerlessDemo.Products/". This ensures that when the dotnet lambda tool will build and deploy just the source code for AWSServerlessDemo.Products, keeping the size of the deployment to a minimum, a contributing factor to lambda performance.

Future of Asp.net as Microservices’ Framework

.Net Core 5 has introduced some promising new features:

These are experimental at this stage but have real potential. Currently the asp.net core lifecycle has a lot of dependency on reflection, slowing down the start-up time. But imagine the source generators replacing all the code dependent on reflection, then member level trimming reducing app size down to only the methods that are called! I'm excited to see how it unfolds!

Discussion (0)