@elf-express/admin-net-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +83 -88
- package/knowledge/Furion_Teaching_Manual/04-1-/351/205/215/347/275/256.md +442 -0
- package/knowledge/Furion_Teaching_Manual/04-2-/351/201/270/351/240/205.md +363 -0
- package/knowledge/Furion_Teaching_Manual/05-1-/345/213/225/346/205/213WebAPI.md +825 -0
- package/knowledge/Furion_Teaching_Manual/05-2-HttpContext.md +217 -0
- package/knowledge/Furion_Teaching_Manual/05-3-/347/257/251/351/201/270/345/231/250/346/224/224/346/210/252/345/231/250AOP.md +581 -0
- package/knowledge/Furion_Teaching_Manual/05-4-/350/253/213/346/261/202/347/250/275/346/240/270/346/227/245/350/252/214.md +129 -0
- package/knowledge/Furion_Teaching_Manual/05-5-/344/270/255/344/273/213/350/273/237/351/253/224Middleware.md +328 -0
- package/knowledge/Furion_Teaching_Manual/05-6-Vue-React-Angular/344/273/213/351/235/242/344/273/243/347/220/206.md +317 -0
- package/knowledge/Furion_Teaching_Manual/06-1/350/246/217/347/257/204/345/214/226/346/216/245/345/217/243.md +1458 -0
- package/knowledge/Furion_Teaching_Manual/06-2/347/254/254/344/270/211/346/226/271API_Scalar.md +91 -0
- package/knowledge/Furion_Teaching_Manual/07-/345/217/213/345/245/275/344/276/213/345/244/226/350/231/225/347/220/206.md +511 -0
- package/knowledge/Furion_Teaching_Manual/08-1-/350/263/207/346/226/231/351/251/227/350/255/211/345/237/272/347/244/216/344/275/277/347/224/250.md +587 -0
- package/knowledge/Furion_Teaching_Manual/10-1-SqlSugar/346/225/264/345/220/210.md +336 -0
- package/knowledge/Furion_Teaching_Manual/11-SaaS /345/244/232/347/247/237/346/210/266/347/255/206/350/250/230.md" +271 -0
- package/knowledge/Furion_Teaching_Manual/12-furion-dependency-injection.md +408 -0
- package/knowledge/Furion_Teaching_Manual/13-/347/211/251/344/273/266/350/263/207/346/226/231/346/230/240/345/260/204/357/274/210Mapster/357/274/211.md +162 -0
- package/knowledge/Furion_Teaching_Manual/14-/345/210/206/345/270/203/345/274/217/347/274/223/345/255/230.md +311 -0
- package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md +832 -0
- package/knowledge/Furion_Teaching_Manual/17-/346/252/242/350/246/226/347/257/204/346/234/254/345/274/225/346/223/216.md +327 -0
- package/knowledge/Furion_Teaching_Manual/18-/346/227/245/350/252/214/350/250/230/351/214/204.md +639 -0
- package/knowledge/Furion_Teaching_Manual/19-1-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/237/272/347/244/216/344/275/277/347/224/250.md +621 -0
- package/knowledge/Furion_Teaching_Manual/19-2-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/351/200/262/351/232/216/346/214/207/345/215/227.md +928 -0
- package/knowledge/Furion_Teaching_Manual/19-3-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/270/270/350/246/213/345/225/217/351/241/214.md +362 -0
- package/knowledge/Furion_Teaching_Manual/20-/350/263/207/346/226/231/345/212/240/350/247/243/345/257/206.md +286 -0
- package/knowledge/Furion_Teaching_Manual/20-/351/231/204/351/214/204-KSort/350/263/207/346/226/231/347/260/275/345/220/215/345/256/214/346/225/264/345/216/237/345/247/213/347/242/274.md +305 -0
- package/knowledge/Furion_Teaching_Manual/21-/345/205/250/347/220/203/345/214/226/345/222/214/346/234/254/345/234/260/345/214/226.md +342 -0
- package/knowledge/Furion_Teaching_Manual/22-/344/272/213/344/273/266/345/214/257/346/265/201/346/216/222EventBus.md +448 -0
- package/knowledge/Furion_Teaching_Manual/23-JSON/345/272/217/345/210/227/345/214/226.md +340 -0
- package/knowledge/Furion_Teaching_Manual/24-/345/215/263/346/231/202/351/200/232/350/250/212SignalR.md +247 -0
- package/knowledge/Furion_Teaching_Manual/25-/350/274/224/345/212/251/350/247/222/350/211/262/346/234/215/345/213/231WorkerService.md +295 -0
- package/knowledge/Furion_Teaching_Manual/26-1-/346/216/222/347/250/213/344/275/234/346/245/255/345/256/232/346/231/202/344/273/273/345/213/231.md +631 -0
- package/knowledge/Furion_Teaching_Manual/26-2-Cron/350/241/250/351/201/224/345/274/217.md +203 -0
- package/knowledge/Furion_Teaching_Manual/26-3-/344/273/273/345/213/231/344/275/207/345/210/227TaskQueue.md +215 -0
- package/knowledge/Furion_Teaching_Manual/27-/345/210/206/346/225/243/345/274/217ID/347/224/237/346/210/220.md +86 -0
- package/knowledge/Furion_Teaching_Manual/28-/346/250/241/347/265/204/345/214/226/351/226/213/347/231/274.md +68 -0
- package/knowledge/Furion_Teaching_Manual/29-/346/265/201/350/256/212/347/211/251/344/273/266Clay.md +313 -0
- package/knowledge/Furion_Teaching_Manual/30-/350/204/253/346/225/217/350/231/225/347/220/206/357/274/210Sensitive Detection).md" +168 -0
- package/knowledge/Furion_Teaching_Manual/32-/346/234/203/350/251/261/345/222/214/347/213/200/346/205/213/347/256/241/347/220/206.md +147 -0
- package/knowledge/Furion_Teaching_Manual/33-IPC/347/250/213/345/272/217/351/200/232/350/250/212.md +98 -0
- package/knowledge/Furion_Teaching_Manual/34-2-Docker/351/203/250/347/275/262.md +313 -0
- package/knowledge/Furion_Teaching_Manual/34-3-Nginx/351/203/250/347/275/262.md +157 -0
- package/knowledge/Furion_Teaching_Manual/34-8-WindowsService/351/203/250/347/275/262.md +112 -0
- package/knowledge/Furion_Teaching_Manual/35-1-Docker/347/222/260/345/242/203/346/214/201/347/272/214/351/203/250/347/275/262Jenkins.md +169 -0
- package/knowledge/Furion_Teaching_Manual/36-1-/345/226/256/345/205/203/346/270/254/350/251/246/346/225/264/345/220/210/346/270/254/350/251/246.md +275 -0
- package/knowledge/Furion_Teaching_Manual/36-3-/345/237/272/346/272/226/346/270/254/350/251/246Benchmarking.md +80 -0
- package/knowledge/attributes.md +153 -0
- package/knowledge/config.md +147 -0
- package/knowledge/entity.md +115 -0
- package/knowledge/event.md +124 -0
- package/knowledge/plugin.md +136 -0
- package/knowledge/service.md +146 -0
- package/knowledge/sqlsugar.md +172 -0
- package/knowledge/vue-typescript.md +338 -0
- package/package.json +3 -2
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
# 8.Ⅰ 資料驗證 — 基礎使用
|
|
2
|
+
|
|
3
|
+
> **版本說明**:以下內容僅適用於 Furion 4.9.8+ 版本(採用 Cordon 資料驗證),且不支援 .NET8 以下版本。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 8.1 資料驗證概述
|
|
8
|
+
|
|
9
|
+
資料驗證是指在程式執行過程中,對輸入或傳輸的資料進行合法性與完整性檢查,以確保其符合業務規則和系統預期,防止無效、惡意或格式錯誤的資料進入核心邏輯,保障應用的穩定性與安全性。
|
|
10
|
+
|
|
11
|
+
### 8.1.1 應用場景
|
|
12
|
+
|
|
13
|
+
Web API 請求校驗、使用者註冊與登入校驗、表單提交驗證、領域模型狀態校驗、微服務資料契約校驗、批次資料匯入校驗、設定參數校驗、多語言錯誤提示、動態規則校驗、非同步唯一性校驗等。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 8.2 快速入門
|
|
18
|
+
|
|
19
|
+
> **安裝包說明**:Furion 框架已內建該功能,無需額外安裝 NuGet 套件。若使用非 Furion 框架,可安裝 `Cordon`(任何 .NET/C# 應用)或 `Cordon.AspNetCore`(Web 應用)。
|
|
20
|
+
|
|
21
|
+
### 8.2.1 傳統驗證方式
|
|
22
|
+
|
|
23
|
+
將校驗邏輯直接寫在業務方法中,會導致職責混雜、重複程式碼、維護困難:
|
|
24
|
+
|
|
25
|
+
```csharp
|
|
26
|
+
[HttpPost]
|
|
27
|
+
public async Task<bool> Register(User user)
|
|
28
|
+
{
|
|
29
|
+
if (string.IsNullOrWhiteSpace(user.Name))
|
|
30
|
+
throw new ArgumentException("姓名不能為空");
|
|
31
|
+
if (user.Name.Length < 2)
|
|
32
|
+
throw new ArgumentException("姓名不能少於 2 個字元");
|
|
33
|
+
if (user.Age < 18)
|
|
34
|
+
throw new ArgumentException("年齡不能小於 18 歲");
|
|
35
|
+
// ...更多校驗
|
|
36
|
+
|
|
37
|
+
await _repository.InsertAsync(user);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 8.2.2 基於特性的驗證
|
|
43
|
+
|
|
44
|
+
將驗證規則直接繫結到模型本身,透過 `ValidationAttribute` 衍生特性以宣告式定義校驗規則:
|
|
45
|
+
|
|
46
|
+
```csharp
|
|
47
|
+
public class User
|
|
48
|
+
{
|
|
49
|
+
[Required(ErrorMessage = "姓名不能為空"), MinLength(2, ErrorMessage = "姓名不能少於 2 個字元")]
|
|
50
|
+
public string? Name { get; set; }
|
|
51
|
+
|
|
52
|
+
[Min(18, ErrorMessage = "年齡不能小於 18 歲")]
|
|
53
|
+
public int Age { get; set; }
|
|
54
|
+
|
|
55
|
+
[Required(ErrorMessage = "密碼不能為空"), Compare(nameof(ConfirmPassword), ErrorMessage = "兩次輸入的密碼不一致")]
|
|
56
|
+
public string? Password { get; set; }
|
|
57
|
+
|
|
58
|
+
public string? ConfirmPassword { get; set; }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
業務方法不再需要任何手動校驗:
|
|
63
|
+
|
|
64
|
+
```csharp
|
|
65
|
+
[HttpPost]
|
|
66
|
+
public async Task<bool> Register(User user)
|
|
67
|
+
{
|
|
68
|
+
await _repository.InsertAsync(user);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
優勢:職責分離、消除重複、集中管理。
|
|
74
|
+
|
|
75
|
+
### 8.2.3 動態驗證(自訂驗證)
|
|
76
|
+
|
|
77
|
+
當驗證邏輯依賴執行時動態資料時,可透過以下三種方式實現。
|
|
78
|
+
|
|
79
|
+
**方式一:使用 `[CustomValidation]` 特性**
|
|
80
|
+
|
|
81
|
+
```csharp
|
|
82
|
+
public static class CustomValidators
|
|
83
|
+
{
|
|
84
|
+
private static readonly string[] _allowedDomains = ["@outlook.com", "@qq.com", "@163.com"];
|
|
85
|
+
|
|
86
|
+
public static ValidationResult? AllowedEmailDomains(object? value, ValidationContext validationContext)
|
|
87
|
+
{
|
|
88
|
+
if (value is null) return ValidationResult.Success;
|
|
89
|
+
if (value is not string email) return new ValidationResult("不是有效的郵箱格式。");
|
|
90
|
+
if (!_allowedDomains.Any(domain => email.EndsWith(domain, StringComparison.OrdinalIgnoreCase)))
|
|
91
|
+
return new ValidationResult("僅支援 outlook、qq 和 163 郵箱格式。");
|
|
92
|
+
return ValidationResult.Success;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 在模型中應用
|
|
97
|
+
public class User
|
|
98
|
+
{
|
|
99
|
+
[Required, EmailAddress]
|
|
100
|
+
[CustomValidation(typeof(CustomValidators), nameof(CustomValidators.AllowedEmailDomains))]
|
|
101
|
+
public string? EmailAddress { get; set; }
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**方式二:自訂驗證特性**
|
|
106
|
+
|
|
107
|
+
繼承 `ValidationAttribute` 並重寫 `IsValid` 方法,可在其中透過 `validationContext.GetService(serviceType)` 解析相依性注入服務。
|
|
108
|
+
|
|
109
|
+
**方式三:實現 `IValidatableObject` 介面**
|
|
110
|
+
|
|
111
|
+
```csharp
|
|
112
|
+
public class User : IValidatableObject
|
|
113
|
+
{
|
|
114
|
+
[Required, EmailAddress]
|
|
115
|
+
public string? EmailAddress { get; set; }
|
|
116
|
+
|
|
117
|
+
private readonly string[] _allowedDomains = ["@outlook.com", "@qq.com", "@163.com"];
|
|
118
|
+
|
|
119
|
+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
120
|
+
{
|
|
121
|
+
if (EmailAddress is not null && !_allowedDomains.Any(domain => EmailAddress.EndsWith(domain, StringComparison.OrdinalIgnoreCase)))
|
|
122
|
+
yield return new ValidationResult("僅支援 outlook、qq 和 163 郵箱格式。", [nameof(EmailAddress)]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 8.2.4 鏈式驗證 ✨
|
|
128
|
+
|
|
129
|
+
框架提供鏈式驗證機制,允許在 `IValidatableObject.Validate` 方法內部以宣告式、強型別的方式建構驗證規則:
|
|
130
|
+
|
|
131
|
+
```csharp
|
|
132
|
+
public class User : IValidatableObject
|
|
133
|
+
{
|
|
134
|
+
[Required, EmailAddress]
|
|
135
|
+
public string? EmailAddress { get; set; }
|
|
136
|
+
|
|
137
|
+
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
138
|
+
{
|
|
139
|
+
return validationContext.With<User>()
|
|
140
|
+
.RuleFor(u => u.EmailAddress)
|
|
141
|
+
.MustAny(["@outlook.com", "@qq.com", "@163.com"],
|
|
142
|
+
(email, domain) => email.EndsWith(domain, StringComparison.OrdinalIgnoreCase))
|
|
143
|
+
.WithMessage("僅支援 outlook、qq 和 163 郵箱格式。")
|
|
144
|
+
.ToResults();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**獨立驗證器類別**(適用於複雜、需複用或第三方類型):
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
public class UserValidator : AbstractValidator<User>
|
|
153
|
+
{
|
|
154
|
+
public UserValidator()
|
|
155
|
+
{
|
|
156
|
+
RuleFor(u => u.Name)
|
|
157
|
+
.Required().WithMessage("姓名不能為空")
|
|
158
|
+
.MinLength(2).WithMessage("姓名不能少於 2 個字元");
|
|
159
|
+
|
|
160
|
+
RuleFor(u => u.Age)
|
|
161
|
+
.Min(18).WithMessage("年齡不能小於 18 歲");
|
|
162
|
+
|
|
163
|
+
RuleFor(u => u.Password)
|
|
164
|
+
.Required().WithMessage("密碼不能為空")
|
|
165
|
+
.Compare(u => u.ConfirmPassword).WithMessage("兩次輸入的密碼不一致");
|
|
166
|
+
|
|
167
|
+
RuleFor(u => u.EmailAddress)
|
|
168
|
+
.Required().EmailAddress()
|
|
169
|
+
.MustAny(["@outlook.com", "@qq.com", "@163.com"], CheckEmailAddress)
|
|
170
|
+
.WithMessage("僅支援 outlook、qq 和 163 郵箱格式。");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private static bool CheckEmailAddress(string email, string domain)
|
|
174
|
+
=> email.EndsWith(domain, StringComparison.OrdinalIgnoreCase);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**使用方式:**
|
|
179
|
+
|
|
180
|
+
- 在模型中透過 `IValidatableObject` 整合:`validationContext.ValidateWith<UserValidator>()`
|
|
181
|
+
- 手動實例化呼叫:`new UserValidator().Validate(user)`
|
|
182
|
+
- 透過相依性注入在服務中使用
|
|
183
|
+
- 透過特性繫結:`[ValidateWith<UserValidator>]`
|
|
184
|
+
- 驗證器建構函式支援相依性注入(如 `IStringLocalizer<T>`)
|
|
185
|
+
|
|
186
|
+
**啟用相依性注入服務支援:**
|
|
187
|
+
|
|
188
|
+
```csharp
|
|
189
|
+
services.AddValidationCore(builder =>
|
|
190
|
+
{
|
|
191
|
+
builder.AddValidator(typeof(UserValidator));
|
|
192
|
+
// 或掃描程式集自動註冊
|
|
193
|
+
// builder.AddValidatorsFromAssemblies([assembly1, assembly2, ..]);
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 8.2.5 巢狀物件與集合驗證
|
|
198
|
+
|
|
199
|
+
```csharp
|
|
200
|
+
// 聚合根集中設定
|
|
201
|
+
validationContext.With<User>()
|
|
202
|
+
.RuleFor(u => u.Name).Required().MinLength(3)
|
|
203
|
+
.RuleFor(u => u.Age).Age(true)
|
|
204
|
+
.RuleFor(u => u.Address).Required().ChildRules(addr =>
|
|
205
|
+
{
|
|
206
|
+
addr.RuleFor(a => a.City).Required()
|
|
207
|
+
.RuleFor(a => a.Street).Required();
|
|
208
|
+
})
|
|
209
|
+
.RuleForCollection(u => u.WorkExperiences).MaxLength(5).ChildRules(exp =>
|
|
210
|
+
{
|
|
211
|
+
exp.RuleFor(e => e.Company).Required()
|
|
212
|
+
.RuleFor(e => e.Position).Required()
|
|
213
|
+
.RuleFor(e => e.Years).Range(1900, 2026);
|
|
214
|
+
})
|
|
215
|
+
.RuleForCollection(u => u.Hobbies).EachRules(h => h.Required().MinLength(2))
|
|
216
|
+
.ToResults();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> **推薦實踐**:採用「模型自治」策略——每個模型負責自身驗證邏輯,聚合根僅關注結構層面的約束(如非空性、集合長度),不侵入子物件內部細節。
|
|
220
|
+
|
|
221
|
+
### 8.2.6 主動驗證與相依性注入
|
|
222
|
+
|
|
223
|
+
**方式一:透過 `IValidationService`(推薦)**
|
|
224
|
+
|
|
225
|
+
```csharp
|
|
226
|
+
services.AddValidationCore();
|
|
227
|
+
|
|
228
|
+
// 注入後使用
|
|
229
|
+
var isValid = validationService.IsValid(user);
|
|
230
|
+
var validationResults = validationService.GetValidationResults(user);
|
|
231
|
+
validationService.Validate(user); // 失敗時拋出 ValidationException
|
|
232
|
+
|
|
233
|
+
// 多模型驗證
|
|
234
|
+
validationService.Validate([user, order]);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**方式二:在非相依性注入環境中使用**
|
|
238
|
+
|
|
239
|
+
```csharp
|
|
240
|
+
var validationService = Validators.Service(); // 靜態工廠
|
|
241
|
+
// 或 new ValidationService()
|
|
242
|
+
// 或 Validators.AttributeObject()
|
|
243
|
+
// 或使用 .NET 內建 Validator.TryValidateObject()
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 8.2.7 單值鏈式驗證
|
|
247
|
+
|
|
248
|
+
```csharp
|
|
249
|
+
var validator = Validators.Value<string>()
|
|
250
|
+
.Required()
|
|
251
|
+
.EmailAddress().WithMessage("這不是一個有效的電子郵箱")
|
|
252
|
+
.MinLength(10)
|
|
253
|
+
.WithName("Value");
|
|
254
|
+
|
|
255
|
+
var isValid = validator.IsValid(value);
|
|
256
|
+
validator.Validate(value);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
也可以封裝為獨立的值驗證器類別:
|
|
260
|
+
|
|
261
|
+
```csharp
|
|
262
|
+
public class EmailValidator : AbstractValueValidator<string>
|
|
263
|
+
{
|
|
264
|
+
public EmailValidator()
|
|
265
|
+
{
|
|
266
|
+
Rule().Required().EmailAddress().MinLength(10).WithName("Value");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
在控制器參數上使用:
|
|
272
|
+
|
|
273
|
+
```csharp
|
|
274
|
+
[HttpPost]
|
|
275
|
+
public void UpdateName([ValidateWith<NameValidator>] string name) { }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 8.2.8 在 ASP.NET 應用中使用
|
|
279
|
+
|
|
280
|
+
```csharp
|
|
281
|
+
services.AddControllers()
|
|
282
|
+
.AddValidationCore(); // 或 .AddValidationCore(builder => {});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
透過 `[ValidationOptions]` 特性指定執行時驗證選項(如規則集):
|
|
286
|
+
|
|
287
|
+
```csharp
|
|
288
|
+
[ApiController]
|
|
289
|
+
[Route("[controller]/[action]")]
|
|
290
|
+
[ValidationOptions(["*"])] // 控制器層級
|
|
291
|
+
public class UserController
|
|
292
|
+
{
|
|
293
|
+
[HttpPost]
|
|
294
|
+
[ValidationOptions(["modify", "set-password"])] // 操作方法層級(覆蓋控制器設定)
|
|
295
|
+
public Task Update(User user) => Task.CompletedTask;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 8.2.9 規則集(場景化驗證)
|
|
300
|
+
|
|
301
|
+
同一模型在不同場景下執行不同的驗證邏輯:
|
|
302
|
+
|
|
303
|
+
```csharp
|
|
304
|
+
public class UserValidator : AbstractValidator<User>
|
|
305
|
+
{
|
|
306
|
+
public UserValidator()
|
|
307
|
+
{
|
|
308
|
+
// 預設規則集(萬用字元 '*')
|
|
309
|
+
RuleFor(u => u.Name).Required().MinLength(3).UserName();
|
|
310
|
+
RuleFor(u => u.EmailAddress).Required().EmailAddress();
|
|
311
|
+
|
|
312
|
+
// 密碼設定規則集
|
|
313
|
+
RuleSet("set-password", () =>
|
|
314
|
+
{
|
|
315
|
+
RuleFor(u => u.Password).Required().StrongPassword().Compare(u => u.ConfirmPassword);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// 修改密碼規則集
|
|
319
|
+
RuleSet("modify-password", () =>
|
|
320
|
+
{
|
|
321
|
+
RuleFor(u => u.OldPassword).Required().StrongPassword();
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
規則集組合範例:
|
|
328
|
+
|
|
329
|
+
| 場景 | 規則集 |
|
|
330
|
+
|------|--------|
|
|
331
|
+
| 使用者註冊 | `["*", "set-password"]` |
|
|
332
|
+
| 修改密碼 | `["modify-password", "set-password"]` |
|
|
333
|
+
| 更新使用者資料 | `["*", "modify-password", "set-password"]` |
|
|
334
|
+
|
|
335
|
+
其中 `"*"` 是保留名稱,代表預設規則集。
|
|
336
|
+
|
|
337
|
+
在控制器中指定:
|
|
338
|
+
|
|
339
|
+
```csharp
|
|
340
|
+
[HttpPost]
|
|
341
|
+
[ValidationOptions(["*", "set-password"])]
|
|
342
|
+
public Task Create(User user) => Task.CompletedTask;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
> **設計建議**:更推薦為不同業務操作定義專用的輸入模型(如 `RegisterUserDto`、`ChangePasswordDto`)。規則集適合多個操作確實共享同一輸入模型且驗證邏輯高度重疊的場景。
|
|
346
|
+
|
|
347
|
+
### 8.2.11 驗證錯誤資訊本地化(多語言)
|
|
348
|
+
|
|
349
|
+
**驗證特性的本地化:**
|
|
350
|
+
|
|
351
|
+
- 方式一:使用 `ErrorMessage` 屬性(隱式資源查找)
|
|
352
|
+
- 方式二:使用 `ErrorMessageResourceType` 和 `ErrorMessageResourceName`
|
|
353
|
+
- 方式三:在自訂驗證特性中透過 `validationContext.GetService<IStringLocalizer<T>>()` 動態取得
|
|
354
|
+
|
|
355
|
+
**驗證器或鏈式驗證中的本地化:**
|
|
356
|
+
|
|
357
|
+
```csharp
|
|
358
|
+
// 在 IValidatableObject 中
|
|
359
|
+
var localizer = validationContext.GetService<IStringLocalizer<User>>();
|
|
360
|
+
return validationContext.With<User>()
|
|
361
|
+
.RuleFor(u => u.Name)
|
|
362
|
+
.Required().WithMessage(localizer.GetString("姓名不能為空"))
|
|
363
|
+
.ToResults();
|
|
364
|
+
|
|
365
|
+
// 在獨立驗證器中
|
|
366
|
+
public class UserValidator : AbstractValidator<User>
|
|
367
|
+
{
|
|
368
|
+
public UserValidator(IStringLocalizer<User> localizer)
|
|
369
|
+
{
|
|
370
|
+
RuleFor(u => u.Name).Required().WithMessage(localizer["姓名不能為空"]);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**全域預設驗證錯誤資訊的替換:**
|
|
376
|
+
|
|
377
|
+
```csharp
|
|
378
|
+
services.AddValidationCore(builder =>
|
|
379
|
+
{
|
|
380
|
+
// 替換 .NET 內建驗證特性的預設錯誤資訊
|
|
381
|
+
builder.ConfigureDataAnnotationValidationMessages(message =>
|
|
382
|
+
{
|
|
383
|
+
message["RequiredAttribute_ValidationError"] = "欄位 {0} 是必填項。";
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// 替換框架提供的驗證器預設錯誤資訊
|
|
387
|
+
builder.ConfigureValidationMessages(message =>
|
|
388
|
+
{
|
|
389
|
+
message["AgeValidator_ValidationError"] = "欄位 {0} 不是有效的年齡。";
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// 一鍵啟用中文
|
|
393
|
+
builder.UseChineseDataAnnotationMessages();
|
|
394
|
+
builder.UseChineseValidationMessages();
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 8.3 驗證器 ValidatorBase
|
|
401
|
+
|
|
402
|
+
`ValidatorBase` 是一個抽象基底類別,用於封裝具體的驗證邏輯,作為整個驗證體系的最小核心單元。
|
|
403
|
+
|
|
404
|
+
### 8.3.1 成員概覽
|
|
405
|
+
|
|
406
|
+
**建構函式**:`new()`、`new(string)`、`new(Func<string>)`
|
|
407
|
+
|
|
408
|
+
**屬性**:`ErrorMessage`、`ErrorMessageResourceType`、`ErrorMessageResourceName`、`RuleSets`
|
|
409
|
+
|
|
410
|
+
**方法**:`IsValid`、`GetValidationResults`、`Validate`、`TryValidate`、`FormatErrorMessage`
|
|
411
|
+
|
|
412
|
+
### 8.3.2 驗證操作
|
|
413
|
+
|
|
414
|
+
```csharp
|
|
415
|
+
var validator = Validators.EmailAddress();
|
|
416
|
+
|
|
417
|
+
// 1. IsValid — 判斷是否有效
|
|
418
|
+
var isValid = validator.IsValid(value);
|
|
419
|
+
|
|
420
|
+
// 2. GetValidationResults — 取得驗證結果
|
|
421
|
+
var results = validator.GetValidationResults(value, "data");
|
|
422
|
+
|
|
423
|
+
// 3. Validate — 失敗時拋出 ValidationException
|
|
424
|
+
validator.Validate(value, "data");
|
|
425
|
+
|
|
426
|
+
// 4. TryValidate — 回傳 ValidatorResult
|
|
427
|
+
var result = validator.TryValidate(value, "data");
|
|
428
|
+
result.ThrowIfInvalid();
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 8.3.3 驗證上下文
|
|
432
|
+
|
|
433
|
+
`IValidationContext` 用於在驗證過程中向驗證器傳遞執行時上下文資訊,其預設實作為 `ValidationContext<T>`。
|
|
434
|
+
|
|
435
|
+
### 8.3.4 錯誤資訊
|
|
436
|
+
|
|
437
|
+
```csharp
|
|
438
|
+
// 直接指定
|
|
439
|
+
var validator = new EmailAddressValidator().WithMessage("這不是有效的電子郵箱格式");
|
|
440
|
+
|
|
441
|
+
// 使用資源
|
|
442
|
+
var validator = new EmailAddressValidator()
|
|
443
|
+
.WithMessage(typeof(YourValidationMessages), "EmailAddressValidator_ValidationError");
|
|
444
|
+
|
|
445
|
+
// Must 驗證中動態設定
|
|
446
|
+
var validator = new MustValidator<int?>(value => value switch
|
|
447
|
+
{
|
|
448
|
+
< 10 => false,
|
|
449
|
+
10 => Must.WithMessage("值不能等於 10"),
|
|
450
|
+
_ => true
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 8.4 內建驗證器
|
|
457
|
+
|
|
458
|
+
框架提供了豐富的內建驗證器,所有驗證器均以 `Validator` 結尾,並可透過 `Validators` 靜態工廠類別建立實例。
|
|
459
|
+
|
|
460
|
+
以下為內建驗證器速查表:
|
|
461
|
+
|
|
462
|
+
| # | 驗證器 | 功能描述 | 工廠方法 |
|
|
463
|
+
|---|--------|----------|----------|
|
|
464
|
+
| 1 | `AgeValidator` | 年齡驗證(0-120,可限成年) | `Validators.Age()` |
|
|
465
|
+
| 2 | `AllowedValuesValidator` | 允許值列表 | `Validators.AllowedValues(...)` |
|
|
466
|
+
| 3 | `AttributeObjectValidator` | 物件級驗證特性 | `Validators.AttributeObject()` |
|
|
467
|
+
| 4 | `AttributePropertyValidator` | 單屬性驗證特性 | `Validators.AttributeProperty<T>(...)` |
|
|
468
|
+
| 5 | `AttributeValueValidator` | 單值驗證特性 | `Validators.AttributeValue(...)` |
|
|
469
|
+
| 6 | `BankCardValidator` | 銀行卡號(Luhn 演算法) | `Validators.BankCard()` |
|
|
470
|
+
| 7 | `Base64StringValidator` | Base64 字串 | `Validators.Base64String()` |
|
|
471
|
+
| 8 | `ChineseNameValidator` | 中文姓名 | `Validators.ChineseName()` |
|
|
472
|
+
| 9 | `ChineseValidator` | 純中文字元 | `Validators.Chinese()` |
|
|
473
|
+
| 10 | `ColorValidator` | CSS 顏色 | `Validators.Color()` |
|
|
474
|
+
| 11 | `CompareValidator` | 比較兩個屬性 | `Validators.Compare<T>(...)` |
|
|
475
|
+
| 12 | `CustomValidationAttribute` | 自訂驗證特性 | `Validators.CustomValidation(...)` |
|
|
476
|
+
| 13 | `DateOnlyValidator` | 日期 | `Validators.DateOnly()` |
|
|
477
|
+
| 14 | `DateTimeValidator` | 日期時間 | `Validators.DateTime()` |
|
|
478
|
+
| 15 | `DecimalPlacesValidator` | 小數位數 | `Validators.DecimalPlaces(...)` |
|
|
479
|
+
| 16 | `DeniedValuesValidator` | 禁止值列表 | `Validators.DeniedValues(...)` |
|
|
480
|
+
| 17 | `DomainValidator` | 網域名稱 | `Validators.Domain()` |
|
|
481
|
+
| 18 | `EmailAddressValidator` | 電子郵件地址 | `Validators.EmailAddress()` |
|
|
482
|
+
| 19 | `EmptyValidator` | 空值/空集合/空字串 | `Validators.Empty()` |
|
|
483
|
+
| 20 | `EndsWithValidator` | 結尾字串 | `Validators.EndsWith(...)` |
|
|
484
|
+
| 21 | `EnumValidator` | 列舉成員 | `Validators.Enum<T>()` |
|
|
485
|
+
| 22 | `EqualToValidator` | 相等 | `Validators.EqualTo(...)` |
|
|
486
|
+
| 23 | `FailureValidator` | 固定失敗 | `Validators.Failure()` |
|
|
487
|
+
| 24 | `FileExtensionsValidator` | 檔案副檔名 | `Validators.FileExtensions(...)` |
|
|
488
|
+
| 25 | `GreaterThanOrEqualToValidator` | 大於等於 | `Validators.GreaterThanOrEqualTo(...)` |
|
|
489
|
+
| 26 | `GreaterThanValidator` | 大於 | `Validators.GreaterThan(...)` |
|
|
490
|
+
| 27 | `HaveLengthValidator` | 固定長度 | `Validators.HaveLength(...)` |
|
|
491
|
+
| 28 | `IDCardValidator` | 身分證號 | `Validators.IDCard()` |
|
|
492
|
+
| 29 | `IpAddressValidator` | IP 位址 | `Validators.IpAddress()` |
|
|
493
|
+
| 30 | `JsonValidator` | JSON 格式 | `Validators.Json()` |
|
|
494
|
+
| 31 | `LengthValidator` | 長度範圍 | `Validators.Length(...)` |
|
|
495
|
+
| 32 | `LessThanOrEqualToValidator` | 小於等於 | `Validators.LessThanOrEqualTo(...)` |
|
|
496
|
+
| 33 | `LessThanValidator` | 小於 | `Validators.LessThan(...)` |
|
|
497
|
+
| 34 | `MaxLengthValidator` | 最大長度 | `Validators.MaxLength(...)` |
|
|
498
|
+
| 35 | `MaxValidator` | 最大值 | `Validators.Max(...)` |
|
|
499
|
+
| 36 | `MD5StringValidator` | MD5 字串 | `Validators.MD5String()` |
|
|
500
|
+
| 37 | `MinLengthValidator` | 最小長度 | `Validators.MinLength(...)` |
|
|
501
|
+
| 38 | `MinValidator` | 最小值 | `Validators.Min(...)` |
|
|
502
|
+
| 39 | `MustValidator` | 自訂條件 | `Validators.Must(...)` |
|
|
503
|
+
| 40 | `NotBlankValidator` | 非空白字串 | `Validators.NotBlank()` |
|
|
504
|
+
| 41 | `NotEmptyValidator` | 非空集合/陣列/字串 | `Validators.NotEmpty()` |
|
|
505
|
+
| 42 | `NotEqualToValidator` | 不相等 | `Validators.NotEqualTo(...)` |
|
|
506
|
+
| 43 | `NotNullValidator` | 非 null | `Validators.NotNull()` |
|
|
507
|
+
| 44 | `NullValidator` | 必須為 null | `Validators.Null()` |
|
|
508
|
+
| 45 | `PasswordValidator` | 密碼(普通/強) | `Validators.Password()` |
|
|
509
|
+
| 46 | `PhoneNumberValidator` | 手機號(中國) | `Validators.PhoneNumber()` |
|
|
510
|
+
| 47 | `PostalCodeValidator` | 郵遞區號(中國) | `Validators.PostalCode()` |
|
|
511
|
+
| 48 | `RangeValidator` | 數值範圍 | `Validators.Range(...)` |
|
|
512
|
+
| 49 | `RegularExpressionValidator` | 正規表示式 | `Validators.RegularExpression(...)` |
|
|
513
|
+
| 50 | `RequiredValidator` | 必填 | `Validators.Required()` |
|
|
514
|
+
| 51 | `SingleValidator` | 單項 | `Validators.Single()` |
|
|
515
|
+
| 52 | `StartsWithValidator` | 開頭字串 | `Validators.StartsWith(...)` |
|
|
516
|
+
| 53 | `StringContainsValidator` | 字串包含 | `Validators.StringContains(...)` |
|
|
517
|
+
| 54 | `StringLengthValidator` | 字串長度 | `Validators.StringLength(...)` |
|
|
518
|
+
| 55 | `StringNotContainsValidator` | 字串不包含 | `Validators.StringNotContains(...)` |
|
|
519
|
+
| 56 | `StrongPasswordValidator` | 強密碼 | `Validators.StrongPassword()` |
|
|
520
|
+
| 57 | `TelephoneValidator` | 座機(中國) | `Validators.Telephone()` |
|
|
521
|
+
| 58 | `TimeOnlyValidator` | 時間 | `Validators.TimeOnly()` |
|
|
522
|
+
| 59 | `UrlValidator` | URL 位址 | `Validators.Url()` |
|
|
523
|
+
| 60 | `UserNameValidator` | 使用者名稱 | `Validators.UserName()` |
|
|
524
|
+
| 61 | `ValidatorProxy` | 驗證器代理 | — |
|
|
525
|
+
| 62 | `CompositeValidator` | 組合驗證器 | `Validators.Composite<T>(...)` |
|
|
526
|
+
| 63 | `ConditionalValidator` | 條件驗證器 | `Validators.Conditional<T>(...)` |
|
|
527
|
+
| 64 | `ComparisonValidator` | 比較驗證器基底類別 | — |
|
|
528
|
+
| 65 | `NumericComparisonValidator` | 數值比較驗證器基底類別 | — |
|
|
529
|
+
|
|
530
|
+
### 驗證器通用使用模式
|
|
531
|
+
|
|
532
|
+
所有內建驗證器均遵循統一的使用模式:
|
|
533
|
+
|
|
534
|
+
```csharp
|
|
535
|
+
// 建立實例(推薦使用 Validators 靜態工廠)
|
|
536
|
+
var validator = Validators.EmailAddress();
|
|
537
|
+
|
|
538
|
+
// 自訂錯誤資訊
|
|
539
|
+
var validator = Validators.EmailAddress().WithMessage("這不是有效的電子郵箱");
|
|
540
|
+
|
|
541
|
+
// 執行驗證
|
|
542
|
+
var isValid = validator.IsValid(value);
|
|
543
|
+
var results = validator.GetValidationResults(value, "displayName");
|
|
544
|
+
validator.Validate(value, "displayName"); // 失敗時拋出 ValidationException
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### 驗證錯誤資訊資源替換
|
|
548
|
+
|
|
549
|
+
每個驗證器都有對應的資源鍵,可透過以下方式統一替換預設錯誤資訊:
|
|
550
|
+
|
|
551
|
+
```csharp
|
|
552
|
+
services.AddValidationCore(builder =>
|
|
553
|
+
{
|
|
554
|
+
builder.ConfigureValidationMessages(message =>
|
|
555
|
+
{
|
|
556
|
+
message["EmailAddressValidator_ValidationError"] = "欄位 {0} 不是有效的電子郵件地址。";
|
|
557
|
+
message["RequiredValidator_ValidationError"] = "欄位 {0} 是必填項。";
|
|
558
|
+
// ...更多替換
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// 一鍵啟用中文
|
|
562
|
+
builder.UseChineseValidationMessages();
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### 進階驗證器
|
|
567
|
+
|
|
568
|
+
**CompositeValidator — 組合驗證器**
|
|
569
|
+
|
|
570
|
+
支援三種驗證模式:`FailFast`(預設,任一失敗即停止)、`All`(全部必須通過)、`Any`(任一通過即可)。
|
|
571
|
+
|
|
572
|
+
**ConditionalValidator — 條件驗證器**
|
|
573
|
+
|
|
574
|
+
根據執行時條件動態選擇驗證規則:
|
|
575
|
+
|
|
576
|
+
```csharp
|
|
577
|
+
var validator = new ConditionalValidator<User>(builder =>
|
|
578
|
+
{
|
|
579
|
+
builder.When(u => u.Type == UserType.Admin).Then(v => v.Required().EmailAddress());
|
|
580
|
+
builder.When(u => u.Type == UserType.Guest).Then(v => v.StringLength(1, 10));
|
|
581
|
+
builder.Otherwise(v => v.Required().UserName());
|
|
582
|
+
});
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**ComparisonValidator / NumericComparisonValidator — 比較基底類別**
|
|
586
|
+
|
|
587
|
+
為所有基於值比較的驗證器(`GreaterThan`、`LessThanOrEqualTo` 等)提供統一基底類別。`NumericComparisonValidator` 會自動將輸入值統一轉換為 `decimal` 類型進行比較,適合金融、計量等對精度敏感的場景。
|