在MVC2.0 中 进行 LINQTOSQL 实体统一验证方法(上)

发布于 2022-09-07 15:18:20 字数 20942 浏览 10 评论 1

场景
      当我把项目从 MVC1.0 升级到 MVC2.0 时,原以为可以方便的使用 System.ComponentModel.DataAnnotations 结合 MVC2.O 的
      ModelState.IsValid 进行数据有效验证。比如以下验证: 1 public class SystemUserMetaData
2     {
3         [Required(ErrorMessage =
"不能为空!")]
4         [StringLength(6, ErrorMessage =
"用户名长度不能超过6!")]
5         public string UserName { get; set; }
6         [Required(ErrorMessage =
"IsValid is required.")]
7         public string ChineseName { get; set; }
8         [Required(ErrorMessage =
"IsValid is required.")]
9         public bool IsValid { get; set; }
10         [Required(ErrorMessage =
"Department is required.")]
11         public int DepartmentID { get; set; }
12         [Required(ErrorMessage =
"Password is required.")]
13         public string Password { get; set; }
14         [Required(ErrorMessage =
"Rank is required.")]
15         public int RankID { get; set; }
16         [PhoneAttribute(ErrorMessage =
"电话号码不正确")]
17         public string MobilePhone { get; set; }
18         public int UserID { get; set; }
19     }

代码1 1
public
class SystemUserMetaData
2     {
3         [Required(ErrorMessage =
"不能为空!")]
4         [StringLength(6, ErrorMessage =
"用户名长度不能超过6!")]
5
public
string UserName { get; set; }
6         [Required(ErrorMessage =
"中文名不能为空")]
7
public
string ChineseName { get; set; }
8         [Required(ErrorMessage =
"部门不能为空")]
9
public
int DepartmentID { get; set; }
10         [Required(ErrorMessage =
"密码不能为空")]
11
public
string Password { get; set; }
12         [Required(ErrorMessage =
"职位不能为空")]
13
public
int RankID { get; set; }
14         [PhoneAttribute(ErrorMessage =
"电话号码不正确")]// 自定义ValidationAttribute “只能验证 MobilePhone 的值”
15

public
string MobilePhone { get; set; }
16
public
int UserID { get; set; }
17     }

     这些Annotation特性验证可以很轻松通过 mvc2.0  ViewData.ModelState.Values 获取到验证错误的提示信息。但是当我们的验证条件变得更加
复杂时,比如在修改一个LinqToSQL 实体时需通过该实体的主键和唯一索引进行验证实体是否唯一性时,此时需要两个字段同时验证,当这种验证出现时我
发现无法简单的使用 DataAnnotaion 进行同一实体的多字段验证。自定义 ValidationAttribute 特性重写 IsValid 时 无法根据当前的属性获取到其他属性
的值。因为ValidationAttribute 特性是附加在一个类的属性上的。可能聪明的你此刻已想到了将验证特性直接加载 LinqToSQL 的 类上。当你为这个特性
编写验证方法时就可以通过反射得到 LinqToSql 实体的所有属性的值,或许单一的 ValidationAttribute 属性验证特性不能完成的任务就可以得到解决。
        当我把LINQTOSQL 类的验证特性写完后附加到 LinqTOSQL partial 类上代码如下:    [UniqueName("UserID", "UserName", typeof(SystemUser), ErrorMessage =
"该用户已存在。")]
    [MetadataType(
typeof(SystemUserMetaData))]
   
public
partial
class SystemUser { }

