我正在尝试从查询中获取信封的信息。信封定义如下。
[<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/