目录
无论是进行ASP.NET Core MVC还是ASP.NET Core Web API项目开发,我们都应该对请求数据的合法性进行校验,比如重置密码操作中,我们需要检查两次输入的密码是否一致,以及密码的长度是否符合要求。对请求数据进行充分的合法性校验不仅有助于提升用户界面的友好性,而且有助于提高后台程序的安全性和稳定性。
为了提高响应速度和界面的可用性,一般在客户端我们都会对用户填写的数据进行校验,这样不需要把数据发送到服务器端,用户就会知道数据填写错误。但是我们不能完全依赖客户端的校验,不仅因为恶意用户可以绕过客户端校验直接向服务器发起请求,而且服务器端也需要对于客户端开发人员对数据校验不到位的地方做兜底的工作。因此,服务器端的数据校验必不可少。
1、.NET Core内置数据校验的不足
.NET Core中内置了对数据校验的支持,在System.ComponentModel.DataAnnotations命名空间下定义了非常多的校验规则Atribute,比如[Required]用来设置值必须是非空的、Emilddress用来设置值必须是Email格式的、[ReglarExpression]用来根据给定的正则表达式对数据进行校验。我们也可以使用CustomValiatonoAttibute或者模型类实现IValidatableObjet接口来编写自定义的校验规则。关于.NET Core内部数据校验机制的用法可以参考官方文档。
.NET Core内置的校验机制有以下几个问题。
1)、无论是通过在属性上标注校验规则Atibue的方式,还是实现Vildiablbiel接口的方式,我们的校验规则都是和模型类耦合在一起的, 这违反了面向对象的“单一职责原则"。
2)、.NET Core中内置的校验规则不够多,根多常用的校验需求都用要我们编导自定义验规则。
2、Fluentaldation的基本使用
上文中,我们提到了.NET Core中内置数据校验机制的不足,这里向读者推荐优秀的数据校验框架FentValidaion 它可以让我们用类似于.NET Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中。 FluentValidation可以用于控制台、WPF、ASP.NET Core等各种.NET项目中,这里只讲第它在ASP.NET Core项目中的用法。
第1步,在项目中安装NuGet包FluentValidation.AspNetCore.
第2步,在Program.cs中添加注册相关服务的代码
builder.Services.AddFluentvalldation(fv => {
Assembly assembly = Assembly.GetExecutiongAssemblyt();
fv.RegisterValidatorsFromAssembly (asserbly);
}):
ReitrVaidatorsFrmossernbly 方法用于把指定程序集中所有实现了IVaidator 接口的数据校验类注册到依赖注入容器中。因为这个例子中只有一个项目, 所有的数据校验类也都写到了这个项目中,所以我们用Assembly.GetExecutingAssembly获取入口项目的程序集。在实际的项目中,数据校验类可能会位于多个程序集中,我们可以调用RigisterValirdatorsFromAssemblies来指定这些程序集进行注册。
第3步,编写一个模型类Login2Request,如下:
public record Login2Request(string Email,string Password,string Password2);
可以看到,这里的 Login2Request类只是一个普通的C#类,没有标注任何的 Attribute或者实现任何的接口,这个类的唯一责任就是传递数据。
第4步,编写一个继承自 AbstractValidator的数据校验类,如下:
public class Login2RequestValidator: AbstractValidator<Login2Request>
{
public Login2RequestValidator(){
RuleFor(x=>x.Email) .NotNull().EmailAddress()
.Must(v=>v,EndsWith("@qq.com")||v.Endswith("@163.com"))
.WithMessage("只支持QQ和163邮箱");
RuleFor(x =>x.Passward).NotNull().Length (3,10)
.withMessage(“密码长度必须介于3到10之间”)
.Equal(x =>x.Password2) .WithMessage("两次密码必须一致");
}
}
数据校验类一般继承自AbstractValidator,AbstractValidator类是一个泛型类,我们需要通过泛型参数指定这个数据校验类对哪个类进行校验;校验规则写到校验类的构造方法中;我们通过RuleFor 来指定要对哪个属性进行校验,然后使用NotNull(非空)、EmailAddress(邮箱地址)、Length(字符串长度)等内置方法来编写校验规则;多个校验规则可以采用链式调用的写法;每个需要校验的属性对应一组 RuleFor调用,上面的第5~7行代码用于对 Email属性进行校验,而第8~10行代码用于对 Password属性进行校验。
FluentValidation中内置了丰富的校验规则,如果想编写自定义校验规则,我们可以在Must方法中编写,如第6行代码所示。
FluentValidation内置的校验规则有默认的报错信息,我们也可以通过 WithMessage方法自定义报错信息,WithMessage方法设置的报错信息只作用于它之前的那个校验规则,如第7行代码和第10行代码所示。
第5步,我们编写一个操作方法,用Login2Request作为参数。然后我们在客户端向这个操作方法对应的路径发送非法的数据,服务器端响应报文。
可以看到,使用 FluentValidation 以后,我们可以把数据校验的规则写到单独的数据校验类中,这样模型类和数据校验类各司其职,符合“单一职责原则”,而且在FluentValidation中编写自定义校验代码也更加简单。FluentValidation和.NET Core内置的校验方式是可以共存的,也就是我们可以一部分校验规则用FluentValidation写,另一部分校验规则用.NET Core内置的校验方式写,不过为了代码的统一,建议不要混用这两者。
3、FluentValidation中注入服务
在编写数据校验代码的时候,有时候我们需要调用依赖注入容器中的服务,FluentValidntiotth的籽据校验类是通过依赖注入容器实例化的,因此我们同样可以通过构造方法来向数据校验类中注入服务。本节中,我们来看FluentValidation中如何注入服务。
假如数据库的一张表中记录着系统中已有的用户名、密码等信息,用户表的实体类为User,我们通过TestDbContext来读取数据库。EF Core的这些代码比较简单,这里就不再重复。
下面来实现在登录的时候检查用户名是否存在的校验类。
定义一个类Logn3Request ,包含UeseName用户名、Password密码两个属性。然后,我们再编写一个对Logn3Request 进行校验的类,如下:
public class Login3RequestValidator:AbstractValidator<Login3Request>
{
public Login3RequestValidator (TestDbcContext dbCtx)
{
RuleFor(x => x.UserName) .NotNull()
.Must (name=>dbCtx.Users.Any (u=>u.UserName== name))
.withMessage(c => $"用户名{c.UserName )不存在");
}
}
可以看到,我们通过构造方法注入了TestDbContext,并且在第6行代码的Must方法中使用了TestDbContext 服务检查用户名是否存在。值得注意的是,我们在第7行代码的WibMes 方法中还可以用Lambda表达式的形式使用模型类中的属性对报错信息进行格式化
由于异步代码通常能给系统带来更好的吞吐量,因此我们编写代码的原则是“能用异步代码就不要用同步代码”。第6行代码使用同步的Any方法判断用户名是否存在,而EF Core中 有一个AnyAsync方法,它是异步版本的Any方法。在FluentValidation中我们可以用MustAsyme 和AnyAsync来编写异步校验规则,如下:
RuleFor (x => x.UserName).NotNull ()
.MustAsync((name,_) => dbCtx.Users.AnyAsync(u=> u.UserName == name))
.WithMessage(c => $"用户名{c.UserName }不存在");