ASP.NET Core 3.1 Web API Quick Start

Dustin Wilcock
Imagine Learning Engineering
12 min readJul 13, 2020

Welcome to a walk-through of how to create an ASP.NET Core 3.1 Web API application from the ground up. We’ll make it a useful service modeling a real world scenario with a data store and integration tests. Along the way we’ll highlight some of what’s new and cool in ASP.NET Core 3.1.

It is assumed you have a basic knowledge of C#, .NET Core, dependency injection, RESTful API’s, HTTP, JSON, etc. You’ll need the following development tools installed:

  1. Visual Studio 2019 Community, or Visual Studio Code (with C# extension), or other editor
  2. .NET Core 3.1 SDK latest edition

Create a Web API project

We’ll use the .NET Core CLI to create and manage our project, for a cross-platform, editor-agnostic path that works for all. Although know that each of these steps (like creating a project or adding a package reference) has a Visual Studio IDE equivalent, which may be preferred by some.

Open a command terminal in an empty directory and run:

dotnet new sln -n QuickStart

This creates a new solution file to house our project with a name of QuickStart.sln. Now to create a project:

dotnet new webapi -n QuickStart

The webapi argument indicates to use the ASP.NET Core Web API template to create a new project in a new sub-directory with a name of QuickStart. Then to add the project to the solution:

dotnet sln QuickStart.sln add ./QuickStart/QuickStart.csproj

Following best software craftsmanship principles, we’ll create a test project to exercise our code, and add it to the solution:

dotnet new xunit -n QuickStart.Tests
dotnet sln QuickStart.sln add ./QuickStart.Tests/QuickStart.Tests.csproj

We’ll want our test project to have a reference to the main project. To do so, run:

dotnet add ./QuickStart.Tests/QuickStart.Tests.csproj reference ./QuickStart/QuickStart.csproj

Take a pulse

Make sure it builds, and the token test passes:

dotnet build
dotnet test

Run!

Now open up the solution in your editor of choice. In Visual Studio, simply select Debug, then Start Debugging.

In Visual Studio Code

In VS Code, it may ask in a lower right corner notification to add some required assets to build and debug. (If not, when you try to debug it will prompt you). Select Yes. That will create a .vscode directory. Open .vscode/launch.json. Add a uriFormat property to the serverReadyAction element, for parity with Visual Studio launch settings that came free as part of the project template:

"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat":"%s/weatherforecast"
},

Then Select the Play (green play button with bug) left side bar, then Run (green play button at the top of the pane, with .NET Core Launch (web) next to it).

Or with the CLI:

If you’re a command-line die hard, or using some other IDE without integrated run/debugging, you can run using the CLI:

dotnet run -p ./QuickStart/QuickStart.csproj

You should see console output like the following:

info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development

In a browser window, navigate to: http://localhost:5000/weatherforecast or similar URL (port may vary) from the output above.

We’re in business!

If everything went well, you should see a page in your browser with some lovely, minified JSON representing a wild and woolly weather forecast.

[{"date":"2020-07-10T08:22:00.0144368-06:00","temperatureC":-13,"temperatureF":9,"summary":"Chilly"},{"date":"2020-07-11T08:22:00.0149679-06:00","temperatureC":51,"temperatureF":123,"summary":"Warm"},{"date":"2020-07-12T08:22:00.0149722-06:00","temperatureC":27,"temperatureF":80,"summary":"Warm"},{"date":"2020-07-13T08:22:00.0149729-06:00","temperatureC":29,"temperatureF":84,"summary":"Chilly"},{"date":"2020-07-14T08:22:00.0149734-06:00","temperatureC":34,"temperatureF":93,"summary":"Hot"}]

Voilà! You have a working Web API application with an endpoint.

How it works

Let’s look at the sample controller class that came with the project template, Controllers/WeatherForecastController.cs:

The controller class and its method define the endpoint and its behavior. The [Route] and [HttpGet] attributes tell ASP.NET to route an HTTP GET method request to the path /weathterforecast to the Get() method for processing, which returns an IEnumerable<WeatherForecast>. As we saw in our first run, that return value gets converted into a JSON array, the response we saw in the browser.

