Building API Clients in C# and .NET
Consuming API services
In this lesson we discussed the idea of an API which operates over the internet via HTTP.
In this current lesson we will build a console application that will interact
with the One List API
to manage todo lists. All of the information about the
todo items will be stored in the API itself. In this way our console application
will be considered a client
of the API. In other lessons we will build full
web page applications that also serve as a client that runs in our browser. We
could also develop a mobile phone application in Android or iOS that could also
access this API. In this way the API is agnostic to its client. As long as the
client software (in this case our C#
console application) complies with the
API requirements it will work with the API. This is why APIs are so powerful and
help power the modern internet.
Creating our one list API client.
We will generate a console application with:
dotnet new sdg-console -o OneListClient
Initially this application contains just the
Console.WriteLine("Hello World!");
code so we will remove that and add this
code:
var client = new HttpClient();
NOTE: You will need to add
using System.Net.Http;
The HttpClient
is the built-in class we will use to send and receive
information to APIs over HTTP. The HttpClient
object we create, client
, will
have many methods for both sending and receiving data which we will see over the
course of this lesson.
The first we will review is a method that makes a GET
request to a server and
returns the response body
as a string
.
Fetching data from a server and receiving back a string
The next line of code we would like to add uses the HttpClient
method
GetStringAsync
. However, before we can add a line such as
var responseAsString = client.GetStringAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");
we need to take a minor detour to talk about the concept of Async
.
DETOUR: ASYNC
As we mentioned in our discussion of APIs we are making these requests over the
internet. Anyone who has visited a busy web page will notice that sending data
to, and receiving information from, a remote service (web site, API, etc) on the
internet is not instantaneous. There is always going to be some amount of
delay as the request is constructed, sent out by our computer, it traverses the
various connections over the internet on its way to the destination server,
received, parsed, executed, response created, and then the response traverses
the return path through the internet back to our computer. Due to the inherent
and unpredictable delay in sending requests and receiving a response the code we
write for interacting with such services should be a-synchronous
. The term
synchronous means "at the same time" and in this context we could also
interpret it as "stop and wait for this to be done." Thus the idea of
asynchronous
or async
would mean that opposite, or "do not wait for this to
be done"
We would like these requests to be asynchronous so that our programs do not freeze while we are waiting for the response. We've all experienced a synchronous event when our browsers give us a "wait" cursor (spinning wheel, etc) when a web page freezes. That does not provide a great user experience.
The idea of async methods will become very common as we write code that interacts with remote resources such as HTTP APIs or even our own databases. That the .NET core team made these methods asynchronous allows us the flexibility to make our applications as responsive as possible.
However, learning to deal with async
methods requires an even longer detour
than we want to take at this time. Fortunately the C#
and dotnet
team
provided a way to allow us to tell these async
methods that we'd just prefer
to wait for them to be done.
For this we introduce a new keyword, the await
keyword. When we put this
keyword in front of any function call that is async
we tell the code that we
wish for it to make the async
as it would but to immediately just wait for it
to be complete and return the result. In this way we turn something
asynchronous into something synchronous. This does mean, however, that our
code will pause while the function is running and may make our application
seem unresponsive.
Fetching data, with GetStringAsync
and await
Let's add code to fetch a list of todo items from One List
using System;using System.Net.Http;namespace OneListClient{class Program{static void Main(string[] args){var client = new HttpClient();var responseAsString = await client.GetStringAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");Console.WriteLine(responseAsString);}}}
If we were to look in our editor or try to run this code we will notice an error.
The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'
Any time we ask a method, in this case Main
to use await
we need to make the
method itself async
or to have it return a new kind of object called a Task
.
We need to do this so that the code that calls us know that we will be
synchronous. This allows the calling method to know that they can run us in
an async way to make the system responsive. So while await + async
is powerful
it does come with some conditions we need to adhere to.
If you place your cursor on the error and press Control .
(Windows) or
Command .
(Mac) you will see the Quick Fix suggestion Make method async
.
Doing so turns the code into:
using System;using System.Net.Http;namespace OneListClient{class Program{static async System.Threading.Tasks.Task Main(string[] args){var client = new HttpClient();var responseAsString = await client.GetStringAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");Console.WriteLine(responseAsString);}}}
Notice that Main
has changed from public void Main
to
public async System.Threading.Tasks.task Main
. We have made this method
async
and note that it returns a Task
. This satisfies .NET to allow us to
use await
. It tells the calling code, in this case our entire program run,
that our code uses await
and thus Main should be treated as asynchronous.
The idea of returning Task
will come more into play when we are creating
APIs. For now know that if our method returns Task
we can return a variable
that is, itself, the result of an async
call (such as GetStringAsync
) and
the Task
will run that code and process the results for us. This will be
helpful when we learn to write our own APIs.
One last refactor is to take the long System.Threading.Tasks.Task
and make it
just Task
and then add a using System.Threading.Tasks;
to our code.
using System;using System.Net.Http;using System.Threading.Tasks;namespace OneListClient{class Program{static async Task Main(string[] args){var client = new HttpClient();var responseAsString = await client.GetStringAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");Console.WriteLine(responseAsString);}}}
Go ahead and run this code to see an output similar to (but perhaps different if there are more, less, or different tasks in the API at the moment)
[{"id": 1590,"user_id": 143,"text": "Write some documentation about Insomnia","complete": false,"created_at": "2020-04-24T19:32:43.653Z","updated_at": "2020-04-24T19:32:43.653Z"},{"id": 1591,"user_id": 143,"text": "Write more about how awesome APIs are","complete": false,"created_at": "2020-04-24T19:37:21.547Z","updated_at": "2020-04-24T19:37:21.547Z"},{"id": 1592,"user_id": 143,"text": "Write more about the PEDAC process of problem solving.","complete": true,"created_at": "2020-04-24T19:37:46.305Z","updated_at": "2020-04-24T19:37:46.305Z"}]
Notice that this JSON, even though it is not formatted nicely with extra
whitespace and indentation, is just like the results we would receive if we used
a tool such as httpie
or Insomnia
to interact with the API.
CONGRATULATIONS, you have just written your first API client that accesses data remotely over the Internet!
Processing the data
However, notice that this is just one long string of data. Also notice that the
JSON data represent an array. It would be convenient if we could convert this
JSON string into some objects that C#
knows how to deal with nicely.
This introduces the idea of serialization
DETOUR: Serialization
One part of sending and receiving information over an API is how the data is
represented as it is sent to the API and when results return. We have also
learned that JSON
is a very common data format for this representation. Thus
there must be a way we can take data we send to an API and format it in JSON
and there must also be a way to take information from an API and convert it
from JSON to C#
objects we are familiar with.
Fortunately there is a process known as serialization
and de-serialization
,
coming from the idea that we are taking data and turning it into a series of
characters, like a JSON string. Serialization is the process of taking an object
(an array, a string, a number, a date, an array of objects, or any complex data
structure) and turning it into a string (series of characters) in a specific
form (in this case JSON). De-serialization is the reverse of this process,
taking a series of characters (a string) and processing it via a format (in this
case JSON) to restore the original object (an array, a string, a number, a date,
an array of objects, or any complex data structure)
Additionally it is fortunate that C#
and dotnet
provide us a set of built-in
classes to handle this.
Let's add those to our code to get back a better representation of our object.
Processing the data by defining a class to store the results
Looking at the API we see that the return from our GET
is an array of items.
And looking at the objects in that array we see they follow this format:
id: numbertext: stringcomplete: boolcreated_at: stringupdated_at: string
We would like to deal with each of the entries as a familiar object, a POCO
(Plain Old Csharp Object). So we can define a class
to represent these items:
using System;using System.Net.Http;using System.Threading.Tasks;namespace OneListClient{class Program{class Item{public int id { get; set; }public string text { get; set; }public bool complete { get; set; }public string created_at { get; set; }public string updated_at { get; set; }}static async Task Main(string[] args){var client = new HttpClient();var responseAsString = await client.GetStringAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");Console.WriteLine(responseAsString);}}}
Notice that we define the properties of our class to have the same names as the keys of our JSON object. This is a CRITICAL point because it is this pattern the deserializer will use to know where to put the data. For instance if we were to receive this data in the response:
{"id": 6,"text": "Finish Assignment","complete": true,"created_at": "2016-08-17T20:06:34.874Z","updated_at": "2018-10-02T16:10:59.754Z"}
The deserializer will make a new instance of Item
for us and copy the 6
from the "id"
JSON key into the id
property in our object. Then it will copy
the value "Finish Assignment"
from the "text"
key in the JSON into the
text
property of our object, and so on. If we follow this convention
(pattern) the deserializer will do a lot of work on our behalf without having to
write individual statements to tell it how to work.
NOTE: This follows an idea known as "Convention Over Configuration". That is, by following a set of rules we can rely on behavior of code to do work for us without having to configure (or code) each and every step. This is a powerful developer tool as it saves us time when coding. It also allows us not to think about different names in the JSON data and our objects. For instance, if we called the
text
propertyDescription
we would have to remember: "Oh, yeah, Description property comes from the text JSON key." These small nuisances on a large project can add up over time. Let's use these conventions to our advantage.
In order to use the deserializer to convert our response into a List
of
Item
objects we need to make a modification. The first is to not retrieve the
GET
result as a string
but as a stream
. A stream
is a variable that
knows how to read data one chunk at a time (often one character at a time). By
using a stream
the deserializer can process data a little at a time in case
the input is quite large.
var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");
And then we need to supply this stream
to the deserializer
var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);
NOTE: we need to add
using System.Text.Json;
as well asusing System.Collections.Generic
here
Again we see await
since the process of deserializing the list is done in an
async
manner. The JsonSerializer.DeserializeAsync
also requires us to tell
it what kind of object to extract. We do that by specifying List<Item>
inside
of <>
. The outer <>
are for the DeserializeAsync
to mean "inside these
<>
is the type of data to try to deserialize" and inside the List<Item>
says
that the JSON data, inside the responseAsStream
should be processed as a
List
of Item
objects. Finally we supply the stream, responseAsStream
This gives us a List<Item>
that we can iterate.
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks;namespace OneListClient{class Program{class Item{public int id { get; set; }public string text { get; set; }public bool complete { get; set; }public string created_at { get; set; }public string updated_at { get; set; }}static async Task Main(string[] args){var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);// For each item in our deserialized List of Itemforeach (var item in items){// Output some details on that itemConsole.WriteLine($"The task {item.text} was created on {item.created_at} and has a completion of: {item.complete}");}}}}
Running the code you will see a much nicer output:
The task Write some documentation about Insomnia was created on 2020-04-24T19:32:43.653Z and has a completion of: FalseThe task Write more about how awesome APIs are was created on 2020-04-24T19:37:21.547Z and has a completion of: FalseThe task Write more about the PEDAC process of problem solving. was created on 2020-04-24T19:37:46.305Z and has a completion of: True
Improve our Item
class
Notice the date is formatted as a long string that is not very user friendly. We
can improve this by changing the data type from string
to DateTime
in our
Item
class. Thus when the deserializer is processing those fields it will
try to convert (the technical term is coerce) the string format into a
DateTime
. Luckily for us that string is in a very specific format called an
ISO8601 format and DateTime
knows
how to deal with it.
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks;namespace OneListClient{class Program{class Item{public int id { get; set; }public string text { get; set; }public bool complete { get; set; }public DateTime created_at { get; set; }public DateTime updated_at { get; set; }}static async Task Main(string[] args){var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);// For each item in our deserialized List of Itemforeach (var item in items){// Output some details on that itemConsole.WriteLine($"The task {item.text} was created on {item.created_at} and has a completion of: {item.complete}");}}}}
And now our output looks like:
The task Write some documentation about Insomnia was created on 4/24/2020 7:32:43 PM and has a completion of: FalseThe task Write more about how awesome APIs are was created on 4/24/2020 7:37:21 PM and has a completion of: FalseThe task Write more about the PEDAC process of problem solving. was created on 4/24/2020 7:37:46 PM and has a completion of: False
The False
is not friendly so lets improve that. We can add another custom
property which contains logic for its get
implementation:
public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn complete ? "completed" : "not completed";}}
And we can use it as such:
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks;namespace OneListClient{class Program{class Item{public int id { get; set; }public string text { get; set; }public bool complete { get; set; }public DateTime created_at { get; set; }public DateTime updated_at { get; set; }public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn complete ? "completed" : "not completed";}}}static async Task Main(string[] args){var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);// For each item in our deserialized List of Itemforeach (var item in items){// Output some details on that itemConsole.WriteLine($"The task {item.text} was created on {item.created_at} and is {item.CompletedStatus}");}}}}
You may have noticed we gave the CompletedStatus
a more traditional C#
name
in CapitalCase
styling. We can do the same for the other fields with a little
bit of work.
But first we will make yet another detour.
DETOUR: C# Attributes
We are going to add some hints to our Item
class to tell the serializer how
to match the names in the JSON to the names of our properties. We will do that
by adding the concept of an attribute
. A C#
attribute is written in []
just before the class, method, or property it describes. Different language
features and libraries add specific attributes that add abilities to the
classes, methods, or properties they are attached to. Over the course of our
C#
work we will use attributes
in many ways to apply configuration to the
code we write.
NOTE: Here we are using the
Configuration
part ofConvention over Configuration
. We are deciding that having betterC#
property names is better since we are familiar with theCapitalCase
style rather than thesnake_case
that the API is using. We aren't renaming the properties (though we could do that as well) we are just making the case-style more familiar.
In this instance we are using the JsonPropertyName
attribute. It takes an
argument of the name of the JSON key used to populate the C#
property that
follows.
Using JsonPropertyName to give better property names.
Applying JsonPropertyName
to all of our Item
properties allows us to use
C#
style properties.
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Text.Json.Serialization;using System.Threading.Tasks;namespace OneListClient{class Program{class Item{[JsonPropertyName("id")]public int Id { get; set; }[JsonPropertyName("text")]public string Text { get; set; }[JsonPropertyName("complete")]public bool Complete { get; set; }[JsonPropertyName("created_at")]public DateTime CreatedAt { get; set; }[JsonPropertyName("updated_at")]public DateTime UpdatedAt { get; set; }public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn Complete ? "completed" : "not completed";}}}static async Task Main(string[] args){var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);// For each item in our deserialized List of Itemforeach (var item in items){// Output some details on that itemConsole.WriteLine($"The task {item.Text} was created on {item.CreatedAt} and is {item.CompletedStatus}");}}}}
Add some visual appeal!
Wow, we have a great little app that can fetch todo items from our API. It deserves a little bit of extra styling. First let's put the output in a nice table format. To do this we will add an external library to our app:
dotnet add package ConsoleTables
The Documentation for ConsoleTables shows us how to create a table with headers, add rows, and output the table itself.
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Text.Json.Serialization;using System.Threading.Tasks;using ConsoleTables;namespace OneListClient{class Program{class Item{[JsonPropertyName("id")]public int Id { get; set; }[JsonPropertyName("text")]public string Text { get; set; }[JsonPropertyName("complete")]public bool Complete { get; set; }[JsonPropertyName("created_at")]public DateTime CreatedAt { get; set; }[JsonPropertyName("updated_at")]public DateTime UpdatedAt { get; set; }public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn Complete ? "completed" : "not completed";}}}static async Task Main(string[] args){var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var responseAsStream = await client.GetStreamAsync("https://one-list-api.herokuapp.com/items?access_token=sdg-handbook");// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);var table = new ConsoleTable("Description", "Created At", "Completed");// For each item in our deserialized List of Itemforeach (var item in items){// Add one row to our tabletable.AddRow(item.Text, item.CreatedAt, item.CompletedStatus);}// Write the tabletable.Write();}}}
And now our output looks like this:
+--------------------------------------------------------+----------------------+---------------+| Description | Created At | Completed |+--------------------------------------------------------+----------------------+---------------+| Write some documentation about Insomnia | 4/24/2020 7:32:43 PM | not completed |+--------------------------------------------------------+----------------------+---------------+| Write more about how awesome APIs are | 4/24/2020 7:37:21 PM | not completed |+--------------------------------------------------------+----------------------+---------------+| Write more about the PEDAC process of problem solving. | 4/24/2020 7:37:46 PM | completed |+--------------------------------------------------------+----------------------+---------------+
Nice! However, our application still only allows us to show the list for a predefined token. Let's update the code to supply the token via the command line! Now we can have multiple lists and switch between them.
Getting the name of the list from the command line.
We've been ignoring a variable that has been present in all of our C#
applications so far, args
. The string[] args
parameter to Main
are the
command line arguments that appear after our dotnet run
command. So we can use
this to get the name of the list we want to process. Since this is an array we
can access the 0th (first) element: var token=args[0]
and then use string
interpolation to generate our URL:
var url = $"https://one-list-api.herokuapp.com/items?access_token={token}";
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Text.Json.Serialization;using System.Threading.Tasks;using ConsoleTables;namespace OneListClient{class Program{class Item{[JsonPropertyName("id")]public int Id { get; set; }[JsonPropertyName("text")]public string Text { get; set; }[JsonPropertyName("complete")]public bool Complete { get; set; }[JsonPropertyName("created_at")]public DateTime CreatedAt { get; set; }[JsonPropertyName("updated_at")]public DateTime UpdatedAt { get; set; }public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn Complete ? "completed" : "not completed";}}}static async Task Main(string[] args){var token = args[0];var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var url = $"https://one-list-api.herokuapp.com/items?access_token={token}";var responseAsStream = await client.GetStreamAsync(url);// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);var table = new ConsoleTable("Description", "Created At", "Completed");// For each item in our deserialized List of Itemforeach (var item in items){// Add one row to our tabletable.AddRow(item.Text, item.CreatedAt, item.CompletedStatus);}// Write the tabletable.Write();}}}
dotnet run sdg-handbook
-------------------------------------------------------------------------------------------------| Description | Created At | Completed |-------------------------------------------------------------------------------------------------| Write some documentation about Insomnia | 4/24/2020 7:32:43 PM | not completed |-------------------------------------------------------------------------------------------------| Write more about how awesome APIs are | 4/24/2020 7:37:21 PM | not completed |-------------------------------------------------------------------------------------------------| Write more about the PEDAC process of problem solving. | 4/24/2020 7:37:46 PM | not completed |-------------------------------------------------------------------------------------------------
However, what would happen if the user forgot to supply a name?
dotnet run
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
Uh oh, let's handle that in the code by prompting the user if there is nothing
in the args
array.
using System;using System.Collections.Generic;using System.Net.Http;using System.Text.Json;using System.Text.Json.Serialization;using System.Threading.Tasks;using ConsoleTables;namespace OneListClient{class Program{class Item{[JsonPropertyName("id")]public int Id { get; set; }[JsonPropertyName("text")]public string Text { get; set; }[JsonPropertyName("complete")]public bool Complete { get; set; }[JsonPropertyName("created_at")]public DateTime CreatedAt { get; set; }[JsonPropertyName("updated_at")]public DateTime UpdatedAt { get; set; }public string CompletedStatus{get{// Uses a ternary to return "completed" if the `complete` variable is true, returns "not completed" if falsereturn Complete ? "completed" : "not completed";}}}static async Task Main(string[] args){var token = "";if (args.Length == 0){Console.Write("What list would you like? ");token = Console.ReadLine();}else{token = args[0];}var client = new HttpClient();// Make a `GET` request to the API and get back a *stream* of data.var url = $"https://one-list-api.herokuapp.com/items?access_token={token}";var responseAsStream = await client.GetStreamAsync(url);// Supply that *stream of data* to a Deserialize that will interpret it as a List of Item objects.var items = await JsonSerializer.DeserializeAsync<List<Item>>(responseAsStream);var table = new ConsoleTable("Description", "Created At", "Completed");// For each item in our deserialized List of Itemforeach (var item in items){// Add one row to our tabletable.AddRow(item.Text, item.CreatedAt, item.CompletedStatus);}// Write the tabletable.Write();}}}
Our todo list app is really going places!
Next up, let's add a little menu system that will let us get a list of items, get a specific item by its ID, add an item, and remove an item.