提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

如果你编写这样LINQ to Entities查询:

1 var results = from c in ctx.Vehicles.OfType<Car>()
2 select c;

这会返回,Cars包括那些派生自Car类型,如SportCar或SUV类型的汽车。

如果你仅想要Cars即不想要如SportCar或SUV等派生类型汽车,你会在LINQ to Objects中这样写:

1 var results = from c in vehiclesCollection
2 where c.GetType() == typeof(Car)
3 select c;

但不幸的是LINQ to Entities不知道怎样翻译它。

注意:

在Entity SQL中实现这个相当容易。如果你使用 OFTYPE(collection, [ONLY]type) 并包含ONLY这个可选的关键字,将会排除派生类型的对象。

例如这个Entity SQL:

1 SELECT VALUE(C)
2 FROM Container.Vehicles AS C
3 WHERE C IS OF(ONLY Model.Car)

将只会返回Car,那些派生自Car的实体,如SUV,都会被排除。

大约六个月前,在提示5中,我展示了一种变通方法。你只需简单的按如下这样处理:

1 var results = from c in ctx.Vehicles.OfType<Car>()
2 where !(c is SUV) && !(c is SportsCar)
3 select c;

但是这种解决方案很笨重并且容易出错,所以我决定找到一种更好的解决方案。

你将可以编写如下这样的代码:

1 var results = from c in ctx.Vehicles.OfTypeOnly<Car>()
2 select c;

在这个方法的背后需要需要完成如下这些:

  1. 在源ObjectQuery上调用OfType<Car>()方法来得到一个OjbectQuery<Car>()
  2. 识别哪些实体类型派生自Car
  3. 构建一个Lambda表达式有结果中排除所有这些派生类型。
  4. 在OjbectQuery<Car>()上调用Where(Expression<Func<Car,bool>>),其中传入上一步的Lambda表达式

让我们看一下代码是什么样。

下面的方法将所有代码结合在一起:

 1 public static IQueryable<TEntity> OfTypeOnly<TEntity>(
2 this ObjectQuery query)
3 {
4 query.CheckArgumentNotNull("query");
5 // Get the C-Space EntityType
6 var queryable = query as IQueryable;
7 var wkspace = query.Context.MetadataWorkspace;
8 var elementType = typeof(TEntity);
9 // Filter to limit to the DerivedType of interest
10 IQueryable<TEntity> filter = query.OfType<TEntity>();
11 // See if there are any derived types of TEntity
12 EntityType cspaceEntityType =
13 wkspace.GetCSpaceEntityType(elementType);
14 if (cspaceEntityType == null)
15 throw new NotSupportedException("Unable to find C-Space type");
16 EntityType[] subTypes = wkspace.GetImmediateDescendants(cspaceEntityType).ToArray();
17 if (subTypes.Length == 0) return filter;
18 // Get the CLRTypes.
19 Type[] clrTypes = subTypes
20 .Select(st => wkspace.GetClrTypeName(st))
21 .Select(tn => elementType.Assembly.GetType(tn))
22 .ToArray();
23
24 // Need to build the !(a is type1) && !(a is type2) predicate and call it
25 // via the provider
26 var lambda = GetIsNotOfTypePredicate(elementType, clrTypes);
27 return filter.Where(
28 lambda as Expression<Func<TEntity, bool>>
29 );
30 }

正如你所见我们在MetadataWorkspace使用了一个称作 GetCSpaceEntityType() 的扩展方法,其接受一个CLR类型,返回相应的EntityType。

该函数如下:

 1 public static EntityType GetCSpaceEntityType(
2 this MetadataWorkspace workspace,
3 Type type)
4 {
5 workspace.CheckArgumentNotNull("workspace");
6 // Make sure the metadata for this assembly is loaded.
7 workspace.LoadFromAssembly(type.Assembly);
8 // Try to get the ospace type and if that is found
9 // look for the cspace type too.
10 EntityType ospaceEntityType = null;
11 StructuralType cspaceEntityType = null;
12 if (workspace.TryGetItem<EntityType>(
13 type.FullName,
14 DataSpace.OSpace,
15 out ospaceEntityType))
16 {
17 if (workspace.TryGetEdmSpaceType(
18 ospaceEntityType,
19 out cspaceEntityType))
20 {
21 return cspaceEntityType as EntityType;
22 }
23 }
24 return null;
25 }

