心得经验总结:深入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 <

相关文章
|
5月前
|
SQL 机器学习/深度学习 缓存
心得经验总结:深入Dapper.NET源码(文长)
心得经验总结:深入Dapper.NET源码(文长)
80 0
|
6月前
|
开发框架 .NET C#
使用C#进行.NET框架开发:深入探索与实战
【5月更文挑战第28天】本文探讨了C#在.NET框架中的应用,展示了其作为强大编程语言的特性,如类型安全、面向对象编程。C#与.NET框架的结合,提供了一站式的开发环境,支持跨平台应用。文中介绍了C#的基础知识,如数据类型、控制结构和面向对象编程,以及.NET的关键技术,包括LINQ、ASP.NET和WPF。通过一个实战案例,展示了如何使用C#和ASP.NET开发Web应用,包括项目创建、数据库设计、模型和控制器编写,以及视图和路由配置。本文旨在揭示C#在.NET开发中的深度和广度,激发开发者探索更多可能性。
|
开发框架 .NET 编译器
C#OOP之十四 .Net Framework简介
C#OOP之十四 .Net Framework简介
87 0
|
安全 Java C#
C#(二十四)之.NET框架
本篇内容记录了.NET框架的一些基本用法。
153 0
C#(二十四)之.NET框架
|
.NET
一起谈.NET技术,走向ASP.NET架构设计——第七章:阶段总结,实践篇(上篇)
  示例说明   本篇的例子的是一个在线订票的服务系统。这个系统向外界暴露了一些可以通过Http协议访问的API,在这个订票服务下面允许任意多个隶属机构来使用服务API进行真正的售票活动。如下图所示:   就好比银行外面的那些自动取款机(对应图中的Affiliate A, B, C),可以把它们看成是银行系统的隶属机构,我们就是通过这些取款机来进行存取活动的,其实这些取款机是调用了银行系统的一些服务来进行数据操作,当然我们也可以直接到银行柜台(对应图中的Ticket Shop)去进行存取款操作。
794 0
|
缓存 .NET 数据库
一起谈.NET技术,.NET 分布式架构开发实战之二 草稿设计
  前言:   本篇之所以称为草稿设计,是因为设计的都是在纸上完成的。反映了一个思考的过程。   本篇的议题如下:   1) 第一个数据层草图的提出   2) 对数据访问层的思考   3) 第二个数据层草图的提出   1.数据层草图的提出   Richard开始着手设计,一开始他没有就立刻在自己的计算机开始敲代码。
1074 0
|
.NET
走向ASP.NET架构设计——第七章:阶段总结,“.NET研究”实践篇(上篇)
  示例说明   本篇的例子的是一个在线订票的服务系统。这个系统向外界暴露了一些可以通过Http协议访问的API,在这个订票服务下面允许任意多个隶属机构来使用服务API进行真正的售票活动。如下图所示:   就好比银行外面的那些自动取款机(对应图中的Affiliate A, B, C),可以把它们看成是银行系统的隶属机构,我们就是通过这些取款机来进行存取活动的,其实这些取款机是调用了银行系统的一些服务来进行数据操作,当然我们也可以直接到银行柜台(对应图中的Ticket Shop)去进行存取款操作。
775 0
|
缓存 .NET 数据库
.NET 分布式架构开发实战之二 草稿设计“.NET研究”
  前言:   本篇之所以称为草稿设计,是因为设计的都是在纸上完成的。反映了一个思考的过程。   本篇的议题如下:   1) 第一个数据层草图的提出   2) 对数据访问层的思考   3) 第二个数据层草图的提出   1.数据层草图的提出   Richard开始着手设计,一开始他没有就立刻在自己的计算机开始敲代码。
1031 0
|
架构师 .NET 测试技术
一起谈.NET技术,走向ASP.NET架构设计——第一章:走向设计
  前言:很多做开发的人都在不断的摸索着,积极的学习,试图找出一条走向架构设计的成功法则。每当有人问起我们的职业,我们也常常在说:”软件设计”。有时,我就在想:”设计”,这个已经被我们嚼烂了的词,到底有多少人真正懂”设计”的含义。
1320 0
|
安全 前端开发 .NET
一起谈.NET技术,.NET 4九大新特性 FrameWork达到新境界
  本文将向您介绍.NET框架4中的主要功能和改进特征。请注意,本文中并没有提供有关这些新功能的综合信息,并随时可能更改。   请注意,.NET框架4引入了一个改进的安全模式。有关该内容的更多的信息,请参阅文章《.NET框架4中的安全变化》。
1367 0