[ASP.NET Core MVC][Vue3] 統一欄位格式定義及驗證設計範例 – 後端驗證 #CH1
在開發網站系統設計階段,對於欄位格式定義 (Data Annotation) 及驗證 (Validate) 就要先設計好,讓相同欄位在不同頁面使用時,都能維持相同的格式驗證,當欄位格式有異動時,也只需要修改一個地方就好,讓所有使用到的頁面都能一致同步修改。
例如專案有一個欄位叫 ”學號”,資料庫欄位名稱為 “StudentID”,格式為數字,長度固定為 10 碼,當在設計頁面時有用到這個欄位輸入,我們會在 Model 欄位附加屬性。
如果每次都要指定格式及長度的話,就會過於重複設定了,如果未來格式有異動,那回頭檢查程式就要檢查很多地方,如果漏掉修改,就會產生 Bug 了。
而且當專案為多數人共同開發時,難免會發生有人設定錯誤或是沒設定,導致錯誤發生。
當相同欄位名稱應該有相同格式及驗證時,我建議可以統一放在資料表集中管理,當頁面使用到這欄位時,由程式自動驗證格式,減少人為疏失。
此範例環境使用 ASP.NET Core MVC 版本是 .NET6,前端使用 Vue3 框架,後端使用 Dapper 套件連接 SQL Server 2019,文末有範例可以下載。
通常在做資料的格式檢查時,會分別在前端與後端檢查,這兩者都很重要,在後端檢查是做最後的把關,避免有問題的資料寫入,而在前端檢查可以快速回應給使用者,避免過多不需要的後端傳送要求。
此範例先實作後端驗證的部份,待下一篇文章再來實作前端的欄位即時檢查。
Contents
建立專案
開啟 Visual Studio,建立新專案為「ASP.NET Core Web 應用程式 (Model-View-Controller)」。
輸入專案名稱、路徑。
架構選擇「.NET 6.0」版本,按下「建立」就會建立此專案。
引用 Vue3 套件
打開「\Views\Shared\_Layout.cshtml」,在下方引用 JavaScript 部份加上引用 Vue3。
<script src="https://unpkg.com/vue@3"></script>
停用 Json 回傳預設小寫設定
在 .NET Framework 使用 Json 回傳時,前端收到的 Json 物件大小寫設定與 ViewModel 相同,而在 .NET Core 時則預設開頭為小寫 (駝峰式命名),這裡我都會調整成與 ViewModel 大小寫相同。
在 Program.cs 加入以下語法:
1 2 3 4 5 |
// 維持 Json 回傳大小寫與 ViewModel 相同 builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); |
欄位定義資料表語法
我會建立一個資料表,來定義欄位的名稱、格式、長度及範圍,當頁面用到這欄位名稱時,就會採用資料表內的設定,以方便統一管理與修改。
建立資料表語法:
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE TABLE [dbo].[ColumnDefine]( [ColumnID] [varchar](50) NOT NULL, [ColumnName] [varchar](50) NOT NULL, [ColumnFormat] [varchar](10) NULL, [ColumnMaxLength] [smallint] NULL, [ColumnMinLength] [smallint] NULL, [ColumnRangeStart] [float] NULL, [ColumnRangeEnd] [float] NULL, CONSTRAINT [PK_ColumnDD] PRIMARY KEY CLUSTERED ( [ColumnID] ASC )) ON [PRIMARY] |
欄位 ColumnID
是專案會用到的欄位名稱,通常會和資料表內設定一樣的名稱。
欄位 ColumnName
是顯示的中文名稱,當有錯誤時就會顯示這個名稱給使用者看。
欄位 ColumnFormat
是設定要檢查的格式,可以自定名稱,在程式內會依照這個名稱檢查格式邏輯,通常可用正規表達式來檢查。
欄位 ColumnMaxLength, ColumnMinLength
是限制文字長度。
欄位 ColumnRangeStart, ColumnRangeEnd
是當格式為數字時,可以限制數字範圍的大小。
這裡我繼續新增 7 筆測試資料,分別定義不同的欄位格式及限制。
1 2 3 4 5 6 7 |
insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('StudentID','學號','NUM',10,10,null,null) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('Name','姓名',null,5,null,null,null) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('PID','身份證字號','PID',10,10,null,null) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('Marks','分數','INT',3,null,0,100) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('Email','E-Mail','EMAIL',50,null,null,null) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('Mobile','手機號碼','PHONE',10,10,null,null) insert into [dbo].[ColumnDefine]([ColumnID],[ColumnName],[ColumnFormat],[ColumnMaxLength],[ColumnMinLength],[ColumnRangeStart],[ColumnRangeEnd]) values ('CreateDate','建立日期','DATE',10,10,null,null) |
當我們在資料表內設定好格式及限制後,之後建立 ViewModel 時就不用再寫 Data Annotation 了。
設計畫面
這裡我只會展示一個表單輸入,模擬資料新增頁面,我直接修改在預設頁面「\Views\Home\Index.cshtml」,先清空原有語法,並貼上以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
<h1>統一欄位格式定義及驗證設計範例</h1> <div id="AddPanel" class="card"> <div class="card-header"> 新增 </div> <div class="card-body"> <div class="row"> <div class="col-auto"> <label for="StudentID" class="col-form-label">學號</label> </div> <div class="col-auto"> <input type="text" id="StudentID" class="form-control" v-model="addForm.StudentID"> </div> </div> <div class="row"> <div class="col-auto"> <label for="Name" class="col-form-label">姓名</label> </div> <div class="col-auto"> <input type="text" id="Name" class="form-control" v-model="addForm.Name"> </div> </div> <div class="row"> <div class="col-auto"> <label for="Name" class="col-form-label">身份證字號</label> </div> <div class="col-auto"> <input type="text" id="PID" class="form-control" v-model="addForm.PID"> </div> </div> <div class="row"> <div class="col-auto"> <label for="Marks" class="col-form-label">分數</label> </div> <div class="col-auto"> <input type="text" id="Marks" class="form-control" v-model="addForm.Marks"> </div> </div> <div class="row"> <div class="col-auto"> <label for="Email" class="col-form-label">E-Mail</label> </div> <div class="col-auto"> <input type="text" id="Email" class="form-control" v-model="addForm.Email"> </div> </div> <div class="row"> <div class="col-auto"> <label for="Mobile" class="col-form-label">手機號碼</label> </div> <div class="col-auto"> <input type="text" id="Mobile" class="form-control" v-model="addForm.Mobile"> </div> </div> <div class="row"> <div class="col-auto"> <label for="CreateDate" class="col-form-label">建立日期</label> </div> <div class="col-auto"> <input type="text" id="CreateDate" class="form-control" v-model="addForm.CreateDate"> </div> </div> </div> <div class="card-footer"> <button type="button" class="btn btn-primary" v-on:click="AddSave()">儲存</button> </div> </div> @section scripts { <script> const app = Vue.createApp({ data() { return { addForm: { StudentID: '' ,PID:'' ,Name:'' ,Marks:'' ,Email:'' ,Mobile:'' ,CreateDate:'' } } } , methods: { // 新增儲存 AddSave() { var self = this; // 組合表單資料 var postData = {}; postData['StudentID'] = self.addForm.StudentID; postData['PID'] = self.addForm.PID; postData['Name'] = self.addForm.Name; postData['Marks'] = self.addForm.Marks; postData['Email'] = self.addForm.Email; postData['Mobile'] = self.addForm.Mobile; postData['CreateDate'] = self.addForm.CreateDate; // 使用 jQuery Ajax 傳送至後端 $.ajax({ url: '@Url.Content("~/Home/AddSave")', method: 'POST', dataType: 'json', data: { inModel: postData, __RequestVerificationToken: $('@Html.AntiForgeryToken()').val() }, success: function (datas) { if (datas.ErrMsg) { alert(datas.ErrMsg); return; } alert(datas.ResultMsg); }, error: function (err) { alert(err.status + " " + err.statusText + '\n' + err.responseText); } }); } } }); const vm = app.mount('#AddPanel'); </script> } |
這頁面設計了 7 個不同格式的欄位,按下「新增」後就會將資料透過 Ajax 傳送到 Controller。
Controller 接收資料
接著打開「\Controllers\HomeController.cs」,新增 Action 來接收欄位資料。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[ValidateAntiForgeryToken] public IActionResult AddSave(Student inModel) { AddSaveOut outModel = new AddSaveOut(); // 檢查參數 if (ModelState.IsValid == false) { // 檢查失敗訊息 outModel.ErrMsg = string.Join("\n", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)); return Json(outModel); } // 省略資料庫動作... // 模擬成功訊息 outModel.ResultMsg = "新增完成"; return Json(outModel); } |
這個 Action 的重點放在驗證 ModelState.IsValid
的結果是否成功,資料庫的動作我就省略了。
設定 ViewModel 欄位
這裡在「Models」資料夾新增一個 ViewModel 取名為「HomeViewModel」,然後在類別中加入前端要傳入的欄位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class HomeViewModel { public class Student : ModelBase { public string? StudentID { get; set; } public string? PID { get; set; } public string? Name { get; set; } public string? Marks { get; set; } public string? Email { get; set; } public string? Mobile { get; set; } public string? CreateDate { get; set; } } public class AddSaveOut { public string? ErrMsg { get; set; } public string? ResultMsg { get; set; } } } |
Student 類別的 7 個欄位跟前端 View 設定的欄位名稱是一樣的,但這裡我並沒有指定欄位的名稱、格式、長度等限制,因為格式統一由資料表內設定,這樣就可以做到統一集中管理。
如果要必填的需求,可以再自行增加 [Required] 設定。
Student
類別我繼承了 ModelBase
,這是一個新類別,目的是統一驗證欄位格式,當要檢查欄位格式時,就會寫在 ModelBase 裡面。
我有在專案內新增一個資料夾名稱為「ProjectClass」,將 ModelBase
新類別放在 ProjectClass 裡面。
在 ModelBase.cs 類別裡面貼上以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
public class ModelBase : IValidatableObject { /// <summary> /// 統一檢查欄位格式 /// </summary> /// <param name="validationContext"></param> /// <returns></returns> public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { // 取得資料庫 DD 設定 List<ColumnDefine> ColumnDDs = ColumnDefine.GetList(); StringBuilder sbMsg = new StringBuilder(); PropertyInfo[] propsM = this.GetType().GetProperties(); for (int l = 0; l < propsM.Length; l++) { PropertyInfo pInfo = propsM[l]; Type type = pInfo.PropertyType; if (pInfo.GetValue(this) != null && pInfo.GetValue(this).ToString() != "") { string value = pInfo.GetValue(this).ToString(); ColumnDefine dd = ColumnDDs.Where(w => w.ColumnID.ToString() == pInfo.Name).FirstOrDefault(); if (dd != null) { // 自定名稱 string columnName = dd.ColumnName.ToString(); DisplayAttribute display = (DisplayAttribute)pInfo.GetCustomAttributes(typeof(DisplayAttribute), true).SingleOrDefault(); if (display != null) { columnName = display.Name; } DisplayNameAttribute displayName = (DisplayNameAttribute)pInfo.GetCustomAttributes(typeof(DisplayNameAttribute), true).SingleOrDefault(); if (displayName != null) { columnName = displayName.DisplayName; } if (dd.ColumnFormat != null) { // 格式檢查 switch (dd.ColumnFormat.ToString()) { case "INT": if (FormatCheckUtil.IsInt(value) == false) { sbMsg.AppendLine($"[{columnName}] 整數格式錯誤"); } break; case "NUM": if (FormatCheckUtil.IsNum(value) == false) { sbMsg.AppendLine($"[{columnName}] 數字格式錯誤"); } break; case "EMAIL": if (FormatCheckUtil.IsEmail(value) == false) { sbMsg.AppendLine($"[{columnName}] E-Mail 格式錯誤"); } break; case "PHONE": if (FormatCheckUtil.IsPhone(value) == false) { sbMsg.AppendLine($"[{columnName}] 手機格式錯誤"); } break; case "PID": if (FormatCheckUtil.IsPID(value) == false) { sbMsg.AppendLine($"[{columnName}] 身份證字號格式錯誤"); } break; case "DATE": if (FormatCheckUtil.IsDate(value) == false) { sbMsg.AppendLine($"[{columnName}] 日期格式錯誤"); } break; } } // 長度檢查 if (dd.ColumnMaxLength != null) { if (value.ToString().Length > Convert.ToInt32(dd.ColumnMaxLength)) { sbMsg.AppendLine($"[{columnName}] 長度最多 {dd.ColumnMaxLength} 字元"); } } if (dd.ColumnMinLength != null) { if (value.ToString().Length < Convert.ToInt32(dd.ColumnMinLength)) { sbMsg.AppendLine($"[{columnName}] 長度最少 {dd.ColumnMaxLength} 字元"); } } //數字範圍檢查 double d; if (dd.ColumnRangeStart != null && double.TryParse(value, out d)) { if (double.TryParse(value, out d)) { if (Convert.ToDouble(value.ToString()) < Convert.ToDouble(dd.ColumnRangeStart)) { sbMsg.AppendLine($"[{columnName}] 數字範圍最小為 {dd.ColumnRangeStart}"); } } } if (dd.ColumnRangeEnd != null) { if (double.TryParse(value, out d)) { if (Convert.ToDouble(value.ToString()) > Convert.ToDouble(dd.ColumnRangeEnd)) { sbMsg.AppendLine($"[{columnName}] 數字範圍最大為 {dd.ColumnRangeEnd}"); } } } } } } if (sbMsg.Length > 0) { yield return new ValidationResult(sbMsg.ToString()); } } } |
我所繼承的 IValidatableObject 介面,會在驗證時觸發 Validate
事件,我統一在 Validate
事件內讀取資料表欄位設定,並寫了一些格式檢查範例,你可以自行增加一些格式檢查,其格式名稱只要與資料表欄位 ColumnFormat
相同就行。
新增欄位定義類別
這裡我新增一個新類別 ColumnDefine
,一樣放在 ProjectClass 底下,目的為取得資料庫內的設定。
在 ColumnDefine.cs 加入以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
using Dapper; using Microsoft.Extensions.Caching.Memory; using System.Data.SqlClient; namespace CoreMVCColumnDefineValidate.ProjectClass { public class ColumnDefine { static MemoryCache cache = new MemoryCache(new MemoryCacheOptions()); public object ColumnID { get; set; } public object ColumnName { get; set; } public object ColumnFormat { get; set; } public object ColumnMaxLength { get; set; } public object ColumnMinLength { get; set; } public object ColumnRangeStart { get; set; } public object ColumnRangeEnd { get; set; } /// <summary> /// 取得資料庫定義 /// </summary> /// <returns></returns> public static List<ColumnDefine> GetList() { List<ColumnDefine> list = null; // 使用 Cache 暫存在記憶體內 if (!cache.TryGetValue("ColumnDefine", out list)) { // 不存在 Cache 時才從資料庫內取得設定 IConfiguration conf = (new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build()); string connStr = conf.GetConnectionString("SqlServer"); using (var cn = new SqlConnection(connStr)) { string sql = "SELECT * FROM ColumnDefine"; list = (List<ColumnDefine>)cn.Query<ColumnDefine>(sql); } // 寫入 Cache MemoryCacheEntryOptions policy = new MemoryCacheEntryOptions(); // 多久時間沒有使用 cache 就回收 policy.SlidingExpiration = TimeSpan.FromHours(1);// 一小時 cache.Set("ColumnDefine", list, policy); } return list; } } } |
這類別的目的就是取得資料表欄位設定,可是因為太頻繁使用,而且讀取的內容都一樣,所以這裡我加上了 Cache 機制,將讀到的內容先暫存在記憶體內,當一段時間沒人使用時才會釋放,這樣可以提高效能。
新增欄位檢查類別
這裡我將常用的欄位格式檢查邏輯抽離至新類別 FormatCheckUtil 裡面,一樣放在 ProjectClass 底下。
在 FormatCheckUtil 加入以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
public class FormatCheckUtil { /// <summary> /// 檢查格式 - 整數 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsInt(string value) { return int.TryParse(value, out _); } /// <summary> /// 檢查格式 - 數字 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsNum(string value) { return double.TryParse(value, out _); } /// <summary> /// 檢查格式 - 英文&正整數 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsEngAndInt(string value) { if (string.IsNullOrEmpty(value)) { return false; } return Regex.IsMatch(value, @"^[A-Za-z0-9]+$"); } /// <summary> /// 檢查格式 - Email /// </summary> /// <param name="email"></param> /// <returns></returns> public static bool IsEmail(string value) { try { var addr = new System.Net.Mail.MailAddress(value); return addr.Address.ToUpper() == value.ToUpper(); } catch { return false; } } /// <summary> /// 檢查格式 - 手機 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsPhone(string value) { if (string.IsNullOrEmpty(value)) { return false; } return Regex.IsMatch(value, @"^09[0-9]{8}$");//規則:09開頭,後面接著8個0~9的數字 } /// <summary> /// 檢查格式 - 市話 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsTel(string value) { if (string.IsNullOrEmpty(value)) { return false; } return Regex.IsMatch(value, @"^(\d{3,4}-)?\d{6,8}$"); } /// <summary> /// 檢查格式 - 身份證 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsPID(string value) { if (string.IsNullOrEmpty(value)) { return false; } bool flag = Regex.IsMatch(value.ToUpper(), @"^[A-Z]{1}[1-2]{1}[0-9]{8}$");//先判定是否符合一個大寫字母+1或2開頭的1個數字+8個數字 if (flag)//如果符合第一層格式 { int Esum = 0; int Nsum = 0; int count = 0; string[] country = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "W", "Z", "I", "O" }; for (int index = 0; index < country.Length; index++) { if (value.ToUpper().Substring(0, 1) == country[index]) { index += 10;//A是從10開始編碼,每個英文的碼都跟index差異10,先加回來 Esum = (((index % 10) * 9) + (index / 10)); //英文轉成的數字, 個位數(把數字/10取餘數)乘9再加上十位數 //加上十位數(數字/10,因為是int,後面會直接捨去) break; } } for (int i = 1; i < 9; i++) {//從第二個數字開始跑,每個數字*相對應權重 Nsum += (Convert.ToInt32(value[i].ToString())) * (9 - i); } count = 10 - ((Esum + Nsum) % 10);//把上述的總和加起來,取餘數後,10-該餘數為檢查碼,要等於最後一個數字 if (count == Convert.ToInt32(value[9].ToString()))//判斷計算總和是不是等於檢查碼 { return true; } } return false; } /// <summary> /// 檢查格式 - IP /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsIP(string value) { if (string.IsNullOrEmpty(value)) { return false; } return Regex.IsMatch(value, @"(\d{1,2}|1 \d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])"); } /// <summary> /// 檢查格式 - 日期 /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsDate(string value) { value = value.Replace("/", "").Replace("-", ""); if (!string.IsNullOrEmpty(value)) { if ((value.Length != 8) && (value.Length != 7)) { return false; } try { int year = (value.Length == 8) ? Convert.ToInt32(value.Substring(0, 4)) : (Convert.ToInt32(value.Substring(0, 3)) + 0x777); int month = (value.Length == 8) ? Convert.ToInt32(value.Substring(4, 2)) : Convert.ToInt32(value.Substring(3, 2)); int day = (value.Length == 8) ? Convert.ToInt32(value.Substring(6, 2)) : Convert.ToInt32(value.Substring(5, 2)); if ((month < 1) || (month > 12)) { return false; } if ((day < 1) || (day > 0x1f)) { return false; } if ((((month == 4) || (month == 6)) || ((month == 9) || (month == 11))) && (day == 0x1f)) { return false; } if (month == 2) { bool isleap = ((year % 4) == 0) && (((year % 100) != 0) || ((year % 400) == 0)); if ((day > 0x1d) || ((day == 0x1d) && !isleap)) { return false; } } } catch { return false; } } return true; } } |
讀取 appsettings.json
我將資料庫連線放在 appsettings.json 裡面,打開 appsettings.json 後,加入以下連線字串。
1 2 3 |
"ConnectionStrings": { "SqlServer": "Data Source=127.0.0.1;Initial Catalog=Teach;Persist Security Info=false;User ID=test;Password=test;" } |
安裝 Dapper
我資料庫讀取物件使用微型 ORM 套件 Dapper,需要安裝 Dapper 才能使用。
執行「相依性 > 管理 NuGet 套件」。
搜尋「Dapper」,安裝此套件。
測試驗證
寫到這裡已經完成了後端的檢查,可以按 F5 測試一下專案。
我故意輸入一些錯誤資料,送出後在後端就全部檢查,將所有的錯誤訊息一次顯示。
當有錯誤的欄位,也會帶出中文名稱。
抽離 ModelState.IsValid 驗證
我在 Action 開頭所寫的語法 ModelState.IsValid
它其實是一段固定的語法,在每個 Action 執行前都需要先驗證,因此為求簡化語法,我們可以將此檢查邏輯使用剖面導向程式設計 (AOP) 方式抽離出來,讓 Action 專注在業務邏輯就好。
我在 ProjectClass 底下新增新類別 FilterValidateModel,並繼承 ActionFilterAttribute
,然後實作 OnActionExecuting
事件,就可以在 Action 執行前先驗證我們的 Model 輸入內容。
在 FilterValidateModel.cs 加入以下語法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class FilterValidateModel : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext context) { if (context.ModelState.IsValid == false) { string ErrMsg = string.Join("\n", context.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)); context.Result = new BadRequestObjectResult(ErrMsg); } base.OnActionExecuting(context); } } |
FilterValidateModel 是一個附加屬性類別,可以在需要時附加在 Action 上面,加上 [FilterValidateModel]
。
也可以將此附加屬性加入全域註冊,讓所有的 Action 執行前都先執行 Model 驗證。
打開 Program.cs,在 Services 註冊全域屬性:
1 2 3 4 5 6 7 |
// 全域註冊附加屬性驗證 builder.Services.AddControllersWithViews( options => { options.Filters.Add<FilterValidateModel>(); } ); |
註冊之後,就不用在 Action 上附加屬性 [FilterValidateModel]
了。
以上我的教學並非是主流方式,而是我個人發展出來的專案架構,幫助想要達成統一驗證的人一種思考方式,如果此做法有盲點或是 Bug,歡迎指教,謝謝。
範例下載
下一篇教學文章
相關學習文章
- [ASP.NET Core MVC + Vue3 + Dapper] 前後台網站公告範例 – 後台查詢頁面教學 #CH1
- [ASP.NET MVC] 使用 AOP 驗證系統功能執行權限
- [ASP.NET Core MVC] 使用一般 Controller 建立 Web Api 與實作 Client 端呼叫
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