Jeremy Parnell

Best practices when consuming an API through C# and .NET

11 min read

October, 2020 — Let’s talk about the best practices to use when consuming a third-party API — or even for working with your own!

Grab an official library

If you’re lucky, there will be an official client library available as a Nuget package for the API you’re working with. If there is, this is usually the best route to go since it will save you a lot of work. You just install the package and you’re good to go. You keep track of any updates to the library through Nuget.

Most really popular APIs do have an official .NET library, but not always. Sometimes you’ll get an API that isn’t targeting .NET developers, or they’re in competition with Microsoft, or something of that nature, and then it may be difficult to find .NET support directly. It all depends on the community of developers out there.

Don’t grab an unofficial library

If there is not an official client library, be very cautious about pulling some random library that says it’s for the API you want to work with. These are often experiments, tutorial projects, unfinished, or unsupported. You may be tempted, thinking it’ll save you time in using something someone else started, but in the end you run a risk of wasting time unraveling how it works, and finding what problems they didn’t solve.

You’re an awesome programmer. You should write your own API client library!

Writing your own API client gives you the benefit of knowing the code, and only including the parts of the API you’re actually interested in or plan to use. When there is no official library, the time you spend writing your own code is usually a good investment.

Steps for creating an API client

Pull up the third-party API documentation and let’s get started!

DTO classes

I always start by mapping the parts of the API I need to local Data Transfer Object classes. DTOs are used as containers for holding data and passing it along to other areas of the application. DTO classes do not contain any logic, just the properties representing the data. For example:

public class ExampleDTO
{
	public int Id { get; set; }
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string DisplayName { get; set; }
}

Create classes with properties that match the properties of the objects listed in the API documentation. What you’re doing here is modeling the external API into strongly typed objects that you can use in your own local code.

One thing to note, however, is that APIs almost exclusively return JSON. JSON property naming conventions differ from C# in that they are usually camelcase. The property “ExampleProperty” in C# will probably be represented as “exampleProperty” in JSON. That’s even if the API authors follow conventions. You may find that they use underscores like “example_property”.

Deserializing the JSON you get back from an API into local DTOs may need the assistance of a DataContract. The DataContract tells our class exactly what to expect in the external data. For example:

using System.Runtime.Serialization;

[DataContract]
public class ExampleDTO
{
	[DataMember(Name = "id")]
	public int Id { get; set; }
	[DataMember(Name = "first_name")]
	public string FirstName { get; set; }
	[DataMember(Name = "last_name")]
	public string LastName { get; set; }
	[DataMember(Name = "display_name")]
	public string DisplayName { get; set; }
}

The [DataMember] annotation here translates each external property name to a local property. You’re not going to get any compiling errors or make Intellisense angry if you give the local property the same name as the API does…

public string first_name { get; set; }

However, that’s bad C#. Put in the effort of using the [DataContract] and [DataMember] annotations to make the data represent C# standards.

In addition to mapping what is being returned from the API into local classes, you’ll want to create DTO classes for everything they are expecting you to send as request data as well. It’s a two-way street. These are done in the same way.

using System.Runtime.Serialization;

[DataContract]
public class ExampleRequest
{
	[DataMember(Name = "id")]
	public int Id { get; set; }
}

Now, your property “Id” is properly formatted as “id”, which is what they are expecting on their end.

Create a reusable RestClient

After you’ve completely modeled the API (or just the parts you plan to use), the next step is to write a client that will do the work of communicating between your application and the API. The client sends requests and receives data in return.

Fortunately, you really only need to write one RestClient and it doesn’t have to change much to work with different APIs. In REST there are only so many ways to call an API. Usually it’s a GET or POST call (although less often you may also have a PUT call to transfer files). If it’s a GET call, you may have query string parameters that need to go along with the URL. If it’s a POST call, you’re sending JSON to it. You can simplify all of that into a reusable RestClient class.

Here’s an example client:

