Определение ключей Entity Framework с использованием Fluent API
Я пытаюсь определить ключ для типа модели, который имеет два ключевых свойства и определяется следующим образом:
type Model () =
member val IdOne = 0 with get, set
member val IdTwo = 0 with get, set
member val OtherProperty = "" with get, set
Когда я пытаюсь использовать эту модель в Entity Framework 5, я получаю сообщение об ошибке "У модели нет определенного ключа. Определите ключ для этого EntityType". Типы моделей приведены, я не могу их изменить и добавить [<Key>]
приписывать. Поэтому я попробовал Fluent API.
В C# вы бы сделали что-то вроде этого:
modelBuilder.Entity<Model>().HasKey(m => new { m.IdOne, m.IdTwo });
Он использует анонимный тип. Но для жизни я не могу понять, как осуществить это в F#. Я попробовал Tuples, Records, даже обычный тип, который имеет свойства IdOne и 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
Ни один из этих подходов не работает, я получаю ArgumentNullException каждый раз. У меня нет идей...
РЕДАКТИРОВАТЬ С кодом Tomas, предоставленным ниже (который вызывает то же самое ArgumentNullException между прочим), я сделал некоторое слежение вокруг. Вот что я нашел:
Я использовал следующую функцию для анализа дерева выражений, которое строит C#:
static void Analyze<T>(Expression<Func<Model, T>> function)
{
}
// Call it like this:
Analyze(m => new { m.IdOne, m.IdTwo });
Затем я посмотрел на сгенерированную лямбду в отладчике. Вот что генерирует C#:
{m => new <>f__AnonymousType0'2(IdOne = m.IdOne, IdTwo = m.IdTwo)}
Делая то же самое на стороне F#, используя функцию getFuncTree из Tomas, используя <@ fun (m : Model) -> ModelKey(m.IdOne, m.IdTwo) @>
выходы:
{m => new ModelKey(m.IdOne, m.IdTwo)}
Как вы можете видеть, явное именование - что это такое в любом случае выглядит как свойства - аргументы отсутствуют в коде F#. Я вручную воссоздал все дерево выражений в F#, чтобы оно выглядело как версия C#:
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"
)
]
)
Часть, которая отсутствовала в представлении F#, является последовательностью членов. Когда я перемещаю мышь над этим выражением, появляется это представление:
{m => new ModelKey(IdOne = m.IdOne, IdTwo = m.IdTwo)}
Как видите, кроме класса, это выглядит так же. Но когда я пытаюсь использовать это выражение в HasKey
метод, я получаю следующее 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 }'.
Поэтому мне кажется, что этот синтаксис анонимного класса делает что-то особенное...
3 ответа
Похоже, проблема в два раза:
- Компилятор F# никогда не устанавливает
Members
коллекция LINQNewExpression
во время перевода цитаты, но это используется, чтобы отметить конструкцию анонимного типа, которую ожидает EF. - EF действительно требователен: даже в C# делает
m => new { A = m.IdOne, B = m.IdTwo }
не будет работать - имена свойств анонимного типа должны совпадать с именами свойств модели.
Один из способов обойти эту проблему (которая, вероятно, излишня, но работает) - динамически создавать новый тип во время выполнения, в котором есть поля с правильными именами, а затем просто использовать кортеж в коде F#:
open Quotations.Patterns
open Quotations.ExprShape
open System.Reflection
open System.Linq.Expressions
module AnonymousTypeFixer =
let private mb =
let ab = System.AppDomain.CurrentDomain.DefineDynamicAssembly(AssemblyName("codeGen"), Emit.AssemblyBuilderAccess.ReflectionOnly)
ab.DefineDynamicModule("codeGen")
let transform (Lambda(v, (NewTuple exprs)) : Quotations.Expr<'a -> 'b>) =
let newV = Expression.Variable(v.Type, v.Name)
let cvtExpr (PropertyGet(Some(Var v'), p, [])) =
assert (v = v')
Expression.Property(newV, p) :> Expression, p
let ty = mb.DefineType(v.Type.Name)
let ctor = ty.DefineConstructor(MethodAttributes.Public (*||| MethodAttributes.RTSpecialName ||| MethodAttributes.SpecialName*), CallingConventions.HasThis, exprs |> List.map (fun e -> e.Type) |> List.toArray)
ctor.GetILGenerator().Emit(Emit.OpCodes.Ret)
let fields =
[for (_, p) in exprs |> List.map cvtExpr ->
ty.DefineField(p.Name, p.PropertyType, FieldAttributes.Public) :> MemberInfo]
ty.CreateType()
let newE = Expression.New(ctor, exprs |> Seq.map (cvtExpr >> fst), fields)
Expression.Lambda<System.Func<'a, obj>>(newE, newV)
let mb = System.Data.Entity.DbModelBuilder()
mb.Entity<Model>().HasKey(AnonymousTypeFixer.transform <@ fun (m:Model) -> m.IdOne, m.IdTwo @>)
В F# 4.6 это сработало после долгих усилий.
По-видимому, все, что вам нужно, это кортеж. Имеет смысл, поскольку анонимный объект без явных имен членов по сути является кортежем.
Я не могу поверить, что никто не поместил это в документацию MS.
modelBuilder.Entity<Entity>()
.HasKey(fun e -> (e.Key1, e.Key2) :> obj)
|> ignore
РЕДАКТИРОВАТЬ: Техника ниже будет необходимо в F# 2.0, но это не должно быть необходимо в более новых версиях. Должна быть другая проблема с F#-генерированным деревом выражений...
Я думаю, что проблема в том, что Entity Framework хочет, чтобы вы указали лямбда-выражение в виде дерева выражений:
modelBuilder.Entity<Model>().HasKey(fun (m : Model) -> ModelKey (m.IdOne, m.IdTwo))
Это должно работать только в F# 3.1 в Visual Studio 2013, но это не поддерживается в F# 3.0. Вы все еще можете сделать это, но вам придется использовать цитаты F# и написать немного кода, который преобразует цитату в дерево выражений LINQ - есть помощник, который выполняет большую часть работы:
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 @>
Используя это, вы сможете позвонить:
modelBuilder.Entity<Model>().HasKey(getFuncTree <@ fun (m : Model) ->
ModelKey (m.IdOne, m.IdTwo) @>)
Вы можете определить метод расширения как HasKeyQuot
это делает это под прикрытием, чтобы сделать код немного лучше.
// F# 3.0
open Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter
// Regular type with properties IdOne & IdTwo.
type ModelKey (idOne, idTwo) =
member this.IdOne = idOne
member this.IdTwo = idTwo
modelBuilder.Entity<Model>()
.HasKey(QuotationToLambdaExpression(
<@ Func<_,_>(fun m -> NewAnonymousObjectHelper<_>(ModelKey(m.IdOne, m.IdTwo))) @>
)
)