顯示具有 C# 標籤的文章。 顯示所有文章
顯示具有 C# 標籤的文章。 顯示所有文章
2022-08-01 11:37

[轉載] NLog Variables

轉載自:當條件等于不作業時.net核心Nlog過濾器

我必須從我的啟動類傳遞變數值

LogManager.Configuration.Variables["environment"] = "Development";

我在我的 nlog.config 檔案中添加了以下過濾器

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters>
            <when condition="equals('${var:environment}', 'Development')" action="Ignore" />
        </filters>
    </logger>
</rules>

即使我將值作為 Development 傳遞,該訊息仍會被記錄而不是忽略。

但是,當我對它的作業值進行硬編碼時

您在 NLog中發現了一個錯誤,但如果您這樣做,它應該可以作業(也會更快):

<rules>
    <logger name="*" minlevel="Error" writeTo="logfile">
        <filters defaultAction='log'>
            <when condition="'${var:environment}' == 'Development'" action="Ignore" />
        </filters>
    </logger>
</rules>

您也可以用 minLevel="${var:EnvironmentMinLevel:whenEmpty=Error}" 處理,這比 <filters> 快得多

<rules>
    <logger name="*" minlevel="${var:EnvironmentMinLevel:whenEmpty=Error}" writeTo="logfile" />
</rules>

設定 Variables 要記得呼叫 Reconfig,或者在 config 中設定 autoReload="true"

NLog.LogManager.Configuration.Variables["EnvironmentMinLevel"] = "Off";
NLog.LogManager.ReconfigExistingLoggers();

另請參閱 https://github.com/NLog/NLog/wiki/Filtering-log-messages#semi-dynamic-routing-rules

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

2020-08-04 19:09

利用 redirect 跳轉到 預設頁 或 預設查詢

在網站開發有一些技巧可以增加後續的維護性,利用重導向來做預設內容的處裡,這樣可以統一集中的進行控制。

預設頁面最常會因為業務的策略因素進行調整,這時候散落在各地的連結都要調整,費工又容易漏。

預設查詢這個麻煩點在於參數會變動,散落在各地的連結一樣是個麻煩。


但重導向這種方式也是有損失的,將會多一個 HTTP 請求,這對 UI 反應速度很要求的狀況來說,並不是一個好方法,可能就要改用統一網址管理來處理。


利用 MVC 的 Index() 來控制預設頁面,Index 將不會有實體頁面,而是用來進行重導向。
public class UserController : Controller
{
    public ActionResult Index()
    {
        return RedirectToAction(nameof(List));
    }

    public ActionResult List(DateTime? date)
    {
        //...
        return View();
    }

}


判斷 QueryString 為空時,進行預設查詢的重導向,以 QueryString 為判斷點的好處是有時候就是要查詢全部資料,這樣就不會被預設查詢卡到。
public class UserController : Controller
{
    public ActionResult List(DateTime? date)
    {
        if (Request.QueryString.Count == 0)
        {
            RedirectToAction(nameof(List), new 
            { 
                date = DateTime.Today.ToString("yyyy-MM-dd") 
            });
        }

        //...
        return View();
    }


}


2020-07-06 13:35

LINQ 方法解釋一覽

轉載自: WIDEC

這張圖很好的解釋了 LINQ 常用的方法會產生的結果:


2020-07-06 13:25

C# MVC 生命週期圖

轉仔自: Detailed ASP.NET MVC Pipeline

2020-07-06 13:18

C# MVC Cassette 隔離區快取找不到的錯誤

有使用 Cassette 的專案,在發布站台之後網頁開起來卻發生了 JS 跟 CSS 全部掉光,查看錯誤訊息時發現 Cassette 抓不到在隔離區的快取。

原因是當 JS 跟 CSS 有更新時,Cassette 會重新產生快取的 key,卻沒有產生隔離區的快取檔案,造成抓不到快取的問題,IIS pool 重啟也無效,Cassette 的管理畫面也進不去。

解決辦法就是呼叫 Bundles.RebuildCache(),最好方式就是在 Global.asax.cs 增加自動處裡。

protected void Application_Error(object sender, EventArgs e)
{
    /* 重建 Cassette 綑綁 */
    var ex = Server.GetLastError() as System.IO.FileNotFoundException;
    if (ex != null && ex.StackTrace.Contains("Cassette."))
    { Bundles.RebuildCache(); }
}
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;
}
2019-07-22 16:49

C# COM 元件使用 MTA

先前有用到一個通訊用的 COM 元件,因為連線不穩的時候會影響到 Main Thread 造成這個 WinForm UI 卡住,連帶所有 Main Thread 下的其他 Thread 都卡住,最先找到的方法是在 Program Main 上改用 MTAThreadAttribute,的確是可以解決卡住的問題。

但 WPF 就不可以用 MTAThreadAttribute,因為 WPF 必須執行在 STAThread 的環境上,又開始苦惱這個問題了,問題應該還是有解套的辦法的只是知識不足,最後在 WIKI 中看到重要的知識。

