客官,.NETCore無程式碼侵入的模型驗證瞭解下

福祿網路技術團隊發表於2021-03-09

背景

.NETCore下的模型驗證相信絕大部分的.NET開發者或多或少的都用過,微軟官方提供的模型驗證相關的類位於System.ComponentModel.DataAnnotations命令空間下,在使用的時候只需要給屬性新增不同的特性即可實現對應的模型驗證。如下所示:

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }
}

在WebApi中,當請求介面時,程式會自動對模型進行驗證,如無法驗證通過,則會直接終止後續的邏輯執行,並響應400狀態碼,響應內容如下所示:

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-4b16460fc83d7b4daa4f10d939016982-f823eebede419a4a-00",
"errors": {
"aa": [
"The aa field is required."
]
}
}

當然,你也可以自定義響應的內容,這不是本文的重點。本文的重點是,.NETCore系統預設的模型驗證功能並不夠強大,僅支援在Controller的Action中使用,不支援非Controller中或者控制檯程式的驗證,且程式碼侵入性較強。

而FluentValidation(https://fluentvalidation.net/ )則是功能更為強大的模型驗證框架,支援任何場景下的模型驗證,且不侵入程式碼。

下面就來和筆者一起了解下FluentValidation的用法。

接入

FluentValidation支援一下平臺:

  • .NET 4.6.1+
  • .NET Core 2.0+
  • .NET Standard 2.0+

各個平臺的整合方式大同小異,本文僅講解.NETCore3.1的整合方式。

首先,使用NuGet安裝FluentValidation.AspNetCore依賴。

新增需要驗證的模型類,如Student類,程式碼如下:

public class Student
{
    public int Id { get; set; }

    public int Age { get; set; }

    public string Name { get; set; }
}

然後建立類StudentValidator,並整合類AbstractValidator,程式碼如下:

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(x => x.Age).InclusiveBetween(10, 50);
        RuleFor(x => x.Name).NotEmpty().MaximumLength(5);
    }
}

上述的驗證類中,要求Age大於10且小於50,Name不為空,且長度小於5。

最後,還需要將驗證類註冊到服務中。修改Startup的ConfigureServices,部分程式碼如下:

services.AddControllers().AddFluentValidation(conf =>
    {
        conf.RegisterValidatorsFromAssemblyContaining<StudentValidator>();
        conf.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
    });

上述程式碼中,RegisterValidatorsFromAssemblyContaining方法的作用是掃描StudentValidator類所在的程式集中的所有驗證類,並註冊到服務中。

RunDefaultMvcValidationAfterFluentValidationExecutes為false時,會遮蔽掉系統預設的模型驗證,如需相容系統預設的模型驗證,將RunDefaultMvcValidationAfterFluentValidationExecutes的值改為true即可。此引數預設為true。

下面在Controller中,新增一個Action,程式碼如下:

[HttpPost]
public IActionResult Add([FromBody] Student student)
{
    return Ok(student);
}

開啟swagger,訪問介面,響應如下所示:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-6331a76578228b4cb9044aa40f514bc9-89fd8547c1921340-00",
  "errors": {
    "Age": [
      "'Age' 必須在 10 (包含)和 25 (包含)之間, 您輸入了 0。"
    ],
    "Name": [
      "'Name' 必須小於或等於5個字元。您輸入了6個字元。"
    ]
  }
}

至此,在 ASP.NET Core中整合FluentValidation就完成了。但到現在為止,這和系統預設的模型驗證並沒有區別。 在文章的開頭筆者也提到過,FluentValidation不僅支援Controller中對模型進行驗證,下面的程式碼就是非Controller場景下的驗證。

public class DemoService
{
    private readonly IValidator<Student> _studentValidator;

    public DemoService(IValidator<Student> studentValidator)
    {
        _studentValidator = studentValidator;
    }

    public bool Run(Student student)
    {
        var valid = _studentValidator.Validate(student);
        return valid.IsValid;
    }
}

在上述程式碼中,通過建構函式注入的方式,獲取到了IValidator例項,在Run方法中只需要呼叫Validate方法,引數是需要驗證的物件,返回的物件就包含了驗證的是否通過以及不通過時,具體的錯誤資訊。

