iT邦幫忙

1

關於驗證(Validation) 這件事

關於驗證(Validation) 這件事

小明來問說, 他想要檢查輸入內容是否驗證通過,
檢查模型(Model)的所有屬性(Property)內容,
查看裡面某些屬性(Property) 內容或是格式是不是有問題?

以下是小明提供的程式碼片段, 它想要驗證customer 內容是不是有問題

public void Process(Customer customer)
{
   if( customer.UserId <= 0 ) {
      throw new Exception("UserId must be a positive number");
   }
   if( customer.Email == null ){
      throw new Exception("Email can't be null"); 
   }
   ...
}

驗證輸入內容的方法有很多種, 這沒有唯一正確的標準答案,
因此與團隊合作了解哪種方法最適合解決目前遭遇的問題,
就是好的方法.

通常驗證輸入內容有三種方法

  • 拋出異常
  • 驗證規則模式
  • 帶有驗證的結果物件

拋出異常

此方法使用 Exception 涉及直接系統中斷, 該模式是最常用的驗證模式, 它包括直接檢查輸入和引發異常.
就像小明來詢問, 所提供的程式碼片段寫法.

此方法的特色如下

  • 一旦引發異常, 方法的執行就結束, 處理起來更快, 但是您只會得到第一個無效輸入的結果.
    在小明的例子中, 如果 userId 為 0, 則您只會得到 userId 無效輸入的結果.
  • 它很靈活, 因為您可以在任何方法中撰寫指定任何規則.
  • 有可能在多個地方重複進行驗證.

驗證規則模式

驗證規則模式源自Visitor 設計模式, 我們可以使用dotnet 提供的Validator,
dotnet 提供了許多常用的ValidationAttribute , 常見的有

Attributes 說明
Required 必填
StringLength(20) 字串長度
MinLength(2) 最小字串長度
MaxLength(2) 最大字串長度
Range(18, 20) 數值範圍
EmailAddress Email格式

dotnet Validator 範例如下

public class Customer 
{
   public int UserId { get;set; }

   [Required]
   public string Name { get; set;}

   [EmailAddress]
   public string Email { get; set; }
}

public void Process(Customer customer)
{
   var context = new ValidationContext(customer, null, null);
   Validator.ValidateObject(customer, context, true);
   ...
}

透過dotnet 提供的常用Atturibute 就能應付一般大部分的驗證情況,
不過有時候我們還是需要自訂的驗證方式.

第一種是自訂自己的Validation Attribute, 下面示範用來檢查欄位的內容是否為 null

public class MyRequiredValidationAttribute : ValidationAttribute
{
	protected override ValidationResult IsValid(object value, ValidationContext validationContext)
	{
		var text = (string) value;
		if ( String.IsNullOrEmpty(text) )
		{
			return new ValidationResult("Name can't be null!", 
            new[] { validationContext.MemberName });
		}
		return base.IsValid(value, validationContext);
	}
}

然後在Customer 物件中, 掛載MyRequired Attribute

public class Customer
{
   [MyRequired]
   public string Name { get; set;}
}

也有與 Required 同樣的效果.

也許有的人會不喜歡在Customer 中掛載許多驗證 Attribute 來設定驗證方法,
想要統一在一個地方做設定驗證方法.

那你可以用第二種方式, 實作IValidatableObject , 範例如下

public class Customer : IValidatableObject
{
   public int UserId { get;set; }
   public string Name { get; set;}
   public string Email { get; set; }

   public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
   {
      if ( String.IsNullOrEmpty(Name) )
      {
         yield return new ValidationResult("Name can't be null", new[] {" Name "});
      }
   }
}

此模式具有以下優點

  • 所有驗證規則都是分開的, 可以分別維護
  • Process 方法中的驗證程式碼很小並且易於閱讀
  • 它允許您一次套用多個驗證規則

缺點

  • 對於特定的內容或條件, 必須創建單獨的驗證規則物件.

帶有驗證的結果物件

用dotnet Validator 的使用方式如下

public void Process(Customer customer) {
   Validator.ValidateObject(customer, context, true);
   var validationResults = new Collection<ValidationResult>();
   var isSuccess = Validator.TryValidateObject(customer, context, validationResults, true);
   ...
}

