9

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、ラムダ式が単なるプロパティ アクセスではなくなったことを意味します。元の見積もりでプロパティ値を囲み、内部でスプライシングをまったく行わないと、同じエラーが発生しますforMember(以下を参照)。ただし、プロパティ値をボックス化しないと、期待Expression<Func<'a, 'b>>されるパラメーターの型と一致しないものになってしまいます。ForMemberExpression<Func<'a, obj>>

AutoMapper が完全にジェネリックである場合、これは機能すると思いますForMemberが、メンバー アクセス式の戻り値の型を強制することは、F# で既に直接型であり、サブクラスではないobjプロパティに対してのみ使用できることを意味します。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現在はどちらも機能しています。

興味のある方は、ここで完全なコードを見つけることができます。

4

2 に答える 2

5

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)))
于 2016-09-22T08:05:54.123 に答える
4

生成された式ツリーを修正する方法がよくわかりません (後処理することで実行できますが、AutoMapper が期待するものを見つけるのは面倒です)。ただし、次の 2 つの方法があります。

最初のオプションとして、翻訳する必要がある式はかなり単純です。ほとんどの場合、メソッド呼び出し、プロパティ ゲッター、および変数の使用です。つまり、必要なコードを正確に生成する式ツリー トランスレータへの独自の見積もりを記述できるはずです (その後obj、おそらくExpression.Convert式ツリーを構築するために を呼び出して、独自の処理を追加することもできます)。サンプルとして簡単な引用トランスレーターを書きました。これは、サンプル内のほとんどのものを処理する必要があります。

2 番目のオプションとして、 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 を使用できます。マッピングごとにラムダ式を繰り返し記述する必要がないためです。

于 2012-05-18T20:56:11.497 に答える