2021-10-16 23:40

C# MVC 的 UserException 攔截處理

為了將錯誤訊息呈現給使用者,要完成幾件事:

  • 用 Filter 攔截 UserException
  • 將錯誤訊息存到 TempData,之後呈現在畫面上
  • 回到原本的畫面,需要產生 ViewResult
  • 針對 Ajax 請求用 400 狀態回應,並將訊息放在 http content


.Net Framework MVC

  1. using System; 
  2. using System.Linq; 
  3. using System.Text.RegularExpressions; 
  4. using System.Web.Mvc; 
  5.  
  6. namespace XXXXX.Mvc.Filters 
  7. { 
  8.    /// <summary>例外訊息過濾器</summary> 
  9.    public class ExceptionMessageActionFilter : ActionFilterAttribute 
  10.    { 
  11.  
  12.        /// <summary></summary> 
  13.        public override void OnActionExecuting(ActionExecutingContext filterContext) 
  14.        { 
  15.            /* 自動將 Action 的 Parameters 中的 ViewModel 賦予給 ViewData 
  16.             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain; 
  17.             */ 
  18.  
  19.            string paramName = filterContext.ActionDescriptor.GetParameters() 
  20.                .OrderBy(x => Regex.IsMatch(x.ParameterType.ToString(), @"(ViewModel|Domain)\b") ? 0 : 1) 
  21.                .Select(x => x.ParameterName) 
  22.                .FirstOrDefault(); 
  23.  
  24.            if (paramName != null) 
  25.            { filterContext.Controller.ViewData.Model = filterContext.ActionParameters[paramName]; } 
  26.        } 
  27.  
  28.  
  29.        /// <summary></summary> 
  30.        public override void OnActionExecuted(ActionExecutedContext filterContext) 
  31.        { 
  32.            base.OnActionExecuted(filterContext); 
  33.  
  34.            /* 不處理子 Action */ 
  35.            if (filterContext.IsChildAction) { return; } 
  36.  
  37.            /* 判斷 Exception 是否已經被處理了 */ 
  38.            if (filterContext.ExceptionHandled) { return; } 
  39.  
  40.            /* 只針對 UserException 進行錯誤處理*/ 
  41.            var ex = filterContext.Exception as UserException; 
  42.            if (ex == null) { return; } 
  43.  
  44.  
  45.            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */ 
  46.            filterContext.ExceptionHandled = true; 
  47.  
  48.            if (filterContext.HttpContext.Request.IsAjaxRequest()) 
  49.            { 
  50.                /* 對 Ajax 請求的處理 */ 
  51.                filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; 
  52.                filterContext.HttpContext.Response.StatusCode = 400; 
  53.                filterContext.Result = new ContentResult { Content = ex.Message }; 
  54.            } 
  55.            else if (ex is JwNoDataException) 
  56.            { 
  57.                /* 資料不存在的處理 */ 
  58.                filterContext.Controller.TempData["StatusError"] = ex.Message; 
  59.                filterContext.Result = new HttpNotFoundResult("[" + ex.Message + "]"); 
  60.            } 
  61.            else 
  62.            { 
  63.                /* 一般畫面的處理 */ 
  64.                filterContext.Controller.TempData["StatusError"] = ex.Message; 
  65.                filterContext.Result = new ViewResult 
  66.                { 
  67.                    ViewData = filterContext.Controller.ViewData, 
  68.                    TempData = filterContext.Controller.TempData 
  69.                }; 
  70.            } 
  71.        } 
  72.  
  73.    } 
  74. } 

在 FilterConfig.cs 配置

  1. public class FilterConfig 
  2. { 
  3.    public static void RegisterGlobalFilters(GlobalFilterCollection filters) 
  4.    { 
  5.        //... 
  6.  
  7.        filters.Add(new ExceptionMessageActionFilter()); 
  8.  
  9.        //... 
  10.    } 
  11. } 


Net core 3.1 MVC

  1. using System; 
  2. using System.Linq; 
  3. using System.Text.RegularExpressions; 
  4. using Microsoft.AspNetCore.Mvc; 
  5. using Microsoft.AspNetCore.Mvc.Filters; 
  6.  
  7. namespace Orion.Mvc.Filters 
  8. { 
  9.    /// <summary>例外訊息過濾器</summary> 
  10.    public class ExceptionMessageActionFilter : ActionFilterAttribute 
  11.    { 
  12.  
  13.        /// <summary></summary> 
  14.        public override void OnActionExecuting(ActionExecutingContext filterContext) 
  15.        { 
  16.            /* 自動將 Action 的 Arguments 中的 ViewModel 賦予給 ViewData 
  17.             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain; 
  18.             */ 
  19.  
  20.            var arguments = filterContext.ActionArguments.Values.Where(x => x != null); 
  21.  
  22.            object model = arguments 
  23.                .OrderBy(x => Regex.IsMatch(x.GetType().Name, @"(ViewModel|Domain)\b") ? 0 : 1) 
  24.                .FirstOrDefault(); 
  25.  
  26.            var controller = filterContext.Controller as Controller; 
  27.            controller.ViewData.Model = model; 
  28.        } 
  29.  
  30.  
  31.        /// <summary></summary> 
  32.        public override void OnActionExecuted(ActionExecutedContext filterContext) 
  33.        { 
  34.            /* 判斷 Exception 是否已經被處理了 */ 
  35.            base.OnActionExecuted(filterContext); 
  36.            if (filterContext.ExceptionHandled) { return; } 
  37.  
  38.  
  39.            /* 只針對 UserException 進行錯誤處理*/ 
  40.            var ex = filterContext.Exception as UserException; 
  41.            if (ex == null) { return; } 
  42.  
  43.            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */ 
  44.            filterContext.ExceptionHandled = true; 
  45.  
  46.  
  47.            var controller = filterContext.Controller as Controller; 
  48.  
  49.            var headers = filterContext.HttpContext.Request.Headers; 
  50.            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest"; 
  51.  
  52.            if (isAjax) 
  53.            { 
  54.                /* 對 Ajax 請求的處理 */ 
  55.                filterContext.HttpContext.Response.StatusCode = 400; 
  56.                filterContext.Result = new ContentResult { StatusCode = 400, Content = ex.Message }; 
  57.            } 
  58.            else if (ex is UserNoDataException) 
  59.            { 
  60.                /* 資料不存在的處理 */ 
  61.                controller.TempData["StatusError"] = ex.Message; 
  62.                filterContext.HttpContext.Response.StatusCode = 404; 
  63.            } 
  64.            else 
  65.            { 
  66.                /* 一般畫面的處理 */ 
  67.                controller.TempData["StatusError"] = ex.Message; 
  68.                filterContext.Result = new ViewResult 
  69.                { 
  70.                    ViewData = controller.ViewData, 
  71.                    TempData = controller.TempData 
  72.                }; 
  73.            } 
  74.        } 
  75.  
  76.    } 
  77. } 

