应用程序框架实战二十五:查询条件(规约模式应用)

  前面已经做了一些准备工作,本篇将介绍查询条件的封装,它是规约模式的一个应用。

  规约使用一个对象来封装谓词,我之前已经介绍过它在验证方面的应用,本篇是规约模式在查询方面的应用。

  规约的强大之处在于,能够将一堆杂乱无章的条件判断或查询条件封装起来,以一个清晰的概念来表达,并使得这些谓词具备了可复用的能力。

  首先在Util.Domains项目的Repositories目录中创建ICriteria接口,这个接口表示一个查询条件,代码如下。

using System;
using System.Linq.Expressions;

namespace Util.Domains.Repositories {
    /// <summary>
    /// 查询条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    public interface ICriteria<TEntity> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 获取谓词
        /// </summary>
        Expression<Func<TEntity, bool>> GetPredicate();
    }
}

  由于我们使用了EF这种ORM框架,查询条件的结果是一个Expression<Func<TEntity, bool>>的谓词表达式。

  在Util.Datas项目中,打开Extensions.Query.cs文件,增加以下代码。

        /// <summary>
        /// 过滤
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="criteria">查询条件</param>
        public static IQueryable<T> Filter<T>( this IQueryable<T> source, ICriteria<T> criteria ) where T : class,IAggregateRoot {
            if ( criteria == null )
                return source;
            var predicate = criteria.GetPredicate();
            if ( predicate == null )
                return source;
            return source.Where( predicate );
        }

  我们在IQueryable对象上扩展了一个Filter方法,该方法接收一个查询条件,如果查询条件有效,就使用Where方法添加过滤条件。

  基础工作就这么多,下面来看几个范例。

  在信息系统中,经常会进行范围查询,比如一个日期段的查询。这看起来是一个简单的需求,初学者一般这样写,t => t.Date >= BeginDate && t.Date <= EndDate,其结果可能是错误的,这是由于从表现层传入的查询条件是可选的,如果客户没有进行输入,结果就是错的。

  对于范围查询来讲,还有更多的细节需要思考,比如,起始日期和结束日期都没有输入,或只输入了起始日期或结束日期,也可能客户输入的起始日期比结束日期还大。为了获得健壮性,我们会对查询条件进行各种判断,从而导致杂乱无章的代码。更要命的是,这些代码无法复用,在另一个范围查询的位置,我们必须把之前的代码复制过去进行修改。

  一个更好的办法是把范围查询逻辑封装到查询条件对象中,以后需要进行范围查询时,即可随手拈来。

  在Util.Datas项目Queries目录中,新建Criterias目录,创建一个查询条件基类CriteriaBase,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;
using Util.Domains.Repositories;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// 查询条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    public abstract class CriteriaBase<TEntity> : ICriteria<TEntity> where TEntity : class, IAggregateRoot {
        /// <summary>
        /// 谓词
        /// </summary>
        protected Expression<Func<TEntity, bool>> Predicate { get; set; }

        /// <summary>
        /// 获取谓词
        /// </summary>
        public virtual Expression<Func<TEntity, bool>> GetPredicate() {
            return Predicate;
        }
    }
}

  根据数据类型不同,范围查询有很多种类,比如日期范围查询、日期时间范围查询、整数范围查询、浮点数范围查询等。我们需要为范围查询条件创建一个基类SegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;
