【问题标题】:Using AutoMapper with F#将 AutoMapper 与 F# 一起使用
【发布时间】:2012-05-18 18:51:25
【问题描述】:

我正在尝试使用 F# 中的 AutoMapper,但由于 AutoMapper 大量使用 LINQ 表达式,我无法设置它。

具体来说,AutoMapper 类型 IMappingExpression<'source, 'dest> 有一个带有这个签名的方法:

ForMember(destMember: Expression<Func<'dest, obj>>, memberOpts: Action<IMemberConfigurationExpression<'source>>)

这通常在 C# 中这样使用:

Mapper.CreateMap<Post, PostsViewModel.PostSummary>()
    .ForMember(x => x.Slug, o => o.MapFrom(m => SlugConverter.TitleToSlug(m.Title)))
    .ForMember(x => x.Author, o => o.Ignore())
    .ForMember(x => x.PublishedAt, o => o.MapFrom(m => m.PublishAt));

我制作了一个 F# 包装器,它可以安排事物以便类型推断可以工作。这个包装器允许我将上面的 C# 示例翻译成这样的:

Mapper.CreateMap<Post, Posts.PostSummary>()
|> mapMember <@ fun x -> x.Slug @> <@ fun m -> SlugConverter.TitleToSlug(m.Title) @>
|> ignoreMember <@ fun x -> x.Author @>
|> mapMember <@ fun x -> x.PublishedAt @> <@ fun m -> m.PublishAt @>
|> ignore

这段代码可以编译,就语法和用法而言,它看起来很干净。但是,在运行时 AutoMapper 会告诉我:

AutoMapper.AutoMapperConfigurationException:成员的自定义配置仅支持类型上的顶级单个成员。

我认为这是因为我必须将Expr&lt;'a -&gt; 'b&gt; 转换为Expression&lt;Func&lt;'a, obj&gt;&gt;。我使用强制转换将 'b 转换为 obj,这意味着我的 lambda 表达式不再是简单的属性访问。如果我在原始报价中将属性值装箱,并且在 forMember 内根本不做任何拼接,我会得到同样的错误(见下文)。但是,如果我不将属性值装箱,我最终会得到Expression&lt;Func&lt;'a, 'b&gt;&gt;,它与ForMember 期望的参数类型Expression&lt;Func&lt;'a, obj&gt;&gt; 不匹配。

我认为如果 AutoMapper 的 ForMember 是完全通用的,这将起作用,但强制成员访问表达式的返回类型为 obj 意味着我只能在 F# 中将它用于已经直接类型的属性obj 而不是子类。我总是可以使用ForMember 的重载,它将成员名称作为字符串,但我想在我放弃编译时错字检查之前,我想看看是否有人有出色的解决方法。

