【ASP.NET Core】處理異常--轉


老周寫的【ASP.NET Core】處理異常非常的通俗易懂,拿來記錄下。

轉自老周:http://www.cnblogs.com/tcjiaan/p/8461408.html

今天咱們聊聊有關異常處理的破事吧,也可以說是錯誤處理,反正就這個意思,你理解就好,專業名詞不必較勁,只有那些吃飽了撐着的“學術人才”才會跟名詞較勁。

老辦法,咱們結合示例來講述,這樣各位觀眾不會乏味。

大家知道,娛樂產品腎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 的參數。就會返回剛剛自定義的錯誤。

 

 好了,今天的內容就說到這里,下次有空繼續扯。

示例源代碼下載地址


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM