2018年5月14日 星期一

部落格正式搬家到 https://toyo0103.github.io


原本寫部落格只是想記錄工作碰到的問題與解法,並希望藉由回顧部落格來檢視自己是否有在前進與成長,不知不覺就這樣寫了5年多。

最近無意間看到Hexo後,覺得用Markdown的寫作方式相當吸引我(以前常常為了調整CSS覺得很阿雜),而且Hexo有許多第三方的整合與支援,讓部落格只要稍微設定一下就有一定水準。

因為上述原因,決定花了一些時間把部落格搬家到 https://toyo0103.github.io/ ,之後這邊文章還是會保留,只是就不會再進行維護跟新增了,也感謝這段時間大家的回饋跟指教!!

2018年5月7日 星期一

【MVC教學】驗證參數- 透過FluentValidation


Demo範例 : Git位置

上一篇簡介了Controller如何安排程式邏輯流程、驗證參數,卻也發現了驗證參數導致程式寫得冗長不易維護,這一篇要介紹【FluentValidation】這個套件來解決這個問題。


首先先透過Nuget來安裝套件,因為我們用的版本是MVC5,所以安裝這個版本



安裝完後應該會在參考中看到多出兩個FluentValidation的組件


設定


把之前的程式碼稍作整理,將Account與Password新建一個類型(Class)來封裝起來,原本Controller裡面的程式整理一下






將驗證的邏輯透過FluentValidation來做

建立一個Class命名為UserSignUpParameterValidation



將UserSignUpParameter掛上Validator Attribute,意思是UserSignUpParameter請幫我用UserSignUpParameterValidation裡面的邏輯來驗證



將原本的驗證邏輯慢慢的移入UserSignUpParameterValidation之中

1. 帳號密碼不能為空值


    /// <summary>
    /// Validation UserSignUpParameter
    /// </summary>
    /// <seealso cref="FluentValidation.AbstractValidator{HelloDotNetMVC.Parameters.UserSignUpParameter}" />
    public class UserSignUpParameterValidation:AbstractValidator<UserSignUpParameter>
    {
        public UserSignUpParameterValidation()
        {
            //AbstractValidator為FluentValidation提供的抽象類別
            //目的是讓使用者可以透過繼承這個抽象類別後,實作自己的驗證邏輯
            //而泛型T, AbstractValidator<T> ,則帶入你想驗證的類別
            //所以這邊帶入UserSignUpParameter,這個我們剛剛製作的Class

            //Account
            RuleFor(x => x.Account)
            .NotEmpty()
            .WithMessage("帳號不能為Empty")
            .NotNull()
            .WithMessage("帳號不能為Null");

            //Password
            RuleFor(x=>x.Password)
            .NotEmpty()
            .WithMessage("密碼不能為Empty")
            .NotNull()
            .WithMessage("密碼不能為Null");
        }
    }

RuleFor是FluentValidation提供的方法,意思是我為Account這個屬性建立驗證的Rule,而Rule分別是,NotEmpty(不能為空值)、NotNull(不能為Null),而緊接在每個驗證邏輯下面的WithMessage則是表示,當發生它上面的錯誤時,錯誤訊息請回傳這行。


接著我們可以將Controller的這塊驗證邏輯拿掉,交給UserSignUpParameterValidation專責處理就好。




2. 帳號必須包含@字元,Password必須大於6個字

因為判斷字元@並不像判斷空值與Null一樣,官方有實做好的方法,必須自己透過Must來定義,這邊提供兩種寫法給讀者參考




上面是匿名方法的寫法,但如果你對於匿名委派還不是很熟悉,也可以用比較簡單的方式寫


先寫好驗證的方法,然後在Must中帶入方法。

