locked
How to convert C# Linq expression in F# RRS feed

  • Question

  • Hello:

    I want to write a program to find when one SQL Server data table gets new insert or delete or update.  I found SqlTableDependency seems to be a good package to use. 

    I followed the instruction on: https://tabledependency.codeplex.com/wikipage?title=SqlTableDependency

    Since the instruction was in C#, so I create a C# project and install this package with NuGet command:

    PM> Install-Package SqlTableDependency -Version 6.1.0

    First create a data table in SQL Server 2016:

    CREATE TABLE [dbo].[Client](

            [Id] [int] IDENTITY(1,1) NOT NULL,

            [First Name] [nvarchar](50) NOT NULL,

            [Second Name] [nvarchar](50) NOT NULL)

    The following is the C# code:

    using System;

    using TableDependency;

    using TableDependency.EventArgs;

    using TableDependency.Enums;

    using TableDependency.SqlClient;

    namespace sqlTableDep1

    {

        public class Customer

        {

            public int Id { get; set; }

            public string Name { get; set; }

            public string Surname { get; set; }

        }

        class Program

        {

            static void Main(string[] args)

            {

                var _con = "data source=.; initial catalog=myDB; integrated security=True";

                var mapper = new ModelToTableMapper<Customer>();

                mapper.AddMapping(c => c.Surname, "Second Name");

                mapper.AddMapping(c => c.Name, "First Name");

                using (var dep = new SqlTableDependency<Customer>(_con, "Client", mapper))

                {

                    dep.OnChanged += Changed;

                    dep.Start();

                    Console.WriteLine("Press a key to exit");

                    Console.ReadKey();

                    dep.Stop();

                }

            }

            static void Changed(object sender, RecordChangedEventArgs<Customer> e)

            {

                if (e.ChangeType != ChangeType.None)

                {

                    var changedEntity = e.Entity;

                    Console.WriteLine("DML operation: " + e.ChangeType);

                    Console.WriteLine("ID: " + changedEntity.Id);

                    Console.WriteLine("Name: " + changedEntity.Name);

                    Console.WriteLine("Surame: " + changedEntity.Surname);

                }

            }

        }

    }

    The code works, as when I insert a record, using this SQL statement:

    INSERT INTO Client ([First Name], [Second Name]) VALUES('John', 'Doe')

    I can see the C# program shows the result:

    Press a key to exit

    DML operation: Insert

    ID: 1

    Name: John

    Surame: Doe

    Now, I want to convert the above C# code to F#, but I can’t figure out how to rewrite the C# Linq expression: (Also install SqlTableDependency using: PM> Install-Package SqlTableDependency -Version 6.1.0)

    The following is my F# code, but it can’t get complied:

    #light

    open System

    open TableDependency

    open TableDependency.Enums

    open TableDependency.SqlClient

    let connDBString = "server=.; Integrated Security=True; Database=myDB"

    type Customer = { ID: int; name: string; surname: string }

    let mapper = new ModelToTableMapper<Customer>()

    mapper.AddMapping(fun cust->cust.surname, "Second Name")

    The compiler complains:

    Error FS0503 A member or object constructor 'AddMapping' taking 1 arguments is not accessible from this code location. All accessible versions of method 'AddMapping' take 2 arguments.

    For this statement:

    mapper.AddMapping(fun cust->cust.surname, "Second Name")

    This seems to be a tough one.  Any suggestion?

    It will be great, if you can rewrite the whole C# program in F#.

    Thanks,

    Sunday, June 25, 2017 8:27 PM

Answers

  • I'd been assuming that the function was of type

    ModelToTableMapper.AddMapping(mapping: Func<Customer,obj>, columnName: string)

    As it's actually after an Expression, rather than the compiled Func then I believe that the simpler form

    mapper.AddMapping((fun cust->cust.surname), "Second Name")

    may actually be what you need, as F# 3.0 or later should apply a type-directed conversion when you pass a lambda where an Expression is expected.

    Otherwise it's off to the depths of F# quotations, and hand-crafting the expression, with something like

    open System
    open System.Linq.Expressions
    open Microsoft.FSharp.Quotations
    open Microsoft.FSharp.Linq.RuntimeHelpers
    
    let toLinq (expr : Expr<'a -> 'b>) =
      let linq = LeafExpressionConverter.QuotationToExpression expr
      let call = linq :?> MethodCallExpression
      let lambda = call.Arguments.[0] :?> LambdaExpression
      Expression.Lambda<Func<'a, 'b>>(lambda.Body, lambda.Parameters)
    
    let expression = toLinq <@ fun (cust : Customer) -> cust.surname @>
    mapper.AddMapping(expression, "Second Name")
    Again, caveat emptor -- these are based off theory (i.e. reading the friendly manual), rather than actual experiment.

    • Marked as answer by zydjohn Monday, August 7, 2017 8:05 PM
    Wednesday, June 28, 2017 6:26 AM

