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

2015-02-26 14:40

[PHP][Java][C#] 用 XSD 驗證 XML

menu_config.xml 要驗證的 XML
<?xml version="1.0" encoding="utf-8"?>
<menu_config>
    <menu title="文章管理" url="~/Article" target="" allow="">
        <submenu title="列表" url="~/Article/list" target="" allow=""/>
        <submenu/>
        <submenu title="新增" url="~/Article/add" target="" allow=""/>
    </menu>
    <menu/>
    <menu title="帳號管理" url="~/Admin"/>
</menu_config>


menu_config.xsd 結構定義
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">

    <xs:element name="menu_config">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="menu" type="menuType" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:complexType name="menuType">
        <xs:sequence>
            <xs:element name="submenu" type="menuType" minOccurs="0" maxOccurs="unbounded" />
        </xs:sequence>
        <xs:attribute name="title" type="xs:string"/>
        <xs:attribute name="url" type="xs:string"/>
        <xs:attribute name="target" type="xs:string"/>
        <xs:attribute name="allow" type="xs:string"/>
    </xs:complexType>

</xs:schema>
element minOccurs
最少的出現次數,不設置為至少出現 1 次,設置 0 為可有可無。
element maxOccurs
最大的出現次數,設置 unbounded 為無上限。
attribute type
型別必須為 QName,常用的有 xs:string, xs:date, xs:int, xs:integer, xs:decimal, xs:boolean, xs:double, xs:float。


PHP
$xmlFile = 'menu_config.xml';
$xsdFile = 'menu_config.xsd';

/* 啟用自行錯誤處裡 */
libxml_use_internal_errors(true);

$xml = new DOMDocument();
$xml->load($xmlFile);

if (!$xml->schemaValidate($xsdFile)) {
    /* 顯示錯誤訊息 */
    print_r(libxml_get_errors());
    libxml_clear_errors();
}


C#
var menuConfig = XDocument.Load("menu_config.xml");
schemas.Add("", XmlReader.Create("menu_config.xsd"));

menuConfig.Validate(schemas, (o, e) => {
    e.Message.Dump();
});


Java
import java.io.File;
import java.io.IOException;

import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.xml.sax.SAXException;


public class TestXsd {

    public static void main(String[] args) {
        File xsd = new File("menu_config.xsd");
        File xml = new File("menu_config.xml");

        try {
            SchemaFactory factory = SchemaFactory.newInstance(
                XMLConstants.W3C_XML_SCHEMA_NS_URI
            );
            Schema schema = factory.newSchema(xsd);
            Validator validator = schema.newValidator();

            validator.validate(new StreamSource(xml));
            System.out.println("xml is valid");
        }
        catch (SAXException|IOException e) {
            System.out.println("Reason: " + e.getLocalizedMessage());
        }
    }
}



參考資料:
XML 結構描述項目 - MSDN
XML Schema Tutorial W3Schools
2014-03-09 17:47

理解 PredicateBuilder 實作方式

PredicateBuilder 提供了以 OR 串接 bool 的 Lambda Expression,使用上會像下面的程式:
var predicate = PredicateBuilder.False<Product>();

predicate = predicate.Or(p => p.Name == 'Shoe');
predicate = predicate.Or(p => p.Price > 100);


它的原始碼如下:
public static Expression<Func<T, bool>> Or<T> (
    this Expression<Func<T, bool>> expr1,
         Expression<Func<T, bool>> expr2
)
{
    var invokedExpr = Expression.Invoke (
        expr2, 
        expr1.Parameters.Cast<Expression>()
    );
 
    return Expression.Lambda<Func<T, bool>>(
        Expression.OrElse (expr1.Body, invokedExpr), 
        expr1.Parameters
    );
}


上面的程式雖然不長,但實在有點小難懂,搞不懂他為什麼要這麼寫??讓我們逐步解釋吧!


假設 expr1 是 x => x.Name == 'Shoe'
假設 expr2 是 y => y.Price > 100

var invokedExpr = Expression.Invoke(
    expr2, 
    expr1.Parameters.Cast<Expression>()
);
先組合出( 利用 expr1 的參數去執行 expr2 )的表示式 invokedExpr
產生出了語法會像 (y => y.Price > 100) (x)

Expression.OrElse(expr1.Body, invokedExpr)
將 expr1.Body 與 invokedExpr 以 || 運算子組合起來
產生出了語法會像 x.Name == 'Shoe' || (y => y.Price > 100) (x)

Expression.Lambda<Func<T, bool>>(
    Expression.OrElse(expr1.Body, invokedExpr), 
    expr1.Parameters
);
將剛剛組合好的 || 運算表示式再組合成 Lambda 表示式
產生出了語法會像 x => (x.Name == 'Shoe' || (y => y.Price > 100) (x))


參考來源:
Dynamically Composing Expression Predicates
2014-03-05 23:38

利用 HTTP Status Codes 傳遞 Ajax 成功失敗的狀態

一般處理 Ajax 回應時會傳送的資訊種類有:資料、成功訊息、錯誤訊息、失敗訊息以及處理狀態,傳遞的資訊種類並不一致,再加上除了資料之外,通常還希望能傳遞處理狀態,這種情況大部分會選擇是以 JSON 的方式傳遞這兩個訊息,以下是常見的幾種格式:

{ code: 1, msg: "OK" }
{ success: true, result: "data", errorMsg: "" }
{ status: 'success', result: [], errorMsg: "" }
//...

但以執行狀態跟操作行為作一個歸納,可以區分以下幾種回傳結果:
資料操作 HTTP Method 成功 錯誤/失敗
檢視(Read) GET 資料 錯誤/失敗訊息
新增(Create)
修改(Update)
刪除(Delete)
POST 成功訊息 錯誤/失敗訊息

從上面的歸納可以看出規律性,接著只要有方法可以傳送處理的狀態,以及能夠區分資料的種類,其實就單純很多,而 HTTP Status Codes 就是用來傳遞 HTTP 的處理狀態,如果利用這個方式來傳遞自訂的處理狀態,這樣 HTTP Content 就可以很單純傳遞資料,讓資料格式不受限於 JSON,還可以使用其他格式(text, xml, html),而且 XMLHttpRequest 本身就有處理 HTTP Status Codes 的能力,而 jQuery.ajax 也有提供 error status 的處理,所以可以利用這個來定義狀態的處理,在 HTTP Status Codes 有幾個已經定義狀態,很適合用來回傳處理狀態的資訊:

400 Bad Request 錯誤的請求 適用在表單內容的錯誤,如必填欄位未填、Email 格式錯誤
403 Forbidden 沒有權限,被禁止的 適用在沒有登入或權限不足
500 Internal Server Error 內部服務器錯誤 適用在程式的錯誤


jQuery 接收資訊的範例
$.ajax({
    type: "POST",
    url: document.location,
    success: function (data, textStatus, jqXHR) {
        alert(data);
    },
    error: function (jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
    }
});


PHP 傳遞錯誤訊息的範例
if (php_sapi_name() == 'cgi'){
    header("Status: 400 Bad Request");
}else{
    header("HTTP/1.0 400 Bad Request");
}
exit("儲存失敗!!");


C# MVC 傳遞錯誤訊息的範例
Response.TrySkipIisCustomErrors = true;
Response.StatusCode = 400;
return Content("儲存失敗!!");
2014-02-21 17:43

[T4] url, base64, sprite 三種格式的 icons.css 產生器

之前有寫過 [PHP] url, base64, sprite 三種格式的 icons.css 產生器,這次換用 C# T4 Text Templates 來製作這個功能:

int spriteGap = 30;  
var scanTypes = new string[]{".jpg", ".gif", ".png"};

string workDirectory = 
    Path.GetDirectoryName(this.Host.TemplateFile);


/* 取得圖檔資訊 */  
List<ImageInfo> imageList = Directory
    .EnumerateFiles(Path.Combine(workDirectory, "icons"))
    .Where(path => scanTypes.Contains(Path.GetExtension(path)))
    .Select(path => 
    {
        var image = Image.FromFile(path);

        return new ImageInfo{
            FilePath = path,
            FileName = Path.GetFileName(path),
            IconImage = image,
            IconName = Path.GetFileNameWithoutExtension(path),
            TopOffset = 0,
            Width = image.Width,
            Height = image.Height,
            IsAnimated = ImageAnimator.CanAnimate(image),
        };
    })
    .OrderBy(info => info.IsAnimated)
    .ToList();



/*檢查重複的圖檔名稱*/
List<string> repeatList = imageList.GroupBy(info => info.IconName)
    .Where(g => g.Count() > 1)
    .SelectMany(g => g.Select(x => x.FileName))
    .ToList();

if(repeatList.Count > 0){
    throw new Exception(
        "出現重複的圖檔名稱[" + String.Join(", ", repeatList) + "]"
    );
}   



/* 製作 CSS Sprite */
int sumHeight = imageList.Sum( info => info.Height);
int spriteWidth = imageList.Max( info => info.Width);
int spriteHeight = sumHeight + (imageList.Count - 1) * spriteGap;

var bitmap = new Bitmap(spriteWidth, spriteHeight);

using (Graphics graphics = Graphics.FromImage(bitmap))
{           
    int nextTop = 0;
    for(int i=0; i< imageList.Count; i++){
        /*忽略具有動畫的 GIF 圖片*/ 
        if(imageList[i].IsAnimated){ continue; } 

        imageList[i].TopOffset = nextTop;
        
        graphics.DrawImage(imageList[i].IconImage, 0, nextTop);
            
        nextTop = nextTop + imageList[i].Height + spriteGap;
    }
        
    graphics.Save();
}

SaveOutput("icons.sprite.png"); 
bitmap.Save(
    Path.Combine(workDirectory, "icons.sprite.png"), 
    ImageFormat.Png
);
bitmap.Dispose();

下載完整程式: t4_make_icons_css.zip
2014-02-15 14:48

[C#] 取得 Domain 的 IP 列表

//using System.Net;
IPAddress[] ipList = Dns.GetHostAddresses("www.google.com");


來源:
Dns.GetHostAddresses 方法 (System.Net)

2014-02-15 14:45

[C#] delegate 到 Lambda Expressions 語法演進

一開始要看懂 Lambda Expressions 有點困難,下面會以演進方式來介紹如何做到語法省略。

首先定義一個單參數的 delegate
delegate int Del(int x);

以傳統 delegate 的語法來建構 delegate
Del a = delegate(int x) { return x + 2; };

去掉 delegate 改成 Lambda 表示式
Del a = (int x) => { return x + 2; };

由於大括號裡只有一句陳述式,而且是一個 return 的陳述式,所以可以省略大括號跟 return
Del a = (int x) => x + 2;

在 delegate 已經有定義輸入參數的型別,所以在小括號裡的型別可以省略
Del a = (x) => x + 2;

由於小括號裡面只有一個輸入參數,所以可以再進一步省略小括號
Del a = x => x + 2;

參考來源:
Lambda 運算式 (C# 程式設計手冊)
2014-02-10 23:43

[C#] 對 FirstOrDefault 新的認識

FirstOrDefault 會依據最後的型別去決定 Default 時回傳的值,例如下面的範例:
int? a = (new List<int>{2}).Select(x => x).FirstOrDefault();
// 2

int? b = (new List<int>{}).Select(x => x).FirstOrDefault();
// 0


int? c = (new List<int>{2}).Select(x => (int?)x).FirstOrDefault();
// 2

int? d = (new List<int>{}).Select(x => (int?)x).FirstOrDefault();
// null
2014-02-10 23:38

[C#] 用 LINQ 將字串切割成整數陣列

int id;
int[] idArray = "1,2,3,4,5".Split(',')
    .Where(idStr => Int32.TryParse(idStr, out id))
    .Select(Int32.Parse)
    .ToArray();

這裡用到將 Int32.Parse 這個一般 method 指向給 delegate 的技巧。
2014-02-05 20:53

[C#] HttpUtility.ParseQueryString 的隱藏密技

在使用 Request.QueryString 發現 ToString 會產生 URL 的 query 字串,嘗試用 NameValueCollection 的 ToString 卻不是產生 URL 的 query 字串,這一整個就很奇怪,明明都是 NameValueCollection 確有不一樣的結果,透過 Reflector 發現 Request.QueryString 的 instance 型別是一個 HttpValueCollection,想說可以直接 new HttpValueCollection 出來使用,但 HttpValueCollection 卻是 System.Web 的內部 Class,外部是無法直接 new 出來使用,還好在又發現 HttpUtility.ParseQueryString 回傳的 NameValueCollection 的 instance 是 HttpValueCollection 這個型別,所以可以透過 HttpUtility.ParseQueryString 來建立 HttpValueCollection。

// using System.Web;

var qs1 = HttpUtility.ParseQueryString("id=5&type=1");
qs1.ToString(); // "id=5&type=1"

var qs2 = HttpUtility.ParseQueryString(String.Empty);
qs2["id"] = "11";
qs2["name"] = "Tom"; 

qs2.ToString(); // "id=11&name=Tom"


HttpValueCollection 的簽名
[Serializable]     
internal class HttpValueCollection : NameValueCollection     
{
}


ParseQueryString 的簽名
public static NameValueCollection ParseQueryString(string query, Encoding encoding)
{
    if (query == null)
    {
        throw new ArgumentNullException("query");
    }
    if (encoding == null)
    {
        throw new ArgumentNullException("encoding");
    }
    if ((query.Length > 0) && (query[0] == '?'))
    {
        query = query.Substring(1);
    }
    return new HttpValueCollection(query, false, true, encoding);
}
2014-01-30 20:33

[C#] 圖片縮圖

System.Drawing namespace 提供對 GDI+ 基本繪圖功能的存取,可以方便處理圖片的操作,下面是一個處理圖片縮圖的範例:

//using System.Drawing;

var image = Image.FromFile(@"D:\001.jpg");

int width = 120, height = 100;
float targetRatio = (float)width / (float)height;
float imageRatio = (float)image.Width / (float)image.Height;


if(imageRatio < targetRatio)
{
    width = Math.Max(1, height * image.Width / image.Height);
}
else
{
    height = Math.Max(1, width * image.Height / image.Width); 
}


var thumbnail = image.GetThumbnailImage(width, height, null, IntPtr.Zero);

thumbnail.Save(@"D:\thumb.jpg");

這個方式可以容易的做到圖片縮圖,但使用這個方式會很佔用記憶體,在處理尺寸大的圖片時有可能會出現 out of memory,而且在 MSDN 有以下的警示:
Classes within the System.Drawing namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions. For a supported alternative, see Windows Imaging Components.
2013-05-01 21:59

[C#] 利用 LINQ 解析 XML 至 POCO

menu_config.xml
<?xml version="1.0" encoding="utf-8"?>
  <menu_config>
    <menu title="文章管理" url="~/Article" target="" allow="">
        <submenu title="列表" url="~/Article/list" allow="" />
        <submenu />
        <submenu title="新增" url="~/Article/add" allow="" />
    </menu>
    <menu />
    <menu title="帳號管理" url="~/Admin" />
  </menu_config>


//using System.Collections.Generic;
//using System.IO;
//using System.Linq;
//using System.Xml.Linq;
//using System.Text;

public class MenuDataModel
{
    /*POCO 資料欄位*/
    public string Title { get; set; }
    public string Url { get; set; }
    public string Target { get; set; }
    public string Allow { get; set; }
    public string Icon { get; set; }
    public List<MenuDataModel> Submenu { get; set; }

    /*取得資料清單*/
    public static List<MenuDataModel> GetList(string menuConfigPath)
    {
        /*讀取 XML 檔案*/
        var xmlContent = File.ReadAllText(menuConfigPath, Encoding.UTF8);
        var menuDocument = XDocument.Parse(xmlContent);

        /*利用 LINQ 轉成 POCO*/
        return menuDocument.Root.Elements("menu")
        .Select(menu => new MenuDataModel
        {
            /*取得 Element 上的 Attribute*/
            Title   = (string) menu.Attribute("title"),
            Url     = (string) menu.Attribute("url"),
            Target  = (string) menu.Attribute("target"),
            Icon    = (string) menu.Attribute("icon") ?? "Item",
            Allow   = (string) menu.Attribute("allow"),

            /*取得子層級 Element 上的 Attribute*/
            Submenu = menu.Elements("submenu")
            .Select(sub => new MenuDataModel
            {
                Title   = (string) sub.Attribute("title"),
                Url     = (string) sub.Attribute("url"),
                Target  = (string) sub.Attribute("target"),
                Icon    = (string) sub.Attribute("icon") ?? "Item",
                Allow   = (string) sub.Attribute("allow"),
            }).ToList(),
        }).ToList();
    }
}


void Main()
{
    var path = @"D:\menu_config.xml";

    List<MenuDataModel> list =  MenuDataModel.GetList(path);

    list.Dump();
}
2013-05-01 21:42

[C#] 取得 URL 頁面上的 title 內容

//using System.Net;
//using System.IO;
//using System.Text;

string url = @"http://msdn.microsoft.com/en-us/library/az24scfc.aspx";
string title = String.Empty;

WebResponse response = null;
WebRequest request = WebRequest.Create(url);

/*設定最長執行的毫秒數*/
request.Timeout = 10000; 

try{
    /*取得 URL 頁面資料*/
    response = request.GetResponse();
    StreamReader stream = new StreamReader(
        response.GetResponseStream(), Encoding.UTF8
    );

    /*只取得前 4096 個字*/
    char[] buf = new char[4096];
    stream.Read(buf, 0, buf.Length);

    /*尋找標題字串*/
    string pageText = new String(buf);
    string pattern = @"(?<=<title[^>]*>)([^<]*)(?=</title>)";
    title = Regex.Match(pageText, pattern, RegexOptions.IgnoreCase)
            .Value.Trim();

}catch(WebException e){
}finally{
    if(response!=null){ response.Close(); }
}

title.Dump();