最佳实践系列:ASP.NET Core 3.0中的验证机制——给
利用资源文件管理验证错误信息
上一篇文章的最后我们提到了利用ResourceManager
对验证的默认错误信息进行管理,今天就来详细阐述一下具体的实现方式。
资源(Resource)并非什么新奇的东西,这在早期的.Net版本中已经得到了普遍使用。
-
首先,我们在项目中新建一个资源文件
新建资源.png
我们将这个资源文件命名为DataAnnotations.resx
。 -
双击打开文件后,在其中增加一些错误信息模板
错误信息.png
可以看到,以验证类的全名作为资源的名称,值中填写错误信息模板。
-
回顾一下上一篇文章的某段核心代码,没错,是
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
进行检索,从而得到对应的错误消息模板。
- 当然,
ResourceManager
我们需要在Starup.cs
中配置的时候就传递进来。
var resourceManager = new ResourceManager("MvcDemo.Web.Resources.DataAnnotations", Assembly.GetExecutingAssembly());
services.AddControllersWithViews(op => op.ModelMetadataDetailsProviders.Add(new ValidationMetadataLocalizationProvider(resourceManager)));
- 完成
由此,我们为所有的验证特性类都配置了默认的错误信息,而无需在每一个类中重复性的编写,这节省了我们在项目开发过程中的很大工作量。
同时,我们并没有失去为某个校验信息进行指定特有的错误信息的能力,依然可以通过赋值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
对象,可以让我们查询定义的资源对象。
- 随后我们要开始编写具体的实现接口。
这跟之前为验证特性没有设定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);
}
初步的实现非常简单,先不论好坏,我们继续往下走。
-
在资源文件中定义属性的
资源.pngDisplayName
。
打开DataAnnotations.resx
,我们添加一些字段进行测试。
我们添加了三行,分别以属性名为资源名称,值是该属性的DisplayName
。 -
删除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变得更加的清爽了!
- 在Startup.cs中进行配置。
services.AddControllersWithViews(op =>
{
op.ModelMetadataDetailsProviders.Add(new ValidationMetadataLocalizationProvider(resourceManager));
op.ModelMetadataDetailsProviders.Add(new DisplayAttributeLocalizationProvider(resourceManager));
});
在原来的验证默认值处理的Provider下面,我们又增加了一个Provider。
现在看运行效果。
可以看到,我们添加的三行资源对象里,ProjectName和ContractPrice因为匹配上了,所以呈现出资源文件里定义的名称。
而其他的字段因为删除了
DisplayAttribute
,导致显示名称变回属性名。
这样一来,不管在哪个ViewModel中定义了ProjectName
这个属性,我们都可以准确地得到它的显示名称,而不用重复性地定义。
- 改进模型。
之后在调试过程中,我们发现了几个小问题:
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);
}
-
效果测试
我们添加了一条资源项
资源文件
运行查看效果
页面
如预期显示。
总结
这一章我们提供了一种通过资源文件来定义验证错误信息模板、DisplayName的方法。
后续我将会继续介绍模型验证的内容,下一章也许会讲解自动化验证的处理流程,敬请期待。
注:文中的代码环境为VS2019 Preview、.NetCore3.0 Preview 7