Util应用程序框架公共操作类(四):验证公共操作类

  为了能够验证领域实体,需要一个验证公共操作类来提供支持。由于我将使用企业库(Enterprise Library)的验证组件来完成这项任务,所以本文也将演示对第三方框架的封装要点。

  .Net提供了一个称为DataAnnotations的验证技术,即在对象的属性上添加一些Attribute,比如[Required]用来验证必填项。这是非常强大的特性,通过附加元数据的方式来提供验证,甚至在Mvc框架中还能自动生成Js客户端验证,从而可以非常方便的实现客户端和服务端的双重验证。

  但是遗憾的是,.Net没有直接提供验证DataAnnotations特性的功能。在Mvc中提供了一个ModelState.IsValid来进行验证,但使用这个方法有很多缺陷,我会在后面的领域实体验证一文中详细介绍这个问题。所以我们现在需要自己来实现验证DataAnnotations的功能。

  先来考虑一下接口,现在需要一个方法来验证对象是否有效,所以只需要一个参数,参数类型为object即可。

  那么,返回什么结果呢?由于对象有多个属性,每个属性上可能有多个DataAnnotations特性,这意味着可能有多个属性会验证失败。.Net中提供了一个ValidationResult来表示验证结果,它不仅能够指示是否验证成功,而且包含验证失败的错误消息,这正是我们需要的。可以直接返回ValidationResult的一个集合,比如List<ValidationResult>,不过用一个自定义的集合类包装一下更易用。

  验证结果集合类取名为ValidationResultCollection,代码如下。

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Util.Validations {
    /// <summary>
    /// 验证结果集合
    /// </summary>
    public class ValidationResultCollection : IEnumerable<ValidationResult> {
        /// <summary>
        /// 初始化验证结果集合
        /// </summary>
        public ValidationResultCollection() {
            _results = new List<ValidationResult>();
        }
        /// <summary>
        /// 验证结果
        /// </summary>
        private readonly List<ValidationResult> _results;

        /// <summary>
        /// 是否有效
        /// </summary>
        public bool IsValid {
            get {
                return _results.Count == 0;
            }
        }

        /// <summary>
        /// 验证结果个数
        /// </summary>
        public int Count {
            get {
                return _results.Count;
            }
        }

        /// <summary>
        /// 添加验证结果
        /// </summary>
        /// <param name="result">验证结果</param>
        public void Add( ValidationResult result ) {
            if ( result == null )
                return;
            _results.Add( result );
        }

        /// <summary>
        /// 添加验证结果集合
        /// </summary>
        /// <param name="results">验证结果集合</param>
        public void AddResults( IEnumerable<ValidationResult> results ) {
            if ( results == null )
                return;
            foreach( var result in results )
                Add( result );
        }

        /// <summary>
        /// 获取迭代器
        /// </summary>
        IEnumerator<ValidationResult> IEnumerable<ValidationResult>.GetEnumerator() {
            return _results.GetEnumerator();
        }

        /// <summary>
        /// 获取迭代器
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator() {
            return _results.GetEnumerator();
        }
    }
}

ValidationResultCollection

  验证接口取名为IValidation,代码如下。

namespace Util.Validations {
    /// <summary>
    /// 验证操作
    /// </summary>
    public interface IValidation {
        /// <summary>
        /// 验证
        /// </summary>
        /// <param name="target">验证目标</param>
        ValidationResultCollection Validate( object target );
    }
}

  下面准备来实现这个验证接口。

  一个办法是通过反射来查找所有属性上的ValidationAttribute特性,然后调用它的IsValid方法检查是否失败,代码如下。