Out of the box, ASP.NET Core 3.1 uses the new System.Text.Json.JsonSerializer to convert C# objects to (and from) JSON, instead of the long-time mainstay Json.NET (or Newtonsoft.Json). From a performance and dependency perspective, System.Text.Json was a major foundation of .NET Core 3.1 and its integration with ASP.NET Core 3.1. We’ll learn more on how to use the new serialization later.

Let’s check out the plumbing in Startup.cs:

The ConfigureServices() method is where you do service registration with the IServiceCollection (think dependency injection root). AddControllers() is the Web API middleware which will register all the controllers (classes derived from ControllerBase).

NOTE: AddControllers() is the lighter weight Web API version of MVC’s AddControllersWithViews() or AddRazorPages() or AddMvc() in that it does not register services used for views and pages, which would be useful for a user-facing ASP.NET Core MVC web application.

UseRouting() and UseEndpoints() are the key configuration elements that add to ASP.NET’s middleware pipeline decision points for routing (as defined by attributes like [Route()] and [HttpGet] we saw in the controller class).

Rolling your own

Let’s create an endpoint of our own. Right now, nothing will respond at the root of the application. We can repair that easily by adding a Controller that will respond to “/”. Add a DefaultController.cs to the Controllers directory.

Now change the launch URL setting for your IDE — Properties/launchSettings.json for Visual Studio, .vscode/launch.json for Code — from /weatherforecast to the application root (or just change the path to / in your browser after start-up) and run or debug the project. You should see {"status":"Up"} in your browser.

Notice also our use of Microsoft.Extensions.Logging‘s ILogger<>, which get’s served up automatically to our class through dependency injection. With the default logging configuration, you should see the following in the console output for every time you hit application root (yes, too chatty for the real world, but for great for our demonstration purposes):

info: QuickStart.Controllers.DefaultController[0]
Status pinged: Up

Test, test, test!

Every application needs to have automated tests available to easily verify functionality as new features or elements are added. For this example we will use xUnit with Microsoft’s TestHost TestServer. This TestServer will host your application in memory and allow you to hit your endpoints using a faux HTTP client.

These types of tests are integration tests, and unit tests of course are possible but outside the scope of this article.

ASP.NET Core 3.1 provides the WebApplicationFactory class to streamline bootstrapping your service under test with a TestServer. We’ll use the WebApplicationFactory with an xUnit test collection, so our TestServer will be reused for all of the tests and then be torn down at the end.

Before we add the code we will need to add the dependent nuget package. We will do that through the dotnet cli but you could use a nuget package manager extension in your editor if you like or you can simply copy and paste the packages into the .csproj file and then run dotnet restore. Run the following commands through the CLI (if you are in the same directory as your desired csproj file to edit you can leave out which csproj it is and the CLI will assume the one that is in the same directory):

dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet restore

Your QuickStart.Tests.csproj file should look like this:

You make get newer versions depending on when you run this tutorial but the PackageReferences are your nuget packages. This is similar to the packages.json file if you are coming from a NodeJS background.

Create a TestCollection.cs file in the QuickStart.Tests folder that has the following code:

WebApplicationFactory implements IDisposable and that will be called by xUnit once all of the tests in the collection have run. Additionally, on line 8 it is using a Startup class. That is the same Startup class used by the main program. So all of your service bindings and configuration will be part of your TestHost, pretty neat.

Now we can move onto our first test itself. You can just rename the UnitTest1.cs file to DefaultControllerTests.cs. Generally speaking it’s recommended to group tests by their individual controllers or on the test fixtures that they require. Here is the code for the test:

Line 23 shows the retrieval of the faux HttpClient we can use to communicate with our in memory TestServer. On line 14 you’ll notice that we use a constructor parameter to get access to the WebApplicationFactory’s TestServer. That is automatically injected by xUnit because of the [Collection] annotation.

From there it’s just simple tests talking to our endpoints. You could imagine having different test collections for different purposes. Maybe you have one created where authentication is enabled, maybe another where authentication is disabled, or maybe another with different environment variables set. You get the idea. (You might accomplish that by creating customized/alternative Startup classes.) You get nice control of your testing environment and can run your tests quickly and efficiently.

