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這樣一個屬性。