这个方法看起来熟悉吗?是的,在提示13中我介绍过它。事实上这个函数是你EF工具箱中一个很方便的工具。

一旦我们得到EntityType,我们就能查找派生的EntityType,这时 GetImmediateDescendants() 方法登场了。该方法如下:

 1 public static IEnumerable<EntityType> GetImmediateDescendants(
2 this MetadataWorkspace workspace,
3 EntityType entityType)
4 {
5 foreach (var dtype in workspace
6 .GetItemCollection(DataSpace.CSpace)
7 .GetItems<EntityType>()
8 .Where(e =>
9 e.BaseType != null &&
10 e.BaseType.FullName == entityType.FullName))
11 {
12 yield return dtype;
13 }
14 }

注意:我只对直接派生类感兴趣,因为当直接派生类被过滤掉,它们的派生类也将被过滤。

下一步我们需要得到每一个EntityType对应的CLR类型。要完成这个工作使用一个通过EF元数据来查找每个Entity Type对应的CLR类型名函数,其如下这样:

 1 public static string GetClrTypeName(
2 this MetadataWorkspace workspace,
3 EntityType cspaceEntityType)
4 {
5 StructuralType ospaceEntityType = null;
6 if (workspace.TryGetObjectSpaceType(
7 cspaceEntityType, out ospaceEntityType))
8 return ospaceEntityType.FullName;
9 else
10 throw new Exception("Couldn’t find CLR type");
11 }

你可以将其方法与一些得到特定类型名称对应的CLR类型的代码进行组合。

编写一些防错误的方法会使情况变复杂,但在我的例子中我仅假设所有类型都在与TEntity相同的程序集中。这样事情就变得很简单:

1 // Get the CLRTypes.
2 Type[] clrTypes = subTypes
3 .Select(st => wkspace.GetClrTypeName(st))
4 .Select(tn => elementType.Assembly.GetType(tn))
5 .ToArray();

…我非常确信如果需要此功能,你可以指出怎样使这个方法更强壮一些:)

这时候我们暂时把EF元数据API放在后面,转向Expression API。

Gulp!

实际上我曾认为这很简单。

我们仅需要一个lambda表达式来滤掉所有派生的CLR类型。等价于这样的形式:

(TEntity entity) => !(entity is TSubType1) && !(entity is TSubType2)

所以我添加了下面这个方法,第一个参数是lambda参数的类型,然后传入所有要排除的类型:

 1 public static LambdaExpression GetIsNotOfTypePredicate(
2 Type parameterType,
3 params Type[] clrTypes)
4 {
5 ParameterExpression predicateParam =
6 Expression.Parameter(parameterType, "parameter");
7
8 return Expression.Lambda(
9 predicateParam.IsNot(clrTypes),
10 predicateParam
11 );
12 }

正如你所见,这个方法创建了一个参数,然后调用另一个扩展方法来创建所需的AndAlso表达式:

 1 public static Expression IsNot(
2 this ParameterExpression parameter,
3 params Type[] types)
4 {
5 types.CheckArgumentNotNull("types");
6 types.CheckArrayNotEmpty("types");
7 Expression merged = parameter.IsNot(types[0]);
8 for (int i = 1; i < types.Length; i++)
9 {
10 merged = Expression.AndAlso(merged,
11 parameter.IsNot(types[i]));
12 }
13 return merged;
14 }
15 public static Expression IsNot(
16 this ParameterExpression parameter,
17 Type type)
18 {
19 type.CheckArgumentNotNull("type");
20 var parameterIs = Expression.TypeIs(parameter, type);
21 var parameterIsNot = Expression.Not(parameterIs);
22 return parameterIsNot;
23 }

正如所见,第一个方法遍历所有类型并创建一个IsNot表达式(通过调用第二个方法),然后通过创建一个AndAlso表达式来与之前创建的表达式合并。

注意:你可能已经注意到这段代码可能会产生深度很大的AndAlso调用层次图像。我认为这或许还好,但是如果你有一个层次特别宽广的类型,你可能想要考虑如何重写这个查询来平衡调用树。

到目前为止我们有一种方法来创建一个LambdaExpression来进行需要的过滤,我们仅需将其转换为 Expression<Func<Tentity, bool>> 并将其传入 Where(…) 扩展方法,像如下这样:

1 var lambda = GetIsNotOfTypePredicate(elementType, clrTypes);
2 return filter.Where(
3 lambda as Expression<Func<TEntity, bool>>
4 );

