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 passedHttpContent
)
Thoughts:
-
HttpClient
is made for re-use but right now I create a new one for every request. I could store it as astatic
field inAbstractRequest
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:
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 for
InternalExecuteRequestAsync()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.