美文网首页ASP.NET MVCAsp.net core mvcAsp.net开发
最佳实践系列:ASP.NET Core 3.0中的验证机制——给

最佳实践系列:ASP.NET Core 3.0中的验证机制——给

作者: 别逼逼快上车 | 来源:发表于2019-08-03 21:32 被阅读0次

    利用资源文件管理验证错误信息

    上一篇文章的最后我们提到了利用ResourceManager对验证的默认错误信息进行管理,今天就来详细阐述一下具体的实现方式。

    资源(Resource)并非什么新奇的东西,这在早期的.Net版本中已经得到了普遍使用。

    1. 首先,我们在项目中新建一个资源文件

      新建资源.png
      我们将这个资源文件命名为DataAnnotations.resx
    2. 双击打开文件后,在其中增加一些错误信息模板


      错误信息.png

      可以看到,以验证类的全名作为资源的名称,值中填写错误信息模板。

    3. 回顾一下上一篇文章的某段核心代码,没错,是ValidationMetadataLocalizationProvider中的CreateValidationMetadata方法。

    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        ......
        foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
        {
            ValidationAttribute tAttr = attribute as ValidationAttribute;
            if (tAttr != null && tAttr.ErrorMessage == null && tAttr.ErrorMessageResourceName == null)
            {
                var name = tAttr.GetType().Name;
                tAttr.ErrorMessage = _resourceManager.GetString(name);
            }
        }
    }
    

    在这个方法里的foreach循环中,我们获取每一个验证的特性类,根据其类型作为名称,通过ResourceManager进行检索,从而得到对应的错误消息模板。

    1. 当然,ResourceManager我们需要在Starup.cs中配置的时候就传递进来。
    var resourceManager = new ResourceManager("MvcDemo.Web.Resources.DataAnnotations", Assembly.GetExecutingAssembly());
    services.AddControllersWithViews(op => op.ModelMetadataDetailsProviders.Add(new ValidationMetadataLocalizationProvider(resourceManager)));
    
    1. 完成
      由此,我们为所有的验证特性类都配置了默认的错误信息,而无需在每一个类中重复性的编写,这节省了我们在项目开发过程中的很大工作量。
      同时,我们并没有失去为某个校验信息进行指定特有的错误信息的能力,依然可以通过赋值ErrorMessage达到目的。
      现在,我们在上一章中的ViewModel变成了如下模样:
        public class ProjectViewModel
        {
            public int? Id { get; set; }
            [Display(Name = "项目名称")]
            [Required, MaxLength(50)]
            public string ProjectName { get; set; }
            [DataType(DataType.Date)]
            [Display(Name = "开始时间")]
            [Required]
            public DateTime? BeginDate { get; set; }
            [DataType(DataType.Date)]
            [Display(Name = "截止时间")]
            [Required]
            public DateTime? EndDate { get; set; }
            [Display(Name = "所在地")]
            [Required]
            public int? LocationId { get; set; }
            [Required]
            [DataType(DataType.Currency)]
            [Display(Name = "合同金额(万元)")]
            [RegularExpression(@"\d{1,5}(.\d{1,4})?", ErrorMessage = "{0}不得超过10亿,最多输入4位小数")]
            public decimal? ContractPrice { get; set; }
            [Display(Name = "工期(日历天)")]
            [Required]
            [Range(0, 1000)]
            public int? WorkDays { get; set; }
            [Display(Name = "联系人")]
            public string ContactPhone { get; set; }
            public string Remarks { get; set; }
        }
    

    是否已看起来不是那么的凌乱?
    但其中还是有一些碍眼的地方,比如在每个属性都出现的DisplayAttribute
    对同一个实体来说,可能会存在多个ViewModel,我并不想到处出现Display(Name = "项目名称")这样的标记。
    另外,一个属性上过多的Attribute实在影响代码阅读的流畅性。
    带着这个问题,我们开始文章的第二部分。

    利用资源文件管理

    通过查阅Microsoft.AspNetCore.Mvc的源码可知,在模型验证不通过,生成错误信息的时候,ValidationAttributeAdapter是通过ModelMetadata.GetDisplayName()方法得到DisplayAttribute设置的Name值。
    而查找DefaultModelMetadata的源码,可以看到它又是从DisplayMetadata获取到的。

    public override string DisplayName
    {
        get
        {
            if (DisplayMetadata.DisplayName == null)
            {
                return null;
            }
    
            return DisplayMetadata.DisplayName();
        }
    }
    

    DisplayMetadata是通过IDisplayMetadataProvider.CreateDisplayMetadata产生的。

    public DisplayMetadata DisplayMetadata
    {
        get
        {
            if (_details.DisplayMetadata == null)
            {
                var context = new DisplayMetadataProviderContext(Identity, _details.ModelAttributes);
                _detailsProvider.CreateDisplayMetadata(context);
                _details.DisplayMetadata = context.DisplayMetadata;
            }
    
            return _details.DisplayMetadata;
        }
    }
    

    这就又给了我们可乘之机。

    1.首先,我们也编写一个实现IDisplayMetadataProvider的对象。

    public class DisplayAttributeLocalizationProvider : IDisplayMetadataProvider
        {
            private ResourceManager _resourceManager;
            public DisplayAttributeLocalizationProvider(ResourceManager resourceManager)
            {
                _resourceManager = resourceManager;
            }
    
            public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
            {
            }
        }
    

    在这个对象中我们同样接收一个ResourceManager对象,可以让我们查询定义的资源对象。

    1. 随后我们要开始编写具体的实现接口。
      这跟之前为验证特性没有设定ErrorMessage属性,我们给他默认赋值一个是一样的思路:
      如果某个对象的属性没有定义DisplayAttribute,那我们就为它自动赋值一个DisplayName的默认值!
      Name值从哪里来?自然是从资源文件中获取!
      具体实现接口的代码如下:
    public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
    {
        var attributes = context.Attributes;
        var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
        if (displayAttribute != null)
        {
            return;
        }
        context.DisplayMetadata.DisplayName = () => _resourceManager.GetString(context.Key.Name);
    }
    

    初步的实现非常简单,先不论好坏,我们继续往下走。

    1. 在资源文件中定义属性的DisplayName
      打开DataAnnotations.resx,我们添加一些字段进行测试。

      资源.png
      我们添加了三行,分别以属性名为资源名称,值是该属性的DisplayName
    2. 删除ViewModel中所有的DisplayAttribute

    public class ProjectViewModel
        {
            public int? Id { get; set; }
            [Required, MaxLength(50)]
            public string ProjectName { get; set; }
            [DataType(DataType.Date)]
            [Required]
            public DateTime? BeginDate { get; set; }
            [DataType(DataType.Date)]
            [Required]
            public DateTime? EndDate { get; set; }
            [Required]
            public int? LocationId { get; set; }
            [Required]
            [DataType(DataType.Currency)]
            [RegularExpression(@"\d{1,5}(.\d{1,4})?", ErrorMessage = "{0}不得超过10亿,最多输入4位小数")]
            public decimal? ContractPrice { get; set; }
            [Required]
            [Range(0, 1000)]
            public int? WorkDays { get; set; }
            public string ContactPhone { get; set; }
            public string Remarks { get; set; }
        }
    

    现在的ViewModel变得更加的清爽了!

    1. 在Startup.cs中进行配置。
    services.AddControllersWithViews(op =>
    {
        op.ModelMetadataDetailsProviders.Add(new ValidationMetadataLocalizationProvider(resourceManager));
        op.ModelMetadataDetailsProviders.Add(new DisplayAttributeLocalizationProvider(resourceManager));
    });
    

    在原来的验证默认值处理的Provider下面,我们又增加了一个Provider。

    现在看运行效果。

    页面.png
    可以看到,我们添加的三行资源对象里,ProjectName和ContractPrice因为匹配上了,所以呈现出资源文件里定义的名称。
    而其他的字段因为删除了DisplayAttribute,导致显示名称变回属性名。

    这样一来,不管在哪个ViewModel中定义了ProjectName这个属性,我们都可以准确地得到它的显示名称,而不用重复性地定义。

    1. 改进模型。
      之后在调试过程中,我们发现了几个小问题:
      1)我们只需要对ViewModel进行配置DisplayName的操作,其他的类不需要、也最好别去动它。但目前的做法是会影响到所有的类。
      2)有时候我们对于相同的属性名,需要显示不同的名称,需要有一种自定义的能力。

    对于问题1,我们自定义了一个AutoDisplayAttribute类,用于标记。

        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
        public class AutoDisplayAttribute : Attribute
        {
        }
    

    没错,这个特性类的实现是空的。
    我们给我们需要的ViewModel进行标记,让我们自己的IDisplayMetadataProvider能够识别出来即可。

    [AutoDisplay]
    public class ProjectViewModel
    {
    ......
    }
    

    第二个问题,处理方式有很多,比如可以直接通过DisplayAttribute进行定义,这样就不会被自动程序所赋值。
    当然,我们根据自身业务所需要的场景提供了一种根据“类名.属性名”进行精确匹配的实现。

    最后,改进后的接口的代码如下:

    public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
    {
        var attributes = context.Attributes;
        var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
        if (displayAttribute != null)
        {
            return;
        }
    
        //只有标记了AutoDisplay的类或属性才能启动自动读取
        var hasAutoDisplay = context.Key.ModelType.GetCustomAttributes(typeof(AutoDisplayAttribute), true).Length > 0 ||
            context.Key.ContainerType?.GetCustomAttributes(typeof(AutoDisplayAttribute), true).Length > 0;
        if (!hasAutoDisplay)
        {
            return;
        }
    
        //优先读取模式:ClassA.PropertyB
        if (context.Key.ContainerType != null)
        {
            var name = context.Key.ContainerType.Name + "." + context.Key.Name;
    
            if (_resourceManager.GetString(name) != null)
            {
                context.DisplayMetadata.DisplayName = () => _resourceManager.GetString(name);
                return;
            }
        }
    
        context.DisplayMetadata.DisplayName = () => _resourceManager.GetString(context.Key.Name);
    
    }
    
    1. 效果测试
      我们添加了一条资源项


      资源文件

      运行查看效果


      页面
      如预期显示。

    总结

    这一章我们提供了一种通过资源文件来定义验证错误信息模板、DisplayName的方法。
    后续我将会继续介绍模型验证的内容,下一章也许会讲解自动化验证的处理流程,敬请期待。

    注:文中的代码环境为VS2019 Preview、.NetCore3.0 Preview 7

    相关文章

      网友评论

        本文标题:最佳实践系列:ASP.NET Core 3.0中的验证机制——给

        本文链接:https://www.haomeiwen.com/subject/vhjqdctx.html