Tiempo de lectura: 6 minutos

In an “ideal world” 🌍 developing systems should only focus on the business logic needed to make an idea a reality. However, developers tend to spend too much time on scalability, security and so on.

This article explains why that “ideal world” is not so far away and what tools exist so that you only have to worry about writing good business logic. 😉

What is serverless?

Let’s start by explaining what Serverless is. A good definition is the one given by Simon Wadley in this tweet:

That is, serverless development treats code execution as the response to an event (event-driven), which also allows you to pay only for what you use (utility-based) without saving state between executions (stateless).

Why Serverless?

From the developer’s point of view, the platform we use – for example, Amazon Web Services – takes care of scalability, resilience, and, in general, all the services we may need: database, authentication, queues… In this way, we can focus on developing the business logic, which is what really interests us 💪

From a business point of view, this focus on concentrating development efforts on what is important means being able to reduce time to market and encourage innovation and experimentation.

There is also another essential aspect: by paying only for what we use, we are helping to convert fixed costs into variable costs, which can allow us to minimise business risk, better understand the costs of a feature, minimise unused capacity… In general, optimise.

What if we focus even more? 💡

Even in an environment where we are going to worry only about business logic, we find that we need auxiliary functionalities that force us to write more code. For example, we often want to enrich logs with traceability information or return errors in a “friendly” format.

For node.js there is an excellent library called middy that helps us to concentrate these functionalities. With it, we can configure our project to use some of its middlewares.

As we didn’t have something similar for .Net, we decided to make a port of this library.

MiddyNet

MiddyNet is a middleware library for .NET Core and AWS Lambda.

A middleware is an artefact that has a section of code that can be executed before your function and another section of code that can be executed after. These middlewares can be chained together, allowing you to build a pipeline.

Example of use

Let’s see an example of how easy it is to add functionality to our application with MiddyNet and how the code is cleaner. Let’s assume this is our lambda that just returns a greeting and leaves a log:

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

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace WithoutMiddyNet
{
    public class Greeter
    {
      public APIGatewayHttpApiV2ProxyResponse Handler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
      {
            var message = "Hello World.";

            context.Logger.Log("Message generated");

            return new APIGatewayHttpApiV2ProxyResponse
            {
                StatusCode = 200,
                Body = message
            };
      }
    }
}

If we wanted to enrich our log with the correlation information received in the traceparent and tracestate headers we would have to do something like this:

1. We read the headers:

var traceParent = request.Headers.ContainsKey("traceparent")
                    ? request.Headers["traceparent"]
                    : string.Empty;

                var traceState = request.Headers.ContainsKey("tracestate")
                    ? request.Headers["tracestate"]
                    : string.Empty;

2. We add the correlation info to the logs:

context.Logger.Log($"Message generated. Traceparent: {traceParent}, tracestate: {traceState} ");

Our lambda would look like this:

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Threading.Tasks;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace WithoutMiddyNet
{
    public class Greeter
    {
      public async Task<APIGatewayHttpApiV2ProxyResponse> Handler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
      {
                var traceParent = request.Headers.ContainsKey("traceparent")
                    ? request.Headers["traceparent"]
                    : string.Empty;

                var traceState = request.Headers.ContainsKey("tracestate")
                    ? request.Headers["tracestate"]
                    : string.Empty;

                var message = "Hello World";

                context.Logger.Log($"Message generated. Traceparent: {traceParent}, tracestate: {traceState} ");

                return new APIGatewayHttpApiV2ProxyResponse
                {
                    StatusCode = 200,
                    Body = message
                };
      }
    }
}

As we can see, we are “messing up” our business logic with additional code that fulfils an auxiliary functionality.

MiddyNet provides us with a middleware that allows us to do this very easily. 

Let’s take a look at it step by step:

1. We add the MiddyNet nuget

dotnet add package Voxel.MiddyNet

2. In addition, we add the nuget corresponding to the middleware that we are going to use

dotnet add package Voxel.MiddyNet.Tracing.ApiGatewayMiddleware

3. We make our class derive from MiddyNet indicating the types of input and output events, in this case, those used by APIGateway V2.

public class Greeter : MiddyNet<APIGatewayHttpApiV2ProxyRequest, APIGatewayHttpApiV2ProxyResponse>

4. We configure the middleware in the constructor

Use(new ApiGatewayHttpApiV2TracingMiddleware());

5. We change our Handler method to override the method provided by MiddyNet

