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

  上一篇介绍了IQueryable的Where方法存在的问题,并扩展了一个名为Filter的过滤方法,它是Where方法的加强版。本篇将介绍查询的另外一个重要主题——分页与排序。框架

  对于任何一个信息系统,查询都须要分页,由于不可能直接返回表中的全部数据。单元测试

  若是直接使用原始的Ado.Net,咱们能够编写一个通用分页存储过程来进行分页查询,而后经过一个DataTable返回给业务层。不过进入Entity Framework时代,分页变得异常简单,经过Skip和Take两个方法配合就能够完成任务。测试

  为了让分页查询变得更加简单,咱们须要进一步扩展和封装。this

  先考虑输入参数,表现层须要将一些分页参数传递到应用层,为此咱们能够定义一个分页对象来承载和计算分页相关的数据。spa

  在Util.Domains项目的Repositories目录中,建立IPager接口和它的实现类Pager。设计

  IPager接口代码以下。code

namespace Util.Domains.Repositories { /// <summary>
    /// 分页 /// </summary>
    public interface IPager { /// <summary>
        /// 页数,即第几页,从1开始 /// </summary>
        int Page { get; set; } /// <summary>
        /// 每页显示行数 /// </summary>
        int PageSize { get; set; } /// <summary>
        /// 总行数 /// </summary>
        int TotalCount { get; set; } /// <summary>
        /// 总页数 /// </summary>
        int PageCount { get; } /// <summary>
        /// 跳过的行数 /// </summary>
        int SkipCount { get; } /// <summary>
        /// 排序条件 /// </summary>
        string Order { get; set; } } }

 

  Pager类代码以下。对象

namespace Util.Domains.Repositories { /// <summary>
    /// 分页 /// </summary>
    public class Pager : IPager { /// <summary>
        /// 初始化分页 /// </summary>
        public Pager() : this( 1 ) { } /// <summary>
        /// 初始化分页 /// </summary>
        /// <param name="page">页索引</param>
        /// <param name="pageSize">每页显示行数,默认20</param> 
        /// <param name="totalCount">总行数</param>
        public Pager( int page, int pageSize = 20, int totalCount = 0 ) { Page = page; PageSize = pageSize; TotalCount = totalCount; } private int _pageIndex; /// <summary>
        /// 页索引,即第几页,从1开始 /// </summary>
        public int Page { get { if ( _pageIndex <= 0 ) _pageIndex = 1; return _pageIndex; } set { _pageIndex = value; } } /// <summary>
        /// 每页显示行数 /// </summary>
        public int PageSize { get; set; } /// <summary>
        /// 总行数 /// </summary>
        public int TotalCount { get; set; } /// <summary>
        /// 总页数 /// </summary>
        public int PageCount { get { if ( TotalCount == 0 ) return 0; if ( ( TotalCount % PageSize ) == 0 ) return TotalCount / PageSize; return ( TotalCount / PageSize ) + 1; } } /// <summary>
        /// 跳过的行数 /// </summary>
        public int SkipCount { get { if ( Page > PageCount ) Page = PageCount; return PageSize * ( Page - 1 ); } } /// <summary>
        /// 排序条件 /// </summary>
        public string Order { get; set; } } }

  我将排序条件Order也打包到IPager接口中,这是由于排序与分页密切相关,甚至在调用Skip方法以前,.Net强制要求设置排序条件。blog

  在调用Skip方法时须要计算出跳过的行数,SkipCount提供了这个功能。排序

  因为客户端可能传递错误的分页参数,因此须要在Pager中进行修正。

  PagerTest单元测试代码以下。

