2017年10月28日 星期六

C# HttpClient WebAPI : 16. 呼叫 Web API 之後端與用戶端例外異常處理用法,避免應用程式異常中斷

在我們提供的 HttpClient 用戶端程式碼中,有將 HttpClient 的相關存取程式碼,都加入到 try 區塊內,這樣,當用戶端程式碼發生例外異常的時候,我們就可以將其例外異常捕捉起來;不過,對於 Web API 伺服器那端,若發生了例外異常之後,我們在用戶端處理上,就會有些狀況發生。
在我們的設計模式中,希望不論後端系統是否有例外異常發生的時候,都能夠回傳 APIResult JSON 編碼內容,這樣,我們就可以憑藉著這個些屬性值,做出適當的判斷與處理。由於我們後端的 Web API 程式碼採用的是 ASP.NET Core 的方式來進行開發,在後端那哩,我們可以設計一個繼承 ExceptionFilterAttribute 類別的物件,並且套用到整個控制器或者相關動作中,這樣,在用戶端那哩,我們就可以統一接收到 APIResult JSON 編碼內容。

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式


呼叫 Web API 之後端與用戶端例外異常處理用法,避免應用程式異常中斷

在這個 GetExceptionFilter 方法中,我們會接收一個字串參數,這個參數會用來產生不同的 URL,當傳入的是 GetExceptionFilter 字串,我們將會呼叫有套用客製化的 CustomExceptionFilterAttribute 屬性,而傳入的是 GetException 字串,則沒有套用任何 ExceptionFilterAttribute
當取得 HttpResponseMessage 物件之後,我們會使用 response.IsSuccessStatusCode 屬性來判斷這次的呼叫,是否有成功,若沒有成功的話,我們會先嘗試解碼回傳內容,若成功的話,就可以取得後端伺服器回覆的內容,但是解碼失敗的話,則會在用戶端自行產生一個新的 APIResult 物件,並且設定回傳錯誤資訊。
private static async Task<APIResult> GetExceptionFilter(string action)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/values/{action}";
                HttpResponseMessage response = null;

                #region  設定相關網址內容
                var fooFullUrl = $"{FooUrl}";

                // Accept 用於宣告客戶端要求服務端回應的文件型態 (底下兩種方法皆可任選其一來使用)
                //client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Content-Type 用於宣告遞送給對方的文件型態
                //client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

                response = await client.GetAsync(fooFullUrl);
                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                    }
                    else
                    {
                        // 這裡將會取得這次例外異常的錯誤資訊
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });

                        if (fooAPIResult == null)
                        {
                            fooAPIResult = new APIResult
                            {
                                Success = false,
                                Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.RequestMessage),
                                Payload = null,
                            };
                        }
                    }
                }
                else
                {
                    fooAPIResult = new APIResult
                    {
                        Success = false,
                        Message = "應用程式呼叫 API 發生異常",
                        Payload = null,
                    };
                }
                #endregion
            }
            catch (Exception ex)
            {
                fooAPIResult = new APIResult
                {
                    Success = false,
                    Message = ex.Message,
                    Payload = ex,
                };
            }
        }
    }

    return fooAPIResult;
}

觸發的 Web API 動作

當 URL 指向的是 http://vulcanwebapi.azurewebsites.net/api/values/GetExceptionFilter ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public APIResult GetExceptionFilter() 動作(Action),其有套用我們自訂的 CustomExceptionFilter C# 屬性 (Attribute) 在這個方法上,其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpGet("GetExceptionFilter")]
[CustomExceptionFilter]
public APIResult GetExceptionFilter()
{
    APIResult foo = new APIResult();
    throw new Exception("喔喔,我發生錯誤了");
    return foo;
}
而這個 CustomExceptionFilterAttribute 類別,則是當後端 Web API 有例外異常發生的時候,就會執行 OnException 這裡覆寫方法,在這個方法內,我們取得當時後端伺服器中發生的例外異常呼叫堆疊與訊息,並且把些資料,設定到 APIResul 類別物件內,這樣,用戶端就會得到這個回傳訊息(雖然,伺服器端已經發生了錯誤)。
這裡,我們也使用了 context.HttpContext.Response.StatusCode 設定了此次 Http 呼叫的處理狀態碼為 500。
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        APIResult foo = new APIResult();
        // Unhandled errors
        var msg = context.Exception.GetBaseException().Message;
        string stack = context.Exception.StackTrace;

        foo.Success = false;
        foo.Message = msg;
        foo.Payload = stack;

        context.HttpContext.Response.StatusCode = 500;
        context.Result = new JsonResult(foo);

        base.OnException(context);
    }

}
當 URL 指向的是 http://vulcanwebapi.azurewebsites.net/api/values/GetException ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public APIResult GetException() 動作(Action),這裡沒有套用任何自訂的 ExceptionFilterAttribute C# 屬性 (Attribute) 在這個方法上,其該動作的原始碼如下所示。
這個 Web API 動作,將不會回傳一個 APIData 的 JSON 資料。
[HttpGet("GetException")]
public APIResult GetException()
{
    APIResult foo = new APIResult();
    throw new Exception("喔喔,我發生錯誤了");
    return foo;
}