上述程式碼 validationResults 變數儲存所有驗證的結果.

dotnet Validator 提供兩種方式 ValidateObject 和 TryValidateObject

我們也能利用 FluentValidation 來達到這個方式, 它的使用方式如下

public class MyValidator : AbstractValidator<Customer>
{
	public MyValidator()
	{
		ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure;
      RuleFor(c => c.Name)
			.NotNull()
			.WithMessage("{PropertyName} can't be null");
      ...
	}
}

public void Process(Customer customer)
{
	var validator = new MyValidator();
	var validationResult = validator.Validate(customer);
	if (!validationResult.IsValid)
	{
      var errorMssage = string.Join("\r\n", validationResult.Errors.Select(e => e.ErrorMessage));
      throw new Exception(errorMessage);
	}
}

你可以在一個地方添加許多RuleFor 驗證規則.

當然FluentValidation 也提供我們自訂驗證規則, 自訂驗證規則方式如下

public class MyRequiredValidator : PropertyValidator {
   public MyRequiredValidator() 
      : base("'{PropertyValue}' can't not be null.") { }

   protected override bool IsValid(PropertyValidatorContext context) {
      var text = (string) context.PropertyValue;
      if (string.IsNullOrEmpty(text)) {
         return false;
      }
      return true;
    }
}

寫好自訂的 PropertyValidator 之後, 透過 SetValidator 照下面示範

public class MyValidator : AbstractValidator<Customer>
{
	public MyValidator()
	{
		ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure;
      RuleFor(c => c.Name)
			.SetValidator(new MyRequiredValidator());
      ...
	}
}

到這裡你可以發現dotnet 和FluentValidation 套用 MyRequired 驗證規則的方式不同.

dotnet Validation 通常方式是在Customer 物件中掛載驗證規則

[MyRequired]
public string Name{ get; set;} 

FluentValidation 通常方式是在自訂的MyValidator 驗證器中掛載驗證規則

public class MyValidator : AbstractValidator<Customer> {
   public MyValidator()
	{
      RuleFor(c => c.Name)
			.SetValidator(new MyRequiredValidator());
      ...
	}
}

而FluentValidation 只有這一種執行驗證方式

public void Process(Customer customer) {
   var validator = new MyValidator();
	var validationResult = validator.Validate(customer);
	if (!validationResult.IsValid)
	{
      var errorMssage = string.Join("\r\n", validationResult.Errors.Select(e => e.ErrorMessage));
      ...
	}
}

但你會發現, 假如應用程式有很多個模型(models),
用 FluentValidation 就必須為每個模型(model)編寫 Validators.
也許有人會覺得很麻煩. 會想要寫一個通用驗證器(Validator).
而dotnet Validator 恰恰就正好是一個通用的驗證器, 兩者作法不同.

另外在FluentValidation 中, 或許你會覺得每增加自訂驗證規則還要多寫 PropertyValidator 很麻煩,
你也可以用 Custom 方法來提供自訂驗證規則.

public class MyValidator : AbstractValidator<Customer> {
   public MyValidator()
	{
      RuleFor(c => c.Name)
         .Custom((value, context) =>
			{
				if (string.IsNullOrEmpty(value))
				{
					context.AddFailure("Name", "Name can't be null");
				}
			});
	}
}

一旦用Custom 撰寫自訂驗證規則, 你就很難在其他地方重複使用.

無論採用PropertyValidator 或是Custom 方法編寫自訂驗證規則,
我認為這取決於你要放入其中的邏輯類型.

如果您想以更容易重複使用的方式編寫它, 請堅持使用PropertyValidator.
如果要使用Custom 的樣式編寫, 請使用類似 AbstractValidator 派生類.
在很多種情況下, 確實沒有對/錯的方法.

這種 "帶有驗證的結果物件" 模式像是前兩種方法的組合. 即使有一條驗證規則不通過, 但是我們仍然可以鏈接錯誤訊息, 繼續往下執行其他的驗證規則.


尚未有邦友留言

立即登入留言