我正在使用这段代码(加上 F# PowerPack 的 LINQ 部分)将 F# 引用转换为 LINQ 表达式:

namespace Microsoft.FSharp.Quotations

module Expr =
    open System
    open System.Linq.Expressions
    open Microsoft.FSharp.Linq.QuotationEvaluation

    // http://stackoverflow.com/questions/10647198/how-to-convert-expra-b-to-expressionfunca-obj
    let ToFuncExpression (expr:Expr<'a -> 'b>) =
        let call = expr.ToLinqExpression() :?> MethodCallExpression
        let lambda = call.Arguments.[0] :?> LambdaExpression
        Expression.Lambda<Func<'a, 'b>>(lambda.Body, lambda.Parameters) 

这是 AutoMapper 的实际 F# 包装器:

namespace AutoMapper

/// Functions for working with AutoMapper using F# quotations,
/// in a manner that is compatible with F# type-inference.
module AutoMap =
    open System
    open Microsoft.FSharp.Quotations

    let forMember (destMember: Expr<'dest -> 'mbr>) (memberOpts: IMemberConfigurationExpression<'source> -> unit) (map: IMappingExpression<'source, 'dest>) =
        map.ForMember(Expr.ToFuncExpression <@ fun dest -> ((%destMember) dest) :> obj @>, memberOpts)

    let mapMember destMember (sourceMap:Expr<'source -> 'mapped>) =
        forMember destMember (fun o -> o.MapFrom(Expr.ToFuncExpression sourceMap))

    let ignoreMember destMember =
        forMember destMember (fun o -> o.Ignore())

更新:

我能够使用Tomas's sample code 编写这个函数,它为IMappingExpression.ForMember 的第一个参数生成一个AutoMapper 满意的表达式。

let toAutoMapperGet (expr:Expr<'a -> 'b>) =
    match expr with
    | Patterns.Lambda(v, body) ->
        // Build LINQ style lambda expression
        let bodyExpr = Expression.Convert(translateSimpleExpr body, typeof<obj>)
        let paramExpr = Expression.Parameter(v.Type, v.Name)
        Expression.Lambda<Func<'a, obj>>(bodyExpr, paramExpr)
    | _ -> failwith "not supported"

我仍然需要 PowerPack LINQ 支持来实现我的 mapMember 函数,但它们现在都可以工作了。

有兴趣的可以找full code here。

【问题讨论】:

  • 您不再需要 F# PowerPack 来代替 .ToLinqExpression() 它现在在 F# 中为 Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpress‌​ion

标签: f# automapper


【解决方案1】:

现在 F# 很乐意直接从 fun 表达式生成 Expression&lt;Func&lt;...&gt;&gt;,这相对容易解决。现在最大的问题是 F# 编译器似乎被 ForMember 方法的重载弄糊涂了,无法正确推断出你想要什么。这可以通过定义一个不同名称的扩展方法来规避:

type AutoMapper.IMappingExpression<'TSource, 'TDestination> with
    // The overloads in AutoMapper's ForMember method seem to confuse
    // F#'s type inference, forcing you to supply explicit type annotations
    // for pretty much everything to get it to compile. By simply supplying
    // a different name, 
    member this.ForMemberFs<'TMember>
            (destGetter:Expression<Func<'TDestination, 'TMember>>,
             sourceGetter:Action<IMemberConfigurationExpression<'TSource, 'TDestination, 'TMember>>) =
        this.ForMember(destGetter, sourceGetter)

然后您可以或多或少地使用 ForMemberFs 方法,因为原始 ForMember 旨在工作,例如:

this.CreateMap<Post, Posts.PostSummary>()
    .ForMemberFs
        ((fun d -> d.Slug),
         (fun opts -> opts.MapFrom(fun m -> SlugConverter.TitleToSlug(m.Title)))

【讨论】:

    【解决方案2】:

    我不太确定如何修复生成的表达式树(这可以通过对其进行后处理来实现,但找出 AutoMapper 的期望是很痛苦的)。但是,有两种选择:

    作为第一个选项 - 您需要翻译的表达式相当简单。它们大多只是方法调用、属性获取器和变量的使用。这意味着应该可以将您自己的引用写入表达式树翻译器,以准确生成您想要的代码(然后您也可以添加您自己的obj 处理,也许通过调用Expression.Convert 来构建表达式树)。我写了一个simple quotation tranlsator as a sample,它应该可以处理您示例中的大部分内容。

    作为第二个选项 - 如果 AutoMapper 提供了一个选项来指定一个属性名称 - 您可以只使用 &lt;@ x.FooBar @&gt; 形式的引号。使用Patterns.PropertyGet 模式应该很容易解构这些。 API 应该如下所示:

    Mapper.CreateMap<Post, Posts.PostSummary>(fun post summary mapper ->
      mapper |> mapMember <@ post.Slug @> // not sure what the second argument should be?
             |> ignoreMember <@ post.Author @> )
    

    或者,事实上,即使在第一种情况下,您也可以使用这种风格的 API,因为您不需要为每个映射重复编写 lambda 表达式,所以也许它会更好一点:-)

    【讨论】:

    • 谢谢托马斯。这给了我一些可以玩的东西。我真的很喜欢 PropertyGet 表达式。如果我能把这些翻译成 AutoMapper 可以接受的东西,那将是比 C# 更好的语法。
    • 哦,在这种情况下,mapMember 的第二个参数将是一个表达式,它采用 PostSummary 的实例并返回要分配给 Post.Slug 的值,例如通过转换 @ 987654330@。 CreateMap 方法不接受任何参数。
    • 我确实能够使用您的示例创建一个函数,该函数使用 Expression.Convert 创建 AutoMapper 满意的东西!用函数更新了问题。
    猜你喜欢
    • 2019-10-07
    • 1970-01-01
    • 2019-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多