9

My Question: How do I do this?

So, I hadn't touched anything .Net in about 6 years until this week. There's a lot that I've forgotten and even more that I never knew and while I love the idea of the async/await keywords, I'm having a slight problem implementing the following requirements for a client's API implementation:

  1. The ServerAPI class has a method for each of the API methods, taking appropriate input parameters (e.g. the method Login takes in an id and a password, makes the API call and returns the result to the caller).
  2. I want to abstract away the JSON so that my API methods return the actual object you're fetching (e.g. the Login method above returns a User object with your auth token, uid, etc.)
  3. Some API methods return a 204 on success or no meaningful content (not meaningful in my usecase maybe I only care about success/failure), for these I'd like to return either a bool (true = success) or the status code.
  4. I'd like to keep the async/await (or equivalent) design, because it seems to really work well so far.
  5. For some methods, I might need to just return the HttpResponseMessage object and let the caller deal with it.

This is roughly what I have so far and I'm not sure how to make it compliant with the above OR whether I'm even doing this right. Any guidance is appreciated (flaming, however, is not).

// 200 (+User JSON) = success, otherwise APIError JSON
internal async Task<User> Login (string id, string password)
{
    LoginPayload payload = new LoginPayload() { LoginId = id, Password = password};
    var request = NewRequest(HttpMethod.Post, "login");
    JsonPayload<LoginPayload>(payload, ref request);

    return await Execute<Account>(request, false);
}

// 204: success, anything else failure
internal async Task<Boolean> LogOut ()
{
    return await Execute<Boolean>(NewRequest(HttpMethod.Delete, "login"), true);
}

internal async Task<HttpResponseMessage> GetRawResponse ()
{
    return await Execute<HttpResponseMessage>(NewRequest(HttpMethod.Get, "raw/something"), true);
}

internal async Task<Int32> GetMeStatusCode ()
{
    return await Execute<Int32>(NewRequest(HttpMethod.Get, "some/intstatus"), true);
}

private async Task<RESULT> Execute<RESULT>(HttpRequestMessage request, bool authenticate)
{
    if (authenticate)
        AuthenticateRequest(ref request); // add auth token to request

    var tcs = new TaskCompletionSource<RESULT>();
    var response = await client.SendAsync(request);     

    // TODO: If the RESULT is just HTTPResponseMessage, the rest is unnecessary        

    if (response.IsSuccessStatusCode)
    {
        try
        {
            // TryParse needs to handle Boolean differently than other types
            RESULT result = await TryParse<RESULT>(response);
            tcs.SetResult(result);
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }

    }
    else
    {
        try
        {
            APIError error = await TryParse<APIError>(response);
            tcs.SetException(new APIException(error));
        }
        catch (Exception e)
        {
            tcs.SetException(new APIException("Unknown error"));
        }
    }

    return tcs.Task.Result;
}

This is the APIError JSON structure (it's the status code + a custom error code).

{
  "status": 404,
  "code":216,
  "msg":"User not found"
}

I would prefer to stay with System.Net, but that's mostly because I don't want to switch all my code over. If what I want is easier done in other ways then it's obviously worth the extra work.

Thanks.

copolii
  • 14,208
  • 10
  • 51
  • 80
  • *My Question: **How do I do this?*** I'd say a *Code Review* is for code you consider complete not code you don't know how to complete. – copolii Mar 23 '16 at 21:58
  • Try looking into the `[JsonProperty("myPropertyName")]`, it might help you. – Lifes Mar 24 '16 at 01:27
  • So, do you mean you have already done the whole UWP project and tested it work well, but want to know if these code is meet the above requirements ,and if someone know better way to implement these requirements? – Sunteen Wu Mar 24 '16 at 09:48
  • @Sunteen I have it partially working: Login works because it returns an object, but Logout always fails because it tries to parse the content of a 204 (NoContent) and there's nothing there (I'd like this to return `true`/`false`). So I need help with returning `Int32` or `HttpResponseMessage` depending on the type. I could always have 3 different `execute` methods and call them accordingly if I have to, but it seems like I'm missing a tiny bit here. A push in the right direction. – copolii Mar 24 '16 at 17:51
  • 1
    According to your idea, it seems code like `if (RESULT IS HttpResponseMessage)if(RESULT IS Boolen)if(!(RESULT is ValueType))` is the only way meeting your requirements. But I recommend you not to use above code. Different features have separate execute method as you thinking about is a better way. The code looks like a little much doesn't mean a bad design, oppositely, like the code above, put the execute logic all in one, will increase coupling. For example, if you only want to change the logic of the Logout, you should update the only execute method, accidentally affect the Login feature. – Sunteen Wu Mar 25 '16 at 05:59