進行測試

在程式進入點函式,我們呼叫了兩次 GetExceptionFilter 方法,不過,由於傳送的引數字串不同,會導致執行不同 URL 的 Http GET 要求。
static async Task Main(string[] args)
{
    var foo = await GetExceptionFilter("GetExceptionFilter");
    Console.WriteLine($"使用 Get 方法呼叫,並有套用 ExceptionFilter");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"其他訊息 : {foo.Payload}");
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();

    foo = await GetExceptionFilter("GetException");
    Console.WriteLine($"使用 Get 方法呼叫,沒有套用 ExceptionFilter");
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    Console.WriteLine($"其他訊息 : {foo.Payload}");
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey(); 
}

執行結果

這個測試將會輸出底下內容
使用 Get 方法呼叫,並有套用 ExceptionFilter
結果狀態 : False
結果訊息 : 喔喔,我發生錯誤了
其他訊息 :    at VulcanWebAPI.Controllers.ValuesController.GetExceptionFilter() in D:\Vulcan\GitHub\CSharpNotes\WebAPI\VulcanWebAPI\Controllers\ValuesController.cs:line 86
   at lambda_method(Closure , Object , Object[] )
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextExceptionFilterAsync>d__23.MoveNext()

Press any key to Exist...

使用 Get 方法呼叫,沒有套用 ExceptionFilter
結果狀態 : False
結果訊息 : Error Code:InternalServerError, Error Message:Method: GET, RequestUri: 'http://vulcanwebapi.azurewebsites.net/api/values/GetException', Version: 1.1, Content: <null>, Headers:
{
  Accept: application/json
}
其他訊息 :

Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    這裡將會呼叫 URL 有套用 CustomExceptionFilterAttribute 的屬性之控制器動作。
GET http://vulcanwebapi.azurewebsites.net/api/values/GetExceptionFilter HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
    由於 ASP.NET Core 系統會自動捕捉到任何在呼叫 Web API 過程中所產生的例外異常,因此,在用戶端中,我們可以透過回傳的 JSON 字串,經過反序列化得到 APIResult 類別物件;我們可以從這個物件,得到更多關於此次伺服器上發生的錯誤資訊。
HTTP/1.1 500 Internal Server Error
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:24:12 GMT

7f0
{"success":false,"message":"喔喔,我發生錯誤了","payload":"   at VulcanWebAPI.Controllers.ValuesController.GetExceptionFilter() in D:\\Vulcan\\GitHub\\CSharpNotes\\WebAPI\\VulcanWebAPI\\Controllers\\ValuesController.cs:line 86\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextExceptionFilterAsync>d__23.MoveNext()"}
0
  • 請求 (Request)
    這裡將會呼叫 URL 將沒有套用 CustomExceptionFilterAttribute 的屬性之控制器動作。
GET http://vulcanwebapi.azurewebsites.net/api/values/GetException HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
  • 反應 (Response)
    您可以看到,由於沒有套用任何 ExceptionFilterAttribute 的客製類別屬性,所以,所得到的 Http 回應封包卻沒有辦法看到任何此次異常發生了甚麼問題,只知道狀態碼是 500 Internal Server Error
HTTP/1.1 500 Internal Server Error
Content-Length: 0
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:24:16 GMT

相關文章索引

C# HttpClient WebAPI 系列文章索引

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式



關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


C# HttpClient WebAPI : 15. 呼叫 Web API 的處理進度事件回報與強制取消用法

