使用AutoMapper和F#

10

我正在尝试从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>>所导致的。我通过强制转换将'b转换为obj,这意味着我的lambda表达式不再仅仅是属性访问。如果我直接在原始引号中装箱属性值,并且在forMember内部不进行任何插入操作(见下文),我会得到相同的错误。然而,如果我不装箱属性值,则最终会得到Expression<Func<'a, 'b>>,它与ForMember期望的参数类型Expression<Func<'a, obj>>不匹配。

我认为如果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

    // https://dev59.com/XmPVa4cB1Zd3GeqP-vrW
    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的示例代码编写此函数,该函数生成一个表达式,AutoMapper对于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函数,但它们现在都可以工作。

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


1
你现在不再需要F# PowerPack来使用.ToLinqExpression(),因为它已经作为Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpress‌​ion集成在F#中了。 - Maslow
2个回答

7

现在,F#可以直接从fun表达式生成Expression<Func<...>>,因此这个问题相对容易解决。现在最大的问题是,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)))

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表达式,所以可能会更好一些 :-)


谢谢Tomas。这给了我一些东西可以玩耍。我真的很喜欢PropertyGet表达式。如果我能将它们翻译成AutoMapper接受的内容,那么这将比C#更好的语法。 - Joel Mueller
哦,而在这种情况下mapMember的第二个参数将是一个表达式,它接受PostSummary实例并返回要分配给Post.Slug的值,例如通过转换PostSummary.TitleCreateMap方法不需要任何参数。 - Joel Mueller
我确实能够使用你的示例来创建一个使用Expression.Convert的函数,以便与AutoMapper兼容!已更新问题并附上了该函数。 - Joel Mueller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接