【发布时间】:2012-05-18 18:51:25
【问题描述】:
我正在尝试使用 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 编写这个函数,它为IMappingExpression.ForMember 的第一个参数生成一个AutoMapper 满意的表达式。
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。
【问题讨论】:
-
您不再需要
F# PowerPack来代替.ToLinqExpression()它现在在 F# 中为Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpression
标签: f# automapper