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 課程


沒有留言:

張貼留言