WIKI 元件物件模型
一個COM物件只能存在於一個套間。COM物件一經建立就確定所屬套間,並且直到銷毀它一直存在於這個套間。

所以只要用其他 Thread 去建立 COM 元件就不會影響到 Main Thread 了,簡單的解決問題,果然是知識不足。

ActEasyIF actConnection;

var waiter = new AutoResetEvent(false);

new Thread(() =>
{
    /* 建構 COM 元件 */
    _actConnection = new ActEasyIF();

    waiter.Set();
}).Start();

waiter.WaitOne(500); /* 等待建立結束 */
2019-07-22 15:36

C# struct 轉換到 byte array

StructLayout: https://docs.microsoft.com/zh-tw/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8
Pack: 資料欄位的對齊,這會影響最短欄位的 byte 長度

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct PollResponse
{
    public int AppId;
    public byte Serial;
    public short Station;
}


void Main()
{
    var data = new PollResponse
    {
        AppId = 1,
        Serial = 2,
        Station = 3,
    };

    Type type = typeof(PollResponse);
    int size = Marshal.SizeOf(type);
    var bytes = new byte[size];

    /* struct to byte array */
    IntPtr ptrIn = Marshal.AllocHGlobal(size);
    Marshal.StructureToPtr(data, ptrIn, true);
    Marshal.Copy(ptrIn, bytes, 0, size);
    Marshal.FreeHGlobal(ptrIn);

    BitConverter.ToString(bytes).Dump();
    /* 01-00-00-00 - 02 - 03-00 */


    /* byte array to struct */
    IntPtr ptrOut = Marshal.AllocHGlobal(size);
    Marshal.Copy(bytes, 0, ptrOut, size);
    var result = (PollResponse)Marshal.PtrToStructure(ptrOut, type);
    Marshal.FreeHGlobal(ptrOut);

    result.Dump();
    /* { AppId = 1, Serial = 2, Station = 3 } */
}
2019-07-22 13:52

C# 用 Lambda 設定 MVC Route Constraint

MVC 的 Route Constraint 支援 Regular Pattern (string) 以及 IRouteConstraint,簡單的限制還可以用 Regular 處理,複雜的就需要實作 IRouteConstraint 了,既然都是一次工,索性就想要搭配 Lambda 讓設定可以更彈性的點。


routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    constraints: new { controller = new ValueConstraint(x => x.ToUpper() != "WCF") }
);

public class ValueConstraint : IRouteConstraint
{
    private Func<string, bool> _match;

    public ValueConstraint(Func<string, bool> match)
    {
        _match = match;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return _match("" + values[parameterName]);
    }
}


順便增加了 HttpContextBase 的處理
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    constraints: new { authenticated = new HttpConstraint(x => x.Request.IsAuthenticated) }
);

public class HttpConstraint : IRouteConstraint
{
    private Func<HttpContextBase, bool> _match;

    public HttpConstraint(Func<HttpContextBase, bool> match)
    {
        _match = match;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return _match(httpContext);
    }
}
2019-07-21 17:21

C# 讓 Dequeue 更方便的擴充方法

void Main()
{
    var queue = new Queue<int>();

    for (int i = 0; i < 10; i++) { queue.Enqueue(i); }
    string.Join(",", queue).Dump(); /* 0,1,2,3,4,5,6,7,8,9 */


    var take = queue.EnumerateDequeue().Take(4).ToList();
    string.Join(",", take).Dump(); /* 0,1,2,3 */
    string.Join(",", queue).Dump(); /* 4,5,6,7,8,9 */


    var take2 = queue.EnumerateDequeue().Take(40).ToList();
    string.Join(",", take2).Dump(); /* 4,5,6,7,8,9 */
    string.Join(",", queue).Dump(); /* */
}



public static class QueueExtensions
{

   public static IEnumerable<T> EnumerateDequeue<T>(this Queue<T> source)
   {
       while (source.Count > 0) { yield return source.Dequeue(); }
   }

   public static IEnumerable<T> EnumerateDequeue<T>(this ConcurrentQueue<T> source)
   {
       T outValue;
       while (source.TryDequeue(out outValue)) { yield return outValue; }
   }

}
2019-07-21 17:06

C# 在 Enum 上增加附加資訊

C# 的 Enum 是個很方便的類型,如果可以再增加額外的資訊就更方便了,這裡利用 Attribute 去定義 Enum 額外的資訊,再用擴充方法取得 Enum 所屬的資訊。

用 Attribute 來定義有個好處,未來在增減 Enum 時可以一起進行修改,不用擔心會有遺漏而沒修改的問題。

void Main()
{
    PortAreaCode.F1Front.GetFloor().Dump(); /* F1 */
}


public enum PortAreaCode
{
    [AreaMeta("None", 0)]
    None,

    [AreaMeta("F1", 1)]
    F1Front,

    [AreaMeta("F2", 1)]
    F2Front,
}



/// <summary>PortAreaCode 額外附屬資訊定義的 Attribute</summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
class AreaMetaAttribute : Attribute
{
    public string Floor { get; private set; }
    public int WarehouseId { get; private set; }

