9

我正在尝试使用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<'a -> 'b>Expression<Func<'a, obj>>. 我使用强制转换将其转换为'bobj这意味着我的 lambda 表达式不再是简单的属性访问。如果我在原始报价中将属性值装箱,并且根本不做任何拼接forMember(见下文),我会得到同样的错误。但是,如果我不将属性值装箱,我最终会得到与预期Expression<Func<'a, 'b>>的参数类型不匹配的.ForMemberExpression<Func<'a, obj>>

我认为如果 AutoMapperForMember是完全通用的,这将起作用,但是强制成员访问表达式的返回类型是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 的示例代码来编写这个函数,它会为IMappingExpression.ForMember.

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功能,但它们现在都可以工作。

如果有人感兴趣,他们可以在这里找到完整的代码

4

2 回答 2

5

现在 F# 很乐意Expression<Func<...>>直接从fun表达式生成 a,这相对容易解决。现在最大的问题是 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)))
于 2016-09-22T08:05:54.123 回答
4

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

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

作为第二个选项- 如果 AutoMapper 提供了一个选项来指定一个属性名称 - 你可以只使用表单的引号<@ x.FooBar @>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 表达式,所以它可能会更好一些 :-)

于 2012-05-18T20:56:11.497 回答