异步下载和反序列化

时间:2016-05-21 23:53:47

标签: c# asynchronous xamarin.android

晚上好,

我正在尝试使用异步编程来优化一些代码,并且想知道以下代码是否编写良好,以及是否有任何改进方法。

其目的是从给定的URL下载一些JSON并将其反序列化为对象。

我有三个(现在四个)问题(和帖子末尾描述的1个问题):

  • 我应该使用TaskEx.Run来运行 Newtonsoft.Json.JsonConvert.DeserializeObject<T>
  • 有没有好的方法(没有检查对象属性) 知道rootObject是否成功创建?
  • 我应该在某处检查是否要求取消?
  • 新问题)在进行新请求之前,我应该是CancelPendingRequests吗?

不用多说,这里是代码:

internal static class WebUtilities
{
    /// <summary>
    /// Downloads the page of the given url
    /// </summary>
    /// <param name="url">url to download the page from</param>
    /// <param name="cancellationToken">token to cancel the download</param>
    /// <returns>the page content</returns>
    internal static async Task<string> DownloadStringAsync(string url, CancellationToken cancellationToken)
    {
        try
        {
            // create Http Client and dispose of it even if exceptions are thrown (same as using finally statement)
            using (var client = new HttpClient() {Timeout = TimeSpan.FromSeconds(5)})
            {
                // should I always do this?
                client.CancelPendingRequests();

                // do request and dispose of it when done
                using (var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false))
                {
                    // if response was successful (otherwise it will return null)
                    if (response.IsSuccessStatusCode)
                    {
                        // return its content
                        return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    }
                }
            }
        }
        catch (Exception ex) when (ex is System.Net.Sockets.SocketException || 
                                    ex is InvalidOperationException || 
                                    ex is OperationCanceledException ||
                                    ex is System.Net.Http.HttpRequestException)
        {
            WriteLine("DownloadStringAsync task has been cancelled.");
            WriteLine(ex.Message);
            return null;
        }

        // return null if response was unsuccessful
        return null;
    }

    /// <summary>
    /// Downloads Json from a given url and attempts its deserialization to a given reference type (class)
    /// </summary>
    /// <typeparam name="T">the class to deserialize to</typeparam>
    /// <param name="url">url to download the json from</param>
    /// <param name="cancellationToken">token to cancel the download</param>
    /// <returns>the deserialized object</returns>
    internal static async Task<T> DownloadJsonAndDeserialize<T>(string url, CancellationToken cancellationToken) where T : class, new()
    {
        // download json from the given url
        var jsonResponse = await DownloadStringAsync(url, cancellationToken).ConfigureAwait(false);

        // if the response is invalid, no need to go further
        if (string.IsNullOrEmpty(jsonResponse))
            // return a default constructor instance
            return new T();

        // try to deserialize
        try
        {
            // Deserialize json data to the given .NET object
            // Should I use TaskEx.Run here?
            return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(jsonResponse);
        }
        catch (Exception ex) when (ex is JsonException)
        {
            WriteLine("Something went wrong while deserializing json to the given reference type.");
            WriteLine(ex.Message);
        }

        // return a default constructor instance
        return new T();
    }
}

要调用代码,可以执行以下操作:

internal static async Task CallAsync()
    {
        RootObject root = null;

        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
        {
            var token = cts.Token;

            root = await WebUtilities.DownloadJsonAndDeserialize<RootObject>(URL, token).ConfigureAwait(false);
        }

        // do something with rootObject
        // any good way to know if rootObject was successfully created, before moving on?
    }

你会改变什么?为什么?

谢谢!

修改

  • @codran建议 - 每个await现在都使用ConfigureAwait(false)
  • CancellationTokenSource现已被处理(使用声明)
  • @codran建议 - 现在已过滤了一些例外
  • DownloadStringAsync现在using内有2 try-catch个语句 阻止更多可读性(而不是try-catch-finally)。
  • 现在检查是否有回复IsSuccessStatusCode

发现问题(为其创建了另一个问题 - Timeouts in Xamarin HTTP requests):

