Search code examples
f#dapper

Dapper generic typehandler for F# Union types


I'm using union types similar to enums on my dapper objects:

type Confidence =
    | Low
    | Medium
    | High

type Goal = {
    Confidence: Confidence
    ...
 }

I've created a custom type handler in order to make it work:

type UnionHandler<'T>() =
       inherit SqlMapper.TypeHandler<'T>()

       override __.SetValue(param, value) =
            param.Value <- value.ToString()
            ()   

       override x.Parse(value: obj) =
            Union.parse <| string value


let registerTypeHandlers() =
    SqlMapper.AddTypeHandler (UnionHandler<Confidence>())

This works fine, but it would be even nicer if I didn't have to register a new one for each new union type.

Is it possible to make the type handler generic in such a way that it can handle all union types with only one registration?


Solution

  • This can be done with Reflection:

    let internal addUnionTypeHandlers() =
        let assembly = Assembly.GetExecutingAssembly()
        let unionHandlerType = 
            assembly.GetTypes() 
            |> Seq.filter(fun t -> t.Name.Contains("UnionHandler") && t.IsGenericTypeDefinition)
            |> Seq.head
    
        assembly.GetTypes()
        |> Seq.filter(fun t -> not t.IsGenericType && FSharpType.IsUnion(t, BindingFlags.Default))
        |> Seq.iter(fun t -> 
              let ctor = unionHandlerType
                             .MakeGenericType(t)
                             .GetConstructor(Array.empty)
                             .Invoke(Array.empty)
    
              (typeof<SqlMapper>.GetMethods()
              |> Seq.filter(fun methodInfo -> 
                  if methodInfo.Name = "AddTypeHandler" && methodInfo.IsGenericMethodDefinition then 
                      let gp = methodInfo.GetParameters()
                      not <| isNull gp && gp.Length = 1 && gp.[0].ParameterType.Name.Contains("TypeHandler")
                  else false)
              |> Seq.head)          
               .MakeGenericMethod(t)
               .Invoke(null, [| ctor |]) |> ignore
        )
    

    Note:

    1. It would have been much simpler if Dapper have had the signature of AddTypeHandler in a form ITypeHandler -> unit. But it accepts TypeHandler and in addition has overloaded version. So we need GMD for method AddTypeHandler and instantiate it with method MakeGenericMethod and then call this method with parameter which we obtains from GetConstructor ... Invoke

    2. Playing further with reflection you can decide to mark some discriminated unions with some attribute to ignore adding the mapping. You can extend code to analyse if type has attribute. Also you can do manipulations on module basis I assume using FSharpType.IsModule