延伸閱讀 : 
MSDN : Func<T,TResult> 委派 
MSDN : 匿名方法

    /// <summary>
    /// Validation UserSignUpParameter
    /// </summary>
    /// <seealso cref="FluentValidation.AbstractValidator{HelloDotNetMVC.Parameters.UserSignUpParameter}" />
    public class UserSignUpParameterValidation:AbstractValidator<UserSignUpParameter>
    {
        public UserSignUpParameterValidation()
        {
            //AbstractValidator為FluentValidation提供的抽象類別
            //目的是讓使用者可以透過繼承這個抽象類別後,實作自己的驗證邏輯
            //而泛型T, AbstractValidator<T> ,則帶入你想驗證的類別
            //所以這邊帶入UserSignUpParameter,這個我們剛剛製作的Class

            //Account
            RuleFor(x => x.Account)
            .NotEmpty()
            .WithMessage("帳號不能為Empty")
            .NotNull()
            .WithMessage("帳號不能為Null")
            .Must(IncludeAccountKeyword)
            .WithMessage("帳號不符合格式");

            //Password
            RuleFor(x=>x.Password)
            .NotEmpty()
            .WithMessage("密碼不能為Empty")
            .NotNull()
            .WithMessage("密碼不能為Null")
            .MinimumLength(7)
            .WithMessage("密碼必須大於6個字元");
        }

        /// <summary>
        /// Accounts 必須包含@.
        /// </summary>
        /// <param name="account">The account.</param>
        /// <returns></returns>
        private bool IncludeAccountKeyword(string account)
        {
            if (!string.IsNullOrWhiteSpace(account))
            {
                return account.Contains("@");
            }

            return false;
        }
    }

整理後的Controller



透過ModelState來得知參數是否符合規則

ModelState是本來MVC就有提供的參數驗證方式,而使用方法這邊提供幾篇文章給有興趣的讀者參考
What is the ModelState? - ASP.NET MVC Demystified
保哥 : ASP.NET MVC 開發心得分享 (28):深入了解 ModelState 內部細節

而因為開發習慣的關係,我習慣用FluentValidation的方式來取代原本ModelState驗證方法與設定方式。


接著我們在Global.asax中呼叫FluentValidationModelValidatorProvider.Configure()


        [HttpPost]
        public ActionResult SignUp(UserSignUpParameter parameter)
        {
            if (!ModelState.IsValid)
            {
                //IsValida為False時,表示驗證參數不過
                //取出第一筆錯誤訊息回傳
                var Error = ModelState.Values.SelectMany(x => x.Errors).First();
                TempData["Message"] = Error.ErrorMessage;
                return RedirectToAction("signup", "user");
            }

            //這邊沒有搬到FluentValidation去驗證的原因是
            //使用者存在與否比較屬於服務層的事情,通常需要讀取資料庫才能判斷
            //開發上習慣盡量讓驗證參數越乾淨簡單越好,而不是在裡面呼叫很多外部服務(例如資料庫)後做驗證
            //這會讓職責過於複雜
            if (parameter.Account != "steven@mymail.com")
            {
                TempData["Message"] = "註冊成功!!";
                //註冊成功,導到首頁 
                return RedirectToAction("index", "home");

            }
            else
            {
                TempData["Message"] = "帳號已經存在";
                return RedirectToAction("signup", "user");
            }
        }

接著執行程式,驗證看看是否之前的驗證邏輯都還是正常運作。






比較一下最一開始與套用FluentValidation後的程式碼,應該可以明顯感受到差異,這不僅僅只是讓程式變乾淨,更重要的是驗證參數的職責被分離到UserSignUpParameterValidation這個類別中,以後要改參數驗證的邏輯只要到這調整即可,不用再擔心Controller會被不小心改壞。




延伸閱讀



2018年4月25日 星期三

【MVC教學】Controller簡介


Demo範例 : Git位置

前面幾篇解說了Route設定,讓我們的程式能順利找到對應的Controller來執行,那今天要來談談Controller幫我們做些什麼?


我們通常會在Controller這層就將使用者的參數驗證完畢,並且依據傳入的參數,找到對應的商業邏輯去執行,並且回傳結果給使用者知道,已註冊頁面作為實際應用來解說。