在MVC2.0 中当我们使用 TryUpdateModel 方法时 发现 UniqueName 的 IsValid 方法始终没有被调用。但是当 MetadataType 移除除掉,我们再调用
TyUpdateaModel方法时UniqueName 特性的 IsValid 验证方法就被正常调用了。此时我明白了问题应该是由 MVC  TryUpdateModel 方法引起,将该方
法换成 UpdateModel 后问题依旧。MetadataType 特性覆盖了 UniqueName 特性,当然了如果想知道具体的原因,可以 Reflect 出 TryUpdateModel
的方法找到到答案。为了解决这个问题,我决定使用自定义的方法进行实体验证,代码如下:代码3
public
class Validation
    {
        
public
static
void ValidateAttributes<TEntity>(TEntity entity)
        {
            var validationInstance
=
new Validation();
            validationInstance.ValidateAttributesInternal(entity);
        }

        public
virtual
void ValidateAttributesInternal<TEntity>(TEntity entity)
        {
            var validationIssues
=
new List<ValidationIssue>();

            var props =
typeof(TEntity).GetProperties();
            var metatype
=
typeof(TEntity).GetCustomAttributes(typeof(MetadataTypeAttribute), false).FirstOrDefault();
            var type
= ((System.ComponentModel.DataAnnotations.MetadataTypeAttribute)(metatype)).MetadataClassType;
            var s
= type.GetProperties();

            var customAttrs =
typeof(TEntity).GetCustomAttributes(true).Where(t => t.GetType().Namespace.Contains("ValidationMeta"));
            
foreach (var attr in customAttrs)
            {
                var validate
= (ValidationAttribute)attr;
               
//执行 附加在 linqtosql partial 类 上的 ValidationAttribute 验证方法

bool valid = validate.IsValid(entity);
               
if (!valid)
                {
                    validationIssues.Add(
new ValidationIssue(null, null, validate.ErrorMessage));
                }
            }

            //执行附加在  linqtosql partial 类 属性上的 ValidationAttribute 验证方法

foreach (var prop in s)
                ValidateProperty(validationIssues, entity, prop);

            // throw exception?

if (validationIssues.Count >
0)
               
throw
new ValidationIssueException(validationIssues);
        }

        protected
virtual
void ValidateProperty<TEntity>(List<ValidationIssue> validationIssues, TEntity entity, PropertyInfo property)
        {
            
//得到验证特性的集合
            var validators = property.GetCustomAttributes(typeof(ValidationAttribute), false);

            foreach (ValidationAttribute validator in validators)
                ValidateValidator(validationIssues, entity, property, validator);
        }

        protected
virtual
void ValidateValidator<TEntity>(List<ValidationIssue> validationIssues, TEntity entity, PropertyInfo property, ValidationAttribute validator)
        {
            var dataEntityProperty
=
typeof(TEntity).GetProperties().FirstOrDefault(p => p.Name == property.Name);
            var value
= dataEntityProperty.GetValue(entity, null);

            if (!validator.IsValid(value))
            {
                validationIssues.Add(
new ValidationIssue(property.Name, value, validator.ErrorMessage));
            }
        }
    }

大家留意一下代码3 中的注释,这样 Validation 这个类就就可以替代MVC TryUpdateModel 的验证功能同时让代码1的 UniqueName 和 MetaDataType 两个特性 “共存”。

MetadataType 的职责:验证实体的单一属性值的有效性。
LINQ实体类上的其他的自定义特性:如代码1中的 UniqueName 则可以进行复杂的属性验证如多属性值同时验证等。
这样我们就彻底的解决了开发过程中验证代码统一的编码规范。而不是同一个数据有效性验证的代码满天飞的局面。
小结

当我完成了以上代码似乎已经达到了预期的目的,但测试代码时候发现如果使用TryUpdateModel 更新另外一个LINQTOSQL 模型(Order表),这个被
更新的模型从数据库上来看它属于 SystemUser 的外键表。通过Order表中的UserID 字段关联到 SystemUser。当Order实体被MVC TryUpdateModel 时会同时把SystemUser 的 自定义的 [UniqueName] 特性的方法 IsValid() 也调用了,很显然这不是我们想要的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

∞梦里开花 2022-09-10 11:51:13

当验证都规范好后再测试代码发现还存在一些隐藏问题未解决。问题的产生请看下图:

假设我们的数据库只有这两张表

因为项目建立在LinqToSQL基础上,所以当我们在MVC内调用 TryUpdateModel 将 UI 传递过来的 FormCollection 表单值赋值到LinqToSQL实体对象属性。如果该实体对象从数据库来说只是一张基础表此表会做为其他表的外键表。如上图中的 Department 表,当我们保存Department到数据库前,调用 MVC 的TryUpdateModel 时该方法将验证 MetadataType 指定的 ValidationAttribute 特性。代码如下:[UniqueDepartmentName("DepartmentName")]
[MetadataType(
typeof(DepartmentMetaData))]
public
partial
class Department { }

上一篇文章有提及到MVC2.0默认验证只能执行DepartmentMetaData 类中的 ValidationAttribute 特性,是无法执行UniqueDepartmentName 这个自定义特性的IsValid方法。所以我自定义了独立的模型验证代码解决这个问题。随后我发现了更奇怪的问题,当我保存SystemUser 这个LinqToSql的对象时同样执行TryUpdateModel 方法给该对象赋值。作为Department 类的自定义ValidationAttribute 特性 UniqueDepartmentName 的 IsValid方法奇迹般的被执行了...
看起来有点痛苦,当你想执行时它不执行,不应该执行时它执行的却挺好!该如何解决这个问题呢?难道不用TryUpdateModel 这个方法了?当然不太现实,毕竟这个方法让我们少写了很多机械性的代码。于是我写了一个通用方法:UpdateEntityValue
///
<summary>
/// 更新目标对象属性值,该值从FormCollection 获取。
        
