Entity Framework技巧系列之九 - Tip 35 - 36

提示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…

时间: 2024-10-13 10:31:09

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

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

提示51. 怎样由任意形式的流中加载EF元数据 在提示45中我展示了怎样在运行时生成一个连接字符串,这相当漂亮. 其问题在于它依赖于元数据文件(.csdl .ssdl .msl)存在于本地磁盘上. 但是如果这些文件存在于web服务器中或者类似的位置,甚至你无权访本地文件系统而无法把它们拷贝到本地呢? 原来你也可以由流中加载元数据,这篇提示将告诉你怎么做. 步骤1:获得用于CSDL,MSL与SSDL的XmlTextReaders: 这可以尽可能的简单,如'new XmlTextReader(url

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

提示42. 怎样使用Code-Only创建一个动态模型 背景: 当我们给出使用Code-Only的例子,总是由创建一个继承自ObjectContext的强类型的Context开始.这个类用于引导模型. 例如这个类(处于简化问题考虑省略了属性体): 1 public class MyContext : ObjectContext 2 { 3 public ObjectSet<Category> Categories { get; } 4 public ObjectSet<Product&g

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

提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预先加载一个相关项目(比方说,Reviews),但又仅要那些匹配一些条件的reviews(如,Review.Stars==5). 不幸的是EF的预先加载对此没有完整的支持,如,对于 ObjectQuery<Movie>.Include(…) 方法,Include或者是全部加载或者是不加载任何东西.

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

提示6. 如何及何时使用贪婪加载 什么时候你需要使用贪婪加载? 通常在你的程序中你知道对查询到的实体将要进行怎样的操作. 例如,如果你查询一个订单以便为一个客户重新打印,你知道没有组成订单的项目即产品的信息重打印将是不完整的,所以你知道你将需要同时加载这些信息. 这是贪婪加载起作用的一类场景. 如果你知道你需要额外信息,或实体,你可能也会预先加载这些实体(贪婪加载),因为这将省下生在将来的查询. 怎样进行贪婪加载? 与一些普遍存在的错误观念相反,Entity Framework中贪婪加载即可行也

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

提示37. 怎样进行按条件包含(Conditional Include) 问题 几天前有人在StackOverflow上询问怎样进行按条件包含. 他们打算查询一些实体(比方说Movies),并且希望预先加载一个相关项目(比方说,Reviews),但又仅要那些匹配一些条件的reviews(如,Review.Stars==5). 不幸的是EF的预先加载对此没有完整的支持,如,对于 ObjectQuery<Movie>.Include(-) 方法,Include或者是全部加载或者是不加载任何东西.

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

提示16. 当前如何模拟.NET 4.0的ObjectSet<T> 背景: 当前要成为一名EF的高级用户,你确实需要熟悉EntitySet.例如,你需要理解EntitySet以便使用 AttachTo(-) 或创建EntityKey. 在大部分情况下,针对每个对象/clr类型只有一个可能的EntitySet.Tip 13正是利用这种想法来简化附加(Attach)对象并且你也可以对Add使用类似的技巧. 然而为了在.NET 4.0中解决这个问题,我们添加了一个叫做 ObjectSet<T&

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

提示20. 怎样处理固定长度的主键 这是正在进行中的Entity Framework提示系列的第20篇. 固定长度字段填充: 如果你的数据库中有一个固定长度的列,例如像NCHAR(10)类型的列,当你进行一次插入时,填充会自动发生.所以例如如果你插入'12345',你将得到5个自动填充的空格,来创建一个10个字符长度的字符串. 大多数情况下,这种自动填充不会有问题.但是在使用Entity Framework时如果你使用这些列的一个作为你的主键,你可能会在进行标识识别(identity resol

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

提示46. 怎样使用Code-Only排除一个属性  这次是一个真正简单的问题,由StackOverflow上这个问题引出.  问题:  当我们使用Code-Only把一个类的信息告诉Entity Framework,默认情况下每个属性会成为Entity的一部分,并作为一个存储于数据库中的结果. 通常这是你想要的结果. 但是也有例外,考虑这个类: 1 public class Person{ 2 public int ID {get;set;} 3 public string Firstname

Entity Framework技巧系列之七 - Tip 26 – 28

提示26. 怎样避免使用不完整(Stub)实体进行数据库查询 什么是不完整(Stub)实体? 不完整实体是一个部分填充实体,用于替代真实的对象. 例如: 1 Category c = new Category {ID = 5}; 就是一个不完整实体. 这个实体中只有ID被填充,表示这是一个代表Category 5的Stub. Stub实体什么时候有用? 当你真正不需要知道一个实体的一切对象时,Stub实体就很有用,主要因为通过使用这种实体你可以避免不必要的查询,但也因为它们比EntityKey更