Note our use of System.Text.Json ‘s JsonSerializer to de-serialize the JSON response from our endpoint. We need to configure it with an option to match the camelCasing ( "status" for Status property) that ASP.NET used by default to serialize our endpoint result object into JSON.

There’s a little taste of async/await programming here. We are (in practice) sending an HTTP request over the wire (completion of thread 1), waiting for the response, (awaken thread 2) then acting on that response. More of that to come when we add a data source.

Run dotnet test from within the QuickStart.Tests folder to run the test and see the results. If it doesn’t pass, look closely at the error messages.

Health first

Eventually this service will be deployed somewhere, in my case it will exist on a Kubernetes cluster which is polling for application health. If you have other technologies in mind for this you can just skip this section.

To add a /health endpoint so Kubernetes can make sure the service stays alive and self heal, we will use ASP.NET’s health checks middleware. Adjust your Startup.cs like this:

Just by adding a few using directives, and lines 27 and 44, our application will now respond to the /health endpoint. Run your application and give it a try. Very simple and easy to do. The extra JsonResponseWriter customizes the response. Instead of the default text/plain response of Healthy, we send an application/json response of {"status":"Healthy"} similar to our default / path. Health checks get more interesting when you add a dependent data source, and want service health to be determined by the ability to successfully and efficiently access that dependent data source (see implementing IHealthCheck).

For a test of the /health endpoint, let’s group it with our test for the / root (default) endpoint, by renaming DefaultControllerTests.cs to BasicControllerTests.cs:

We can use reuse what would have been nearly duplicate code employing xUnit’s Theory/Data. Common test code, run with multiple test cases representing different input and expected output. dotnet test should show 2 tests passed.

Get real!

Ok. Time to make this service “real.” First of all, you can delete the template’s sample Controllers/WeatherForecastController.cs and WeatherForecast.cs. We’ll have no more need of them. Let’s begin building out a real-world model of a school roster, with teachers and students, etc. Create a QuickStart/Models/ sub-directory. Add the following files:

And a new controller in the Controllers folder:

And for our convenience, change the following line in Startup.cs:

services.AddControllers();

To:

services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.IgnoreNullValues = true;
options.JsonSerializerOptions.WriteIndented = true;
});

Now run the program navigate to /students/1 in your browser. What do you know. We’ve retrieved a (canned) representation of a Student in all it’s prettified JSON glory!

A little too much glory, perhaps. All we asked for was the Student, but got back nested details about related model entities (Class, Teacher, School). While our model implies that a Student must be in a Class (and a Class must belong to a Teacher, etc.), we need a way to only return from our RESTful API endpoint, what’s pertinent to the Student, as that’s all that was asked for. Especially if that might mean fewer queries to a data store, thus faster performance.

Enter Data Transfer Objects (DTO’s) as the contract for our API. This is good architecture, to separate a communication model from a domain model. In some multi-tier applications, there’s yet a 3rd layer (or what may seem like 3 almost copies of model entities) separating domain model from data model. But we’ll stop short of that in this demo. Create a sub-folder QuickStart/Controllers/DTOs and a new class:

And modify the controller:

Run and hit /students/1 now we get a leaner, flatter Student, with just enough information that we could query details about related entities with other API calls.

A Data Source

Let’s add a data source to our back end. For this we’ll employ Entity Framework Core.

Entity Framework (EF) Core is a lightweight, extensible, open source and cross-platform version of the popular Entity Framework data access technology.

EF Core can serve as an object-relational mapper (O/RM), enabling .NET developers to work with a database using .NET objects, and eliminating the need for most of the data-access code they usually need to write.

EF Core supports many database engines.

We’ve got a relational model (essentially a domain model and data model combined), so we’ll use a model-first approach. (Entity Framework can also go the other direction and generate a model from a database). We’ll start with a DbContext which configures database access and model mappings.