这样就完成了!

首先我承认这并不完全是“小时一桩”,但是我乐于开发这样的解决方案,它促使我更多的了解Expression与EF元数据API。

希望你也觉得这很有趣。

提示36. 怎样通过查询构造

在写作提示系列文章同时编写用于MVC的EF控制器的过程中,我发现我规律性的想要创建并附加一个Stub实体。

不幸的是这并不十分容易,你需要首先确保实体没有已经被加载,否则你将看到一些恼人的异常。

要避免这些异常,我常发现我自己不得不写一些下面这样的代码:

1 Person assignedTo = FindPersonInStateManager(ctx, p => p.ID == 5);
2 if (assignedTo == null)
3 {
4 assignedTo = new Person{ID = 5};
5 ctx.AttachTo(“People”, assignedTo);
6 }
7 bug.AssignedTo = assignedTo;

但是这些代码很笨重,一大堆属于EF功能的部分污染了我的业务逻辑,使其变得很难读取与编写。

我希望自己可以编写这样的代码来替代:

1 bug.AssignedTo = ctx.People.GetOrCreateAndAttach(p => p.ID == 5);

现在有一些机制来使这成为可能,但是核心问题是将如下:

1 (Person p) => p.ID == 5;

这样的断言或查询转换为如下:

1 () => new Person {ID = 5};

这样的包含成员初始化表达式(MemberInitExpression)体的Lambda表达式。

通过例子查询(Query By Example)

熟悉ORM历史的人可能记得,在“过去的好时光”里一大些“ORM”使用一种称为Query by Example的模式:

1 Person result = ORM.QueryByExample(new Person {ID = 5});

通过Query by Example你可以创建一个想要由数据库类的实例并填充某些字段,ORM使用这个样例对象基于其中被设置的值来创建一个查询。

通过查询构造?

我提到这个是因为由一个查询得到实例的过程看起来与由一个实例生成一个查询的方式(如Query by Example)恰好相反。

因此这篇博客的标题为:“通过查询构造(Construct by Example)”

对于我这种类比/对照使这个想法更加绚丽。

但是,哈,那是我!

实现

不管怎么说…我们如何能真正做到这一点:

工作第一步,我们需要一个方法在ObjectStateManager中查找一个实体:

 1 public static IEnumerable<TEntity> Where<TEntity>(
2 this ObjectStateManager manager,
3 Func<TEntity, bool> predicate
4 ) where TEntity: class
5 {
6 return manager.GetObjectStateEntries(
7 EntityState.Added |
8 EntityState.Deleted |
9 EntityState.Modified |
10 EntityState.Unchanged
11 )
12 .Where(entry => !entry.IsRelationship)
13 .Select(entry => entry.Entity)
14 .OfType<TEntity>()
15 .Where(predicate);
16 }

然后我们实际编写 GetOrCreateAndAttachStub(…) 这个扩展方法:

 1 public static TEntity GetOrCreateAndAttachStub<TEntity>(
2 this ObjectQuery<TEntity> query,
3 Expression<Func<TEntity, bool>> expression
4 ) where TEntity : class
5 {
6 var context = query.Context;
7 var osm = context.ObjectStateManager;
8 TEntity entity = osm.Where(expression.Compile())
9 .SingleOrDefault();
10
11 if (entity == null)
12 {
13 entity = expression.Create();
14 context.AttachToDefaultSet(entity);
15 }
16 return entity;
17 }

这一步中在ObjectStateManager中查找一个匹配。

如果基于被编译的断言表达式转换的带有MemberInitExpression体的LambdaExpression无法找到对象,则调用这个Lambda表达式的Create方法来创建一个TEntity的实例并附加它。

我不准备深入展开AttachToDefaultSet方法,因为在之前的提示13中我已分享了具体代码。

所以我们跳过它,马上开始…

问题的本质

Create扩展方法,看起来如这样:

1 public static T Create<T>(
2 this Expression<Func<T, bool>> predicateExpr)
3 {
4 var initializerExpression = PredicateToConstructorVisitor
5 .Convert<T>(predicateExpr);
6 var initializerFunction = initializerExpression.Compile();
7 return initializerFunction();
8 }

PredicateToConstructorVisitor是一个特定的ExpressionVisitor,其仅将一个断言表达式转换为一个MemberInitExpression。

  1 public class PredicateToConstructorVisitor