基礎用法

內建規則

FluentValidation內建了多個常用的驗證器,下面簡單介紹幾個特別常用或容易出錯的驗證器。

NotNull 和 NotEmpty

NotNull是確保指定的屬性不為null,NotEmpty則表示確保指定的屬性不為null、空字串或空白(值型別的預設值,比如int型別的預設值為0),如果int型別屬性設定NotEmpty驗證器,則當值為0時,驗證是無法通過的。

NotEqual 和 Equal

NotEqual 和 Equal分別是不相等和相等驗證器,可與指定的值或者指定的屬性進行比較。

MaximumLength、MinimumLength和Length

MaximumLength為最大長度驗證器,MinimumLength為最小長度驗證器,而Length則是二者的結合,需要注意的是,這三種驗證器僅對字串有效,且不會驗證null,當值為null時,則不對長度進行驗證,所以使用長度驗證器時,建議結合NotNull一起使用。

LessThan、LessThanOrEqualTo、GreaterThan、GreaterThanOrEqualTo

上述的幾個驗證器為比較驗證器,僅適用於繼承IComparable介面的屬性,分別表示的是:小於、小於或等於、大於、大於或等於。

Matches

正規表示式驗證器,用於確保指定的屬性與給定的正規表示式匹配。

ExclusiveBetween和InclusiveBetween

示例程式碼如下:

RuleFor(x => x.Id).ExclusiveBetween(1,10);
RuleFor(x => x.Id).InclusiveBetween(1,10);

以上程式碼均表示輸入的Id的值需要在1,10之間,而兩者的區別是,InclusiveBetween驗證器是包含頭和尾的,而ExclusiveBetween是不包含的,例如當Id值為1時,ExclusiveBetween驗證失敗,但InclusiveBetween則驗證成功。

覆蓋驗證器預設的錯誤提示

在文章的開頭提到了,當驗證Student的Age屬性不通過時,提示資訊是:'Age' 必須在 10 (包含)和 25 (包含)之間, 您輸入了 0。

這個提示資訊對於開發者來講,定位問題已經很清晰了,但如果要在WebApi中講驗證的錯誤資訊返回給前端,那麼這個提示就會被使用者看到,則此錯誤資訊就不太友好,FluentValidation提供了多種覆蓋錯誤提示的方式,下面就來一起看下。

佔位符

我們可以將驗證Age的程式碼改為如下所示:

RuleFor(x => x.Age).InclusiveBetween(10, 25).WithMessage("年齡必須在{From}到{To}之間");

當驗證不通過時,輸出的錯誤資訊則為:年齡必須在10到25之間。

程式自動將{From}和{To}進行了替換。每個驗證器的佔位符都不一樣,有關佔位符的完整列表,請檢視官方文件 https://docs.fluentvalidation.net/en/latest/built-in-validators.html。

覆蓋屬性名稱

此方法是將屬性的名稱使用指定的字串替換,如下所示:

RuleFor(x => x.Age).InclusiveBetween(10, 25).WithName("年齡");

當發生錯誤時,會自動將系統預設的錯誤提示資訊中的"Age"替換為"年齡"

預設情況下,When或者Otherwise將應用於鏈式呼叫的所有前置的驗證器,如果只希望條件引用於前面的第一個驗證器,則必須使用ApplyConditionTo.CurrentValidator顯示指定

 RuleFor(x => x.Age).GreaterThan(10).LessThan(20).When(x => x.Sex == 2,ApplyConditionTo.CurrentValidator);

上述的程式碼,如果不加ApplyConditionTo.CurrentValidator,則當Sex等於2時,則要求Age大於10且小於20。而Sex不等於2時,則不作任何驗證。如果加上ApplyConditionTo.CurrentValidator,則Age大於10的驗證跟Sex的值沒有任何關係了,程式會始終驗證Age是否大於10

帶條件的驗證規則

使用When方法可控制規則執行的條件。例如,國家的法定結婚年齡為女性20歲,則驗證年齡屬性時,只有當性別為女時,才對年齡大於等於20進行校驗。

RuleFor(x => x.Age).GreaterThan(20).When(x => x.Sex == 2);