在 Startup.cs 配置

  1. public void ConfigureServices(IServiceCollection services) 
  2. { 
  3.    //... 
  4.  
  5.    IMvcBuilder mvcBuilder = services 
  6.        .AddMvc(options => 
  7.        { 
  8.            //... 
  9.            options.Filters.Add(new ExceptionMessageActionFilter()); 
  10.            //... 
  11.        }) 
  12.        .AddControllersAsServices() 
  13.        ; 
  14.  
  15.    //... 
  16. } 


Net core 3.1 Razor Page

  1. using System; 
  2. using System.Linq; 
  3. using Microsoft.AspNetCore.Mvc; 
  4. using Microsoft.AspNetCore.Mvc.Filters; 
  5. using Microsoft.AspNetCore.Mvc.RazorPages; 
  6.  
  7. namespace XXXXX.Api.Filters 
  8. { 
  9.  
  10.    public class ExceptionMessagePageFilter : AbstractPageFilter 
  11.    { 
  12.        public override void OnPageHandlerExecuted(PageHandlerExecutedContext context) 
  13.        { 
  14.            if (context.ExceptionHandled) { return; } 
  15.  
  16.            /* 判斷是否有指定的 Exception */ 
  17.            var ex = context.Exception; 
  18.            if (ex is UserException userEx) { handleUserException(context, userEx); return; } 
  19.            if (ex is HttpException httpEx) { handleHttpException(context, httpEx); return; } 
  20.        } 
  21.  
  22.  
  23.        private void handleUserException(PageHandlerExecutedContext context, UserException ex) 
  24.        { 
  25.            /* 只針對 PageModel 進行錯誤處理*/ 
  26.            var page = context.HandlerInstance as PageModel; 
  27.            if (page == null) { return; } 
  28.  
  29.            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */ 
  30.            context.ExceptionHandled = true; 
  31.  
  32.            var headers = filterContext.HttpContext.Request.Headers; 
  33.            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest"; 
  34.  
  35.            if (isAjax) 
  36.            { 
  37.                /* 對 Ajax 請求的處理 */ 
  38.                context.HttpContext.Response.StatusCode = 400; 
  39.                context.Result = new ContentResult 
  40.                { 
  41.                    StatusCode = 400, 
  42.                    Content = ex.Message 
  43.                }; 
  44.            } 
  45.            else if (ex is UserNoDataException) 
  46.            { 
  47.                /* 資料不存在的處理 */ 
  48.                page.TempData["StatusError"] = ex.Message; 
  49.                context.HttpContext.Response.StatusCode = 404; 
  50.            } 
  51.            else 
  52.            { 
  53.                /* 一般畫面的處理 */ 
  54.                page.TempData["StatusError"] = ex.Message; 
  55.                context.Result = page.Page(); 
  56.            } 
  57.        } 
  58.  
  59.  
  60.  
  61.        private void handleHttpException(PageHandlerExecutedContext context, HttpException ex) 
  62.        { 
  63.            context.ExceptionHandled = true; 
  64.  
  65.            var headers = context.HttpContext.Request.Headers; 
  66.            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest"; 
  67.  
  68.            if (isAjax) 
  69.            { 
  70.                context.HttpContext.Response.StatusCode = ex.StatusCode; 
  71.                context.Result = new ContentResult 
  72.                { 
  73.                    StatusCode = ex.StatusCode, 
  74.                    Content = ex.Message, 
  75.                }; 
  76.            } 
  77.            else 
  78.            { 
  79.                context.HttpContext.Response.StatusCode = ex.StatusCode; 
  80.                context.Result = new StatusCodeResult(ex.StatusCode); 
  81.            } 
  82.        } 
  83.    } 
  84. } 