接著我們來實作上述的內容,但因為只是MVC初學,所以我們還不加入分層、資料庫溝通之類比較深的內容,只專注在Controller怎麼做。


我們先建立一個UserController,專門處理User相關的服務跟邏輯



接著寫一個SignUp的Action,這邊有看到我們掛HttpGet的Attribute,只是指定呼叫動詞必須是Get的方式才會執行到這個Action,如果不知道Http動詞的話可以參考 : HTTP請求方法




而這個Action沒有任何內容,只有回傳一個View而已,接著我們來實作出回傳的頁面,在這個Action中點擊右鍵 > 新增檢視







接著直接執行,應該就能看到我們剛剛做出來的SignUp頁面


而這邊網址是 /user/signup,我覺得不夠直覺,所以到RouteConfig改一下




接著把頁面簡單做起來



寫一個Action來接收傳過來的資料

[HttpPost]
        public ActionResult SignUp(string account,string password)
        {
            bool Result = false;
            //帳號密碼都不能為空值
            if (!string.IsNullOrWhiteSpace(account) &&
                !string.IsNullOrWhiteSpace(password))
            {
                //帳號必須要有@字元
                //密碼必須大於六個字元
                if (account.Contains("@") && password.Length > 6)
                {
                    //我們判斷是否有註冊過的帳號,因為還沒有連結資料庫
                    //所以先假定steven@mymail.com被註冊過
                    if (account != "steven@mymail.com")
                    {
                        TempData["Message"] = "註冊成功!!";
                        Result = true;
                    }
                    else
                    {
                        TempData["Message"] = "帳號已經存在";
                    }
                }
                else
                {
                    TempData["Message"] = "帳號密碼不符合格式";
                }
            }
            else
            {
                TempData["Message"] = "帳號密碼不能為空值";
            }

            if (Result)
            {
                //註冊成功,導到首頁 
                return RedirectToAction("index", "home");
            }
            else
            {
                return RedirectToAction("signup", "user");
            }
        }

這邊注意到我們的Attribute就下了HttpPost,表示只有Post可以呼叫到這個Action,而我們在Form那邊也設定了方法用Post



接著你可能會注意到裡面有用到TempData,通常TempData是用來跨Action傳遞資料用的,底層其實是將資料存在Session之中,而且你只要取用過一次裡面的值就會被清掉。 而因為我們這個Action只是在處理註冊的相關邏輯,執行完後可以看到最後回傳的都是RedirectToAction,也就是導到我們指定的Action去回傳頁面,所以會跨兩個Action以上,存在TempData是個簡單的處理方式。


接著執行看看你會發現,好像一切有照著我們的邏輯在執行,但唯獨訊息不會顯示出來,因為我們雖然將TempData之中,卻沒有寫顯示訊息的那一段,通常這一段邏輯我們會放在共用的_Layout裡面。




因為還沒解說View的關係,這邊就先照著寫,之後會解說到。再次執行就會發現訊息會正確S顯示出來了。





上述的Controller撰寫方式,就是通常我們在Controller做的工作,驗證參數 、 依據使用者輸入的值執行對應的邏輯 、最後回傳結果。


但你應該也會發現,整個Action邏輯裡面光是驗證參數就佔了大半的篇幅,這往往會讓程式碼複雜度提高,閱讀變得困難,這部分我們會在下一篇講解該如何把這類的邏輯分隔出去,讓程式碼更好維護美觀一些。


另外Action其實還可以回傳很多種結果,前面範例用到了

View : 回傳頁面

RedirectToAction :回傳導頁結果

底層還支援了一些回傳方式,靈活應用就可以達成大部分的功能了。詳細參考: MSDN
圖片出處:https://msdn.microsoft.com/zh-tw/library/dd410269(v=vs.98).aspx


2018年4月24日 星期二

雜湊表(Hash Table)


