将AutoMapper与F#一起使用

Joe*_*ler 9 f# automapper

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

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

ForMember(destMember: Expression<Func<'dest, obj>>, memberOpts: Action<IMemberConfigurationExpression<'source>>)
Run Code Online (Sandbox Code Playgroud)

这通常在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));
Run Code Online (Sandbox Code Playgroud)

我做了一个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
Run Code Online (Sandbox Code Playgroud)

这段代码编译,就语法和用法而言似乎相当干净.但是,在运行时AutoMapper告诉我这个:

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

我认为这是由于我必须转换Expr<'a -> 'b>成的事实Expression<Func<'a, obj>>.我用转换转换为'bto 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) 
Run Code Online (Sandbox Code Playgroud)

这是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())
Run Code Online (Sandbox Code Playgroud)

更新:

我能够使用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"
Run Code Online (Sandbox Code Playgroud)

我仍然需要PowerPack LINQ支持来实现我的mapMember功能,但它们现在都可以工作.

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

Ian*_*ths 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)
Run Code Online (Sandbox Code Playgroud)

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

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


Tom*_*cek 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 @> )
Run Code Online (Sandbox Code Playgroud)

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