HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Basic API wrapper around a Restful service

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
aroundwrapperservicerestfulapibasic

Problem

I'm writing a basic wrapper around a restful service that returns a list of Stores. I'm using RestSharp which I'm injecting into the class.

Naturally, I want to make it completely testable (that's a major requirement!).

I'm also thinking that it could be more generic i.e. GetStores but I'm struggling how to do as I need to specify things like the RootElement.

public class TopManClient : ITopManClient
{
    private readonly IRestClient _restClient;
    private const string BaseUrl = "https://TopmanStores.Api.com/";
    private const string AcceptTenant = "uk";
    private const string AcceptLanguage = "en-GB";

    public TopManClient(IRestClient restClient)
    {
        _restClient = restClient;

        _restClient.BaseUrl = new Uri(BaseUrl);
        _restClient.AddDefaultHeader("Accept-Tenant", AcceptTenant);
        _restClient.AddDefaultHeader("Accept-Language", AcceptLanguage);
    }

    public IRestRequest GetRequest(string url)
    {
        return new RestRequest(url, Method.GET);
    }

    public List GetStores(string postCode)
    {
        var request = GetRequest("stores");
        request.RootElement = "Stores";
        request.AddQueryParameter("q", postCode);

        var response = _restClient.Execute>(request);

        if (response.ErrorException != null)
        {
            const string message = "Error retrieving response. Check inner details for more info.";
            throw new ApplicationException(message, response.ErrorException);
        }

        return response.Data;
    }
}

Solution

I would personally prefer to use HttpClient directly as it provides full control and perfect testability - see here.

We can define our REST API as:

// see https://jsonplaceholder.typicode.com/
// this site exposes some REST API example to automate
public interface ITypicode
{
    Task GetPostsAsync();
    Task GetPostAsync(int id);
}

public class Typicode : JsonRestApi, ITypicode
{
    public Typicode(HttpMessageHandler handler = null)
        : base("https://jsonplaceholder.typicode.com", handler)
    {
    }

    public Task GetPostsAsync() => GetAsync("posts");
    public Task GetPostAsync(int id) => GetAsync($"posts/{id}");
}

public class BlogPost
{
    public int UserId { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}


Where base class to simplify API definition is:

public abstract class JsonRestApi
{
    static readonly JsonSerializerSettings Settings = new JsonSerializerSettings()
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters = new [] { new StringEnumConverter() }
    };

    protected JsonRestApi(string baseAddress, HttpMessageHandler handler = null)
    {
        Client = handler == null ? new HttpClient() : new HttpClient(handler);
        Client.BaseAddress = new Uri(baseAddress);
        Client.DefaultRequestHeaders
            .Accept
            .Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    protected async Task GetAsync(string uri) =>
        await ParseJson(await Client.GetAsync(uri));

    protected async Task PostAsync(string uri, object data = null) =>
        await ParseJson(await Client.PostAsync(uri, JsonContent.From(data)));

    protected HttpClient Client { get; }

    async Task ParseJson(HttpResponseMessage response) =>
        JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(), Settings);

    class JsonContent : StringContent
    {
        public static JsonContent From(object data) =>
            data == null ? null : new JsonContent(data);

        public JsonContent(object data)
            : base(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")
        {
        }
    }
}


This code is fully testable.

Code Snippets

// see https://jsonplaceholder.typicode.com/
// this site exposes some REST API example to automate
public interface ITypicode
{
    Task<BlogPost[]> GetPostsAsync();
    Task<BlogPost> GetPostAsync(int id);
}

public class Typicode : JsonRestApi, ITypicode
{
    public Typicode(HttpMessageHandler handler = null)
        : base("https://jsonplaceholder.typicode.com", handler)
    {
    }

    public Task<BlogPost[]> GetPostsAsync() => GetAsync<BlogPost[]>("posts");
    public Task<BlogPost> GetPostAsync(int id) => GetAsync<BlogPost>($"posts/{id}");
}

public class BlogPost
{
    public int UserId { get; set; }
    public int Id { get; set; }
    public string Title { get; set; }
    public string Body { get; set; }
}
public abstract class JsonRestApi
{
    static readonly JsonSerializerSettings Settings = new JsonSerializerSettings()
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters = new [] { new StringEnumConverter() }
    };

    protected JsonRestApi(string baseAddress, HttpMessageHandler handler = null)
    {
        Client = handler == null ? new HttpClient() : new HttpClient(handler);
        Client.BaseAddress = new Uri(baseAddress);
        Client.DefaultRequestHeaders
            .Accept
            .Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    protected async Task<T> GetAsync<T>(string uri) =>
        await ParseJson<T>(await Client.GetAsync(uri));

    protected async Task<T> PostAsync<T>(string uri, object data = null) =>
        await ParseJson<T>(await Client.PostAsync(uri, JsonContent.From(data)));

    protected HttpClient Client { get; }

    async Task<T> ParseJson<T>(HttpResponseMessage response) =>
        JsonConvert.DeserializeObject<T>(await response.Content.ReadAsStringAsync(), Settings);

    class JsonContent : StringContent
    {
        public static JsonContent From(object data) =>
            data == null ? null : new JsonContent(data);

        public JsonContent(object data)
            : base(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json")
        {
        }
    }
}

Context

StackExchange Code Review Q#140412, answer score: 6

Revisions (0)

No revisions yet.