团队开发框架实战—数据验证

团队开发框架实战—数据验证
为了能够验证领域实体,需要一个验证公共操作类来提供支持。由于我将使用企业库(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.Generic;
using System.ComponentModel.DataAnnotations;
using System.Collections;

namespace Tdf.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>
        /// <returns></returns>
        IEnumerator<ValidationResult> IEnumerable<ValidationResult>.GetEnumerator()
        {
            return _results.GetEnumerator();
        }

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

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

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

下面准备来实现这个验证接口。
  一个办法是通过反射来查找所有属性上的ValidationAttribute特性,然后调用它的IsValid方法检查是否失败。
另外,在企业库中包含一个验证组件,它也可以完成这个任务。
  是选择自己实现,还是选择第三方框架,哪种更好呢?我考虑了以下几个问题。

  • 第一是性能,因为对实体进行验证是一个常规任务,换句话说,调用频率很高,并且每个实体可能包含大量属性,所以提升性能就显得相当重要了。我简单测试了一下,在相同对象上执行100万次验证操作,发现企业库验证组件性能要高出10几倍
  • 第二是健壮性和扩展性。我们的代码能够考虑到的边界十分有限,在某些特定条件下就会出现Bug,另外,我们只能完成目前想要的那点功能,当使用起来以后,有新的需求就需要持续维护。而第三方著名框架在全球范围使用,已经非常稳定和健壮,并且它能满足全球用户的需求,说明已经覆盖了我们的大部分需求,所以对于某些特定功能,比如日志等,使用第三方框架远远优于自己开发。
  • 第三个问题是是否开源。如果我现在只拿到一个dll,我可能不会采用它,因为如果有Bug或者不满足我的需求,我却无法修改。对于只有一个dll的情况,一般建议不要用,除非它实现了你完不成的任务,这是走投无路的最后一招。对于企业库来说,它完全开源,而且免费使用,所以没什么好顾虑的。
  • 第四个问题是引入程序集的数量。为了一个很简单的功能,引入一大堆程序集划算吗?对于Enterprise Library 5.0,为了实现这个验证功能,需要引入5个程序集,这经常让我生起干掉它的念头。不过到了Enterprise Library 6.0,只需要引入2个程序集就可以了,而这个数目在我的可接受范围内。
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EnterpriseLibrary.Common" version="6.0.1304.0" targetFramework="net452" />
  <package id="EnterpriseLibrary.Validation" version="6.0.1304.0" targetFramework="net452" />
  <package id="log4net" version="2.0.5" targetFramework="net452" />
</packages>

通过上面的考虑,我决定使用企业库的验证组件来完成验证工作
  我们刚才定义了一个验证接口,这非常重要,除了可以清晰的表明我们需要什么功能,还有一个作用就是隔离外部依赖。你不应该在项目上或应用程序框架内部直接调用企业库验证组件的API,因为以后你发现更好的验证组件时将动弹不得。定义了接口以后,在所有调用的地方使用这个接口,就可以为将来进行扩展奠定基础,只要接口不变,通过多态的方式切换实现,整个系统都不会受影响。这是使用第三方框架或外部接口最重要的一点。
  现在用企业库验证组件来实现我们的验证接口。
  打开团队开发框架VS解决方案,考虑一下,我们应该把实现验证接口的代码放到什么地方合适。最简单的办法是直接放进Tdf类库中,然后给Tdf类库引用企业库依赖程序集。但这会给Tdf类库造成高度耦合,如果下回你切换验证框架,就得修改Tdf类库,或者你其它地方在使用Tdf类库,但不需要进行验证,但还是会把企业库依赖程序集带走。
  更好的办法是为有依赖的部分创建单独的程序集,这样你就可以按需所用,另外切换实现的时候也更加容易,添加一个新的程序集即可。这对于初学者会比较困难,因为初学者习惯于在少量程序集上工作,面对大量程序集会无所适从。不过随着经验的增加,你会慢慢熟悉,并且当一个VS解决方案中的程序集数量较多时,需要果断拆分成多个VS解决方案。还有一个问题是,初学者不喜欢根据依赖关系分类,如果他发现一个程序集中只有一个文件,他就会觉得这个程序集没什么用,需要合并。这里主要忽略了依赖关系的存在,如果这个程序集引用了某些外部程序集,哪怕只有一个文件, 也是需要拆分的,因为没有它,将会把外部程序集引用到我们更重要的程序集中,从而导致高度耦合。
  先创建一个名为Tdf.Validations.EntLib的类库项目,然后创建名为Tdf.Validations.EntLib.Tests的单元测试项目,并添加相关依赖引用。
  创建一个用来测试的样例对象Test,代码如下。

using System.ComponentModel.DataAnnotations;

namespace UnitValidationTest
{
    public class Test
    {
        [Required(ErrorMessage = "姓名不能为空")]
        public string Name { get; set; }

        [StringLength(5, ErrorMessage = "描述不能超过5位")]
        public string Description { get; set; }
    }
}

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

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tdf.Validations;
using Tdf.Validations.EntLib;
using System.Linq;

namespace UnitValidationTest
{
    [TestClass]
    public class ValidationTest
    {
        private Test _test;
        private IValidation _validation;

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

        [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 Microsoft.Practices.EnterpriseLibrary.Validation;
using System.Collections.Generic;

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

        /// <summary>
        /// 获取验证结果
        /// </summary>
        /// <param name="results"></param>
        /// <returns></returns>
        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接口需要放在Tdf类库的Validations文件夹中,把接口与实现它的类分离到不同的程序集,被称为分离接口模式,这让你在必要时可以通过新增程序集的方式扩展系统。
  本文为实体验证打下一个良好的基础,不过当实体验证失败时,需要进行处理,一个常规作法是抛出一个自定义异常,这是下一篇将要介绍的内容——异常处理。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,460评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 这些天因为婆婆针炙,经常在网上查资料,找来找去无意中找到了山药社区静水浮萍的文章。一共55篇日志,夜以继日地被我看...
    青山妩媚和Samuel阅读 452评论 5 6
  • 我是一名学生,一名有着几百万对手的高考生。我紧张,迷茫,害怕。我的梦想是川美,但是它离我越来越远。我最近开始变得抓...
    叶清荣阅读 313评论 0 0
  • 散 步 古克 月上东山漫僻陬,长堤徐步畅怀游。 江波潋滟风清...
    黄杰平阅读 209评论 1 2