36
loading...
This website collects cookies to deliver better user experience
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
}
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();
select
Id,
Login,
(select UserId, Number, Kind from PhoneNumbers where UserId = u.Id FOR JSON PATH) as PhonesJson
from Users u
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}] |
User
model based on the new query: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();
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>
. System.Text.Json
it will look like that: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);
}
}
Json<T>
with PhoneNumber[]
then we need to add it to SqlMapper
like that:SqlMapper.AddTypeHandler(new JsonTypeHandler<PhoneNumber[]>());
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();
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:SqlMapper.AddTypeHandler(new JsonTypeHandler<User[]>());
SqlMapper.AddTypeHandler(new JsonTypeHandler<EmailDetails>());
SqlMapper.AddTypeHandler(new JsonTypeHandler<Role>());
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! Json<T>
and create a module initializer(another new trick) that will add all required types to SqlMapper
auto-magically.Json<T>
. 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);
}
}
}
[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}"";
}}
}}");
}
}
}
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>());
}
}
}
Json<T>
, it will automatically be added to SqlMapper
.