相反的,Unless表示的是當指定條件不滿足時,才執行校驗。

RuleFor(x => x.Age).GreaterThan(20).Unless(x => x.Sex == 2);

上述程式碼表示當Sex值不為2時,校驗Age是否大於等於20

如果需要為多個驗證規則指定相同的條件,可以呼叫When的頂級方法,而不是在規則末尾呼叫When方法。

When(x => x.Sex == 2, () =>
{
    RuleFor(x => x.Name).Must(x => !x.EndsWith("國慶"));
    RuleFor(x => x.Age).LessThan(30);
});

上述程式碼表示是,當Sex等於2時,Age需要小於30,並且名字不能以"國慶"結尾。

將Otherwise方法連結到When呼叫,表示When條件不滿足時,執行的驗證規則。

When(x => x.Sex == 2, () =>
{
    RuleFor(x => x.Name).Must(x => x.EndsWith("國慶"));
    RuleFor(x => x.Age).LessThan(30);
}).Otherwise(() =>
{
    RuleFor(x => x.Age).LessThan(50);
});

上述程式碼中的Otherwise方法表示的是,當Sex不等於2時,則Age需要小於50

鏈式呼叫

當一個屬性使用多個驗證規則時,可將多個驗證器連結在一起,比如,Student類的Name屬性不能為空,並且,長度需要小於10,則對應的程式碼為:

public StudentValidator()
{
    RuleFor(x =>x.Name).NotEmpty().MaximumLength(10);
}

CascadeMode

CascadeMode是一個列舉型別的屬性,有兩個選項:Continue和Stop

如果設定為Stop,則檢測到失敗的驗證,則立即終止,不會繼續執行剩餘屬性的驗證。預設值為Continue

CascadeMode = CascadeMode.Stop;
RuleFor(x => x.Name).NotEmpty().MaximumLength(10);
RuleFor(x => x.NickName).NotEmpty().MaximumLength(10);

如上述程式碼所示,當Name值不滿足要求時,則會停止對NickName的校驗

依賴規則

預設情況下,FluentValidation 中的所有規則都是獨立的,不能彼此影響。這是非同步驗證工作所必需的,也是必要的。但是,在某些情況下,您可能希望確保某些規則僅在另一個規則完成之後執行。您可以使用DependentRules它來做到這一點。

比如,只有身高超過130的兒童,才需要驗證是否購票,則可以通過如下的程式碼實現:

RuleFor(x => x.Height).GreaterThan(130).DependentRules(() =>
{
    RuleFor(x => x.HasTicket).NotEmpty();
});

高階用法

非同步驗證

在某些情況下,你可能希望定義非同步規則,比如從資料庫或者外部api判斷。

public StudentValidator(IStudentService studentService)
{
    _studentService = studentService;
    RuleFor(x => x.Name).MustAsync(async (name, token) => await _studentService.CheckExist(name));
}

上述程式碼中,通過一個非同步方法的返回值驗證Name屬性。
另外,如果在非Controller場景下使用,則必須呼叫ValidateAsync方法進行驗證。

轉換值

您可以在對屬性值執行驗證之前使用 Transform方法轉換屬性值。

RuleFor(x => x.Weight).Transform(x => int.TryParse(x, out int val)?(int?)val:null).GreaterThan(10);

上述程式碼先試圖將string型別轉換成int型別,如果轉換成功則對轉換後的值做大於驗證。如果轉換失敗,則不做驗證。

回撥

如果驗證失敗,可以使用回撥做一些操作。

RuleFor(x => x.Weight).NotEmpty().OnFailure(x =>
            {
                Console.WriteLine("驗證失敗");
            });

預驗證

如果需要每次呼叫驗證器前執行特定程式碼,可以通過重寫PreValidate方法來做到這一點。

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(x => x.Weight).NotEmpty();
    }

    protected override bool PreValidate(ValidationContext<Student> context,ValidationResult result)
    {
        if (context.InstanceToValidate == null) return true;
        result.Errors.Add(new ValidationFailure("", "實體不能為null"));
        return false;
    }
}

福祿ICH.架構出品

作者:福爾斯

2021年3月

相關文章