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.