///
</summary>
///
<typeparam name="T"></typeparam>
///
<param name="targetObj"></param>
///
<param name="sourceObj"></param>
///
<returns></returns>

public
static
void UpdateEntityValue<T>(T targetObj, T sourceObj) where T : class
        {
            
try
            {
                Type targetType
= targetObj.GetType();
                PropertyInfo[] targetProps
= targetType.GetProperties();
                targetProps
= targetProps.Where(p => p.GetCustomAttributes(typeof(ColumnAttribute), true).Count() >
0).ToArray();

                Type sourceType = targetObj.GetType();
                PropertyInfo[] sourceProps
= sourceType.GetProperties();
                sourceProps
= sourceProps.Where(p => p.GetCustomAttributes(typeof(ColumnAttribute), true).Count() >
0).ToArray();

                foreach (var tProp in targetProps)
                {
                    PropertyInfo sProp
= sourceProps.FirstOrDefault(s => s.Name == tProp.Name);
                    tProp.SetValue(targetObj, sProp.GetValue(sourceObj,
null), null);
                }
            }
            
catch
            {

            }
        }

UpdateEntityValue 方法很简单,不作解释了。该方法调用如下:代码 1
public JsonResult SaveSystemUser(FormCollection fc)
2         {
3
try
4             {
5                 SystemUser user =
null;
6
string strUserID = fc.Get("UserID");
7
8
if (String.IsNullOrEmpty(strUserID))//新增
9
                {
10                     user =
new SystemUser();
11                     TryUpdateModel(user);
12                     _DataContext.SystemUsers.InsertOnSubmit(user);
13                 }
14
else//修改
15
                {
16                     user = _DataContext.SystemUsers.FirstOrDefault(u => u.UserID ==
int.Parse(strUserID));
17                     SystemUser sourceUser =
new SystemUser();
18
//TryUpdateModel(user);//TryUpdateModel 该方法更新模型对象属性值时同时还进行了 Department 该类的ValidationAttribute验证。
19
                    TryUpdateModel(sourceUser);
20                     UpdateEntityValue<SystemUser>(user, sourceUser);//这样做就不会执行Department 这个类的 UniqueDepartmentName 自定义 的ValidationAttribute 验证方法
21
                }
22
23
//独立验证实体,具体见我的 在MVC2.0 中 进行 LINQTOSQL 实体统一验证方法(上)这篇博文。
24
                Validation.ValidateAttributes<SystemUser>(user);
25
26                 _DataContext.SubmitChanges();
27
28
return Json(new { Success =
true, Msg =
"保存成功。" }, JsonRequestBehavior.AllowGet);
29             }
30
catch
31             {
32
return Json(new { Success =
false, Msg =
"保存失败。" }, JsonRequestBehavior.AllowGet);
33             }
34         }

产生这问题的关键所在是由于TryUpdateModel SystemUser 这个LinqToSQL对象时,将这个对象的Department 属性上Annotation验证方法也给执行了。通过UpdateEntityValue这个方法解决了这个问题,貌似又“风平浪静”了,一切又正如我们所预期的那样。细心的你会发现如果这样子不是每次都要为TryUpdateModel 准备一个 source 对象。这样的代码会在工程内出现无数次,这也不是我们想见到的。话说MVC提供给开发人员很多自定义的特性。有没有办法改造以上代码呢?答案当然是肯定的。我们可以让TryUpdateModel只负责赋值模型属性值不负责验证,这做法在我的项目里是完全可以的,因为我只调用自定义的 Validation.ValidateAttributes<SystemUser>(user) 验证模型。不用MVC内置模型验证。那如何做到只赋值不验证呢,其实我们只要修改 MVC2.0 的 ModelBinders.Binders.DefaultBinder 即可。在Global.asax的 Application_Start() 方法内添加 ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); CustomModelBinder 类是自定义的类,其基类是 DefaultModelBinder 我们只要重写这个基类的 OnPropertyValidated 虚方法。代码如下:代码
public
class CustomModelBinder : DefaultModelBinder
    {
     
        
protected
override
void OnPropertyValidated(ControllerContext controllerContext,
                ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor,
               
object value)
        {

            // Do Nothing;

        }
    }

这样先前 UpdateEntityValue 这个方法就可以剔除出去了,代码也得到了进一步优化。

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文