通常,若我們需要從 Web API 取得大量的資料到用戶端的時候,或者因為網路品質不好,導致資料回傳時間過長,我們希望提供當時取得網路資源的進度資訊給使用者知道,在這個練習中,我們將會嘗試從網路上下載一個圖片檔案,並且顯示這個圖片的完整取得進度百分比。
另外,我們也提供了一項功能,那就是當使用者在取得資料的過程中,使用者可以決定是否要終止此次的網路存取行為,為了要能夠讓使用者可以操作這樣效果,所以,我們在用戶端這裡,會適時地暫停一小段時間,讓使用者可以決定是否有取消這次存取行為。

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式


呼叫 Web API 的處理進度事件回報與強制取消用法

這個 ReportProgress 方法,將是進行 HttpClient 讀取資料的時候,要回報進度的委派方法,您也可以視為他是的 Call Back 方法。
在 DownloadImageAsync 方法中,我們需要三個參數,第一個是下載圖片的檔案名稱,第二個是要回報進度的 IProgress,第三個則是用來取消 HttpClient 動作的 CancellationToken 類別物件。
在我們進行取得圖片的資源的時候,使用這個 await client.GetAsync(fooFullUrl, HttpCompletionOption.ResponseHeadersRead); 敘述,其中, HttpCompletionOption.ResponseHeadersRead列舉值,指示 HttpClient 應該儘快的回傳任何回應內容,而不是要把所有的內容都放置到緩衝區內,才結束這個方法執行。透過這個列舉值的使用,我們可以提早得到這個 Http 回應的 Header,並且可以知道檔案的全部大小。
在我們執行相關 Http 請求動作並且得到 HttpResponseMessage 物件之後,接著判斷此次的請求動作的狀態碼是成功的,我們就可以透過 var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L; 取得這個圖片檔案的全部檔案大小數值。
在進行將 Http 回傳的圖片內容,將其寫入到本機檔案中,我們使用了 await response.Content.ReadAsStreamAsync() 表示式,每次讀取一定大小的資料(我們這裡,每次讀取 4096 Bytes),接著寫入到檔案,然後反覆這樣的動作,直到所有的內容都成功寫入到檔案內。當每個小區塊資料寫入到檔案之後,我們就會執行傳入參數的IProgress物件,在這裡使用 progress.Report((totalRead * 1d) / (total * 1d) * 100); 表示式來執行,這樣,我們在呼叫這個方法的地方,就可以反映出現在下載完成百分比進度。
另外,在這個反覆讀取網路資源小區塊資料,且入到檔案的反覆過程中,我們看到了一個表示式 token.ThrowIfCancellationRequested();,當這個 token 物件所歸屬的 CancellationTokenSource 類別物件,執行了 cts.Cancel(); 方法,那麼,當執行 token.ThrowIfCancellationRequested(); 的時候,就會產生例外異常,當然,也就取消了這次的 Web API 呼叫。
因為我們下載的圖片檔案沒有很大,因此,每讀、寫一次 Stream 區塊,我們就執行 await Task.Delay(200); 敘述,暫停個 0.2 秒鐘。
private static void ReportProgress(double obj)
{
    Console.WriteLine($"下載完成進度 {obj}");
}

private static async Task<APIResult> DownloadImageAsync(string filename,
    IProgress<double> progress, CancellationToken token)
{
    string ImgFilePath = $"My_{filename}";
    ImgFilePath = Path.Combine(Environment.CurrentDirectory, ImgFilePath);
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/Datas/";
                HttpResponseMessage response = null;

                #region  設定相關網址內容
                var fooFullUrl = $"{FooUrl}{filename}";

                response = await client.GetAsync(fooFullUrl, 
                    HttpCompletionOption.ResponseHeadersRead);
                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        #region 狀態碼為 OK
                        var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L;
                        var canReportProgress = total != -1 && progress != null;
                        using (var filestream = File.Open(ImgFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
                        {
                            using (var stream = await response.Content.ReadAsStreamAsync())
                            {
                                var totalRead = 0L;
                                var buffer = new byte[4096];
                                var isMoreToRead = true;

                                do
                                {
                                    token.ThrowIfCancellationRequested();

                                    var read = await stream.ReadAsync(buffer, 0, buffer.Length);

                                    if (read == 0)
                                    {
                                        isMoreToRead = false;
                                    }
                                    else
                                    {
                                        await filestream.WriteAsync(buffer, 0, read);

                                        totalRead += read;

                                        if (canReportProgress)
                                        {
                                            progress.Report((totalRead * 1d) / (total * 1d) * 100);
                                        }
                                    }
                                    // 故意暫停,讓使用者可以取消下載
                                    await Task.Delay(200);
                                } while (isMoreToRead);
                            }
                        }
                        fooAPIResult = new APIResult
                        {
                            Success = true,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.Content),
                            Payload = ImgFilePath,
                        };
                        #endregion
                    }
                    else
                    {
                        fooAPIResult = new APIResult
                        {
                            Success = false,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.RequestMessage),
                            Payload = null,
                        };
                    }
                }
                else
                {
                    fooAPIResult = new APIResult
                    {
                        Success = false,
                        Message = "應用程式呼叫 API 發生異常",
                        Payload = null,
                    };
                }
                #endregion
            }
            catch (Exception ex)
            {
                fooAPIResult = new APIResult
                {
                    Success = false,
                    Message = ex.Message,
                    Payload = ex,
                };
            }
        }
    }

    return fooAPIResult;
}

