Web API 的 ExceptionFilterAttribute、IExceptionFilter 能幫我們處理應用程式等級的例外,但無法處理載體 (Host) 的例外,當使用 IIS 作為載體,假使發生未處理的例外時會出現黃頁,OWIN 作為載體時,ExceptionFilterAttribute、IExceptionFilter 卻攔截不到錯誤,此時應該改用 Microsoft.Owin.Diagnostics.IAppBuilder.UseErrorPage 擴充方法,接下來將會利用 Error Handler 這個情境,介紹 OWIN Middleware 的幾種使用方式。
有關 OWIN 的介紹可以參考以下
ASP.NET: Understanding OWIN, Katana, and the Middleware Pipeline
開發環境
- VS IDE 2019
- .NET Framework 4.8
新增 Console App 專案範本,安裝以下套件
Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Diagnostics
Install-Package Microsoft.Owin.Host.SystemWeb
定義 MyExceptionHandler 印出錯誤訊息
public class MyExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
Console.WriteLine(context.Exception.Message);
}
}
使用 app.Use Middleware 引發錯誤
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
// Web API configuration and services
config.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());
SwaggerConfig.Register(config);
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional}
);
app.Use((ctx, next) =>
{
var msg = "故意引發例外";
Console.WriteLine(msg);
throw new Exception(msg);
})
.UseWebApi(config);
}
}
啟動 OWIN
internal class Program
{
private const string HOST_ADDRESS = "http://localhost:9527";
private static void Main(string[] args)
{
var webApp = WebApp.Start<Startup>(HOST_ADDRESS);
Console.WriteLine($"伺服器已啟動, 位置:{HOST_ADDRESS}/swagger");
Console.WriteLine("按下任意建離開應用程式");
Console.ReadLine();
webApp.Dispose();
}
}
按 F5 執行站台,然後用瀏覽器訪問 http://localhost:9527,得到 HTTP ERROR 500 錯誤,主控台也沒有攔截到錯誤,服務不知道怎麼死掉的,這造成了除錯的難度,執行結果如下:
為了解決問題,再新增一個捕捉例外的 Middleware
完整代碼如下:
public class Startup
{
public void Configuration(IAppBuilder app)
{
...
app.Use(async (ctx, next) =>
{
try
{
await next();
}
catch (Exception ex)
{
this.ErrorHandle(ex, ctx);
}
})
.Use((ctx, next) =>
{
var msg = "故意引發例外";
Console.WriteLine(msg);
throw new Exception(msg);
})
.UseWebApi(config);
}
private void ErrorHandle(Exception ex, IOwinContext context)
{
//紀錄詳細訊息,完整的例外推疊 ex.ToString()
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ReasonPhrase = "Internal Server Error";
context.Response.ContentType = "application/json";
//回應前端,部份訊息,ex.Message
context.Response.Write(ex.Message);
}
}
攔截到 OWIN 的錯誤了,執行結果如下:
攔截例外的代碼擠在 Startup.cs 有點亂,可以把它抽成一個 Middleware 類別,他需要 Func<IDictionary<string, object>, Task> 物件,這個是用來收集、傳遞參數
using AppFunc = Func<IDictionary<string, object>, Task>;
public class ErrorHandler
{
private readonly AppFunc _next;
public ErrorHandler(AppFunc next)
{
if (next == null)
{
throw new ArgumentNullException("next");
}
this._next = next;
}
public async Task Invoke(IDictionary<string, object> environment)
{
try
{
await this._next(environment);
}
catch (Exception ex)
{
var owinContext = new OwinContext(environment);
this.ErrorHandle(ex, owinContext);
}
}
private void ErrorHandle(Exception ex, IOwinContext context)
{
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ReasonPhrase = "Internal Server Error";
context.Response.ContentType = "application/json";
context.Response.Write(ex.Message);
}
}
調用端 app.Use<ErrorHandler>()
public class Startup
{
public void Configuration(IAppBuilder app)
{
...
app.Use<ErrorHandler>()
.Use((ctx, next) =>
{
var msg = "故意引發例外";
Console.WriteLine(msg);
throw new Exception(msg);
})
.UseWebApi(config);
}
}
或者是實作 OwinMiddleware,看起來更精簡
public class ErrorHandlerOwinMiddleware : OwinMiddleware
{
public ErrorHandlerOwinMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
try
{
await this.Next.Invoke(context);
}
catch (Exception ex)
{
try
{
this.Handle(ex, context);
return;
}
catch (Exception)
{
// If there's a Exception while generating the error page, re-throw the original exception.
}
throw;
}
}
private void Handle(Exception ex, IOwinContext context)
{
//Build a model to represet the error for the client
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
context.Response.ReasonPhrase = "Internal Server Error";
context.Response.ContentType = "application/json";
context.Response.Write(JsonConvert.SerializeObject(ex));
}
}
利用捕捉 OWIN Host 的案例,介紹了三種使用 Middleware 的方法,相信應該知道怎麼用它了,最後,在分享一個,Microsoft.Owin.Diagnostics 裡面有 IAppBuilder.UseErrorPage 擴充方法可以使用
public class Startup
{
public void Configuration(IAppBuilder app)
{
...
app.UseErrorPage()
.UseWelcomePage("/")
.Use((ctx, next) =>
{
var msg = "故意引發例外";
Console.WriteLine(msg);
throw new Exception(msg);
})
.UseWebApi(config)
;
}
錯誤訊息相當的完整,執行結果如下:
範例位置
https://github.com/yaochangyu/sample.dotblog/tree/master/OWIN/Lab.OwinErrorHandler
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET