将AutoMapper与F#一起使用

时间:2012-05-18 18:51:25

标签: f# automapper

我正在尝试使用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

    // 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来编写这个函数,它生成了一个表达式,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功能,但它们现在都可以使用。

如果有人有兴趣,他们可以找到full code here

2 个答案:

答案 0 :(得分:4)

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

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

作为第二个选项 - 如果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表达式,所以也许它更好一些: - )

答案 1 :(得分:4)

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