3 Answers3

16

Here is an example of how I've done it using MVC API 2 as backend. My backend returns a json result if the credentials are correct. UserCredentials class is the exact same model as the json result. You will have to use System.Net.Http.Formatting which can be found in the Microsoft.AspNet.WebApi.Client NugetPackage

public static async Task<UserCredentials> Login(string username, string password)
{
    string baseAddress = "127.0.0.1/";
    HttpClient client = new HttpClient();

    var authorizationHeader = Convert.ToBase64String(Encoding.UTF8.GetBytes("xyz:secretKey"));
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authorizationHeader);



    var form = new Dictionary<string, string>
    {
        { "grant_type", "password" },
        { "username", username },
        { "password", password },
    };

    var Response = await client.PostAsync(baseAddress + "oauth/token", new FormUrlEncodedContent(form));
    if (Response.StatusCode == HttpStatusCode.OK)
    {
        return await Response.Content.ReadAsAsync<UserCredentials>(new[] { new JsonMediaTypeFormatter() });
    }
    else
    {
        return null;
    }
}

and you also need Newtonsoft.Json package.

public class UserCredentials
    {
        [JsonProperty("access_token")]
        public string AccessToken { get; set; }

        [JsonProperty("token_type")]
        public string TokenType { get; set; }

        [JsonProperty("expires_in")]
        public int ExpiresIn { get; set; }

        //more properties...
    }
Khant Htet Naing
  • 128
  • 1
  • 3
  • 11
Stamos
  • 3,938
  • 1
  • 22
  • 48
  • The wordy methods are exactly what I want to avoid. Also, is there a reason you're instantiating a new `HttpClient` in each method or did you include that just for the sake of completeness? I've declared one instance of `HttpClient` in my `ServerAPI` class and plan to route all requests through that. Thanks. – copolii Mar 28 '16 at 18:31
  • 1
    Correct, it was just for the sake of completeness. I was also between wordy or somethink more universal but I choose to handle each api call in different method cause its easier for me to handle what I have to do. Also is easier for my team mates to comprehend it and make changes. – Stamos Mar 28 '16 at 18:52
2

i would use a Deserializer.

HttpResponseMessage response = await client.GetAsync("your http here");
            var responseString = await response.Content.ReadAsStringAsync();
[Your Class] object= JsonConvert.DeserializeObject<[Your Class]>(responseString.Body.ToString());
1

So, first to address the you need Newtonsoft.Json comments, I really haven't felt the need yet. I've found the built in support to work well so far (using the APIError Json in my original question:

[DataContract]
internal class APIError
{
    [DataMember (Name = "status")]
    public int StatusCode { get; set; }
    [DataMember (Name = "code")]
    public int ErrorCode { get; set; }
}

I have also defined a JsonHelper class to (de)serialize:

public class JsonHelper
{
    public static T fromJson<T> (string json)
    {
        var bytes = Encoding.Unicode.GetBytes (json);

        using (MemoryStream mst = new MemoryStream(bytes))
        {
            var serializer = new DataContractJsonSerializer (typeof (T));
            return (T)serializer.ReadObject (mst);
        }
    }

    public static string toJson (object instance)
    {
        using (MemoryStream mst = new MemoryStream())
        {
            var serializer = new DataContractJsonSerializer (instance.GetType());
            serializer.WriteObject (mst, instance);
            mst.Position = 0;

            using (StreamReader r = new StreamReader(mst))
            {
                return r.ReadToEnd();
            }
        }
    }
}

The above bits I already had working. As for a single method that would handle each request execution based on the type of result expected while it makes it easier to change how I handle things (like errors, etc), it also adds to the complexity and thus readability of my code. I ended up creating separate methods (all variants of the Execute method in the original question:

// execute and return response.StatusCode
private static async Task<HttpStatusCode> ExecuteForStatusCode (HttpRequestMessage request, bool authenticate = true)
// execute and return response without processing
private static async Task<HttpResponseMessage> ExecuteForRawResponse(HttpRequestMessage request, bool authenticate = true)
// execute and return response.IsSuccessStatusCode
private static async Task<Boolean> ExecuteForBoolean (HttpRequestMessage request, bool authenticate = true)
// execute and extract JSON payload from response content and convert to RESULT 
private static async Task<RESULT> Execute<RESULT>(HttpRequestMessage request, bool authenticate = true)

I can move the unauthorized responses (which my current code isn't handling right now anyway) into a new method CheckResponse that will (for example) log the user out if a 401 is received.

copolii
  • 14,208
  • 10
  • 51
  • 80