All replies

  • In the expression

     mapper.AddMapping(fun cust->cust.surname, "Second Name")

    the text

    (fun cust->cust.surname, "Second Name")

    is parsed as an anonymous function taking a Customer and returning a 2-tuple.  That's why it talks about overloads of one argument.

    However, writing

     mapper.AddMapping((fun cust->cust.surname), "Second Name")

    won't fix it, because an F# anonymous function is not the same type as a C# delegate.  You need to declare a wrapping type like

    type Mapping1 = delegate of Customer -> string
    mapper.AddMapping(new Mapping1(fun cust->cust.surname), "Second Name")

    Warning : there may be some rough edges in the above as this is done w/o compiling anything, but the core principle about what is parsed, and the need to convert F# function types to delegates for consumption by C# is what's important. The friendly manual at MSDN should be able to take you from there.

    It will be great, if you can rewrite the whole C# program in F#.

    There's a limit to how much of your work I'll do for free :)

    Monday, June 26, 2017 7:30 AM
  • Hello:
    Follow your advice, I have tried the following code:

    #light
    open System
    open TableDependency
    open TableDependency.Enums
    open TableDependency.SqlClient
    let connDBString = "server=.; Integrated Security=True; Database=myDB"
    type Customer = { ID: int; name: string; surname: string }
    type Mapping1 = delegate of Customer -> string
    let mapper = new ModelToTableMapper<Customer>()
    mapper.AddMapping(new Mapping1(fun cust->cust.surname), "Second Name")

    I got 3 complier errors:
    Error Possible overload: 'ModelToTableMapper.AddMapping(expression: Linq.Expressions.Expression<Func<Customer,obj>>, columnName: string) : ModelToTableMapper<Customer>'. Type constraint mismatch. The type Mapping1 is not compatible with type Linq.Expressions.Expression<Func<Customer,obj>>    
    The type 'Mapping1' is not compatible with the type 'Linq.Expressions.Expression<Func<Customer,obj>>'.
    Error Possible overload: 'ModelToTableMapper.AddMapping(pi: Reflection.PropertyInfo, columnName: string) : unit'. Type constraint mismatch. The type  Mapping1 is not compatible with type Reflection.PropertyInfo    
    The type 'Mapping1' is not compatible with the type 'Reflection.PropertyInfo'.
    Error No overloads match for method 'AddMapping'. The available overloads are shown below (or in the Error List window).

    It seems either I don't understand your meaining correctly, or your trick didn't work.
    But I have another crazy idea, the author for this software package has his email published, so I can reach him by email, I am thinking about asking him to change his software package, so we can use F# to pass the Linq expression as we can.  But I don't know what I should ask him, I believe he is very good in C#, but don't know if he knows any F# or not.
    Any suggestion?
    Thanks,
    Tuesday, June 27, 2017 4:08 PM
  • I'd been assuming that the function was of type

    ModelToTableMapper.AddMapping(mapping: Func<Customer,obj>, columnName: string)

    As it's actually after an Expression, rather than the compiled Func then I believe that the simpler form

    mapper.AddMapping((fun cust->cust.surname), "Second Name")

    may actually be what you need, as F# 3.0 or later should apply a type-directed conversion when you pass a lambda where an Expression is expected.

    Otherwise it's off to the depths of F# quotations, and hand-crafting the expression, with something like

    open System
    open System.Linq.Expressions
    open Microsoft.FSharp.Quotations
    open Microsoft.FSharp.Linq.RuntimeHelpers
    
    let toLinq (expr : Expr<'a -> 'b>) =
      let linq = LeafExpressionConverter.QuotationToExpression expr
      let call = linq :?> MethodCallExpression
      let lambda = call.Arguments.[0] :?> LambdaExpression
      Expression.Lambda<Func<'a, 'b>>(lambda.Body, lambda.Parameters)
    
    let expression = toLinq <@ fun (cust : Customer) -> cust.surname @>
    mapper.AddMapping(expression, "Second Name")
    Again, caveat emptor -- these are based off theory (i.e. reading the friendly manual), rather than actual experiment.

    • Marked as answer by zydjohn Monday, August 7, 2017 8:05 PM
    Wednesday, June 28, 2017 6:26 AM