skills/csharp-validation/SKILL.md
C# 輸入驗證規範:DataAnnotations、FluentValidation 選型、驗證層級劃分與 ASP.NET Core 整合策略。
npx skillsauth add CloudyWing/ai-dotfiles csharp-validationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
當撰寫或審查輸入驗證邏輯時,請自動套用以下規範。
輸入驗證分為三個層級,各有職責,不可混淆:
| 層級 | 職責 | 負責位置 | 範例 | | --- | --- | --- | --- | | 格式驗證 | 資料型別、必填、長度、格式 | Model Binding / Request Model | 必填欄位、Email 格式、字串長度 | | 商業規則驗證 | 需要查詢資料庫或外部狀態的規則 | Service Layer | 帳號是否重複、庫存是否足夠 | | 不變式保護 | Domain Model 自身的完整性約束 | Domain Model 建構函式 | 金額不可為負、起始日不可晚於結束日 |
適用於大多數格式驗證場景。ASP.NET Core Model Binding 原生支援,零設定即可使用。
public class CreateOrderRequest {
[Required(ErrorMessage = "客戶名稱為必填。")]
[StringLength(100, ErrorMessage = "客戶名稱不可超過 100 字。")]
public required string CustomerName { get; set; }
[Range(1, 10000, ErrorMessage = "數量必須介於 1 到 10000 之間。")]
public required int Quantity { get; set; }
[EmailAddress(ErrorMessage = "Email 格式不正確。")]
public string? ContactEmail { get; set; }
}
適用於需要跨屬性條件判斷、依情境切換規則、或需要注入服務的驗證場景。
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest> {
public CreateOrderRequestValidator() {
RuleFor(x => x.CustomerName)
.NotEmpty().WithMessage("客戶名稱為必填。")
.MaximumLength(100).WithMessage("客戶名稱不可超過 100 字。");
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 10000).WithMessage("數量必須介於 1 到 10000 之間。");
RuleFor(x => x.ContactEmail)
.EmailAddress().WithMessage("Email 格式不正確。")
.When(x => x.ContactEmail is not null);
}
}
| Attribute | 用途 |
| --- | --- |
| [Required] | 必填(搭配 NRT 的 required keyword 使用) |
| [StringLength(max)] | 字串最大長度 |
| [MinLength] / [MaxLength] | 集合或字串的長度限制 |
| [Range(min, max)] | 數值範圍 |
| [EmailAddress] | Email 格式 |
| [Phone] | 電話格式 |
| [Url] | URL 格式 |
| [RegularExpression] | 自訂正規表達式 |
| [Compare] | 兩個屬性值必須一致(如密碼確認) |
| [AllowedValues] | 限制允許的值(.NET 8+) |
| [DeniedValues] | 限制禁止的值(.NET 8+) |
| [Length(min, max)] | 同時限制最小與最大長度(.NET 8+) |
ErrorMessage,不使用框架預設的英文訊息。[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class TaiwanIdAttribute : ValidationAttribute {
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) {
if (value is not string id) {
return ValidationResult.Success;
}
if (!TaiwanIdValidator.IsValid(id)) {
return new ValidationResult(ErrorMessage ?? "身分證格式不正確。");
}
return ValidationResult.Success;
}
}
// 掃描組件中所有 Validator 並註冊
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();
Minimal API 不像 MVC 有自動 Model Validation Filter。需要手動驗證或搭配 Endpoint Filter。
// Endpoint Filter 方式
public class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
where T : class {
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next
) {
T? model = context.Arguments.OfType<T>().FirstOrDefault();
if (model is null) {
return Results.Problem(title: "無效的請求", statusCode: 400);
}
ValidationResult result = await validator.ValidateAsync(model).ConfigureAwait(false);
if (!result.IsValid) {
return Results.ValidationProblem(result.ToDictionary());
}
return await next(context).ConfigureAwait(false);
}
}
// 使用
app.MapPost("/api/orders", (CreateOrderRequest request) => { /* ... */ })
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
public class DateRangeRequestValidator : AbstractValidator<DateRangeRequest> {
public DateRangeRequestValidator() {
RuleFor(x => x.StartDate)
.NotEmpty().WithMessage("起始日期為必填。");
RuleFor(x => x.EndDate)
.NotEmpty().WithMessage("結束日期為必填。")
.GreaterThanOrEqualTo(x => x.StartDate)
.WithMessage("結束日期不可早於起始日期。");
}
}
RuleFor(x => x.CompanyName)
.NotEmpty().WithMessage("公司名稱為必填。")
.When(x => x.CustomerType == CustomerType.Business);
ASP.NET Core MVC 預設啟用 [ApiController] 時,Model Validation 失敗會自動回傳 ValidationProblemDetails(400)。不需要在每個 Action 中手動檢查 ModelState.IsValid。
// ❌ 不需要:[ApiController] 已自動處理
[HttpPost]
public IActionResult Create(CreateOrderRequest request) {
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
// ...
}
// ✅ 正確:直接使用,驗證由框架處理
[HttpPost]
public IActionResult Create(CreateOrderRequest request) {
// request 到達此處已通過格式驗證
// ...
}
若需要自訂驗證流程(如部分欄位條件式驗證),可在特定 Controller 停用:
builder.Services.Configure<ApiBehaviorOptions>(options => {
options.SuppressModelStateInvalidFilter = true;
});
// ✅ 正確:公開方法驗證參數
public class OrderService(IOrderRepository repository) {
public async Task<Order> GetOrderAsync(int orderId, CancellationToken cancellationToken) {
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(orderId);
return await repository.GetByIdAsync(orderId, cancellationToken).ConfigureAwait(false)
?? throw new KeyNotFoundException($"找不到 ID 為 {orderId} 的訂單。");
}
}
// ❌ 錯誤:私有方法重複驗證已在公開方法中驗證過的值
private decimal CalculateTotal(int quantity, decimal unitPrice) {
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // 多餘
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(unitPrice); // 多餘
return quantity * unitPrice;
}
tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.
data-ai
多份資料檔整合流程。當需要將兩份以上的資料檔(如 JSON、CSV)合併、補齊闕漏欄位或去重成單一檔案時使用。以 dry-run、筆數核對與抽樣比對降低整合錯誤。