Making Dapper and JSON friends
Tags: csharp, sql, dapper, sourcegenerators
Published: 2021-12-06 18:03:54

Let's speak about improvements that we can bring to the Dapper. Improvements in the 'quality of life' category, to be precise. I think that everyone, who had some experience with SQL in the past, had a situation like that. You have a model that you want to show to the user. The data is stored in the SQL database. The model itself is straightforward from the C# perspective.

Here is it:
``` cs
class User 
{
    public int Id { get; set;}
    public string Login { get; set;}
    public PhoneNumber[] Phones { get; set;}
}

class PhoneNumber 
{
    public string Number { get; set; }
    public PhoneNumberKind Kind { get; set; } 
}

enum PhoneNumberKind
{
    Office,
    Mobile,
    Home
}
```

It consists of a few fields and an array inside. As you may guess, the main problem will be the array because SQL doesn't have an easy way to put a list of items in the column. So, to fill the model, we will need to execute multiple queries or play around with a SQL reader. Still, both cases require additional manipulation with numerous rows to construct a model. 

Sample with multiple requests:
``` cs 
var usersData = await connection.QueryAsync<(int Id, string Login)>("select Id, Login from Users");

var phoneNumbersData = await connection.QueryAsync<(int UserId, string Number, PhoneNumberKind Kind)>(
    "select UserId, Number, Kind from PhoneNumbers where UserId in @UserIds", new { UserIds = usersData.Select(o => o.Id)});

var phoneNumbersByUserId = phoneNumbersData
    .GroupBy(o => o.UserId)
    .ToDictionary(o => o.Key, o => o.ToArray());

var users = usersData
    .Select(o => 
        new User() 
        { 
            Id = o.Id, 
            Login = o.Login, 
            PhoneNumbers = phoneNumbersByUserId.ContainsKey(o.Id) ? 
                phoneNumbersByUserId[o.Id]
                    .Select(phone => new PhoneNumber 
                        { 
                            Number = phone.Number, 
                            Kind = phone.Kind 
                        })
                    .ToArray() :
                Array.Empty<PhoneNumber>()
        })
    .ToArray();
```

As for me, such a solution looks clunky and has huge potential for mistakes. Also, in this sample, we are keeping the whole result set in the memory. That is not great either. To avoid it, we need to introduce batching. At the same time, we may need to add more joins. All of that makes logic even more complex. 

## Making Dapper and JSON friends

At this point, we can remember the fact that the modern SQL databases have support for JSON. The MSSQL server, for example, can execute selectors and pack results into columns as a JSON. In our case, the query can be simplified to look like that:
``` sql 
select 
    Id,
    Login,
    (select UserId, Number, Kind from PhoneNumbers where UserId = u.Id FOR JSON PATH) as PhonesJson
from Users u
```

The result of the query will be next:

| Id | Login | PhonesJson |
| :--- | :--- | :--- |
| 16 | Karita\_Ringo | \[{"UserId":16,"Number":"3377787","Kind":2}\] |
| 17 | Chiquia\_Dud | NULL |
| 18 | Caprice\_Hugibert | \[{"UserId":18,"Number":"8454027","Kind":0}\] |
| 19 | Pat\_Tedie | \[{"UserId":19,"Number":"4646231","Kind":1}\] |
| 20 | Vally\_Fitzgerald | \[{"UserId":20,"Number":"1836548","Kind":2}\] |
| 21 | Jasmina\_Xerxes | NULL |
| 22 | Kristy\_Nathaniel | \[{"UserId":22,"Number":"7018212","Kind":1}\] |
| 23 | Farica\_Ives | \[{"UserId":23,"Number":"0874725","Kind":2}\] |
| 24 | Clementine\_Gun | NULL |
| 25 | Jerry\_Whit | \[{"UserId":25,"Number":"0013362","Kind":0}\] |


Now lets fill the `` User `` model based on the new query:
```cs 
var usersData = await connection.QueryAsync<(int Id, string Login, string PhonesJson)>(@"
    select 
        Id,
        Login,
        (select UserId, Number, Kind from PhoneNumbers where UserId = u.Id FOR JSON PATH) as PhonesJson
    from Users u");

var users = usersData
    .Select(o => 
        new User 
        { 
            Id = o.Id, 
            Login = o.Login, 
            PhoneNumbers = !string.IsNullOrEmpty(o.PhonesJson) ? 
                JsonSerializer.Deserialize<PhoneNumber[]>(o.PhonesJson) :
                Array.Empty<PhoneNumber>()
        })
    .ToArray();
```

Done! The logic flow now is much simple. At the same time, this approach allows us to enable un-buffered mode and fetch rows in batches. In some scenarios, it can be a life-saving feature. We can start processing much earlier and free memory from processed rows. And all of this without the need to build complex logic.

## Making Dapper and JSON even closer friends 

Can we make our solution even better? Yes, we can. The `` Dapper `` has a way to add custom user-defined types. So, what if we can create a type like `` Json<T> `` and use it as a column type? It can achieve it via the `` SqlMapper.TypeHandler ``. Lets create our `` JsonTypeHandler<T> ``. 

For `` System.Text.Json `` it will look like that:
``` cs
public class JsonTypeHandler<T> : SqlMapper.TypeHandler<Json<T>>
{
    public override void SetValue(IDbDataParameter parameter, Json<T> value)
    {
        parameter.Value = JsonSerializer.Serialize(value.Value);
    }

    public override Json<T> Parse(object value)
    {
        if (value is string json)
        {
            return new Json<T>(JsonSerializer.Deserialize<T>(json));
        }

        return new Json<T>(default);
    }
}
```

