65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (6投票s)

2013年12月3日

CPOL

7分钟阅读

viewsIcon

24299

downloadIcon

176

本文介绍了如何实现传入的 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 中正确解析(integerdouble),则注册一个失败。

正如您所注意到的,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() 方法的实现。如果验证成功,则不发生任何事情。如果验证失败,则抛出 ValidationExceptionValidationException 将包含一个名称-值对列表 - 属性路径和错误消息。为了在 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.asaxApplication_Start 事件中添加这一行。

 GlobalConfiguration.Configuration.Filters.Add(new ErrorFilterAttribute());  

现在,每次抛出 ValidationException 时,它都将由 ErrorHandlerAttribute 处理器处理,并且属性路径和错误消息列表将作为 JSON 在 HTTP 响应中发送回来。

前端

最后一步是将这些消息显示在 HTML 中的正确位置。您可以自由地为 AngularJSKnockoutJS 或其他任何框架编写自定义 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

参考文献

© . All rights reserved.