protected override Task<APIGatewayHttpApiV2ProxyResponse> Handle(APIGatewayHttpApiV2ProxyRequest request, MiddyNetContext context)

The result would look something like this:

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Threading.Tasks;
using Voxel.MiddyNet;
using Voxel.MiddyNet.Tracing.ApiGatewayMiddleware;


[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace WithMiddyNet
{
    public class Greeter : MiddyNet<APIGatewayHttpApiV2ProxyRequest, APIGatewayHttpApiV2ProxyResponse>
    {
        public Greeter()
        {
            Use(new ApiGatewayHttpApiV2TracingMiddleware());
        }

        protected override Task<APIGatewayHttpApiV2ProxyResponse> Handle(APIGatewayHttpApiV2ProxyRequest request, MiddyNetContext context)
        {
            var message = "Hello World";

            context.Logger.Log(LogLevel.Info, "Message generated.");

            return Task.FromResult(new APIGatewayHttpApiV2ProxyResponse
            {
                StatusCode = 200,
                Body = message
            });
        }
    }
}

As we can see, our code is again focused exclusively on business logic. We only had to configure the use of the ApiGatewayHttpApiV2TracingMiddleware middleware in the project, which is in charge of collecting the correlation headers and enriching our logs with this information.

There are other middlewares in MiddyNet that offer other functionalities, such as returning errors in ProblemDetails format, handling CORS, etc. You can see a list of them in this link.

As we add more functionality via middlewares, we can save more and more code.

The following example shows the use of three middlewares: 

  • one to get correlation information, 
  • one to return errors in ProblemDetails format, 
  • and one to read a configuration parameter.

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Collections.Generic;
using System.Threading.Tasks;
using Voxel.MiddyNet;
using Voxel.MiddyNet.ProblemDetailsMiddleware;
using Voxel.MiddyNet.SSMMiddleware;
using Voxel.MiddyNet.Tracing.ApiGatewayMiddleware;

[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace WithMiddyNet
{
    public class Greeter : MiddyNet<APIGatewayHttpApiV2ProxyRequest, APIGatewayHttpApiV2ProxyResponse>
    {
        public Greeter()
        {
            Use(new ApiGatewayHttpApiV2TracingMiddleware());
            Use(new ProblemDetailsMiddlewareV2(new ProblemDetailsMiddlewareOptions().Map<WrongGreetingTypeException>(400)));
            Use(new SSMMiddleware<APIGatewayHttpApiV2ProxyRequest, APIGatewayHttpApiV2ProxyResponse>(new SSMOptions
            {
                ParametersToGet = new List<SSMParameterToGet> { new SSMParameterToGet("greetingsType", "greetingsType") }
            }));
        }

        protected override Task<APIGatewayHttpApiV2ProxyResponse> Handle(APIGatewayHttpApiV2ProxyRequest request, MiddyNetContext context)
        {
            var name = request.PathParameters["name"];

            context.Logger.Log(LogLevel.Info, $"Greeter called with name {name}");

            var greetingsType = context.AdditionalContext["greetingsType"].ToString();

            if (greetingsType != "formal" && greetingsType != "colloquial")
                throw new WrongGreetingTypeException();

            var greeting = greetingsType == "formal" ? "Hello" : "Hi";

            var message = $"{greeting} {name}. Welcome to our online store.";

            context.Logger.Log(LogLevel.Info, "Greeting message generated.");

            return Task.FromResult(new APIGatewayHttpApiV2ProxyResponse
            {
                StatusCode = 200,
                Body = message
            });
        }
    }
}

 

As you can see, there are two MiddyNet proprietary objects that we are using. The first one is the library’s own logger. This logger has more features than the default logger, such as writing structured logging or adding AWS Lambda context properties. The second is the AdditionalContext dictionary. Middlewares use this dictionary to put information that other middlewares or the main function may need, in this case, the value of greetingType.

Conclusion

We have talked about how important it is to focus on developing our business logic and that to achieve this, there are technologies such as Serverless that help us a lot.

We have also seen how the MiddyNet library provides us with a collection of middlewares that allow us to use auxiliary functions without messing up our code.

We invite you to use it in your projects, and if you find a missing middleware that fills an interesting need, we encourage you to contribute by writing one yourself, as indicated in this guide 😊.

We hope you find it useful!

About the authors

Meri Herrera, Software Engineer at Voxel. Twitter. LinkedIn.

Josep Bocanegra, Software Engineer at Voxel. Twitter. Linkedin.

Vicenç García, VP of Engineering at Voxel. Twitter. Linkedin.

Leave a Reply