2021-10-16

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());
            //...
        })
        ;

    //...
}


沒有留言:

張貼留言

你好!歡迎你在我的 Blog 上留下你寶貴的意見。