using Util.Lambdas;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// 段过滤条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    /// <typeparam name="TValue">值类型</typeparam>
    public abstract class SegmentCriteria<TEntity, TProperty, TValue> : CriteriaBase<TEntity>
        where TEntity : class, IAggregateRoot
        where TValue : struct {
        /// <summary>
        /// 初始化段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        protected SegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, TValue? min, TValue? max ) {
            Builder = new ExpressionBuilder<TEntity>();
            PropertyExpression = propertyExpression;
            Min = min;
            Max = max;
            if ( IsMinGreaterMax( min, max ) ) {
                Min = max;
                Max = min;
            }
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        protected abstract bool IsMinGreaterMax( TValue? min, TValue? max );

        /// <summary>
        /// 属性表达式
        /// </summary>
        public Expression<Func<TEntity, TProperty>> PropertyExpression { get; set; }

        /// <summary>
        /// 表达式生成器
        /// </summary>
        private ExpressionBuilder<TEntity> Builder { get; set; }

        /// <summary>
        /// 最小值
        /// </summary>
        public TValue? Min { get; set; }

        /// <summary>
        /// 最大值
        /// </summary>
        public TValue? Max { get; set; }

        /// <summary>
        /// 获取谓词
        /// </summary>
        public override Expression<Func<TEntity, bool>> GetPredicate() {
            var first = CreateLeftExpression();
            var second = CreateRightExpression();
            return Builder.ToLambda( first.And( second ) );
        }

        /// <summary>
        /// 创建左操作数,即 t => t.Property >= Min
        /// </summary>
        private Expression CreateLeftExpression() {
            if ( Min == null )
                return null;
            return Builder.Create( PropertyExpression, Operator.GreaterEqual, GetMinValue() );
        }

        /// <summary>
        /// 获取最小值
        /// </summary>
        protected virtual TValue? GetMinValue() {
            return Min;
        }

        /// <summary>
        /// 创建右操作数,即 t => t.Property &lt;= Max
        /// </summary>
        private Expression CreateRightExpression() {
            if ( Max == null )
                return null;
            return Builder.Create( PropertyExpression, GetMaxOperator(), GetMaxValue() );
        }

        /// <summary>
        /// 获取最大值相关的运算符
        /// </summary>
        protected virtual Operator GetMaxOperator() {
            return Operator.LessEqual;
        }

        /// <summary>
        /// 获取最大值
        /// </summary>
        protected virtual TValue? GetMaxValue() {
            return Max;
        }
    }
}

  对于日期范围查询,日期是否包含时间非常重要,它们在行为上是不同的。如果日期不包含时间,那么需要为结束日期加一天,并修改运算符为小于。

  日期时间范围查询条件DateTimeSegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// 日期时间段过滤条件 - 包含时间
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    public class DateTimeSegmentCriteria<TEntity, TProperty> : SegmentCriteria<TEntity, TProperty, DateTime> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 初始化日期时间段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public DateTimeSegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, DateTime? min, DateTime? max )
            : base( propertyExpression, min, max ) {
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        protected override bool IsMinGreaterMax( DateTime? min, DateTime? max ) {
            return min > max;
        }
    }
}

  日期范围查询条件DateSegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// 日期段过滤条件 - 不包含时间
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    public class DateSegmentCriteria<TEntity, TProperty> : SegmentCriteria<TEntity, TProperty, DateTime> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 初始化日期段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public DateSegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, DateTime? min, DateTime? max )
            : base( propertyExpression, min, max ) {
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        protected override bool IsMinGreaterMax( DateTime? min, DateTime? max ) {
            return min > max;
        }

        /// <summary>
        /// 获取最小值
        /// </summary>
        protected override DateTime? GetMinValue() {
            return base.GetMinValue().SafeValue().Date;
        }

        /// <summary>
        /// 获取最大值
        /// </summary>
        protected override DateTime? GetMaxValue() {
            return base.GetMaxValue().SafeValue().Date.AddDays( 1 );
        }

        /// <summary>
        /// 获取最大值相关的运算符
        /// </summary>
        protected override Operator GetMaxOperator() {
            return Operator.Less;
        }
    }
}

  整数范围查询条件IntSegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// 整数段过滤条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    public class IntSegmentCriteria<TEntity, TProperty> : SegmentCriteria<TEntity, TProperty,int> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 初始化整数段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public IntSegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, int? min, int? max )
            : base( propertyExpression,min,max){
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        protected override bool IsMinGreaterMax( int? min, int? max ) {
            return min > max;
        }
    }
}

  double范围查询条件DoubleSegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// double数值段过滤条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    public class DoubleSegmentCriteria<TEntity, TProperty> : SegmentCriteria<TEntity, TProperty, double> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 初始化double数值段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public DoubleSegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, double? min, double? max )
            : base( propertyExpression, min, max ) {
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        protected override bool IsMinGreaterMax( double? min, double? max ) {
            return min > max;
        }
    }
}

  decimal范围查询条件DecimalSegmentCriteria,代码如下。

using System;
using System.Linq.Expressions;
using Util.Domains;

namespace Util.Datas.Queries.Criterias {
    /// <summary>
    /// decimal数值段过滤条件
    /// </summary>
    /// <typeparam name="TEntity">实体类型</typeparam>
    /// <typeparam name="TProperty">属性类型</typeparam>
    public class DecimalSegmentCriteria<TEntity, TProperty> : SegmentCriteria<TEntity, TProperty, decimal> where TEntity : class,IAggregateRoot {
        /// <summary>
        /// 初始化decimal数值段过滤条件
        /// </summary>
        /// <param name="propertyExpression">属性表达式</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public DecimalSegmentCriteria( Expression<Func<TEntity, TProperty>> propertyExpression, decimal? min, decimal? max )
            : base( propertyExpression, min, max ) {
        }

