Search code examples
c#win-universal-appsystem.net

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.


Solution

  • 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.