2021-10-16 23:40

C# MVC 的 UserException 攔截處理

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

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


.Net Framework MVC

using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;

namespace XXXXX.Mvc.Filters
{
    /// <summary>例外訊息過濾器</summary>
    public class ExceptionMessageActionFilter : ActionFilterAttribute
    {

        /// <summary></summary>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            /* 自動將 Action 的 Parameters 中的 ViewModel 賦予給 ViewData
             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain;
             */

            string paramName = filterContext.ActionDescriptor.GetParameters()
                .OrderBy(x => Regex.IsMatch(x.ParameterType.ToString(), @"(ViewModel|Domain)\b") ? 0 : 1)
                .Select(x => x.ParameterName)
                .FirstOrDefault();

            if (paramName != null)
            { filterContext.Controller.ViewData.Model = filterContext.ActionParameters[paramName]; }
        }


        /// <summary></summary>
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);

            /* 不處理子 Action */
            if (filterContext.IsChildAction) { return; }

            /* 判斷 Exception 是否已經被處理了 */
            if (filterContext.ExceptionHandled) { return; }

            /* 只針對 UserException 進行錯誤處理*/
            var ex = filterContext.Exception as UserException;
            if (ex == null) { return; }


            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            filterContext.ExceptionHandled = true;

            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                /* 對 Ajax 請求的處理 */
                filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
                filterContext.HttpContext.Response.StatusCode = 400;
                filterContext.Result = new ContentResult { Content = ex.Message };
            }
            else if (ex is JwNoDataException)
            {
                /* 資料不存在的處理 */
                filterContext.Controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new HttpNotFoundResult("[" + ex.Message + "]");
            }
            else
            {
                /* 一般畫面的處理 */
                filterContext.Controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new ViewResult
                {
                    ViewData = filterContext.Controller.ViewData,
                    TempData = filterContext.Controller.TempData
                };
            }
        }

    }
}

在 FilterConfig.cs 配置

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        //...

        filters.Add(new ExceptionMessageActionFilter());

        //...
    }
}


Net core 3.1 MVC

using System;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Orion.Mvc.Filters
{
    /// <summary>例外訊息過濾器</summary>
    public class ExceptionMessageActionFilter : ActionFilterAttribute
    {

        /// <summary></summary>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            /* 自動將 Action 的 Arguments 中的 ViewModel 賦予給 ViewData
             * 不然要自己在 Action 的第一行寫 ViewData.Model = domain;
             */

            var arguments = filterContext.ActionArguments.Values.Where(x => x != null);

            object model = arguments
                .OrderBy(x => Regex.IsMatch(x.GetType().Name, @"(ViewModel|Domain)\b") ? 0 : 1)
                .FirstOrDefault();

            var controller = filterContext.Controller as Controller;
            controller.ViewData.Model = model;
        }


        /// <summary></summary>
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            /* 判斷 Exception 是否已經被處理了 */
            base.OnActionExecuted(filterContext);
            if (filterContext.ExceptionHandled) { return; }


            /* 只針對 UserException 進行錯誤處理*/
            var ex = filterContext.Exception as UserException;
            if (ex == null) { return; }

            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            filterContext.ExceptionHandled = true;


            var controller = filterContext.Controller as Controller;

            var headers = filterContext.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                /* 對 Ajax 請求的處理 */
                filterContext.HttpContext.Response.StatusCode = 400;
                filterContext.Result = new ContentResult { StatusCode = 400, Content = ex.Message };
            }
            else if (ex is UserNoDataException)
            {
                /* 資料不存在的處理 */
                controller.TempData["StatusError"] = ex.Message;
                filterContext.HttpContext.Response.StatusCode = 404;
            }
            else
            {
                /* 一般畫面的處理 */
                controller.TempData["StatusError"] = ex.Message;
                filterContext.Result = new ViewResult
                {
                    ViewData = controller.ViewData,
                    TempData = controller.TempData
                };
            }
        }

    }
}

在 Startup.cs 配置

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddMvc(options =>
        {
            //...
            options.Filters.Add(new ExceptionMessageActionFilter());
            //...
        })
        .AddControllersAsServices()
        ;

    //...
}