2 {
3 public static Expression<Func<T>> Convert<T>(
4 Expression<Func<T, bool>> predicate)
5 {
6 PredicateToConstructorVisitor visitor =
7 new PredicateToConstructorVisitor();
8 return visitor.Visit<T>(predicate);
9 }
10 protected Expression<Func<T>> Visit<T>(
11 Expression<Func<T, bool>> predicate)
12 {
13 return VisitLambda(predicate as LambdaExpression)
14 as Expression<Func<T>>;
15 }
16 protected virtual Expression VisitLambda(
17 LambdaExpression lambda)
18 {
19 if (lambda.Body is BinaryExpression)
20 {
21 // Create a new instance expression i.e.
22 NewExpression newExpr =
23 Expression.New(lambda.Parameters.Single().Type);
24
25 BinaryExpression binary =
26 lambda.Body as BinaryExpression;
27
28 return Expression.Lambda(
29 Expression.MemberInit(
30 newExpr,
31 GetMemberAssignments(binary).ToArray()
32 )
33 );
34 }
35 throw new InvalidOperationException(
36 string.Format(
37 "OnlyBinary Expressions are supported.\n\n{0}",
38 lambda.Body.ToString()
39 )
40 );
41 }
42
43 protected IEnumerable<MemberAssignment> GetMemberAssignments(
44 BinaryExpression binary)
45 {
46 if (binary.NodeType == ExpressionType.Equal)
47 {
48 yield return GetMemberAssignment(binary);
49 }
50 else if (binary.NodeType == ExpressionType.AndAlso)
51 {
52 foreach (var assignment in
53 GetMemberAssignments(binary.Left as BinaryExpression).Concat(GetMemberAssignments(binary.Right as BinaryExpression)))
54 {
55 yield return assignment;
56 }
57 }
58 else
59 throw new NotSupportedException(binary.ToString());
60 }
61
62 protected MemberAssignment GetMemberAssignment(
63 BinaryExpression binary)
64 {
65 if (binary.NodeType != ExpressionType.Equal)
66 throw new InvalidOperationException(
67 binary.ToString()
68 );
69
70 MemberExpression member = binary.Left as MemberExpression;
71
72 ConstantExpression constant
73 = GetConstantExpression(binary.Right);
74
75 if (constant.Value == null)
76 constant = Expression.Constant(null, member.Type);
77
78 return Expression.Bind(member.Member, constant);
79 }
80
81 protected ConstantExpression GetConstantExpression(
82 Expression expr)
83 {
84 if (expr.NodeType == ExpressionType.Constant)
85 {
86 return expr as ConstantExpression;
87 }
88 else
89 {
90 Type type = expr.Type;
91
92 if (type.IsValueType)
93 {
94 expr = Expression.Convert(expr, typeof(object));
95 }
96
97 Expression<Func<object>> lambda
98 = Expression.Lambda<Func<object>>(expr);
99
100 Func<object> fn = lambda.Compile();
101
102 return Expression.Constant(fn(), type);
103 }
104 }
105 }

真正的工作在VisitLambda中完成。

基本上,如果:

  1. 这个Lambda表达式不是一个BinaryExpression。
  2. Lambda表达式有多于一个参数。我们仅能构造一个!

这个函数将抛出异常。

然后我们开始遍历BinaryExpression直到我们得到判断相等的节点,如(p.ID == 5),我们将其转换为成员赋值语句(ID = 5),这样我们就可以构造一个MemberInitExpression。

当创建一个成员赋值语句,我们也要把所有等号右侧的表达式转换为一个常量。例如,如果Lambda表达式如下这样:

(Person p) => p.ID == GetID();

我们要计算GetID(),这样我们可以在成员赋值语句中使用这个结果。

摘要

又一次我演示了混合EF元数据与CLR表达式来使编写真正有用的帮助函数变得可能,也使你编写应用的过程少了许多痛苦。

Enjoy…

