Preface Link to heading

In my day-to-day work, I often find myself needing to access external data via an external API.

If you’re using C# and recent versions of .NET (both ASP.NET Core and Worker flavors) and need to do something similar, you may find useful to create a client for the external API and inject it whenever you need it via Dependency Injection.

Writing everything from scratch can lead to a lot of boilerplate. Luckily, we can use the help of a great library called RestSharp (GitHub).

Required packages Link to heading

Make sure to install the NuGet package for RestSharp in your project. This article was written with RestSharp v108.0.1, but you may check for updates and migration paths for newer versions on the official docs.

Creating the contract for the API Link to heading

Let’s start by declaring the shape of the API that we need to call:

public interface IMusicClient
{
  Task<IEnumerable<SongData>> GetSongsAsync(Guid artistId, CancellationToken cancellationToken);
}

and create an implementation of it:

public class MusicClient : IMusicClient
{
  private readonly RestClient _restClient;

  public MusicClient(RestClient restClient)
  {
    _restClient = restClient;

    // provide the base URL for all calls
    // this should be coming from an IConfiguration or an IOption setting possibly
    _restClient.Options.BaseUrl = new Uri("https://your_external_api_uri");

    // optionally inject default parameters in all calls
    // could be useful for setting the User-Agent, or maybe passing an authorization token or an API key
    _restClient.DefaultParameters.AddParameter(new HeaderParameter("Custom-Header", "CustomValueXYZ"));
  }

  Task<IEnumerable<SongData>> GetSongsAsync(Guid artistId, CancellationToken cancellationToken)
  {
    // compose the URI of the resource (will be appended to the base URL)
    // you can pass parameters without string interpolation, they will be added a few lines after this line
    const string uri = "MusicLibrary/Artists/{artistId}/Songs";

    // create the request object
    RestRequest restRequest = new(uri);

    // replace the URI parameters provided above
    restRequest.AddUrlSegment("artistId", artistId);
    // there are also other utility methods such as AddQueryParameter();

    // fire the request
    RestResponse<IEnumerable<SongData>> response = await _restClient.ExecuteAsync<IEnumerable<SongData>>(restRequest, cancellationToken);

    // check if the response was successful, if not then throw an exception
    response.ThrowIfNotSuccessful();

    // the content of the response inside .Data is typed!
    return response.Data;
  }
}

As you can see, the implementation is really straightforward, and we get a lot of benefits from using RestSharp:

  • URI parameters handling
  • automatic deserialization of response data
  • header customization for all requests
  • error checking and exception throwing in case of failure
  • automatic cancellation handling via CancellationToken
  • …and a lot more that you can find in the docs!

Adding the services to the DI container Link to heading

In your Program.cs (or wherever you have your host initialization code) add the required code for RestSharp and your custom implementations:

public static void Main(string[] args)
{
  IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
      // add RestSharp's client
      services.AddTransient<RestClient>();

      // add the custom-made API client library
      services.AddSingleton<IMusicClient, MusicClient>();

      // your other services...
    })
    .Build()
    .Run();
}

Using the API client Link to heading

At this point, you are ready to use the API client in your classes by injecting it via costructor injection for example:

public class Worker : BackgroundService
{
  private readonly IMusicClient _musicClient;

  public Worker(IMusicClient musicClient)
  {
    _musicClient = musicClient;
  }

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    // call your API client methods
    _musicClient.GetSongsAsync(Guid.NewGuid(), stoppingToken);

    // rest of your code...
  }
}

That’s it!

Testing Link to heading

Creating a contract for your API client is a good way to abstract away the implementation details and enable you to test consumer code way better.

If you followed up until here, the Worker class is highly testable (at least from a unit-test perspective) because you can provide a fake implementation for the IMusicClient interface or even a mock and limit your tests to the logic of the worker itself, rather than focusing on testing the API calls too.

If you need to test the client itself without firing the actual requests over the network, you should check out a very good library called RichardSzalay.MockHttp.

Here’s how a typical test would look like (using NUnit, FluentAssertions and Moq):

[TestFixture]
public class MusicClientTests
{
  [SetUp]
  public void SetUp()
  {
    // setup a mock http message handler, so we don't actually connect to the network for the tests
    _mockHttpMessageHandler = new MockHttpMessageHandler();

    // setup the RestSharp client with the mock http message handler
    _restClient = new RestClient(_mockHttpMessageHandler);
  }

  private MockHttpMessageHandler _mockHttpMessageHandler = null!;
  private RestClient _restClient = null!;

  [Test]
  public async Task GetSongsAsync_ShouldMakeTheCorrectRequest_WhenCalled()
  {
    // arrange
    Guid artistId = Guid.NewGuid();

    IEnumerable<SongData> songData = CreateFakeSongData(); // omitted for brevity

    _mockHttpMessageHandler
      .Expect(HttpMethod.Get, $"/MusicLibrary/Artists/{artistId}/Songs")
      .Respond("application/json", JsonSerializer.Serialize(songData));

    MusicClient sut = new(_restClient);

    // act
    List<SongData> result = (await sut.GetSongsAsync(artistId, CancellationToken.None)).ToList();

    // assert
    _mockHttpMessageHandler.VerifyNoOutstandingExpectation();

    result.Should().NotBeNull();
    result.Should().HaveCount(songData.Count());
  }
}

Conclusion Link to heading

Whenever possible, you should try to take advantage of static and strong typing in C#, to allow you to reuse code and apply refactorings in a secure manner. On top of that, clean code is easier code usually, and RestSharp makes it really easy to parse the code visually while reading it.