If you're a .NET developer working with AWS Lambda I hope you've heard about the new Lambda Annotations Framework. It's an open-source framework for .NET-based Lambda functions which (in my opinion) simplifies your function code, reducing boilerplate. In this blog post, published with the release of the original preview, you'll find the ideas behind the framework discussed in more detail.
The development team's been hard at work since that initial release, and recently added support to make it really simple to return HTTP status data. So, I thought the time was right to look at how simple it is to update an existing function that has a traditional Lambda function signature to use the framework.
The function I'm going to work on, called Backmask, is part of a sample serverless application used in the AMster & the Brit's Code Corner show, broadcast on AWS on Air. The application contains an API and backing Lambda function that accepts a text string message, converts it to audio using Amazon Polly, then reverses the audio and finally returns the Base64-encoded text version of the audio. You might be familiar with this idea from music, where an artist embeds reversed messages inside the main audio (or so my co-host, AM, on Code Corner tells me!). In the latest episode of Code Corner, I work through the steps of converting the function to use the new framework live. This post represents the condensed version without all the chit-chat.
You can find the sample application in this GitHub repo. It comprises a Serverless Application Model (SAM) template, a Lambda function using .NET 6, and an API Gateway endpoint. The function runs when the endpoint receives a request. A Lambda layer carries the dependency on ffmpeg for processing of the audio created by Polly. You can also find the original Python version, used in an earlier Code Corner episode, there too.
Below is the function signature, and, while it's perfectly good C# it could be more expressive in terms of what the function expects and returns. As written, it returns an object of type
APIGatewayHttpApiV2ProxyResponse that will contain the encoded text and status code, and accepts two objects as function parameters. One is the Lambda context, providing information about the running environment of the function. This includes a
Logger object that the function makes use of to log activity. The other, and more important parameter, is an API Gateway event object,
APIGatewayHttpApiV2ProxyRequest. This contains the event payload from the API - the request body, headers, and any query and path parameters:
The text to backmask is supplied in the request body. An optional query string parameter, voiceOverride, can select the voice that Polly should use to create the audio. Inside the function body, there's a lot of boilerplate code processing the event data sent to the function from the API endpoint, before we actually get to anything useful, highlighted below.
Converting this function to use the new annotations framework will make it easier to see what the function accepts as parameters, and what the function is actually doing, and it only takes a few steps.
The Amazon.Lambda.Annotations NuGet package contains the new framework, so I start by adding it to the project. If you're following along, open a command line shell and run the command below from the folder containing the project file, or use your favorite environment to add the package:
dotnet add package Amazon.Lambda.Annotations
In the function's code file (function.cs, if you're following along), add a couple of
using statements for the namespaces in this new package. The first is for the attributes we'll use from the framework, the second relates to the
HttpApi types used with the function's API Gateway resource.
using Amazon.Lambda.Annotations; using Amazon.Lambda.Annotations.APIGateway;
Before changing the function signature to better represent what the function accepts, first add the
LambdaFunction attribute above the function. Note that I'm using this attribute with no arguments, but you can use some to set custom data such as desired timeout, memory size, role, policies, and more.
Second, the function runs when a request arrives at a HttpApi endpoint in API Gateway, so I add the
HttpApi attribute too. For this function, I specify the verb (POST) and a route for the endpoint (/backmask in this case).
Next, I need to change the function signature. I still want to return an HTTP result containing both status and the encoded text, so the return type will be
IHttpResult (this is the new functionality added recently to the framework). I could, if I wanted, change the return type to be just a string representing the text-encoded audio, but I want to keep the initial payload validation and associated HTTP status code results, so an
IHttpResult result is ideal.
Now I update the parameters of the function. Instead of the original
APIGatewayHttpApiV2ProxyRequest type, I want the function signature to express the true intent; it should accept a string representing the id of the voice to use when generating the audio, and a second string containing the text to encode. The voice parameter will come from the query string, so I decorate it with the
FromQuery attribute and make sure we use the same name as we expect in the request URL. The text to encode will come from the body so similarly I use a
FromBody attribute to express the body as the source.
Note that I'm keeping the Lambda context parameter as I want to keep the logging code making use of it, but could remove it otherwise.
Next, I can clean up the boilerplate code seen earlier that extracted the real parameters needed from the original
APIGatewayHttpApiV2ProxyRequest parameter. Instead of constructing
APIGatewayHttpApiV2ProxyResponse objects, if validation fails, l use the newly added
HttpResults type in the framework.
The result is below, which you can compare with the original above. I think you'll agree it's a lot simpler and easier to read (for context, I've once again highlighted the "actual work" part of the function).
Those few steps were all that were required to convert the function's code. The next step is to build the project. Once we've done that, inspection of the serverless template file for the project shows a change - the source generator implemented inside the annotations framework has output a new
AWS::Serverless::Function resource for the function into the template. It's easy to spot, as the logical Lambda function name has the suffix Generated. This generated function resource has an added benefit in that I no longer have to keep the function handler string in the resource definition in sync with the function code. If I decide to change the name of the actual method implementing the function (the one I attributed with
[LambdaFunction]), or the class, or the namespace containing the class then, when I build the code, the source generator in the framework will keep the related resource in the template up-to-date for me.
I'm almost done with all the conversion steps. In my case, the function resource declared in the template had a custom permissions policy specified, allowing the code to call the
polly:SynthesizeSpeech API. It also referenced a Lambda layer (also declared in the template) that contains the ffmpeg binaries used to reverse the audio generated by Polly. So, I simply copy the relevant items to the generated function resource definition, and then delete the old function resource.
The highlighted boxes in the image below show the data that I simply copied from the original function resource definition. Note that I also deleted the
Timeout elements from the generated function definition. I did this since I have them specified (as the same default values) in a
Globals section of my template. If, however, I'd specified them as arguments to the
[LambdaFunction] attribute I attached to the code I would have kept them. As I noted earlier, I can also set these values, along with role and policy information, in that attribute. If I do so, the values I set get emitted by the source generator into the template and kept in sync with changes to the attribute.
That's it, "job done" for this function! We can now deploy the serverless application and test it out or, if it contains more functions, convert those to use the new annotations framework. If you want to look at the project and the changes in their entirety, checkout the s1e2_backmask.net-annotations branch in the Code Corner repository on GitHub.
Hopefully, this post will have intrigued you enough to check out the new framework, and try converting some of your existing functions into what I think is a much more readable and easier to comprehend format. Also, the development team is always interested in feedback, which you can supply in the issues folder of their repo. You can also find the design document for the annotations in this issue.