顯示具有 Net Core 標籤的文章。 顯示所有文章
顯示具有 Net Core 標籤的文章。 顯示所有文章
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-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();
    }
}

2020-02-13 10:35

EF Core 3.1 取得 IQueryable 的 SQL

private static T getPrivate<T>(this object obj, string privateField)
{
    return (T)obj?.GetType()
        .GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)
        ?.GetValue(obj);
}

public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
{
    var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();

    var relationalQueryContext = enumerator.getPrivate<RelationalQueryContext>("_relationalQueryContext");
    var relationalCommandCache = enumerator.getPrivate<RelationalCommandCache>("_relationalCommandCache");

#pragma warning disable EF1001 // Internal EF Core API usage.
    IRelationalCommand command = relationalCommandCache.GetRelationalCommand(relationalQueryContext.ParameterValues);
#pragma warning restore EF1001 // Internal EF Core API usage.

    return command.CommandText;
}