当前位置: 首页 > 工具软件 > Upida > 使用案例 >

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

颜修为
2023-12-01

目录

介绍

背景

实现

前端


介绍

在上一篇文章中,我描述了Jeneva.Net如何帮助解决实现基于WebAPIWeb应用程序中的常见问题。本文显示Jeneva.Net如何也可以大大简化您的验证例程。

*注意,本文假设您已经阅读了我的上一篇文章(后端)

背景

如果您将我上一篇文章中的技术应用于Web应用程序,则可以大大减少自定义编码的数量。但是Jeneva.Net仍然有一个非常重要且有用的功能——验证。

实现非常简单。首先,您必须确定需要验证的类,通常这些是领域类。其次,您必须为每个类标识验证组——例如:类Client有两个组——保存前验证和更新前验证。这意味着同一类——Client可以通过两种不同的方式进行验证——保存和更新。有时,您可能需要不同的验证组——例如,分配合并或其他。最后一步是实现验证器类。例如,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'");
    }
}

上面的逻辑非常有帮助,并将使验证方法非常简单易读。例如,我将不再需要为320个字符之间的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类的验证器后,我们可以自由定义一个Facade类,该类将注入我们的服务或控制器中,并将用于触发验证。这是验证外观的示例:

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之前进行验证。请参见AssertClientExists()ValidationFacade 类中的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失败时,则抛出一个带由just的异常,并且属性路径为空。

就是这样。现在验证必须起作用。如果您从业务层调用AssertValid()方法,它将根据提供的组识别要使用的类型验证器类。然后它将调用抽象Validate()方法的实现。如果验证成功,则什么都不会发生。如果验证失败,ValidationException则被抛出。ValidationException将包含名称/值对列表——属性路径和错误消息。为了在ASP.NET MVC中正确处理此异常,我将创建并注册一个自定义的ExceptionFilterAttribute。该技术在ASP.NET MVC中很常见,您可以在Internet中找到如何实现的方法。因此,这是自定义异常过滤器的实现。

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);
    }
}

我将HTTP响应内容替换为JSON数组(属性路径-错误消息)对——FailResponse类。

现在我必须注册ErrorFilterAttribute。我必须在Global.asax Application_Start事件中添加此行。

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

现在,每次抛出ValidationException,它将由ErrorHandlerAttribute处理程序处理,并且属性路径和错误消息的列表将作为JSONHTTP响应中发送回。

前端

最后一步是将这些消息显示在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验证指令的工作方式相同,即,它告诉名称文本框负责名称属性路径。下面的范围将重复显示分配给名称属性路径的验证消息。

您可以在下一篇文章中找到如何使用AngularJS创建单页应用程序(SPA):带有Upida/Jeneva的ASP.NET MVC单页应用程序(前端/AngularJS)

 类似资料: