Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using AutoMapper with F#

Tags:

f#

automapper

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.

like image 620
Joel Mueller Avatar asked May 18 '12 18:05

Joel Mueller


2 Answers

Now that F# is happy to generate a Expression<Func<...>> directly from a fun expression, this is relatively easy to solve. The biggest problem now is that the F# compiler seems to get confused by the overloading of the ForMember method, and is unable to infer correctly what you want. This can be circumvented by defining an extension method with a different name:

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)

You can then use the ForMemberFs method more or less as the original ForMember is intended to work, e.g.:

this.CreateMap<Post, Posts.PostSummary>()
    .ForMemberFs
        ((fun d -> d.Slug),
         (fun opts -> opts.MapFrom(fun m -> SlugConverter.TitleToSlug(m.Title)))
like image 173
Ian Griffiths Avatar answered Oct 27 '22 02:10

Ian Griffiths


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

like image 36
Tomas Petricek Avatar answered Oct 27 '22 02:10

Tomas Petricek