iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 16
5
Software Development

🌊 進階學習 ADO.NET、Dapper、Entity Framework 系列 第 16

【深入Dapper.NET源碼】TypeHandler 自訂Mapping邏輯使用、底層邏輯

遇到想要客製某些屬性Mapping邏輯時,在Dapper可以使用TypeHandler

使用方式 :

  • 建立類別繼承SqlMapper.TypeHandler
  • 將要客製的類別指定給泛型,e.g : JsonTypeHandler<客製類別> : SqlMapper.TypeHandler<客製類別>
  • 查詢的邏輯使用override實作Parse方法,增刪改邏輯實作SetValue方法
  • 假如多個類別Parse、SetValue共用同樣邏輯,可以將實作類別改為泛型方式,客製類別在AddTypeHandler時指定就可以,可以避免建立一堆類別,e.g : JsonTypeHandler<T> : SqlMapper.TypeHandler<T> where T : class

舉例 :
想要特定屬性成員在資料庫保存Json,在AP端自動轉成對應Class類別,這時候可以使用SqlMapper.AddTypeHandler<繼承實作TypeHandler的類別>

以下例子是User資料變更時會自動在Log欄位紀錄變更動作。

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 = "暐翰",
			Age = 26,
			Level = 1,
			Logs = new List<Log>() {
				new Log(){Time=DateTime.Now,Remark="CreateUser"}
			}
		};

		//新增資料
		{
			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動作
		{
			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; }
}

效果圖 :
20190929231937.png


接著追蹤TypeHandler源碼邏輯,需要分兩個部份來追蹤 : SetValue,Parse

SetValue底層原理

  1. AddTypeHandlerImpl方法管理緩存的添加
  2. 在CreateParamInfoGenerator方法Emit建立動態AddParameter方法時,假如該Mapping類別TypeHandler緩存內有資料,Emit添加呼叫SetValue方法動作。
if (handler != null)
{
	il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache<int>.SetValue))); // stack is now [parameters] [[parameters]] [parameter]
}
  1. 在Runtime呼叫AddParameters方法時會使用LookupDbType,判斷是否有自訂TypeHandler
    20191006151723.png
    20191006151614.png
  2. 接著將建立好的Parameter傳給自訂TypeHandler.SetValue方法
    20191006151901.png

最後查看IL轉成的C#代碼

    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);
        //略...
    }

可以發現生成的Emit IL會去從TypeHandlerCache取得我們實作的TypeHandler,接著呼叫實作SetValue方法運行設定的邏輯,並且TypeHandlerCache特別使用泛型類別依照不同泛型以Singleton方式保存不同handler,這樣有以下優點 :

  1. 只要傳遞泛型類別參數就可以取得同一個handler避免重複建立物件
  2. 因為是泛型類別,取handler時可以避免了反射動作,提升效率

https://ithelp.ithome.com.tw/upload/images/20190929/20105988x970H6xWXC.png
https://ithelp.ithome.com.tw/upload/images/20190929/20105988S7VZLLXLZo.png
https://ithelp.ithome.com.tw/upload/images/20190929/20105988Q1mWkL0GP6.png


Parse對應底層原理

主要邏輯是在GenerateDeserializerFromMap方法Emit建立動態Mapping方法時,假如判斷TypeHandler緩存有資料,以Parse方法取代原本的Set屬性動作。
https://ithelp.ithome.com.tw/upload/images/20190930/20105988JvCw5z207s.png

查看動態Mapping方法生成的IL代碼 :

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        

轉成C#代碼來驗證 :

	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;
		}
	}

上一篇
【深入Dapper.NET源碼】QueryMultiple(多個結果)底層原理
下一篇
【深入Dapper.NET源碼】 CommandBehavior的細節處理
系列文
🌊 進階學習 ADO.NET、Dapper、Entity Framework 30

尚未有邦友留言

立即登入留言