我正在尝试从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
函数,但它们现在都可以工作。
如果有人感兴趣,他们可以在这里找到完整的代码。
F# PowerPack
来使用.ToLinqExpression()
,因为它已经作为Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpression
集成在F#中了。 - Maslow