The thing that makes API requests

Posted on

Problem

I’m creating a thing that does things. Part of the things the thing does is querying an API for information about movies and TV shows. The API allows me to query for changes to their data so I will first iterate over every entry they have (IDs are numerically represented), store its information locally and then periodically update the database with information I get from the Changes API.

The part of the thing you see here is how I create requests and how each API call will be implemented. The setup is simple: since almost all kinds of requests are the same, I just create subclasses of a general request that implement request-specific behaviour.

Advantages:

  • GET, POST, PUT, DELETE, etc are all trivially implemented
  • Minimal code duplication
  • Creating a new API call takes less time than removing a booger

Disadvantages:

  • Some requests have information they can’t use (GetRequest is being passed HttpContent)

Thoughts:

  • HttpClient is made for re-use but right now I create a new one for every request. I could store it as a static field in AbstractRequest but I’m not entirely sure if this would be a good approach.

  • Is this a scalable approach? I’ve thought about things that might hinder me in the future but since each request can take an authorization header and HttpContent, I think most common API call scenarios have been covered.

  • I’m using a Response class to store information about the response which is right now just the HTTP status code. I’m still debating what combination of “return the data”, “return the response” and “write to logfile” to use as to how I handle failed requests.

Type hierarchy:

enter image description here

Code

API interface

namespace TMDbWrapper
{
// ReSharper disable once InconsistentNaming
    public class TMDbApi
    {
        private readonly string _apikey;
        private const string BaseUrl = "https://api.themoviedb.org/3/";

        public TMDbApi(string apikey)
        {
            _apikey = apikey;
        }

        private string GetUrl(string apiRequest)
        {
            return BaseUrl + apiRequest + "?api_key=" + _apikey;
        }

        public async Task<IEnumerable<Genre>> GetTvGenres()
        {
            return (await new GetRequest<GetGenresJsonModel>().ExecuteRequestAsync(GetUrl("genre/tv/list"))).Data.Genres;
        }

        public async Task<IEnumerable<Genre>> GetMovieGenres()
        {
            return (await new GetRequest<GetGenresJsonModel>().ExecuteRequestAsync(GetUrl("genre/movie/list"))).Data.Genres;
        }

        public async Task<Movie> GetMovie(int movieId)
        {
            return (await new GetRequest<Movie>().ExecuteRequestAsync(GetUrl("movie/" + movieId))).Data;
        }

        public async Task<IEnumerable<Keyword>> GetKeywords(int movieId)
        {
            return (await new GetRequest<GetKeywordsJsonModel>().ExecuteRequestAsync(GetUrl("movie/" + movieId + "/keywords"))).Data.Keywords;
        }
    }
}

Request baseclass

namespace TMDbWrapper.Requests
{
    public abstract class AbstractRequest<TResponse> where TResponse : class
    {
        public async Task<Response<TResponse>> ExecuteRequestAsync(string url, AuthenticationHeaderValue authHeader = null, HttpContent httpContent = null)
        {
            return await InternalExecuteRequestAsync(url, authHeader, httpContent);
        }

        protected async Task<Response<TResponse>> InternalExecuteRequestAsync(string url, AuthenticationHeaderValue authenticationHeader = null, HttpContent httpContent = null)
        {
            using (var client = CreateClient(authenticationHeader))
            {
                var response = await ExecuteRequestSpecificBehaviourAsync(client, url, httpContent);
                if (response.IsSuccessStatusCode)
                {
                    var json = await response.Content.ReadAsStringAsync();
                    var data = JsonConvert.DeserializeObject<TResponse>(json);
                    return new Response<TResponse>
                    {
                        Data = data,
                        StatusCode = response.StatusCode
                    };
                }

                return new Response<TResponse>
                {
                    Data = null,
                    StatusCode = response.StatusCode
                };
            }
        }

        private HttpClient CreateClient(AuthenticationHeaderValue authenticationHeader = null)
        {
            var client = new HttpClient();
            if (authenticationHeader != null)
            {
                client.DefaultRequestHeaders.Authorization = authenticationHeader;
            }
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            return client;
        }

        protected abstract Task<HttpResponseMessage> ExecuteRequestSpecificBehaviourAsync(HttpClient client, string url, HttpContent httpContent);
    }
}

Individual requests

public class GetRequest<TResponse> : AbstractRequest<TResponse> where TResponse : class
{
    protected override async Task<HttpResponseMessage> ExecuteRequestSpecificBehaviourAsync(HttpClient client, string url, HttpContent httpContent)
    {
        return await client.GetAsync(url);
    }
}

public class PostRequest<TResponse> : AbstractRequest<TResponse> where TResponse : class
{
    protected override async Task<HttpResponseMessage> ExecuteRequestSpecificBehaviourAsync(HttpClient client, string url, HttpContent httpContent)
    {
        return await client.PostAsync(url, httpContent);
    }
}

Other models

public class Response<TResponse>
{
    public TResponse Data { get; set; }

    public HttpStatusCode StatusCode { get; set; }
}

public class GetGenresJsonModel
{
    [JsonProperty("genres")]
    public IEnumerable<Genre> Genres { get; set; }
}

public class GetKeywordsJsonModel
{
    [JsonProperty("id")]
    public int MovieId { get; set; }

    [JsonProperty("keywords")]
    public IEnumerable<Keyword> Keywords { get; set; } 
}

Notes

  • I haven’t needed a Post request yet and there very well might not be any, nor PUT or DELETE for that matter. I have however had other projects with a similar approach to this that I might introduce this to as well.

Solution

Instead of having a private CreateClient() method you should consider to make it protected virtual if you would ever have the need to use headers different to "application/json".

Changing to protected virtual forInternalExecuteRequestAsync()should be done too.


You have small code duplication in the TMDbApi class.

The GetTvGenres() and the GetMovieGenres() are doing the same thing but with a different url. This should be extracted to a separate method.


Instead of always adding the strings BaseUrl + apiRequest + "?api_key=" + _apikey; in the GetUrl() method, you should consider to either change _apikey to _apikeyOperator which you initialize in the constructor to _apikeyOperator = "?api_key=" + apikey;

private string GetUrl(string apiRequest)
{
    return BaseUrl + apiRequest + _apikeyOperator;
}

or to change BaseUrl from const to readonly initialized to BaseUrl = "https://api.themoviedb.org/3/{0}?api_key=" + apikey; and then change the GetUrl() method to using String.Format().

private string GetUrl(string apiRequest)
{
    return String.Format(BaseUrl, apiRequest);
}

You should consider to break the return of the GetKeywords() into 2 lines to grasp it at first glance without scrolling.


Because you use apiRequest you should stick to camelCase casing for apikey too.


I would stick to creating the HttpClient for every request because by using the using keyword you ensure that all ressources are freed.


Otherwise you code looks clean and well structured.

Leave a Reply

Your email address will not be published. Required fields are marked *