using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace Util.Validations {
    /// <summary>
    /// 验证操作
    /// </summary>
    public class Validation : IValidation {
        /// <summary>
        /// 初始化验证操作
        /// </summary>
        public Validation() {
            _result = new ValidationResultCollection();
        }

        /// <summary>
        /// 验证目标
        /// </summary>
        private object _target;
        /// <summary>
        /// 结果
        /// </summary>
        private readonly ValidationResultCollection _result;

        /// <summary>
        /// 验证
        /// </summary>
        /// <param name="target">验证目标</param>
        public ValidationResultCollection Validate( object target ) {
            target.CheckNull( "target" );
            _target = target;
            Type type = target.GetType();
            var properties = type.GetProperties();
            foreach( var property in properties )
                ValidateProperty( property );
            return _result;
        }

        /// <summary>
        /// 验证属性
        /// </summary>
        private void ValidateProperty( PropertyInfo property ) {
            var attributes = property.GetCustomAttributes( typeof( ValidationAttribute ), true );
            foreach( var attribute in attributes ) {
                var validationAttribute = attribute as ValidationAttribute;
                if ( validationAttribute == null )
                    continue;
                ValidateAttribute( property, validationAttribute );
            }
        }

        /// <summary>
        /// 验证特性
        /// </summary>
        private void ValidateAttribute( PropertyInfo property, ValidationAttribute attribute ) {
            bool isValid = attribute.IsValid( property.GetValue( _target ) );
            if( isValid )
                return;
            _result.Add( new ValidationResult( GetErrorMessage( attribute ) ) );
        }

        /// <summary>
        /// 获取错误消息
        /// </summary>
        private string GetErrorMessage( ValidationAttribute attribute ) {
            if( !string.IsNullOrEmpty( attribute.ErrorMessage ) )
                return attribute.ErrorMessage;
            return Resource.GetString( attribute.ErrorMessageResourceType.FullName, attribute.ErrorMessageResourceName,attribute.ErrorMessageResourceType.Assembly );
        }
    }
}

Validation

  另外,在企业库中包含一个验证组件,它也可以完成这个任务。

  是选择自己实现,还是选择第三方框架,哪种更好呢?我考虑了以下几个问题。

  第一是性能,因为对实体进行验证是一个常规任务,换句话说,调用频率很高,并且每个实体可能包含大量属性,所以提升性能就显得相当重要了。我简单测试了一下,在相同对象上执行100万次验证操作,发现企业库验证组件性能要高出10几倍。

  第二是健壮性和扩展性。我们的代码能够考虑到的边界十分有限,在某些特定条件下就会出现Bug,另外,我们只能完成目前想要的那点功能,当使用起来以后,有新的需求就需要持续维护。而第三方著名框架在全球范围使用,已经非常稳定和健壮,并且它能满足全球用户的需求,说明已经覆盖了我们的大部分需求,所以对于某些特定功能,比如日志等,使用第三方框架远远优于自己开发。

  第三个问题是是否开源。如果我现在只拿到一个dll,我可能不会采用它,因为如果有Bug或者不满足我的需求,我却无法修改。对于只有一个dll的情况,一般建议不要用,除非它实现了你完不成的任务,这是走投无路的最后一招。对于企业库来说,它完全开源,而且免费使用,所以没什么好顾虑的。

  第四个问题是引入程序集的数量。为了一个很简单的功能,引入一大堆程序集划算吗?对于Enterprise Library 5.0,为了实现这个验证功能,需要引入5个程序集,这经常让我生起干掉它的念头。不过到了Enterprise Library 6.0,只需要引入2个程序集就可以了,而这个数目在我的可接受范围内。

  通过上面的考虑,我决定使用企业库的验证组件来完成验证工作。

  我们刚才定义了一个验证接口,这非常重要,除了可以清晰的表明我们需要什么功能,还有一个作用就是隔离外部依赖。你不应该在项目上或应用程序框架内部直接调用企业库验证组件的API,因为以后你发现更好的验证组件时将动弹不得。定义了接口以后,在所有调用的地方使用这个接口,就可以为将来进行扩展奠定基础,只要接口不变,通过多态的方式切换实现,整个系统都不会受影响。这是使用第三方框架或外部接口最重要的一点。

  现在用企业库验证组件来实现我们的验证接口。

  打开Util应用程序框架VS解决方案,考虑一下,我们应该把实现验证接口的代码放到什么地方合适。最简单的办法是直接放进Util类库中,然后给Util类库引用企业库依赖程序集。但这会给Util类库造成高度耦合,如果下回你切换验证框架,就得修改Util类库,或者你其它地方在使用Util类库,但不需要进行验证,但还是会把企业库依赖程序集带走。

  更好的办法是为有依赖的部分创建单独的程序集,这样你就可以按需所用,另外切换实现的时候也更加容易,添加一个新的程序集即可。这对于初学者会比较困难,因为初学者习惯于在少量程序集上工作,面对大量程序集会无所适从。不过随着经验的增加,你会慢慢熟悉,并且当一个VS解决方案中的程序集数量较多时,需要果断拆分成多个VS解决方案。还有一个问题是,初学者不喜欢根据依赖关系分类,如果他发现一个程序集中只有一个文件,他就会觉得这个程序集没什么用,需要合并。这里主要忽略了依赖关系的存在,如果这个程序集引用了某些外部程序集,哪怕只有一个文件, 也是需要拆分的,因为没有它,将会把外部程序集引用到我们更重要的程序集中,从而导致高度耦合。

  先创建一个名为Util.Validations.EntLib的类库项目,然后创建名为Util.Validations.EntLib.Tests的单元测试项目,并添加相关依赖引用。

  创建一个用来测试的样例对象Test,代码如下。