using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Domains.Repositories; namespace Util.Domains.Tests.Repositories { /// <summary>
    /// 分页测试 /// </summary>
 [TestClass] public class PagerTest { #region 测试初始化

        /// <summary>
        /// 分页 /// </summary>
        private Pager _pager; /// <summary>
        /// 测试初始化 /// </summary>
 [TestInitialize] public void TestInit() { _pager = new Pager(); } #endregion

        #region 默认值

        /// <summary>
        /// 分页默认值 /// </summary>
 [TestMethod] public void Test_Default() { Assert.AreEqual( 1, _pager.Page ); Assert.AreEqual( 20, _pager.PageSize ); Assert.AreEqual( 0, _pager.TotalCount ); Assert.AreEqual( 0, _pager.PageCount ); } #endregion

        #region PageCount(总页数)

        /// <summary>
        /// 总行数为0,每页20行,页数为0 /// </summary>
 [TestMethod] public void TestPageCount_TotalCountIs0() { _pager.TotalCount = 0; Assert.AreEqual( 0, _pager.PageCount ); } /// <summary>
        /// 总行数为100,每页20行,页数为5 /// </summary>
 [TestMethod] public void TestPageCount_TotalCountIs100() { _pager.TotalCount = 100; Assert.AreEqual( 5, _pager.PageCount ); } /// <summary>
        /// 总行数为1,每页20行,页数为1 /// </summary>
 [TestMethod] public void TestPageCount_TotalCountIs1() { _pager.TotalCount = 1; Assert.AreEqual( 1, _pager.PageCount ); } /// <summary>
        /// 总行数为100,每页10行,页数为10 /// </summary>
 [TestMethod] public void TestPageCount_PageSizeIs10_TotalCountIs100() { _pager.PageSize = 10; _pager.TotalCount = 100; Assert.AreEqual( 10, _pager.PageCount ); } #endregion

        #region Page(页索引)

        /// <summary>
        /// 页索引小于1,则修正为1 /// </summary>
 [TestMethod] public void TestPage_Less1() { _pager.Page = 0; Assert.AreEqual( 1, _pager.Page ); _pager.Page = -1; Assert.AreEqual( 1, _pager.Page ); } #endregion

        #region SkipCount(跳过的行数)

        /// <summary>
        /// 跳过的行数 /// </summary>
 [TestMethod] public void TestSkipCount() { _pager.TotalCount = 100; _pager.Page = 0; Assert.AreEqual( 0, _pager.SkipCount ); _pager.Page = 1; Assert.AreEqual( 0, _pager.SkipCount ); _pager.Page = 2; Assert.AreEqual( 20, _pager.SkipCount ); _pager.Page = 3; Assert.AreEqual( 40, _pager.SkipCount ); _pager.Page = 4; Assert.AreEqual( 60, _pager.SkipCount ); _pager.Page = 5; Assert.AreEqual( 80, _pager.SkipCount ); _pager.Page = 6; Assert.AreEqual( 80, _pager.SkipCount ); } /// <summary>
        /// 跳过的行数 /// </summary>
 [TestMethod] public void TestSkipCount_2() { _pager.TotalCount = 99; _pager.Page = 0; Assert.AreEqual( 0, _pager.SkipCount ); _pager.Page = 1; Assert.AreEqual( 0, _pager.SkipCount ); _pager.Page = 2; Assert.AreEqual( 20, _pager.SkipCount ); _pager.Page = 3; Assert.AreEqual( 40, _pager.SkipCount ); _pager.Page = 4; Assert.AreEqual( 60, _pager.SkipCount ); _pager.Page = 5; Assert.AreEqual( 80, _pager.SkipCount ); _pager.Page = 6; Assert.AreEqual( 80, _pager.SkipCount ); } /// <summary>
        /// 跳过的行数 /// </summary>
 [TestMethod] public void TestSkipCount_3() { _pager.TotalCount = 0; _pager.Page = 1; Assert.AreEqual( 0, _pager.SkipCount ); } #endregion } }

  如今有了Pager来传递分页参数,但分页结果采用什么类型返回呢?一种办法是经过List<T>返回对象集合,再定义几个out参数来返回分页参数,但这种作法比较丑陋,out只应该在必要时才使用。

  一个更好的办法是建立派生自List<T>的自定义集合,只须要添加几个分页属性便可。

  在Util.Domains项目的Repositories目录中,建立PagerList分页列表,代码以下。

 

using System; using System.Collections.Generic; using System.Linq; namespace Util.Domains.Repositories { /// <summary>
    /// 分页集合 /// </summary>
    /// <typeparam name="T">元素类型</typeparam>
    public class PagerList<T> : List<T> { /// <summary>
        /// 分页集合 /// </summary>
        /// <param name="pager">查询对象</param>
        public PagerList( IPager pager ) : this( pager.Page, pager.PageSize, pager.TotalCount, pager.Order ) { } /// <summary>
        /// 分页集合 /// </summary>
        /// <param name="totalCount">总行数</param>
        public PagerList( int totalCount ) : this( 1, 20, totalCount ) { } /// <summary>
        /// 分页集合 /// </summary>
        /// <param name="page">页索引</param>
        /// <param name="pageSize">每页显示行数</param>
        /// <param name="totalCount">总行数</param>
        public PagerList( int page, int pageSize, int totalCount ) : this( page, pageSize, totalCount, "" ) { } /// <summary>
        /// 分页集合 /// </summary>
        /// <param name="page">页索引</param>
        /// <param name="pageSize">每页显示行数</param>
        /// <param name="totalCount">总行数</param>
        /// <param name="order">排序条件</param>
        public PagerList( int page, int pageSize, int totalCount, string order ) { var pager = new Pager( page, pageSize, totalCount ); TotalCount = pager.TotalCount; PageCount = pager.PageCount; Page = pager.Page; PageSize = pager.PageSize; Order = order; } /// <summary>
        /// 页索引,即第几页,从1开始 /// </summary>
        public int Page { get; private set; } /// <summary>
        /// 每页显示行数 /// </summary>
        public int PageSize { get; private set; } /// <summary>
        /// 总行数 /// </summary>
        public int TotalCount { get; private set; } /// <summary>
        /// 总页数 /// </summary>
        public int PageCount { get; private set; } /// <summary>
        /// 排序条件 /// </summary>
        public string Order { get; private set; } /// <summary>
        /// 转换分页集合的元素类型 /// </summary>
        /// <typeparam name="TResult">目标元素类型</typeparam>
        /// <param name="converter">转换方法</param>
        public PagerList<TResult> Convert<TResult>( Func<T, TResult> converter ) { var result = new PagerList<TResult>( Page, PageSize, TotalCount, Order ); result.AddRange( this.Select( converter ) ); return result; } } }

 

  PagerList能够接收一个IPager的参数,这样能够快速设置分页参数。

  当你从仓储中获取到PagerList<T>,T类型参数是一个领域层的聚合,若是你的应用层操做的是Dto,这个PagerList就没法使用,将一个PagerList<TEntity>完整转换为PagerList<TDto>须要好几行乏味的赋值代码。为了解决这个问题,提供了一个Convert方法,该方法接收一个Func<T, TResult>参数,Func是.Net内置的一个标准委托,咱们能够传递一个方法完成Entity到Dto的转换,其它分页参数的赋值操做会在Convert中完成。

  PagerListTest单元测试代码以下。

using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Domains.Repositories; using Util.Domains.Tests.Samples; namespace Util.Domains.Tests.Repositories { /// <summary>
    /// 分页集合测试 /// </summary>
 [TestClass] public class PagerListTest { /// <summary>
        /// 分页集合 /// </summary>
        private PagerList<Employee> _list; /// <summary>
        /// 测试初始化 /// </summary>
 [TestInitialize] public void TestInit() { _list = new PagerList<Employee>( 1, 2, 3 ); _list.Add( new Employee() ); _list.Add( new Employee(){Name = "B"} ); } /// <summary>
        /// 元素个数 /// </summary>
 [TestMethod] public void TestCount() { Assert.AreEqual( 2, _list.Count ); } /// <summary>
        /// 用索引获取元素 /// </summary>
 [TestMethod] public void TestIndex() { Assert.AreEqual( "B", _list[1].Name ); } /// <summary>
        /// 转换类型 /// </summary>
 [TestMethod] public void TestConvert() { var result = _list.Convert( t => new EmployeeDto() ); Assert.AreEqual( 2, result.Count ); Assert.AreEqual( 1, result.Page ); Assert.AreEqual( 2, result.PageSize ); Assert.AreEqual( 3, result.TotalCount ); Assert.AreEqual( 2, result.PageCount ); } } }

  准备工做已经就绪,如今开始扩展IQueryable的分页和排序功能。

  注意观察IPager接口中的排序条件Order,它是一个字符串类型,使用弱类型的字符串是有缘由的。要在IQueryable上进行排序,第一次升序调用OrderBy,降序调用OrderByDescending,若是要继续添加第二个排序条件,升序调用ThenBy,降序调用ThenByDescending。能够看到,排序API并不易用,若是要设置多个排序条件至关麻烦。更重要的一点是这些方法的参数是强类型的Func或Expression,而表现层传过来的参数通常都是字符串,这些字符串没法直接传递给上述方法,更不要谈排序方向和多个排序字段。

  从上面能够看出,弱类型也不是一无可取,它能够提供强大的灵活性。为了弥补Linq强类型查询的不足,微软提供了一组动态查询帮助类,其中DynamicQueryable为IQueryable扩展了几个经常使用方法,它能够接收字符串参数,并解析为相应的Expression。

  因为这一组帮助类内容不多,因此我不想为此引用一个额外的程序集。我将这些帮助类放到了Util项目的Lambdas目录的Dynamics子目录中,并修改它们的命名空间为Util.Lambdas.Dynamics,这样Resharper就不会显示警告了。

  这几个动态查询帮助类的代码就不贴了,有兴趣可下载本文的示例代码文件。

  在Util.Datas项目中找到Extensions.Query.cs文件,添加下面的扩展代码。

using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Util.Datas.Queries; using Util.Domains.Repositories; using Util.Lambdas.Dynamics; namespace Util.Datas { /// <summary>
    /// 查询扩展 /// </summary>
    public static class Extensions { /// <summary>
        /// 过滤 /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="predicate">谓词</param>
        public static IQueryable<T> Filter<T>( this IQueryable<T> source, Expression<Func<T, bool>> predicate ) { predicate = QueryHelper.ValidatePredicate( predicate ); if ( predicate == null ) return source; return source.Where( predicate ); } /// <summary>
        /// 排序 /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="propertyName">排序属性名,多个属性用逗号分隔,降序用desc字符串,范例:Name,Age desc</param>
        public static IQueryable<T> OrderBy<T>( this IQueryable<T> source, string propertyName ) { return source.OrderByDynamic( propertyName ); } /// <summary>
        /// 建立分页列表 /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="page">页索引,表示第几页,从1开始</param>
        /// <param name="pageSize">每页显示行数,默认20</param>
        public static PagerList<T> PagerResult<T>( this IQueryable<T> source, int page, int pageSize = 20 ) { return PagerResult( source, new Pager( page, pageSize ) ); } /// <summary>
        /// 建立分页列表 /// </summary>
        /// <typeparam name="T">实体类型</typeparam>
        /// <param name="source">数据源</param>
        /// <param name="pager">分页对象</param>
        public static PagerList<T> PagerResult<T>( this IQueryable<T> source, IPager pager ) { source = OrderBy( source, pager ); source = Pager( source, pager ); return CreatePageList( source, pager ); } /// <summary>
        /// 排序 /// </summary>
        private static IQueryable<T> OrderBy<T>( IQueryable<T> source, IPager pager ) { if ( pager.Order.IsEmpty() ) return source; return source.OrderBy( pager.Order ); } /// <summary>
        /// 分页 /// </summary>
        private static IQueryable<T> Pager<T>( IQueryable<T> source, IPager pager ) { if ( pager.TotalCount <= 0 ) pager.TotalCount = source.Count(); return source.Skip( pager.SkipCount ).Take( pager.PageSize ); } /// <summary>
        /// 建立分页列表 /// </summary>
        private static PagerList<T> CreatePageList<T>( IEnumerable<T> source, IPager pager ) { var result = new PagerList<T>( pager ); result.AddRange( source.ToList() ); return result; } } }

  这里扩展了OrderBy方法,在方法内部委托给OrderByDynamic执行,OrderByDynamic方法由DynamicQueryable提供。

  PagerResult方法用来获取分页结果,有两个重载,第一个重载方法 PagerList<T> PagerResult<T>( this IQueryable<T> source, int page, int pageSize = 20 ) 接收两个分页参数,在使用这个重载以前假定排序已经完成。另外一个重载方法 PagerList<T> PagerResult<T>( this IQueryable<T> source, IPager pager ) 接收一个分页对象,它会同时完成分页和排序操做。

  我在实际应用中,几乎老是使用第二个重载,由于我在应用层使用了查询实体,查询实体是从Pager派生的查询参数对象,待介绍到应用层再详述。

  还有一点须要注意,Pager对象的TotalCount是容许设置的,我在获取总行数的时候做了一个判断,若是TotalCount已经被设置,就不会调用Count方法。这样设计的缘由是调用Count方法的开销很高,可能致使表扫描或索引扫描,若是在执行 PagerResult以前已经执行过Count,就不须要再重复执行。

  本篇介绍的方法,应用层能够这样调用。

var dtos = Repository.Find().Filter( t => t.Name.Contains( "a" ) ).OrderBy( t => t.CreateTime ).PagerResult( 1 ).Convert( t => t.ToDto() );

var dtos = Repository.Find().Filter( t => t.Name.Contains( testQuery.Name ) ).PagerResult( testQuery ).Convert( t => t.ToDto() );

  上面的代码已经比较简单,不过我将查询功能单独提取出来,使用查询对象模式进行封装,进一步简化操做。

  下一篇将介绍查询条件,它是规约模式的一种实现。

  

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

  谢谢你们的持续关注,个人博客地址:http://www.cnblogs.com/xiadao521/

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

相关文章
相关标签/搜索