Net core 3.1 Razor Page

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace XXXXX.Api.Filters
{

    public class ExceptionMessagePageFilter : AbstractPageFilter
    {
        public override void OnPageHandlerExecuted(PageHandlerExecutedContext context)
        {
            if (context.ExceptionHandled) { return; }

            /* 判斷是否有指定的 Exception */
            var ex = context.Exception;
            if (ex is UserException userEx) { handleUserException(context, userEx); return; }
            if (ex is HttpException httpEx) { handleHttpException(context, httpEx); return; }
        }


        private void handleUserException(PageHandlerExecutedContext context, UserException ex)
        {
            /* 只針對 PageModel 進行錯誤處理*/
            var page = context.HandlerInstance as PageModel;
            if (page == null) { return; }

            /* 標記 Exception 已經被處理了,讓後續的 Filter 不用再處理 */
            context.ExceptionHandled = true;

            var headers = filterContext.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                /* 對 Ajax 請求的處理 */
                context.HttpContext.Response.StatusCode = 400;
                context.Result = new ContentResult
                {
                    StatusCode = 400,
                    Content = ex.Message
                };
            }
            else if (ex is UserNoDataException)
            {
                /* 資料不存在的處理 */
                page.TempData["StatusError"] = ex.Message;
                context.HttpContext.Response.StatusCode = 404;
            }
            else
            {
                /* 一般畫面的處理 */
                page.TempData["StatusError"] = ex.Message;
                context.Result = page.Page();
            }
        }



        private void handleHttpException(PageHandlerExecutedContext context, HttpException ex)
        {
            context.ExceptionHandled = true;
            
            var headers = context.HttpContext.Request.Headers;
            bool isAjax = headers["X-Requested-With"] == "XMLHttpRequest";

            if (isAjax)
            {
                context.HttpContext.Response.StatusCode = ex.StatusCode;
                context.Result = new ContentResult
                {
                    StatusCode = ex.StatusCode,
                    Content = ex.Message,
                };
            }
            else
            {
                context.HttpContext.Response.StatusCode = ex.StatusCode;
                context.Result = new StatusCodeResult(ex.StatusCode);
            }
        }
    }
}

在 Startup.cs 配置

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddRazorPages(options =>
        {
            //...
        })
        .AddMvcOptions(options =>
        {
            //...
            options.Filters.Add(new ExceptionMessagePageFilter());
            //...
        })
        ;

    //...
}


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 秒的範本

private static readonly ILogger _log = LogManager.GetCurrentClassLogger();

private int _cycleMSec = 1000;
private bool _runFlag = false;
private string _prevError;

public void Start()
{
    if (_runFlag) { return; }
    _runFlag = true;

    var thread = new Thread(() =>
    {
        while (_runFlag)
        {
            /* 先睡可以避免在程式啟動時過於忙碌 */
            Thread.Sleep(_cycleMSec);

            try
            {
                /* 要執行的程式邏輯 */
                cycleHandler();
                if (_prevError != null) { _log.Info("錯誤結束"); }
                _prevError = null;
            }
            catch (Exception ex)
            {
                /* 避免相同的錯誤一直被記錄 */
                if (_prevError == ex.Message) { continue; }

                _prevError = ex.Message;
                _log.Fatal(ex, "執行錯誤");
            }
        }
    });

    thread.Start();
}

public void Stop()
{
    _runFlag = false;
}


private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}


週期大於 10 秒的範本

private static readonly ILogger _log = LogManager.GetCurrentClassLogger();

private int _cycleSec = 30;
private bool _runFlag = false;
private string _prevError;
private DateTime _nextTime = DateTime.Now;

public void Start()
{
    if (_runFlag) { return; }
    _runFlag = true;

    var thread = new Thread(() =>
    {
        while (_runFlag)
        {
            /* 先睡可以避免在程式啟動時過於忙碌 */
            Thread.Sleep(1000);

            /* 檢查是否符合執行時間 */
            if (_nextTime > DateTime.Now) { continue; }

            /* 更新下一次的執行時間 */
            _nextTime = DateTime.Now.AddSeconds(_cycleSec);

            try
            {
                /* 要執行的程式邏輯 */
                cycleHandler();
                if (_prevError != null) { _log.Info("錯誤結束"); }
                _prevError = null;
            }
            catch (Exception ex)
            {
                /* 避免相同的錯誤一直被記錄 */
                if (_prevError == ex.Message) { continue; }

                _prevError = ex.Message;
                _log.Fatal(ex, "執行錯誤");
            }
        }
    });

    thread.Start();
}

public void Stop()
{
    _runFlag = false;
}


private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}


停啟頻繁的範本

public enum CycleStatus
{
    Stop,
    Start,
    Stoping,
}

public CycleStatus RunStatus { get; private set; } = CycleStatus.Stop;


