通过业界前辈、StackOverflow多年推广,「Dapper搭配Entity Framework」成为一种功能强大的组合,它知足「安全、方便、高效、好维护」
需求。html
但目前中文网路文章,虽然有不少关于Dapper的文章但都停留在如何使用,没人系统性解说底层原理。因此有了此篇「深刻Dapper源码」想带你们进入Dapper底层,了解Dapper的精美细节设计、高效原理,并学起来
实际应用在工做当中。git
在前期开发阶段由于表格结构还在调整阶段
,或是不值得额外宣告类别轻量需求,使用Dapper dynamic Query能够节省下来回修改class属性的时间。当表格稳定下来后使用POCO生成器快速生成Class转成强型别
维护。github
追溯Query
方法源码能够发现两个重点面试
DapperRow
再隐性转型为dynamic。IDynamicMetaObjectProvider
而且实做对应方法。此段逻辑我这边作一个简化版本的Dapper dynamic Query让读者了解转换逻辑 :算法
dynamic
类别变量,实体类别是ExpandoObject
IDictionary<string, object>
public static class DemoExtension { public static IEnumerable<dynamic> 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<string,object>; for (int i = 0; i < reader.FieldCount; i++) d.Add(reader.GetName(i),reader[i]); return e; } }
有了前面简单ExpandoObject Dynamic Query例子的概念后,接着进到底层来了解Dapper如何细节处理,为什么要自订义DynamicMetaObjectProvider。sql
假设使用下面代码数据库
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
取值express
接着查看源码GetDapperRowDeserializer方法,它掌管dynamic如何运行的逻辑,并动态创建成Func给上层API呼叫、缓存重复利用。
c#
此段Func逻辑 :缓存
DapperTable虽然是方法内的局部变量,可是被生成的Func引用,因此不会被GC
一直保存在内存内重复利用。
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; }
public DapperRow(DapperTable table, object[] values) { this.table = table ?? throw new ArgumentNullException(nameof(table)); this.values = values ?? throw new ArgumentNullException(nameof(values)); }
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<string, object> - GetItem方法
跟DapperRow - SetValue方法
最后Dapper利用DataReader的栏位顺序性
,先利用栏位名称取得Index,再利用Index跟Values取得值
能够思考一个问题 : 在DapperRowMetaObject能够自行定义Get跟Set行为,那么不使用Dictionary - GetItem方法,改用其余方式,是否表明不须要继承IDictionary<string,object>
?
Dapper这样作的缘由之一跟开放原则有关,DapperTable、DapperRow都是底层实做类别,基于开放封闭原则不该该开放给使用者
,因此设为private
权限。
private class DapperTable{/*略*/} private class DapperRow :IDictionary<string, object>, IReadOnlyDictionary<string, object>,System.Dynamic.IDynamicMetaObjectProvider{/*略*/}
那么使用者想要知道栏位名称
怎么办?
由于DapperRow实做IDictionary因此能够向上转型为IDictionary<string, object>
,利用它为公开介面
特性取得栏位资料。
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable{/*略*/}
举个例子,笔者有作一个小工具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 : <table><thead><tr><th>Name</th><th>Age</th><th>Gender</th></tr></thead><tbody><tr><td>ITWeiHan</td><td>25</td><td>M</td></tr></tbody></table> }
接下来是Dapper关键功能 Strongly Typed Mapping
,由于难度高,这边会切分红多篇来解说。
第一篇先以ADO.NET DataReader GetItem By Index跟Dapper Strongly Typed Query对比,查看二者IL的差别,了解Dapper Query Mapping的主要逻辑。
有了逻辑后,如何实做,我这边依序用三个技术 :Reflection、Expression、Emit
从头实做三个版本Query方法来让读者渐进式了解。
首先使用如下代码来追踪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<User>("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 开源工具,它能够在Runtime查看DynamicMethod生成的IL。
它预设支持vs 201五、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: 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<User> Query<T>(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
的缘由之一。
在前面ADO.NET Mapping例子能够发现严重问题「没办法多类别共用方法,每新增一个类别就须要重写代码」
。要解决这个问题,能够写一个共用方法在Runtime时期针对不一样的类别作不一样的逻辑处理。
实做方式作主要有三种Reflection、Expression、Emit,这边首先介绍最简单方式:「Reflection」,我这边会使用反射方式从零模拟Query写代码,让读者初步了解动态处理概念。(假若有经验的读者能够跳过本篇)
逻辑 :
泛型的条件约束new()
达到动态创建物件属性字串名称当Key
,可使用Reflection取得动态类别的属性名称,在借由DataReader this[string parameter]
取得数据库资料最后获得如下代码 :
public static class DemoExtension { public static IEnumerable<T> Query<T>(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<T>(); } } //1.使用泛型传递动态类别 private static T CastToType<T>(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版本优势是代码简单
,但它有如下问题
「查多少用多少,不浪费」
(这段以后讲解)。「查表法 + 动态创建方法」
以空间换取时间。GetOrdinal
方法,能够查看MSDN官方解释,效率比Index取值差
。
接着使用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就是使用此概念来动态创建方法。
举例 : 假设有一段代码以下,咱们能够从结果得出
void Main() { using (var cn = Connection) { var result = cn.Query<User>("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在缓存的算法特别针对此优化。
有了前面的逻辑,就着使用Expression实做动态创建方法。
除了有能力动态创建方法,相比Emit有如下优势 :
可读性好
,可用熟悉的关键字,像是变量Variable对应Expression.Variable、创建物件New对应Expression.New方便Runtime Debug
,能够在Debug模式下看到Expression对应逻辑代码因此特别适合介绍动态方法创建,但Expression相比Emit没法做一些细节操做,这点会在后面Emit讲解到。
逻辑 :
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<T> Query<T>(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 static Func<DbDataReader, object> CreateMappingFunction(IDataReader reader, Type type) { //1. 取得sql select全部栏位名称 var names = Enumerable.Range(0, reader.FieldCount).Select(index => reader.GetName(index)).ToArray(); //2. 取得mapping类别的属性资料 > 将index,sql栏位,class属性资料作好对应封装在一个变量内方便后面使用 var props = type.GetProperties().ToList(); var members = names.Select((columnName, index) => { var property = props.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) ?? props.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); return new { index, columnName, property }; }); //3. 动态创建方法 : 从数据库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; } */ var exBodys = new List<Expression>(); { // 方法(IDataReader reader) var exParam = Expression.Parameter(typeof(DbDataReader), "reader"); // Mapping类别 物件 = new Mapping类别(); var exVar = Expression.Variable(type, "mappingObj"); var exNew = Expression.New(type); { exBodys.Add(Expression.Assign(exVar, exNew)); } // var value = defalut(object); var exValueVar = Expression.Variable(typeof(object), "value"); { exBodys.Add(Expression.Assign(exValueVar, Expression.Constant(null))); } var getItemMethod = typeof(DbDataReader).GetMethods().Where(w => w.Name == "get_Item") .First(w => w.GetParameters().First().ParameterType == typeof(int)); foreach (var m in members) { //reader[0] var exCall = Expression.Call( exParam, getItemMethod, Expression.Constant(m.index) ); // value = reader[0]; exBodys.Add(Expression.Assign(exValueVar, exCall)); //user.Name = (string)value; var exProp = Expression.Property(exVar, m.property.Name); var exConvert = Expression.Convert(exValueVar, m.property.PropertyType); //(string)value var exPropAssign = Expression.Assign(exProp, exConvert); //if ( !(value is System.DBNull)) // (string)value var exIfThenElse = Expression.IfThen( Expression.Not(Expression.TypeIs(exValueVar, typeof(System.DBNull))) , exPropAssign ); exBodys.Add(exIfThenElse); } // return user; exBodys.Add(exVar); // Compiler Expression var lambda = Expression.Lambda<Func<DbDataReader, object>>( Expression.Block( new[] { exVar, exValueVar }, exBodys ), exParam ); return lambda.Compile(); } } }
查询效果图 :
最后查看Expression.Lambda > DebugView(注意是非公开属性)验证代码 :
.Lambda #Lambda1<System.Func`2[System.Data.Common.DbDataReader,System.Object]>(System.Data.Common.DbDataReader $reader) { .Block( UserQuery+User $mappingObj, System.Object $value) { $mappingObj = .New UserQuery+User(); $value = null; $value = .Call $reader.get_Item(0); .If ( !($value .Is System.DBNull) ) { $mappingObj.Name = (System.String)$value } .Else { .Default(System.Void) }; $value = .Call $reader.get_Item(1); .If ( !($value .Is System.DBNull) ) { $mappingObj.Age = (System.Int32)$value } .Else { .Default(System.Void) }; $mappingObj } }
有了前面Expression版本概念后,接着能够进到Dapper底层最核心的技术 : Emit。
首先要有个概念,MSIL(CIL)目的是给JIT编译器看的,因此可读性会不好、难Debug,但比起Expression来讲能够作到更细节的逻辑操做。
在实际环境开发使用Emit,通常会先写好C#代码后 > 反编译查看IL > 使用Emit创建动态方法
,举例 :
1.首先创建一个简单打印例子 :
void SyaHello() { Console.WriteLine("Hello World"); }
2.反编译查看IL
SyaHello: IL_0000: nop IL_0001: ldstr "Hello World" IL_0006: call System.Console.WriteLine IL_000B: nop IL_000C: ret
3.使用DynamicMethod + Emit创建动态方法
void Main() { // 1. 创建 void 方法() DynamicMethod methodbuilder = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(),typeof(void),null); // 2. 创建方法Body内容,借由Emit var il = methodbuilder.GetILGenerator(); il.Emit(OpCodes.Ldstr, "Hello World"); Type[] types = new Type[1] { typeof(string) }; MethodInfo method = typeof(Console).GetMethod("WriteLine", types); il.Emit(OpCodes.Call,method); il.Emit(OpCodes.Ret); // 3. 转换指定类型的Func or Action var action = (Action)methodbuilder.CreateDelegate(typeof(Action)); action(); }
可是对已经写好的专案来讲就不是这样流程了,开发者不必定会好心的告诉你当初设计的逻辑,因此接着讨论此问题。
个人解决方式是 : 「既然只有Runtime才能知道IL,那么将IL保存成静态档案再反编译查看」
这边可使用MethodBuild + Save
方法将IL保存成静态exe档案 > 反编译查看
,但须要特别注意
region if 指定版本
来作区分,不然不能使用,如图片代码以下 :
//使用MethodBuilder查看别人已经写好的Emit IL //1. 创建MethodBuilder AppDomain ad = AppDomain.CurrentDomain; AssemblyName am = new AssemblyName(); am.Name = "TestAsm"; AssemblyBuilder ab = ad.DefineDynamicAssembly(am, AssemblyBuilderAccess.Save); ModuleBuilder mb = ab.DefineDynamicModule("Testmod", "TestAsm.exe"); TypeBuilder tb = mb.DefineType("TestType", TypeAttributes.Public); MethodBuilder dm = tb.DefineMethod("TestMeThod", MethodAttributes.Public | MethodAttributes.Static, type, new[] { typeof(IDataReader) }); ab.SetEntryPoint(dm); // 2. 填入IL代码 //..略 // 3. 生成静态档案 tb.CreateType(); ab.Save("TestAsm.exe");
接着使用此方式在GetTypeDeserializerImpl方法反编译Dapper Query Mapping IL,能够得出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]; if (!(obj is DBNull)) { user2.Name = (string)obj; } index = 1; object obj2 = value = P_0[1]; if (!(obj2 is DBNull)) { user2.Age = (int)obj2; } user = user2; return user; } catch (Exception ex) { SqlMapper.ThrowDataException(ex, index, P_0, value); return user; } }
有了C#代码后再来了解Emit逻辑会快不少,接着就能够进到Emit版本Query实做部分。
如下代码是Emit版本,我把C#对应IL部分都写在注解。
public static class DemoExtension { public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql) where T : new() { using (var command = cnn.CreateCommand()) { command.CommandText = sql; using (var reader = command.ExecuteReader()) { var func = GetTypeDeserializerImpl(typeof(T), reader); while (reader.Read()) { var result = func(reader as DbDataReader); yield return result is T ? (T)result : default(T); } } } } private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false) { var returnType = type.IsValueType ? typeof(object) : type; var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(IDataReader) }, type, true); var il = dm.GetILGenerator(); //C# : User user = new User(); //IL : //IL_0001: newobj //IL_0006: stloc.0 var constructor = returnType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0]; //这边简化成只会有预设constructor il.Emit(OpCodes.Newobj, constructor); var returnValueLocal = il.DeclareLocal(type); il.Emit(OpCodes.Stloc, returnValueLocal); //User user = new User(); // C# : //object value = default(object); // IL : //IL_0007: ldnull //IL_0008: stloc.1 // value var valueLoacl = il.DeclareLocal(typeof(object)); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Stloc, valueLoacl); int index = startBound; var getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int)) .Select(p => p.GetGetMethod()).First(); foreach (var p in type.GetProperties()) { //C# : value = P_0[0]; //IL: //IL_0009: ldarg.0 //IL_000A: ldc.i4.0 //IL_000B: callvirt System.Data.IDataRecord.get_Item //IL_0010: stloc.1 // value il.Emit(OpCodes.Ldarg_0); //取得reader参数 EmitInt32(il, index); il.Emit(OpCodes.Callvirt, getItem); il.Emit(OpCodes.Stloc, valueLoacl); //C#: if (!(value is DBNull)) user.Name = (string)value; //IL: // IL_0011: ldloc.1 // value // IL_0012: isinst System.DBNull // IL_0017: ldnull // IL_0018: cgt.un // IL_001A: ldc.i4.0 // IL_001B: ceq // IL_001D: stloc.2 // IL_001E: ldloc.2 // IL_001F: brfalse.s IL_002E // IL_0021: ldloc.0 // user // IL_0022: ldloc.1 // value // IL_0023: castclass System.String // IL_0028: callvirt UserQuery+User.set_Name il.Emit(OpCodes.Ldloc, valueLoacl); il.Emit(OpCodes.Isinst, typeof(System.DBNull)); il.Emit(OpCodes.Ldnull); var tmpLoacl = il.DeclareLocal(typeof(int)); il.Emit(OpCodes.Cgt_Un); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ceq); il.Emit(OpCodes.Stloc,tmpLoacl); il.Emit(OpCodes.Ldloc,tmpLoacl); var labelFalse = il.DefineLabel(); il.Emit(OpCodes.Brfalse_S,labelFalse); il.Emit(OpCodes.Ldloc, returnValueLocal); il.Emit(OpCodes.Ldloc, valueLoacl); if (p.PropertyType.IsValueType) il.Emit(OpCodes.Unbox_Any, p.PropertyType); else il.Emit(OpCodes.Castclass, p.PropertyType); il.Emit(OpCodes.Callvirt, p.SetMethod); il.MarkLabel(labelFalse); index++; } // IL_0053: ldloc.0 // user // IL_0054: stloc.s 04 //不须要 // IL_0056: br.s IL_0058 // IL_0058: ldloc.s 04 //不须要 // IL_005A: ret il.Emit(OpCodes.Ldloc, returnValueLocal); il.Emit(OpCodes.Ret); var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(IDataReader), returnType); return (Func<IDataReader, object>)dm.CreateDelegate(funcType); } private static void EmitInt32(ILGenerator il, int value) { switch (value) { case -1: il.Emit(OpCodes.Ldc_I4_M1); break; case 0: il.Emit(OpCodes.Ldc_I4_0); break; case 1: il.Emit(OpCodes.Ldc_I4_1); break; case 2: il.Emit(OpCodes.Ldc_I4_2); break; case 3: il.Emit(OpCodes.Ldc_I4_3); break; case 4: il.Emit(OpCodes.Ldc_I4_4); break; case 5: il.Emit(OpCodes.Ldc_I4_5); break; case 6: il.Emit(OpCodes.Ldc_I4_6); break; case 7: il.Emit(OpCodes.Ldc_I4_7); break; case 8: il.Emit(OpCodes.Ldc_I4_8); break; default: if (value >= -128 && value <= 127) { il.Emit(OpCodes.Ldc_I4_S, (sbyte)value); } else { il.Emit(OpCodes.Ldc_I4, value); } break; } } }
这边Emit的细节概念很是的多,这边没法所有都讲解,先挑出重要概念讲解
在Emit if/else须要使用Label定位,告知编译器条件为true/false时要跳到哪一个位子,举例 : 「boolean转整数」,假设要简单将Boolean转换成Int,C#代码能够用「若是是True返回1不然返回0」逻辑来写:
public static int BoolToInt(bool input) => input ? 1 : 0;
当转成Emit写法的时候,须要如下逻辑 :
(注意,这时候Label位子还没肯定)
符合条件
要运行区块的前一行
,使用MarkLabel方法标记Label的位子
。最后写出的C# Emit代码 :
public class Program { public static void Main(string[] args) { var func = CreateFunc(); Console.WriteLine(func(true)); //1 Console.WriteLine(func(false)); //0 } static Func<bool, int> CreateFunc() { var dm = new DynamicMethod("Test" + Guid.NewGuid().ToString(), typeof(int), new[] { typeof(bool) }); var il = dm.GetILGenerator(); var labelTrue = il.DefineLabel(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Brtrue_S, labelTrue); il.Emit(OpCodes.Ldc_I4_0); il.Emit(OpCodes.Ret); il.MarkLabel(labelTrue); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Ret); var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(bool), typeof(int)); return (Func<bool, int>)dm.CreateDelegate(funcType); } }
这边能够发现Emit版本
优势 :
缺点 :
接着来看Dapper做者的建议,如今通常专案当中没有必要使用Emit,使用Expression + Func/Action已经能够解决大部分动态方法的需求,尤为是Expression支援Block等方法状况。连结 c# - What's faster: expression trees or manually emitting IL
话虽如此,但有一些厉害的开源专案就是使用Emit管理细节,若是想看懂它们,就须要基础的Emit IL概念
。
前面介绍到动态使用 Emit IL 创建 ADO.NET Mapping 方法,但单就这功能没法让 Dapper 被称为轻量ORM效率之王。
由于动态创建方法是须要成本、并耗费时间
的动做,单纯使用反而会拖慢速度。但当配合 Cache 后就不同,将创建好的方法保存在 Cache 内,能够用『空间换取时间』
概念加快查询的效率,也就是俗称查表法
。
接着追踪Dapper源码,此次须要特别关注的是QueryImpl方法下的Identity、GetCacheInfo
Identity主要封装各缓存的比较Key属性 :
接着搭配GetCacheInfo方法内Dapper使用的缓存类别ConcurrentDictionary<Identity, CacheInfo>
,使用TryGetValue
方法时会去先比对HashCode接着比对Equals特性,如图片源码。
将Key类别Identity借由override Equals
方法实现缓存比较算法,能够看到如下Dapper实做逻辑,只要一个属性不同就会创建一个新的动态方法、缓存。
public bool Equals(Identity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(other, null)) return false; int typeCount; return gridIndex == other.gridIndex && type == other.type && sql == other.sql && commandType == other.commandType && connectionStringComparer.Equals(connectionString, other.connectionString) && parametersType == other.parametersType && (typeCount = TypeCount) == other.TypeCount && (typeCount == 0 || TypesEqual(this, other, typeCount)); }
以此概念拿以前Emit版本修改为一个简单Cache Demo让读者感觉:
public class Identity { public string sql { get; set; } public CommandType? commandType { get; set; } public string connectionString { get; set; } public Type type { get; set; } public Type parametersType { get; set; } public Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType) { this.sql = sql; this.commandType = commandType; this.connectionString = connectionString; this.type = type; this.parametersType = parametersType; unchecked { hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this hashCode = (hashCode * 23) + commandType.GetHashCode(); hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0); hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0); hashCode = (hashCode * 23) + (connectionString == null ? 0 : StringComparer.Ordinal.GetHashCode(connectionString)); hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0); } } public readonly int hashCode; public override int GetHashCode() => hashCode; public override bool Equals(object obj) => Equals(obj as Identity); public bool Equals(Identity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(other, null)) return false; return type == other.type && sql == other.sql && commandType == other.commandType && StringComparer.Ordinal.Equals(connectionString, other.connectionString) && parametersType == other.parametersType; } } public static class DemoExtension { private static readonly Dictionary<Identity, Func<DbDataReader, object>> readers = new Dictionary<Identity, Func<DbDataReader, object>>(); public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql,object param=null) where T : new() { using (var command = cnn.CreateCommand()) { command.CommandText = sql; using (var reader = command.ExecuteReader()) { var identity = new Identity(command.CommandText, command.CommandType, cnn.ConnectionString, typeof(T), param?.GetType()); // 2. 若是cache有资料就使用,没有资料就动态创建方法并保存在缓存内 if (!readers.TryGetValue(identity, out Func<DbDataReader, object> func)) { //动态创建方法 func = GetTypeDeserializerImpl(typeof(T), reader); readers[identity] = func; Console.WriteLine("没有缓存,创建动态方法放进缓存"); }else{ Console.WriteLine("使用缓存"); } // 3. 呼叫生成的方法by reader,读取资料回传 while (reader.Read()) { var result = func(reader as DbDataReader); yield return result is T ? (T)result : default(T); } } } } private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false) { //..略 } }
效果图 :
了解实做逻辑后,接着延伸一个Dapper使用的重要观念,SQL字串
为缓存重要Key值之一,假如不一样的SQL字串,Dapper会为此创建新的动态方法、缓存,因此使用不当状况下就算使用StringBuilder也会形成效率慢、内存泄漏问题
。
至于为什么要以SQL字串当其中一个关键Key,而不是单纯使用Mapping类别的Handle,其中缘由之一是跟查询栏位顺序
有关,在前面有讲到,Dapper使用「结果反推程式码」
方式创建动态方法,表明说顺序跟资料都必需要是固定
的,避免SQL Select栏位顺序不同又使用同一组动态方法,会有A栏位值给B属性
错值大问题。
最直接解决方式,对每一个不一样SQL字串创建不一样的动态方法,并保存在不一样的缓存。
举例,如下代码只是简单的查询动做,查看Dapper Cache数量却达到999999个,如Gif动画显示
using (var cn = new SqlConnection(@"connectionString")) { for (int i = 0; i < 999999; i++) { var guid = Guid.NewGuid(); for (int i2 = 0; i2 < 2; i2++) { var result = cn.Query<User>($"select '{guid}' ").First(); } } }
要避免此问题,只须要保持一个原则重复利用SQL字串
,而最简单方式就是参数化
, 举例 : 将上述代码改为如下代码,缓存数量降为1
,达到重复利用目的 :
using (var cn = new SqlConnection(@"connectionString")) { for (int i = 0; i < 999999; i++) { var guid = Guid.NewGuid(); for (int i2 = 0; i2 < 2; i2++) { var result = cn.Query<User>($"select @guid ",new { guid}).First(); } } }
假如遇到必要拼接SQL字串需求的状况下,举例 : 有时候值使用字串拼接会比不使用参数化效率好,特别是该栏位值只会有几种固定值
。
这时候Dapper可使用Literal Replacements
功能,使用方式 : 将要拼接的值字串以{=属性名称}
取代,并将值保存在Parameter参数内,举例 :
void Main() { using (var cn = Connection) { var result = cn.Query("select N'暐翰' Name,26 Age,{=VipLevel} VipLevel", new User{ VipLevel = 1}).First(); } }
首先追踪源码GetCacheInfo下GetLiteralTokens方法,能够发现Dapper在创建缓存以前
会抓取SQL字串
内符合{=变量名称}
规格的资料。
private static readonly Regex literalTokens = new Regex(@"(?<![\p{L}\p{N}_])\{=([\p{L}\p{N}_]+)\}", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); internal static IList<LiteralToken> GetLiteralTokens(string sql) { if (string.IsNullOrEmpty(sql)) return LiteralToken.None; if (!literalTokens.IsMatch(sql)) return LiteralToken.None; var matches = literalTokens.Matches(sql); var found = new HashSet<string>(StringComparer.Ordinal); List<LiteralToken> list = new List<LiteralToken>(matches.Count); foreach (Match match in matches) { string token = match.Value; if (found.Add(match.Value)) { list.Add(new LiteralToken(token, match.Groups[1].Value)); } } return list.Count == 0 ? LiteralToken.None : list; }
接着在CreateParamInfoGenerator方法生成Parameter参数化动态方法,此段方法IL以下 :
IL_0000: ldarg.1 IL_0001: castclass <>f__AnonymousType1`1[System.Int32] IL_0006: stloc.0 IL_0007: ldarg.0 IL_0008: callvirt System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand IL_000d: pop IL_000e: ldarg.0 IL_000f: ldarg.0 IL_0010: callvirt System.String get_CommandText()/System.Data.IDbCommand IL_0015: ldstr "{=VipLevel}" IL_001a: ldloc.0 IL_001b: callvirt Int32 get_VipLevel()/<>f__AnonymousType1`1[System.Int32] IL_0020: stloc.1 IL_0021: ldloca.s V_1 IL_0023: call System.Globalization.CultureInfo get_InvariantCulture()/System.Globalization.CultureInfo IL_0028: call System.String ToString(System.IFormatProvider)/System.Int32 IL_002d: callvirt System.String Replace(System.String, System.String)/System.String IL_0032: callvirt Void set_CommandText(System.String)/System.Data.IDbCommand IL_0037: ret
接着再生成Mapping动态方法,要了解此段逻辑我这边作一个模拟例子方便读者理解 :
public static class DbExtension { public static IEnumerable<User> Query(this DbConnection cnn, string sql, User parameter) { using (var command = cnn.CreateCommand()) { command.CommandText = sql; CommandLiteralReplace(command, parameter); using (var reader = command.ExecuteReader()) while (reader.Read()) yield return Mapping(reader); } } private static void CommandLiteralReplace(IDbCommand cmd, User parameter) { cmd.CommandText = cmd.CommandText.Replace("{=VipLevel}", parameter.VipLevel.ToString(System.Globalization.CultureInfo.InvariantCulture)); } private static User Mapping(IDataReader reader) { var user = new User(); var value = default(object); value = reader[0]; if(!(value is System.DBNull)) user.Name = (string)value; value = reader[1]; if (!(value is System.DBNull)) user.Age = (int)value; value = reader[2]; if (!(value is System.DBNull)) user.VipLevel = (int)value; return user; } }
看完以上例子,能够发现Dapper Literal Replacements底层原理就是字串取代
,一样属于字串拼接方式,为什么能够避免缓存问题?
这是由于取代的时机点在SetParameter动态方法内,因此Cache的SQL Key是没有变更过的
,能够重复利用一样的SQL字串、缓存。
也由于是字串取代方式,因此只支持基本Value类别
,假如使用String类别系统会告知The type String is not supported for SQL literals.
,避免SQL Injection问题。
接着讲解Dapper Multi Mapping
(多对应)实做跟底层逻辑,毕竟工做当中不可能都是一对一律念。
使用方式 :
Query<Func逻辑>(SQL,Parameter,Mapping逻辑Func)
Query<Func第一个类别,Func第二个类别,..以此类推,Func最后返回类别>
(最多支持六组泛型参数)ID
,假如不同须要特别指定 (这段后面特别讲解)由左至右
举例 : 有订单(Order)跟会员(User)表格,关系是一对多关系,一个会员能够有多个订单,如下是C# Demo代码 :
void Main() { 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]([ID] int, [Name] nvarchar(10)); INSERT INTO [User]([ID], [Name])VALUES(1, N'大雄'),(2, N'小明'); CREATE TABLE [Order]([ID] int, [OrderNo] varchar(13), [UserID] int); INSERT INTO [Order]([ID], [OrderNo], [UserID])VALUES(1, 'SO20190900001', 1),(2, 'SO20190900002', 1),(3, 'SO20190900003', 2),(4, 'SO20190900004', 2); "); var result = cn.Query<Order,User,Order>(@" select * from [order] T1 left join [User] T2 on T1.UserId = T2.ID ", (order, user) => { order.User = user; return order; } ); ts.Dispose(); } } public class Order { public int ID { get; set; } public string OrderNo { get; set; } public User User { get; set; } } public class User { public int ID { get; set; } public string Name { get; set; } }
在初期常变更表格结构或是一次性功能不想宣告Class,Dapper Multi Mapping也支援dynamic方式
void Main() { using (var ts = new TransactionScope()) using (var connection = Connection) { const string createSql = @" create table Users (Id int, Name nvarchar(20)) create table Posts (Id int, OwnerId int, Content nvarchar(20)) insert Users values(1, N'小明') insert Users values(2, N'小智') insert Posts values(101, 1, N'小明第1天日记') insert Posts values(102, 1, N'小明第2天日记') insert Posts values(103, 2, N'小智第1天日记') "; connection.Execute(createSql); const string sql = @"select * from Posts p left join Users u on u.Id = p.OwnerId Order by p.Id "; var data = connection.Query<dynamic, dynamic, dynamic>(sql, (post, user) => { post.Owner = user; return post; }).ToList(); } }
Split预设是用来切割主键,因此预设切割字串是Id
,假如当表格结构PK名称为Id
能够省略参数,举例
var result = cn.Query<Order,User,Order>(@" select * from [order] T1 left join [User] T2 on T1.UserId = T2.ID ", (order, user) => { order.User = user; return order; } );
假如主键名称是其余名称,请指定splitOn字串名称
,而且对应多个可使用,
作区隔,举例,添加商品表格作Join :
var result = cn.Query<Order,User,Item,Order>(@" select * from [order] T1 left join [User] T2 on T1.UserId = T2.ID left join [Item] T3 on T1.ItemId = T3.ID " ,map : (order, user,item) => { order.User = user; order.Item = item; return order; } ,splitOn : "Id,Id" );
这边先以一个简单Demo带读者了解Dapper Multi Mapping 概念
public static class MutipleMappingDemo { public static IEnumerable<TReturn> Query<T1, T2, TReturn>(this IDbConnection connection, string sql, Func<T1, T2, TReturn> map) where T1 : Order, new() where T2 : User, new() //这两段where单纯为了Demo方便 { //1. 按照泛型类别参数数量创建对应数量的Mapping Func集合 var deserializers = new List<Func<IDataReader, object>>(); { //2. Mapping Func创建逻辑跟Query Emit IL同样 deserializers.Add((reader) => { var newObj = new T1(); var value = default(object); value = reader[0]; newObj.ID = value is DBNull ? 0 : (int)value; value = reader[1]; newObj.OrderNo = value is DBNull ? null : (string)value; return newObj; }); deserializers.Add((reader) => { var newObj = new T2(); var value = default(object); value = reader[2]; newObj.ID = value is DBNull ? 0 : (int)value; value = reader[4]; newObj.Name = value is DBNull ? null : (string)value; return newObj; }); } using (var command = connection.CreateCommand()) { command.CommandText = sql; using (var reader = command.ExecuteReader()) { while (reader.Read()) { //3. 呼叫使用者的Custom Mapping Func,其中参数由前面动态生成的Mapping Func而来 yield return map(deserializers[0](reader) as T1, deserializers[1](reader) as T2); } } } } }
以上概念就是此方法的主要逻辑,接着讲其余细节部分
Dapper为了强型别多类别Mapping
使用多组泛型参数方法
方式,这方式有个小缺点就是没办法动态调整
,须要以写死方式来处理。
举例,能够看到图片GenerateMapper方法,依照泛型参数数量,写死强转型逻辑,这也是为什么Multiple Query有最大组数限制,只能支持最多6组的缘由。
泛型类别
来强型别
保存多类别的资料override
的GetType方法,来客制泛型比较逻辑,避免形成跟Non Multi Query缓存冲突
。
由于SplitOn分组基础依赖于Select的顺序
,因此顺序一错就有可能属性值错乱
状况。
举例 : 假如上面例子的SQL改为如下,会发生User的ID变成Order的ID;Order的ID会变成User的ID。
select T2.[ID],T1.[OrderNo],T1.[UserID],T1.[ID],T2.[Name] from [order] T1 left join [User] T2 on T1.UserId = T2.ID
缘由能够追究到Dapper的切割算法
倒序
方式处理栏位分组(GetNextSplit方法能够看到从DataReader Index大到小
查询)倒序
方式处理类别的Mapping Emit IL Func正序
,方便后面Call Func对应泛型使用
使用方式例子 :
using (var cn = Connection) { using (var gridReader = cn.QueryMultiple("select 1; select 2;")) { Console.WriteLine(gridReader.Read<int>()); //result : 1 Console.WriteLine(gridReader.Read<int>()); //result : 2 } }
使用QueryMultiple优势 :
减小Reqeust次数
共用同一组Parameter参数
QueryMultiple的底层实做逻辑 :
DataReader NextResult
取得下一组查询结果没有
下一组查询结果才会将DataReader释放
缓存的算法多增长gridIndex判断,主要对每一个result mapping动做作一个缓存,Emit IL的逻辑跟Query同样。
注意Read方法使用的是buffer = true = 返回结果直接ToList保存在内存,因此没有延迟查询特性。
Dapper 呼叫QueryMultiple方法时会将DataReader封装在GridReader物件内,只有当最后一次Read
动做后才会回收DataReader
因此没有读取完
再开一个GridReader > Read会出现错误:已经开启一个与这个 Command 相关的 DataReader,必须先将它关闭
。
要避免以上状况,能够改为using
区块方式,运行完区块代码后就会自动释放DataReader
using (var gridReader = cn.QueryMultiple("select 1; select 2;")) { //略.. }
感受Dapper GridReader好像有机会能够实做是否有NextResult
方法,这样就能够配合while
方法一次读取完多组查询资料
,等以后有空来想一想有没有机会作成。
概念代码 :
public static class DbExtension { public static IEnumerable<IEnumerable<dynamic>> GetMultipleResult(this IDbConnection cn,string sql, object paramters) { using (var reader = cn.QueryMultiple(sql,paramters)) { while(reader.NextResult()) { yield return reader.Read(); } } } }
遇到想要客制某些属性Mapping逻辑时,在Dapper可使用TypeHandler
使用方式 :
SqlMapper.TypeHandler
泛型
,e.g : JsonTypeHandler<客制类别> : SqlMapper.TypeHandler<客制类别>
查询
的逻辑使用override实做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; } }
效果图 :
接着追踪TypeHandler源码逻辑,须要分两个部份来追踪 : SetValue,Parse
if (handler != null) { il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache<int>.SetValue))); // stack is now [parameters] [[parameters]] [parameter] }
最后查看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,这样有如下优势 :
避免重复创建物件
提高效率
主要逻辑是在GenerateDeserializerFromMap方法Emit创建动态Mapping方法时,假如判断TypeHandler缓存有资料,以Parse方法取代本来的Set属性动做。
查看动态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如何在底层利用CommandBehavior优化查询效率,如何选择正确Behavior在特定时机。
我这边整理了各方法对应的Behavior表格 :
方法 | Behavior |
---|---|
Query | CommandBehavior.SequentialAccess & CommandBehavior.SingleResult |
QueryFirst | CommandBehavior.SequentialAccess & CommandBehavior.SingleResult & CommandBehavior.SingleRow |
QueryFirstOrDefault | CommandBehavior.SequentialAccess & CommandBehavior.SingleResult & CommandBehavior.SingleRow |
QuerySingle | CommandBehavior.SingleResult & CommandBehavior.SequentialAccess |
QuerySingleOrDefault | CommandBehavior.SingleResult & CommandBehavior.SequentialAccess |
QueryMultiple | CommandBehavior.SequentialAccess |
首先能够看到每一个方法都使用CommandBehavior.SequentialAccess
,该标签主要功能 使DataReader顺序读取行和列,行和列不缓冲,读取一列后,它会从内存中删除。
,有如下优势 :
避免二进制大资源一次性读取到内存
,尤为是Blob或是Clob会配合GetBytes 或 GetChars 方法限制缓冲区大小,微软官方也特别标注注意 :加快查询效率
但它却不是
DataReader的预设行为,系统预设是CommandBehavior.Default
CommandBehavior.Default有着如下特性 :
多个
结果集(Multi Result)这两个特性跟生产环境状况差满多,毕竟大多时刻是只须要一组结果集配合有限的内存
,因此除了SequentialAccess外Dapper还特别在大多方法使用了CommandBehavior.SingleResult
,知足只需一组结果就好避免浪费资源。
这段还有一段细节的处理,查看源码能够发现除了标记SingleResult外,Dapper还特别加上一段代码在结尾while (reader.NextResult()){}
,而不是直接Return(如图片)
早些前我有特别发Issue(连结#1210)询问过做者,这边是回答 : 主要避免忽略错误,像是在DataReader提前关闭状况
有时候咱们会遇到select top 1
知道只会读取一行资料的状况,这时候可使用QueryFirst
。它使用CommandBehavior.SingleRow
能够避免浪费资源只读取一行资料。
另外能够发现此段除了while (reader.NextResult()){}
外还有while (reader.Read()) {}
,一样是避免忽略错误,这是一些公司自行土炮ORM会忽略的地方。
二者差异在QuerySingle没有使用CommandBehavior.SingleRow,至于为什么没有使用,是由于须要有多行资料才能判断是否不符合条件并抛出Exception告知使用者
。
这段有一个特别好玩小技巧能够学,错误处理直接沿用对应LINQ的Exception,举例:超过一行资料错误,使用new int[2].Single()
,这样不用另外维护Exceptiono类别,还能够拥有i18N多国语言化。
接着进到Dapper的另外一个关键功能 : 「Parameter 参数化」
主要逻辑 :
GetCacheInfo检查是否缓存内有动态方法 > 假如没有缓存,使用CreateParamInfoGenerator方法Emit IL创建AddParameter动态方法 > 创建完后保存在缓存内
接着重点来看CreateParamInfoGenerator方法内的底成逻辑跟「精美细节处理」,使用告终果反推代码方法,忽略「没使用的栏位」
不生成对应IL代码,避免资源浪费状况。这也是前面缓存算法要去判断不一样SQL字串的缘由。
如下是我挑出的源码重点部分 :
internal static Action<IDbCommand, object> CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList<LiteralToken> literals) { //...略 if (filterParams) { props = FilterParameters(props, identity.sql); } var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt; foreach (var prop in props) { //Emit IL动做 } //...略 } private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyInfo> parameters, string sql) { var list = new List<PropertyInfo>(16); foreach (var p in parameters) { if (Regex.IsMatch(sql, @"[?@:]" + p.Name + @"([^\p{L}\p{N}_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)) list.Add(p); } return list; }
接着查看IL来验证,查询代码以下
var result = connection.Query("select @Name name ", new { Name = "暐翰", Age = 26}).First();
CreateParamInfoGenerator AddParameter 动态方法IL代码以下 :
IL_0000: ldarg.1 IL_0001: castclass <>f__AnonymousType1`2[System.String,System.Int32] IL_0006: stloc.0 IL_0007: ldarg.0 IL_0008: callvirt System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand IL_000d: dup IL_000e: ldarg.0 IL_000f: callvirt System.Data.IDbDataParameter CreateParameter()/System.Data.IDbCommand IL_0014: dup IL_0015: ldstr "Name" IL_001a: callvirt Void set_ParameterName(System.String)/System.Data.IDataParameter IL_001f: dup IL_0020: ldc.i4.s 16 IL_0022: callvirt Void set_DbType(System.Data.DbType)/System.Data.IDataParameter IL_0027: dup IL_0028: ldc.i4.1 IL_0029: callvirt Void set_Direction(System.Data.ParameterDirection)/System.Data.IDataParameter IL_002e: dup IL_002f: ldloc.0 IL_0030: callvirt System.String get_Name()/<>f__AnonymousType1`2[System.String,System.Int32] IL_0035: dup IL_0036: brtrue.s IL_0042 IL_0038: pop IL_0039: ldsfld System.DBNull Value/System.DBNull IL_003e: ldc.i4.0 IL_003f: stloc.1 IL_0040: br.s IL_005a IL_0042: dup IL_0043: callvirt Int32 get_Length()/System.String IL_0048: ldc.i4 4000 IL_004d: cgt IL_004f: brtrue.s IL_0058 IL_0051: ldc.i4 4000 IL_0056: br.s IL_0059 IL_0058: ldc.i4.m1 IL_0059: stloc.1 IL_005a: callvirt Void set_Value(System.Object)/System.Data.IDataParameter IL_005f: ldloc.1 IL_0060: brfalse.s IL_0069 IL_0062: dup IL_0063: ldloc.1 IL_0064: callvirt Void set_Size(Int32)/System.Data.IDbDataParameter IL_0069: callvirt Int32 Add(System.Object)/System.Collections.IList IL_006e: pop IL_006f: pop IL_0070: ret
IL转成对应C#代码:
public class TestType { public static void TestMeThod(IDataReader P_0, object P_1) { var anon = (<>f__AnonymousType1<string, int>)P_1; IDataParameterCollection parameters = ((IDbCommand)P_0).Parameters; IDbDataParameter dbDataParameter = ((IDbCommand)P_0).CreateParameter(); dbDataParameter.ParameterName = "Name"; dbDataParameter.DbType = DbType.String; dbDataParameter.Direction = ParameterDirection.Input; object obj = anon.Name; int num; if (obj == null) { obj = DBNull.Value; num = 0; } else { num = ((((string)obj).Length > 4000) ? (-1) : 4000); } dbDataParameter.Value = obj; if (num != 0) { dbDataParameter.Size = num; } parameters.Add(dbDataParameter); } }
能够发现虽然传递Age参数,可是SQL字串没有用到,Dapper不会去生成该栏位的SetParameter动做IL。这个细节处理真的要给Dapper一个赞!
IN 参数化
,Dapper支援 ?原理
([?@:]参数名)(?!\w)(\s+(?i)unknown(?-i))?
)()
+ 多个属性名称+流水号
方式替换关键程式部分
如下用sys.objects来查SQL Server的表格跟视图当追踪例子 :
var result = cn.Query(@"select * from sys.objects where type_desc In @type_descs", new { type_descs = new[] { "USER_TABLE", "VIEW" } });
Dapper会将SQL字串改为如下方式执行
select * from sys.objects where type_desc In (@type_descs1,@type_descs2) -- @type_descs1 = nvarchar(4000) - 'USER_TABLE' -- @type_descs2 = nvarchar(4000) - 'VIEW'
查看Emit IL能够发现跟以前的参数化IL很不同,很是的简短
IL_0000: ldarg.1 IL_0001: castclass <>f__AnonymousType0`1[System.String[]] IL_0006: stloc.0 IL_0007: ldarg.0 IL_0008: callvirt System.Data.IDataParameterCollection get_Parameters()/System.Data.IDbCommand IL_000d: ldarg.0 IL_000e: ldstr "type_descs" IL_0013: ldloc.0 IL_0014: callvirt System.String[] get_type_descs()/<>f__AnonymousType0`1[System.String[]] IL_0019: call Void PackListParameters(System.Data.IDbCommand, System.String, System.Object)/Dapper.SqlMapper IL_001e: pop IL_001f: ret
转成C#代码来看,会很惊讶地发现:「这段根本不须要使用Emit IL简直画蛇添足」
public static void TestMeThod(IDbCommand P_0, object P_1) { var anon = (<>f__AnonymousType0<string[]>)P_1; IDataParameterCollection parameter = P_0.Parameters; SqlMapper.PackListParameters(P_0, "type_descs", anon.type_descs); }
没错,是画蛇添足,甚至IDataParameterCollection parameter = P_0.Parameters;
这段代码根本不会用到。
Dapper这边作法是有缘由的,由于要能跟非集合参数配合使用
,像是前面例子加上找出订单Orders名称的资料逻辑
var result = cn.Query(@"select * from sys.objects where type_desc In @type_descs and name like @name" , new { type_descs = new[] { "USER_TABLE", "VIEW" }, @name = "order%" });
对应生成的IL转换C#代码就会是如下代码,达到能搭配使用目的 :
public static void TestMeThod(IDbCommand P_0, object P_1) { <>f__AnonymousType0<string[], string> val = P_1; IDataParameterCollection parameters = P_0.Parameters; SqlMapper.PackListParameters(P_0, "type_descs", val.get_type_descs()); IDbDataParameter dbDataParameter = P_0.CreateParameter(); dbDataParameter.ParameterName = "name"; dbDataParameter.DbType = DbType.String; dbDataParameter.Direction = ParameterDirection.Input; object obj = val.get_name(); int num; if (obj == null) { obj = DBNull.Value; num = 0; } else { num = ((((string)obj).Length > 4000) ? (-1) : 4000); } dbDataParameter.Value = obj; if (num != 0) { dbDataParameter.Size = num; } parameters.Add(dbDataParameter); }
另外为什么Dapper这边Emit IL会直接呼叫工具方法PackListParameters
,是由于IN的参数化数量是不固定
,因此不能由固定结果反推程式码
方式动态生成方法。
该方法里面包含的主要逻辑:
SQL参数字串的取代逻辑也写在这边,如图片
这边用个例子带读者了解DynamicParameter原理,举例如今有一段代码以下 :
using (var cn = Connection) { var paramter = new { Name = "John", Age = 25 }; var result = cn.Query("select @Name Name,@Age Age", paramter).First(); }
前面已经知道String型态Dapper会自动将转成数据库Nvarchar
而且长度为4000
的参数,数据库实际执行的SQL以下 :
exec sp_executesql N'select @Name Name,@Age Age',N'@Name nvarchar(4000),@Age int',@Name=N'John',@Age=25
这是一个方便快速开发的贴心设计,但假如遇到栏位是varchar
型态的状况,有可能会由于隐性转型致使索引失效
,致使查询效率变低。
这时解决方式可使用Dapper DynamicParamter指定数据库型态跟大小,达到优化效能目的
using (var cn = Connection) { var paramters = new DynamicParameters(); paramters.Add("Name","John",DbType.AnsiString,size:4); paramters.Add("Age",25,DbType.Int32); var result = cn.Query("select @Name Name,@Age Age", paramters).First(); }
接着往底层来看如何实现,首先关注GetCacheInfo方法,能够看到DynamicParameters创建动态方法方式代码很简单,就只是呼叫AddParameters方法
Action<IDbCommand, object> reader; if (exampleParameters is IDynamicParameters) { reader = (cmd, obj) => ((IDynamicParameters)obj).AddParameters(cmd, identity); }
代码能够这么简单的缘由,是Dapper在这边特别使用「依赖于介面」
设计,增长程式的弹性
,让使用者能够客制本身想要的实做逻辑。这点下面会讲解,首先来看Dapper预设的实做类别DynamicParameters
中AddParameters
方法的实做逻辑
public class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper.IParameterLookup, SqlMapper.IParameterCallbacks { protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { var literals = SqlMapper.GetLiteralTokens(identity.sql); foreach (var param in parameters.Values) { if (param.CameFromTemplate) continue; var dbType = param.DbType; var val = param.Value; string name = Clean(param.Name); var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter; SqlMapper.ITypeHandler handler = null; if (dbType == null && val != null && !isCustomQueryParameter) { #pragma warning disable 618 dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler); #pragma warning disable 618 } if (isCustomQueryParameter) { ((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name); } else if (dbType == EnumerableMultiParameter) { #pragma warning disable 612, 618 SqlMapper.PackListParameters(command, name, val); #pragma warning restore 612, 618 } else { bool add = !command.Parameters.Contains(name); IDbDataParameter p; if (add) { p = command.CreateParameter(); p.ParameterName = name; } else { p = (IDbDataParameter)command.Parameters[name]; } p.Direction = param.ParameterDirection; if (handler == null) { #pragma warning disable 0618 p.Value = SqlMapper.SanitizeParameterValue(val); #pragma warning restore 0618 if (dbType != null && p.DbType != dbType) { p.DbType = dbType.Value; } var s = val as string; if (s?.Length <= DbString.DefaultLength) { p.Size = DbString.DefaultLength; } if (param.Size != null) p.Size = param.Size.Value; if (param.Precision != null) p.Precision = param.Precision.Value; if (param.Scale != null) p.Scale = param.Scale.Value; } else { if (dbType != null) p.DbType = dbType.Value; if (param.Size != null) p.Size = param.Size.Value; if (param.Precision != null) p.Precision = param.Precision.Value; if (param.Scale != null) p.Scale = param.Scale.Value; handler.SetValue(p, val ?? DBNull.Value); } if (add) { command.Parameters.Add(p); } param.AttachedParam = p; } } // note: most non-priveleged implementations would use: this.ReplaceLiterals(command); if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals); } }
能够发现Dapper在AddParameters为了方便性跟兼容其余功能,像是Literal Replacement、EnumerableMultiParameter功能,作了许多判断跟动做,因此代码量会比之前使用ADO.NET版本多,因此效率也会比较慢。
假若有效率苛求的需求,能够本身实做想要的逻辑,由于Dapper此段特别设计成「依赖于介面」
,只须要实做IDynamicParameters
介面就能够。
如下是我作的一个Demo,可使用ADO.NET SqlParameter创建参数跟Dapper配合
public class CustomPraameters : SqlMapper.IDynamicParameters { private SqlParameter[] parameters; public void Add(params SqlParameter[] mParameters) { parameters = mParameters; } void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) { if (parameters != null && parameters.Length > 0) foreach (var p in parameters) command.Parameters.Add(p); } }
查询、Mapping、参数讲解完后,接着讲解在增、删、改
状况Dapper咱们会使用Execute方法,其中Execute Dapper分为单次执行、屡次执行
。
以单次执行来讲Dapper Execute底层是ADO.NET的ExecuteNonQuery的封装,封装目的为了跟Dapper的Parameter、缓存
功能搭配使用,代码逻辑简洁明了这边就不作多说明,如图片
这是Dapper一个特点功能,它简化了集合操做Execute之间的操做,简化了代码,只须要 : connection.Execute("sql",集合参数);
。
至于为什么能够这么方便,如下是底层的逻辑 :
一个共同DbCommand
提供foreach迭代使用,避免重复创建浪费资源CreateParameter > 对Parameter赋值 > 使用Parameters.Add添加新建的参数
,如下是Emit IL转成的C#代码 :public static void ParamReader(IDbCommand P_0, object P_1) { var anon = (<>f__AnonymousType0<int>)P_1; IDataParameterCollection parameters = P_0.Parameters; IDbDataParameter dbDataParameter = P_0.CreateParameter(); dbDataParameter.ParameterName = "V"; dbDataParameter.DbType = DbType.Int32; dbDataParameter.Direction = ParameterDirection.Input; dbDataParameter.Value = anon.V; parameters.Add(dbDataParameter); }
foreach
该集合参数 > 除了第一次外,每次迭代清空DbCommand的Parameters > 从新呼叫同一个
动态方法添加Parameter > 送出SQL查询实做方式简洁明了,而且细节考虑共用资源避免浪费(e.g共用同一个DbCommand、Func
),但遇到大量执行追求效率需求状况,须要特别注意此方法每跑一次对数据库送出一次reqesut
,效率会被网路传输拖慢,因此这功能被称为「屡次执行」而不是「批量执行」
的主要缘由。
举例,简单Execute插入十笔资料,查看SQL Profiler能够看到系统接到10次Reqeust:
using (var cn = new SqlConnection(@"Data Source=(localdb)\MSSQLLocalDB;Integrated Security=SSPI;Initial Catalog=Northwind;")) { cn.Open(); using (var tx = cn.BeginTransaction()) { cn.Execute("create table #T (V int);", transaction: tx); cn.Execute("insert into #T (V) values (@V)", Enumerable.Range(1, 10).Select(val => new { V = val }).ToArray() , transaction:tx); var result = cn.Query("select * from #T", transaction: tx); Console.WriteLine(result); } }
ExecuteScalar由于其只能读取第一组结果、第一笔列、第一笔资料
特性,是一个常被遗忘的功能,但它在特定需求下仍是能派上用场,底下用「查询资料是否存在」例子来作说明。
假若有EF经验的读者会答使用Any
而不是Count() > 1
。
使用Count系统会帮转换SQL为 :
SELECT COUNT(*) AS [value] FROM [表格] AS [t0]
SQL Count 是一个汇总函数,会迭代符合条件的资料行判断每列该资料是否为null
,并返回其行数。
而Any语法转换SQL使用EXISTS
,它只在意是否有没有资料
,表明不用检查到每列
,只须要其中一笔有资料就有结果,因此效率快。
SELECT (CASE WHEN EXISTS( SELECT NULL AS [EMPTY] FROM [表格] AS [t0] ) THEN 1 ELSE 0 END) AS [value]
SQL Server可使用SQL格式select top 1 1 from [表格] where 条件
搭配 ExecuteScalar 方法,接着在作一个扩充方法,以下 :
public static class DemoExtension { public static bool Any(this IDbConnection cn,string sql,object paramter = null) { return cn.ExecuteScalar<bool>(sql,paramter); } }
效果图 :
使用如此简单缘由,是利用Dapper ExecuteScalar会去呼叫ExecuteScalarImpl其底层Parse逻辑
private static T ExecuteScalarImpl<T>(IDbConnection cnn, ref CommandDefinition command) { //..略 object result; //..略 result = cmd.ExecuteScalar(); //..略 return Parse<T>(result); } private static T Parse<T>(object value) { if (value == null || value is DBNull) return default(T); if (value is T) return (T)value; var type = typeof(T); //..略 return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture); }
使用 Convert.ChangeType 转成 bool : 「0=false,非0=true」
特性,让系统能够简单转型为bool值。
不要QueryFirstOrDefault代替,由于它须要在SQL额外作Null的判断,不然会出现「NullReferenceException」。
这缘由是二者Parse实做方式不同,QueryFirstOrDefault判断结果为null时直接强转型
而ExecuteScalar的Parce实做多了为空时使用default值
的判断
Dapper系列到这边,重要底层原理差很少都讲完了,这系列总共花了笔者连续25天的时间,除了想帮助读者外,最大的收获就是我本身在这期间更了解Dapper底层原理,而且学习Dapper精心的细节、框架处理。
另外想提Dapper做者之一Marc Gravell,真的很是热心,在写文章的期间有几个概念疑问,发issue询问,他都会热心、详细的回覆。而且也发现他对代码的品质要求之高,举例 : 在S.O发问,遇到他在底下留言 : 「他对目前Dapper IL的架构实际上是不满意的,甚至以为粗糙,想搭配protobuf-net技术打掉重写」
(谜之声 : 真使人敬佩 )
连结 : c# - How to remove the last few segments of Emit IL at runtime - Stack Overflow
最后笔者想说 :
写这篇的初衷,是但愿本系列能够帮助到读者
渐进式
了解Dapper底层强型别Mapping逻辑「结果反推程式码」
不能使用错误字串拼接SQL
感谢你们阅读到最后,假如喜欢本系列,欢迎留言、交流 :)