Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting the content of HttpResponseMessage to object

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.

like image 676
copolii Avatar asked Mar 23 '16 21:03

copolii


3 Answers

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...
    }
like image 166
Stamos Avatar answered Oct 22 '22 09:10

Stamos


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.

like image 24
copolii Avatar answered Oct 22 '22 09:10

copolii


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());
like image 43
Timothy Moultt Lubinga Avatar answered Oct 22 '22 11:10

Timothy Moultt Lubinga