Trace Dapper.NET Source Code - TypeHandler custom Mapping logic & its underlying logic
Wei
Posted on December 13, 2020
Github Link : Trace-Dapper.NET-Source-Code
18. TypeHandler custom Mapping logic & its underlying logic
When you want to customize some attribute Mapping logic, you can use TypeHandler
in Dapper
- Create a class to inherit SqlMapper.TypeHandler
- Assign the class to be customized to the generic, e.g:
JsonTypeHandler<custom_type>: SqlMapper.TypeHandler<custom_type>
- Override
Parse
method to customQuery
logic, and overrideSetValue
method to customCreate,Delete,Updte
logic - If there are multiple class Parse and SetValue share the same logic, you can change the implementation class to a generic method. The custom class can be specified in
AddTypeHandler
, which can avoid creating a lot of class, eg:JsonTypeHandler<T>: SqlMapper.TypeHandler< T> where T: class
Example : when User level is changed, the change action will be automatically recorded in the Log field.
public class JsonTypeHandler<T> : SqlMapper.TypeHandler<T>
where T : class
{
public override T Parse(object value)
{
return JsonConvert.DeserializeObject<T>((string)value);
}
public override void SetValue(IDbDataParameter parameter, T value)
{
parameter.Value = JsonConvert.SerializeObject(value);
}
}
public void Main()
{
SqlMapper.AddTypeHandler(new JsonTypeHandler<List<Log>>());
using (var ts = new TransactionScope())
using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
{
cn.Execute("create table [User] (Name nvarchar(200),Age int,Level int,Logs nvarchar(max))");
var user = new User()
{
Name = "Wei",
Age = 26,
Level = 1,
Logs = new List<Log>() {
new Log(){Time=DateTime.Now,Remark="CreateUser"}
}
};
// add
{
cn.Execute("insert into [User] (Name,Age,Level,Logs) values (@Name,@Age,@Level,@Logs);", user);
var result = cn.Query("select * from [User]");
Console.WriteLine(result);
}
// Level up
{
user.Level = 9;
user.Logs.Add(new Log() {Remark="UpdateLevel"});
cn.Execute("update [User] set Level = @Level,Logs = @Logs where Name = @Name", user);
var result = cn.Query("select * from [User]");
Console.WriteLine(result);
}
ts.Dispose();
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public int Level { get; set; }
public List<Log> Logs { get; set; }
}
public class Log
{
public DateTime Time { get; set; } = DateTime.Now;
public string Remark { get; set; }
}
Then trace the TypeHandler source code logic, which needs to be traced in two parts: SetValue, Parse
The underlying logic of SetValue
AddTypeHandlerImpl method to manage the addition of cache
When creating a dynamic AddParameter method in the CreateParamInfoGenerator method Emit, if there is data in the TypeHandler cache of the Mapping type, Emit adds an action to call the SetValue method.
if (handler != null)
{
il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache<int>.SetValue))); // stack is now [parameters] [[parameters]] [parameter]
}
- LookupDbType will be used when calling the AddParameters method at Runtime to determine whether there is a custom TypeHandler
- Then pass the created Parameter to the custom TypeHandler.SetValue method
Finally, the C# code converted from IL
public static void TestMeThod(IDbCommand P_0, object P_1)
{
User user = (User)P_1;
IDataParameterCollection parameters = P_0.Parameters;
//...
IDbDataParameter dbDataParameter3 = P_0.CreateParameter();
dbDataParameter3.ParameterName = "Logs";
dbDataParameter3.Direction = ParameterDirection.Input;
SqlMapper.TypeHandlerCache<List<Log>>.SetValue(dbDataParameter3, ((object)user.Logs) ?? ((object)DBNull.Value));
parameters.Add(dbDataParameter3);
//...
}
It can be found that the generated Emit IL will get our implemented TypeHandler from TypeHandlerCache, and then call the implemented SetValue method
to run the set logic, and TypeHandlerCache uses generic type
to save different handlers in Singleton mode
according to different generics. This has the following advantages :
- Same handler can be obtained to avoid repeated creation of objects
- Because it is a generic type, reflection actions can be avoided when the handler is taken, and
efficiency can be improved
Parse corresponds to the underlying principle
The main logic is when the GenerateDeserializerFromMap method Emit establishes the dynamic Mapping method, if it is judged that the TypeHandler cache has data, the Parse method replaces the original Set attribute action.
View the IL code generated by the dynamic Mapping method:
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: newobj Void .ctor()/Demo.User
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: dup
IL_000a: ldc.i4.0
IL_000b: stloc.0
IL_000c: ldarg.0
IL_000d: ldc.i4.0
IL_000e: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0013: dup
IL_0014: stloc.2
IL_0015: dup
IL_0016: isinst System.DBNull
IL_001b: brtrue.s IL_0029
IL_001d: unbox.any System.String
IL_0022: callvirt Void set_Name(System.String)/Demo.User
IL_0027: br.s IL_002b
IL_0029: pop
IL_002a: pop
IL_002b: dup
IL_002c: ldc.i4.1
IL_002d: stloc.0
IL_002e: ldarg.0
IL_002f: ldc.i4.1
IL_0030: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0035: dup
IL_0036: stloc.2
IL_0037: dup
IL_0038: isinst System.DBNull
IL_003d: brtrue.s IL_004b
IL_003f: unbox.any System.Int32
IL_0044: callvirt Void set_Age(Int32)/Demo.User
IL_0049: br.s IL_004d
IL_004b: pop
IL_004c: pop
IL_004d: dup
IL_004e: ldc.i4.2
IL_004f: stloc.0
IL_0050: ldarg.0
IL_0051: ldc.i4.2
IL_0052: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0057: dup
IL_0058: stloc.2
IL_0059: dup
IL_005a: isinst System.DBNull
IL_005f: brtrue.s IL_006d
IL_0061: unbox.any System.Int32
IL_0066: callvirt Void set_Level(Int32)/Demo.User
IL_006b: br.s IL_006f
IL_006d: pop
IL_006e: pop
IL_006f: dup
IL_0070: ldc.i4.3
IL_0071: stloc.0
IL_0072: ldarg.0
IL_0073: ldc.i4.3
IL_0074: callvirt System.Object get_Item(Int32)/System.Data.IDataRecord
IL_0079: dup
IL_007a: stloc.2
IL_007b: dup
IL_007c: isinst System.DBNull
IL_0081: brtrue.s IL_008f
IL_0083: call System.Collections.Generic.List`1[Demo.Log] Parse(System.Object)/Dapper.SqlMapper+TypeHandlerCache`1[System.Collections.Generic.List`1[Demo.Log]]
IL_0088: callvirt Void set_Logs(System.Collections.Generic.List`1[Demo.Log])/Demo.User
IL_008d: br.s IL_0091
IL_008f: pop
IL_0090: pop
IL_0091: stloc.1
IL_0092: leave IL_00a4
IL_0097: ldloc.0
IL_0098: ldarg.0
IL_0099: ldloc.2
IL_009a: call Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_009f: leave IL_00a4
IL_00a4: ldloc.1
IL_00a5: ret
Convert it into C# code to verify:
public static User TestMeThod(IDataReader P_0)
{
int index = 0;
User user = new User();
object value = default(object);
try
{
User user2 = user;
index = 0;
object obj = value = P_0[0];
//..
index = 3;
object obj4 = value = P_0[3];
if (!(obj4 is DBNull))
{
user2.Logs = SqlMapper.TypeHandlerCache<List<Log>>.Parse(obj4);
}
user = user2;
return user;
}
catch (Exception ex)
{
SqlMapper.ThrowDataException(ex, index, P_0, value);
return user;
}
}
Posted on December 13, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.