在 Startup.cs 配置

  1. public void ConfigureServices(IServiceCollection services) 
  2. { 
  3.    //... 
  4.  
  5.    IMvcBuilder mvcBuilder = services 
  6.        .AddRazorPages(options => 
  7.        { 
  8.            //... 
  9.        }) 
  10.        .AddMvcOptions(options => 
  11.        { 
  12.            //... 
  13.            options.Filters.Add(new ExceptionMessagePageFilter()); 
  14.            //... 
  15.        }) 
  16.        ; 
  17.  
  18.    //... 
  19. } 


2021-10-16 23:09

用 Exception 作為使用者錯誤訊息的通道

自訂一個 UserException 來傳遞給使用者的訊息,然後程式裡只要簡單的 throw 訊息,接著就是在全域的錯誤處理中將訊息呈現給使用者。

這是一種非常方便的方式,因為可以無視 method 的階層深度,在程式的任何地方都可以將訊息傳到使用者面前。

原則上盡可能將錯誤轉化成 UserException,並且錯誤訊息要能讓使用者[明確知道]要如何正確操作軟體,但是只有工程師才可以修正的錯誤就應該記錄下來,並給使用者這樣的訊息[系統出現錯誤,請通知管理者],只有盡可能的修正錯誤才不會讓使用者常常打電話吵你。

2021-10-16 12:50

週期性執行的子執行緒範本

關於週期性執行的子執行緒有幾個要點:

  • 單次 Sleep 的時間不要太久,會影響程式的關閉
  • 不可以用 SpinWait 來做程式暫停,要用 Sleep 來暫停,這樣才會釋放 CPU
  • Error log 要注意會重複出現相同的 error
  • 有排除重複 error 時,要記得加上[錯誤解除]的 log,這樣才能知道程式是否有回到正常執行
  • 要執行的程式邏輯移到一個 method 裡,這樣可以避免 return 造成的迴圈中斷,也能分離邏輯的關注點


週期小於 10 秒的範本

  1. private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  2.  
  3. private int _cycleMSec = 1000; 
  4. private bool _runFlag = false; 
  5. private string _prevError; 
  6.  
  7. public void Start() 
  8. { 
  9.    if (_runFlag) { return; } 
  10.    _runFlag = true; 
  11.  
  12.    var thread = new Thread(() => 
  13.    { 
  14.        while (_runFlag) 
  15.        { 
  16.            /* 先睡可以避免在程式啟動時過於忙碌 */ 
  17.            Thread.Sleep(_cycleMSec); 
  18.  
  19.            try 
  20.            { 
  21.                /* 要執行的程式邏輯 */ 
  22.                cycleHandler(); 
  23.                if (_prevError != null) { _log.Info("錯誤結束"); } 
  24.                _prevError = null; 
  25.            } 
  26.            catch (Exception ex) 
  27.            { 
  28.                /* 避免相同的錯誤一直被記錄 */ 
  29.                if (_prevError == ex.Message) { continue; } 
  30.  
  31.                _prevError = ex.Message; 
  32.                _log.Fatal(ex, "執行錯誤"); 
  33.            } 
  34.        } 
  35.    }); 
  36.  
  37.    thread.Start(); 
  38. } 
  39.  
  40. public void Stop() 
  41. { 
  42.    _runFlag = false; 
  43. } 
  44.  
  45.  
  46. private void cycleHandler() 
  47. { 
  48.    // 主要的邏輯程式寫在這裡 
  49. } 


週期大於 10 秒的範本

  1. private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  2.  
  3. private int _cycleSec = 30; 
  4. private bool _runFlag = false; 
  5. private string _prevError; 
  6. private DateTime _nextTime = DateTime.Now; 
  7.  
  8. public void Start() 
  9. { 
  10.    if (_runFlag) { return; } 
  11.    _runFlag = true; 
  12.  
  13.    var thread = new Thread(() => 
  14.    { 
  15.        while (_runFlag) 
  16.        { 
  17.            /* 先睡可以避免在程式啟動時過於忙碌 */ 
  18.            Thread.Sleep(1000); 
  19.  
  20.            /* 檢查是否符合執行時間 */ 
  21.            if (_nextTime > DateTime.Now) { continue; } 
  22.  
  23.            /* 更新下一次的執行時間 */ 
  24.            _nextTime = DateTime.Now.AddSeconds(_cycleSec); 
  25.  
  26.            try 
  27.            { 
  28.                /* 要執行的程式邏輯 */ 
  29.                cycleHandler(); 
  30.                if (_prevError != null) { _log.Info("錯誤結束"); } 
  31.                _prevError = null; 
  32.            } 
  33.            catch (Exception ex) 
  34.            { 
  35.                /* 避免相同的錯誤一直被記錄 */ 
  36.                if (_prevError == ex.Message) { continue; } 
  37.  
  38.                _prevError = ex.Message; 
  39.                _log.Fatal(ex, "執行錯誤"); 
  40.            } 
  41.        } 
  42.    }); 
  43.  
  44.    thread.Start(); 
  45. } 
  46.  
  47. public void Stop() 
  48. { 
  49.    _runFlag = false; 
  50. } 
  51.  
  52.  
  53. private void cycleHandler() 
  54. { 
  55.    // 主要的邏輯程式寫在這裡 
  56. } 


