【ASP.NET Core】处理异常
依照老周的良好作風,開始之前先說點題外話。
前面的博文中,老周介紹過自定義 MVC 視圖的搜索路徑,即向?ViewLocationFormats 列表添加相應的內容,其實,對 Razor Page 模型,也可以向?PageViewLocationFormats 列表添加相應的搜索路徑,比如 /MyPages/{1}/{0}.cshtml。其中,0 是視圖名,1 是頁面名稱。比如這樣。
services.AddMvc().AddRazorOptions(opt =>{opt.ViewLocationFormats ... ? ? ? ? ? ? ? ?opt.PageViewLocationFormats ...});然而,我們知道,基于 Razor 的 Web Page 模型是以頁面為單位的,也就是說路徑路由是直接指向頁面的(不包含.cshtml 擴展名),即不需要 MVC 模型的路由方式。所以,我們并不需要修改?PageViewLocationFormats 中的內容。許多時候,我們只要告訴應用程序在哪個目錄下查找 Page 就行了。
默認的搜索位置是 /Pages 目錄,我們可以通過以下代碼來修改。
public void ConfigureServices(IServiceCollection services)
? ? ? ? {
? ? ? ? ? ? services.AddMvc().AddRazorPagesOptions(opt =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? opt.RootDirectory = "/UI";
? ? ? ? ? ? });
? ? ? ? }
以上代碼寫在 Startup 類中,這個應該明白吧。RootDirectory 就是用來指定應用程序查找 Razor 頁面的根目錄路徑,此處我指寫了 /UI,所以,在我的項目中,我只要建一個 UI 目錄,然后各類 Razor 頁就往里面放就行了。
?
好了,題外話扯完了,開始說正題吧。今天咱們聊聊有關異常處理的破事吧,也可以說是錯誤處理,反正就這個意思,你理解就好,專業名詞不必較勁,只有那些吃飽了撐著的“學術人才”才會跟名詞較勁。
老辦法,咱們結合示例來講述,這樣各位觀眾不會乏味。
大家知道,娛樂產品腎Phone已經成為流行玩具,近年來,購買腎Phone不一定只能用貨幣,比較典型的一種支付方式是賣腎買Phone。說實話,現在許多國產娛樂產品也很便宜,配置也不錯,幾百塊錢就能玩得刷刷響了,割腎真沒什么必要。
為了方便人們以腎換 Phone ,老周特意開發了一個在線賣腎系統。大致流程是這樣的,如果你有閑置的腎,可以打開主頁,輸入你的一些信息,然后報個價,其他用戶看見后,如果覺得合理,就認購此腎。
?
?為了使操作流程更簡單,易上手,輕入門,該平臺只需要輸入姓名和腎的價格即可參加報價。
?
大致的頁面代碼如下。
<form method="post"><div class="form-group"><label for="name">姓名:</label><input type="text" class="form-control" name="name"/></div><div class="form-group"><label for="price">價格:</label><input type="number" name="price" class="form-control"/></div><div class="form-group"><button type="submit" class="btn btn-success w-100">提 交</button></div></form>Razor 頁面很像我們以前玩過的 aspx 頁面,每個頁面都配套一個隱藏代碼文件。Razor 頁也會配有一個頁面模型類,注意這個模型類要從 PageModel 派生,不是 Page 類,別搞錯了,Page 類只是作為生成 HTML 代碼的基類,我們的 .cshtml 文件在預編譯后,是隱式繼承自?RazorPage 類的。除非你要開發自己的標記語言,否則你不必理會這些類。
記住了,與 Razor 頁關聯的模型類是從?PageModel 類派生的,比如,本例中,當有人填寫了閑置腎的相關信息后,以 POST 方式提交,這是候,如果頁面模型類中包含了名字為 OnPost、OnPostAsync ……的方法時,就會自動調用。如果想把我們上面那個 form 中的 name 和 price 的值傳遞給方法,直接讓 OnPost 方法的參數與 form 中的元素名稱相同就可以了。
public class IndexModel : PageModel
? ? {
? ? ? ?public IActionResult OnPost(string name, decimal price)
? ? ? ? {
? ? ? ? ? ? if (string.IsNullOrWhiteSpace(name))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? throw new Exception("你怎么不留下姓名啊,賣腎又不是丟人的事。");
? ? ? ? ? ? }
? ? ? ? ? ? if(price <= 0.0M)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? throw new Exception("靠!你的腎這么不值錢嗎?還免費送,包郵不?");
? ? ? ? ? ? }
? ? ? ? ? ? return RedirectToPage("/Success");
? ? ? ? }
? ? }
OnPost 不是 PageModel 基類的方法,而是我們自己寫的,只是代碼約定,Asp.net Core 里面用到很多代碼約定,它在運行的時候會查找這些特定的名字。
上面代碼中,還對傳遞進來的 form 值進行驗證,如果不符合要求,會拋出異常。
?
一般來說,在 Startup 類的 Configure 方法中,我們會判斷一下,如果應用程序處于開發階段,為了方便測試,應該加入這些代碼。
if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}這樣,我們在測試時能看到詳細的異常信息。
?
但是,在實際便用時,我們不能公開這么詳細的信息,這樣容易勾起人們的犯罪沖動。所以,一般會添加一個頁面,專門用來顯示錯誤信息。比如:
@page
<div class="card">
? ? <div class="card-header bg-danger">
? ? ? ? <span class="text-light">錯誤</span>
? ? </div>
? ? <div class="card-body">
? ? ? ? <span class="card-text">唉,真抱歉。你提交的腎不符合國際標準,沒人要的。</span>
? ? </div>
</div>
然后我們要在 Startup.Configure 方法中配置一下。
app.UseExceptionHandler("/Error");加上這一行后,當發生異常時,就會跳轉到 /Error 頁面。
?
?不過,你也許會覺得,雖然不能公開異常信息,但一些必要的描述應該要的,不然,用戶不知道發生了啥事。我們可以通過 HttpContext 的?Features 集合獲取一個用來處理異常的 Feature,它的原型接口是?IExceptionHandlerFeature,我們不必關心它的實現類型是誰,只要訪問它的 Error 屬性就能得到關聯的 Exception 實例。
因此,我們的錯誤頁可以改一下。
@page
@using Microsoft.AspNetCore.Diagnostics
@{
? ? IExceptionHandlerFeature exf = HttpContext.Features.Get<IExceptionHandlerFeature>();
? ? Exception ex = exf?.Error;
}
<div class="card">
? ? <div class="card-header bg-danger">
? ? ? ? <span class="text-light">錯誤</span>
? ? </div>
? ? <div class="card-body">
? ? ? ? @if (ex == null)
? ? ? ? {
? ? ? ? ? ? <span class="card-text">唉,真抱歉。你提交的腎不符合國際標準,沒人要的。</span>
? ? ? ? }
? ? ? ? else
? ? ? ? {
? ? ? ? ? ? <span class="card-text">@ex.Message</span>
? ? ? ? }
? ? </div>
</div>
通過以下代碼獲得異常實例的引用。
IExceptionHandlerFeature exf = HttpContext.Features.Get<IExceptionHandlerFeature>();Exception ex = exf?.Error;這樣就可以在頁面上顯示異常的描述信息了。
?可能你又想到了,我不想輸出個頁面,我只想返回一些簡單的文本,那么,你在 Startup.Configure 中可以這樣寫。
app.UseExceptionHandler(x =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? x.Run(async context =>
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? var ex = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>()?.Error;
? ? ? ? ? ? ? ? ? ? string msg = ex == null ? "發生錯誤。" : ex.Message;
? ? ? ? ? ? ? ? ? ? context.Response.ContentType = "text/plain;charset=utf-8";
? ? ? ? ? ? ? ? ? ? await context.Response.WriteAsync(msg);
? ? ? ? ? ? ? ? });
? ? ? ? ? ? });
里面的變量 x 就是當前的?IApplicationBuilder ,與傳遞給 Configure 方法的 app 參數類型一樣,這時候我們可以用 Reponse 的方法返回自定義的文本。
?
?好了,今天的內容就介紹到這兒吧,其實異常處理還有一種方法——使用 Filter,這個咱們留到下一篇博文再和大伙分享。
上一篇中,老周給大伙伴們扯了有關 ASP.NET Core 中異常處理的簡單方法。按照老周的優良作風,我們應該順著這個思路繼續挖掘。
本文老周就不自量力地介紹一下如何使用 MVC Filter 來處理異常。MVC 模型(當然適用于 Razor Page 、Web API 模型)可以用一系列的 Filter 來對請求與回應消息進行過濾處理。其中,在?Microsoft.AspNetCore.Mvc.Filters 命名空間下,你會發現有兩個接口,它們跟異常處理有關:
IExceptionFilter:實現?OnException 方法,可以自定義回傳給客戶端的異常信息。
IAsyncExceptionFilter:跟上面的一樣的,只不過這廝支持異步等待而已。
?
在實現處理異常的 Filter 時,傳給?OnException /?OnExceptionAsync 方法的有一個?ExceptionContext 類型參數,我們可以通過它來設置自定義的返回結果。
訪問?Exception 屬性,你可以得到相關的異常實例,當然這個屬性是可寫的,所以你可以獲取異常實例后,將它改為其他異常實例,再重新賦給這個屬性,比如,你用你自己編寫的異常類來重新封裝。通過?Result 屬性設置返回結果,這個與 MVC Action 方法的返回方法一樣,不同的是,在 Action 方法中,你可以調用 Controller 基類的方法來返回對應的 Result ,而對于 Result 屬性,你必須顯式地去創建實現了 IActionResult 接口的類型實例。
另外,值得注意的是,ExceptionContext 類還有一個 ExceptionHandled 屬性,該屬性值可讀可寫,主要是用于標識當前發生的異常是否已經過處理。這主要是應對 Filter 的執行順序的,一種情況是你可能使用了多個 Filter 來處理異常,在處理過程中你就可以將這個屬性值設為 true 以表示這個錯誤已處理過了,后面的就不必處理了;另一種情況是,以 Attribute 方式使用的 Filter 的優先級會比全局使用的 Filter 高,也許在 Attribute 上我沒有對異常進行處理,那么到了全局 Filter 執行的時候,我就可以檢查一下這個屬性,如果沒有處理就進行一下處理。關于 Attribute 方式使用 Filter 老周隨后會說的,這里先提一下。
?
好了,咱們先說說如何實現自己的異常處理 Filter,其實很簡單,看下面代碼。
public class MyExceptionFilter : IExceptionFilter, IFilterMetadata
? ? {
? ? ? ? public void OnException(ExceptionContext context)
? ? ? ? {
? ? ? ? ? ? if(context.ExceptionHandled == false)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? string msg = context.Exception.Message;
? ? ? ? ? ? ? ? context.Result = new ContentResult
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Content = msg,
? ? ? ? ? ? ? ? ? ? StatusCode = StatusCodes.Status200OK,
? ? ? ? ? ? ? ? ? ? ContentType = "text/html;charset=utf-8"
? ? ? ? ? ? ? ? };
? ? ? ? ? ? }
? ? ? ? ? ? context.ExceptionHandled = true; //異常已處理了
? ? ? ? }
在?OnException 方法中,我直接獲取異常信息,然后用一個 ContentResult 對象來返回,這個是類似于 MVC 中 Controller . Action 方法返回結果,我這里簡單地以 HTML 文本形式返回,一旦處理到異常,應用程序會自動把這個 Result 返回給客戶端。
你可能發現了,我除了實現?IExceptionFilter 接口外,還實現了一個?IFilterMetadata 接口,這個接口是必須的,不然待會兒我們無法應用這個 Filter 了,為什么呢,等一下你就會明白了。
這里實現的這個是同步調用的,如果你希望有一個可異步等待的版本,那么,你就順便實現一下?IAsyncExceptionFilter 接口。把上面的代碼改為:
public class MyExceptionFilter : IExceptionFilter, IAsyncExceptionFilter, IFilterMetadata
? ? {
? ? ? ? public void OnException(ExceptionContext context)
? ? ? ? {
? ? ? ? ? ? if(context.ExceptionHandled == false)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? string msg = context.Exception.Message;
? ? ? ? ? ? ? ? context.Result = new ContentResult
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? Content = msg,
? ? ? ? ? ? ? ? ? ? StatusCode = StatusCodes.Status200OK,
? ? ? ? ? ? ? ? ? ? ContentType = "text/html;charset=utf-8"
? ? ? ? ? ? ? ? };
? ? ? ? ? ? }
? ? ? ? ? ? context.ExceptionHandled = true; //異常已處理了
? ? ? ? }
? ? ? ? public Task OnExceptionAsync(ExceptionContext context)
? ? ? ? {
? ? ? ? ? ? OnException(context);
? ? ? ? ? ? return Task.CompletedTask;
? ? ? ? }
? ? }
好了,接下來咱們得考慮怎么用它了。在 Startup.ConfigureServices 方法中,添加 MVC 功能后可以把咱們自己寫的 Filter 添加進去。
public void ConfigureServices(IServiceCollection services)
? ? ? ? {
? ? ? ? ? ? services.AddMvc(opt =>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? opt.Filters.Add<MyExceptionFilter>();
? ? ? ? ? ? });
? ? ? ? }
上面代碼添加 Filter 后,是用于全局的,說白了,當應用程序內不管哪個 Controller 里面發生的異常,都會經過咱們添加的 Filter 處理。
?
現在我們測試一下這個異常處理的 Filter 起到什么作用。為了不影響測試,請把 Configure 方法中這段代碼刪除。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
? ? ? ? {
? ? ? ? ? ? if (env.IsDevelopment())
? ? ? ? ? ? {
? ? ? ? ? ? ? ? app.UseDeveloperExceptionPage();
? ? ? ? ? ? }
? ? ? ? ? ? app.UseMvc();
? ? ? ? }
變成這樣
public void Configure(IApplicationBuilder app, IHostingEnvironment env){app.UseMvc();}?
然后,隨便弄段代碼來測試。
? ? ? ?[HttpPost("/code")]
? ? ? ? public IActionResult SubmitSome(int val)
? ? ? ? {
? ? ? ? ? ? if(val <= 0)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? throw new ArgumentException("號碼不能小于或等于 0。");
? ? ? ? ? ? }
? ? ? ? ? ? return Content($"恭喜你,中獎了。\n中獎號碼為:{val}", "text/html;charset=utf-8");
? ? ? ? }
這個邏輯很簡單,就是在前臺頁面輸入一個數值,然后 POST 上來,如果數值不是大于 0 的值就拋異常。
?
然后我故意輸入一個 -10。
?
?POST 后在服務器上引發異常。
繼續執行,讓 Filter 對異常進行處理。
最后,異常信息就返回給瀏覽器了。
?
?這樣說明咱們寫的 Filter 起作用了。
剛剛說過,在 ConfigureServices 方法中添加的 Filter 是用于全局的,如果我們的項目中有個別的 Controller 或者 Controller 中的個別方法,希望使用專門的 Filter 去處理異常,這時候就可以考慮以 Attribute 的方式去處理。
要用 Attribute 方式處理異常,需要實現?ExceptionFilterAttribute 抽象類。該抽象類已實現了咱們上面提到過的幾個接口。
這個類還實現了?IOrderedFilter 接口,可以用來安排多個 Attribute 實例在處理異常上的順序(假設你用了多個實例來處理)。
?
下面咱們自己實現一個 Attribute ,用來處理異常。
public class MyExceptionFilterAttribute : ExceptionFilterAttribute
? ? {
? ? ? ? public override void OnException(ExceptionContext context)
? ? ? ? {
? ? ? ? ? ? var ex = context.Exception;
? ? ? ? ? ? // 構建錯誤信息對象
? ? ? ? ? ? var dic = new Dictionary<string, object>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? ["err_code"] = 80250,
? ? ? ? ? ? ? ? ["err_msg"] = ex.Message,
? ? ? ? ? ? ? ? ["err_sol"] = "建議攜帶你的數據到醫院做檢查。"
? ? ? ? ? ? };
? ? ? ? ? ? // 設置結果
? ? ? ? ? ? context.Result = new JsonResult(dic);
? ? ? ? ? ? context.ExceptionHandled = true;
? ? ? ? }
? ? ? ? public override Task OnExceptionAsync(ExceptionContext context)
? ? ? ? {
? ? ? ? ? ? OnException(context);
? ? ? ? ? ? return Task.CompletedTask;
? ? ? ? }
? ? }
上面代碼中,我以 JSON 格式返回錯誤數據。
?
這個 Attribute 可以用于類與方法,然后咱們用 Web API 來測試。
[Route("api/[controller]")]
? ? public class DemoController : Controller
? ? {
? ? ? ? [HttpGet]
? ? ? ? [MyExceptionFilter]
? ? ? ? public IActionResult Compute(int m, int n)
? ? ? ? {
? ? ? ? ? ? if (m < 0 || n < 0)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? throw new Exception("數值不能小于 0。");
? ? ? ? ? ? }
? ? ? ? ? ? return Json(new { num1 = m, num2 = n, result = m + n });
? ? ? ? }
? ? }
此處把 attrbute 用到方法上。
?
運行應用程序,然后請出 Postman 大叔來幫我們測試 Web API。為參數 m 和 n 賦值,然后以 GET 方式發送請求。
獲得正確的結果,現在咱們提交小于 0 的參數。就會返回剛剛自定義的錯誤。
?
?好了,今天的內容就說到這里,下次有空繼續扯。
原文地址:http://www.cnblogs.com/tcjiaan/p/8461408.html?
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的【ASP.NET Core】处理异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: OIDC在 ASP.NET Core中的
- 下一篇: 树莓派3B上部署运行.net core