using System.ComponentModel.DataAnnotations;

namespace Util.Validations.EntLib.Tests.Samples {
    /// <summary>
    /// 测试实体
    /// </summary>
    public class Test{
        /// <summary>
        /// 姓名
        /// </summary>
        [Required( ErrorMessage = "姓名不能为空" )]
        public string Name { get; set; }
        /// <summary>
        /// 描述
        /// </summary>
        [StringLength(5,ErrorMessage = "描述不能超过5位")]
        public string Description { get; set; }
    }
}

  创建一个单元测试ValidationTest,代码如下。

using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Util.Validations.EntLib.Tests.Samples;

namespace Util.Validations.EntLib.Tests {
    /// <summary>
    /// 验证测试
    /// </summary>
    [TestClass]
    public class ValidationTest {
        /// <summary>
        /// 测试
        /// </summary>
        private Test _test;
        /// <summary>
        /// 验证操作
        /// </summary>
        private IValidation _validation;

        /// <summary>
        /// 测试初始化
        /// </summary>
        [TestInitialize]
        public void TestInit() {
            _test = new Test();
            _validation = new Validation();
        }

        /// <summary>
        /// 验证姓名为必填项
        /// </summary>
        [TestMethod]
        public void TestRequired() {
            var result = _validation.Validate( _test );
            Assert.AreEqual( "姓名不能为空", result.First().ErrorMessage );
        }

        /// <summary>
        /// 验证姓名为必填项及描述过长
        /// </summary>
        [TestMethod]
        public void TestRequired_StringLength() {
            _test.Description = "123456";
            var result = _validation.Validate( _test );
            Assert.AreEqual( 2,result.Count );
            Assert.AreEqual( "描述不能超过5位", result.Last().ErrorMessage );
        }
    }
}

  封装企业库验证组件的Validation类,代码如下。

using System.Collections.Generic;
using Microsoft.Practices.EnterpriseLibrary.Validation;

namespace Util.Validations.EntLib {
    /// <summary>
    /// 企业库验证操作
    /// </summary>
    public class Validation : IValidation {
        /// <summary>
        /// 验证
        /// </summary>
        /// <param name="target">验证目标</param>
        public ValidationResultCollection Validate( object target ) {
            var validator = ValidationFactory.CreateValidator( target.GetType() );
            var results = validator.Validate( target );
            return GetResult( results );
        }

        /// <summary>
        /// 获取验证结果
        /// </summary>
        private ValidationResultCollection GetResult( IEnumerable<ValidationResult> results ) {
            var result = new ValidationResultCollection();
            foreach ( var each in results )
                result.Add( new System.ComponentModel.DataAnnotations.ValidationResult( each.Message ) );
            return result;
        }
    }
}

  最后,补充一下,ValidationResultCollection和IValidation接口需要放在Util类库的Validations文件夹中,把接口与实现它的类分离到不同的程序集,被称为分离接口模式,这让你在必要时可以通过新增程序集的方式扩展系统。

  本文为实体验证打下一个良好的基础,不过当实体验证失败时,需要进行处理,一个常规作法是抛出一个自定义异常,这是下一篇将要介绍的内容——异常公共操作类。

  .Net应用程序框架交流QQ群: 386092459,欢迎有兴趣的朋友加入讨论。

  谢谢大家的持续关注,我的博客地址:http://www.cnblogs.com/xiadao521/

  下载地址:http://files.cnblogs.com/xiadao521/Util.2014.11.18.1.rar

时间: 2024-09-30 01:14:39

Util应用程序框架公共操作类(四):验证公共操作类的相关文章

Util应用程序框架公共操作类(十二):Lambda表达式公共操作类(三)

今天在开发一个简单查询时,发现我的Lambda操作类的GetValue方法无法正确获取枚举类型值,以至查询结果错误. 我增加了几个单元测试来捕获错误,代码如下. /// <summary> /// 测试值为枚举 /// </summary> [TestMethod] public void TestGetValue_Enum() { var test1 = new Test1(); test1.NullableEnumValue = LogType.Error; //属性为枚举,值

Util应用程序框架公共操作类(六):验证扩展

