Building common SaaS features à la serverless: Prelude

Building common SaaS features à la serverless: Prelude

This post marks the beginning of the second half of this blog series, which will focus on showcasing frequently encountered application features built in an event-driven, serverless fashion as AWS Lambda applications.

As discussed in the epilogue of my last blog post, we'll start by introducing a re-usable framework (read: library) for abstracting away the different AWS services that can be utilized as Lambda function triggers. I want to emphasize that the code is implemented with an eye for best practices so the input is generic, meaning you can use this as is (or adopt the mental model of this library) with any underlying serverless computing platform (Azure Functions, GCP Cloud Functions or AWS Lambda Functions).

There are multiple benefits here and the following are just some of those:

  • considerably reduced boilerplate code for creating new AWS Lambda functions in .NET,
  • standardized, familiar-looking code and
  • improved team velocity because this will allow your engineers to focus on the thing that matters most: your business (instead of infrastructure).

Much of this body of work was inspired by our conversations with the evergreen Renato Golia a few years back and his work, which he has since open-sourced, on C# project templates for AWS Lambda:

Kralizek/AWSLambdaSharpTemplate
A template for AWS Lambda functions written in C# with .NET Core - Kralizek/AWSLambdaSharpTemplate

If I had to sum this up, I'd say that you don't have to give up SOLID development principles when implementing serverless code and this post aims to be a functional demo of how to go on about accomplishing that.

public class Function : EventFunction<string>
{
    protected override void Configure(IConfigurationBuilder builder)
    {

    }

    protected override void ConfigureLogging(ILoggingBuilder logging,
    IExecutionEnvironment executionEnvironment)
    {

    }

    protected override void ConfigureServices(IServiceCollection services, 
    IExecutionEnvironment executionEnvironment)
    {
        RegisterHandler<EventHandler>(services);
    }
}
The core idea of this approach is that your Lambda function can minimally look like this

Which is remarkably similar to a boilerplate ASP.NET Core Startup.cs class!

This class provides methods for addressing all the usual cross-cutting application development concerns such as logging, environment variable configuration, secrets management, dependency injection wiring etc.

Let's start from the top though; the EventFunction class that our Lambda's Function.cs class inherits these methods from is part of a .NET Standard 2.1 library that you could reference as a NuGet package in your project.

EventFunction deep dive

Really looked hard for a better picture alas...

An EventFunction, promptly named after event-driven architecture 'n all, represents a Lambda function that is not expected to produce any value, aka we won't be returning anything back to whatever AWS service triggered the invocation of our Lambda function.

To create an EventFunction, simply change your Function.cs class so that it inherits from EventFunction<TInput> where TInput is a class representing the incoming data.

TInput can be whatever really and that's the beauty of this ♥. It can be anything ranging from a string (that represents the JSON payload of the Lambda function's trigger)  you'll deserialize to a type for further processing or possibly one of the classes that ship with the various libraries of the AWS SDK for .NET in the lambda events namespace, for instance: Amazon.Lambda.SQSEvents. Those classes contain the metadata attributes and body of an event associated with a particular AWS service, in this case SQS. So it goes without saying that, if you do the latter, you should use the AWS SDK's NuGet package that matches the service you're using as a trigger for your particular Lambda function or serverless app.

Finally, an EventFunction requires an handler that implements IEventHandler<TInput> to be registered in ConfigureServices (which you can see as part of the code sample above at the very end of the class). But more about this bit later on.

The end-game here is you becoming familiar enough with how this works, that you are able to either start crunching immediately by adopting such an approach in your serverless projects to streamline the handling of cross-cutting concerns or possibly even extend EventFunction by creating new .NET libraries in case you need to add or modify specific functionality like skipping execution for certain types of events (the event type might part of the payload's metadata for certain services); think a DynamoDB event stream to a relational SQL database, you might not want to process DELETE type of events if your SQL database schema revolves around "soft" deletes.

An EventHandler interface implementation

Think of an EventHandler interface implementation as the class where all your business logic occurs. This mental model provides a much-sought after separation of concerns.

public interface IEventHandler<TInput>
{
    Task HandleAsync(TInput input, ILambdaContext context);
}
The EventHandler interface

A minimal, concrete implementation of the EventHandler interface might look like this for a cloud service trigger with a string payload in JSON format:

public class ConcreteImplHandler : INotificationHandler<string>
{
        private readonly ILogger<string> _logger;

        public ConcreteImplHandler(ILogger<string> logger)
        {
            _logger = logger ?? 
            throw new ArgumentNullException(nameof(logger));
        }

        public async Task HandleAsync(string input,
        ILambdaContext context)
        {
        	JsonSerializer.Deserialize<YourAwesomeType>(input);
        	// TODO: business logic goes here
        }
}    
ConcreteImplHandler.cs is an implementation of IEventHandler above

Conclusions

tl;dr In event-driven, serverless applications you receive a payload per invocation the structure of which differs depending on the kind of cloud service that is being used as the function's trigger. We can abstract and streamline the work of dealing with the details of how that event looks by introducing generics into our code.

Obligatory Loki reference to urge you on in your serverless journey

In the next post we will build, by leveraging the mental model exhibited in this one, the following common SaaS feature ala serverless: a user uploading a file from some web client to a cloud provider's blob storage service (think AWS S3) and how we can leverage an S3 PUT event as a Lambda trigger to execute business code, in an asynchronous fashion, that will process this file as well as notify the user of the status and completion of said processing.

The processing bit could be anything, ranging from fancier business use cases like image recognition ML code if the file is a picture or a .PDF to more common ones like extracting information out of an Excel file.

The important thing to remember is that this 👆 is not the important thing! Your product, your code. I'm just showing a way of laying the necessary infrastructure for writing serverless applications in a reliable, re-usable and streamlined manner.