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