使用 Upida/Jeneva.Net 验证传入 JSON






4.75/5 (6投票s)
本文介绍了如何实现传入的 JSON 数据验证
引言
在上一篇文章中,我描述了 Jeneva.Net 如何帮助解决实现基于 WebAPI 的 Web 应用程序中的常见问题。本文展示了 Jeneva.Net 如何也能极大地简化您的验证例程。
*注意,本文假设您已阅读了我的上一篇文章。
背景
如果您在 Web 应用程序中应用我上一篇文章中的技术,可以大大减少自定义编码量。但 Jeneva.Net 仍然有一个非常重要且有用的功能——验证。
实现相当简单。首先,您必须识别需要验证的类,通常这些是领域类。其次,您必须为每个类识别验证组——例如:类 Client
有两个组——保存前验证和更新前验证。这意味着同一个类——Client
可以以两种不同的方式进行验证——用于保存和用于更新。有时,您可能需要不同的验证组——例如,分配(Assign)或合并(Merge)或任何其他。最后一步是实现一个验证器类。例如,Client
类将拥有验证器——ClientValidator
。
实施
让我们为 Client
类创建这些验证方法。为了遵循所有 SOLID 原则,我将创建一个单独的类——ClientValidator
,它将包含这些验证方法。基于 Jeneva 的验证主要思想如下:您必须在每次需要验证时创建 JenevaValidationContext
类的新实例。每次发现错误时,您必须使用其方法将其注册到上下文实例中。使用上下文实例可确保每条错误消息都与相应的属性路径相关联。顺便说一句,上下文类已经包含了一些简单的验证例程,例如,它可以检查特定字段是否为 null
,或者它是否出现在 JSON 中,它可以检查文本长度或集合大小,它可以检查正则表达式等。您应该知道,Jeneva 管理数据反序列化,并且它存储了每个 JSON 字段的信息,因此稍后您可以验证字段是否存在于 JSON 中,它是否为 null
或在反序列化期间是否正确解析。此信息可通过 JenevaValidationContext
类方法访问。
JenevaValidationContext
类的一个主要目标是跟踪属性路径。例如,当您验证一个对象然后验证其嵌套子对象时,上下文类会确保所有错误消息都与相应的属性路径连接。验证结果是一个失败列表,其中失败是属性路径文本和消息。此失败结构被序列化为 JSON 并发送回浏览器,在那里它被解析并在 HTML 中的正确位置正确显示。
最佳实践是派生自 JenevaValidationContext
类,在其中扩展一些额外的应用程序特定验证例程,然后在验证方法中使用子类。下面是扩展上下文类的示例
public class ValidationContext : JenevaValidationContext
{
public ValidationContext(IJenevaContext jenevaContext)
: base(jenevaContext)
{
}
public void Required()
{
this.Assigned("is required");
if (this.IsFieldValid && this.IsValidFormat())
{
this.NotNull("is required");
}
}
public void Missing()
{
this.NotAssigned("must be empty");
}
public void Number()
{
this.ValidFormat("must be valid number");
}
public void Date()
{
this.ValidFormat("must be valid date");
}
public void Float()
{
this.ValidFormat("must be valid floating point number");
}
public void Email()
{
const string expr = @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b";
this.Regex(expr, "must be valid Email");
}
public void Text()
{
this.StringLengthBetween(3, 20, "must be between 3 and 20 characters");
}
public void TrueFalse()
{
this.ValidFormat("must be 'yes' or 'no'");
}
}
上面的逻辑非常有用,它将使验证方法极其简单和可读。例如,我将不再需要为 3 到 20 个字符之间的 string
编写冗余错误消息,我将使用 Text()
。日期、双精度浮点数、数字、必填字段等也是如此。
在下面的代码片段中,您可以看到 Save
的验证方法 ValidateForSave()
是如何实现的
public class ClientValidator : IClientValidator
{
public ILoginValidator LoginValidator { get; set; }
public void ValidateForSave(Client target, IValidationContext context)
{
context.SetField("id", target.Id); // validate id property now
context.Missing(); // it must be missing - not present in json
context.SetField("name", target.Name); // validate name property now
context.Required(); // it is required - present in json and not null
context.Text(); // it also must be between 3 and 20 characters length
context.SetField("lastname", target.Lastname);
context.Required();
context.Text();
context.SetField("age", target.Age);
context.Required();
context.Number();
context.MustBeGreaterThan(0, "must be greater than zero");
context.SetField("logins", target.Logins);
context.Required();
context.MustHaveCountBetween(1, 5, "must be at least one login");
context.AddNested(); // let us validate child properties - logins
int index = 0;
foreach (Login login in target.Logins)
{
context.SetIndex(index++); // the logins - is indexed property - property path will have index - logins[4].name
context.SetTarget(login); // validate login class
this.LoginValidator.ValidateForSave(login, context);
}
context.RemoveNested(); // switch back - up in property path - back to Client's properties
}
}
SetField()
方法告诉上下文当前正在验证哪个字段,即,如果您在上下文中注册了一个失败,它将具有当前字段的属性路径。验证例程必须在 SetFiled()
方法调用之后。JenevaValidationContext
类包含许多验证例程。这些例程通常执行两个操作:首先 - 它们检查一个条件(例如,检查字段值是否为 null
),其次 - 如果条件不为真,它们会注册一个失败(使用 Fail()
方法)。例如,Assigned()
- 检查字段是否已分配(是否存在于 JSON 中),如果为 false
- 它使用当前属性路径注册一个失败;NotNull()
、Null()
是自描述的;ValidFormat()
- 如果字段值未从 JSON 中正确解析(integer
或 double
),则注册一个失败。
正如您所注意到的,JenevaValidationContext
类除了验证例程方法之外,还包含其他重要方法:SetTarget()
设置当前验证对象,此方法很重要,应始终在任何验证例程之前调用;AddNested()
- 此方法将当前属性作为嵌套对象传播到属性路径中,所有后续对 SetField()
的调用都将导致属性名称与嵌套对象名称连接;RemoveNested()
方法执行相反的操作;SetIndex()
方法还会向当前属性路径添加索引 - "[" + index + "]
"。
这里您可以看到 Login
类的验证方法。
public class LoginValidator : ILoginValidator
{
public void ValidateForSave(Login target, IValidationContext context)
{
context.SetField("id", target.Id);
context.Missing();
context.SetField("name", target.Name);
context.Required();
context.Text();
context.SetField("password", target.Password);
context.Required();
context.Text();
context.SetField("enabled", target.Enabled);
context.TrueFalse();
context.SetField("client", target.Client);
context.Missing();
}
}
当所有领域和 DTO 类的验证器都完成后,我们就可以自由地定义一个外观类,该类将被注入到我们的服务或控制器中,并用于触发验证。这是验证外观的一个示例
public class ValidationFacade : IValidationFacade
{
public IValidationContextFactory ContextFactory { get; set; }
public IClientValidator ClientValidator { get; set; }
public void AssertClientForSave(Client target)
{
IValidationContext context = this.ContextFactory.GetNew();
context.SetTarget(target);
this.ClientValidator.ValidateForSave(target, context);
context.Assert();
}
public void AssertClientForUpdate(Client target)
{
IValidationContext context = this.ContextFactory.GetNew();
context.SetTarget(target);
this.ClientValidator.ValidateForUpdate(target, context);
context.Assert();
}
}
这个类看起来非常简单,我在这里使用简单的工厂方法来获取验证上下文的新实例。最重要的是每个方法的最后一行——Assert()
——如果上下文中至少注册了一个错误,该方法会抛出 ValidationException
,否则它什么也不做。
在这里您可以看到外观是如何在服务层中注入和使用的
public class ClientService : IClientService
{
public IMapper Mapper { get; set; }
public IValidationFacade Validator { get; set; }
public IClientDao ClientDao { get; set; }
public Client GetById(int id)
{
Client item = this.ClientDao.GetById(id);
return this.Mapper.Filter(item, Levels.DEEP);
}
public IList<client> GetAll()
{
IList<client> items = this.ClientDao.GetAll();
return this.Mapper.FilterList(items, Levels.GRID);
}
public void Save(Client item)
{
this.Validator.AssertClientForSave(item);
using (ITransaction tx = this.ClientDao.BeginTransaction())
{
this.Mapper.Map(item);
this.ClientDao.Save(item);
tx.Commit();
}
}
public void Update(Client item)
{
this.Validator.AssertClientForUpdate(item);
using (ITransaction tx = this.ClientDao.BeginTransaction())
{
Client existing = this.ClientDao.GetById(item.Id.Value);
this.Mapper.MapTo(item, existing);
this.ClientDao.Merge(existing);
tx.Commit();
}
}
}
顺便说一句,验证上下文类非常方便,易于扩展和以不同方式使用。您总是可以尝试其方法并获得不同的行为。在示例代码中,您将看到它如何在不同情况下用于验证 - 例如,Delete
前的验证。请参阅 ValidationFacade
类中的 AssertClientExists()
和 AssertMoreThanOneClient()
方法。
public void AssertClientExists(Client item)
{
if (item == null)
{
IValidationContext context = this.ContextFactory.GetNew();
context.Fail("Client does not exist");
context.Assert();
}
}
public void AssertMoreThanOneClient(long count)
{
if (count == 1)
{
IValidationContext context = this.ContextFactory.GetNew();
context.Fail("Cannot delete the only client");
context.Assert();
}
}
在这些方法中,您不依赖于目标验证对象。您只需断言满足某个条件。在这种情况下,会抛出一个只包含一个失败的 ValidationException
,并且属性路径为空。
就是这样。现在验证应该可以工作了。如果您从业务层调用 AssertValid()
方法,它将根据提供的组识别要使用的验证器类类型。然后它将调用抽象的 Validate()
方法的实现。如果验证成功,则不发生任何事情。如果验证失败,则抛出 ValidationException
。ValidationException
将包含一个名称-值对列表 - 属性路径和错误消息。为了在 ASP.NET MVC 中正确处理此异常,我将创建并注册一个自定义的 ExceptionFilterAttribute
。这种技术在 ASP.NET MVC 中很常见,您可以在互联网上找到如何操作。因此,这是自定义异常过滤器的实现。
public class ErrorFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
FailResponse response;
if (context.Exception is ValidationException)
{
response = (context.Exception as ValidationException).BuildFailResponse();
}
else
{
response = new FailResponse(context.Exception.ToString());
}
context.Response =
context.Request.CreateResponse(HttpStatusCode.InternalServerError, response);
}
}
我用我的 JSON 数组(属性路径 - 错误消息)对替换 HTTP 响应内容 - FailResponse
类。
现在我必须注册 ErrorFilterAttribute
。我必须在 Global.asax 的 Application_Start
事件中添加这一行。
GlobalConfiguration.Configuration.Filters.Add(new ErrorFilterAttribute());
现在,每次抛出 ValidationException
时,它都将由 ErrorHandlerAttribute
处理器处理,并且属性路径和错误消息列表将作为 JSON 在 HTTP 响应中发送回来。
前端
最后一步是将这些消息显示在 HTML 中的正确位置。您可以自由地为 AngularJS 或 KnockoutJS 或其他任何框架编写自定义 JavaScript。Jeneva.Net 附带了一个用于 AngularJS 验证的小型 JavaScript 库。这些库使显示这些错误变得更简单。例如,如果您使用 Angular,您的 HTML 必须看起来像这样
<label>Name:</label>
<input name="name" type="text" ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath" ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
jVpath
指令的作用与任何 AngularJS 验证指令相同,即它告诉名称文本框负责“name”属性路径。下面的 span 将重复显示分配给“name”属性路径的验证消息。
您可以在我的下一篇文章中了解如何使用 AngularJS 创建单页应用程序 (SPA): AngularJS 单页应用程序和 Upida/Jeneva.Net。