    public AreaMetaAttribute() : this("None", 0) { }

    public AreaMetaAttribute(string floor, int warehouseId)
    {
        Floor = floor;
        WarehouseId = warehouseId;
    }
}



/// <summary>PortAreaCode 的擴充方法</summary>
public static class PortAreaCodeExtensions
{
    private static AreaMetaAttribute _defaultMeta = new AreaMetaAttribute();

    private static AreaMetaAttribute getMeta(PortAreaCode value)
    {
        FieldInfo field = typeof(PortAreaCode).GetField(value.ToString());
        if(field == null) { return _defaultMeta; }

        var meta = field.GetCustomAttribute<AreaMetaAttribute>();
        return meta ?? _defaultMeta;
    }

    public static string GetFloor(this PortAreaCode value)
    {
        return getMeta(value).Floor;
    }

    public static int GetWarehouseId(this PortAreaCode value)
    {
        return getMeta(value).WarehouseId;
    }
}
2019-07-21 16:29

C# 用 gzip 壓縮字串並取得 base64 字串

這個使用方式的效果是有但書的,當 Source 的重複率不高壓縮的效果就不會好,再加上 base64 就是用可見文字去表示 byte 值,這會讓 base64 後的結果比 byte array 還要長,所以壓縮率沒有到達一定的程度下,輸出反而會比 Source 的字串還要長。

//using System.IO.Compression;


void Main()
{
    string text = "OptionPostal,OptionClassType,OptionTalentItem";
    text.Length.Dump(); /* 45 */

    string compressBase64 = compress(text);
    compressBase64.Length.Dump(); /* 76 */
    compressBase64.Dump(); 
    /* H4sIAAAAAAAEAPMvKMnMzwvILy5JzNHxB3OccxKLi0MqC1Kh/JDEnNS8Es+S1FwAaY6qVC0AAAA= */

    string decompressText = decompress(compressBase64);
    decompressText.Dump(); 
    /* OptionPostal,OptionClassType,OptionTalentItem */

    text = "OptionPostal,OptionClassType,OptionTalentItem,OptionPostal,OptionClassType,OptionTalentItem,OptionPostal,OptionClassType,OptionTalentItem,OptionPostal,OptionClassType,OptionTalentItem";
    text.Length.Dump(); /* 183 */

    compressBase64 = compress(text);
    compressBase64.Length.Dump(); /* 84 */
    compressBase64.Dump(); 
    /* H4sIAAAAAAAEAPMvKMnMzwvILy5JzNHxB3OccxKLi0MqC1Kh/JDEnNS8Es+S1FyowCBQDQBPmlWktwAAAA== */

}


/*壓縮*/
private static string compress(string text)
{
    if (string.IsNullOrEmpty(text)) { return text; }

    byte[] buffer = Encoding.UTF8.GetBytes(text);

    using (var outStream = new MemoryStream())
    using (var zip = new GZipStream(outStream, CompressionMode.Compress))
    {
        zip.Write(buffer, 0, buffer.Length);
        zip.Close();

        string compressedBase64 = Convert.ToBase64String(outStream.ToArray());
        return compressedBase64;
    }
}


/*解壓縮*/
private static string decompress(string compressed)
{
    if (string.IsNullOrEmpty(compressed)) { return compressed; }

    byte[] buffer = Convert.FromBase64String(compressed);

    using (var inStream = new MemoryStream(buffer))
    using (var outStream = new MemoryStream())
    using (var zip = new GZipStream(inStream, CompressionMode.Decompress))
    {
        zip.CopyTo(outStream);
        zip.Close();

        string text = Encoding.UTF8.GetString(outStream.ToArray());
        return text;
    }
}
2019-07-21 15:53

產生 IP v6 的 mask byte array

int length = 121; /* total 128 */

var mask = new byte[16];

for (int i = 0; i < 16; i++)
{
    mask[i] = 0xff;
    if (length > -8) { length -= 8; }
    if (length < 0) { mask[i] = (byte)(mask[i] << -length); }
    /* 當 length 出現負值時代表需要進行位移 */
}

BitConverter.ToString(mask).Dump();
/* FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-FF-80 */
2019-07-21 15:43

C# DateTimeOffset Parse Patch

DateTimeOffset 在 Parse 時會使用 Local TimeZone,這會與期望的 TimeZone 產生偏差,需要進行差值修補。

//TimeZoneInfo.GetSystemTimeZones().Dump();

/* (UTC+02:00) 開羅 */
var zone = TimeZoneInfo.FindSystemTimeZoneById("Egypt Standard Time");
zone.Dump();

var date = DateTimeOffset.Parse("2019-07-01 15:00:00");
date.Dump(); /* 2019/7/1 下午 03:00:00 +08:00 */

var diff = date.Offset - zone.BaseUtcOffset;
diff.Dump(); /* 06:00:00 */

date = date.Add(diff);
date.Dump(); /* 2019/7/1 下午 09:00:00 +08:00 */

date = TimeZoneInfo.ConvertTime(date, zone);
date.Dump(); /* 2019/7/1 下午 03:00:00 +02:00 */