AWS Developer Tools Blog

Introducing .NET Core Support for AWS Amplify Backend Functions

Earlier this month, the AWS Amplify team announced support for backend functions that use runtimes beyond the existing support for Node.js. With this new feature, customers can now write backend functions using Python, Java, Go, and .NET Core to handle requests from their REST or GraphQL APIs, triggers from services like Amazon DynamoDB and Amazon Kinesis Data Streams, or even scheduled periodic execution using Amplify CLI’s new support for cron-based Lambdas. In this article, I’m going to focus on the support for .NET Core. I’ll walk you through the process for creating a REST API backend written in C# and deployed to your AWS environment by Amplify.

For this article, we’ll use a browser-hosted front-end application written in Angular. Amplify also supports React and Vue, and the steps we’ll be following below have equivalents for those libraries. For more information, see the Getting Started guide in Amplify’s documentation, under “Framework Support.”

Preparing our Application

First, let’s ensure we have installed the tools we’ll need. In the instructions below we will walk you through the process of installing the following dependencies for our application:

For this example, we’ll be building an Angular front-end application, and using the Angular Command Line Interface (CLI) to create it. If you don’t already have the Angular CLI tool installed, you can install it following the guide at https://angular.io/guide/setup-local, which walks you through the process of installing the CLI. If you already have the latest version of the Amplify CLI installed, you can upgrade it by running npm i -g @aws-amplify/cli.

Let’s create our Angular application. Once the CLI has been installed, we can create our application using the ng new command. For the purposes of this walk-through, we’ll keep things simple and accept the default options the Angular CLI uses for a new application:

ng new AmplifyDotnet --defaults

Let’s run the application locally to verify everything’s working. The Angular CLI will have placed the application’s components into a new sub-folder named “AmplifyDotnet,” so let’s navigate our command prompt to this folder by running cd AmplifyDotnet. Please note that all of the commands we’ll be running for the rest of this example will be from this location. Once we’re in this folder, we can serve the application using the ng serve command. This will start a lightweight Web server on your machine that will allow you to connect your Web browser to your application.

By default, the application is served at http://localhost:4200, but the output of the ng serve command will indicate if it is using a different port. If you connect your web browser to this address, you should see a welcome page that shows our application is working. Incidentally, one of the features of the ng serve command is that it will automatically pick up any changes we make to the application, so we’ll leave this command running.

Adding a REST API for our Application

Adding Amplify to our Project

Now that we have our front-end working, we’ll add Amplify support to the project. Again, we’re letting the previous command continue to run, so let’s open a new command window and navigate to the project’s folder. We will now initialize Amplify:

  1. From a command-line prompt in our Angular project, run the amplify init command.
  2. You will be prompted for information regarding this project; you can learn more about project initialization from the Amplify Documentation on the init process. For this article, we will simply accept the default values provided by the CLI, with the exception of the environment name for which there is no default offered; for this article, we’ll use an environment name of dev.
  3. After answering the remaining prompts, Amplify will set up an environment hosted in AWS to which we will add our other backend components later in this example.

Adding a .NET Core Function

Amplify provides support for several types of cloud-hosted backend resources, such as Amazon S3 buckets, Amazon DynamoDB tables, AWS Lambda functions, Amazon API Gateway endpoints, and others. These different resource types are exposed as categories through the Amplify CLI, with each category offering its own set of specific commands. In the case of Lambda, Amplify includes a function category, and we will use the add command to create a new function:

amplify function add

We will now be prompted for information regarding our function:

  1. A name for the resource that will be created. This is a “human-friendly” name that will be used to uniquely identify the resource in your Amplify project when you view the list of resources, and will also be used to generate a name for the AWS CloudFormation stack used to deploy it. I’ll use “ServerlessDotnet”, but you can use another name if you prefer.
  2. A name for your function, which corresponds to the name of the Lambda resource that will be deployed. By default, the CLI will use the name of the resource, but again, this can be changed.
  3. The runtime for your function. By default, this is set to “NodeJS”, so let’s change it to “.NET Core 3.1”.
  4. The template we’d like to use for the function. We intend to expose this function via API Gateway, so let’s choose the “Serverless” template.
  5. Whether we will need access to any other resources defined in this Amplify project; this will be a standalone REST API, so we will answer “no”.
  6. Whether we would like this function to be invoked on a recurring schedule; in this case, we intend to have it respond to HTTP requests to our API Gateway, so we will answer “no”.
  7. Whether we would like to edit the source for the Lambda handler; we will skip this step for now, so we will answer “no” here as well.

The code for our function will be generated using the names, runtime, and template we entered in the steps above and placed under an “amplify/backend/function” sub-folder named using the name we provided for our resource. Under this folder, there is a “src” sub-folder that contains a C# project; if you like, you can open this project in Visual Studio or Visual Studio Code and inspect the source for our Lambda handler. If you do inspect the source, you will note that our C# Lambda handler’s code is using the strongly-typed classes for receiving and responding to API Gateway requests defined in the Amazon.Lambda.APIGatewayEvents NuGet package and currently includes hard-coded responses:

