心得经验总结:深入Dapper.NET源码(文长)

简介: 心得经验总结:深入Dapper.NET源码(文长)

目录

前言、目录、安装环境

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_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: 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 <

相关文章
|
7月前
|
开发框架 .NET BI
ASP.NET公立医院健康体检信息管理系统源码
健康体检信息管理系统是专门针对医院体检中心的日常业务运作的特点和流程,结合数字化医院建设要求进行设计研发的一套应用系统。该系统覆盖体检中心的所有业务,完成从预约、登记、收费、检查、检验、出报告、分析、报表等所有工作,规范了体检流程,提高了工作效率。体检系统为每个体检者建立一套完整的体检档案,与病人的门诊、住院诊疗信息有机集成, 真正体现数字化医院以病人为中心的建设原则。
115 1
|
7月前
|
开发框架 安全 .NET
ASP.NET三甲医院手术麻醉信息管理系统源码 对接麻醉机、监护仪、血气分析仪
辅助医院建设 •支持三级医院评级需求 •支持智慧医院评级需求 •支持互联互通评级需求 •支持电子病历评级需求
79 0
|
7月前
|
开发框架 Oracle 关系型数据库
ASP.NET实验室LIS系统源码 Oracle数据库
LIS是HIS的一个组成部分,通过与HIS的无缝连接可以共享HIS中的信息资源,使检验科能与门诊部、住院部、财务科和临床科室等全院各部门之间协同工作。 
85 4
|
7月前
|
开发框架 前端开发 .NET
分享119个ASP.NET源码总有一个是你想要的
分享119个ASP.NET源码总有一个是你想要的
153 1
|
7月前
|
开发框架 前端开发 JavaScript
盘点72个ASP.NET Core源码Net爱好者不容错过
盘点72个ASP.NET Core源码Net爱好者不容错过
174 0
|
7月前
|
开发框架 前端开发 JavaScript
分享129个ASP.NET源码总有一个是你想要的
分享129个ASP.NET源码总有一个是你想要的
105 0
|
7月前
|
开发框架 前端开发 .NET
分享68个ASP.NET源码总有一个是你想要的
分享68个ASP.NET源码总有一个是你想要的
762 0
|
7月前
|
开发框架 前端开发 JavaScript
分享53个ASP.NET源码总有一个是你想要的
分享53个ASP.NET源码总有一个是你想要的
103 0
|
7月前
|
开发框架 前端开发 .NET
分享42个ASP.NET源码总有一个是你想要的
分享42个ASP.NET源码总有一个是你想要的
68 0
|
7月前
|
存储 开发框架 前端开发
前端框架EXT.NET Dotnet 3.5开发的实验室信息管理系统(LIMS)成品源码 B/S架构
发展历史:实验室信息管理系统(LIMS),就是指通过计算机网络技术对实验的各种信息进行管理的计算机软、硬件系统。也就是将计算机网络技术与现代的管理思想有机结合,利用数据处理技术、海量数据存储技术、宽带传输网络技术、自动化仪器分析技术,来对实验室的信息管理和质量控制等进行全方位管理的计算机软、硬件系统,以满足实验室管理上的各种目标(计划、控制、执行)。
72 1