        /// <summary>
        /// 最小值是否大于最大值
        /// </summary>
        protected override bool IsMinGreaterMax( decimal? min, decimal? max ) {
            return min > max;
        }
    }
}

  我们现在进行日期范围查询,就比较简单了,代码如下。

queryable.Filter( new DateSegmentCriteria<Test, DateTime>( t => t.Date, BeginDate,EndDate ) );

  不过上面的代码用起来还不是太顺手,可以将范围查询扩展到IQueryable,代码如下。

/// <summary>
        /// 过滤整数段
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <typeparam name="TProperty">属性类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyExpression">属性表达式,范例:t => t.Age</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public static IQueryable<T> FilterInt<T, TProperty>( this IQueryable<T> source,
            Expression<Func<T, TProperty>> propertyExpression, int? min, int? max ) where T : class,IAggregateRoot {
            return source.Filter( new IntSegmentCriteria<T, TProperty>( propertyExpression, min, max ) );
        }

        /// <summary>
        /// 过滤double数值段
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <typeparam name="TProperty">属性类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyExpression">属性表达式,范例:t => t.Age</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public static IQueryable<T> FilterDouble<T, TProperty>( this IQueryable<T> source,
            Expression<Func<T, TProperty>> propertyExpression, double? min, double? max ) where T : class,IAggregateRoot {
            return source.Filter( new DoubleSegmentCriteria<T, TProperty>( propertyExpression, min, max ) );
        }

        /// <summary>
        /// 过滤decimal数值段
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <typeparam name="TProperty">属性类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyExpression">属性表达式,范例:t => t.Age</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public static IQueryable<T> FilterDecimal<T, TProperty>( this IQueryable<T> source,
            Expression<Func<T, TProperty>> propertyExpression, decimal? min, decimal? max ) where T : class,IAggregateRoot {
            return source.Filter( new DecimalSegmentCriteria<T, TProperty>( propertyExpression, min, max ) );
        }

        /// <summary>
        /// 过滤日期段,不包含时间
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <typeparam name="TProperty">属性类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyExpression">属性表达式,范例:t => t.Age</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public static IQueryable<T> FilterDate<T, TProperty>( this IQueryable<T> source,
            Expression<Func<T, TProperty>> propertyExpression, DateTime? min, DateTime? max ) where T : class,IAggregateRoot {
            return source.Filter( new DateSegmentCriteria<T, TProperty>( propertyExpression, min, max ) );
        }

        /// <summary>
        /// 过滤日期时间段,包含时间
        /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <typeparam name="TProperty">属性类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyExpression">属性表达式,范例:t => t.Age</param>
        /// <param name="min">最小值</param>
        /// <param name="max">最大值</param>
        public static IQueryable<T> FilterDateTime<T, TProperty>( this IQueryable<T> source,
            Expression<Func<T, TProperty>> propertyExpression, DateTime? min, DateTime? max ) where T : class,IAggregateRoot {
            return source.Filter( new DateTimeSegmentCriteria<T, TProperty>( propertyExpression, min, max ) );
        }

  日期范围查询的调用代码简化为如下代码。

queryable.FilterDate( t => t.Date, BeginDate, EndDate)

  本文介绍了如何使用查询条件对象封装范围查询,当然你可以用类似的方法将业务中的查询条件封装起来。

  规约模式还有其它用法,更强大的用法,请参考陈晴阳老兄的这篇http://www.cnblogs.com/daxnet/p/3925426.html

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

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

时间: 2024-08-03 23:33:55

应用程序框架实战二十五:查询条件(规约模式应用)的相关文章

应用程序框架实战二十六:查询对象

信息系统的查询需求千变万化,在仓储中为每个查询需求创建一个特殊方法,将导致大量乏味而臃肿的接口. 一种更加可行的办法是,在应用层服务中描述查询需求,并通过仓储执行查询. 为了能够更好的描述查询需求,可以将查询功能从仓储中抽取出来,专门创建一个查询对象. 查询最复杂的部分是条件过滤,这也是查询对象的主要职责.查询对象可以认为是规约模式的一个变种,允许查询对象动态创建查询条件. 在Util.Domains项目Repositories目录中,创建查询对象基接口IQueryBase,代码如下. usin

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

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

应用程序框架实战二十三:基础查询扩展