private static readonly ILogger _log = LogManager.GetCurrentClassLogger();
private int _cycleMSec = 1000;
private string _prevError;


public void Start()
{
    if (RunStatus != CycleStatus.Stop)
    { throw new Exception("程序還在進行中"); }

    RunStatus = CycleStatus.Start;

    var thread = new Thread(() =>
    {
        while (RunStatus == CycleStatus.Start)
        {
            /* 先睡可以避免在程式啟動時過於忙碌 */
            Thread.Sleep(_cycleMSec);

            try
            {
                /* 要執行的程式邏輯 */
                cycleHandler();
                if (_prevError != null) { _log.Info("錯誤結束"); }
                _prevError = null;
            }
            catch (Exception ex)
            {
                /* 避免相同的錯誤一直被記錄 */
                if (_prevError == ex.Message) { continue; }

                _prevError = ex.Message;
                _log.Fatal(ex, "執行錯誤");
            }
        }

        RunStatus = CycleStatus.Stop;
    });

    thread.Start();
}


public void Stop()
{
    if (RunStatus == CycleStatus.Stop)
    { throw new Exception("程序已經停止"); }

    if (RunStatus == CycleStatus.Stoping)
    { throw new Exception("程序正在停止"); }

    RunStatus = CycleStatus.Stoping;
}


private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}

2021-10-16 11:51

重構基礎-邏輯反相

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

這是一段重構前的程式:

foreach (var item in list)
{
    Guid guid = new Guid(item.project_GUID.Value.ToString());
    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid);
    if (project != null)
    {
        var mails = !String.IsNullOrEmpty(project.AlertEmail) ? project.AlertEmail.Split(new char[]{';'}) : null;
        if (mails != null)
        {
            if(!mails.Any(x => x.mail == "a@b.c"))
            {
                foreach (var mail in mails)
                {
                    if (!mailDic.ContainsKey(mail.Trim()))
                        mailDic.Add(mail, new List<int>());

                    mailDic[mail].Add(item.projectID);
                }

            }
        }
    }
}

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

foreach (var item in list)
{
    Guid guid = new Guid(item.project_GUID.Value.ToString());
    var project = _dc.Project.FirstOrDefault(w => w.ProjectID == guid);
    if (project == null) { continue; }

    if (String.IsNullOrEmpty(project.AlertEmail)) { continue; }

    var mails = project.AlertEmail.Split(new char[]{';'});
    if(mails.Any(x => x.mail == "a@b.c")){ continue; }

    foreach (var mail in mails)
    {
        if (!mailDic.ContainsKey(mail.Trim()))
        { mailDic.Add(mail, new List<int>()); }

        mailDic[mail].Add(item.projectID);
    }
}

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

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

if (a != null || b == null) { return; }

/* 這可以拆分成兩行,不用拘泥一行完成 */

if (a != null) { return; }
if (b == null) { return; }

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

2021-10-15 17:26

全域的 Exception 處理

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


Console, WinForm 程式

using System;
using System.Security.Permissions;
using System.Threading;
using System.Windows.Forms;
using NLog;

namespace AppThreadException
{
    static class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        [STAThread]
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
        public static void Main(string[] args)
        {
            /* ThreadException 用來攔截 UI 錯誤 */
            Application.ThreadException += threadExceptionHandler;

            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            Application.Run(new MyForm());
        }
        

        /// <summary>攔截 UI 錯誤</summary>
        private static void threadExceptionHandler(object sender, ThreadExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "操作錯誤");
            MessageBox.Show(e.Exception.Message, "操作錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
        

        /// <summary>攔截不可挽回的錯誤,不能阻止程式關閉</summary>
        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;

            _log.Fatal(ex, "執行錯誤");
            MessageBox.Show(ex.Message, "執行錯誤", MessageBoxButtons.OK, MessageBoxIcon.Stop);
        }
    }
}

Web 程式

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

using System;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using NLog;

namespace MvcReport
{
    public class MvcApplication : System.Web.HttpApplication
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();

        //...

        protected void Application_Error(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();
            _log.Fatal(ex, ex.Message);
        }

        //...
    }
}

Net core 程式

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

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Autofac.Extensions.DependencyInjection;
using EverTop.Api;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NLog;


namespace MyWebApp
{
    public class Program
    {
        private static readonly ILogger _log = LogManager.GetCurrentClassLogger();