停啟頻繁的範本

  1. public enum CycleStatus 
  2. { 
  3.    Stop, 
  4.    Start, 
  5.    Stoping, 
  6. } 
  7.  
  8. public CycleStatus RunStatus { get; private set; } = CycleStatus.Stop; 
  9.  
  10.  
  11. private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  12. private int _cycleMSec = 1000; 
  13. private string _prevError; 
  14.  
  15.  
  16. public void Start() 
  17. { 
  18.    if (RunStatus != CycleStatus.Stop) 
  19.    { throw new Exception("程序還在進行中"); } 
  20.  
  21.    RunStatus = CycleStatus.Start; 
  22.  
  23.    var thread = new Thread(() => 
  24.    { 
  25.        while (RunStatus == CycleStatus.Start) 
  26.        { 
  27.            /* 先睡可以避免在程式啟動時過於忙碌 */ 
  28.            Thread.Sleep(_cycleMSec); 
  29.  
  30.            try 
  31.            { 
  32.                /* 要執行的程式邏輯 */ 
  33.                cycleHandler(); 
  34.                if (_prevError != null) { _log.Info("錯誤結束"); } 
  35.                _prevError = null; 
  36.            } 
  37.            catch (Exception ex) 
  38.            { 
  39.                /* 避免相同的錯誤一直被記錄 */ 
  40.                if (_prevError == ex.Message) { continue; } 
  41.  
  42.                _prevError = ex.Message; 
  43.                _log.Fatal(ex, "執行錯誤"); 
  44.            } 
  45.        } 
  46.  
  47.        RunStatus = CycleStatus.Stop; 
  48.    }); 
  49.  
  50.    thread.Start(); 
  51. } 
  52.  
  53.  
  54. public void Stop() 
  55. { 
  56.    if (RunStatus == CycleStatus.Stop) 
  57.    { throw new Exception("程序已經停止"); } 
  58.  
  59.    if (RunStatus == CycleStatus.Stoping) 
  60.    { throw new Exception("程序正在停止"); } 
  61.  
  62.    RunStatus = CycleStatus.Stoping; 
  63. } 
  64.  
  65.  
  66. private void cycleHandler() 
  67. { 
  68.    // 主要的邏輯程式寫在這裡 
  69. } 

2021-10-16 11:51

重構基礎-邏輯反相

利用程式具有區段的能力,我們可以將 [如果 A 成立就做 B] 轉換成 [如果 A 不成立就離開],用這個方式做邏輯反相可以讓原本看似複雜的程式簡化很多,讓程式變得清晰思慮也會變的清晰,困難的問題就變得簡單,或著就不在是問題了。

這是一段重構前的程式:

  1. foreach (var item in list) 
  2. { 
  3.    Guid guid = new Guid(item.project_GUID.Value.ToString()); 
  4.    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid); 
  5.    if (project != null) 
  6.    { 
  7.        var mails = !String.IsNullOrEmpty(project.AlertEmail) ? project.AlertEmail.Split(new char[]{';'}) : null; 
  8.        if (mails != null) 
  9.        { 
  10.            if(!mails.Any(x => x.mail == "a@b.c")) 
  11.            { 
  12.                foreach (var mail in mails) 
  13.                { 
  14.                    if (!mailDic.ContainsKey(mail.Trim())) 
  15.                        mailDic.Add(mail, new List<int>()); 
  16.  
  17.                    mailDic[mail].Add(item.projectID); 
  18.                } 
  19.  
  20.            } 
  21.        } 
  22.    } 
  23. } 

將邏輯反相重構後的程式:

  1. foreach (var item in list) 
  2. { 
  3.    Guid guid = new Guid(item.project_GUID.Value.ToString()); 
  4.    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid); 
  5.    if (project == null) { continue; } 
  6.  
  7.    if (String.IsNullOrEmpty(project.AlertEmail)) { continue; } 
  8.  
  9.    var mails = project.AlertEmail.Split(new char[]{';'}); 
  10.    if(mails.Any(x => x.mail == "a@b.c")){ continue; } 
  11.  
  12.    foreach (var mail in mails) 
  13.    { 
  14.        if (!mailDic.ContainsKey(mail.Trim())) 
  15.        { mailDic.Add(mail, new List<int>()); } 
  16.  
  17.        mailDic[mail].Add(item.projectID); 
  18.    } 
  19. } 

可以看出程式的縮排階層減少了,也簡化了程式的複雜度,重構的方法就是將 if 反相將 else 前移,也許一開始很難用這種方式寫程式,但是人腦是可以訓練的,在每次重構時進行調整改寫,習慣這種模式後很自然就會用這種[跳離]的思維去寫程式邏輯了。

當然這種方式也是有缺點的,就是有時候不這麼直覺會不好理解,但是可以增加一些註解來輔助描述,或是拆分判斷條件例如:

  1. if (a != null || b == null) { return; } 
  2.  
  3. /* 這可以拆分成兩行,不用拘泥一行完成 */ 
  4.  
  5. if (a != null) { return; } 
  6. if (b == null) { return; } 

程式邏輯有一個有趣的事,往往用正向邏輯無法處理的問題,改用反相邏輯就會簡單很多,例如:需要用特定的邏輯尋找符合的項目,可能因為那個特定邏輯讓效能非常的差,這時候如果改用不符合特定邏輯的項目排除,留下來的就會是符合的項目,因為是用消去法所以要尋找的數量會越算越少,所以執行效能會收斂。

2021-10-15 17:26

全域的 Exception 處理

大部分具有 Exception 機制的程式語言都有提供全域的 Exception 處理,如果你已經有用 log 去記錄錯誤的習慣的話,與其在程式裡佈滿了 try catch,不如在全域處裡中去記錄沒有被 catch 的 Exception,這樣程式就可以更乾淨了,而且程式如果發生異常的關閉時也會進入全域處裡,可以確保 Exception 妥善地被記錄。


