Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining Entity Framework Keys using Fluent API

I am trying to define a key for a model type that has two key properties and is defined like this:

type Model () =
    member val IdOne = 0 with get, set
    member val IdTwo = 0 with get, set
    member val OtherProperty = "" with get, set

When I try to use this model in Entity Framework 5, I get the error that "Model has no key defined. Define the key for this EntityType". The model types are given, I cannot change them and add the [<Key>] attribute. So I tried the Fluent API.

In C#, you would do something like this:

modelBuilder.Entity<Model>().HasKey(m => new { m.IdOne, m.IdTwo });

It uses an anonymous type. But for the life of me I cannot figure out how to pull this off in F#. I tried Tuples, Records, even a regular type that has the properties IdOne and IdTwo:

// Regular type with properties IdOne & IdTwo.
type ModelKey (idOne, idTwo) =
    member this.IdOne = idOne
    member this.IdTwo = idTwo
modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> ModelKey (m.IdOne, m.IdTwo))
// ArgumentNullException: Value cannot be null. Parameter name: source

// Regular type with default constructor and properties IdOne & IdTwo.
type ModelKey2 () =
    member val IdOne = 0 with get, set
    member val IdTwo = 0 with get, set
modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> ModelKey2 ())
// ArgumentNullException: Value cannot be null. Parameter name: source

// Record type.
type ModelKeyRecord = { IdOne : Int32; IdTwo : Int32 }
modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> { IdOne = m.IdOne; IdTwo = m.IdTwo })
// ArgumentNullException: Value cannot be null. Parameter name: source

// Tuple.
modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> (m.IdOne, m.IdTwo))
// ArgumentNullException: Value cannot be null. Parameter name: source

None of these approaches work, I get an ArgumentNullException every time. I'm out of ideas...


EDIT With the code Tomas provided below (that causes the same ArgumentNullException btw.), I did some snooping around. This is what I found:

I used the function below to analyze the Expression Tree C# is building:

static void Analyze<T>(Expression<Func<Model, T>> function)
{
}

// Call it like this:
Analyze(m => new { m.IdOne, m.IdTwo });

Then I looked at the generated lambda in the debugger. This is what C# generates:

{m => new <>f__AnonymousType0'2(IdOne = m.IdOne, IdTwo = m.IdTwo)}

Doing the same thing on the F# side using the getFuncTree function from Tomas using <@ fun (m : Model) -> ModelKey(m.IdOne, m.IdTwo) @> yields:

{m => new ModelKey(m.IdOne, m.IdTwo)}

As you can see, the explicit naming of the - what is this anyway, looks like properties - arguments is missing in the F# code. I recreated the whole expression tree by hand in F# so that it would look like the C# version:

let modelKeyExpression =
    Expression.Lambda<Func<Model, ModelKey>> (
        body = Expression.New (
            ``constructor`` = typeof<ModelKey>.GetConstructor [| typeof<Int32>; typeof<Int32> |],
            arguments = seq {
                yield Expression.MakeMemberAccess (
                    expression = Expression.Parameter (
                        ``type`` = typeof<Model>,
                        name = "m"
                        ),
                    ``member`` = typeof<Model>.GetProperty "IdOne"
                    ) :> Expression;
                yield Expression.MakeMemberAccess (
                    expression = Expression.Parameter (
                        ``type`` = typeof<Model>,
                        name = "m"
                        ),
                    ``member`` = typeof<Model>.GetProperty "IdTwo"
                    ) :> Expression
                },
            members = seq {
                yield (typeof<ModelKey>.GetProperty "IdOne") :> MemberInfo
                yield (typeof<ModelKey>.GetProperty "IdTwo") :> MemberInfo
                }
            ),
        parameters = [
            Expression.Parameter (
                ``type`` = typeof<Model>,
                name = "m"
                )
            ]
        )

The part that was missing in the F# representation is the members sequence. When I move the mouse over this expression, this representation appears:

{m => new ModelKey(IdOne = m.IdOne, IdTwo = m.IdTwo)}

As you can see, apart from the class, it looks the same. But when I try to use this expression in the HasKey method, I get the following InvalidOperationException:

The properties expression 'm => new ModelKey(IdOne = m.IdOne, IdTwo= m.IdTwo)'
is not valid. The expression should represent a property: C#: 't =>
t.MyProperty'  VB.Net: 'Function(t) t.MyProperty'. When specifying multiple
properties use an anonymous type: C#: 't => new { t.MyProperty1,
t.MyProperty2 }'  VB.Net: 'Function(t) New With { t.MyProperty1,
t.MyProperty2 }'.

So it seems to me that this anonymous class syntax does something special...

like image 467
Nikon the Third Avatar asked Aug 22 '13 13:08

Nikon the Third


People also ask

How do you configure a primary key for an entity in Entity Framework Fluent API?

Configuring a primary key By convention, a property named Id or <type name>Id will be configured as the primary key of an entity. Owned entity types use different rules to define keys. You can configure a single property to be the primary key of an entity as follows: Data Annotations.

How do I add a foreign key in Fluent API?

You can then configure foreign key properties by using the HasForeignKey method. This method takes a lambda expression that represents the property to be used as the foreign key.

How do I add a composite primary key in Fluent API?

The only way to configure composite keys is to use the HasKey method. You specify the properties that form the composite key by passing them in as properties of an anonymous type to the HasKey method.

Why we use Fluent API with Entity Framework?

Fluent API is another way to configure your domain classes. The Code First Fluent API is most commonly accessed by overriding the OnModelCreating method on your derived DbContext. Fluent API provides more functionality for configuration than DataAnnotations.


2 Answers

In F# 4.6, this has worked for me after much struggle.

Apperantly all you need is a tuple. Makes sense since an anon object without explicit member names is basically, a tuple.

I can't believe nobody has put this on the MS documentation tho.

    modelBuilder.Entity<Entity>()
      .HasKey(fun e -> (e.Key1, e.Key2) :> obj)
      |> ignore
like image 131
Dog Avatar answered Oct 06 '22 04:10

Dog


EDIT: The technique below would be needed in F# 2.0, but it should not be needed in the newer versions. There must be some other issue with the F#-generated expression tree...

I think the problem is that Entity Framework wants you to specify a lambda expression as an expression tree:

modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> ModelKey (m.IdOne, m.IdTwo))

This should just work in F# 3.1 in Visual Studio 2013, but it is not supported in F# 3.0. You can still do this, but you'll have to use F# quotations and write a bit of code that converts a quotation to LINQ expression tree - there is a helper that does most of the work:

open System
open System.Linq.Expressions
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Linq.RuntimeHelpers

let getFuncTree (quotation:Expr<'a -> 'b>) = 
  let e = LeafExpressionConverter.QuotationToExpression quotation
  let call = e :?> MethodCallExpression
  let lam = call.Arguments.[0] :?> LambdaExpression
  Expression.Lambda<Func<'a, 'b>>(lam.Body, lam.Parameters)

getFuncTree <@ fun x -> x + 1 @>

Using this, you should be able to call:

modelBuilder.Entity<Model>().HasKey(getFuncTree <@ fun (m : Model) -> 
  ModelKey (m.IdOne, m.IdTwo) @>)

You could define an extension method like HasKeyQuot that does this behind the cover, to make the code a bit nicer.

like image 37
Tomas Petricek Avatar answered Oct 06 '22 05:10

Tomas Petricek