        public static void Main(string[] args)
        {
            /* UnhandledException 只能攔截錯誤,不能阻止程式關閉 */
            AppDomain.CurrentDomain.UnhandledException += unhandledExceptionHandler;

            /* 用來攔截 Task 錯誤 */
            TaskScheduler.UnobservedTaskException += unobservedTaskException;

            var host = Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder
                    .UseStartup<Startup>()
                )
                .Build();

            host.Run(); /* 啟動網站 */
        }


        private static void unhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
        {
            Exception ex = (Exception)e.ExceptionObject;
            _log.Fatal(ex, "執行錯誤");
        }

        private static void unobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            _log.Fatal(e.Exception, "執行錯誤");
            e.SetObserved();
        }
    }
}

Net core 3.1 Web 程式

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

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (_env.IsProduction())
    {
        app.UseExceptionHandler("/Error");
    }
    else
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStatusCodePagesWithReExecute("/Error");


    /* 利用 middleware 進行 Exception 攔截 */
    /* 這裡的順序很重要,不然會被前面 ExceptionHandler 處理掉就拿不到 Exception */
    app.Use(async (context, next) =>
    {
        try
        {
            await next();
        }
        catch (Exception ex)
        {
            _log.Fatal(ex, "執行錯誤");
            throw; /* 把 Exception 再丟出去給別人處理 */
        }
    });
}

週期性 Thread 的處裡方式

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

private bool _runFlag = false;

public void Start()
{
    if (_runFlag) { return; }
    _runFlag = true;

    var thread = new Thread(() =>
    {
        while (_runFlag)
        {
            try
            {
                cycleHandler();
            }
            catch (Exception ex)
            {
                _log.Fatal(ex, "執行錯誤");
            }
            Thread.Sleep(1000);
        }
    });

    thread.Start();
}

public void Stop()
{
    _runFlag = false;
}

private void cycleHandler()
{
    // 主要的邏輯程式寫在這裡
}

PHP 錯誤處理

Ref: set_error_handler, set_exception_handler

<?php
function error_handler($errno, $errstr, $errfile, $errline) {
    if(error_reporting() === 0){ return; }
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler('error_handler', E_ALL^E_NOTICE);


function exception_handler($exception) {
    // 在這記錄 log
    throw $exception;
}
set_exception_handler('exception_handler');


throw new Exception('Uncaught Exception');
//$a = 1 / 0;

2021-10-15 11:27

讓 Net core 3.1 的 PageModel handler 可以用 AuthorizeAttribute

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

using System.Linq;
using System.Reflection;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace XXXXX.Api.Filters
{
    public class HandlerAuthorizeFilter : IPageFilter
    {
        /// <summary>在完成模型系結之後,于處理常式方法執行之前呼叫。</summary>
        public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
        {
            if(context.HandlerMethod == null) { return; }

            /* 取得 handler 上的 AuthorizeAttribute */
            var attr = context.HandlerMethod.MethodInfo
               .GetCustomAttribute<AuthorizeAttribute>();
            if (attr == null) { return; }

            /* 當前登入的使用者 */
            ClaimsPrincipal user = context.HttpContext.User;

            /* 檢查是否符合腳色權限 */
            bool isAuth = attr.Roles.Split(',').Any(user.IsInRole);

            /* 沒權限就給予 ForbidResult */
            if (!isAuth) { context.Result = new ForbidResult(); }
        }


        /// <summary>在選取處理常式方法之後,但在進行模型系結之前呼叫。</summary>
        public void OnPageHandlerSelected(PageHandlerSelectedContext context) { }


        /// <summary>在處理常式方法執行之後,在動作結果執行之前呼叫。</summary>
        public void OnPageHandlerExecuted(PageHandlerExecutedContext context) { }
    }
}

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

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddRazorPages(options =>
        {
            //...
        })
        .AddMvcOptions(options =>
        {
            //...
            options.Filters.Add(new HandlerAuthorizeFilter());
        })
        ;

    //...
}

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

public class IndexModel : PageModel
{
    [Authorize(Roles = "Admin")]
    public IActionResult OnGet()
    {
        return Page();
    }
}
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 配置:


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //...

    /* 使用 FormData 給路由 handler */
    app.Use((context, next) =>
    {
        HttpRequest req = context.Request;

        /* 判斷是否為 POST,且具有 handler 參數,然後覆蓋 Route 的值 */
        if (req.HasFormContentType && req.Form.ContainsKey("handler"))
        { req.RouteValues["handler"] = req.Form["handler"]; }

        return next();
    });

    //...
}
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 進行處理:

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;

