Exploring Video Games With the New IHttpClientFactory In .NET Core 2.1

REST services are everywhere. It’s tough to find an application that doesn’t leverage an externally hosted REST service in some way. Prior to .NET Core 2.1, a common library that was used to perform REST requests was RestSharp. I love RestSharp, but let’s explore the new alternative IHttpClientFactory that became available as part of .NET Core 2.1.

In this demo, we’re going to cover a bunch of awesome features:

  • Simple dependency injection
  • Named instances
  • Typed clients
  • Message handlers

In addition, for this service, we’re going to leverage the Internet Game Database API to pull in some data about Nintendo Switch games. As an aside, if you don’t already own a Nintendo Switch, it’s a fantastic device for the family, for travel, or just for fun. If you want to follow along with the code, you’ll need to create a free account here. Once you have your API key, you’ll want to be sure to include that in the code. There is a host of functionality available with this API, and you could use it to start building your video game collection, embed information about games in your apps, and much more.

You may also wish to download the source code for this. It is available on Github here.

Basic Usage

To start, let’s open Visual Studio 2017 and create an ‘Empty’ project denoting ASP.NET Core 2.1 as the framework.

Once we’re up and running, let’s add a few lines to our Startup.cs class. It should look like this when we’re done. Specifically, note the new services.AddHttpClient(); extension method on IServiceCollection which is exposed in 2.1.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddHttpClient();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();

    app.UseMvc();
}

Next, let’s create a GameController and a Game class for the purposes of deserialization. Notice how we inject the IHttpClientFactory into the controller constructor? We then use the CreateClient() method of the factory inside the controller action to create a basic instance. A noteworthy feature for IHttpClientFactory is that it maintains a pool of reusable clients, each with a lifetime of 2 minutes by default. This keeps the overhead involved with connectivity and instantiation at a more manageable level by default which is great for our overall performance and scalability.

[Route("game")]
public class GameController : Controller
{
	private readonly IHttpClientFactory _clientFactory;

	public GameController(IHttpClientFactory clientFactory)
	{
		_clientFactory = clientFactory;
	}

	[Route("single")]
	public async Task<IActionResult> Single()
	{
		var request = new HttpRequestMessage(HttpMethod.Get, "https://api-endpoint.igdb.com/games/26758"); // 26758 = Super Mario Odyssey
		request.Headers.Add("Accept", "application/json");
		request.Headers.Add("user-key", ""); // your-api-key-here

		var client = _clientFactory.CreateClient();
		var response = await client.SendAsync(request);

		if (response.IsSuccessStatusCode)
		{
			var result = await response.Content.ReadAsAsync<IEnumerable<Game>>();
			var game = result.First();
			return Content(game.name);
		}
		else
		{
			return Content("There was an error retrieving your game, verify your user-key with igdb.com.");
		}
	}
}

public class Game
{
	public int id { get; set; }
	public string name { get; set; }
	public string url { get; set; }
	public string summary { get; set; }
}

Named Instances

In the above code, we are defining the HttpClient inside the controller class, but it’s also possible to have a “named” instance of the HttpClient. We could re-write our line from our Startup.cs class above to use this code instead:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("IgdbClient", client =>
    {
        client.BaseAddress = new Uri("https://api-endpoint.igdb.com");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
        client.DefaultRequestHeaders.Add("user-key", ""); // your-api-key-here
    });

    services.AddMvc();
}

Then, inside our controller, we would inject the IHttpClientFactory the same way, but we would change the CreateClient() method to use our named instance.

var client = _clientFactory.CreateClient("IgdbClient");
var response = await client.GetAsync("/games/26758");

Typed Clients

Cool stuff so far, right? Let’s take it one more step further: typed clients. Let’s add the interface and class below to our application. Note how we have a private HttpClient as part of our IgdbClient object which exposes developer friendly names for each of the Igdb.com API endpoints we can consume in our application. Note: There are plenty more fields available in this API, including artwork, screenshots, and more but we are just keeping it simple for the demo.

public class IgdbClient 
{
	private HttpClient _client { get; }

