Make F# play nice with C# Part 2 - how to handle option and nullable mismatch and work with dapper

Jul 11, 2020 min read

This is the next part of the C# and F# integration journey. This time we will look into Dapper functional wrapper and option vs nullable type mismatch. Together with the previous post, we will close the loop of C# and F# coexistence.

Introduction

If you are interested just in making dapper more usable in F# or conversion between F# option <-> C# Nullable then this is the article for you. However, if you are looking for how-to integrate C# with F# in general you should also read my previous article about composing dependencies in C# and F# hybrid solution.

Nullable to option, option to nullable

If one will try to return option types from C# controller it will end up with JSON like this:

 1[
 2  {
 3    "id": 3,
 4    "openedAt": "2020-06-21T12:26:48.3966667",
 5    "invoicesCount": 0
 6  },
 7  {
 8    "id": 2,
 9    "openedAt": "2020-06-21T12:26:44.81",
10    "closedAt": {
11      "case": "Some", // This is how option type was serialized.
12      "fields": [
13        "2020-06-21T12:26:48.3966667"
14      ]
15    },
16    "invoicesCount": 0
17  }
18]

Not so nice or is it? Keep in mind that None is missing at all in the JSON. Let’s do some option to null conversation to achieve the following:

 1[
 2  {
 3    "id": 3,
 4    "openedAt": "2020-06-21T12:26:48.3966667",
 5    "invoicesCount": 0
 6  },
 7  {
 8    "id": 2,
 9    "openedAt": "2020-06-21T12:26:44.81",
10    "closedAt": "2020-06-21T12:26:48.3966667",
11    "invoicesCount": 0
12  }
13]