namespace XXXXX.Api.Filters
{
    public class PageModelInjectFilter : IPageFilter
    {
        /// <summary>注入器的快取</summary>
        private readonly ConcurrentDictionary<Type, Action<PageModel, IServiceProvider>> _injecterCache =
            new ConcurrentDictionary<Type, Action<PageModel, IServiceProvider>>();


        /// <summary>建構注入器</summary>
        private Action<PageModel, IServiceProvider> buildInjecter(Type type)
        {
            /* delegate 具有疊加的能力,先建構一個空的 delegate */
            Action<PageModel, IServiceProvider> action = (page, provider) => { };

            /* 取得 Properties 且是可以寫入,並具有 [Inject] Attribute */
            var props = type.GetProperties()
                .Where(p => p.CanWrite)
                .Where(p => p.IsDefined(typeof(InjectAttribute)));

            foreach (var prop in props)
            {
                action += (page, provider) =>
                {
                    /* 如果 Property 是已經有 Value 的就不要進行注入 */
                    if (prop.GetValue(page) != null) { return; }

                    /* 從 provider 取得依賴的物件 */
                    object value = provider.GetRequiredService(prop.PropertyType);
                    prop.SetValue(page, value);
                };
            }

            return action;
        }


        /// <summary>在選取處理常式方法之後,但在進行模型系結之前呼叫。</summary>
        public void OnPageHandlerSelected(PageHandlerSelectedContext context)
        {
            /* 判斷是否是 PageModel */
            var page = context.HandlerInstance as PageModel;
            if (page == null) { return; }

            /* 取得或建構注入器 */
            Action<PageModel, IServiceProvider> injecter = 
               _injecterCache.GetOrAdd(page.GetType(), buildInjecter);

            /* 進行注入 */
            injecter(page, context.HttpContext.RequestServices);
        }


        /// <summary>在完成模型系結之後,于處理常式方法執行之前呼叫。</summary>
        public void OnPageHandlerExecuting(PageHandlerExecutingContext context) { }

        /// <summary>在處理常式方法執行之後,在動作結果執行之前呼叫。</summary>
        public void OnPageHandlerExecuted(PageHandlerExecutedContext context) { }
    }
}

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

public void ConfigureServices(IServiceCollection services)
{
    //...

    IMvcBuilder mvcBuilder = services
        .AddRazorPages(options =>
        {
            //...
        })
        .AddMvcOptions(options =>
        {
            //...
            options.Filters.Add(new PageModelInjectFilter());
        })
        ;

    //...
}

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

public class IndexModel : PageModel
{
    [Inject] public IMenuProvider MenuProvider { private get; set; }

    public IActionResult OnGet()
    {
        return Page();
    }
}

2021-10-14 17:21

Exception 是傳遞訊息的通道

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodA()
{
    throw new Exception("error msg");
    show("A");
}

void methodB()
{
    methodA();
    show("B");
}

void methodC()
{
    try
    {
        methodB();
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

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

void show(string tag)
{
    Console.WriteLine(tag);
}

void methodC()
{
    try
    {
        methodB();
            void methodB()
            {
                methodA();
                    void methodA()
                    {
                        throw new Exception("error msg");
    //-----------------------------------------------------
                        show("A");
                    }
                show("B");
            }
        show("C");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

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


* 通透性

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


* 脫離性

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


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

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


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

public class Result
{
    public int Value { get; set; }
    public string Error { get; set; }
}

public Result BadMethod(int input)
{
    var result = new Result();

    if (input > 0)
    {
        result.Value = 100 / input;
    }
    else
    {
        result.Error = "input 需要大於零";
    }
    return result;
}

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

public int GoodMethod(int input)
{
    if (input <= 0)
    {
        throw new Exception("input 需要大於零");
    }

    return 100 / input;
}
2021-10-13 16:45

初學 Exception 的疑問

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

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


* 用 finally 去關閉資源物件

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

FileStream file1 = null;
try
{
    file1 = File.Open("<file path>", FileMode.OpenOrCreate);
    // Do something
}
finally
{
    if (file1 != null) { file1.Close(); }
}

/*== C# 可以用 using 代替上面的程式 ==*/
using (FileStream file2 = File.Open("<file path>", FileMode.OpenOrCreate))
{
    // Do something
}


* 用 catch 去記錄 Exception

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

try
{
    // Do something
}
catch (Expression ex)
{
    _log.Error(ex, "An error occurred.");
}

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

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