上面两篇已经作好准备,本文将进行基础查询扩展.当使用了Entity Framework这样的ORM框架以后,我们查询的核心被集中在IQueryable的Where方法上. 如果UI需要通过姓名查询一个客户,会在UI上放置一个输入框作为客户姓名的查询条件.服务端接收以后通过Where方法进行过滤,如下所示,entities表示DbContext的子类. var queryable = entities.Customers.Where( t => t.Name == name ); 当然,也可以使用

应用程序框架实战三十五:服务概述

上一篇介绍了我对几种实体的认识,本篇将介绍几种服务的用法. 预告一下本系列后续计划,本篇之后,准备进入实战演练阶段,先介绍如何快速解决CRUD操作,从如何使用PD数据建模到使用CodeSmith生成代码,先带你感受一下,再回过来介绍框架内部元素,以免你在阅读时昏昏欲睡. 应用服务介绍 对于一个新的设计元素,可以先假定不需要它,等到确实认识到它的作用再引入.那么,应用服务为我们带来了哪些好处呢? 应用服务帮助简化表现层操作 以MVC为例,如果没有应用服务,那么控制器将直接调用仓储,设置查询条件,转

应用程序框架实战二十:映射层超类型

上一篇介绍了工作单元层超类型的封装演化过程,本文将介绍对Entity Framework映射层超类型的封装. 使用Entity Framework一般需要映射三种类型的对象,即实体.聚合.值对象. 聚合与实体映射的主要区别是:聚合映射单属性标识Id,并需要映射乐观离线锁Version,而实体的标识往往需要映射成复合属性,这样方便物理删除聚合中的实体.Entity Framework通过EntityTypeConfiguration进行实体映射. 值对象以嵌入值模式映射,这需要使用ComplexT

应用程序框架实战二:十年前的回忆

大约10年前,我刚刚步入.Net开发,那时候还很流行单层架构,直接在界面上拖控件,然后绑定数据.数据库操作使用原生的Ado.Net,每次都要创建数据库连接,打开连接,发送Sql,获取结果.关闭连接.每当我需要进行数据库操作的时候,就把这一段复制粘贴过去,就这样干了几个月. 一日,一位师兄给我介绍了名为SqlHelper的数据库辅助类,使用了这玩意以后,我发现开发效率和质量倍增.由于不需要来回复制粘贴,冗余代码变少,代码简洁很多.另外不需要手工关闭数据库连接,也让BUG变得更少.虽然SqlHelp

应用程序框架实战二十七: 基于Mvc+EasyUi+EF+Autofac的CRUD DEMO免费发放,纯干货,附截图

不知不觉,这个系列已经写了好几十篇了.我本来打算把基础介绍完再发放Demo进行整体说明,不过大部分人更喜欢看得见摸得着的表现层,对后端不是太感兴趣,所以我决定先发一个简单的CRUD Demo出来,让大家先感受一下,被应用程序框架封装之后的代码大体是什么样子. 采用EasyUi作为前端框架,主要是它比Dwz强大,另外也是基于Html扩展,比更强大的Ext要简单得多,更重要的是它越来越流行了,对于更详细的决择或前端架构设计,我会在后续文章说明. 虽然是一个简单的单表CRUD操作,但是分层架构和各方面

应用程序框架实战三十六:CRUD实战演练介绍

从本篇开始,本系列将进入实战演练阶段. 前面主要介绍了一些应用程序框架的概念和基类,本来想把所有概念介绍完,再把框架内部实现都讲完了,再进入实战,这样可以让初学者基础牢靠.不过我的精力很有限,文章进度越来越慢,所以准备切换一下介绍顺序,把实战演练提前,以方便你阅读代码. 实战演练介绍 本系列实战演练共分两个部分. 实战演练第一部分介绍如何快速解决CRUD机械操作,这一部分我将手把手带领各位同学从搭建VS环境开始,创建程序集及各程序集间的依赖关系,以及引入依赖的外部DLL,并手工完成代码示例中Ap

应用程序框架实战三十八:项目示例VS解决方案的创建(一)

进行项目开发的第一步,是创建出适合自己团队习惯的VS解决方案,虽然我已经提供了项目示例,但毕竟是我创建的,你直接使用可能并不合适,另外你如果尝试模仿重新创建该示例,中间可能碰到各种障碍,特别是项目间的依赖关系. 本文的目的是帮助.Net架构初学者能顺利搭建起适合自己的VS解决方案,我会在本文演示曾经用过的几种不同风格的目录结构,你可以根据自己的习惯选择一种并自行修改. 本系列假定你已经熟悉如何创建.NET类库等基础知识,并具有.Net开发经验,我不会详细到每一个细节.如果你是.Net初学者,尚未