public async Task<APIGatewayProxyResponse> LambdaHandler(
    APIGatewayProxyRequest request, 
    ILambdaContext context)
{
    var response = new APIGatewayProxyResponse {
        Headers = new Dictionary<string, string> {
            { "Access-Control-Allow-Origin", "*" },
            { 
                "Access-Control-Allow-Headers", 
                "Origin, X-Requested-With, Content-Type, Accept" 
            }
        }
    };

    string contentType = null;
    request.Headers?.TryGetValue("Content-Type", out contentType);

    switch (request.HttpMethod) {
        case "GET":
            context.Logger.LogLine($"Get Request: {request.Path}\n");
            response.StatusCode = (int)HttpStatusCode.OK;
            response.Body = "{ \"message\": \"Hello AWS Serverless\" }";
            response.Headers["Content-Type"] = "application/json";
            break;
        case "POST":
            context.Logger.LogLine($"Post Request: {request.Path}\n");
            if (!String.IsNullOrEmpty(contentType)) {
                context.Logger.LogLine($"Content type: {contentType}");
            }
            context.Logger.LogLine($"Body: {request.Body}");
            response.StatusCode = (int)HttpStatusCode.OK;
            break;
        case "PUT":
            context.Logger.LogLine($"Put Request: {request.Path}\n");
            if (!String.IsNullOrEmpty(contentType)) {
                context.Logger.LogLine($"Content type: {contentType}");
            }
            context.Logger.LogLine($"Body: {request.Body}");
            response.StatusCode = (int)HttpStatusCode.OK;
            break;
        case "DELETE":
            context.Logger.LogLine($"Delete Request: {request.Path}\n");
            response.StatusCode = (int)HttpStatusCode.OK;
            break;
        default:
            context.Logger.LogLine(
                $"Unrecognized verb {request.HttpMethod}\n"
            );
            response.StatusCode = (int)HttpStatusCode.BadRequest;
            break;
    }

    return response;
}

In a real-world application, we would likely access other AWS resources and/or calling other REST APIs, but for the purpose of this example we will leave the code as-is.

Let’s deploy our function. If we return to our command prompt, we can use the amplify push command to deploy our project’s resources to our AWS environment. After executing the command, you will be presented with a list of resources that will be changed along with a prompt to indicate whether you would like to proceed. We’ll enter “Y” to proceed, but in the future, if you would like to skip this prompt (which can be useful in CI/CD environments), you can use the “–yes” flag to bypass it:
amplify push --yes

The deployment process will compile and package our .NET code for deployment, and after a few minutes, a new Lambda function will be created that will run our code. If you open your AWS console and navigate to the Lambda dashboard, you will see the new function; note that the name of your function will include a suffix that indicates the environment to which it belongs; this is added to prevent multiple developers/Amplify environments from accidentally overwriting each other.

Now that you have a way to get your code deployed, one thing you are likely to want to do during the development of your function’s code is to test the execution of the function. We could test from within the AWS Lambda console, but this can be a time-consuming process to wait for each deployment to complete before tests can be performed. To make testing more convenient, Amplify provides a mechanism to simulate the Lambda execution environment on your local development machine using the amplify mock category. It does not simulate the entire Lambda execution environment, but it does provide a way to perform quick testing of basic functionality. This command allows you to run the code for a specific function, passing in a JSON file that contains any event data you expect to have passed to the function. Based upon the template you selected, a sample “event.json” file was generated containing an example of a typical event and placed in the “src” folder. The path to this file will be the default path presented by the command (note that this path is relative to the function resource’s src folder path).

Let’s test our function now; note that, if you used a different name for your resource than the one we used above, you will change the “ServerlessDotnet” name below with the name you entered. When prompted for the path to your event.json file, you can simply hit <enter> to use the sample event file provided:
amplify mock function ServerlessDotnet

The mocking process will compile your C# code, create the simulated hosting environment, load the assembly containing your Lambda handler, and invoke it, passing the contents of the specified event.json file as the event data. Similar to the amplify push command above, you can make the mock command more scripting or CI/CD-friendly by providing the path to the event file from the command-line using the “–event <path to event file>” command-line option. Again, note that this path is relative to the folder created for the resource (under “amplify/backend/function/<resource name>/src”). This feature also allows you to define multiple event files in order to pass different event types to your code to verify logic.

Invoking our Function via API Gateway

In addition to Lambda functions, Amplify supports the ability to define API Gateway resources that can host an HTTPS interface for interacting with these functions via an API category. This category allows you to define resource paths, indicate what functions are invoked when these paths are accessed, and configure how you authorize user access. Let’s add an API Gateway to our project:
amplify api add