Console, WinForm 程式

  1. using System; 
  2. using System.Security.Permissions; 
  3. using System.Threading; 
  4. using System.Windows.Forms; 
  5. using NLog; 
  6.  
  7. namespace AppThreadException 
  8. { 
  9.    static class Program 
  10.    { 
  11.        private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  12.  
  13.  
  14.        [STAThread] 
  15.        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)] 
  16.        public static void Main(string[] args) 
  17.        { 
  18.            /* ThreadException 用來攔截 UI 錯誤 */ 
  19.            Application.ThreadException += threadExceptionHandler; 
  20.  
  21.            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */ 
  22.            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); 
  23.            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler; 
  24.  
  25.            Application.Run(new MyForm()); 
  26.        } 
  27.  
  28.  
  29.        /// <summary>攔截 UI 錯誤</summary> 
  30.        private static void threadExceptionHandler(object sender, ThreadExceptionEventArgs e) 
  31.        { 
  32.            _log.Fatal(e.Exception, "操作錯誤"); 
  33.            MessageBox.Show(e.Exception.Message, "操作錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop); 
  34.        } 
  35.  
  36.  
  37.        /// <summary>攔截不可挽回的錯誤,不能阻止程式關閉</summary> 
  38.        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) 
  39.        { 
  40.            Exception ex = (Exception)e.ExceptionObject; 
  41.  
  42.            _log.Fatal(ex, "執行錯誤"); 
  43.            MessageBox.Show(ex.Message, "執行錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop); 
  44.        } 
  45.    } 
  46. } 

Web 程式

在 Global.asax.cs 中可以設定 Application_Error 就可以攔截未處裡的 Exception,除了用這個方法記錄錯誤,還可以用現有的套件(elmah)幫我們完成。

  1. using System; 
  2. using System.Web.Mvc; 
  3. using System.Web.Optimization; 
  4. using System.Web.Routing; 
  5. using NLog; 
  6.  
  7. namespace MvcReport 
  8. { 
  9.    public class MvcApplication : System.Web.HttpApplication 
  10.    { 
  11.        private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  12.  
  13.        //... 
  14.  
  15.        protected void Application_Error(object sender, EventArgs e) 
  16.        { 
  17.            Exception ex = Server.GetLastError(); 
  18.            _log.Fatal(ex, ex.Message); 
  19.        } 
  20.  
  21.        //... 
  22.    } 
  23. } 

Net core 程式

Net core 的程式都是相同的方式,Web Request 的要另外配置

  1. using System; 
  2. using System.IO; 
  3. using System.Threading; 
  4. using System.Threading.Tasks; 
  5. using Autofac.Extensions.DependencyInjection; 
  6. using EverTop.Api; 
  7. using Microsoft.AspNetCore.Hosting; 
  8. using Microsoft.Extensions.Hosting; 
  9. using NLog; 
  10.  
  11.  
  12. namespace MyWebApp 
  13. { 
  14.    public class Program 
  15.    { 
  16.        private static readonly ILogger _log = LogManager.GetCurrentClassLogger(); 
  17.  
  18.  
  19.        public static void Main(string[] args) 
  20.        { 
  21.            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */ 
  22.            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler; 
  23.  
  24.            /* 用來攔截 Task 錯誤 */ 
  25.            TaskScheduler.UnobservedTaskException += unobservedTaskException; 
  26.  
  27.            var host = Host.CreateDefaultBuilder(args) 
  28.                .UseServiceProviderFactory(new AutofacServiceProviderFactory()) 
  29.                .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder 
  30.                    .UseStartup<Startup>() 
  31.                ) 
  32.                .Build(); 
  33.  
  34.            host.Run(); /* 啟動網站 */ 
  35.        } 
  36.  
  37.  
  38.        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) 
  39.        { 
  40.            Exception ex = (Exception)e.ExceptionObject; 
  41.            _log.Fatal(ex, "執行錯誤"); 
  42.        } 
  43.  
  44.        private static void unobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) 
  45.        { 
  46.            _log.Fatal(e.Exception, "執行錯誤"); 
  47.            e.SetObserved(); 
  48.        } 
  49.    } 
  50. } 

Net core 3.1 Web 程式

在 Startup.cs 中配置 middleware 進行 Exception 攔截

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
  2. { 
  3.    if (_env.IsProduction()) 
  4.    { 
  5.        app.UseExceptionHandler("/Error"); 
  6.    } 
  7.    else 
  8.    { 
  9.        app.UseDeveloperExceptionPage(); 
  10.    } 
  11.  
  12.    app.UseStatusCodePagesWithReExecute("/Error"); 
  13.  
  14.  
  15.    /* 利用 middleware 進行 Exception 攔截 */ 
  16.    /* 這裡的順序很重要,不然會被前面 ExceptionHandler 處理掉就拿不到 Exception */ 
  17.    app.Use(async (context, next) => 
  18.    { 
  19.        try 
  20.        { 
  21.            await next(); 
  22.        } 
  23.        catch (Exception ex) 
  24.        { 
  25.            _log.Fatal(ex, "執行錯誤"); 
  26.            throw; /* 把 Exception 再丟出去給別人處理 */ 
  27.        } 
  28.    }); 
  29. } 

週期性 Thread 的處裡方式