有趣的是,当无法访问主机时(例如,离线本地服务器),GetAsync之后没有任何反应几分钟后(大约3分钟)会抛出System.Net.WebException,说错误:ConnectFailure(连接超时)。内部异常为System.Net.Sockets.SocketsException(此处为完整日志:http://pastebin.com/MzHyp2FM)。

我尝试将client.Timeout设置为5秒,但这似乎无法正常工作

可能是一个Xamarin错误(https://forums.xamarin.com/discussion/5941/system-net-http-httpclient-timeout-seems-to-be-ignored)。

无论哪种方式,不应该在10秒后自动取消

此超时问题发生在/ when:

  • 离线/无法访问的IP地址(在我的情况下是一个 请求脱机本地服务器,例如192.168.1.101:8080)(例如,GetAsync,SendAsync,GetResponseAsync)

该代码适用于/何时:

  • 没有互联网连接(抛出异常)
  • DNS无法解析URL(抛出异常)
  • 提供了有效的URL(启动并运行)

  • 请求来自桌面客户端(例如,WPF),如果 IP处于脱机/无法访问状态,它会非常快速地抛出4个异常( No 可以建立连接,因为目标机器主动拒绝 它

结论

  • Xamarin似乎在这些请求中有一些错误(Timeouts at 至少?),因为他们没有给出预期的结果 桌面应用。

2 个答案:

答案 0 :(得分:2)

  

我应该使用TaskEx.Run来运行Newtonsoft.Json.JsonConvert.DeserializeObject<T>吗?

将JSON反序列化抛到另一个线程上可能无法实现任何目标。你正在释放当前的线程,但是需要等待你的工作安排在另一个线程上。除非您特别需要Newtonsoft.Json API,否则请考虑使用ReadAsAsync<T>。您可以使用JsonMediaTypeFormatteruses Newtonsoft.Json internally anyway

  

有没有好的方法(不检查对象属性)知道rootObject是否已成功创建?

您需要定义成功的样子。例如,您的JSON可能是null,因此rootObject为空。或者它可能缺少属性,或者您的JSON具有未反序列化的额外属性。我不认为有一种方法可以使序列化过多或缺少属性,所以你必须自己检查一下。在这一点上我可能是错的。

  

我应该检查某处是否要求取消?

您需要查看代码中有意义的内容以及何时取消代码。例如,当所有工作都已完成时,作为函数中最后一行的取消令牌没有任何意义。

如果已下载JSON但尚未反序列化,您的应用程序是否有理由取消操作?如果JSON已被反序列化,但是对象图尚未经过验证(问题2),您的应用程序是否有理由取消操作?

这实际上取决于你的需求,但如果下载还没有完成,我可能会取消 - 这似乎你已经在做 - 但是一旦下载完成,那就是不归路。

  

你会改变什么?为什么?

ConfigureAwait(false)的任务上使用await。如果代码阻塞并等待生成的任务(例如.Wait().Result),这可以防止死锁。

使用using块代替try - finally

答案 1 :(得分:0)

我在生产中使用以下代码而没有任何问题。

// Creating and disposing HttpClient is unnecessary
// if you are going to use it multiple time
// reusing HttpClient improves performance !!
// do not worry about memory leak, HttpClient
// is designed to close resources used in single "SendAsync"
// method call
private static HttpClient client;


public Task<T> DownloadAs<T>(string url){

    // I know I am firing this on another thread
    // to keep UI free from any smallest task like
    // preparing httpclient, setting headers
    // checking for result or anything..., why do that on
    // UI Thread?

    // this helps us using excessive logging required for
    // debugging and diagnostics

    return Task.Run(async ()=> {

       // following parses url and makes sure
       // it is a valid url
       // there is no need for this to be done on 
       // UI thread
       Uri uri = new Uri(url);

       HttpRequestMessage request = 
          new HttpRequestMessage(uri,HttpMethod.Get);

       // do some checks, set some headers...
       // secrete code !!!

       var response = await client.SendAsync(request,
           HttpCompletionOption.ReadHeaders);

       string content = await response.Content.ReadAsStringAsync();

       if(((int)response.StatusCode)>300){

           // it is good to receive error text details
           // not just reason phrase

           throw new InvalidOperationException(response.ReasonPhrase  
              + "\r\n" + content);
       }
       return JsonConvert.DeserializeObject<T>(content);

    });
}

ConfigureAwait很棘手,使调试更加复杂。此代码将毫无问题地运行。

如果您想使用CancellationToken,则可以使用client.GetAsync方法传递令牌。