In this fourth article, we’ll work through the steps needed to create an AWS backend that serves up our dataset via an HTTP API.
Welcome to this fourth article in a series demonstrating how to create a Blazor client application that incorporates a Telerik UI For Blazor Chart component.
If you haven’t already read the second article, that should be your starting point.
One objective of this series is to demonstrate how we can combine our client application with simple serverless function backend applications.
In this article, we’ll be exploring how to use an AWS Lambda Function. This is AWS’s version of the increasingly popular “serverless computing paradigm.”
If you’re new to “serverless computing” and more accustomed to working with traditional websites/services where code is published to a host server, it’s useful to understand that with AWS Lambda:
AWS Lambda differs slightly from Azure and Google Cloud in that, by default, the underlying storage is not exposed as a distinct resource. This may help to simplify our mental image of a Lambda Function as being a “self-contained compute service” (and arguably helps to declutter the list of separate resources that are presented to cloud customers in the Console).
Note: AWS Lambda Functions support deployments of both file-based packages and container images. In this article, we’ll be demonstrating using a basic file-based deployment.
Note: AWS: Lambda deployment packages identify that “if your deployment package is larger than 50 MB, we recommend uploading your function code and dependencies to an Amazon S3 bucket.” This may sound like an optional recommendation, but it’s not—AWS Lambda has various different usage quotas, which include a deployment package limit of 50 MB.
If you’re planning to follow along with this series, you will need to create a Blazor client application, following the instructions in Part 2 (using Telerik Blazor Chart). Having completed that step, we’ll then look at creating a backend API—in this article, we’ll be looking at creating that API using AWS Lambda Functions.
If you’re interested to learn how the experience compares between the cloud providers, a succinct comparison is in Part 1, and you may further be interested in reading all three of the platform-specific articles in this series.
In this article, we’ll use AWS Lambda to create a simple serverless HTTP API backend that responds to a request with a JSON dataset.
AWS Lambda supports a number of languages including Node and Python. However, in this article, we will continue using .NET with C#, as we have previously with our Blazor client.
AWS Lambda Functions are event-driven, meaning that their code is triggered in response to various different types of “bound events,” such as a change to a DynamoDB database.
For our purposes, we’re interested in something generally referred to as an “HTTP-triggered function,” which is driven by an event originating from the AWS API Gateway service.
Note: AWS architects HTTP-triggered functions in a way that is similar to Azure Functions—we write our code using an “API Gateway” proxy class, specifically
Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse
. In contrast, Google Cloud Functions (GCF) simply uses the conventional ASP.NETHttpContext
class.
In this article, we’re going to:
» The code will return a payload of JSON data.
» For continuity with other articles in this series, that JSON payload will be exactly the same dataset.
» We’ll retrieve the dataset from a remote source in GitHub.
We’re going to expand upon the solution that we developed in the second article of this series.
In addition to those previous requirements, we should now include the following:
» AWS: What is the AWS Command Line Interface?
» AWS: Getting started with the AWS CLI
» Later in the article, there will be a requirement to properly configure the CLI with security credentials before we can use it.
» This guide provides details: AWS: AWS Command Line Interface—Configuration basics.
» This guide provides details: AWS: Using Lambda with the AWS CLI.
Specific to .NET developers, the AWS official docs about Lambda Functions can be found here:
The above documentation identifies that AWS Lambda provides several .NET libraries as NuGet packages. Strangely, no further explanation or onward reference seems to be provided.
Therefore, readers of this article will find it useful to know that there is a swath of additional material to be found over on AWS’s GitHub repo—particularly within the various “README” files:
An objective of this series of articles is to present solutions implemented using the “big three” cloud providers, in a way that aligns each solution as closely as possible.
For .NET developers, it’s a common practice to use dependency injection (DI) patterns in our code. Because of this, each of the sample solutions presented in this series includes examples of basic DI usage.
Two primary examples—AWS: .NET Core CLI and GitHub: Amazon.Lambda.APIGatewayEvents—show us most of the building blocks that we’ll be needing. However, there seems to be nothing in the official AWS docs/samples that shine a light on a recommended way to incorporate DI into our project.
This article—StackOverflow: How to use Dependency Injection in AWS Lambda C# implementation?—discusses the topic, including an interesting suggestion that we could:
ServiceCollection
. This then provides a place to register dependencies, in a way familiar to many .NET developers.The ideas presented in the StackOverflow solution result in code that looks somewhat “hammered into a different shape, just to support DI.” With that said, the solution works—and does so within a relatively small footprint of code.
Because of this, we’ll adopt a derivative of this approach in this article. Credit to those various contributors on StackOverflow, as they’re the ones that helped to figure this out. We’ll explore the actual code later in this article.
Note: It’s worth noting that AWS Lambda presents the interesting option of hosting an entire ASP.NET Web API application using “serverless” infrastructure. This would offer conventional startup code, including DI configuration and provision for improved local debugging, by virtue of being able to self-host. An example of this approach is detailed in GitHub: Amazon.Lambda.AspNetCoreServer.
However, in our scenario, this approach could be considered to be a somewhat heavyweight solution to a lightweight problem. Furthermore, this approach would also depart from any equivalency with our other “basic function” examples, as found in the other articles of this series—so we won’t explore this option further.
If you haven’t already done so, you need to install and configure the AWS CLI, as identified in the Requirements section above, before progressing.
With that done, we next need to download the .NET project templates specific to AWS Lambda Functions—these have been created by AWS.
dotnet new -i Amazon.Lambda.Templates
Great—now we’re ready, let’s create some code! Using our command prompt:
BlazorTelerikCloudDemo
.LambdaService
and navigate to it.md LambdaService
cd LambdaService
dotnet new lambda.EmptyFunction
The tooling will create a selection of project files for us. Incidentally, these are organized into src
and test
subfolders.
(Optional) Next, for consistency, we’ll add the Lambda project to the overall .NET solution. Because the AWS Templates scaffold a src
subfolder, we’ll additionally use the --in-root
parameter
to avoid creating unnecessary solution folders (when viewing with Visual Studio):
Enter the following:
cd..
dotnet sln add LambdaService\src\LambdaService --in-root
Before we progress further, let’s quickly test that our basic templated example system is working.
AWS Lambda Functions differ slightly from solutions provided by Azure and Google Cloud, by not offering the option to fully test/debug our code locally—either by hosting directly or by running from within a locally hosted runtime.
Instead, AWS presents a testing tool that provides a restricted way to test our code. It works by hooking its service into a debug build of our code (i.e., we will need to have compiled the project before using the tool).
Note: Incidentally, the testing tool happens to have been created using Blazor.
» Note that, although compiling a debug build is the default behavior, using the optional -c debug
parameter is a way for us to be certain.
cd <your repofolder>\BlazorTelerikCloudDemo\LambdaService\src\LambdaService
dotnet build -c debug
dotnet tool install -g Amazon.Lambda.TestTool-3.1
dotnet lambda-test-tool-3.1
» A new browser window containing the testing tool UI should also open.
FunctionHandler
method.» Enter the value "hello world"
(it’s essential to surround the text with quotes, otherwise we’ll see errors).
» We don’t need to change any of the other settings.
Note: Earlier we briefly mentioned that AWS supports hosting of an entire ASP.NET application within a Lambda serverless architecture—GitHub: Amazon.Lambda.AspNetCoreServer. This option could represent an alternative way to test code locally.
Next, we want our AWS Lambda Function to return some data.
» If we want, we can view that data directly in our browser:https://raw.githubusercontent.com/SiliconOrchid/BlazorTelerikCloudDemo/master/cloud-searchtrend-data.json
Note: The purpose of this is so that we can learn from a really simple system. We can always layer in more complexity later. It’s completely reasonable to assume that we wouldn’t use this approach in a real system. Instead, it’s much more likely that we would use a separate storage account or even query a database.
Within the top-level files of our newly templated “LambdaService” project, go ahead and create the following:
ApiImplementation.cs
.using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
using Amazon.Lambda.APIGatewayEvents;
namespace LambdaFunction
{
public class ApiImplementation
{
private readonly HttpClient _httpClient;
public ApiImplementation(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<APIGatewayHttpApiV2ProxyResponse> Run(APIGatewayHttpApiV2ProxyRequest apiGatewayHttpApiV2ProxyRequest)
{
//"APIGatewayHttpApiV2ProxyRequest" has been passed, to show us how to make it available ...
//... However, it's not actually used in this example!
var remoteDatset = "https://raw.githubusercontent.com/SiliconOrchid/BlazorTelerikCloudDemo/master/cloud-searchtrend-data.json";
var remoteResponse = await _httpClient.GetAsync(remoteDatset);
if (remoteResponse.IsSuccessStatusCode)
{
return new APIGatewayHttpApiV2ProxyResponse()
{
StatusCode = (int)HttpStatusCode.OK,
Body = await remoteResponse.Content.ReadAsStringAsync(),
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"}
}
};
}
else
{
return new APIGatewayHttpApiV2ProxyResponse()
{
StatusCode = (int)HttpStatusCode.InternalServerError,
Body = string.Empty,
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"}
}
};
}
}
}
}
If you’ve been following along with the solutions included in the Azure and GCF articles of this series, you’ll notice that this code is very similar to the API implementation in those examples.
What may be unfamiliar is that our implementation code has instead been placed into this separate ApiImplementation
class instead of being found directly into the Amazon.Lambda.Core.Function
class.
The reason for this relates to a point we highlighted earlier in this article—we need to create a way to support dependency injection.
Specifically in our case, we want to be able to inject an instance of System.Net.Http.HttpClient
into the ApiImplementation()
constructor.
Let’s inspect the above code more closely:
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
.» This is used to “wire-up” our class, with a serializer provided by AWS that lets our .NET code work with their wider cross-language platform.
_httpClient
to the class.» This forms part of the code that we use to make HttpClient
available through dependency injection.
» This also forms part of the code needed to make HttpClient
available through dependency injection.
Run
—we:» defined a hard-coded URL to a demonstration dataset. This is the JSON dataset stored on GitHub.
» used the HttpClient
to retrieve the dataset.
» returned an HTTP response containing the dataset along with the appropriate response status code.
Microsoft.AspNetCore.Http.HttpContext
(as for example, Google Cloud Function does)—instead we work with the AWS API-Gateway proxy-object Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse
(this is similar to how Azure Functions does it).» This maps to JSON message/payload, which, behind the scenes, is passed between the API Gateway service and the Lambda service (at this point the language used to develop the Function itself becomes irrelevant).
»
AWS documents the JSON payload in this article: AWS: Working with AWS Lambda proxy integrations for HTTP APIs.
» The proxy object gives us the option to customize our HTTP response and its headers (i.e., setting a response body, response-codes, content-types, etc.).
Note: A reference to
APIGatewayHttpApiV2ProxyRequest
has been included in our sample code above. This has been included purely to demonstrate how we could access it (e.g., adapting this example in the future). The inclusionAPIGatewayHttpApiV2ProxyRequest
adds nothing further to this example!
Next, we’ll add code that lets us configure dependency injection.
The AWS templating that we used, created a class named Function
—originally, it included sample code for a basic Lambda Function.
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace LambdaFunction
{
public class Function
{
private ServiceCollection _serviceCollection;
public Function()
{
ConfigureServices();
}
public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest apiGatewayHttpApiV2ProxyRequest)
{
using (ServiceProvider serviceProvider = _serviceCollection.BuildServiceProvider())
{
// entry to run app.
return await serviceProvider.GetService<ApiImplementation>().Run(apiGatewayHttpApiV2ProxyRequest);
}
}
private void ConfigureServices()
{
// add dependencies here
_serviceCollection = new ServiceCollection();
_serviceCollection.AddTransient<HttpClient>();
_serviceCollection.AddTransient<ApiImplementation>();
}
}
}
Let’s inspect the above code more closely:
ServiceCollection
object that will contain the collection of services that will be registered.ConfigureServices()
, called at startup, where we do the actual service registration.FunctionHandler
method:» We’ve removed code that would normally represent the API implementation.
» We introduced a mechanism that allows a service provider to be created using BuildServiceProvider()
.
» The code that normally
represents the API implementation has been moved to the separate class ApiImplementation
. We invoke that code using the ApiImplementation.Run()
method.
Later in this article, we’ll be slightly updating our Blazor application that we created in the second article.
The change we’re going to make is that, instead of retrieving the JSON dataset from a local file, we’ll revise the code to request the data from an HTTP endpoint (our new Lambda Function).
If we don’t change anything, a problem we’re about to run into is a CORS policy issue.
Explaining Cross-Origin Resource Sharing (CORS) is outside of the scope of this article, but very briefly: browsers implement security that, by default, prevents an application from just retrieving data from resources other than the immediate host. You can learn more here:
The AWS developer docs don’t seem to explicitly demonstrate how to do this using .NET, but fortunately we’ve already learned about the APIGatewayProxyResponse
proxy object—so it’s fairly straightforward for us to adapt.
Let’s make the necessary change to the code:
ApiImplementation.cs
.Access-Control-Allow-Origin
header, like this:...
Headers = new Dictionary<string, string>
{
{"Content-Type", "application/json"},
{"Access-Control-Allow-Origin", "*"}
}
...
In this case, we’ve used a wildcard entry to completely open up access. For security reasons, this is not at all recommended and is absolutely not something we should do in a real system. This step is intended as a convenience while learning.
Note: Hard-coding the setting makes it easier to learn what’s happening, but, looking ahead, we will probably be interested in supplementing this code with some form of environment-specific configuration.
… OK, sorry, that title was misleading.
AWS’s support for testing/debugging a locally hosted Lambda Function is frustratingly limited. The Lambda Function Test Tool, which we were introduced to earlier, isn’t intended for much more beyond basic testing. It does support the connection of a debugger, which goes some way to provide a better developer experience.
Although we could use the tool to demonstrate that our API is returning a result, it doesn’t actually host a function-as-a-service.
This means that we can’t connect our client-side Blazor application to it—so we’ll skip this part of the test.
Note: As another reminder, an alternative option to host an entire ASP.NET Core Server within a lambda serverless instance—which could present a solution for running the service locally. However, we’ve chosen not to go down that route.
Image credit: Jessica Lewis Creative on Pexels
Next, we’ll start moving our API service into the cloud. To do that, we first need to create some resources.
When we create any AWS resources, we have a large choice of data centers from around the globe. AWS uses the term “Region” to describe their major data centers.
For the purpose of this article, we’ll show examples that use a region called “eu-west-2” (which happens to be in London).
Normally, we should choose an AWS Region that is nearest to our customers. If we wanted to adapt the example in this article, we’d need to know what choices of “Azure Region” we have.
AWS Lambda is not available in all regions, so check availability using the doc below:
We can set the region as a default choice by adding it to our CLI configuration (along with credentials, etc.) using the CLI command:
aws configure
Alternatively, we can supply the region as an additional argument each time we create resources. We’ll demonstrate this option in subsequent CLI examples.
Tip: The AWS Console only lists resources for a particular region—if this is your first time using AWS, the console may have defaulted to showing a different region, from the one you are publishing to. There is a dropdown in the top-right of the page, next to your user information, that shows the currently selected region.
Next, we need to define an AWS IAM Execution Role, which is used to give the Function permission to use AWS resources.
This is a subject area that introduces just a bit too much complexity/detail to fit well within this article. Because of this, we need to jointly refer to the following guide:
To successfully create a new role and assign permissions, we will need to use the following commands. Important: The create-role command below is partial—meaning that you will need to supply additional arguments to make this work (as detailed in the above doc).
aws iam create-role --role-name lambda-role ..... <additional arguments required>
» We should name the role as “lambda-role” to fit with subsequent examples.
aws iam attach-role-policy --role-name lambda-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
» We should use the policy “AWSLambdaBasicExecutionRole.”
Note: In this article, we’ll be using the AWS CLI, but, if you prefer, using the web-based interface found in the AWS Console is also a really good option.
dotnet lambda deploy-function LambdaFunction --function-role lambda-role --region eu-west-2
LambdaFunction
is just a name and can be anything.» By coincidence it’s the same as the code namespace, but it doesn’t have to be.
lambda-role
is the name of an AWS IAM Role that we needed to prepare earlier.» When the app has finished deploying, the CLI will return some confirmation information. However, unlike solutions provided by Azure Functions or Google Cloud Functions, we don’t expect to find the URL that we can use to invoke the function for testing—that is something we must do separately.
dotnet lambda invoke-function LambdaFunction --cli-binary-format raw-in-base64-out
If everything has worked, we should expect to see a JSON dataset returned as output directly in the console.
Every resource in AWS is identified with a unique name called an Amazon Resource Name (ARN).
In a moment, we’re going to connect an “AWS API Gateway” to our Lambda Function, but, to do that, we’ll first need to know the ARN of our Function.
aws lambda get-function --function-name LambdaFunction
This command will return a chunk of information, but we’re only interested in the FunctionArn
value. It will look something very similar to this:
arn:aws:lambda:eu-west-2:123412341234:function:LambdaFunction
Tip: We should copy + paste the ARN to some form of notepad, as we’ll be wanting this string shortly.
AWS API Gateway can be used to create public HTTP endpoints that can trigger the events of various different AWS services.
As already mentioned, unlike other cloud providers, AWS Lambda Functions do not automatically provision an HTTP endpoint—so we need to create one using the AWS API Gateway service and integrate it to our Lambda Function.
We can learn about configuring the AWS API Gateway here:
Go ahead and enter the following command, adapting it to your own specific value of “function ARN” (that we made a note of a few steps earlier):
aws apigatewayv2 create-api --name DemoAPI --protocol-type HTTP --target arn:aws:lambda:eu-west-2:123412341234:function:LambdaFunction
DemoAPI
is just a name we give the API and can be anything.--target
needs to be that of the “LambdaFunction” that we made a note of, when we created the Function.When the command has run successfully, it will return various pieces of information that we should make a note of—especially the ApiEndPoint
and ApiId
values.
Tip: If you prefer to use the AWS Console rather than the CLI, the guide AWS: Getting started with API Gateway may be of interest to you.
AWS is relatively verbose, so in addition to creating resources, we need to associate the Endpoint to the Lambda Function and provide it with permission to execute the Lambda Function.
AWS: Working with HTTP APIs advises us that the “API Gateway must have the required permissions” … but then only provides generic guidance as to how we go about doing that.
Fortunately, a separate AWS support article shows us a way forward:
Below is cut + pasted from the above document, which describes the required format of the CLI command:
aws lambda add-permission \
--function-name "$YOUR_FUNCTION_ARN" \
--source-arn "arn:aws:execute-api:$API_GW_REGION:$YOUR_ACCOUNT:$API_GW_ID/*/$METHOD/$RESOURCE" \
--principal apigateway.amazonaws.com \
--statement-id $STATEMENT_ID \
--action lambda:InvokeFunction
The guidance in the above AWS support article takes us most of the way, but because the expected syntax is especially fiddly, we’ve included an additional example using real-world values (that worked successfully). This should help readers to make progress through this process:
aws lambda add-permission
--function-name "arn:aws:lambda:eu-west-2:123412341234:function:LambdaFunction"
--source-arn "arn:aws:execute-api:eu-west-2:123412341234:ntrvr685z4/*/$default"
--statement-id 7744ab49-1b3f-5036-8857-ac65adc352a9
--principal apigateway.amazonaws.com
--action lambda:InvokeFunction
Tip: As long as we don’t mind breaking away from the CLI and making use of using the AWS Console—the required permission-command can be found as an auto-generated snippet, buried away within the console. Look at the screenshot below for guidance:
At last, we’re in the final straight—let’s test that the API is working.
Earlier, when we initially created the API Gateway resource, the console responded with various information, including the URI of the endpoint.
If we forgot to make a note of that value when we created the resource, we can still get hold of that information again, using this command:
aws apigatewayv2 get-apis
ApiEndpoint
into a browser.If everything is behaving, we should receive a large payload of JSON as a response.
Tip: We should copy + paste the URL to some form of notepad, as we’ll be wanting this string again shortly.
Finally, let’s test our finished result:
Return back to the Blazor application that we created in the second article of this series:
/Pages/Index.razor
.OnInitializedAsync()
method like this (modifying the URL to include your unique name as appropriate):protected override async Task OnInitializedAsync()
{
//seriesData = await Http.GetFromJsonAsync<SeriesDataModel[]>("sample-data/cloud-searchtrend-data.json");
seriesData = await Http.GetFromJsonAsync<SeriesDataModel[]>("https://<YOUR_UNIQUE_API_ENDPOINT");
}
If you’re curious to learn how my experience with AWS stacks up against Azure and Google Cloud, I wrote more about this in Part 1. You can also learn more specifics in Part 3 on Azure and Part 5 on Google Cloud.
You may find it helpful to have awareness about the following subjects. If you get stuck, here are some useful articles:
Take your time to carefully work through the following documentation provided by AWS:
» AWS: Configuring the AWS CLI
» AWS: Using an IAM role in the AWS CLI—this link is the key bit that we need to complete.
» AWS: How can I fix the error “Unable to locate credentials” when I try to connect to my Amazon S3 bucket using the AWS CLI?—this link could be useful if we get stuck.
Thanks to Shawn Vause, an AWS-certified professional, for his AWS-specific technical review.
Thanks to Mandy Mowers & Layla Porter for the overall document review.
I always disclose my position, association or bias at the time of writing. As an author, I received a fee from Progress Telerik in exchange for the content found in this series of articles. I have no association with Microsoft, Amazon nor Google—this series of articles is in no way intended to offer biased opinion nor recommendation as to your eventual choice of cloud provider.
A UK-based freelance developer using largely Microsoft technologies, Jim McG has been working with web and backend services since the late ‘90s. Today, he generally gravitates around cloud-based solutions using C#, .NET Core and Azure, with a particular interest in serverless technologies. Most recently, he's been transitioning across to Unity and the amazing tech that is Augmented Reality.