學生時期學資料結構跟演算法時,每次看到厚厚的課本加上一堆用C語言寫的範例,雖然都有修過,但說真的不知道它可以拿來做什麼? 直到出了社會開始寫一些專案要調教效能時,才發現原來以前學的是這麼厲害的東西阿。


開始前想先推薦一下這本書【演算法圖鑑:26種演算法 + 7種資料結構,人工智慧、數據分析、邏輯思考的原理和應用全圖解】,作者用簡單的圖解方式帶領讀者瞭解艱澀的資料結構與演算法的歷程,雖然要實際應用在專案中還需要一些內化,但已經比我以前的課本好多了(拭淚),對這方面有興趣的非常推薦買這本書來看看


圖片出處: http://www.books.com.tw/products/0010771263


這篇要說的是雜湊表的原理跟實際上如何應用在專案中。


案例 :

公司有派送Coupon券的需求,而條件是該券不能與過往中的任何一張重複,所以在產生Coupon券代碼後,最好跟以前的做一下比對來確保沒有發生重複的情形。


而Coupon券為英數字12個字元格式,區分大小寫,比對方法想過以下幾種方式

1. 寫入的時候,用SQL語法的方式要求DB先搜尋確定沒有再寫

優點 : 寫法簡單

缺點 : 耗掉DB效能,當要寫入的筆數一多時,DB會出現效能瓶頸


2. 將全部的Coupon券撈出來後,進行比對,確定沒有再進行寫入

優點 : 因為是將資料撈出DB再從Application端做比對,所以對DB負擔較小

缺點 : 進行字串比對時該如何有效率執行是個問題,尤其是字串比對,如果處理方式不佳,也一樣會在Application端產生效能瓶頸


這邊我採取方式2並搭配雜湊表來解決,而為何要用雜湊表以下慢慢說明

產生Coupon券的程式如下

/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}


執行方式如下


如果你是寫C#的,那要比對是否有一樣的東西存在List中最快的方式就是用Any()這個方法,而我們知道List儲存方式實際是這樣



記憶體位置放置資料內容,而每個節點會記錄下一筆資料的記憶體位置在哪,所以List裡面的資料未必是一個相連的記憶體區段,但它只要知道開頭那筆資料,就可以依序將資料逐筆讀取出來。


換言之,如果要搜尋一個列表中是否有相同的資料存在,必須用線性的方式搜尋,也就是逐筆檢查,從第一筆開始每筆拿出來看看,直到比對到為止,最佳的狀況是第一筆就是你要比對的資料,最差,就是最後一筆才是你要的資料,而且搜尋的成本會隨著資料的增長而遞增。


驗證


先從1萬筆開始
int 產生的資料筆數 = 10000;

void Main()
{
 //準備要用來搜尋的資料
 var Pools = CreateSearchPool();
 
 //透過Stopwatch來看看實際搜尋要花費的時間
 Stopwatch sw = new System.Diagnostics.Stopwatch();

 Random rnd = new Random();
 
 //總共花費的時間
 double TotalTime = 0;
 
 //搜尋一百次
 for (int i = 0; i < 100; i++)
 {
  //動態從產生的資料母體中抽一筆作為我們要搜尋的目標
  var RandomIndex = rnd.Next(0, 產生的資料筆數 -1);
  
  //取出要搜尋的字
  var Target = Pools[RandomIndex];
  
  //碼表歸零
  sw.Reset();
  //碼表開始計時
  sw.Start();
  
  //透過Any方式對List做搜尋
  var Result = Pools.Any(x=> x == Target);
  
  //搜尋結束,碼錶停止
  sw.Stop();
  
  //將時間加上這次搜尋花費的時間,為毫秒
  TotalTime += sw.Elapsed.TotalMilliseconds;
 }
 
 //算出平均每次搜尋,耗費的秒數
 (TotalTime /100).Dump();
}

//建立要搜尋的母體
private List<string> CreateSearchPool() 
{
 List<string>  Pool = new List<string>();
 for (int i = 0; i < 產生的資料筆數; i++)
 {
  //動態新增Coupon資料
  var Code = CreateNewCode(12);
  //丟進我們要用來搜尋Pool
  Pool.Add(Code);
 }
 
 return Pool;
}