	public IgdbClient(HttpClient client)
	{           
		_client = client;
	}

	public async Task<Game> GetGameById(int id)
	{
		var response = await _client.GetAsync("/games/" + id);
		response.EnsureSuccessStatusCode();

		var result = await response.Content.ReadAsAsync<IEnumerable<Game>>();
		if (!result.Any()) return null;

		return result.First();
	}

	public async Task<IEnumerable<Game>> GetPopularGames(int platformId, int limit = 10)
	{
		var response = await _client.GetAsync($"/games/?fields=name,url,summary&order=popularity:desc&filter[platforms][eq]={platformId}&limit={limit}");
		response.EnsureSuccessStatusCode();

		var result = await response.Content.ReadAsAsync<IEnumerable<Game>>();
		if (!result.Any()) return null;

		return result;
	}
}

Finally, let’s modify our Startup class to inject our configured client for our application, and also modify our GameController class so that it exposes these two new methods for testing purposes. We’re configuring the Startup class to inject our application specific configuration (so we can distribute this class to other applications which each have their own API key, and we’re also injecting IgdbClient this into the controller.

// This goes inside ConfigureServices inside Startup.cs
services.AddHttpClient<IgdbClient>(client =>
{
	client.BaseAddress = new Uri("https://api-endpoint.igdb.com");
	client.DefaultRequestHeaders.Add("Accept", "application/json");
	client.DefaultRequestHeaders.Add("user-key", ""); // your-api-key-here
});
//Updated GameController.cs
private readonly IHttpClientFactory _clientFactory;
private readonly IgdbClient _igdbClient;

public GameController(IHttpClientFactory clientFactory, IgdbClient igdbClient)
{
	_clientFactory = clientFactory;
	_igdbClient = igdbClient;
}

[Route("id")]
public async Task<IActionResult> GetById(int id = 26758)
{
	var game = await _igdbClient.GetGameById(id);
	return Content(game.name);
}

[Route("popular")]
public async Task<IActionResult> GetPopularGames(int platformId = 130, int limit = 10)
{
	var games = await _igdbClient.GetPopularGames(platformId, limit);
	return Content(Newtonsoft.Json.JsonConvert.SerializeObject(games));
}

Message Handlers

The last feature we’re going to discuss is message handlers. Handlers can be attached to clients to monitor messages as they go in, or messages as they go out. You could perform logging for messages or duration, request validation, and more. Handlers can be chained as middleware as well. We’re going to keep it simple for our demo, and simply verify that the user-key which is required for the Igdb.com API is specified for the request before it goes out for our application. Let’s add the following class to our app:

 public class ValidateUserKeyHandler : DelegatingHandler
{
	protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
		CancellationToken cancellationToken)
	{

		if (!request.Headers.Contains("user-key") || 
			string.IsNullOrEmpty(request.Headers.GetValues("user-key").FirstOrDefault()))
		{
			return new HttpResponseMessage(HttpStatusCode.BadRequest)
			{
				Content = new StringContent("You must supply a http header value for user-key")
			};
		}     

		return await base.SendAsync(request, cancellationToken);
	}
}

Let’s also modify our Startup class, specifically the ConfigureServices method so that the following appears after we registered our IgdbClient.

// Updated ConfigureServices method
services.AddHttpClient<IgdbClient>(client =>
{
	client.BaseAddress = new Uri("https://api-endpoint.igdb.com");
	client.DefaultRequestHeaders.Add("Accept", "application/json");
	client.DefaultRequestHeaders.Add("user-key", ""); // your-api-key-here
});            
services.AddTransient<ValidateUserKeyHandler>();
services.AddHttpClient<IgdbClient>().AddHttpMessageHandler<ValidateUserKeyHandler>();

Now, if we try to call the /game/id or /game/popular endpoints without having specified our user-key, this will result in a 400 Bad Request error before it is even attempted to be sent to the Igdb API. This potentially saves us API usage limits and cost as well as improves our performance by failing before the attempt is made.

 

Kyle Ballard