上一篇介紹了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