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

时间:2023-03-09 03:54:18
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

提示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表达式

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

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

该函数如下:

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

你可以将其方法与一些得到特定类型名称对应的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参数的类型,然后传入所有要排除的类型:

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

正如所见,第一个方法遍历所有类型并创建一个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实体。

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

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
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;
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

但是这些代码很笨重,一大堆属于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中查找一个实体:

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
 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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

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

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

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

问题的本质

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

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

提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法
  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 }
提示35. 怎样实现OfTypeOnly<TEntity>()这样的写法

真正的工作在VisitLambda中完成。

基本上,如果:

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

这个函数将抛出异常。

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

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

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

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

摘要

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