Search code examples
reflectionf#

How do I get 'T from seq<'T> from a PropertyInfo array


I'm using reflection to try and create a datatable based on a record type. This works fine for a simple record type, but some of my records have sub records (also fine, just made the function recursive), or lists of sub records which is where I have my problem.

As an example, if i have the following types-

type ExampleSubType =
    {   PropertyA: string
        PropertyB: int }

type ExampleType =
    {   ID: int
        Name: string
        Example1: ExampleSubType list }

I'd like to create a data table with the following columns/types in this order-

ID: int
Name: string
PropertyA: string
PropertyB: int

I can already do this if I don't have a seq/arr/list anywhere in the types, but the ExampleSubType list part is throwing me off.

Here's the code I've got so far-


    let rec private createDataTableColumns record (propertyArr: PropertyInfo []) (dataTable: DataTable)  =
        propertyArr
        |> Seq.iteri(fun i property -> 
            let propType = property.GetType()
            let propValue = propertyArr.[i].GetValue(record)

            match FSharpType.IsRecord(propType), propValue with 
            | true, _ ->
                let subRecordType = propValue.GetType()
                let subPropertyArr = FSharpType.GetRecordFields (subRecordType)
                createDataTableColumns propValue subPropertyArr dataTable

            | false, :? seq<'T> as (_,test) -> 
                let subRecordType = test.GetType()
                let subPropertyArr = FSharpType.GetRecordFields (subRecordType)
                createDataTableColumns propValue subPropertyArr dataTable

            | _, _ ->
                dataTable.Columns.Add(property.Name, property.PropertyType) |> ignore
            )

The second match case is the issue. I can get it to match on that correctly but I'd like subRecordType to be typeof<'T>.

As it currently is, test.GetType() will return seq<'T>, not 'T and I don't know how to extract it from there.

I've also tried let subRecordType = typeof<'T>, which works elsewhere in my code, but here it will just return System.Object, not whatever type 'T is and i'm not sure why.


Solution

  • The problem here is that pattern matching cannot match the shape of a generic type like seq<_> and fill in the type argument 'T for you at runtime. The type argument has to be decided statically during compilation and so (if there are no other constraints), it ends up being compiled as obj and, consequently, your code is matching against seq<obj>.

    The easiest thing is probably to match against non-generic System.Collections.IEnumerable, which lets you iterate over the elements (which you then get as obj values):

    let matchSeq sq = 
      match box sq with
      | :? System.Collections.IEnumerable as e ->
          for v in e do printfn "%A" v
      | _ -> printfn "Not a collection"
    

    If you have a sequence sq, you can also use reflection to find the type of the generic argument to the seq<'T> that it implements and get that as System.Type, which should let you find out about the structure of the record:

    let typ = s.GetType().GetInterface("System.Collections.Generic.IEnumerable`1")
      .GetGenericArguments().[0]
    

    Finally, a more fancy reflection-based trick is to find the type dynamically and use it to invoke your own generic method. That way, you will be able to write code against a concrete implementation of seq<'T>:

    type SeqInvoker = 
      static member Invoke<'T>(items:seq<'T>) = 
        printfn "Type: %s" (typeof<'T>.Name)
    
    let matchSeq s = 
      match box s with
      | :? System.Collections.IEnumerable as e ->
          for v in e do 
            printfn "%A" v
          let typ = s.GetType().GetInterface("System.Collections.Generic.IEnumerable`1")
             .GetGenericArguments().[0]
          typeof<SeqInvoker>.GetMethod("Invoke")
             .MakeGenericMethod(typ).Invoke(null, [| s |])
      | _ -> ()
    
    matchSeq [1;2;3]    // Calls: SeqInvoker.Invoke<int>
    matchSeq ["a"; "b"] // Calls: SeqInvoker.Invoke<string>