將主要邏輯寫在另外 method 裡,這樣可以專注在 Exception 上。

  1. private bool _runFlag = false; 
  2.  
  3. public void Start() 
  4. { 
  5.    if (_runFlag) { return; } 
  6.    _runFlag = true; 
  7.  
  8.    var thread = new Thread(() => 
  9.    { 
  10.        while (_runFlag) 
  11.        { 
  12.            try 
  13.            { 
  14.                cycleHandler(); 
  15.            } 
  16.            catch (Exception ex) 
  17.            { 
  18.                _log.Fatal(ex, "執行錯誤"); 
  19.            } 
  20.            Thread.Sleep(1000); 
  21.        } 
  22.    }); 
  23.  
  24.    thread.Start(); 
  25. } 
  26.  
  27. public void Stop() 
  28. { 
  29.    _runFlag = false; 
  30. } 
  31.  
  32. private void cycleHandler() 
  33. { 
  34.    // 主要的邏輯程式寫在這裡 
  35. } 

PHP 錯誤處理

Ref: set_error_handler, set_exception_handler

  1. <?php 
  2. function error_handler($errno, $errstr, $errfile, $errline) { 
  3.    if(error_reporting() === 0){ return; } 
  4.    throw new ErrorException($errstr, 0, $errno, $errfile, $errline); 
  5. } 
  6. set_error_handler('error_handler', E_ALL^E_NOTICE); 
  7.  
  8.  
  9. function exception_handler($exception) { 
  10.    // 在這記錄 log 
  11.    throw $exception; 
  12. } 
  13. set_exception_handler('exception_handler'); 
  14.  
  15.  
  16. throw new Exception('Uncaught Exception'); 
  17. //$a = 1 / 0; 
2021-10-15 11:27

讓 Net core 3.1 的 PageModel handler 可以用 AuthorizeAttribute

Razor Page 的 AuthorizeAttribute 只能用在 class 上,這樣就做不到細部的權限管控,為了讓 handler 也能用 AuthorizeAttribute 可以從 IPageFilter 進行處裡:

  1. using System.Linq; 
  2. using System.Reflection; 
  3. using System.Security.Claims; 
  4. using Microsoft.AspNetCore.Authorization; 
  5. using Microsoft.AspNetCore.Mvc; 
  6. using Microsoft.AspNetCore.Mvc.Filters; 
  7.  
  8. namespace XXXXX.Api.Filters 
  9. { 
  10.    public class HandlerAuthorizeFilter : IPageFilter 
  11.    { 
  12.        /// <summary>在完成模型系結之後,于處理常式方法執行之前呼叫。</summary> 
  13.        public void OnPageHandlerExecuting(PageHandlerExecutingContext context) 
  14.        { 
  15.            if(context.HandlerMethod == null) { return; } 
  16.  
  17.            /* 取得 handler 上的 AuthorizeAttribute */ 
  18.            var attr = context.HandlerMethod.MethodInfo 
  19.               .GetCustomAttribute<AuthorizeAttribute>(); 
  20.            if (attr == null) { return; } 
  21.  
  22.            /* 當前登入的使用者 */ 
  23.            ClaimsPrincipal user = context.HttpContext.User; 
  24.  
  25.            /* 檢查是否符合腳色權限 */ 
  26.            bool isAuth = attr.Roles.Split(',').Any(user.IsInRole); 
  27.  
  28.            /* 沒權限就給予 ForbidResult */ 
  29.            if (!isAuth) { context.Result = new ForbidResult(); } 
  30.        } 
  31.  
  32.  
  33.        /// <summary>在選取處理常式方法之後,但在進行模型系結之前呼叫。</summary> 
  34.        public void OnPageHandlerSelected(PageHandlerSelectedContext context) { } 
  35.  
  36.  
  37.        /// <summary>在處理常式方法執行之後,在動作結果執行之前呼叫。</summary> 
  38.        public void OnPageHandlerExecuted(PageHandlerExecutedContext context) { } 
  39.    } 
  40. } 

接著在 Startup.cs 進行過濾器配置:

  1. public void ConfigureServices(IServiceCollection services) 
  2. { 
  3.    //... 
  4.  
  5.    IMvcBuilder mvcBuilder = services 
  6.        .AddRazorPages(options => 
  7.        { 
  8.            //... 
  9.        }) 
  10.        .AddMvcOptions(options => 
  11.        { 
  12.            //... 
  13.            options.Filters.Add(new HandlerAuthorizeFilter()); 
  14.        }) 
  15.        ; 
  16.  
  17.    //... 
  18. } 

然後在 PageModel 就可以用下面的方式撰寫:

  1. public class IndexModel : PageModel 
  2. { 
  3.    [Authorize(Roles = "Admin")] 
  4.    public IActionResult OnGet() 
  5.    { 
  6.        return Page(); 
  7.    } 
  8. } 
2021-10-15 11:09

讓 Net core 3.1 的 PageModel 可以用 POST 的參數決定 handler

Razor Page 的 handler 預設只能從 URL 的 QueryString 參數 ?handler=XXXX 決定要去的 handler,這有點不方便,因為我會用 <button type="submit" name="handler" value="Delete"> 的方式去傳送 handler,所以需要用 POST 也能決定 handler。

