user3099164 user3099164 - 27 days ago 7
C# Question

pass expressions and lambdas parameter to C# extension method in F#

I am struggling with this one as I am just a beginner in F# and at present I am in the process of learning F# by creating unit tests for C# code in F# as suggested on F# for fun and for profit.

The problem.

I have the following C# class

public class ObjectMapper<TSource, TTarget>
where TTarget : class, new() {

public readonly TSource Source;
public readonly TTarget Target;

public ObjectMapper(TSource source) {

this.Source = source;
this.Target = new TTarget();
}

public ObjectMapper<TSource, TTarget> Populate<T>(
Expression<Func<TTarget, T>> targetAccessor,
Func<TSource, T> sourceValue) {

var targetPropertyInfo = targetAccessor.ToPropertyInfo();
targetPropertyInfo.SetValue(this.Target, sourceValue(this.Source));

return this;
}
}


and I started out with trying to write a simple unit test in F#

module CompanyName.Utils.Test.ObjectMapper
open System
open Xunit
open Swensen.Unquote
open Microsoft.FSharp.Linq.RuntimeHelpers // do I need this?

open CompanyName.Utils // the namespace of the C# class above

type Source(name: string, code: int, date: DateTime) =
member thi.Name = name
member this.Code = code
member this.Date = date

type Target() =
member this.Id: string = System.String.Empty
member this.Name: string = System.String.Empty
member this.Code: int = 0
member this.Date: DateTime = System.DateTime.MinValue

[<Fact>]
let ``ObjectMapper.Populate maps property values from source to target``() =

// arrange
let name = @"name"
let code = 123
let date = new System.DateTime(1980,2,15)
let source = Source(name, code, date)
// let target = Target()
let mapper = ObjectMapper<Source, Target>(source)

// act // this is how I would like it to be!
mapper.Populate(t => t.Name, s => s.Name)
mapper.Populate(t => t.Code, s => s.Code)
mapper.Populate(t => t.Date, s => s.Date)
mapper.Populate(t => t.Id, s => s.Date+s.Code+s.Name)

// assert
test<@ mapper.Target.Name = source.Name @>
Assert.Equal(mapper.Target.Name, source.Name)

test<@ mapper.Target.Code = source.Code @>
Assert.Equal(mapper.Target.Code, source.Code)

test<@ mapper.Target.Date = source.Date @>
Assert.Equal(mapper.Target.Date, source.Date)

test<@ mapper.Target.Id = @"1980-2-15123name" @>
Assert.Equal(mapper.Target.Id, @"1980-2-15123name")

Assert.True(false)


My problem is with the assert part of the test where ideally I would have liked to be able to just pass the lambdas the way I have used in the code above. Unfortunately, that does not work at all and I tried to refer to other post on stack overflow where this and related topics are discussed. In general the level of these posts is a bit too high for me and although I understand the general concepts I fail to really grasp the necessary details to translate the meta-code above in something that makes any sense in F# and above all compiles and runs.

I would be very grateful if you could help me with


  1. The assert part of the test above by showing how it should be done and why the metacode cannot work.

  2. Trying to understand how lambdas and expressions work between F# and C# in both directions either by giving examples suitable for a 5-year-old.

  3. Any resources that I could use to gain a better understanding of the subject
    possibly ordered in a way that they ramp up in difficulty.



Please, feel free to make any comments to my code that you see fit to help me to become a better developer in F# as well as in C#, I do not mind at all. I am aware that the code above is very simple and some parts are redundant and inefficient. I thought to keep it basic in order not to cloud the central problem.

The assertions are repeated in Unquote and MSTest styles for those who may be familiar with one style but not the other.

Thank you very much for you help.

Below you find the working code thanks to Fyodor Soiki explanation

module CompanyName.FSharp.UtilsFunctions

// Efficient concatenation of objects into a string with string builder
// https://stackoverflow.com/questions/18595597/is-using-a-stringbuilder-a-
right-thing-to-do-in-f
let strconc =
fun (data) ->
let sb = new System.Text.StringBuilder()
for o in data do sb.Append(o.ToString()) |> ignore
sb.ToString()

// examples
strconc ["one"; "two"] |> printfn "%s"
strconc [1;2;3] |> printfn "%s"
strconc (["one"; "two"; 3 ]: list<obj>) |> printfn "%s"

module LogXtreme.Utils.Test.ObjectMapper

open System
open Xunit
open Swensen.Unquote

open CompanyName.Utils
open CompanyName.FSharp.UtilsFunctions


type Source(name: string, code: int, date: DateTime) =
member thi.Name = name
member this.Code = code
member this.Date = date

type Target() =
member val Id: string = System.String.Empty with get, set
member val Name: string = System.String.Empty with get, set
member val Code: int = 0 with get, set
member val Date: DateTime = System.DateTime.MinValue with get, set

[<Fact>]
let ``ObjectMapper.Populate maps property values from source to target``() =

// arrange
let name = @"name"
let code = 123
let date = new System.DateTime(1980,2,15)
let source = Source(name, code, date)
let mapper = ObjectMapper<Source, Target>(source)
let expectedId = strconc ([source.Name; source.Code; source.Date]: list<obj>)

// act
mapper.Populate((fun t -> t.Name), fun s -> s.Name) |> ignore
mapper.Populate((fun t -> t.Code), fun s -> s.Code) |> ignore
mapper.Populate((fun t -> t.Date), fun s -> s.Date) |> ignore
mapper.Populate((fun t -> t.Id), fun s -> strconc ([s.Name; s.Code; s.Date]: list<obj>)) |> ignore

// assert
test<@ mapper.Target.Name = source.Name @>
test<@ mapper.Target.Code = source.Code @>
test<@ mapper.Target.Date = source.Date @>
test<@ mapper.Target.Id = expectedId @>


The test now passes.

Answer Source

In general, F# tends to avoid magical conversions, because they usually cause subtle, hard to find bugs. However, in a few special circumstances, mostly to support C# interop scenarios, it will compromise.

In particular, when calling an object method that expects an Expression<_>, the compiler will convert F# lambda expressions to C# quotations (the same way C# compiler does). This means that you can just pass plain F# lambdas to your methods:

mapper.Populate( (fun t -> t.Name), fun s -> s.Name )

The parentheses around the first lambda are necessary. Without them, the whole thing around be interpreted as fun t -> ( t.Name, fun s -> s.Name )