之前有篇文章(Linq to Entity經驗:表達式轉換)我分享過表達式轉換的問題,當時以為問題解決了,但后來實際應用中發現其實沒有解決,也並非完全沒有解決,只不過不實用,問題如下:
在講問題之前,先來看看表達式轉換的目的:
其實我們這么費勁的進行表達式轉換,就是為了讓我們在UI層寫的表達式樹條件能夠最終轉換成EntityFramework能夠識別的表達式,之所以需要轉換,那是因為EntityFramework只能識別EntityFramework自身的實體對象,如果我們是數據庫優先的話,這些實體就是自動生成的,而我們的業務系統往往有獨立於數據庫的業務實體,這兩者有些情況下相同,有些情況下非常不相同,這是我們需要轉換的原因,比如TestExpression.Model.Courier是業務實體,My.FrameWork.Utilities.Test.Courier是數據庫對象,看如下需求:
UI層進行數據搜索數據的條件搜集:
要想進行數據庫查詢,就需要將上面的表達式樹轉換成下面EntityFramework能夠識別的表達式樹。注意,這兩個表達式雖然對象名稱一樣,但命名空間不同。
問題:
StartsWith的參數是字符串,我們知道這個字符串和其它大多數基元數據不一樣,它不是值類型,而是引用類型,所以當外面的局部變量做為參數傳遞到表達式中時,在表達式樹進行解析時還會發生MemberAccess,而上篇文章中只支持參數類型是ConstantExpression,而這里的MemberExpression在解析時就是報異常。,比如下面的表達式是可以轉換的,直接傳遞"min",這就是一個ConstantExpression類型的參數:
但如果這樣寫就不行:
Expression<Func<TestExpression.Model.Courier, bool>> two2 = c => c.Value.StartsWith(name);
以及文章開關提到的表達式也不行:(下面的mes是一個class)
如何解決?這里分享下我解決問題的過程。
1:既然參數是ConstantExpression的情況可行,那么能否將原本不是ConstantExpression的參數變更為ConstantExpression。
有了這個想法,當時認為既然mes.Name是一個對象的屬性,如果對這個屬性值進行下字符串的深度復制是不是也就擺脫class的引用類型問題了,雖然擺脫了class的問題,但發現string本身就是一個引用類型,所以無論是否轉換還是沒能將非ConstantExpression轉換成ConstantExpression的情況,也就是說下面的處理是徒勞無功的。
{
public static string GetStringDeepClone( this string source)
{
string result = string.Empty;
using (MemoryStream ms = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, source );
source = null;
ms.Position = 0;
result = (( string)bf.Deserialize(ms));
}
return result;
}
}
2:既然引用類型的問題是會發生MemberAccess,那么是否能夠解析帶局部變量的MemberExpression?
像這行代碼,在整個表達式的上下文中是沒有mes類型信息的,它是一個外部的局部變量,這樣在解析時就會增大難度,后來我咨詢過腦袋(http://cnblogs.com/ninputer),他給我提供了一個轉換方法,后來由於代碼不全就沒繼續研究了,代碼的功能是可以解析這種帶局部變量的MemberExpression。
3:既然解析MemberExpression沒能成功,那么是否能夠從MemberExpression中直接抽取出值來呢?
在網上搜索了一番,發現一老外提供了一個方法能夠解決:
{
var objectMember = Expression.Convert(member, typeof( object));
var getterLambda = Expression.Lambda<Func< object>>(objectMember);
var getter = getterLambda.Compile();
return getter();
}
這個方法的思路就是將MemberExpression轉換成可執行的Lambda表達式,通過執行Lambda表達式得到計算值。
有了上面的方法,我們需要修改下原來的解析方法:主要是判斷參數表達式的類型,如果是值類型就走以前的邏輯,如果是引用類型,我們需要從MemberExpression中抽取數值,有了數據我們的問題就解決了。
{
var be = (MethodCallExpression)node;
var resultValue = string.Empty;
switch (be.Arguments[ 0].NodeType)
{
case ExpressionType.MemberAccess:
var exprssion = GetMemberExpressionValue((MemberExpression)be.Arguments[ 0]);
if ( null != exprssion)
{
resultValue = exprssion.ToString();
}
break;
case ExpressionType.Constant:
resultValue=((ConstantExpression)be.Arguments[ 0]).Value.ToString();
break;
}
var expression = GetMethodExpression((MemberExpression)be.Object, resultValue, be.Method.Name, subst);
return expression.Body;
}
到此,表達式樹的轉換終於可以在實際項目應用了,這對我們動態搜集查詢條件非常有幫助,盡管這個問題困擾我超過半個月時間,但最終還是得到解決了。表達式的轉換比較麻煩,如果想讓自己的表達式轉換功能越來越強大,那么我們需要針對不同的情況編寫對應的解決方案才行,沒有完美只有最適合。