using ExampleAPIClient.Models; // Our models described earlier, this is for a Token holder.
using ExampleAPIClient.Utilities; // A utility class for adding query string parameters.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace ExampleAPIClient.Client
{
    public class RestClient : HttpClient
    {
        private string _baseUri = "https://external-api.com";

        private TokenResponse _token;

        public RestClient()
        {
            InitializeTlsProtocol();
        }

        public async Task<T> GetAsync<T>(string url, Dictionary<string, string> query)
        {
            await SetTokenAsync();

            DefaultRequestHeaders.Clear();
            DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true };
            DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);

            using (HttpResponseMessage response = await GetAsync(Utils.AddQueryString(_baseUri + url, query)))
            {
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsAsync<T>();
                }

                throw new Exception(response.ReasonPhrase);
            }
        }

        public async Task<T> GetAsync<T>(string url)
        {
            await SetTokenAsync();

            DefaultRequestHeaders.Clear();
            DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true };
            DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);

            using (HttpResponseMessage response = await GetAsync(_baseUri + url))
            {
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsAsync<T>();
                }

                throw new Exception(response.ReasonPhrase);
            }
        }

        public async Task<T> PostAsync<T>(string url, string data)
        {
            await SetTokenAsync();

            DefaultRequestHeaders.Clear();
            DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true };
            DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);

            var content = new StringContent(data, Encoding.UTF8, "application/json");

            using (HttpResponseMessage response = await PostAsync(_baseUri + url, content))
            {
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsAsync<T>();
                }

                throw new Exception(response.ReasonPhrase);
            }
        }

        public async Task<T> PutAsync<T>(string url, FileStream fs)
        {
            await SetTokenAsync();

            DefaultRequestHeaders.Clear();
            DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true };
            DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken);

            using (HttpResponseMessage response = await PutAsync(_baseUri + url, new StreamContent(fs)))
            {
                if (response.IsSuccessStatusCode)
                {
                    return await response.Content.ReadAsAsync<T>();
                }

                throw new Exception(response.ReasonPhrase);
            }
        }

        private void InitializeTlsProtocol()
        {
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
        }

        private async Task SetTokenAsync()
        {
            if (_token == null || _token.Expiration > DateTime.Now)
            {
                DefaultRequestHeaders.Clear();
                DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", "SOME CREDENTIAL SCHEME");

                var content = new StringContent("grant_type=client_credentials", Encoding.UTF8, "application/x-www-form-urlencoded");

                using (HttpResponseMessage response = await PostAsync(_baseUri + "/some-path-to-get/token", content))
                {
                    if (response.IsSuccessStatusCode)
                    {
                        _token = await response.Content.ReadAsAsync<TokenResponse>();
                    }
                    else
                    {
                        throw new Exception(response.ReasonPhrase);
                    }
                }
            }
        }
    }
}

Anatomy of the RestClient

Tasks

While it isn’t strictly necessary, it is best practice to make all of your calls asynchronous tasks. You don’t know how long it will take the request to complete, and you don’t want blocking calls holding up your user interface.

Authentication

There’s usually some sort of authentication and authorization involved in calling an API. There’s standard authentication schemes, like OAuth2, but an individual API’s authentication may be entirely custom as well. Most involve some sort of token or other data that you have to add as a query string parameter or header in your calls. They usually expire as well. Your code should hold this token, and check for expiration, and only make a call to get a new one once it has expired.

HTTP methods

While POST requests are passed JSON as a single string, GET requests often require multiple query string parameters to be passed along with it. Here we make that easier by including the individual parameters as a dictionary passed to the method. We also have a utility that translates the dictionary into the query string. Here’s what that looks like:

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;

namespace ExampleAPIClient.Utilities
{
    public partial class Utils
    {
        public static string AddQueryString(string uri, IDictionary<string, string> queryString)
        {
            if (uri == null)
            {
                throw new ArgumentNullException(nameof(uri));
            }

            if (queryString == null)
            {
                throw new ArgumentNullException(nameof(queryString));
            }

            return AddQueryString(uri, (IEnumerable<KeyValuePair<string, string>>)queryString);
        }

