目录
前言、目录、安装环境
Dynamic Query 原理 Part1
Dynamic Query 原理 Part2
Strongly Typed Mapping 原理 Part1 : ADO.NET对比Dapper
Strongly Typed Mapping 原理 Part2 : Reflection版本
Strongly Typed Mapping 原理 Part3 : 动态建立方法重要概念「结果反推程式码」优化效率
Strongly Typed Mapping 原理 Part4 : Expression版本
Strongly Typed Mapping 原理 Part5 : Emit IL反建立C#代码
Strongly Typed Mapping 原理 Part6 : Emit版本
Dapper 效率快关键之一 : Cache 缓存原理
错误SQL字串拼接方式,会导致效率慢、内存泄漏
Dapper SQL正确字串拼接方式 : Literal Replacement
Query Multi Mapping 使用方式
Query Multi Mapping 底层原理
QueryMultiple 底层原理
TypeHandler 自订Mapping逻辑使用、底层逻辑
CommandBehavior的细节处理
Parameter 参数化底层原理
IN 多集合参数化底层原理
DynamicParameter 底层原理、自订实作
单次、多次 Execute 底层原理
ExecuteScalar应用
总结
1.前言、目录、安装环境
经过业界前辈、StackOverflow多年推广,「Dapper搭配Entity Framework」成为一种功能强大的组合,它满足「安全、方便、高效、好维护」需求。
但目前中文网路文章,虽然有很多关于Dapper的文章但都停留在如何使用,没人系统性解说底层原理。所以有了此篇「深入Dapper源码」想带大家进入Dapper底层,了解Dapper的精美细节设计、高效原理,并学起来实际应用在工作当中。
建立Dapper Debug环境
到Dapper Github 首页 Clone最新版本到自己本机端
建立.NET Core Console专案
需要安装NuGet SqlClient套件、添加Dapper Project Reference
下中断点运行就可以Runtime查看逻辑
个人环境
数据库 : MSSQLLocalDB
Visaul Studio版本 : 2019
LINQ Pad 5 版本
Dapper版本 : V2.0.30
反编译 : ILSpy
2.Dynamic Query 原理 Part1
在前期开发阶段因为表格结构还在调整阶段,或是不值得额外宣告类别轻量需求,使用Dapper dynamic Query可以节省下来回修改class属性的时间。当表格稳定下来后使用POCO生成器快速生成Class转成强型别维护。
为何Dapper可以如此方便,支援dynamic?
追溯Query方法源码可以发现两个重点
实体类别其实是DapperRow再隐性转型为dynamic。
DapperRow继承IDynamicMetaObjectProvider并且实作对应方法。
此段逻辑我这边做一个简化版本的Dapper dynamic Query让读者了解转换逻辑 :
建立dynamic类别变量,实体类别是ExpandoObject
因为有继承关系可以转型为IDictionary
使用DataReader使用GetName取得栏位名称,借由栏位index取得值,并将两者分别添加进Dictionary当作key跟value。
因为ExpandoObject有实作IDynamicMetaObjectProvider介面可以转换成dynamic
public static class DemoExtension
{
public static IEnumerable Query(this IDbConnection cnn, string sql)
{
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return reader.CastToDynamic();
}
}
}
}
private static dynamic CastToDynamic(this IDataReader reader)
{
dynamic e = new ExpandoObject();
var d = e as IDictionary[span class="hljs-built_in">string,object>;
for (int i = 0; i < reader.FieldCount; i++)
d.Add(reader.GetName(i),reader【i】);
return e;
}
}
3.Dynamic Query 原理 Part2
有了前面简单ExpandoObject Dynamic Query例子的概念后,接着进到底层来了解Dapper如何细节处理,为何要自订义DynamicMetaObjectProvider。
首先掌握Dynamic Query流程逻辑 :
假设使用下面代码
using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
{
var result = cn.Query("select N'暐翰' Name,26 Age").First();
Console.WriteLine(result.Name);
}
取值的过程会是 : 建立动态Func > 保存在缓存 > 使用result.Name > 转成呼叫 ((DapperRow)result)【"Name"】 > 从DapperTable.Values阵列中以"Name"栏位对应的Index取值
接着查看源码GetDapperRowDeserializer方法,它掌管dynamic如何运行的逻辑,并动态建立成Func给上层API呼叫、缓存重复利用。
此段Func逻辑 :
DapperTable虽然是方法内的局部变量,但是被生成的Func引用,所以不会被GC一直保存在内存内重复利用。
因为是dynamic不需要考虑类别Mapping,这边直接使用GetValue(index)向数据库取值
var values = new object【select栏位数量】;
for (int i = 0; i < values.Length; i++)
{
object val = r.GetValue(i);
values【i】 = val is DBNull ? null : val;
}
将资料保存到DapperRow内
public DapperRow(DapperTable table, object【】 values)
{
this.table = table ?? throw new ArgumentNullException(nameof(table));
this.values = values ?? throw new ArgumentNullException(nameof(values));
}
DapperRow 继承 IDynamicMetaObjectProvider 并实作 GetMetaObject 方法,实作逻辑是返回DapperRowMetaObject物件。
private sealed partial class DapperRow : System.Dynamic.IDynamicMetaObjectProvider
{
DynamicMetaObject GetMetaObject(Expression parameter)
{
return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this);
}
}
DapperRowMetaObject主要功能是定义行为,借由override BindSetMember、BindGetMember方法,Dapper定义了Get、Set的行为分别使用IDictionary - GetItem方法跟DapperRow - SetValue方法
最后Dapper利用DataReader的栏位顺序性,先利用栏位名称取得Index,再利用Index跟Values取得值
为何要继承IDictionary?
可以思考一个问题 : 在DapperRowMetaObject可以自行定义Get跟Set行为,那么不使用Dictionary - GetItem方法,改用其他方式,是否代表不需要继承IDictionary?
Dapper这样做的原因之一跟开放原则有关,DapperTable、DapperRow都是底层实作类别,基于开放封闭原则不应该开放给使用者,所以设为private权限。
private class DapperTable{/略/}
private class DapperRow :IDictionary, IReadOnlyDictionary,System.Dynamic.IDynamicMetaObjectProvider{/略/}
那么使用者想要知道栏位名称怎么办?
因为DapperRow实作IDictionary所以可以向上转型为IDictionary,利用它为公开介面特性取得栏位资料。
public interface IDictionary : ICollection
举个例子,笔者有做一个小工具HtmlTableHelper就是利用这特性,自动将Dapper Dynamic Query转成Table Html,如以下代码跟图片
using (var cn = "Your Connection")
{
var sourceData = cn.Query(@"select 'ITWeiHan' Name,25 Age,'M' Gender");
var tablehtml = sourceData.ToHtmlTable(); //Result : NameAgeGenderITWeiHan25M
}
4. Strongly Typed Mapping 原理 Part1 : ADO.NET对比Dapper
接下来是Dapper关键功能 Strongly Typed Mapping,因为难度高,这边会切分成多篇来解说。
第一篇先以ADO.NET DataReader GetItem By Index跟Dapper Strongly Typed Query对比,查看两者IL的差异,了解Dapper Query Mapping的主要逻辑。
有了逻辑后,如何实作,我这边依序用三个技术 :Reflection、Expression、Emit 从头实作三个版本Query方法来让读者渐进式了解。
ADO.NET对比Dapper
首先使用以下代码来追踪Dapper Query逻辑
class Program
{
static void Main(string【】 args)
{
using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=master;"))
{
var result = cn.Query("select N'暐翰' Name , 25 Age").First();
Console.WriteLine(result.Name);
Console.WriteLine(result.Age);
}
}
}
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
这边需要重点来看Dapper.SqlMapper.GenerateDeserializerFromMap方法,它负责Mapping的逻辑,可以看到里面大量使用Emit IL技术。
要了解这段IL逻辑,我的方式 :「不应该直接进到细节,而是先查看完整生成的IL」,至于如何查看,这边需要先准备 il-visualizer //代码效果参考:http://www.zidongmutanji.com/zsjx/39659.html
开源工具,它可以在Runtime查看DynamicMethod生成的IL。它预设支持vs 2015、2017,假如跟我一样使用vs2019的读者,需要注意
需要手动解压缩到
%USERPROFILE%\Documents\Visual Studio 2019路径下面
.netstandard2.0专案,需要建立netstandard2.0并解压缩到该资料夹
最后重开visaul studio并debug运行,进到GetTypeDeserializerImpl方法,对DynamicMethod点击放大镜 > 選擇IL visualizer > 查看Runtime生成的IL代码
可以得出以下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: //代码效果参考:http://www.zidongmutanji.com/zsjx/495559.html
br.s IL_002bIL_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: stloc.1
IL_004e: leave IL_0060
IL_0053: ldloc.0
IL_0054: ldarg.0
IL_0055: ldloc.2
IL_0056: call Void ThrowDataException(System.Exception, Int32, System.Data.IDataReader, System.Object)/Dapper.SqlMapper
IL_005b: leave IL_0060
IL_0060: ldloc.1
IL_0061: ret
要了解这段IL之前需要先了解ADO.NET DataReader快速读取资料方式会使用GetItem By Index方式,如以下代码
public static class DemoExtension
{
private static User CastToUser(this IDataReader reader)
{
var user = new User();
var value = reader【0】;
if(!(value is System.DBNull))
user.Name = (string)value;
var value = reader【1】;
if(!(value is System.DBNull))
user.Age = (int)value;
return user;
}
public static IEnumerable Query(this IDbConnection cnn, string sql)
{
if (cnn.State == ConnectionState.Closed) cnn.Open();
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
while (reader.Read())
yield return reader.CastToUser();
}
}
}
接着查看此Demo - CastToUser方法生成的IL代码
DemoExtension.CastToUser:
IL_0000: nop
IL_0001: newobj User..ctor
IL_0006: stloc.0 // user
IL_0007: ldarg.0
IL_0008: ldc.i4.0
IL_0009: callvirt System.Data.IDataRecord.get_Item
IL_000E: stloc.1 // value
IL_000F: ldloc.1 // value
IL_0010: isinst System.DBNull
IL_0015: ldnull
IL_0016: cgt.un
IL_0018: ldc.i4.0
IL_0019: ceq
IL_001B: stloc.2
IL_001C: ldloc.2
IL_001D: brfalse.s IL_002C
IL_001F: ldloc.0 // user
IL_0020: ldloc.1 // value
IL_0021: castclass System.String
IL_0026: callvirt User.set_Name
IL_002B: nop
IL_002C: ldarg.0
IL_002D: ldc.i4.1
IL_002E: callvirt System.Data.IDataRecord.get_Item
IL_0033: stloc.1 // value
IL_0034: ldloc.1 // value
IL_0035: isinst System.DBNull
IL_003A: ldnull
IL_003B: cgt.un
IL_003D: ldc.i4.0
IL_003E: ceq
IL_0040: stloc.3
IL_0041: ldloc.3
IL_0042: brfalse.s IL_0051
IL_0044: ldloc.0 // user
IL_0045: ldloc.1 // value
IL_0046: unbox.any System.Int32
IL_004B: callvirt User.set_Age
IL_0050: nop
IL_0051: ldloc.0 // user
IL_0052: stloc.s 04
IL_0054: br.s IL_0056
IL_0056: ldloc.s 04
IL_0058: ret
跟Dapper生成的IL比对可以发现大致是一样的(差异部分后面会讲解),代表两者在运行的逻辑、效率上都会是差不多的,这也是为何Dapper效率接近原生ADO.NET的原因之一。
5. Strongly Typed Mapping 原理 Part2 : Reflection版本
在前面ADO.NET Mapping例子可以发现严重问题「没办法多类别共用方法,每新增一个类别就需要重写代码」。要解决这个问题,可以写一个共用方法在Runtime时期针对不同的类别做不同的逻辑处理。
实作方式做主要有三种Reflection、Expression、Emit,这边首先介绍最简单方式:「Reflection」,我这边会使用反射方式从零模拟Query写代码,让读者初步了解动态处理概念。(假如有经验的读者可以跳过本篇)
逻辑 :
使用泛型传递动态类别
使用泛型的条件约束new()达到动态建立物件
DataReader需要使用属性字串名称当Key,可以使用Reflection取得动态类别的属性名称,在借由DataReader this【string parameter】取得数据库资料
使用PropertyInfo.SetValue方式动态将数据库资料赋予物件
最后得到以下代码 :
public static class DemoExtension
{
public static IEnumerable Query(this IDbConnection cnn, string sql) where T : new()
{
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
while (reader.Read())
yield return reader.CastToType();
}
}
//1.使用泛型传递动态类别
private static T CastToType(this IDataReader reader) where T : new()
{
//2.使用泛型的条件约束new()达到动态建立物件
var instance = new T();
//3.DataReader需要使用属性字串名称当Key,可以使用Reflection取得动态类别的属性名称,在借由DataReader this【string parameter】取得数据库资料
var type = typeof(T);
var props = type.GetProperties();
foreach (var p in props)
{
var val = reader【p.Name】;
//4.使用PropertyInfo.SetValue方式动态将数据库资料赋予物件
if( !(val is System.DBNull) )
p.SetValue(instance, val);
}
return instance;
}
}
Reflection版本优点是代码简单,但它有以下问题
不应该重复属性查询,没用到就要忽略
举例 : 假如类别有N个属性,SQL指查询3个栏位,土炮ORM每次PropertyInfo foreach还是N次不是3次。而Dapper在Emit IL当中特别优化此段逻辑 : 「查多少用多少,不浪费」(这段之后讲解)。
效率问题 :
反射效率会比较慢,这点之后会介绍解决方式 : 「查表法 + 动态建立方法」以空间换取时间。
使用字串Key取值会多呼叫了GetOrdinal方法,可以查看MSDN官方解释,效率比Index取值差。
6.Strongly Typed Mapping 原理 Part3 : 动态建立方法重要概念「结果反推程式码」优化效率
接着使用Expression来解决Reflection版本问题,主要是利用Expression特性 : 「可以在Runtime时期动态建立方法」来解决问题。
在这之前需要先有一个重要概念 : 「从结果反推最简洁代码」优化效率,举个例子 : 以前初学程式时一个经典题目「打印正三角型星星」做出一个长度为3的正三角,常见作法会是回圈+递回方式
void Main()
{
Print(3,0);
}
static void Print(int length, int spaceLength)
{
if (length < 0)
return;
else
Print(length - 1, spaceLength + 1);
for (int i = 0; i < spaceLength; i++)
Console.Write(" ");
for (int i = 0; i < length; i++)
Console.Write(" ");
Console.WriteLine("");
}
但其实这个题目在已经知道长度的情况下,可以被改成以下代码
Console.WriteLine(" ");
Console.WriteLine(" ");
Console.WriteLine(" * ");
这个概念很重要,因为是从结果反推代码,所以逻辑直接、效率快,而Dapper就是使用此概念来动态建立方法。
举例 : 假设有一段代码如下,我们可以从结果得出
User Class的Name属性对应Reader Index 0 、类别是String 、 预设值是null
User Class的Age属性对应Reader Index 1 、类别是int 、 预设值是0
void Main()
{
using (var cn = Connection)
{
var result = cn.Query("select N'暐翰' Name,26 Age").First();
}
}
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
假如系统能帮忙生成以下逻辑方法,那么效率会是最好的
User 动态方法(IDataReader reader)
{
var user = new User();
var value = reader【0】;
if( !(value is System.DBNull) )
user.Name = (string)value;
value = reader【1】;
if( !(value is System.DBNull) )
user.Age = (int)value;
return user;
}
另外上面例子可以看出对Dapper来说SQL Select对应Class属性顺序很重要,所以后面会讲解Dapper在缓存的算法特别针对此优化。
7.Strongly Typed Mapping 原理 Part4 : Expression版本
有了前面的逻辑,就着使用Expression实作动态建立方法。
为何先使用 Expression 实作而不是 Emit ?
除了有能力动态建立方法,相比Emit有以下优点 :
可读性好,可用熟悉的关键字,像是变量Variable对应Expression.Variable、建立物件New对应Expression.New
方便Runtime Debug,可以在Debug模式下看到Expression对应逻辑代码
所以特别适合介绍动态方法建立,但Expression相比Emit无法作一些细节操作,这点会在后面Emit讲解到。
改写Expression版本
逻辑 :
取得sql select所有栏位名称
取得mapping类别的属性资料 > 将index,sql栏位,class属性资料做好对应封装在一个变量内方便后面使用
动态建立方法 : 从数据库Reader按照顺序读取我们要的资料,其中代码逻辑 :
User 动态方法(IDataReader reader)
{
var user = new User();
var value = reader【0】;
if( !(value is System.DBNull) )
user.Name = (string)value;
value = reader【1】;
if( !(value is System.DBNull) )
user.Age = (int)value;
return user;
}
最后得出以下Exprssion版本代码
public static class DemoExtension
{
public static IEnumerable Query(this IDbConnection cnn, string sql) where T : new()
{
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
var func = CreateMappingFunction(reader, typeof(T));
while (reader.Read())
{
var result = func(reader as DbDataReader);
yield return result is T ? (T)result : default(T);
}
}
}
}
private <