前面介绍了仓储的基本操作,下面准备开始扩展查询,在扩展查询之前,首先要增加两个公共操作类,一个是经常要用到的验证方法,另一个是Lambda表达式的操作类. 很多时候,我们会判断一个对象是否为null,由于null是一个不能接受的值,它会导致“未将对象引用设置到对象的实例”的严重错误,所以当检测到null值时一般直接抛出ArgumentNullException异常. public void Test( string name ) { if( name == null ) throw new Ar

Util应用程序框架公共操作类(三):数据类型转换公共操作类(扩展篇)

上一篇以TDD方式介绍了数据类型转换公共操作类的开发,并提供了单元测试和实现代码,本文将演示通过扩展方法来增强公共操作类,以便调用时更加简化. 下面以字符串转换为List<Guid>为例进行讨论. string input = "83B0233C-A24F-49FD-8083-1337209EBC9A,EAB523C6-2FE7-47BE-89D5-C6D440C3033A"; var result = Util.Conv.ToGuidList( input ); 观察上面

Util应用程序框架公共操作类(五):异常公共操作类

任何系统都需要处理错误,本文介绍的异常公共操作类,用于对业务上的错误进行简单支持. 对于刚刚接触.Net的新手,碰到错误的时候,一般喜欢通过返回bool值的方式指示是否执行成功. public bool 方法名() { //执行代码,成功返回true,否则返回false } 不过上面的方法有一个问题是,无法知道确切的错误原因,所以需要添加一个out参数来返回错误消息. public bool 方法名( out string errorMessage ) { //执行代码,成功返回true,否则返

Util应用程序框架公共操作类(一):数据类型转换公共操作类(介绍篇)

本系列文章将介绍一些对初学者有帮助的辅助类,这些辅助类本身并没有什么稀奇之处,如何能发现需要封装它们可能更加重要,所谓授之以鱼不如授之以渔,掌握封装公共操作类的技巧才是关键,我会详细说明创建这些类的动机和思考过程,以帮助初学者发现和封装自己需要的东西.创建公共操作类的技巧,大家可以参考我的这篇文章——应用程序框架实战十二:公共操作类开发技巧(初学者必读). 封装公共操作类,不仅要把技术上困难的封装进来,还需要不断观察自己的代码,以找出哪些部分可以更加简化.本文将介绍一个容易被大家所忽视的东西——

Util应用程序框架公共操作类(十一):表达式生成器

本篇介绍的表达式生成器,用于动态创建表达式. 在Util项目Lambdas目录中,添加ExpressionBuilder,代码如下. using System; using System.Linq.Expressions; namespace Util.Lambdas { /// <summary> /// 表达式生成器 /// </summary> public class ExpressionBuilder<TEntity> { /// <summary>

Util应用程序框架公共操作类(十):可空值类型扩展

当你使用可空的值类型时,你会发现取值很不方便,比如Guid? obj,你要从obj中获取值,可以使用Value属性obj. Value,但obj可能为null,这时候就会抛出一个异常. 可空值类型提供了一个HasValue属性,它可以识别出obj是不是一个null值,每当你获取可空值都需要加上这个判断if(value.HasValue){ var value = obj.Value;}. 下面我们通过几个扩展方法,把判断封装起来. 在Util项目中添加Extensions.Nullable.cs

应用程序框架实战三十四:数据传输对象(DTO)介绍及各类型实体比较(转)

本文将介绍DDD分层架构中广泛使用的数据传输对象Dto,并且与领域实体Entity,查询实体QueryObject,视图实体ViewModel等几种实体进行比较. 领域实体为何不能一统江湖? 当你阅读我或其它博主提供的示例代码时,会发现几种类型的实体,这几种实体初步看上去区别不大,只是名称不同,特别在这些示例非常简单的情况下更是如此.你可能会疑惑为何要搞得这么复杂,采用一种实体不是更好? 在最理想的情况下,我们只想采用领域实体Entity进行所有的操作. 领域实体是领域层的核心,是业务逻辑的主要

应用程序框架实战二十四:基础查询扩展 - 分页与排序

上一篇介绍了IQueryable的Where方法存在的问题,并扩展了一个名为Filter的过滤方法,它是Where方法的增强版.本篇将介绍查询的另一个重要主题——分页与排序. 对于任何一个信息系统,查询都需要分页,因为不可能直接返回表中的所有数据. 如果直接使用原始的Ado.Net,我们可以编写一个通用分页存储过程来进行分页查询,然后通过一个DataTable返回给业务层.不过进入Entity Framework时代,分页变得异常简单,通过Skip和Take两个方法配合就可以完成任务. 为了让分