我正在尝试从查询中获取信封的信息。信封定义如下。

[<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)
            }
        }

如果我在事件结束后返回该事件并将其映射到Envelope,它就可以正常工作。
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类型。

最佳答案

F#记录的工作方式
F#记录被编译成具有只读属性的.NET类和一个构造函数,该构造函数将所有字段的值用作参数(加上一些接口(interface))。
例如,您的记录将大致用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) )

Entity Framework 限制
碰巧, Entity Framework 不允许在查询中调用非默认构造函数。有一个很好的理由:如果允许的话,原则上您可以构造一个查询,如下所示:
from e in ...
let env = new Envelope<E>( e.Id, ... )
where env.Id > 0
select env

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

如何修复此
为了使此工作有效,您需要使用默认构造函数将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, ... )

为什么将select移到Seq.map会起作用Seq.map调用不是查询表达式的一部分。它不会最终成为IQueryable的一部分,因此不会最终被Entity Framework编译为SQL。取而代之的是,EF从SQL Server提取后,仅编译query内的内容并返回给您结果序列。并且只有在那之后,您才能将Seq.map应用于该序列。Seq.map中的代码在CLR上执行,而不是编译为SQL,因此它可以调用所需的任何内容,包括非默认构造函数。
但是,此“修复”要付出一定的代价:从数据库中获取并实现整个Event实体,而不仅仅是您需要的字段。如果该实体很重,则可能会对性能产生影响。

提防的另一件事
即使通过使用默认构造函数将Envelope设置为类型来解决问题(如上所述),您仍然会遇到下一个问题:JsonConvert.DeserializeObject方法无法编译为SQL,因此Entity Framework也将提示它。您应该这样做的方法是将所有字段都提取到CLR端,然后应用所需的任何非SQL可编译的转换。

关于entity-framework - f#:LINQ to Entities仅支持无参数构造函数和初始化程序,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37768051/

10-11 22:35
查看更多