We’ll also use EF’s InMemory database provider. Very useful in general for writing integration tests that don’t have the dependency of an external database, but also for specifically for this demo (as we’ll forgo setting up a Postgres, MySql, or SQLServer database, with all the baggage of connection string credentials, database name, schema, etc.). First a package to add to QuickStart.csproj:

dotnet add package Microsoft.EntityFrameworkCore.InMemory

In the Models sub-folder add the following DbContext class:

Add a couple using directives, and this line to ConfigureServices in Startup.cs:

using Microsoft.EntityFrameworkCore;
using QuickStart.Models;
//...services.AddDbContext<RosterDbContext>(builder =>
builder.UseInMemoryDatabase("Roster"),
ServiceLifetime.Singleton);

Ok, this is where we’re cheating a little bit. In a real world application, we’d be configuring a connection string with secret credentials into a database provider (e.g. UseSqlServer), and we’d be using ServiceLifetime.Scoped meaning create us 1 context instantiation per HTTP request, rather than 1 for the lifetime of the whole hosted process. In the real world, you might still employ UseInMemoryDatabase for integration tests, which you can accomplish by forking the Startup class to a TestStartup class that has the data source configured differently.

Here’s the updated controller, with an implementation that queries the data source for the requested Student:

Notice the Get method now returns async Task<IActionResult> instead of StudentDTO. Asynchronous because now we’re going over the wire to a data source. Returning IActionResult allows us to control what HTTP status code and resulting object type (that will end up as serialized JSON in the HTTP response body) to return, based the result of our DB query. If we can’t find the requested Student in the DB, we return a 404 Not Found status code. If we can, we return a 200 OK status code with the StudentDTO in the response body. Note the use of slick built-in ControllerBase methods NotFound() and Ok(object) to accomplish this level of control.

Also note the triple-bang /// XML comments and [ProducesResponseType(...)] decorators on the Get method. These will assist us with producing a “self”-documenting API for a later challenge. (What!? Yes, I said a self-documenting API.)

And finally, add a test to QuickStart.Tests prove our endpoint works, complete with “seeding” of data needed for the test into our in-memory data source:

dotnet test to watch it pass. Now here’s the full StudentsController with the rest of the typical CRUD (Create, Read, Update, Delete) operations.

I threw in some extension methods for model-DTO conversion, if you haven’t played with those, which are elegant and handy. And there’s an updated, full suite of StudentControllerTests.cs along with my whole solution at this GitHub repository. You say, “Now, you tell me! I could’ve avoided all that copy/pasting.” But hey, hope you learned something along the way. I know I did.

In writing that suite of tests, I learned I had to implement IClassFixture<> and another class in the Test Collection, in order to do DB seeding, then cleanup, that applied to all the (and only the) tests in the class. If you run dotnet test --collect:"XPlat Code Coverage then examine the XML file that coverlet produces, you’ll see that my tests have 92% line coverage, and 84% branch coverage. :)

On your own

Challenge #1: Create similar controllers for with CRUD for /schools, /teachers, and /classes with integration tests to prove all of them. Hint: Experiment with scaffolding a controller. A lot of the work will be done for you. You’ll still need to create the DTO’s and their conversion to/from model entities.

Challenge #2: Create some additional useful relationship endpoints like: /classes/{id}/students (all the Class’s Student’s), /teachers/{id}/classes (all the Teacher’s Classes), /schools/{id}/teachers (all the School’s Teachers), or even /teachers/{id}/students (all the Teacher’s Students), /schools/{id}/classes (all the School’s Classes), /schools/{id}/students (all the School’s Students). Discover how much EF Core is already doing for you to find what’s needed (read up on lazy loading vs. eager loading, which would apply to a real world DB).

Challenge #3: Install Docker and create a build and test image, that creates a deployment image. See Docker section of Alex Q.’s article (which I leaned on heavily for this article), only use .NET Core 3.1 images: mcr.microsoft.com/dotnet/core/sdk:3.1-alpine and mcr.microsoft.com/dotnet/core/runtime:3.1-alpine in your Dockerfile.

Challenge #4: Play with Swashbuckle and Swagger to make a request to the root (default) “/” return our API’s generated documentation.

Happy coding!

--

--