We will be presented with a set of prompts:

  1. The API type (GraphQL/REST API). For this function, we’re exposing it as a REST API through API Gateway.
  2. A friendly name for the resource. Again, this is a “human-friendly” name that will be used to uniquely identify the resource in your Amplify project. We’ll use “ServerlessAPI,” but if you prefer you can use a different name.
  3. A path to our items. This will be a path relative to the path exposed by our API Gateway; the default will be “/items,” and we will hit <enter> to accept this. Note that API Gateway will automatically create a child resource at this path named “{proxy+}” that responds to any HTTP verb, passing the full API Gateway event (including path information, HTTP verb, and query string parameters) to your Lambda function.
  4. Selection of the Lambda function to be invoked. Since we’ve already defined our Lambda function, we will select “Use a Lambda function already added in the current Amplify project,” and choose our previously-defined function.
  5. Whether you will restrict access to this function. For the purpose of this example, we will choose “N” to disable authorization, but in a production application it is recommended that you add an “auth” resource to add Cognito-based authentication and choose whether to restrict access to only authenticated users.
  6. Whether you would like to specify any additional paths. We will only define the “/items” path for now, so we’ll select “N”.

Now that we have defined the API Gateway, we can deploy our latest changes to the cloud by calling the amplify push command again. Along with creating our new API Gateway in AWS, this process will also generate a new file in the “src” folder of our Angular application named “aws-exports.js.” If you open this file, you will see that it contains JSON defining an awsmobile object, and if you inspect this object you will see a property named aws_cloud_logic_custom that contains the definition of our API, including a property named endpoint. This is the base URL for our API, and the resource we defined above will be accessible from the path we chose in step 3 above. For example, if we chose the default of “/items”, our complete path will be the URL defined in that endpoint property followed by “/items.” If we enter this into your browser’s address bar and hit enter, we should see a response from your Lambda’s code:

{
  "message": "Hello AWS Serverless"
}

Adding Amplify API Support to Our Angular Application

Now that we have created the backend resources necessary to support our application, let’s add code to our application to allow our frontend to exchange data with our backend.

Adding the Amplify library

Note: The below steps are also outlined in Amplify’s “Getting Started” guide for Angular.

In order to use Amplify in our application’s code, we will need to install the “aws-amplify” package that defines the programming interfaces Amplify provides to our code.
npm i --save aws-amplify

Starting with Angular 6 and above, a change must be made to the “polyfills.ts” file in order for Amplify to work. In particular, two global objects that were previously exposed by the Angular runtime (window.global and window.process) that the Amplify libraries depend upon were removed, so we’ll need to add them back. To resolve this, open “polyfills.ts” and add the below code:

(window as any).global = window;
(window as any).process = { 
  env: { DEBUG: undefined }, 
};

Configure Amplify in our Application

Next, we will make our application aware of the Amplify environment’s configuration. Whenever you perform a push or publish operation, Amplify updates a file in the root of our application’s source named “aws-exports.js;” this file provides information about the current Amplify environment such as API endpoints and authentication configuration. Modify main.ts to include the import of this file, along with the “Amplify” class:

import Amplify from '@aws-amplify/core';
import API from '@aws-amplify/auth';
import awsconfig from './aws-exports';

Also in this main.ts file, add the below lines:

Amplify.configure(awsconfig);
API.configure(awsconfig);

Modify our app component

Now it’s time to see the fruits of our labor! Let’s modify our app Component to render a response from our new Serverless API. Let’s modify app.component.ts to call our API and expose the response.

import { Component } from '@angular/core';
import API from '@aws-amplify/api';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  public response: string;
  constructor() {
    API
        .get('ServerlessAPI', '/items/', {})
        .then(data => this.response = data.message)
        .catch(err => console.log(`ERROR: ${err}`));
  }
}

Note a few things that we’re doing in the above code:

  1. We import the class Amplify exposes for for calling REST APIs by adding the below to the imports:
    import API from '@aws-amplify/api';
  2. We add a new public field of type string named “response” to the class.
  3. We added a constructor to the class, and included a GET call to our API’s “/items” path, and when a response returns, set the value of the response field from step 2 to the value of the message property on the returned object.

Let’s replace the original contents of our app component’s HTML with the value of our Component’s response field. Open app.component.html, and replace the contents with the below:

<!-- This should be the entire contents of app.component.html -->
{{response}}

After we save the changes we’ve made to these files, we should now see our Angular app displaying the message from our .NET backend: “Hello AWS Serverless.”

Cleaning Up

After we are done with our application, you will probably want to delete the resources Amplify created for you. The Amplify CLI offers a command that can handle this; if you run amplify delete, the Amplify engine will remove all of the resources that were provisioned in the cloud for you and remove the local Amplify environments and configuration.

Conclusion

In this article, we introduced AWS Amplify’s new support for writing backend functions in .NET Core 3.1. With this new runtime support, customers now have the ability to leverage their existing C# and .NET Core skills to write AWS Lambda functions that can deliver and process data to and from their front-end applications. We also demonstrated how, with a few lines of code, developers can incorporate their new .NET backend into an existing Angular application using Amplify’s API object.

With Amplify’s new extensibility model for runtimes and function templates, we hope to add more languages and samples to the CLI. What would you like to see? If you have an idea for a great feature that would make your application development easier or enables you to do new things, please file a feature request in the AWS Amplify GitHub repository. Our team monitors the repository and we’re always interested in developer feedback. We can’t wait to see what you build in C# using AWS Amplify!