承上篇,解決驗證屬性邏輯膨脹、擴充困難問題。引入 IValidator 介面,讓每個驗證屬性 (RangeAttribute、RequiredAttribute) 實作自身驗證邏輯,實體 Validate() 僅遍歷屬性並委派驗證,符合單一職責與開放封閉原則。
public interface IValidator
{
bool Validate(object value, out string errorMessage);
}
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute : Attribute, IValidator
{
public int Min { get; set; }
public int Max { get; set; }
public string ErrorMessage { get; set; }
public RangeAttribute(int min, int max, string errorMessage)
{
Min = min; Max = max; ErrorMessage = errorMessage;
}
public bool Validate(object value, out string errorMessage)
{
errorMessage = null;
if (value is int val)
{
if (val < Min || val > Max)
{
errorMessage = ErrorMessage;
return false;
}
}
return true;
}
}
[AttributeUsage(AttributeTargets.Property)]
public class RequiredAttribute : Attribute, IValidator
{
public string ErrorMessage { get; set; }
public bool Validate(object value, out string errorMessage)
{
errorMessage = null;
if (value == null || (value is string s && string.IsNullOrWhiteSpace(s)))
{
errorMessage = ErrorMessage;
return false;
}
return true;
}
}
public class Student
{
[Range(0, 100, "分數必須在 0-100 之間")]
public int Score { get; set; }
[Required(ErrorMessage = "姓名不可為空")]
public string Name { get; set; }
public bool Validate(out List errors)
{
errors = new List();
var props = GetType().GetProperties();
foreach (var prop in props)
{
var validators = prop.GetCustomAttributes(typeof(IValidator), true).Cast();
foreach (var v in validators)
{
if (!v.Validate(prop.GetValue(this), out string err))
errors.Add(err);
}
}
return errors.Count == 0;
}
}
[TestMethod]
public void Student_Validation_Fails_On_Invalid_Score()
{
var s = new Student { Score = -1, Name = "Tom" };
var ok = s.Validate(out var errors);
Assert.IsFalse(ok);
Assert.IsTrue(errors.Contains("分數必須在 0-100 之間"));
}
RegexAttribute) 只需實作 IValidator,不修改實體 Validate。Validate 並將錯誤加入 ModelState。