Build a serverless API like a senior developer, Part 1 (revised)
EDIT 6/15/2021: I added the part at the end where I fix the unit tests after we get everything working with wiring up the repository to the query and getting an empty array from the HTTP request.
Part 1: Writing the base code for the API
There are already many, many tutorials out there that talk about getting baby’s first API endpoint up and running with AWS Lambda and AWS API Gateway. I want to go beyond the scope of other tutorials and at the same time lay everything out in a way that is very easy to understand and follow.
We are going to create a simple API with a GET method that simply retrieves information about video courses from a database table. However, in the course of this series, we are going to go over ideas such as infrastructure as code, automated testing, CI/CD, runtime dependency injection, error alerting, local AWS testing, and more.
In the first part of the tutorial series here, I will go over the base API code. Our API will be written in C# and this part of the tutorial series will only cover the .NET Core project setup and code. Remember, there is much more to software than just your code :)
This tutorial is written in fine detail, perhaps more detail than a more experienced developer will need. If you are in a section where you already understand the material, feel free to skip ahead.
To get started with Part 1, you will need the following installed:
Open Visual Studio 2019. Click “Create a New Project”. In the search bar at the top, start typing “AWS Serverless”.
Select the one that says “AWS Serverless Application (.NET Core — C#)”. Don’t select the one that says “AWS Serverless Application with Tests (.NET Core — C#)”, we will be adding our own test project later.
Click “Next”. Choose a name for your project and choose the location in which you want to save the project.
Click “Create”. Now another window will pop up with a bunch of options on what kind of AWS serverless project you want to create.
Select the one in the top left that says “ASP.NET Core Web API” and click “Finish”. Hooray, you have an API!
Feel free to skip to the next section if you just want to dive straight into the building.
Let’s go over each of the most important files created for us and what they do. Below is a tree diagram of the project setup.
│ SampleAPI.sln│└───SampleAPI │ appsettings.Development.json │ appsettings.json │ aws-lambda-tools-defaults.json │ LambdaEntryPoint.cs │ LocalEntryPoint.cs │ Readme.md │ SampleAPI.csproj │ SampleAPI.csproj.user │ serverless.template │ Startup.cs │ ├───bin │ └───Debug │ └───netcoreapp3.1 ├───Controllers │ ValuesController.cs │ ├───obj │ │ project.assets.json │ │ project.nuget.cache │ │ SampleAPI.csproj.nuget.dgspec.json │ │ SampleAPI.csproj.nuget.g.props │ │ SampleAPI.csproj.nuget.g.targets │ │ │ └───Debug │ └───netcoreapp3.1 │ .NETCoreApp,Version=v3.1.AssemblyAttributes.cs │ SampleAPI.AssemblyInfo.cs │ SampleAPI.AssemblyInfoInputs.cache │ SampleAPI.assets.cache │ SampleAPI.csprojAssemblyReference.cache │ └───Properties launchSettings.json
SampleAPI.sln
A .NET application is composed of a solution file and one or more projects. This is the main file that describes data needed to load the application project(s) and configure the computer architecture setup on which it runs. More detail can be found here.
appsettings.json and appsettings.[Environment].json
This provides variable value assignment for each environment in which your application runs. appsettings.json
applies to all environments unless overridden and appsettings.[Environment].json
is for specific environments.
aws-lambda-tools-defaults.json
This file describes default settings for your AWS account with respect to your application, such as region, runtime environment (labeled as “framework”), any serverless templates describing the application’s configuration (more on this later), and more.
LambdaEntryPoint.cs
The code in this file is what is executed when a project actually runs inside of AWS Lambda. A web host builder object is executing a startup method to set up the configuration of the running application. The UseStartup
method on line 38 allows for the execution of Startup.cs, which includes object-level configuration for your application. We will explain more shortly.
LocalEntryPoint.cs
The code in this file is what is executed when a project is running on your local machine as you are testing development. Like with any C# program, there is a Main method and inside of that, there is a method that will execute your object-level configuration from Startup.cs
.
SampleAPI.csproj
This file defines your C#-specific configuration settings for your project, including its runtime environment, its compilation settings, and a list of all external packages and versions for the project.
serverless.template
This file contains definitions for cloud resources and permissions needed for the lambda function to run in AWS. This file is an example of infrastructure as code, which allows us to define everything we need outside of our code to make it a usable software application. We will rework this file in a later part.
Startup.cs
This file creates specific objects required for your application to run. It allows for application level object configuration like allowing the project to use HTTPS redirection, authorization, and specific implementations of object interfaces required for your app.
ValuesController.cs
This file defines a set of HTTP endpoints within our API and what actions are taken when the specific URLs and HTTP methods are hit.
Installing NuGet packages
Okay, let’s get to it. The first thing we will need will be to import some NuGet packages to begin writing the code.
Right click on the SampleAPI
project name and select Manage NuGet Packages
.
Package upgrade detour
Actually, hold on, before we install more NuGet packages, let’s upgrade the one that exists in here now. Click on the package. Notice that next to the description inside the main window, we have v5.2.0
but underneath it, it says v5.3.0
. We're going to want to select the latest stable version of this package.
Keep in mind that 5.3.0 is the latest version of Amazon.Lambda.AspNetCoreServer as of this writing, which is February 15, 2021. If you are reading this at a later time, just keep the latest version of this package.
If you do not have any suggested package upgrades when you open NuGet Manager, feel free to skip to the next part.
Once you have clicked on the package, select the dropdown menu, scroll to the top of the menu and select Latest stable 5.3.0
and click the Update button. In just a little bit, you should see a window called Preview Changes pop up, which will highlight the old and new versions of the package and its dependencies. Click OK.
Now the information about your installed package should look as below. Notice that to the right of the description, there is only v5.3.0
.
Ooookay, now that we have upgraded the existing Amazon AspNetCoreServer, we can begin adding the other packages we will need. Go to the Browse tab next to Installed.
Hey, what do you know, the first package we need is right in front of our face, Newtonsoft.Json. This library will help us with object serialization and deserialization.
If you do not immediately see the Newtonsoft.Json
package, use the search bar to search for it.
At this point, you can click on the class, click the Install button, and then click OK when the Preview Changes window comes up.
Defining the project domain
Creating the Course
object
A popular paradigm for building software projects is domain-driven design, so we’re going to create a Domain
folder in our project. One of the central items will be the Course
object.
Create a new file Course.cs
in the Domain folder. To do this, right click on the Domain folder, hover over "Add", and then select "Class..." at the bottom of that menu.
In our case, our Course
will have a title, an instructor, a description, a category, and a list of associated videos. Inside of the class declaration start writing the following:
[JsonProperty("title")]
public String Title { get; }
C# allows for automatic getters and setters on object attributes by denoting { get; set; }
after you declare the attribute. In this case, we will only allow a public get
because we want the setting of object attributes to be handled by a constructor.
C# naming convention dictates that properties and methods of objects should be written as CamelCase. However, the JSON property we will want to return is title
with a lower case T. However, you will now receive an error on JsonProperty
. To remedy this, hover over the red error line, and then select "Show potential fixes".
The exact fix we need is already in here! Click on “using Newtonsoft.Json;”, and then that will import the library at the top of the file. Your complete file should look like this so far.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SampleAPI.Domain
{
public class Course
{
[JsonProperty("title")]
public String Title { get; }
}
}
Inside of Visual Studio, you will see some grayed out using
statements at the top of the file. This means your file is not using those statements. Feel free to just delete those lines.
Now we are going to want to add the following properties to the file.
[JsonProperty("instructor")]
public string Instructor { get; } [JsonProperty("category")]
public string Category { get; } [JsonProperty("description")]
public string Description { get; } [JsonProperty("videos")]
public List<Video> Videos { get; }
Now you will get another error. It will say something like “The type or namespace name ‘Video’ could not be found (are you missing a using directive or an assembly reference?)”. That’s okay, because we’re going to create that soon. In the meantime, we’re going to create our Course
constructor now that we have its properties.
public Course(string title, string instructor, string category, string description, List<Video> videos)
{
if (string.IsNullOrEmpty(title))
{
throw new ArgumentException("Course needs a name");
}
if (string.IsNullOrEmpty(instructor))
{
throw new ArgumentException("Course needs an instructor");
}
if (string.IsNullOrEmpty(category))
{
throw new ArgumentException("Course needs a category");
}
if (string.IsNullOrEmpty(description))
{
throw new ArgumentException("Course needs a description");
}
Title = title;
Instructor = instructor;
Category = category;
Description = description;
Videos = videos;
}
Here, we are using the constructor just to validate the object creation.
Creating the Video object
Oh yeah, we still have that error telling us we do not have a Video class. So let’s create that now.
Just like we did for Course.cs
, we are going to right click on the Domain folder and add a new file Video.cs
. Inside of the class declaration, we are going to write the following:
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("title")]
public string Description { get; set; }
[JsonProperty("title")]
public string SubsectionName { get; set; }
[JsonProperty("title")]
public string CourseName { get; set; }
As with what happened last time, we will get errors over JsonProperty
. Again, hover over the error, click on "Show potential fixes", and select to import Newtonsoft.Json. Feel free to delete any other import statements.
Now we want to make the constructor for this class. It will look like below.
public Video(string title, string subsection, string description, string course)
{
if (string.IsNullOrEmpty(title))
{
throw new ArgumentException("Video needs a title");
}
if (string.IsNullOrEmpty(subsection))
{
throw new ArgumentException("Video needs an subsection name");
}
if (string.IsNullOrEmpty(description))
{
throw new ArgumentException("Video needs a description");
}
if (string.IsNullOrEmpty(course))
{
throw new ArgumentException("Video needs a course");
}
Title = title;
SubsectionName = subsection;
Description = description;
CourseName = course;
}
Design considerations
That’s a lot of parameters, isn’t it? I was going to go with an anemic domain model here, but I figured we can at least have a constructor to protect and validate the creation of the object.
We’re not going to do any transformations to this object itself. We will only transform the “view” of a series of these Course
objects for when we send the information to the client.
In the future, if I want to add additional attributes to the class, what perhaps I could do is group certain attributes into their own objects and pass those in as parameters into the Course
constructor instead of with individual attributes.
Automated unit testing
If you are familiar with test-driven development (TDD), you will understand that it is (usually) preferable to write tests first and then write the code that passes those tests. The old adage goes as your tests become more specific, your code becomes more generalized.
To begin unit testing, we are going to need to create a new project inside of our solution. Right click on the solution and hover over Add, then click New Project.
We are choosing XUnit because it is expandable and flexible compared to other solutions like NUnit and the built-in MSTest. As you build more complex software solutions, flexibility will be of utmost importance, as you will need to be ready to change anything at any time.
Once the new project wizard pops up, type “XUnit” in the search bar at the top and hit Enter to see the unit test project types.
Select the third option from the top, xUnit Test Project (.NET Core)
with "C#" in the top right of the icon. Click Next, name your project SampleAPI.UnitTest, and click Create. Your project should now look like this.
We’re going to test this API from the levels of the controller, the data gathering and representation (which we will call the query layer). Let’s start with the query.
Right click on the UnitTest1.cs class and click Rename, renaming to QueryUnitTests.cs. Rename the Test1() method to TestGetCoursesQuery().
Now update your TestGetCoursesQuery() method to look like what is seen below.
[Fact]
public void TestGetCoursesQuery()
{
Query queryService = new Query();
CourseModel allCourseData = queryService.GetCourseInformation();
Assert.Equal("Baby's First Course", allCourseData.coursesList.FirstOrDefault(course => course.Title == "Baby's First Course").Title);
}
Whoa, what are we doing here?
We’re following the arrange act assert style of test writing. Basically, we will put together the test setup on which we want to act, then we will execute some sort of operation, and then we will check if the output is exactly what we want. Let’s go line by line.
First, we create a new Query object from which we are going to retrieve data to be presented.
Query queryService = new Query();
Next, we create a view model of type CourseModel to present the course data to the client populated with the Query method GetCourseInformation().
CourseModel allCourseData = queryService.GetCourseInformation();
Finally, we assert that from that course data, we get a course named “Baby’s First Course”.
Assert.Equal("Baby's First Course", allCourseData.coursesList.FirstOrDefault(course => course.Title == "Baby's First Course").Title);
Now, you’re going to see that we have a lot of red showing in our code in Visual Studio. We don’t have a Query object, and we don’t have a CourseModel object.
Let’s create those now.
Creating the Query layer
This is the first iteration of creating our query layer. To see a cleaner version, feel free to skip ahead to the cleanup section further down.
Let’s create a new folder now. Right click on the SampleAPI project and select Add, and then select New Folder.
Let’s call this Data
. We want to have a data repository layer from which we gather the raw data, and then a data query layer for getting specific pieces of data. But since we focused on the Query layer in the test, let's create that.
Inside of this data folder, we are going to create a Queries
folder and a Repositories folder. We will be following a repository pattern, where we will allow for the data storage to be a separate area from the rest of the code. This will allow for our code to account for any method of data storage, whether we get data directly from a database or from an API or wherever.
Create a new abstract class BaseQuery.cs. We want to make an class abstract so that we can implement an actual Query object for gathering and presenting specific data from the repository in whatever way we want, whether we want a mock query or we want an actual query. In either implementation, we will want to get course information.
We’re going to create an abstract method called GetCourseInformation. We wanted to call that in our unit test.
public abstract class BaseQuery
{
public abstract CourseModel GetCourseInformation();
}
Again, we want to get rid of any unused imports. When you’re trying to create a sophisticated application, you need to improve performance wherever you can. Everything is a tradeoff, but in the case of unused imports, removing those is a no-brainer. Your class should now look like this.
Okay, good stuff. Now we want to actually implement the abstract class BaseQuery. Let’s create an implementation in the same folder called Query.cs.
public class Query : BaseQuery
{
public override CourseModel GetCourseInformation()
{
List<Course> courseList = new List<Course>();
return new CourseModel(courseList);
}
}
All we’re doing right now is creating a new empty list of courses and returning it. We’ll come back to this, but first let’s implement the CourseModel view model.
This is the first iteration of creating our query layer. To see a cleaner version, feel free to skip ahead to the cleanup section further down.
Implementing the CourseModel view model
Let’s create yet another folder for our view models. We’re gonna call it Models. Create a new file called CourseModel.cs.
In this case, we’re just gonna keep it super simple. It’ll just be a List of Course
objects. Add this to your course model class.
public List<Course> coursesList { get; }
public CourseModel(List<Course> course_list)
{
coursesList = course_list;
}
And now we have little red underlines everywhere we have references to the Course
object. Remember, hover over any of the red underlined references and select "Show potential fixes".
We’re just going to want to reference the namespace in which our Course
object resides.
And now, like with the BaseQuery abstract class and many others, we have a set of unused using
statements. Let's whack those.
Your final class will look like this.
using SampleAPI.Domain;
using System.Collections.Generic;
namespace SampleAPI.Models
{
public class CourseModel
{
public List<Course> coursesList { get; }
public CourseModel(List<Course> course_list)
{
coursesList = course_list;
}
}
}
Okay, now let’s import this bad boy into our BaseQuery class. Again, you can hover over the red underline over CourseModel and click “Show potential fixes” and from there have it auto-generate the using
statement to import the SampleAPI.Models
namespace.
Your BaseQuery.cs file should now look like this.
using SampleAPI.Models;
namespace SampleAPI.Data.Queries
{
public abstract class BaseQuery
{
public abstract CourseModel GetCourseInformation();
}
}
At this point, you can now go back to Query.cs
and import the necessary namespaces here using the same methodology from earlier. Hover over your red underlines, click "Show potential fixes" and select the using
statement.
Your Query.cs
file should now look like this, with no errors.
using SampleAPI.Domain;
using SampleAPI.Models;
using System.Collections.Generic;
namespace SampleAPI.Data.Queries
{
public class Query : BaseQuery
{
public override CourseModel GetCourseInformation()
{
List<Course> courseList = new List<Course>();
return new CourseModel(courseList);
}
}
}
Now let’s modify the Query.cs
to fulfill our automated test condition. We're going to create a new Course
object and add it to our courseList
.
Add these two lines before you return the CourseModel
object. You can give whatever course details you want, as long as they can match what you're searching for in the automated test.
Course course = new Course("Baby's First Course", "Marwan Nakhaleh", "APIs", "this is a course", new List<Video>());
courseList.Add(course);
Executing the unit test
At this point, we’re ready to bring in the correct using
statements for the unit test and execute it. However, we will actually need to reference the SampleAPI
project since the classes we need are in a separate area from the unit test project. So we'll again hover over the red underlines, but now we'll see a different suggestion when we click "Show potential fixes".
Click on that “Add reference to SampleAPI” suggestion and then your unit test project will take a bit of time to reference the project.
If for whatever reason that doesn’t work, you can also right click the project in the Solution explorer, hover over Add, and then click “Add reference”.
You should be presented with this view. Select SampleAPI and then click OK at the bottom.
Now, after adding the reference and any projects that require additional using
statements, you should now have a unit test class that looks like this.
using SampleAPI.Data.Queries;
using SampleAPI.Models;
using System.Linq;
using Xunit;
namespace SampleAPI.UnitTest
{
public class QueryUnitTests
{
[Fact]
public void TestGetCoursesQuery()
{
Query queryService = new Query();
CourseModel allCourseData = queryService.GetCourseInformation();
Assert.Equal("Baby's First Course", allCourseData.coursesList.FirstOrDefault(course => course.Title == "Baby's First Course").Title);
}
}
}
You should now be able to right click anywhere inside the QueryUnitTests.cs
file and click "Run Test(s)".
Visual Studio will take a little bit to build your projects and then execute the tests. After a little bit, you should see a blue bar at the bottom saying your test passed. I highlighted it on the picture below.
To get more detail, you can click on View at the top and select “Test Explorer”.
This is what Test Explorer should look like for you. Here, you can see how long your test took to run, specific information about any failures, and much more.
Creating the Repository layer
This is the first iteration of creating our repository layer. To see a cleaner version, feel free to skip ahead to the cleanup section further down.
What about when we want to retrieve actual data from a data source? We still want to be able to test our code, to ensure we can get the specific data we need, but we want to be able to retrieve real data.
In this case, we’re going to use what’s called a repository to have a separate area within the application where data retrieval is handled. This makes it such that you don’t have to modify the rest of your code when the method in which you retrieve your data is changed.
Ideally, the way in which you design should allow you to make your changes easy, so that you can make the easy change, in the words of Kent Beck.
Above your test method in QueryUnitTests.cs
, add the following code.
private BaseRepository _dataRepository;
public QueryUnitTests()
{
_dataRepository = new MockRepository();
}
Next, inside your test method, update your line where you create your Query object from this:
Query queryService = new Query();
To this:
Query queryService = new Query(_dataRepository);
Once again, we see tons of red. What we’re going to do now is go back to that Repositories folder we created earlier and create another abstract class called BaseRepository.cs
.
Right now, we just need a way to store courses in our application. Add the following lines to this abstract class.
private List<Course> _courses { get; }
public BaseRepository()
{
_courses = new List<Course>();
}
public abstract List<Course> GetCourses();
After you import the needed libraries and delete the unneeded using
directives, your BaseRepository.cs
class should now look like this.
using SampleAPI.Domain;
using System.Collections.Generic;
namespace SampleAPI.Data.Repositories
{
public abstract class BaseRepository
{
private List<Course> _courses = new List<Course>();
public BaseRepository()
{
_courses = new List<Course>();
}
public abstract List<Course> GetCourses();
}
}
Since GetCourses()
is an abstract method, we can either add a basic implementation here that can be overridden, or we can just leave it as a statement.
Now we want to create a new class for a mock repository, as is needed in our unit test file.
Create a new folder called Mocks in your SampleAPI.UnitTest
project. Next, create a new file called MockRepository.cs
. This will inherit from our BaseRepository abstract class.
public class MockRepository : BaseRepository
Now we need to implement the GetCourses()
method. In this case, we will simply return a new List of made-up Course
objects. Feel free to get creative with the data in your Course
objects, but essentially, we need to create an object of type List<Course>
, create some Course
objects, add those to the course list, and return it. Mine looks like this.
public override List<Course> GetCourses()
{
Course course1 = new Course("Baby's First Course", "Marwan Nakhaleh", "APIs", "this is a course", new List<Video>());
Course course2 = new Course("Yet Another Course", "Marwan Nakhaleh", "APIs", "this is another course", new List<Video>());
_courses.Add(course1);
_courses.Add(course2);
return _courses;
}
Correct until you see no red, and then let’s go back to Query.cs
. We now need a constructor that can accept a BaseRepository parameter. Write the following above our GetCourseInformation()
method.
private readonly BaseRepository _repository;
public Query(BaseRepository repository)
{
_repository = repository;
}
We will now need to reference the namespace in which the BaseRepository
class resides.
Your Query.cs
should now look like this.
using SampleAPI.Data.Repositories;
using SampleAPI.Domain;
using SampleAPI.Models;
using System.Collections.Generic;
namespace SampleAPI.Data.Queries
{
public class Query : BaseQuery
{
private readonly BaseRepository _repository;
public Query(BaseRepository repository)
{
_repository = repository;
}
public override CourseModel GetCourseInformation()
{
List<Course> courseList = new List<Course>();
```Course``` course = new Course("Baby's First Course", "Marwan Nakhaleh", "APIs", "this is a course", new List<Video>());
courseList.Add(course);
return new CourseModel(courseList);
}
}
}
Let’s go back to QueryUnitTests.cs
in our SampleAPI.UnitTest
project and correct the red underlines.
Your unit test class should now look like this.
using SampleAPI.Data.Queries;
using SampleAPI.Data.Repositories;
using SampleAPI.Models;
using SampleAPI.UnitTest.Mocks;
using System.Linq;
using Xunit;
namespace SampleAPI.UnitTest
{
public class QueryUnitTests
{
private BaseRepository _dataRepository;
public QueryUnitTests()
{
_dataRepository = new MockRepository();
}
[Fact]
public void TestGetCoursesQuery()
{
Query queryService = new Query(_dataRepository);
CourseModel allCourseData = queryService.GetCourseInformation();
Assert.Equal("Baby's First Course", allCourseData.coursesList.FirstOrDefault(course => course.Title == "Baby's First Course").Title);
}
}
}
Run the unit tests again. They should still pass, but now we have a repository object accessible to our query layer.
Now we need to modify our Query.cs
so that it returns data from the repository instead of just making it up.
Modify your GetCourseInformation
method so that its body looks like this.
return new CourseModel(_repository._courses);
That’s it for the query! At this point we can re-run that unit test.
Uh oh, we have a unit test failure! Let’s see what it is… Object reference not set to an instance of an object on line 24. Skip to fixes.
Okay, let’s debug the unit test. Set a breakpoint for line 24 in the QueryUnitTests.cs
file. To set a break point, simply click to the left of the line number you want to set the breakpoint for.
Now, to debug the tests, you can right click anywhere inside the test class and instead of selecting Run Test(s), select Debug Test(s).
Basically, this allows us to stop execution of our code at the breakpoint. If you were to select Run Test(s), your breakpoint would be ignored and we would encounter the same failure.
Now that we’ve elected to debug our test, your execution will stop at line 24, where you set the breakpoint.
To see what data we have, start by hovering over where it says allCourseData
. When you hover over a variable while debugging, you can see its name and value. Here, you see its value is {SampleAPI.Models.CourseModel}, which means its value in this case is a CourseModel object. However, when you click on the little arrow on the left next to the allCourseData
dropdown, you will see there are zero Course
objects inside of the courseData.
Now let’s see why that might be the case. You can stop execution by clicking the little Stop button, as circled towards the top right.
Aw heck, I see it now. Go back to Query.cs
and locate this line.
return new CourseModel(_repository._courses);
Change it to the following.
return new CourseModel(_repository.GetCourses());
You should now be able to re-run your unit test and it will pass.
Editing the controller
This is the first iteration of creating our repository layer. To see a cleaner version, feel free to skip ahead to the cleanup section further down.
Now it’s time to edit the controller, where we actually will return the data for the client consuming the API.
Go ahead and delete the unused using
statements, then locate this line.
[Route("api/[controller]")]
Change it to look like this.
[Route("api/v1/[controller]")]
You know what, let’s go ahead and rename the controller from ValuesController.cs
to CourseController.cs
. When it asks you to rename the references in your code to reflect the new class name, click "Yes".
Now let’s just go ahead and delete the whole body of the controller. Your CourseController.cs
file should now look like this.
using Microsoft.AspNetCore.Mvc;
namespace SampleAPI.Controllers
{
[Route("api/v1/[controller]")]
public class CourseController : ControllerBase
{
}
}
Add the following content in the CourseController body.
private readonly BaseQuery _queryService;
public CourseController(BaseQuery queryService)
{
_queryService = queryService;
}
[HttpGet]
public IEnumerable<Course> Get()
{
return _queryService.GetCourseInformation().coursesList;
}
All we’re doing here is allowing for a GET request made to /api/v1/course to return a course list from our data repository.
But when the code is executed, how are we going to ensure our controller has a Query object to call from? We’re going to modify our Startup.cs
file and make these dependencies available at runtime.
In the ConfigureServices
method in Startup.cs
, locate this line.
services.AddControllers();
Add the following underneath.
services.AddAWSService<IAmazonDynamoDB>();
services.AddScoped<BaseRepository, Repository>();
services.AddScoped<BaseQuery, Query>();
Now we have some red to correct. Hover over the first line and select “Show potential fixes”. Elect to install the Amazon.DynamoDBv2 package.
Once you have that… aw hell, we have another error.
IServiceCollection does not contain a definition for AddAWSService. This actually will require us to go back to our NuGet packages and install AWSSDK.Extensions.NETCore.Setup
.
Follow the prompts similar to how we did at the beginning of the tutorial, and go back to Startup.cs
. Hooray, no error when we try to call AddAWSService()
!
Go ahead and bring in the necessary using
statements for our other errors. Your complete Startup.cs
file will now look like this.
using Amazon.DynamoDBv2;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SampleAPI.Data.Queries;
using SampleAPI.Data.Repositories;
namespace SampleAPI
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public static IConfiguration Configuration { get; private set; }
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAWSService<IAmazonDynamoDB>();
services.AddScoped<BaseRepository, Repository>();
services.AddScoped<BaseQuery, Query>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Welcome to running ASP.NET Core on AWS Lambda");
});
});
}
}
}
Now, we just need to make an actual Repository class to implement the BaseRepository abstract class.
In this Part 1 tutorial, we will not test calling an actual database. We will have a mock database for our Repository and in the next part, we will set up DynamoDB and make calls.
Create a new class in the Repositories folder inside Data and call it Repository.cs
. Make sure to inherit the abstract class BaseRepository
. Add the following inside of your Repository class.
public override List<Course> GetCourses()
{
return _courses;
}
After making the necessary references and removing unused statements, your final Repository.cs
class should now look like this.
using SampleAPI.Domain;
using System.Collections.Generic;
namespace SampleAPI.Data.Repositories
{
public class Repository : BaseRepository
{
public override List<Course> GetCourses()
{
return _courses;
}
}
}
Logging
Background information
Any real-world application will need application logs for developers to see into their code and get a look at what exactly is going on while the application is live.
However, when you have real-world applications running and you need to find an issue, you may need to sift through hundreds of thousands of log messages coming from direct or indirect execution of your code. Different points in the code will need what are called different log levels. The main log levels, in order or increasing severity, are Debug, Information, Warning, Error, and Critical.
Essentially, you’ll want different attention on different events in your application. For example, you may want to log each change in state of your application, but you will want raised attention on an application crash within this state. To differentiate between these items, you may log the state changes as Information (or even Debug) and the application crash as Critical.
The different log levels will also come in handy when you need to sift through many many many log messages and you need to narrow down only to application errors or critical events.
Implementation
Go back to the CourseController.cs
file and locate this line.
private readonly BaseQuery _queryService;
Underneath it, add this line.
private readonly ILogger _logger;
Next, locate this line.
public CourseController(BaseQuery queryService)
And update it to look like this.
public CourseController(BaseQuery queryService, ILoggerFactory logger)
Now inside of your CourseController
constructor, add this line.
_logger = logger.CreateLogger("CourseController");
Correct the red underlines by importing Microsoft.Extensions.Logging. Do not use Amazon.Runtime.Internal.Util. We want to use Microsoft’s logging in this case.
Now, in your Get
method, make a log message! Before the Query layer executes, write something like this.
_logger.LogInformation("starting GET /api/v1/courses");
Now we can execute the API (finally!). Go ahead and start the API with the big fat Play button with “IIS Express” written next to it at the top center. I’ve circled it for you.
A few seconds after you click the Play button, a browser window will open up with your API running on your local web server. Yes, I’m using Microsoft Edge. It does wonders for my battery life.
Open up Postman and copy the URL from your new browser window into Postman, followed by our API route from the controller, which will be /api/v1/course
. Remember it's a GET request.
In my case, my full API URL will be https://localhost:44339/api/v1/course
.
Hit the big blue Send button on the right and you’ll get a 200 OK response!
Now, if we want to see our logs, we can go back to the running Visual Studio window, we can take a look at our Output window.
Click on the little dropdown menu where it says “Show output from: Debug”, and select “SampleAPI — ASP.NET Core Web Server”, and we will actually see our application output, with that “Show output from” selection marked in red and the log message we wrote marked in green.
All right! We have a logger implemented, along with a custom log message. We only have a little bit of cleanup left for our code setup (we will set up more configuration and an actual database table in Part 2).
Kill the server by hitting the Stop button a little bit right of top center.
Code improvements
Here are some convenient links for if you skipped ahead from the query layer or repository layer sections and would like to go back.
We want to add logs to the query and repository layers, and we want to clean up their setup a little bit.
Go back to BaseRepository.cs
and locate this line.
private List<Course> _courses { get; }
Change it to this.
protected List<Course> _courses { get; }
We only want this _course
variable accessible to BaseRepository
and any classes that implement it. If we set the variable modifier to private
, it will only be accessible to our BaseRepository
class.
Next, underneath that line, add this.
protected ILogger _logger;
After that, inside of the BaseRepository
constructor, locate this line.
_courses = new List<Course>();
Beneath that, add this.
_logger = logger.CreateLogger("BaseRepository");
We have now cleaned up our BaseRepository
class. At this point, your BaseRepository.cs
file should look like this.
using Microsoft.Extensions.Logging;
using SampleAPI.Domain;
using System.Collections.Generic;
namespace SampleAPI.Data.Repositories
{
public abstract class BaseRepository
{
protected List<Course> _courses { get; }
protected ILogger _logger;
public BaseRepository(ILoggerFactory logger)
{
_courses = new List<Course>();
_logger = logger.CreateLogger("BaseRepository");
}
public abstract List<Course> GetCourses();
}
}
Next, open up Repository.cs
.
Locate this line in the file.
public class Repository : BaseRepository
Modify it to look like this.
public sealed class Repository : BaseRepository
The sealed
keyword in C# makes it such that no class can inherit from Repository
.
We will now want to create a Repository
constructor that extends that of its base class. To do that, add the following the class.
public Repository(ILoggerFactory logger) : base(logger)
{
_logger = logger.CreateLogger("Repository");
}
We are inheriting the constructor from BaseRepository
, but we're creating our logger and calling it Repository
instead of BaseRepository
Finally, inside of our GetCourses
method, we will want to add this line before we actually retrieve course data.
_logger.LogInformation("returning course information");
At this point, your Repository.cs
file should look like this.
using Microsoft.Extensions.Logging;
using SampleAPI.Domain;
using System.Collections.Generic;
namespace SampleAPI.Data.Repositories
{
public sealed class Repository : BaseRepository
{
public Repository(ILoggerFactory logger) : base(logger)
{
_logger = logger.CreateLogger("Repository");
}
public override List<Course> GetCourses()
{
_logger.LogInformation("returning course information");
return _courses;
}
}
}
Now, open the BaseQuery.cs
file. Add the following above the GetCourseInformation
method definition.
protected ILogger _logger;
protected BaseRepository _repository;
public BaseQuery(BaseRepository repository, ILoggerFactory logger)
{
_repository = repository;
_logger = logger.CreateLogger("BaseQuery");
}
Frankly, there should have been a constructor created at the beginning, but that’s okay, because we can modify our code easily and quickly when it’s set up in a clean way.
Remember to correct the red underlines.
At this point, your BaseQuery.cs
file should look like this.
using Microsoft.Extensions.Logging;
using SampleAPI.Data.Repositories;
using SampleAPI.Models;
namespace SampleAPI.Data.Queries
{
public abstract class BaseQuery
{
protected ILogger _logger;
protected BaseRepository _repository;
public BaseQuery(BaseRepository repository, ILoggerFactory logger)
{
_repository = repository;
_logger = logger.CreateLogger("BaseQuery");
}
public abstract CourseModel GetCourseInformation();
}
}
Now, we can fix the Query
class. Open the Query.cs
file.
Locate this line in the file and delete it.
private readonly BaseRepository _repository;
Next, find this line in the file.
public Query(BaseRepository repository)
Modify it to look like this.
public Query(BaseRepository repository, ILoggerFactory logger) : base(repository, logger)
Inside of this constructor, add the following line.
_logger = logger.CreateLogger("Query");
Now, in the GetCourseInformation()
method, before we actually make the query call, add this line.
_logger.LogInformation("retrieving course data for client");
Delete the now unused using
statements at the top.
YourQuery.cs
class should look like this.
using Microsoft.Extensions.Logging;
using SampleAPI.Data.Repositories;
using SampleAPI.Models;
namespace SampleAPI.Data.Queries
{
public sealed class Query : BaseQuery
{
public Query(BaseRepository repository, ILoggerFactory logger) : base(repository, logger)
{
_repository = repository;
_logger = logger.CreateLogger("Query");
}
public override CourseModel GetCourseInformation()
{
_logger.LogInformation("retrieving course data for client");
return new CourseModel(_repository.GetCourses());
}
}
}
Testing the API
If you go ahead and click the IIS Express
button in the top middle.
In a few seconds, you will see a browser window pop up. It should look something like this.
Your web application is running on localhost
. Your port number may be different from mine, but you should see the same text in the actual browser window.
Now you need to test against the specific endpoint we made, which is /api/v1/course
. Open a Postman window and create a new request. You do this by clicking the plus button at the top towards of the left sidebar.
Since this is a GET request, there is no need to modify the request type. In the bar where it says “Enter request URL”, copy the URL from the browser window that opened when we executed our web server and paste it there, then add /api/v1/course
.
Now hit the big blue Send button on the right, and you should see an empty array! This means our API is working successfully. In the next part, we will populate this with test data from a local testing database table.
Now let’s make sure our unit test still works. Stop execution of the web server by clicking the red “stop” button.
Go back to the Test Explorer and execute the tests again.
Uh oh… looks like we introduced some code that broke our unit tests.
The issue is that since we introduced a logger factory in the Query and BaseRepository classes, we hadn’t updated our unit tests so it can handle those.
Fixing the unit tests
Let’s go back to MockRepository.cs
and add a proper constructor.
public MockRepository(ILoggerFactory logger) : base(logger)
{
_logger = logger.CreateLogger<MockRepository>();
}
You’ll get an error where it says ILoggerFactory
. Hover over it, click “Show potential fixes”, and select using Microsoft.Extensions.Logging;
. If that doesn’t show up for whatever reason, just add that code to a new line at the top of a file.
Now we will need to add a _logger
to our QueryUnitTests.cs
file. The issue with the test files is that the MockRepository
and the Query
instantiations do not have that logger in the constructor in the unit tests. Therefore, we will have to create that logger and add it to these classes.
Add the following line in QueryUnitTests.cs
under where it says private BaseRepository _dataRepository;
.
private ILoggerFactory _logger;
Hover over the error and click “Show potential fixes”, then select using Microsoft.Extensions.Logging;
.
Next, add this line above the first line of the QueryUnitTests constructor.
_logger = new NullLoggerFactory();
Hover over the error and click “Show potential fixes”, then select using Microsoft.Extensions.Logging.Abstractions;
.
After that, modify the line that says _dataRepository = new MockRepository()
to say _dataRepository = new MockRepository(_logger)
.
Finally, modify where it says Query queryService = new Query(_dataRepository);
to say Query queryService = new Query(_dataRepository, _logger);
and save the file.
You should now be able to go back to the Test Explorer, run your tests, and you will once again have a unit test project that compiles and executes successfully!
The End
Congratulations on getting to the end of Part 1! I hope you learned some new things.
In the next part, we will introduce local database testing, we will configure the API to access the database table, and we will retrieve the database table records in the API.
Full code can be found here.
Glossary
Anemic domain: This is the idea of having an object made of only properties, such that the validation and transformation of the object resides in another class. Often times, this is considered an antipattern in object oriented programming because one of the central ideas of object-oriented programming is to combine data and processing/transformation. Further reading can be found here. Back to reference
Arrange act assert: A methodology of writing automated tests that dictates data setup, then data transformation, then assertion that the transformation looks the way you want. Further reading can be found here. Back to reference
Deserialization: The act of converting a C# object to a sequence of bytes, but in the case of this project, the C# object is converted to a JSON object. Back to reference
Domain-driven design: A software development paradigm where the heaviest focus is on the customer-oriented objects with which the app needs to interact. Read more on this blog post. Back to reference
Log level: This refers to the severity of the message logged by your application. The default set of logging levels, in order of severity, are Debug, Information, Warning, Error, and Critical. Back to reference
NuGet package: A downloadable code library that contains compiled code to be used in a C# project. Back to reference
Repository: Refers to a data store within your application. It is written as code to separate the method in which you obtain your data from how the rest of the code operates. Back to reference
Serialization: The act of converting a sequence of bytes to a C# object, but in the case of this project, it is a JSON object that is converted to the C# object. Back to reference
View model: A representation of data that will be given to a client. Back to reference