【问题标题】:Defining Entity Framework Keys using Fluent API使用 Fluent API 定义实体框架键
【发布时间】:2013-08-22 13:21:30
【问题描述】:

我正在尝试为具有两个键属性的模型类型定义一个键,其定义如下:

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# 中实现这一点。我尝试了元组、记录,甚至是具有属性 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 });

然后我在调试器中查看了生成的 lambda。这是 C# 生成的:

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

在 F# 端使用来自 Tomas 的 getFuncTree 函数使用 &lt;@ fun (m : Model) -&gt; ModelKey(m.IdOne, m.IdTwo) @&gt; 做同样的事情会产生:

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

所以在我看来,这种匿名类语法做了一些特别的事情......

【问题讨论】:

  • 您使用的是 F# 3.1(在 Visual Studio 2013 预览版中)吗?我想这使用表达式树,仅在 VS 2013 中完全支持...
  • 嗨 Tomas,我正在使用 VS2012 Update 3 和 F# 3.0... 我无法切换。那么最简单的解决方案可能是一个额外的 C# 库来配置实体?
  • @Tomas - 是什么让你这么说? F# 3 中已经有很好的表达式树支持;我怀疑真正的问题是树没有 EF 期望的形式。
  • @kvb 我只是指新功能,当您将fun x -&gt; ... 传递给需要Expression&lt;Func&lt;..&gt;&gt; 的方法时,F# 会自动将其转换为表达式树。我相信这是 F# 3.1 中的新功能。如果我是正确的,那么在 F# 3.0 中,您需要明确地使用引号并进行一些预处理(就像我在回答中所做的那样......)。不过你是对的,这可能仍然会产生 EF 不期望的树。
  • @Tomas - 该功能绝对适用于 F# 3.0。在 F# 3.1 中,有一些更改可以改善某些类似 LINQ 的场景中的重载分辨率,并且可能修复了特定结构的翻译的一些错误,但据我所知没有重大工作。

标签: entity-framework f# ef-code-first entity-framework-5 f#-3.0


【解决方案1】:

在 F# 4.6 中,经过一番努力,这对我有用。

显然你只需要一个元组。这是有道理的,因为没有显式成员名称的匿名对象基本上是一个元组。

我不敢相信没有人将它放在 MS 文档中。

    modelBuilder.Entity<Entity>()
      .HasKey(fun e -> (e.Key1, e.Key2) :> obj)
      |> ignore

【讨论】:

    【解决方案2】:

    编辑: 在 F# 2.0 中将需要以下技术,但在较新的版本中不需要它。 F# 生成的表达式树肯定有其他问题...

    我认为问题在于实体框架希望您将 lambda 表达式指定为表达式树:

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

    这应该只适用于 Visual Studio 2013 的 F# 3.1,但在 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这样的扩展方法,在后面做这个,让代码更好一点。

    【讨论】:

    • 您,先生,是个真正的巫师!
    • 如果有人用 EF Core 遇到这个问题,Expression.Lambda>(lam.Body, lam.Parameters) 应该改为 Expression.Lambda>(lam.Body, lam.Parameters)
    【解决方案3】:

    看起来问题是双重的:

    1. F# 的编译器在引用翻译期间从不设置 LINQ NewExpressionMembers 集合,但这用于标记 EF 期望的匿名类型的构造。
    2. EF 真的很挑剔:即使在 C# 中,m =&gt; 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 @>)
    

    【讨论】:

    • +1 显然,SO 不允许我对自己的帖子投反对票(我认为它可能对 F# 2.0 仍然有用,所以没有删除它。)
    【解决方案4】:
    // 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))) @>
                                           )
                )
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-04-09
      • 2011-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多