Search code examples
f#filehelpers

How do you use FileHelpers.engine with F# option types using generic converters?


This answer shows a nice way to use CsvHelper with generic F# option types. How do you do the same thing with FileHelpers?

I cannot figure out how to make the generic OptionConverter work. Consider the following test.csv:

X,Y,Z
hi,1,hi
bye,,

The following works:

#r "nuget: FileHelpers"

open System
open FileHelpers

Environment.CurrentDirectory <- __SOURCE_DIRECTORY__

type OptionFloatConverter() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<float> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> s |> float  |> Some :> obj

type OptionStringConverter() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<string> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> s |> Some :> obj



[<CLIMutable>]
[<DelimitedRecord(",")>]
[<IgnoreFirst(1)>]
type TestExplicit =
    { X : string;
      [<FieldConverter(typeof<OptionFloatConverter>)>]
      Y : float Option
      [<FieldConverter(typeof<OptionStringConverter>)>]
      Z : string Option
      }

let generic = FileHelperEngine<TestGeneric>()

explicit.ReadFile "test.csv"

(*
val it : TestExplicit [] = [|{ X = "hi"
                               Y = Some 1.0
                               Z = Some "hi" }; { X = "bye"
                                                  Y = None
                                                  Z = None }|]
*)

How do I make this generic version work?

type OptionConverter<'T>() =
    inherit ConverterBase()
    override this.FieldToString(x) = 
        match x :?> Option<'T> with
        | None -> ""
        | Some x -> base.FieldToString(x) 
    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s -> Convert.ChangeType(s,typeof<'T>) |> Some :> obj

[<CLIMutable>]
[<DelimitedRecord(",")>]
[<IgnoreFirst(1)>]
type TestGeneric =
    { X : string;
      [<FieldConverter(typeof<OptionConverter<float>>)>]
      Y : float Option
      [<FieldConverter(typeof<OptionConverter<string>>)>]
      Z : string Option
      }
let explicit = FileHelperEngine<TestExplicit>()
generic.ReadFile "test.csv"

(*
> generic.ReadFile  "test.csv";;
FileHelpers.ConvertException: Line: 2. Field: Y@. The converter for the field: Y@ returns an object of Type: FSharpOption`1  and the field is of type: FSharpOption`1
 ---> System.InvalidCastException: Unable to cast object of type 'Microsoft.FSharp.Core.FSharpOption`1[System.Object]' to type 'Microsoft.FSharp.Core.FSharpOption`1[System.Double]'.
   at lambda_method4(Closure , Object , Object[] )
   at FileHelpers.RecordOperations.StringToRecord(Object record, LineInfo line, Object[] values)
   --- End of inner exception stack trace ---
   at FileHelpers.RecordOperations.StringToRecord(Object record, LineInfo line, Object[] values)
   at FileHelpers.FileHelperEngine`1.ReadStreamAsList(TextReader reader, Int32 maxRecords, DataTable dt)
   at FileHelpers.FileHelperEngine`1.ReadStream(TextReader reader, Int32 maxRecords)
   at FileHelpers.FileHelperEngine`1.ReadFile(String fileName, Int32 maxRecords)
   at FileHelpers.FileHelperEngine`1.ReadFile(String fileName)
   at <StartupCode$FSI_0004>.$FSI_0004.main@()
Stopped due to error
*)

Solution

  • The problem is in your StringToField method. The static type returned by Convert.ChangeType is obj, so the resulting option has type Option<obj>, rather than Option<float>. To fix this, downcast the converted value before calling Some:

    override __.StringToField(x) =
        match x with 
        | "" -> None :> obj
        | s ->
            let value = Convert.ChangeType(s,typeof<'T>) :?> 'T
            Some value :> _