C# 全域性異常捕獲(for .net Core)
序、背景
在16年,筆者曾在部落格裡寫了一篇《C# 全域性異常捕獲》的文章,裡面講了一下如何在整個專案中捕獲未處理的異常,只不過當時寫的時候還是以.net Framework和Asp.net為目標寫的。
然而這兩年裡整個.net的圈子發生了非常大的變化,比如16年剛釋出的還不溫不火的.net Core,終於在這兩年間熊熊的燃燒起來,現在去Nuget上面看,這兩年內更新過的專案,基本都提供了對.net Standard的支援,而曾經的.net Framework因為各種各樣的歷史包袱,開始顯得有些力不從心,甚至在今年.net Framework第一次將被.net Standard甩下——.net Core3.0將首先支援.net Standard 2.1,而.net Framework 4.8則還會在.net Standard 2.0上停留(可以看微軟的這篇部落格《 ofollow,noindex" aria-label="然而這兩年裡整個.net的圈子發生了非常大的變化,比如16年剛釋出的還不溫不火的.net Core,終於在這兩年間熊熊的燃燒起來,現在去Nuget上面看,這兩年內更新過的專案,基本都提供了對.net Standard的支援,而曾經的.net Framework因為各種各樣的歷史包袱,開始顯得有些力不從心,甚至在今年.netFramework第一次將被.netStandard甩下——.net Core3.0將首先支援.netStandard 2.1,而.netFramework 4.8則還會在.netStandard 2.0上停留(可以看微軟的這篇部落格《Announcing.NET Standard 2.1》)(在新視窗開啟)" href="https://blogs.msdn.microsoft.com/dotnet/2018/11/05/announcing-net-standard-2-1/" target="_blank">Announcing.NET Standard 2.1 》)
今天半夜準備睡覺的時候,收到了一位朋友的留言,希望筆者能補充一下在.net Core下,全域性異常捕獲的方式。

所以今天決定趕一下,講講在新的.net Core平臺下,如何進行全域性異常捕獲。
一、差異點
之前的文章裡,筆者一共講了五種方法去進行全域性異常捕獲,下面筆者先羅列一個表格,簡單標記一下之前部落格中的五個方法現在還能不能在.net Core中使用,和上一篇相比,有什麼差異的部分:
方法 | 差異 |
在Program.cs使用Try...Catch... | 還能使用 ,但依舊不能捕獲其他執行緒的異常 |
監聽Application.ThreadException事件 | 不能使用 ,目前.net Core仍不支援System.Windows.Forms名稱空間 |
監聽AppDomain.CurrentDomain.UnhandledException事件 | 可以使用,但有變動的地方。 |
Web專案Global.asax中編寫Application_Error | 不能使用 ,Asp.net Core取消了Global.asax |
Web專案使用IExceptionFilter攔截器攔截異常 | 可以,略有變動 |
二、在Program.cs使用Try...Catch...
這個方法和之前沒有任何的區別,還是那麼的挫…和雞肋,詳細的可以參考筆者之前的部落格,這裡就先略過了。
三、監聽Application.ThreadException事件
截止筆者寫這篇部落格的時候.net Core還是沒有提供System.Windows.Forms名稱空間,而先前的Application類是在System.Windows.Forms名稱空間下的,所以在.net Core下,是沒有辦法監聽Application.ThreadException事件的,之前的方法在.net Core下自然也就沒法用了。
不過據說 .net Core 3.0會引入WinForms和WPF ,就不知道那時候System.Windows.Forms名稱空間和Application類會不會回來了。
四、監聽AppDomain.CurrentDomain.UnhandledException事件
在最初的.net Core 1.x裡,AppDomain名稱空間因為需要執行時的支援,並且對此開銷不菲,所以微軟並沒有提供AppDomain的支援(參見微軟的這篇文件:《 .NET Core》),不過很多開發者並不接受這個理由,而且AppDomain中還有不少有用的類與方法——比如之前用的UnhandledException。所以社群裡關於這塊的討論一直沒有停止過(比如這個在corefx下的issue:《Supportglobal unhandled exception handler》)。(在新視窗開啟)" href="https://docs.microsoft.com/en-us/dotnet/core/porting/libraries#appdomains" target="_blank">Port.NET Framework libraries to .NET Core 》),不過很多開發者並不接受這個理由,而且AppDomain中還有不少有用的類與方法——比如之前用的UnhandledException。所以社群裡關於這塊的討論一直沒有停止過(比如這個在corefx下的issue:《 Supportglobal unhandled exception handler 》)。
(一)在.net Core 2.0及後續版本中
大概是聽到了開發者們的呼聲,微軟終於在.net Standard 2.0( 對應.net Core 2.0 版本)中,重新加入了System.AppDomain名稱空間。
不過在.net Standard 2.0中重新加入的System.AppDomain名稱空間,並不是一個完整的AppDomain。其中的很多類與方法並不能正常工作,會丟擲PlatformNotSupportedException異常(詳見微軟的這篇文件《 .NET Standard FAQ 》):

不過這些問題也不是特別的大,畢竟大夥朝思暮想的AppDomain.CurrentDomain.UnhandledException事件可以在.net Core 2.0及以後的版本上正常工作,只是和之前相比,事件的引數型別有些變化罷了。
比如下面的程式碼,我們在其他執行緒中丟擲一個異常:
using System; using System.Threading; class Program { static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Thread thread = new Thread(() => throw new Exception()); thread.Start(); Console.ReadKey(); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { //下面的程式碼使用了C# 7中新增的模式匹配語法 if (e.ExceptionObject is Exception ex) { Console.WriteLine($"捕獲到未處理異常:{ex.GetType()}"); Console.WriteLine($"異常資訊:{ex.Message}"); Console.WriteLine($"異常堆疊:{ex.StackTrace}"); Console.WriteLine($"CLR即將退出:{e.IsTerminating}"); } Console.ReadKey(); } }
執行一下檢視效果:

完美,基本上和之前沒有任何的差別。
(二)在.net Core 1.x版本中
如果您的專案正在使用.net Core 1.x的版本,且沒有任何想升級到.net Core 2.0及以上版本的想法,那是不是就沒有辦法了呢。
並不是。國外有位大神在閱讀CoreCLR原始碼後發現,在.net Core 1.x版本中,AppDomain其實也是存在的,只不過並沒有對外暴露給.net開發員們。
於是大神就寫了一個程式包( Expressions(在內部利用System.Reflection和System.Linq.Expressions這兩個名稱空間進行了大量的反射)”大概就是利用反射和表示式樹去訪問.netCore 1.x中隱藏的AppDomain。(在新視窗開啟)" href="https://github.com/SamuelEnglard/System.AppDomain" target="_blank">System.AppDomain )用於提供.net Core 1.x版本下的AppDomain功能實現,具體的實現原理麼,大神只是簡單的說了句
Internally A LOT of reflection is going on, makinguse of types in System.Reflection and System.Linq.Expressions
在內部利用System.Reflection和System.Linq.Expressions這兩個名稱空間進行了大量的反射大概就是利用反射和表示式樹去訪問.netCore 1.x中隱藏的AppDomain。
使用方法基本和上面一模一樣,只是首先需要去Nuget上安裝System.AppDomain:
在專案右鍵,單擊“管理Nuget程式包”,然後搜尋瀏覽“System.AppDomain”程式包,這個程式包的作者是Shmueli Englard,找到後安裝即可。

使用下面的程式碼測試效果:
using System; using System.Text; using System.Threading; class Program { static void Main(string[] args) { Console.OutputEncoding = Encoding.Unicode; //.net Core 1.x 防亂碼 AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; Thread thread = new Thread(() => throw new Exception()); thread.Start(); Console.ReadKey(); } private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is Exception ex) { Console.WriteLine($"捕獲到未處理異常:{ex.GetType()}"); Console.WriteLine($"異常資訊:{ex.Message}"); Console.WriteLine($"異常堆疊:{ex.StackTrace}"); Console.WriteLine($"CLR即將退出:{e.IsTerminating}"); } Console.ReadKey(); } }
執行之,還是一樣的效果:

不過這個程式包的作者在github上的專案介紹裡非常嚴肅的說最好不要去使用它,除非你沒得選。一個原因是.net Core可能會後期加入這個功能(確實後來加入了),第二個是因為它用了反射,所以執行速度並不是最快的,而且這樣就不適於UWP程式了。
所以在.net Core 1.x上,如果真的沒有什麼非常深刻的理由,還是建議升級到.net Core2.x,畢竟.net Standard 2.0相比前一個版本(.net Standard 1.6),增加了20000多個新API呢。
五、Web程式全域性異常捕獲
在.net Core的Web開發中,首先最大的變化是踢掉了Asp.net WebForm(大快人心哈哈哈),然後相比於Asp.net MVC,還有一個非常大的變化就是Asp.net Core去除了先前的Global.asax,並引入了Program.cs。
是的!Asp.net Core有Main函數了!這一改動使得Asp.net Core可以徹底的從IIS中獨立出來,可以獨立的啟動與執行。隨著Global.asax一塊離開的還有App_Start資料夾:先前Asp.net里路由的配置檔案就是放在這個資料夾裡的,在Asp.net Core中,使用Startup.cs替代了App_Start資料夾。
另外Asp.net Core預設自帶了依賴注入框架,建議開發者們使用依賴注入的設計原則開發專案。
因為去除了Global.asax,所以Asp.net中,在Global.asax中編寫Application_Error函式來攔截異常的方法是不行了。
不過我們依舊可以使用編寫Filters並註冊的方式去設定全域性異常捕獲,下面筆者將以Asp.net Core 2.1版本為例,為大家展示如何在Asp.net Core中新建自定義異常攔截器並註冊:
新建一個MyExceptionHandler類,並實現介面Microsoft.AspNetCore.Mvc.Filters.IExceptionFilter:
using System; using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; public class MyExceptionHandler : IExceptionFilter { public void OnException(ExceptionContext context) { Exception ex = context.Exception; //這裡可以寫一些日誌記錄的程式碼 context.Result = new ContentResult() { Content = $"捕捉到未處理的異常:{ex.GetType()}\nFilter已進行錯誤處理。", ContentType = "text/plain", StatusCode = (int)HttpStatusCode.InternalServerError }; context.ExceptionHandled = true;//設定異常已經處理 } }
攔截器這塊和之前略有變化,但相差不多,主要是返回輸出的地方和之前有點變化,感覺Asp.net Core的這個返回輸出的方式更人性化點。
建立完攔截器後,就要找Asp.net Core註冊了。之前我們是在App_Start目錄下的FilterConfig.cs裡註冊篩選器的,不過剛剛說了,在Asp.net Core裡,App_Start被替換成了Startup.cs,所以相應的,註冊篩選器的位置也挪到了Startup.cs裡。
在Startup.cs中找到ConfigureServices這個方法,如果這個方法中沒有services.AddMvc();這樣的程式碼,那麼在方法最後面補上:
services.AddMvc(options => { options.Filters.Add<MyExceptionHandler>(); })
如果已經有了,則視情況修改程式碼,按上面的程式碼,將剛剛新建的篩選器註冊到MVC中。比如筆者使用模版新建的Asp.net Core 2.1專案一開始就為筆者填上了這樣的程式碼:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
那就改成這樣:
services.AddMvc(options => { options.Filters.Add<MyExceptionHandler>(); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
好了,啟動一下專案看看吧,不過在此之前得先寫個有問題的控制器:
using System; using Microsoft.AspNetCore.Mvc; public class HomeController : Controller { public IActionResult Test() { throw new Exception(); } }
OK,開啟瀏覽器看看效果:

可以,完美。
六、寫在最後
隨著.net Core 2.0的推出,.net Core勢必將打敗.net Framework成為.net開發主流目標框架,希望各位.net開發者們齊心協力,讓.net Core在國內能夠被重視起來。
微軟早已不是當年的微軟,如果我們也不隨之改變,拿被這股潮流輕鬆甩下。
小柊
2018年12月17日 23:33:20