        private static string AddQueryString(
            string uri,
            IEnumerable<KeyValuePair<string, string>> queryString)
        {
            if (uri == null)
            {
                throw new ArgumentNullException(nameof(uri));
            }

            if (queryString == null)
            {
                throw new ArgumentNullException(nameof(queryString));
            }

            var anchorIndex = uri.IndexOf('#');
            var uriToBeAppended = uri;
            var anchorText = "";

            if (anchorIndex != -1)
            {
                anchorText = uri.Substring(anchorIndex);
                uriToBeAppended = uri.Substring(0, anchorIndex);
            }

            var queryIndex = uriToBeAppended.IndexOf('?');
            var hasQuery = queryIndex != -1;

            var sb = new StringBuilder();
            sb.Append(uriToBeAppended);
            foreach (var parameter in queryString)
            {
                sb.Append(hasQuery ? '&' : '?');
                sb.Append(WebUtility.UrlEncode(parameter.Key));
                sb.Append('=');
                sb.Append(WebUtility.UrlEncode(parameter.Value));
                hasQuery = true;
            }

            sb.Append(anchorText);
            return sb.ToString();
        }
    }
}

That’s about all there is to it. Now you’re ready to start using the client to make some calls!

Use the RestClient in code

You can use your RestClient in controllers, or services, or anywhere else that makes sense in your architecture. The general structure is:

public async Task<ExampleDTO> ExampleMethod(ExampleRequest request)
{
	// Serialize the request into JSON.
	var data = JsonConvert.SerializeObject(request);

	// Post the data and then deserialize the response into ExampleDTO.
	return await _client.PostAsyc<ExampleDTO>("some-end-point-url", data);
}

Those few lines of code are going to be the gist of all your API calls. You’ve already done all the heavy work and now you can reap the benefits of a solid piece of infrastructure.

Some Additional Things to Think About

You don’t have any control over external APIs, so error handling is more important than if you were programming against your own code.

You should never assume that an API is going to be working properly, or even be available when you need it. Diagnosing a problem could be challenging. Sometimes error codes that come back are helpful, other times they just add to the confusion.

Most APIs are rate limited in some fashion.

This means that you only get a certain number of calls in a particular period of time (per day, per hour, etc). Maybe that’s all you get for your money, or maybe they reduce the amount of calls to limit strain on their servers. Even if calls aren’t rate limited, you may be paying for metered usage, which means that it costs you more the more calls you make.

It’s best practice when integrating with third-party systems to think about how to reduce that interaction. Do you really need to call them every time a user requests your page? Could you maybe cache the data and refresh it less often?

Example scenario: You’re tasked with showing a twitter feed on a page. You could fetch the most recent tweets every time that page is shown. However, if the page is visited 1000 times in a day, that’s 1000 calls. If you know that your Marketing department is really only posting four tweets a day, it’s more efficient to cache the content and only call the API every two hours, only when the cache has expired, which results in 4 calls per day instead of 1000. It may mean that you have a slightly out of date list of tweets, but it’s way more efficient and cost-effective.

Some key takeaways from this discussion

  • When working with a third-party API, try to find an official library that is maintained regularly.
  • If there is no official library, don’t be afraid to write your own code.
  • You should model the API, following the documentation, into classes that you can use locally.
  • Your RestClient is going to be a valuable piece of infrastructure that you write once and use over and over.
  • Error handling is a must when you don’t have control over third-party services.
  • Practice caching and reduce your API consumption as much as possible.

This is by no means all the best practices in consuming an API, but hopefully it will get you started. Happy coding!

Jeremy Parnell is a software developer, interactive designer, and entrepreneur based in Lexington, Kentucky.
© 2023 Jeremy Parnell. All rights reserved.