Fireasy Entity的linq內核解析是參考自iqtoolkit源碼的,作者熟讀源碼並吸收其博大精深的思想後,結合項目中的一些需求,對它進行了幾處改進。
一、邏輯刪除標記
做管理系統的開發者可能會習慣於對數據做邏輯刪除處理,即將數據打這一個標記,查詢的時候將這些數據過濾掉。在Fireasy Entity的元數據定義PropertyMapInfo類中,有IsDeletedKey這樣一個屬性,表示使用此列作為邏輯刪除標記。對應的,在查詢數據的時候,需要把這個標記拼到LINQ裡去,不然每次都要這樣的條件,多麻煩:
var list = db.Customers.Where(c => c.DelFlag == 0);
我的做法是,定義一個FakeDeleteFlagRewriter類,對表達式進行重寫,將具有IsDeletedKey屬性的字段等於0的條件加進去。
namespace Fireasy.Data.Entity.Linq.Translators { /// <summary> /// 用於為具有假刪除標記的查詢表達式添加標記條件。 /// </summary> public class FakeDeleteFlagRewriter : DbExpressionVisitor { private ColumnExpression fakeColumn; public static Expression Rewrite(Expression expression) { return new FakeDeleteFlagRewriter().Visit(expression); } /// <summary> /// 訪問 <see cref="SelectExpression"/>。 /// </summary> /// <param name="select">要訪問的表達式。</param> /// <returns></returns> protected override Expression VisitSelect(SelectExpression select) { if (select.From != null && select.From.NodeType == (ExpressionType)DbExpressionType.Table) { var table = (TableExpression)select.From; //首先要找到具有假刪除標記的列表達式 foreach (var column in select.Columns) { base.Visit(column.Expression); } if (fakeColumn != null && fakeColumn.Alias.Equals(table.Alias)) { var where = select.Where; var condExp = fakeColumn.Equal(Expression.Constant(0.ToType(fakeColumn.Type))); return select.Update(select.From, where != null ? Expression.And(where, condExp) : condExp, select.OrderBy, select.GroupBy, select.Skip, select.Take, select.Segment, select.IsDistinct, select.Columns, select.IsReverse); } } else if (select.From != null) { var from = base.Visit(select.From); return select.Update(from, select.Where, select.OrderBy, select.GroupBy, select.Skip, select.Take, select.Segment, select.IsDistinct, select.Columns, select.IsReverse); } return select; } /// <summary> /// 訪問 <see cref="ColumnExpression"/>。 /// </summary> /// <param name="column">要訪問的表達式。</param> /// <returns></returns> protected override Expression VisitColumn(ColumnExpression column) { //記錄下具有假刪除標記的列表達式。 if (fakeColumn == null && column.MapInfo != null && column.MapInfo.IsDeletedKey) { fakeColumn = column; } return column; } } }
這樣,在使用查詢的時候,再不也需要考慮有沒有忘記寫DelFlag == 0了,是不是很方便。
二、對Join表達式中一端具有Group謂詞的改進
某天,發現一個有趣的問題,我需要join一個先被Group後的序列,在join的on條件中,使用到了IGrouping<,>中的Key屬性,問題來了,Key不能被識別,怎麼辦?
using (var context = new DbContext()) { var group = context.ZjPayments .GroupBy(s => s.GcConId) .Select(s => new { s.Key, PaymentMoney = s.Sum(o => o.PaymentMoney), PaymentCount = s.Count() }); var list = context.ConLists .Where(s => s.ItemId == itemId) .Segment(pager) .OrderBy(sorting, u => u.OrderBy(t => t.SignDate)) .Join(group.DefaultIfEmpty(), s => s.Id, s => s.Key, (s, t) => new { s.Id, s.SectId, s.SectName, s.ConName, s.ConMoney, t.PaymentCount, t.PaymentMoney }); }
其實不難發現,只要將Key替換成Group語句中的KeySelector就可以了,並且還要考慮keySelector為匿名對象的情況。
一樣的,定義一個GroupKeyReplacer類,對表達式進行重寫。
namespace Fireasy.Data.Entity.Linq.Translators { /// <summary> /// 如果 <see cref="JoinExpression"/> 表達式中的一邊具有 Group 子表,則需要將連接條件中的 Key 表達式替換為相應的 <see cref="ColumnExpression"/> 對象。 /// </summary> public class GroupKeyReplacer : DbExpressionVisitor { private MemberInfo member = null; private Expression finder = null; public static Expression Replace(Expression expression) { var replacer = new GroupKeyReplacer(); replacer.Visit(expression); return replacer.finder ?? expression; } protected override Expression VisitMember(MemberExpression memberExp) { if (member == null) { member = memberExp.Member; } Visit(memberExp.Expression); return memberExp; } protected override Expression VisitNew(NewExpression newExp) { if (newExp.Type.IsGenericType && newExp.Type.GetGenericTypeDefinition() == typeof(Grouping<,>)) { Visit(newExp.Arguments[0]); return newExp; } return base.VisitNew(newExp); } protected override Expression VisitColumn(ColumnExpression column) { if (member != null && (member.Name == "Key" || member.Name == column.Name)) { finder = column; } return column; } } }
在QueryBinder類中找到BindJoin方法,修改原來的代碼,應用GroupKeyReplacer重寫表達式:
var outerKeyExpr = GroupKeyReplacer.Replace(Visit(outerKey.Body)); var innerKeyExpr = GroupKeyReplacer.Replace(Visit(innerKey.Body));
三、實體All的擴展
相信大家在使用Select謂詞的時候都有這樣的感觸,如果要加一個屬性返回,是不是要把實體類的大部分屬性全列出來,實體屬性少點還好,太多了會不會漏了某一個屬性。
[TestMethod] public void TestAllColumns() { var list = db.Orders.Select(s => new { s.CustomerID, s.EmployeeID, s.OrderID, s.OrderDate, s.Freight, ShortName = s.Customers.CompanyName.Substring(0, 1) }).ToList(); }
這只是一個簡單的示例,現實業務中,這個實體要返回的屬性可能不止這幾個,我一直在想,如果可以把這麼繁瑣的工作省略去那該多好。其實仔細的想一想,那也容易辦到。
對IEntity(所有的實體類型都實現了IEntity)擴展一個All方法:
/// <summary> /// 返回實體的所有屬性,以及 <paramref name="selector"/> 表達式中的字段。 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="entity"></param> /// <param name="selector"></param> /// <returns></returns> public static dynamic All(this IEntity entity, Expression<Func<object, object>> selector) { return null; }
在QueryBinder類裡增加一個BindAllFields方法,如下:
public Expression BindAllFields(Expression source, LambdaExpression selector) { if (selector.Body.NodeType != ExpressionType.New) { throw new ArgumentException(SR.GetString(SRKind.MustBeNewExpression)); } var newExp = (NewExpression)Visit(selector.Body); var arguments = newExp.Arguments.ToList(); var members = newExp.Members.ToList(); foreach (var property in PropertyUnity.GetPersistentProperties(source.Type)) { var columnExp = new ColumnExpression(property.Type, __alias, property.Name, property.Info); arguments.Add(columnExp); members.Add(property.Info.ReflectionInfo); } var keyPairArray = new Expression[members.Count]; var constructor = typeof(KeyValuePair<string, object>).GetConstructors()[0]; for (var i = 0; i < members.Count; i++ ) { keyPairArray[i] = Expression.New(constructor, Expression.Constant(members[i].Name), Expression.Convert(arguments[i], typeof(object))); } var arrayExp = Expression.NewArrayInit(typeof(KeyValuePair<string, object>), keyPairArray); return Expression.Convert(arrayExp, typeof(DynamicExpandoObject)); }
這樣,剛剛的查詢語句可以寫成這樣的了,省掉了多少工作:
[TestMethod] public void TestAllColumns() { var list = db.Orders.Select(s => s.All(t => new { ShortName = s.Customers.CompanyName.Substring(0, 1) })); }
不過請注意,使用All方法後,對象就變成dynamic類型了,序列元素既包含了Order的所有屬性,還包含ShortName這樣一個屬性。