為了做到這點可以在 Startup.cs 進行 middleware 配置:

  1. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
  2. { 
  3.    //... 
  4.  
  5.    /* 使用 FormData 給路由 handler */ 
  6.    app.Use((context, next) => 
  7.    { 
  8.        HttpRequest req = context.Request; 
  9.  
  10.        /* 判斷是否為 POST,且具有 handler 參數,然後覆蓋 Route 的值 */ 
  11.        if (req.HasFormContentType && req.Form.ContainsKey("handler")) 
  12.        { req.RouteValues["handler"] = req.Form["handler"]; } 
  13.  
  14.        return next(); 
  15.    }); 
  16.  
  17.    //... 
  18. } 
2021-10-15 10:34

讓 Net core 3.1 的 PageModel 可以 Properties Autowired

會考慮使用 Properties Autowired 是經過考慮跟評估的,畢竟 Properties Autowired 會造成負面的影響,就是 class 所相依的 class 會不清楚,很難像 Constructor 那樣清楚明白,但一般正常不會自己去建構 Controller 跟 PageModel,要建構的成本太大了,所以 Controller 跟 PageModel 很適合用 Properties Autowired。


為了做到 Properties Autowired 可以用 IPageFilter 進行處理:

  1. using System; 
  2. using System.Collections.Concurrent; 
  3. using System.Linq; 
  4. using System.Reflection; 
  5. using Microsoft.AspNetCore.Components; 
  6. using Microsoft.AspNetCore.Mvc.Filters; 
  7. using Microsoft.AspNetCore.Mvc.RazorPages; 
  8. using Microsoft.Extensions.DependencyInjection; 
  9.  
  10. namespace XXXXX.Api.Filters 
  11. { 
  12.    public class PageModelInjectFilter : IPageFilter 
  13.    { 
  14.        /// <summary>注入器的快取</summary> 
  15.        private readonly ConcurrentDictionary<Type, Action<PageModel, IServiceProvider>> _injecterCache = 
  16.            new ConcurrentDictionary<Type, Action<PageModel, IServiceProvider>>(); 
  17.  
  18.  
  19.        /// <summary>建構注入器</summary> 
  20.        private Action<PageModel, IServiceProvider> buildInjecter(Type type) 
  21.        { 
  22.            /* delegate 具有疊加的能力,先建構一個空的 delegate */ 
  23.            Action<PageModel, IServiceProvider> action = (page, provider) => { }; 
  24.  
  25.            /* 取得 Properties 且是可以寫入,並具有 [Inject] Attribute */ 
  26.            var props = type.GetProperties() 
  27.                .Where(p => p.CanWrite) 
  28.                .Where(p => p.IsDefined(typeof(InjectAttribute))); 
  29.  
  30.            foreach (var prop in props) 
  31.            { 
  32.                action += (page, provider) => 
  33.                { 
  34.                    /* 如果 Property 是已經有 Value 的就不要進行注入 */ 
  35.                    if (prop.GetValue(page) != null) { return; } 
  36.  
  37.                    /* 從 provider 取得依賴的物件 */ 
  38.                    object value = provider.GetRequiredService(prop.PropertyType); 
  39.                    prop.SetValue(page, value); 
  40.                }; 
  41.            } 
  42.  
  43.            return action; 
  44.        } 
  45.  
  46.  
  47.        /// <summary>在選取處理常式方法之後,但在進行模型系結之前呼叫。</summary> 
  48.        public void OnPageHandlerSelected(PageHandlerSelectedContext context) 
  49.        { 
  50.            /* 判斷是否是 PageModel */ 
  51.            var page = context.HandlerInstance as PageModel; 
  52.            if (page == null) { return; } 
  53.  
  54.            /* 取得或建構注入器 */ 
  55.            Action<PageModel, IServiceProvider> injecter =  
  56.               _injecterCache.GetOrAdd(page.GetType(), buildInjecter); 
  57.  
  58.            /* 進行注入 */ 
  59.            injecter(page, context.HttpContext.RequestServices); 
  60.        } 
  61.  
  62.  
  63.        /// <summary>在完成模型系結之後,于處理常式方法執行之前呼叫。</summary> 
  64.        public void OnPageHandlerExecuting(PageHandlerExecutingContext context) { } 
  65.  
  66.        /// <summary>在處理常式方法執行之後,在動作結果執行之前呼叫。</summary> 
  67.        public void OnPageHandlerExecuted(PageHandlerExecutedContext context) { } 
  68.    } 
  69. } 

接著在 Startup.cs 進行過濾器配置

  1. public void ConfigureServices(IServiceCollection services) 
  2. { 
  3.    //... 
  4.  
  5.    IMvcBuilder mvcBuilder = services 
  6.        .AddRazorPages(options => 
  7.        { 
  8.            //... 
  9.        }) 
  10.        .AddMvcOptions(options => 
  11.        { 
  12.            //... 
  13.            options.Filters.Add(new PageModelInjectFilter()); 
  14.        }) 
  15.        ; 
  16.  
  17.    //... 
  18. } 

然後在 PageModel 就可以用下面的方式撰寫:

  1. public class IndexModel : PageModel 
  2. { 
  3.    [Inject] public IMenuProvider MenuProvider { private get; set; } 
  4.  
  5.    public IActionResult OnGet() 
  6.    { 
  7.        return Page(); 
  8.    } 
  9. } 

2021-10-14 17:21

Exception 是傳遞訊息的通道

  1. void show(string tag) 
  2. { 
  3.    Console.WriteLine(tag); 
  4. } 
  5.  
  6. void methodA() 
  7. { 
  8.    throw new Exception("error msg"); 
  9.    show("A"); 
  10. } 
  11.  
  12. void methodB() 
  13. { 
  14.    methodA(); 
  15.    show("B"); 
  16. } 
  17.  
  18. void methodC() 
  19. { 
  20.    try 
  21.    { 
  22.        methodB(); 
  23.        show("C"); 
  24.    } 
  25.    catch (Exception ex) 
  26.    { 
  27.        Console.WriteLine(ex.Message); 
  28.    } 
  29. } 

