上面兩篇已經做好準備,本文將進行基礎查詢擴展。當使用了Entity Framework這樣的ORM框架之後,咱們查詢的核心被集中在IQueryable的Where方法上。html
若是UI須要經過姓名查詢一個客戶,會在UI上放置一個輸入框做爲客戶姓名的查詢條件。服務端接收之後經過Where方法進行過濾,以下所示,entities表示DbContext的子類。程序員
var queryable = entities.Customers.Where( t => t.Name == name );
固然,也可使用Linq語句來完成。sql
var queryable = from c in entities.Customers where c.Name == name select c;
這些代碼看上去很不錯,但不管是上面的擴展方法仍是Linq語句,其結果都是錯的。若是操做人員正好在查詢條件的框中輸入了一個「張三」,確實會把名稱爲「張三」的客戶所有找出來,可是若是操做人員什麼也不輸入,直接點擊查詢按鈕,結果會怎樣?數據庫
上面的代碼會強制引入查詢條件,哪怕輸入值是空的,這與咱們的預期不符,因此你們的辦法是添加一個判斷,像下面這樣。安全
IQueryable<Customer> queryable = entities.Customers; if( name != "" ) queryable = queryable.Where( t => t.Name == name );
將輸入值與""進行比較並不健壯,若是操做人員在某個查詢條件輸入框中不當心打了個空格,依然會引入錯誤查詢條件,因此你把代碼改造爲下面這樣。框架
IQueryable<Customer> queryable = entities.Customers; if(!string.IsNullOrWhiteSpace( name ) ) queryable = queryable.Where( t => t.Name == name );
可是string.IsNullOrWhiteSpace只能針對字符串,對於其它類型須要先調用ToString,代碼繼續修改。測試
IQueryable<Customer> queryable = entities.Customers; if( value != null && !string.IsNullOrWhiteSpace(value.ToString() ) ) queryable = queryable.Where( t => t.XXX == value );
對於非字符串類型的查詢條件,爲了保障ToString的安全,須要在以前判斷是否爲null,不然可能拋出null異常。上面的代碼比較健壯了,可是很是醜陋,若是隻有一個查詢條件,這不是大問題,但有10個條件呢?this
IQueryable<Customer> queryable = entities.Customers; if( value1 != null && !string.IsNullOrWhiteSpace(value1.ToString() ) ) queryable = queryable.Where( t => t.F1 == value1 ); if( value2 != null && !string.IsNullOrWhiteSpace(value2.ToString() ) ) queryable = queryable.Where( t => t.F2 == value2 ); if( value3 != null && !string.IsNullOrWhiteSpace(value3.ToString() ) ) queryable = queryable.Where( t => t.F3 == value3 ); ......
打開你本身的項目來檢查一下,應該和上面代碼相似,這些雜亂無章的判斷把查詢的主題沖淡了。spa
我上面討論的是相等(==)運算符,對於像Contains這樣的Like查詢,它不懼怕空字符串「」,可是若是字符串中帶了空格「 」,查詢結果也是錯的。可見,Where這個核心查詢方法,並不適合直接在應用程序中使用,除非你的查詢條件是必填項。對於從界面傳過來的查詢條件基本都是可選的,因此咱們有必要進行查詢擴展。code
以上介紹了擴展Where方法的動機,下面開始進行擴展。
經過上面的示例代碼能夠看出,每當須要調用where時,都須要進行一個判斷,咱們的目標就是把這個判斷隱藏到框架背後。
首先考慮過濾方法的名稱,我命名爲Filter,表示這是一個過濾器方法。
再考慮Filter的方法簽名,很顯然返回類型是泛型的IQueryable<>,那麼參數呢?
我最初的作法是提供兩個參數,第一個參數是Lambda表達式,第二個參數是查詢條件的輸入值。之因此須要第二個參數,是由於我當時不清楚怎麼從Lambda表達式中把輸入值提取出來,方法以下所示。
/// <summary>
/// 過濾 /// </summary>
/// <typeparam name="TEntity">實體類型</typeparam>
/// <typeparam name="TMember">實體屬性類型</typeparam>
/// <param name=" queryable">查詢對象</param>
/// <param name="predicate">過濾條件</param>
/// <param name="value">屬性值</param>
public static IQueryable<TEntity> Filter<TEntity, TMember>( this IQueryable<TEntity> queryable, Expression<Func<TEntity, bool>> predicate, TMember value ){ if (value == null) return queryable; if (string.IsNullOrWhiteSpace(value.ToString())) return queryable; return queryable.Where( predicate ); }
調用代碼以下。
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, value1 ).Filter( t => t.F2 == value2, value2 ).Filter( t => t.F3 == value3, value3 );
能夠看到,調用代碼比直接使用Where已經清爽多了,不過這個Filter不是完美的,對於值類型的輸入條件,結果是錯的。好比value1是一個int類型,它的默認值爲0,它將逃過string.IsNullOrWhiteSpace的檢測。那麼咱們添加一個條件來檢測默認值好很差呢,好比if(value == default(TMember)) return; 。這是不行的,若是你要搜索某字段爲0的記錄就會失效。
致使這個問題的緣由是值類型沒法爲空,對引用類型沒有影響,個人解決方案是強制使用可空值類型。對於查詢來說,通常不會直接傳遞一個條件參數,由於大部分UI都要求分頁,傳遞多個參數是不方便的。我經過建立一個查詢實體來強制實施上面的原則,查詢實體擁有一些查詢屬性,且每一個屬性都是可空的,而且會幫我過濾掉字符串參數中的空格,待我介紹到應用層的時候再詳細說明。
無獨有偶,我在園子裏看到一篇文章和我上面的查詢擴展很是相似,只是他的第二個參數用了bool類型。使用bool類型的好處是更加靈活,固然代價是須要寫更多代碼。調用代碼以下所示。
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1, !string.IsNullOrWhiteSpace(value1)).Filter( t => t.F2 == value2, value2 != 0 );
在長時間使用了兩個參數的方案後,我感受很是彆扭,我爲何要傳入第二個值?直接從Lambda參數中提取出輸入值不是更好?下面咱們說幹就幹。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate ); }
這裏的關鍵方法是Value,這個自定義方法是上一篇擴展的,它可以從Lambda謂詞表達式中把輸入值提取出來。
這個方案與我以前使用的方案相似,只是省下一個參數,它一樣須要使用可空值類型。
目前的代碼還有一個問題,若是程序員一次傳入多個條件,會致使什麼結果?
IQueryable<Customer> queryable = entities.Customers; queryable = queryable.Filter( t => t.F1 == value1 && t.F2 == value2 && t.F3 == value3 )
若是value1=」a」,value2和value3是空值,我得把t.F1 == value1拆出來,再傳到where中去。固然是能夠作到,但太費力,因此我想了個偷懶的方法,一次只容許傳遞一個條件,一次傳入多個條件將拋出異常。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅容許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate ); }
GetCriteriaCount是我在上一篇建立的第二個方法,用來獲取Lambda謂詞表達式中的條件個數,只要大於1個,就會拋出InvalidOperationException異常。
爲了保證程序員不會把null傳進來,添加一個null檢測。
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { predicate.CheckNull( "predicate" ); if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅容許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return queryable; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return queryable; return queryable.Where( predicate ); }
CheckNull用於檢測對象是否空值,若是爲null將拋出異常。
上面介紹了Filter方法的封裝過程,如今開始擴展Util應用程序框架。
建立一個名爲Util.Datas的類庫,並添加相關依賴,這個項目用於放置數據相關公共操做。建立Extensions.Query.cs文件,它用來對查詢進行擴展,代碼以下。
using System; using System.Linq; using System.Linq.Expressions; using Util.Datas.Queries; namespace Util.Datas { /// <summary>
/// 查詢擴展 /// </summary>
public static class Extensions { /// <summary>
/// 過濾 /// </summary>
/// <typeparam name="T">實體類型</typeparam>
/// <param name="queryable">查詢對象</param>
/// <param name="predicate">謂詞</param>
public static IQueryable<T> Filter<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predicate ) { predicate = QueryHelper.ValidatePredicate( predicate ); if ( predicate == null ) return queryable; return queryable.Where( predicate ); } } }
檢測代碼移到一個名爲QueryHelper的internal類中,由於我後面還須要用到這段邏輯,代碼以下。
using System; using System.Linq.Expressions; namespace Util.Datas.Queries { /// <summary>
/// 查詢操做 /// </summary>
internal class QueryHelper { /// <summary>
/// 驗證謂詞,無效返回null /// </summary>
/// <typeparam name="T">實體類型</typeparam>
/// <param name="predicate">謂詞</param>
public static Expression<Func<T, bool>> ValidatePredicate<T>( Expression<Func<T, bool>> predicate ) { predicate.CheckNull( "predicate" ); if ( Lambda.GetCriteriaCount( predicate ) > 1 ) throw new InvalidOperationException( String.Format( "僅容許添加一個條件,條件:{0}", predicate ) ); if ( predicate.Value() == null ) return null; if ( string.IsNullOrWhiteSpace( predicate.Value().ToString() ) ) return null; return predicate; } } }
爲了讓你們能夠把Demo運行起來,我還建立了Util.Datas.Ef.Tests測試項目,SqlScripts目錄中的Test.sql用來建庫,數據庫名爲UnitTest,之因此不使用Test,是懼怕把你本地的Test數據庫給刪掉了,這個數據庫安裝在你的D:\Data目錄中,若是不合適請自行修改。
Samples目錄中的Employee類是測試的實體,它很是簡單,只有一個Name屬性。
Repositories目錄中的EmployeeRepository是測試倉儲,爲了簡單,沒有建立倉儲的接口,由於這裏沒什麼用。
本文的集成測試FilterTest位於QueryTests目錄,代碼以下。
using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Util.Datas.Ef.Tests.Repositories; using Util.Datas.Ef.Tests.Samples; namespace Util.Datas.Ef.Tests.QueryTests { /// <summary>
/// 過濾測試 /// </summary>
[TestClass] public class FilterTest { /// <summary>
/// 測試初始化 /// </summary>
[TestInitialize] public void TestInit() { EmployeeRepository repository = GetEmployeeRepository(); repository.Clear(); repository.Add( Employee.GetEmployee() ); repository.Add( Employee.GetEmployee2() ); } /// <summary>
/// 獲取員工倉儲 /// </summary>
private EmployeeRepository GetEmployeeRepository() { return new EmployeeRepository( new TestUnitOfWork() ); } /// <summary>
/// 測試Filter過濾 /// </summary>
[TestMethod] public void TestFilter() { EmployeeRepository repository = GetEmployeeRepository(); //用where查詢
var result = repository.Find().Where( t => t.Name == "" ); Assert.AreEqual( 0, result.Count() ); //用Fileter查詢
result = repository.Find().Filter( t => t.Name == "" ); Assert.AreEqual( 2, result.Count() ); Assert.AreEqual( Employee.GetEmployee().Name, result.ToList()[0].Name ); Assert.AreEqual( Employee.GetEmployee2().Name, result.ToList()[1].Name ); } } }
我在測試中比較了Where與Filter的不一樣,你能夠本身運行一下,若是還不知道如何運行測試,請參考Util應用程序框架公共操做類(二):數據類型轉換公共操做類(源碼篇)。
固然使用Where查詢比較死板,你須要在編譯時期固定查詢字段和操做符,這對於某些須要更靈活的場景並不合適,不過通常的系統對查詢靈活性要求都不高。
本文雖然是針對IQueryable進行擴展,但思路上對於更原始的Ado.Net直接操做Sql一樣適用。能夠看出,.Net Framework給你提供的API比較原始,若是須要知足本身的需求,就須要擴展你的應用程序框架。另外不要輕視這個小小的擴展和封裝,由於你的大多業務都須要查詢,若是你有100個模塊,每一個模塊有5個查詢條件,能幫你省下500個判斷。判斷語句不只枯燥並且容易喧賓奪主,擾亂你的查詢主題。
.Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。
謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/
若是須要下載代碼,請參考Util應用程序框架公共操做類(六):驗證擴展