// 隨機產生Couopn券代碼
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}


實際算出來的平均搜尋時間為 0.083978(毫秒)


將搜尋母體放大100倍,也就是從100萬筆資料中隨機抽樣搜尋100次,結果為8.540132(毫秒)


可以觀察到搜尋效率隨著資料量的增長快速遞減

而一百萬筆對於Coupon券來說其實不多,如果你的會員數有一萬人,每個人發到第100張時,總共發出去的數量就達到這個等級了 ,相信很多店商平台所產出的Coupon數遠遠大於這個量。


用雜湊表來試試看

先說明雜湊表的概念,它跟List的最大差異是它非線性搜尋,它將所有要放入的資料先進行雜湊的方式算出一個值後,依據算出來的值放到對應的記憶體位置去,搜尋時也是先將要搜尋的值進行雜湊運算,算出對應位置,直接取出該記憶體的資料進行比對



特點是它非線性搜尋,也就是說它不需要抓到第一筆資料後,依序依據指標,往下找下一筆資料,即便不是還是要每筆遍尋才能知道結果,雜湊表的好處就在於,你要搜尋時,就已經知道該去哪找了。


而可能會有一個問題,那如果經過運算後,兩筆資料要儲存的地方一樣呢? 這時候就是發生所謂的碰撞,一張好的雜湊表理論上要盡量避免碰撞發生,但現實中難以避免,所以進階的用法就是將相同位置內再放入List來存入更多筆資料。




這邊可能會有一個疑問是,那跟我一開始用List有什麼差別 ?  如果我們相信資料是平均分佈,那雜湊結果理論上也會平均分佈,但就如前面提的,現實中實在難以避免碰撞的發生,所以即便真的發生碰撞,我們也能確定List中的資料絕對不會有很多筆,多到導致效能瓶頸的發生。


所以雜湊表的陣列該開出幾格來就是需要經過考量的,如果你有數百萬筆的資料,只開出100格,那最平均的結果就是每一格裡面會有1萬筆的資料,這顯然不理想。


驗證

一樣先從1萬筆開始


int 產生的資料筆數 = 10000;
int 雜湊表格數 = 1000;
void Main()
{
 //準備要用來搜尋的資料
 var Pools = CreateSearchPool();

 //透過Stopwatch來看看實際搜尋要花費的時間
 Stopwatch sw = new System.Diagnostics.Stopwatch();//引用stopwatch物件
 
 Random rnd = new Random();
 
 //總共花費的時間
 double TotalTime = 0;
 
 //搜尋一百次
 for (int i = 0; i < 100; i++)
 {
  //動態從產生的資料母體中抽一筆作為我們要搜尋的目標
  var RandomIndex = rnd.Next(0, 產生的資料筆數 - 1);

  //取出要搜尋的字
  var Target = AllCode[RandomIndex];
  
  //碼表歸零
  sw.Reset();
  //碼表開始計時
  sw.Start();
  
  //取得Hash後應該存放的位置
  var HashPosition = GetHashPosition(Target);
  
  //從陣列中取出該筆資料
  var PositionData = Pools[HashPosition];
  
  //如果有資料
  if (PositionData != null)
  {
   //檢查這個List是否存在相同的Coupon代碼
   var Result = PositionData.Any(x => x == Target);
  }
  //碼錶停止
  sw.Stop();
  
  //將時間加上這次搜尋花費的時間,為毫秒
  TotalTime += sw.Elapsed.TotalMilliseconds;
 }
 
 //算出平均每次搜尋,耗費的秒數
 (TotalTime / 100).Dump();
}