觸發的 Web API 動作

這個範例中,將會指向 URL http://vulcanwebapi.azurewebsites.net/Datas/vulcan.png,此時,將會向 Web API 伺服器要求回傳該圖片回到用戶端上。

進行測試

在程式進入點函式,我們建立一個 CancellationTokenSource 型別的物件,接著,取得 CancellationTokenSource.Token,這個是屬於 CancellationToken 的物件,我們需要將這個 CancellationToken 物件傳送到下載圖片的 DownloadImageAsync 方法內。
此時,我們另外啟動一個新的執行緒,這個執行緒並不做任何事情,只是等候使用者按下任一按鍵,若使用者按下了 C 這個按鍵,則 cts.Cancel(); 將會執行,而當時的 HttpClient 抓取圖片動作,將會取消。
static async Task Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;

    Thread t1 = new Thread(() =>
    {
        if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
            cts.Cancel();
    });

    t1.Start();

    var progressIndicator = new Progress<double>(ReportProgress);

    var fooResult = await DownloadImageAsync("vulcan.png", progressIndicator, token);
    if (fooResult.Success == true)
    {
        Process myProcess = new Process();
        try
        {
            // true is the default, but it is important not to set it to false
            myProcess.StartInfo.UseShellExecute = true;
            myProcess.StartInfo.FileName = fooResult.Payload.ToString();
            myProcess.Start();
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
    else
    {
        Console.WriteLine($"使用者中斷下載作業 {fooResult.Message} {Environment.NewLine}"); 
        Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
        Console.ReadKey();
    }
}

執行結果

這個測試將會輸出底下內容
下載完成進度 5.10764588152406
下載完成進度 7.6614688222861
下載完成進度 10.2152917630481
下載完成進度 12.7691147038102
下載完成進度 15.3229376445722
下載完成進度 17.8767605853342
下載完成進度 20.4305835260963
下載完成進度 22.9844064668583
下載完成進度 25.5382294076203
下載完成進度 28.0920523483824
下載完成進度 30.6458752891444
下載完成進度 33.1996982299064
下載完成進度 35.7535211706684
下載完成進度 38.3073441114305
下載完成進度 40.8611670521925
下載完成進度 43.4149899929545
下載完成進度 45.9688129337166
下載完成進度 48.5226358744786
下載完成進度 51.0764588152406
下載完成進度 53.6302817560027
下載完成進度 56.1841046967647
下載完成進度 58.7379276375267
下載完成進度 61.2917505782888
下載完成進度 63.8455735190508
下載完成進度 66.3993964598128
下載完成進度 68.9532194005749
下載完成進度 71.5070423413369
下載完成進度 74.0608652820989
下載完成進度 76.614688222861
下載完成進度 79.168511163623
下載完成進度 81.722334104385
下載完成進度 84.276157045147
下載完成進度 86.8299799859091
下載完成進度 89.3838029266711
下載完成進度 91.9376258674332
下載完成進度 94.4914488081952
下載完成進度 97.0452717489572
下載完成進度 99.5990946897192
下載完成進度 100
Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
GET http://vulcanwebapi.azurewebsites.net/Datas/vulcan.png HTTP/1.1
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
HTTP/1.1 200 OK
Content-Length: 160387
Content-Type: image/png
Last-Modified: Sun, 08 Oct 2017 16:09:22 GMT
Accept-Ranges: bytes
ETag: "1d3404fcd99d783"
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:22:35 GMT

 PNG
 

IHDR              u         pHYs          +      tIME         8ǫe     tEXtAuthor    H    tEXtDescription      !#   
tEXtCopyr

*** FIDDLER: RawDisplay truncated at 128 characters. Right-click to disable truncation. ***

相關文章索引

C# HttpClient WebAPI 系列文章索引

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式



關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程