Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

f#: Only parameterless constructors and initializers are supported in LINQ to Entities

I am trying to get Envelope's back from a query. Envelope is defined as follows.

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

MyLibAAS.DataStore.MyLibAASDbContext is a EF DbContext written in c#. When I extend it in f# as follows, I get the error: 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)
            }
        } 

If I return the event and map it to Envelope after the fact, it works fine.

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

Why does this make a difference? The Envelope type is not even a EF type.

like image 768
Phillip Scott Givens Avatar asked Jun 11 '16 20:06

Phillip Scott Givens


People also ask

What is Mtouch Facebook?

Facebook Touch is an advanced Facebook app that has many distinct features. H5 apps developed it as an app made especially for touchscreen phones. Available and applicable across all smartphones, Facebook Touch offers a fine user interface and serves as an alternative to the typical Facebook App.

Is free Facebook free?

No, we don't charge you to use Facebook.


1 Answers

How F# records work
F# records get compiled into .NET classes with read-only properties and a constructor that takes values of all fields as parameters (plus a few interfaces).
For example, your record would be expressed in C# roughly as follows:

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.
}

When you want to create an F# record, the F# compiler emits a call to this constructor, supplying values for all fields.
For example, the select part of your query would look in C# like this:

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

Entity Framework limitations
It so happens that Entity Framework does not allow calling non-default constructors in queries. There is a good reason: if it did allow it, you could, in principle, construct a query like this:

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

Entity Framework wouldn't know how to compile this query, because it doesn't know that the value of e.Id passed to the constructor becomes the value of the property env.Id. This is always true for F# records, but not for other .NET classes.
Entity Framework could, in principle, recognize that Envelope is an F# record and apply the knowledge of the connection between constructor arguments and record properties. But it doesn't. Unfortunately, the designers of Entity Framework did not think of F# as a valid use case.
(fun fact: C# anonymous types work the same way, and EF does make an exception for them)

How to fix this
In order to make this work, you need to declare Envelope as a type with default constructor. The only way to do this is to make it a class, not a record:

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

And then create it using property initialization syntax:

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

Why does moving the select to a Seq.map work
The Seq.map call is not part of the query expression. It does not end up as part of the IQueryable, so it does not end up compiled to SQL by Entity Framework. Instead, EF compiles just what's inside query and returns you the resulting sequence, after fetching it from SQL Server. And only after that you apply Seq.map to that sequence.
The code inside Seq.map is executed on CLR, not compiled to SQL, so it can call anything it wants, including non-default constructors.
This "fix" comes with a cost though: instead of just the fields you need, the whole Event entity gets fetched from DB and materialized. If this entity is heavy, this may have a performance impact.

Another thing to watch out for
Even if you fix the problem by making Envelope a type with default constructor (as suggested above), you'll still hit the next problem: the method JsonConvert.DeserializeObject can't be compiled to SQL, so Entity Framework will complain about it, too. The way you should do it is fetch all fields to the CLR side, then apply whatever non-SQL-compilable transformations you need.

like image 154
Fyodor Soikin Avatar answered Sep 24 '22 17:09

Fyodor Soikin