En un “mundo ideal” 🌍 desarrollar sistemas sólo debería consistir en que nos enfoquemos en la lógica de negocio que se necesita para que una idea se haga realidad. Sin embargo, los desarrolladores solemos dedicar demasiado tiempo a atender cuestiones como escalabilidad, seguridad, etc.
Este artículo te explica por qué ese “mundo ideal” no está tan lejos y qué herramientas existen para que sólo tengas que preocuparte en escribir buena lógica de negocio. 😉
¿Qué es serverless?
Comencemos por explicar qué es Serverless. Una buena definición es la que dio Simon Wadley en este tweet:
Es decir, el desarrollo serverless trata la ejecución del código como la respuesta a un evento (event driven), que además te permite pagar sólo por lo que usas (utility based) sin guardar estado entre ejecuciones (stateless).
¿Por qué Serverless?
Desde el punto de vista del desarrollador, la plataforma que usamos -por ejemplo Amazon Web Services- se encarga por defecto de la escalabilidad, de la resiliencia y, en general, de todos los servicios que podamos necesitar: base de datos, autenticación, colas… De esta forma nos podemos centrar en desarrollar la lógica de negocio, que es lo que realmente nos interesa 💪
Desde el punto de vista de negocio, ese foco en centrar los esfuerzos de desarrollo en lo importante, significa poder reducir el time to market e incentivar la innovación y experimentación.
También hay otro aspecto muy importante: al pagar sólo por lo que usamos estamos ayudando a convertir costes fijos en costes variables, lo cual nos puede permitir minimizar el riesgo de negocio, entender mejor los costes de una feature, minimizar la capacidad no utilizada… En general, optimizar.
¿Y si nos enfocamos aún más? 💡
Incluso estando en un entorno en el que nos vamos a preocupar sólo de la lógica de negocio, nos encontramos con que necesitamos funcionalidades auxiliares que nos obligan a escribir código de más. Por ejemplo, frecuentemente queremos enriquecer los logs con información de trazabilidad o devolver los errores en un formato “amigable”.
Para node.js existe una muy buena librería que es middy y nos ayuda a concentrar estas funcionalidades. Con ella podemos configurar nuestro proyecto para que se utilicen algunos de sus middlewares.
Como no teníamos algo parecido para .Net decidimos hacer un port de esta librería.
MiddyNet
MiddyNet es una librería de middlewares para .NET Core y AWS Lambda.
Un middleware es un artefacto que dispone de una sección de código que se puede ejecutar antes de tu función y otra sección de código que se puede ejecutar después. Estos middlewares se pueden encadenar permitiendo construir un pipeline.
Ejemplo de uso
Vamos a ver un ejemplo de lo sencillo que es añadir funcionalidad a nuestra aplicación con MiddyNet y de cómo el código queda más limpio. Supongamos que esta es nuestra lambda que lo que hace es devolver un saludo y dejar un 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
};
}
}
}
Si quisiéramos enriquecer nuestro log con la información de correlación recibida en los headers traceparent y tracestate tendríamos que hacer algo así:
1. Leemos los headers:
var traceParent = request.Headers.ContainsKey("traceparent")
? request.Headers["traceparent"]
: string.Empty;
var traceState = request.Headers.ContainsKey("tracestate")
? request.Headers["tracestate"]
: string.Empty;
2. Añadimos la info de correlación a los logs:
context.Logger.Log($"Message generated. Traceparent: {traceParent}, tracestate: {traceState} ");
Nuestra lambda quedaría así:
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
};
}
}
}
Como vemos, estamos “ensuciando” nuestra lógica de negocio con código adicional que cumple una funcionalidad auxiliar.
MiddyNet nos proporciona un middleware que permite hacer esto mismo de forma muy sencilla.
Vamos a verlo paso a paso:
1. Añadimos el nuget de MiddyNet
dotnet add package Voxel.MiddyNet
2. Además, añadimos el nuget correspondiente al middleware que vamos a utilizar
dotnet add package Voxel.MiddyNet.Tracing.ApiGatewayMiddleware
3. Hacemos que nuestra clase derive de MiddyNet indicando los tipos de los eventos de entrada y salida, en este caso los usados por APIGateway V2
public class Greeter : MiddyNet<APIGatewayHttpApiV2ProxyRequest, APIGatewayHttpApiV2ProxyResponse>
4. Configuramos el middleware en el constructor
Use(new ApiGatewayHttpApiV2TracingMiddleware());
4. 5. Cambiamos nuestro método Handler para que sobreescriba el método proporcionado por MiddyNet
protected override Task<APIGatewayHttpApiV2ProxyResponse> Handle(APIGatewayHttpApiV2ProxyRequest request, MiddyNetContext context)
El resultado sería algo así:
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
});
}
}
}
Como vemos, nuestro código vuelve a centrarse exclusivamente en la lógica de negocio. Tan sólo hemos tenido que configurar el uso del middleware ApiGatewayHttpApiV2TracingMiddleware en el proyecto que se encarga de recoger los headers de correlación y enriquecer nuestros logs con esta información.
Existen otros middlewares en MiddyNet que ofrecen otras funcionalidades, como devolver los errores en formato ProblemDetails, manejar CORS, etc. Podés ver un listado de ellos en este enlace.
Conforme más funcionalidades se añadan mediante middleware, más evidente es la cantidad de código que evitamos tener que escribir.
El siguiente ejemplo muestra el uso de tres middlewares:
- uno para logar información de correlación,
- otro para devolver los errores en formato ProblemDetails
- y otro para leer un parámetro de configuración.
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
});
}
}
}
Como se puede ver, hay dos objetos propios de MiddyNet que estamos utilizando. El primero es el logger propio de la librería. Este logger tiene alguna característica más que el logger por defecto, como escribir logging estructurado o añadir propiedades del contexto de AWS Lambda. El segundo es el diccionario AdditionalContext. Este diccionario es utilizado por los middlewares para poner información que otros middlewares o la función principal puedan necesitar, en este caso el valor de greetingType.
Conclusión
Hemos hablado de lo importante que es poner foco en el desarrollo de nuestra lógica de negocio y que para lograrlo, existen tecnologías como Serverless que nos ayudan mucho.
También hemos visto como la librería MiddyNet nos provee de una colección de middlewares que nos permiten hacer uso de funciones auxiliares sin ensuciar nuestro código.
Te invitamos a usarla en tus proyectos y si encuentras a faltar algún middleware que cubra alguna necesidad interesante, te animamos a contribuir escribiendo uno tú mismo como indica esta guía.😊
¡Esperamos que te sea útil!
Sobre las autoras
Meri Herrera, Software Engineer en Voxel. Twitter. LinkedIn.
Josep Bocanegra, Software Engineer en Voxel. Twitter. Linkedin.
Vicenç García, VP of Engineering en Voxel. Twitter. Linkedin.