Entity Framework技巧系列之九 - Tip 35 - 36的更多相关文章

  1. Entity Framework技巧系列之十三 - Tip 51 - 55

    提示51. 怎样由任意形式的流中加载EF元数据 在提示45中我展示了怎样在运行时生成一个连接字符串,这相当漂亮. 其问题在于它依赖于元数据文件(.csdl .ssdl .msl)存在于本地磁盘上. 但 ...

  2. Entity Framework技巧系列之十一 - Tip 42 - 45

    提示42. 怎样使用Code-Only创建一个动态模型 背景: 当我们给出使用Code-Only的例子,总是由创建一个继承自ObjectContext的强类型的Context开始.这个类用于引导模型. ...

  3. (翻译)Entity Framework技巧系列之十 - Tip 37 - 41

    提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预 ...

  4. Entity Framework技巧系列之十 - Tip 37 - 41

    提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预 ...

  5. Entity Framework技巧系列之二 - Tip 6 - 8

    提示6. 如何及何时使用贪婪加载 什么时候你需要使用贪婪加载? 通常在你的程序中你知道对查询到的实体将要进行怎样的操作. 例如,如果你查询一个订单以便为一个客户重新打印,你知道没有组成订单的项目即产品 ...

  6. Entity Framework技巧系列之五 - Tip 16 – 19

    提示16. 当前如何模拟.NET 4.0的ObjectSet<T> 背景: 当前要成为一名EF的高级用户,你确实需要熟悉EntitySet.例如,你需要理解EntitySet以便使用 At ...

  7. Entity Framework技巧系列之六 - Tip 20 – 25

    提示20. 怎样处理固定长度的主键 这是正在进行中的Entity Framework提示系列的第20篇. 固定长度字段填充: 如果你的数据库中有一个固定长度的列,例如像NCHAR(10)类型的列,当你 ...

  8. Entity Framework技巧系列之十四 - Tip 56

    提示56. 使用反射提供程序编写一个OData Service 在TechEd我收到一大堆有关将数据作为OData暴露的问题. 到目前为止你大概知道可以使用数据服务与Entity Framework将 ...

  9. Entity Framework技巧系列之十二 - Tip 46 - 50

    提示46. 怎样使用Code-Only排除一个属性  这次是一个真正简单的问题,由StackOverflow上这个问题引出.  问题:  当我们使用Code-Only把一个类的信息告诉Entity F ...

随机推荐

  1. vue 相对其他热门 框架 优点 --- 待续

    react vs  vue 1. 处理动画 vue 更有优势 , 这是由于 React 有大量的检查机制 2.性能更高, 在React中,你需要在每个地方去实现 shouldComponentUpda ...

  2. redis34--string 操作

    String类型操作 1.set key value 设置key对应的值为string类型的value  2.mset key1 value1 - keyN valueN 一次设置多个key的值 3. ...

  3. Spring 上传文件

    最近碰到一个上传文件的需求,其实之前也做过但是都是search->copy 没有细究过,这次纯手工. 先看一下需要依赖的包: <dependency> <groupId> ...

  4. ubuntu 搭建python2.x 抓取环境

    1.apt-get install python-bs4 bs4只有py2的代码,安装在py3下会很麻烦 bs4支持HTML parser,也可以支持第三方的分析器 2.apt-get install ...

  5. 全球互联网技术大会GITC 2016 最炫酷技术盛宴

    2016年对于全球互联网产业来说,可谓是不折不扣的"创新爆发年",科技创新的更迭速度和多元化趋势都呈现出全所未见的增长态势.我们看到,云计算.大数据等在多年前萌发的技术创新正在快速 ...

  6. spring注解支持

    Spring基于注解实现Bean定义支持如下三种注解: Spring自带的@Component注解及扩展@Repository.@Service.@Controller JSR-250 1.1版本中中 ...

  7. php日期转时间戳,指定日期转换成时间戳

    写过PHP+MySQL的程序员都知道有时间差,UNIX时间戳和格式化日期是我们常打交道的两个时间表示形式,Unix时间戳存储.处理方便,但 是不直观,格式化日期直观,但是处理起来不如Unix时间戳那么 ...

  8. javascript apply()和call()

    原文链接 http://www.jb51.net/article/30883.htm 想要理解透彻apply()和call() ,还要需要理解this  作用域 局部变量  全局变量 js apply ...

  9. POJ 1042 Gone Fishing#贪心

    (- ̄▽ ̄)-* #include<iostream> #include<cstdio> #include<cstring> using namespace std ...

  10. 安卓访问webAPI,并取回数据

    前言 安卓自从4.0以后,所有的网络访问都需要异步进程操作.其自带的异步类有AsyncTask,Handler,以及可以声明Thread等等.涉及到多进程,必须要提到一个问题,线程与线程之间不能直接进 ...