5
\$\begingroup\$

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<Store> GetStores(string postCode)
    {
        var request = GetRequest("stores");
        request.RootElement = "Stores";
        request.AddQueryParameter("q", postCode);

        var response = _restClient.Execute<List<Store>>(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;
    }
}
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

Injecting the IRestClient doesn't seem to be of much use here as you are setting its BaseUrl to a hardcoded string of the TopManClient. The whole point of DI is to inject an already configured rest client so that you can test it against various targets that you pass via DI. The same goes for other things that you setup in the constructor. Currently no matter what you inject the result will be the same.

ApplicationException

You shouldn't throw this one. Create your own that by its name already names the reason for the exception.


BTW the constructor doesn't match the class name...

\$\endgroup\$
3
  • \$\begingroup\$ I'm thinking that the GetStores should be in another class and then i should inject the TopManClient class as it setup and configured!? \$\endgroup\$ Commented Sep 4, 2016 at 9:29
  • \$\begingroup\$ Could you also explain this a little bit more? I'm not 100% sure what you mean. "Create your own that by its name already names the reason for the exception." \$\endgroup\$ Commented Sep 4, 2016 at 9:30
  • \$\begingroup\$ @user2859298 the exception message is Error retrieving response so you could actually create a new exception like class ResponseException: Exception { ... } - you'll look a it later and you'll know - aha! there is something wrong with the response (even without reading the message yet) - however when you see ApplicationException you think, mhmm, but with what exactly? wait, I need to dig deeper... \$\endgroup\$
    – t3chb0t
    Commented Sep 4, 2016 at 10:15
6
\$\begingroup\$

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<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; }
}

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<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")
        {
        }
    }
}

This code is fully testable.

\$\endgroup\$
3
  • \$\begingroup\$ Your post was very helpful, thank you. How would you go about implementing a Post request to JSONPlaceholder using your REST API? \$\endgroup\$
    – Ryan
    Commented Feb 14, 2017 at 21:03
  • \$\begingroup\$ That's Good Code. But, how can I return a different Type altogether? If, my API returned some ErrorModel which I was not expecting. How can I handle that in the abstract class along with Generics? \$\endgroup\$ Commented Jan 13, 2020 at 19:26
  • 1
    \$\begingroup\$ @JawandSingh Have a look at this one - fully automated code generation. The idea is to have couple type parameters and return one through generic exception. \$\endgroup\$ Commented Jan 14, 2020 at 1:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.