If we want to use the `` Json<T> `` with `` PhoneNumber[] `` then we need to add it to `` SqlMapper `` like that:
``` cs
SqlMapper.AddTypeHandler(new JsonTypeHandler<PhoneNumber[]>());
```

Now, lets have a look at updated sample:
``` cs
var usersData = await connection.QueryAsync<(int Id, string Login, Json<PhoneNumber[]>? PhoneNumbers)>(@"
    select 
        Id,
        Login,
        (select UserId, Number, Kind from PhoneNumbers where UserId = u.Id FOR JSON PATH) as PhoneNumbers
    from Users u");

var users = usersData
    .Select(o => 
        new User 
        { 
            Id = o.Id, 
            Login = o.Login, 
            PhoneNumbers = o.PhoneNumbers?.Value ?? Array.Empty<PhoneNumber>()
        })
    .ToArray();
```

Such an approach allows us to expose our intention about the query more clearly. At the same time, keep the simplicity of the source code. 

But there is an issue. If we want to use other `` Json<T> `` of different types like `` Json<User[]> ``, `` Json<EmailDetails> ``, `` Json<Role> `` and so on. We will need to manually add type handlers to `` SqlMapper `` for each of them. Like that:
``` cs
SqlMapper.AddTypeHandler(new JsonTypeHandler<User[]>());
SqlMapper.AddTypeHandler(new JsonTypeHandler<EmailDetails>());
SqlMapper.AddTypeHandler(new JsonTypeHandler<Role>());
```
That's not great. It completely ruins the ease of use. I have tried to find a way to register all types at once, but the `` Dapper `` documentation didn't provide any way to achieve such behaviour. So, the only way is to add type handlers of each type. It would be a disaster, but it is the C#! It has a lot of tricks up his sleeves! 

## The trick

The trick that we will use is source generators! By itself, it is like an extension for the Roslyn compiler. It allows to analyze the existing source code and, based on it, generate a new one. So, the idea is simple. Let's look for usages of `` Json<T> `` and create a module initializer(another new trick) that will add all required types to `` SqlMapper `` auto-magically.

The source generator consists of the syntax receiver and the source generator. Our syntax receiver will be really stupid. Its single task will be to find the `` Json<T> ``. 

It looks like that:
``` cs
public class DapperSourceGeneratorSyntaxReceiver : ISyntaxReceiver
{
    public List<GenericNameSyntax> Types { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        // select all generic type usages of Json<>
        if (syntaxNode is GenericNameSyntax { Identifier.ValueText: "Json", } type)
        {
            Types.Add(type);
        }
    }
}
```

The source generator is a bit more complex, but the basic idea should be clear.
``` cs

    [Generator]
    public class DapperJsonSourceGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
            // register our syntax reciver 
            context.RegisterForSyntaxNotifications(() => new DapperSourceGeneratorSyntaxReceiver());
        }

        public void Execute(GeneratorExecutionContext context)
        {
            if (context.SyntaxReceiver is not DapperSourceGeneratorSyntaxReceiver syntaxReceiver)
            {
                return;
            }

            try
            {
                // neccesary to make sure that it is our Json<T>
                var jsonType = context.Compilation.GetTypeByMetadataName("Dapper.Json.Json`1");

                var types = syntaxReceiver.Types
                    .Select(o =>
                    {
                        var semanticModel = context.Compilation.GetSemanticModel(o.SyntaxTree);
                        var type = semanticModel.GetTypeInfo(o);
                        if (type.Type is INamedTypeSymbol namedTypeSymbol &&
                            namedTypeSymbol.ConstructedFrom.Equals(jsonType))
                        {
                            // extracting the generic type
                            return namedTypeSymbol.TypeArguments.First();
                        }

                        return null;
                    })
                    .Where(o => o != null)
                    .Distinct(SymbolEqualityComparer.Default)
                    .ToArray();

                // create a module initializer and add all types to SqlMapper
                var source = @$"using System;

namespace Dapper.Json
{{
    public static class DapperJsonModuleInitializer
    {{
        [global::System.Runtime.CompilerServices.ModuleInitializer]
        public static void Init()
        {{
{types.Select(o => $@"            SqlMapper.AddTypeHandler(new JsonTypeHandler<{o.ToGlobalName()}>());").JoinWithNewLine()}
        }}
    }}
}}
";

                context.AddSource("Dapper.Json.g.cs", source.ToSourceText());
            }
            catch (Exception e)
            {
                // some error handeling to simplify source generator debuging experiance
                context.AddSource("Dapper.Json.Error.g.cs", @$"

namespace Dapper.Json
{{
    public static class DapperJsonError
    {{
        public static string Error = ""{e.Message}"";
    }}
}}");
            }
        }
    }
```

The final module initializer may look like that:
``` cs
using System;

namespace Dapper.Json
{
    public static class DapperJsonModuleInitializer
    {
        [global::System.Runtime.CompilerServices.ModuleInitializer]
        public static void Init()
        {
            SqlMapper.AddTypeHandler(new JsonTypeHandler<global::PhoneNumber[]>());
            SqlMapper.AddTypeHandler(new JsonTypeHandler<global::User[]>());
            SqlMapper.AddTypeHandler(new JsonTypeHandler<global::EmailDetails>());
            SqlMapper.AddTypeHandler(new JsonTypeHandler<global::Role>());
        }
    }
}
```

Done! Whenever we use the `` Json<T> ``, it will automatically be added to `` SqlMapper ``. 

## How to use it

The complete instruction you can find at [the repository on github](https://github.com/byme8/Dapper.Json). There is nothing complicated. You need to install two NuGet packages, and you are ready to go.

That is all for today. Feel free to leave a comment or create an issue on [GitHub](https://github.com/byme8/Dapper.Json).

Thanks for your time!