将AutoMapper与F#一起使用 [英] Using AutoMapper with F#

查看:50
本文介绍了将AutoMapper与F#一起使用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用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的示例代码编写此函数,该函数产生一个表达式,表示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功能,但是它们现在都可以工作.

如果有人感兴趣,可以在此处找到完整代码.

解决方案

我不太确定如何修复所生成的表达式树(可以通过对其进行后处理来实现,但是要弄清楚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表达式,所以也许会更好一些:-)

I'm trying to use AutoMapper from F#, but I'm having trouble setting it up due to AutoMapper's heavy use of LINQ Expressions.

Specifically, the AutoMapper type IMappingExpression<'source, 'dest> has a method with this signature:

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

This is typically used in C# like this:

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));

I made an F# wrapper that arranges things so that type inference can work. This wrapper allows me to translate the C# example above into something like this:

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

This code compiles, and it seems pretty clean as far as syntax and usage. However, at runtime AutoMapper tells me this:

AutoMapper.AutoMapperConfigurationException: Custom configuration for members is only supported for top-level individual members on a type.

I presume this is caused by the fact that I have to convert Expr<'a -> 'b> into Expression<Func<'a, obj>>. I convert the 'b to obj with a cast, which means my lambda expression is no longer simply a property access. I get the same error if I box the property value in the original quotation, and don't do any splicing at all inside forMember (see below). However, if I don't box the property value, I end up with Expression<Func<'a, 'b>> which does not match the parameter type that ForMember expects, Expression<Func<'a, obj>>.

I think that this would work if AutoMapper's ForMember was completely generic, but forcing the return type of the member access expression to be obj means that I can only use it in F# for properties that are already directly of type obj and not a subclass. I can always resort to using the overload of ForMember that takes the member name as a string, but I thought I'd check to see if anyone has a brilliant work-around before I give up on compile-time typo-checking.

I'm using this code (plus the LINQ part of F# PowerPack) to convert an F# quotation into a LINQ expression:

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) 

This is the actual F# wrapper for AutoMapper:

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())

Update:

I was able to use Tomas's sample code to write this function, which produces an expression that AutoMapper is satisfied with for the first argument to 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"

I still need the PowerPack LINQ support to implement my mapMember function, but they both work now.

If anyone is interested, they can find the full code here.

解决方案

I'm not quite sure how to fix the generated expression tree (that's doable by post-processing it, but it is pain to find out what AutoMapper expects). However, there are two alternatives:

As a first option - the expressions that you need to translate are fairly simple. They are mostly just method calls, property getters and uses of a variable. This means that it should be possible to write your own quotation to expression trees translator that produces exactly the code you want (then you can also add your own handling of obj, perhaps by calling Expression.Convert to build the expression tree). I wrote a simple quotation tranlsator as a sample, which should handle most of the stuff in your sample.

As a second option - if AutoMapper provides an option to specify just a property name - you could just use quotations of the form <@ x.FooBar @>. These should be quite easy to deconstruct using the Patterns.PropertyGet pattern. The API should maybe look like this:

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

Or, in fact, you could use this style of API even in the first case, because you don't need to write lambda expressions repeatedly for every single mapping, so maybe it is a bit nicer :-)

这篇关于将AutoMapper与F#一起使用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