List<string> AllCode =new List<string>();
private List<string>[] CreateSearchPool() 
{
 List<string>[] SearchPool = new List<string>[雜湊表格數];
 for (int i = 0; i < 產生的資料筆數; i++)
 {
  var Code = CreateNewCode(12);
  AllCode.Add(Code);
  
  var p = GetHashPosition(Code);
  var PositionData = SearchPool[p];
  if (PositionData == null)
  {
   PositionData = new List<string>();
   SearchPool[p] = PositionData;
  }

  PositionData.Add(Code);
 }

 return SearchPool;
}

/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}

SHA256 sha256 = new SHA256CryptoServiceProvider();
//取得Hash後應該存放的位置
private int GetHashPosition(string code) 
{
 var ByteArray = sha256.ComputeHash(Encoding.Default.GetBytes(code));
 var IntResult = BitConverter.ToInt32(ByteArray, 0);

 //轉正
 IntResult = Math.Abs(IntResult);
        //除格子數,餘數就是這筆資料該放的位置
 return IntResult % 雜湊表格數;
}


實際算出來的平均搜尋時間為 0.004124(毫秒)



一樣將數字放大到100萬筆,實際算出來的平均搜尋時間為 0.033612(毫秒)


可以發現即便搜尋筆數擴張了100倍,效率並沒有完全等比遞減





當然各種資料型態跟搜尋狀況不同,可能適用的資料結構與演算法也會略有不同要取捨,雜湊法也並非沒有缺點,例如在製作表時比較耗時,所以適合用在資料變動不大的情境,先將表做起來後放到快取去更新維護都是一些優化的方法,以上提供給大家參考。

2018年4月4日 星期三

【MVC教學】4. 為你的Route加一點限制


上一篇寫了Route比對的邏輯,這次來點更進階的應用,讓我們幫Route比對加上一些些限制。

假設今天About的頁面,他是依據網址帶入的 ID取得對應的會員資料,回傳結果,而ID必定為數字,如果不是數字就不要進到程式碼,直接擋掉該如何做?

//我們希望的網址,最後的ID一定要為數字
/home/about/1

加上Constraints限制,而限制的方法用正規表示法來表達,以上述的只能為數字為例

constraints的部分,我們把ID限制在只能出現數字,(如果對於正規表示法不熟悉的話,推薦可以翻翻這本書,就算記不起來拿來當工具書也很實用 處理大數據的必備美工刀 - 全支援中文的正規表示法精解

接著執行網站試試看 /home/about/123

看起來沒問題,接著我們執行/home/about/Steven,這邊請記得把Default那組Route註解起來,如果還記得上篇Route比對方法的話,這組雖然會因為Steven不是數字而被About那組Route擋掉,但依然符合Default的萬用Route比對規則,而正確執行,為了測試請先註解掉Default那組。


如我們預期的,因為Steven不是數字的原因而被擋掉了,Constraints算是一個可以把Route用得更靈活的技巧,雖然需要懂的正規表示法,但我覺得這兩項學習投資很划算,正規表示法到很多地方都很萬用。


自訂更複雜的Constraints

我們再來出個更刁的要求,假設你老闆就叫Steven,而且他不希望跟別人一樣,每個人都是打ID查資料顯示太一般,他偏偏要只有輸入Steven也要能進到About頁時該怎麼辦?


當然也可以用正規表示法硬做,但可能會讓Constraint寫得很醜難維護,所以這次改用實作IRouteConstraint的方式來完成這個需求


建立一個StevenBossConstraint的Class


實作以下內容


將原本設定ID的Constrainte改成我們寫的StevenBossConstraint


執行後就會發現,數字跟Steven都可以通過Route的檢查,但你打Tom或是Tim之類的其他非數字參數,都會被擋掉




透過小工具來幫助偵錯Route設定

可以透過Nuget來安裝Route Debugger工具,他們告訴我們目前網頁之所以能夠顯示,是因為走了哪一條Route規則,這在初期我對這些設定還不熟悉時幫助非常的大




安裝這組套件



接著重新執行網站就可在看到詳細解說



相信剛開始要改Route的使用者來說,會是相當有幫助的工具喔!!