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这样一个属性。