The F# util module that can help us with null <-> option transformation is straightforward, let me just paste it:

 1module NullableOptionUtil
 2
 3let ToOption (n: System.Nullable<_>) =
 4    if n.HasValue then Some n.Value else None
 5
 6let StringToOption(n: string) =
 7    if System.String.IsNullOrEmpty(n) then None else Some n
 8
 9let ToNullable (n: option<'a>) =
10    if n.IsSome then new System.Nullable<'a>(n.Value) else new System.Nullable<'a>()
11

The util is extra easy to use. Find the example below:

 1namespace Api.Dtos
 2{
 3    using System;
 4    using static NullableOptionUtil;
 5
 6    public class SaleInvociesReportDto
 7    {
 8        public int Id { get; set; }
 9        public DateTime OpenedAt { get; set; }
10        public DateTime? ClosedAt { get; set; }
11        public int InvoicesCount { get; set; }
12
13        public static SaleInvociesReportDto FromQueryResult(Queries.SalesInvoiceReportsResult saleInvoiceReport) =>
14        new SaleInvociesReportDto
15        {
16            Id = saleInvoiceReport.Id,
17            OpenedAt = saleInvoiceReport.OpenedAt,
18            ClosedAt = ToNullable(saleInvoiceReport.ClosedAt),
19            InvoicesCount = saleInvoiceReport.InvoicesCount
20        };
21    }
22}
23

Easy peasy, but can we do better? Can we do something to stop thinking about null to option conversion? Also, I don’t like writing DTOs when I do CQRS - the query results are already prepared for reading, creating DTO had been always 1:1 copy so I just stopped doing them for the “query part”. So let’s instruct our serializer on how to deal with the option. I did it in F# but if you prefer C#, go with C#. It is heavily inspired by Lev Gorodinski’s post [1] about different converters.

 1module OptionConverter
 2
 3open System
 4open Microsoft.FSharp.Reflection
 5open Newtonsoft.Json
 6
 7type OptionConverter() =
 8    inherit JsonConverter()
 9
10    override converter.CanConvert(typeParam) =
11        typeParam.IsGenericType && typeParam.GetGenericTypeDefinition() = typedefof<option<_>>
12
13    override converter.WriteJson(writer, value, serializer) =
14        let value =
15            match value with
16            | null -> null
17            | _ -> (FSharpValue.GetUnionFields(value, value.GetType()) |> snd).[0]
18        serializer.Serialize(writer, value)
19
20    override converter.ReadJson(reader, typeParam, _, serializer) =
21        let innerType = typeParam.GetGenericArguments().[0]
22        let innerType =
23            match innerType.IsValueType with
24            | true -> (typedefof<Nullable<_>>).MakeGenericType([| innerType |])
25            | _ -> innerType
26        let value = serializer.Deserialize(reader, innerType)
27        let cases = FSharpType.GetUnionCases(typeParam)
28        match value with
29        | null -> FSharpValue.MakeUnion(cases.[0], [||])
30        | _ -> FSharpValue.MakeUnion(cases.[1], [| value |])

Now you can delete the DTO, and return the query result directly without bothering about JSON representation of option type. Don’t forget to add the converter 😅

1            services.AddNewtonsoftJson(
2                    options =>
3                    {
4                        ...
5                        options.SerializerSettings.Converters.Add(new OptionConverter.OptionConverter());
6                    })

Dapper in F# - small wrapper to make use of it easy

Using dapper in F# is easy but if you are about to use asynchronous IO then you will have to deal with C# Task by writing Async.AwaitTask : Task<'a> → Async<'a>. This function translates a Task value to an Async value. Dapper is itself written in C# so you know the reason. Example pure dapper example:

1    let insertNewReportWithoutDapper (createSqlConnection: unit -> Async<SqlConnection>) newReport =
2        let (SalesInvoicesReportId reportId) = newReport.Id
3        async {
4            use! sqlConnection = createSqlConnection ()
5            do! sqlConnection.ExecuteAsync("INSERT INTO [Accounting].[SalesReport](Id, OpenedAt) VALUES(@Id, @OpenedAt)",
6                                           {| Id = reportId; OpenedAt = newReport.OpenedAt |})
7                |> Async.AwaitTask
8                |> Async.Ignore
9            }

With our wrapper which I will show you next it will be possible to write operations with SqlConnection in more F# friendly way which will increase code readibility. The same example with our wrapper:

1    let insertNewReport createSqlConnection newReport =
2        let (SalesInvoicesReportId reportId) = newReport.Id
3        async {
4            use! sqlConnection = createSqlConnection ()
5            do! sqlConnection |> dbParamatrizedExecute
6                       "INSERT INTO [Accounting].[SalesReport](Id, OpenedAt) VALUES(@Id, @OpenedAt)"
7                       {| Id = reportId; OpenedAt = newReport.OpenedAt |}
8              }

Much cleaner isn’t it? Here’s the wrapper code inspired by Roman Provazník gist [2] but with a focus on async methods.

 1namespace FSharpDapperWrapper
 2
 3open System.Data
 4open System.Data.SqlClient
 5open Dapper
 6
 7module DapperFSharp =
 8    let dbQuery<'Result> (query: string) (connection: SqlConnection): Async<'Result seq> =
 9        connection.QueryAsync<'Result>(query) |> Async.AwaitTask
10
11    let dbQueryMultiple (query: string) (connection: SqlConnection): Async<SqlMapper.GridReader> =
12        connection.QueryMultipleAsync(query) |> Async.AwaitTask
13
14    let dbParametrizedQueryMultiple (query: string) (param: obj) (connection: SqlConnection): Async<SqlMapper.GridReader> =
15        connection.QueryMultipleAsync(query, param) |> Async.AwaitTask
16
17    let dbParamatrizedQuery<'Result> (query: string) (param: obj) (connection: SqlConnection): Async<'Result seq> =
18        connection.QueryAsync<'Result>(query, param) |> Async.AwaitTask
19
20    let dbParamatrizedExecute (sql: string) (param: obj) (connection: SqlConnection) =
21        connection.ExecuteAsync(sql, param)
22        |> Async.AwaitTask
23        |> Async.Ignore
24
25    let createSqlConnection connectionString =
26        OptionHandler.RegisterTypes() // This option handler translates null to None when reading, and None to null when writing. More in chapter 3.1.
27        async {
28            let connection = new SqlConnection(connectionString)
29            if connection.State <> ConnectionState.Open then do! connection.OpenAsync() |> Async.AwaitTask
30            return connection
31        }
32
33    type Dapper.SqlMapper.GridReader with
34        member reader.Read<'a>() = reader.ReadAsync<'a>() |> Async.AwaitTask

Having a factory method for SQL connection we can make our CompositionRoot described in Part 1 complete:

1Func<FSharpAsync<SqlConnection>> fSqlConnectionFactory =
2    () => DapperFSharp.createSqlConnection(_appSettings.ConnectionString);

This is the “magical” _fSqlConnectionFactory that I left without going into details in the previous post.

Mapping from null to None.

The next step to have Dapper in our functional world is about mapping null to None when reading the data (we don’t want nulls right?) and None to null when writing. The code explains itself:

 1namespace FSharpDapperWrapper
 2open Dapper
 3open System
 4
 5type OptionHandler<'T> () =
 6    inherit SqlMapper.TypeHandler<option<'T>> ()
 7
 8    override __.SetValue (param, value) =
 9        let valueOrNull =
10            match value with
11            | Some x -> box x
12            | None   -> null
13        param.Value <- valueOrNull
14
15    override __.Parse value =
16        if Object.ReferenceEquals(value, null) || value = box DBNull.Value
17        then None
18        else Some (value :?> 'T)
19
20module OptionHandler =
21    let RegisterTypes () =
22        SqlMapper.AddTypeHandler (OptionHandler<bool>())
23        SqlMapper.AddTypeHandler (OptionHandler<byte>())
24        SqlMapper.AddTypeHandler (OptionHandler<sbyte>())
25        SqlMapper.AddTypeHandler (OptionHandler<int16>())
26        SqlMapper.AddTypeHandler (OptionHandler<uint16>())
27        SqlMapper.AddTypeHandler (OptionHandler<int32>())
28        SqlMapper.AddTypeHandler (OptionHandler<uint32>())
29        SqlMapper.AddTypeHandler (OptionHandler<int64>())
30        SqlMapper.AddTypeHandler (OptionHandler<uint64>())
31        SqlMapper.AddTypeHandler (OptionHandler<single>())
32        SqlMapper.AddTypeHandler (OptionHandler<float>())
33        SqlMapper.AddTypeHandler (OptionHandler<double>())
34        SqlMapper.AddTypeHandler (OptionHandler<decimal>())
35        SqlMapper.AddTypeHandler (OptionHandler<char>())
36        SqlMapper.AddTypeHandler (OptionHandler<string>())
37        SqlMapper.AddTypeHandler (OptionHandler<Guid>())
38        SqlMapper.AddTypeHandler (OptionHandler<DateTime>())
39        SqlMapper.AddTypeHandler (OptionHandler<DateTimeOffset>())
40        SqlMapper.AddTypeHandler (OptionHandler<TimeSpan>())
41        SqlMapper.AddTypeHandler (OptionHandler<DateTimeOffset>())
42        SqlMapper.AddTypeHandler (OptionHandler<obj>())

Now you can use dapper in F# without worries! Let me show you two examples;

Samples:

 1    let storeCustomer createSqlConnection customer =
 2        async {
 3            use! sqlConnection = createSqlConnection()
 4            do! sqlConnection
 5                |> dbParamatrizedExecute """
 6INSERT INTO [Customers]
 7( Id,  Name,  Surname,  Email) VALUES
 8(@Id, @Name, @Surname, @Email)
 9        """ (DbCustomer.fromDomain customer)
10        }
11
12    let readCustomerWithOrders createSqlConnection customerId =
13        async {
14            use! sqlConnection = createSqlConnection()
15            let (CustomerId reportId) = customerId
16            let! resultSet = sqlConnection
17                                |> dbParametrizedQueryMultiple """
18SELECT Id, Name, Surname, Email FROM [Customers] WHERE [Id] = @Id
19SELECT
20Id,
21Name,
22Price,
23OrderedDate
24FROM [Orders]
25WHERE CustomerId = @Id
26        """ {|Id = customerId|}
27            let! dbCustomer = resultSet.Read<DbCustomer>()
28            let! dbOrders = resultSet.Read<DbOrder>()
29            return (dbCustomer |> Seq.head |> DbCustomer.toDomain,
30                    dbOrders |> Seq.map(fun order -> DbOrder.toDomain order)
31                             |> Seq.toList
32                   )
33        } 

Poof… that’s it!

Summary

Handling null and option type mismatch is quite easy and intuitive. Dapper required more effort but still, these are just two files - once you have them, they just work. With the previous article about composing dependencies, you have now full power to write C# hosting project with F# projects starting from the application layer, through the domain to different adapters including querying data with dapper. May the F#orce be with you!


References:
Websites:
[1] Lev Gorodinski blog and his code about different type converters
[2] Roman Provazník gist with Dapper wrapper