這裡為了方便我們理解將程式展開成下面的樣子:

  1. void show(string tag) 
  2. { 
  3.    Console.WriteLine(tag); 
  4. } 
  5.  
  6. void methodC() 
  7. { 
  8.    try 
  9.    { 
  10.        methodB(); 
  11.            void methodB() 
  12.            { 
  13.                methodA(); 
  14.                    void methodA() 
  15.                    { 
  16.                        throw new Exception("error msg"); 
  17.    //----------------------------------------------------- 
  18.                        show("A"); 
  19.                    } 
  20.                show("B"); 
  21.            } 
  22.        show("C"); 
  23.    } 
  24.    catch (Exception ex) 
  25.    { 
  26.        Console.WriteLine(ex.Message); 
  27.    } 
  28. } 

從範例中我們可以看出兩個特性:


* 通透性

從 throw 的那一行開始 Exception 會一直向上傳遞,直到 catch 的區段。


* 脫離性

從 throw 的那一行之後的程式都不會被執行,會有類似 return 的效果,不同的是會對每一層的 method 都進行脫離,包含 try 的區段也會脫離。


methodC 的 catch 會捕獲從 methodA 丟出的 Exception,並且 Exception 中的 StackTrace 會紀錄 methodA, methodB, methodC,而 show() 都不會被執行。

由於大部分的程式邏輯都可以轉化成 else 就離開的程式流程,正好符合 throw 的脫離性,所以我們一開始在撰寫程式邏輯的時候,可以先專注在一般正常的流程邏輯上,之後再去補足檢查邏輯或脫離邏輯,最後在合適的位置進行 try catch 處理,或者在全域的錯誤處理中進行處理。


這裡有一個糟糕的 method,他用了個 Result 物件來裝載回傳結果以及錯誤訊息:

  1. public class Result 
  2. { 
  3.    public int Value { get; set; } 
  4.    public string Error { get; set; } 
  5. } 
  6.  
  7. public Result BadMethod(int input) 
  8. { 
  9.    var result = new Result(); 
  10.  
  11.    if (input > 0) 
  12.    { 
  13.        result.Value = 100 / input; 
  14.    } 
  15.    else 
  16.    { 
  17.        result.Error = "input 需要大於零"; 
  18.    } 
  19.    return result; 
  20. } 

如果用 Exception 可以讓程式簡單很多:

  1. public int GoodMethod(int input) 
  2. { 
  3.    if (input <= 0) 
  4.    { 
  5.        throw new Exception("input 需要大於零"); 
  6.    } 
  7.  
  8.    return 100 / input; 
  9. } 
2021-10-13 16:45

初學 Exception 的疑問

一開始學習 Exception 一定會有很多疑問,或者根本沒搞清楚就寫了一堆 try catch,讓程式邏輯變得亂七八糟難以閱讀,甚至小小的修改都不知道從何下手,Exception 是一種邏輯的輔助工具,可以讓你事半功倍,也可以變成噩夢。

我也是摸索了很久才對 Exception 有一定的應用認識,不敢說有多專精,畢竟每種語言跟特性都不同,但也是一定程度的應用經驗,我認為最重要的用途有兩個:


* 用 finally 去關閉資源物件

資源物件有 File, Socket,...,這類的物件在邏輯結束後一定要進行關閉,強調一定一定,實在是吃了自己埋的雷了,沒有關閉的話資源會被咬住,像 Serial Port 這種底層資源沒有關閉的話,想要重新連接就只能重開機了,這是讓人無法接受的。

  1. FileStream file1 = null; 
  2. try 
  3. { 
  4.    file1 = File.Open("<file path>", FileMode.OpenOrCreate); 
  5.    // Do something 
  6. } 
  7. finally 
  8. { 
  9.    if (file1 != null) { file1.Close(); } 
  10. } 
  11.  
  12. /*== C# 可以用 using 代替上面的程式 ==*/ 
  13. using (FileStream file2 = File.Open("<file path>", FileMode.OpenOrCreate)) 
  14. { 
  15.    // Do something 
  16. } 


* 用 catch 去記錄 Exception

為了日後可以在日後除錯時留下線索,Exception 的記錄是很重要的,任何程式都有可能出現錯誤,不管是資料錯誤還是邏輯錯誤,有除錯的線索肯定會有很大的幫助,無論程式的大小都應該留下錯誤紀錄。

  1. try 
  2. { 
  3.    // Do something 
  4. } 
  5. catch (Expression ex) 
  6. { 
  7.    _log.Error(ex, "An error occurred."); 
  8. } 

其餘的清況,在不知道為何要用 try catch 時,最好就不要寫 try catch,基本上程式邏輯一旦出錯就只能退出,如果胡亂地進行修正讓邏輯繼續下去,可能會出現很糟糕的局面,而且資料錯誤就應該修正資料,邏輯錯誤就應該修正邏輯。

錯誤就是要讓人發現才有辦法修正,胡亂的將錯誤屏蔽掉只會埋下未來的苦果,也別用這種方式報復前東家,這只會搞到後面接手的倒楣鬼,本是同根生,相煎何太急。