2016-06-11 55 views
4

我试图从查询中取回Envelope。信封定义如下。f#:只有无参数的构造函数和初始值设定项在LINQ to Entities中受支持

[<CLIMutable>] 
type Envelope<'T> = { 
    Id : Guid 
    StreamId: Guid 
    Created : DateTimeOffset 
    Item : 'T } 

MyLibAAS.DataStore.MyLibAASDbContext是一个用C#编写的EF DbContext。当我在F#它扩展如下,我得到的错误:Only parameterless constructors and initializers are supported in LINQ to Entities.

type MyLibAAS.DataStore.MyLibAASDbContext with 
    member this.GetEvents streamId = 
     query { 
      for event in this.Events do 
      where (event.StreamId = streamId) 
      select { 
       Id = event.Id; 
       StreamId = streamId; 
       Created = event.Timestamp; 
       Item = (JsonConvert.DeserializeObject<QuestionnaireEvent> event.Payload) 
      } 
     } 

如果我回到事件并将其映射在事后信封,它工作正常。

type MyLibAAS.DataStore.MyLibAASDbContext with 
    member this.GetEvents streamId = 
     query { 
      for event in this.Events do 
      where (event.StreamId = streamId) 
      select event 
     } |> Seq.map (fun event -> 
       { 
        Id = event.Id 
        StreamId = streamId 
        Created = event.Timestamp 
        Item = (JsonConvert.DeserializeObject<QuestionnaireEvent> event.Payload) 
       } 
      ) 

为什么这会有所作为?信封类型甚至不是EF类型。

回答

5

F#记录是如何工作的
F#记录编译到.NET类具有只读属性和需要作为参数(加上一些接口)的所有字段的值的构造函数。
例如,您的记录将在C#中大致表述如下:

public class Envelope<T> : IComparable<Envelope<T>>, IEquatable<Envelope<T>>, ... 
{ 
    public Guid Id { get; private set; } 
    public Guid StreamId { get; private set; } 
    public DateTimeOffset Created { get; private set; } 
    public T Item { get; private set; } 

    public Envelope(Guid id, Guid streamId, DateTimeOffset created, T item) { 
     this.Id = id; 
     this.StreamId = streamId; 
     this.Created = created; 
     this.Item = item; 
    } 

    // Plus implementations of IComparable, IEquatable, etc. 
} 

当你想创建一个F#记录,F#编译器发出调用此构造方法,为各个领域提供值。
例如,您的查询的select一部分会看在C#这样的:

select new Envelope<QuestionnaireEvent>( 
    event.Id, streamId, event.Timestamp, 
    JsonConvert.DeserializeObject<QuestionnaireEvent>(event.Payload)) 

实体框架的限制
恰巧,实体框架不允许调用查询非默认的构造函数。有一个很好的理由:如果它没有允许的话,你可以在原则上,建立这样的查询:

from e in ... 
let env = new Envelope<E>(e.Id, ...) 
where env.Id > 0 
select env 

实体框架将不知道如何编译此查询,因为它不知道传递给构造函数的e.Id的值将成为属性env.Id的值。对于F#记录来说,这一直是正确的,但对其他.NET类不是这样。
实体框架原则上可以识别出Envelope是一个F#记录并应用构造函数参数和记录属性之间的连接知识。但事实并非如此。不幸的是,实体框架的设计者并没有将F#视为有效的用例。
(有趣的事实:C#匿名类型相同的方式工作,而EF确实让他们异常)

如何解决这个
为了使这项工作,你需要声明Envelope为一个具有默认构造函数的类型。要做到这一点的唯一方法是使一类,而不是一个纪录:

type Envelope<'T>() = 
    member val Id : Guid = Guid.Empty with get, set 
    member val StreamId : Guid = Guid.Empty with get, set 
    member val Created : DateTimeOffset = DateTimeOffset.MinValue with get, set 
    member val Item : 'T = Unchecked.defaultof<'T> with get, set 

,然后使用属性初始化语法创建它:

select Envelope<_>(Id = event.Id, StreamId = streamId, ...) 

为什么移动selectSeq.map工作
Seq.map呼叫是不是查询表达式的一部分。它不会作为IQueryable的一部分,所以它不会最终由实体框架编译为SQL。相反,EF会编译query中的内容,并在从SQL Server中获取结果序列之后返回给您。只有在此之后,您才能将Seq.map应用于该序列。
Seq.map的代码是在CLR执行,而不是编译到SQL的,所以它可以调用任何它想做,包括非默认的构造函数。
这种“修复”自带虽然成本:不是你需要的领域,整个Event实体会从数据库获取和物化。如果这个实体很重,这可能会对性能产生影响。

再就是通过使Envelope与默认构造类型(如上述建议)要提防
即使你解决问题,你还是会打的下一个问题:该方法JsonConvert.DeserializeObject不能编译成SQL,所以实体框架也会抱怨它。您应该这样做的方式是将所有字段提取到CLR端,然后应用您需要的任何非SQL可编译的转换。

2

使用LINQ到实体,这种情况发生在query计算表达式实际数据库引擎中执行,其可以驻留在远程服务器上的所有内容。它之外的所有东西都在客户端的正在运行的应用程序中执行。

因此,在您的第一个片段中,实体框架拒绝执行Envelope<'T>的构造函数,因为为了这样做,它需要将其转换为服务器的SQL命令。这显然不是可以保证的,因为构造函数可能包含任何类型的副作用和.NET框架代码 - 它可以请求用户输入,从客户端硬盘读取文件,无论如何。

什么EF 可以做,在你的第二个片段,派遣自己的POCO event对象返回到客户端,然后将其与Seq.map平他们负责给你的想象Envelope s,这可以做,因为它是在您的客户端机器上运行,访问完整的.NET框架。

增编:那么,为什么构造好吗?如果我在无参数构造函数中调用MsgBox(),该怎么办?我想认为无参数构造函数通过让客户端构造对象(不知道查询结果),以序列化的形式将它们发送到服务器,并让服务器用查询结果填充对象的属性来工作。

我并没有实际测试过。但是F#记录类型无论如何都没有无参数的构造函数,所以对你的情况来说这个问题是没有意义的。

+0

关于为什么无参数构造函数工作的理论是错误的。 –

+0

我已经阅读了你的(非常全面的)答案,但它并没有完全解决这个问题。如果我声明一个'type Foo()= do MsgBox(“foo”)',并且我在L2E查询表达式中调用该无参数构造函数,会发生什么? – piaste

+0

取决于查询中如何使用该构造函数。如果构造函数用于构造从查询返回的值(例如,在最后的'select'语句中),那么您会为每个返回的行获取一个消息框。如果使用构造函数创建一些未从查询返回的中间值,则不会收到消息框。 –