深入Dapper.NET源碼
繁體中文版本連接 : 深入Dapper.NET源碼 简体中文版本连接 : 深入Dapper.NET源码 (文长) - 暐翰 - 博客园
深入Dapper.NET源碼
目錄
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<string, object>使用DataReader使用GetName取得欄位名稱,藉由欄位index取得值,並將兩者分別添加進Dictionary當作key跟value。
因為ExpandoObject有實作IDynamicMetaObjectProvider介面可以轉換成dynamic

3.Dynamic Query 原理 Part2
有了前面簡單ExpandoObject Dynamic Query例子的概念後,接著進到底層來了解Dapper如何細節處理,為何要自訂義DynamicMetaObjectProvider。
首先掌握Dynamic Query流程邏輯 :
假設使用下面代碼
取值的過程會是 : 建立動態Func > 保存在緩存 > 使用result.Name > 轉成呼叫 ((DapperRow)result)["Name"] > 從DapperTable.Values陣列中以"Name"欄位對應的Index取值
接著查看源碼GetDapperRowDeserializer方法,它掌管dynamic如何運行的邏輯,並動態建立成Func給上層API呼叫、緩存重複利用。 
此段Func邏輯 :
DapperTable雖然是方法內的局部變量,但是被生成的Func引用,所以
不會被GC一直保存在內存內重複利用。
因為是dynamic不需要考慮類別Mapping,這邊直接使用
GetValue(index)向數據庫取值
將資料保存到DapperRow內
DapperRow 繼承 IDynamicMetaObjectProvider 並實作 GetMetaObject 方法,實作邏輯是返回DapperRowMetaObject物件。
DapperRowMetaObject主要功能是定義行為,藉由override
BindSetMember、BindGetMember方法,Dapper定義了Get、Set的行為分別使用IDictionary<string, object> - GetItem方法跟DapperRow - SetValue方法

最後Dapper利用DataReader的
欄位順序性,先利用欄位名稱取得Index,再利用Index跟Values取得值

為何要繼承IDictionary<string,object>?
可以思考一個問題 : 在DapperRowMetaObject可以自行定義Get跟Set行為,那麼不使用Dictionary - GetItem方法,改用其他方式,是否代表不需要繼承IDictionary<string,object>?
Dapper這樣做的原因之一跟開放原則有關,DapperTable、DapperRow都是底層實作類別,基於開放封閉原則不應該開放給使用者,所以設為private權限。
那麼使用者想要知道欄位名稱怎麼辦?
因為DapperRow實作IDictionary所以可以向上轉型為IDictionary<string, object>,利用它為公開介面特性取得欄位資料。
舉個例子,筆者有做一個小工具HtmlTableHelper就是利用這特性,自動將Dapper Dynamic Query轉成Table Html,如以下代碼跟圖片

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邏輯
這邊需要重點來看Dapper.SqlMapper.GenerateDeserializerFromMap方法,它負責Mapping的邏輯,可以看到裡面大量使用Emit IL技術。

要了解這段IL邏輯,我的方式 :「不應該直接進到細節,而是先查看完整生成的IL」,至於如何查看,這邊需要先準備 il-visualizer 開源工具,它可以在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之前需要先了解ADO.NET DataReader快速讀取資料方式會使用GetItem By Index方式,如以下代碼
接著查看此Demo - CastToUser方法生成的IL代碼
跟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方式動態將數據庫資料賦予物件
最後得到以下代碼 :
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的正三角,常見作法會是迴圈+遞迴方式
但其實這個題目在已經知道長度的情況下,可以被改成以下代碼
這個概念很重要,因為是從結果反推代碼,所以邏輯直接、效率快,而Dapper就是使用此概念來動態建立方法。
舉例 : 假設有一段代碼如下,我們可以從結果得出
User Class的Name屬性對應Reader Index 0 、類別是String 、 預設值是null
User Class的Age屬性對應Reader Index 1 、類別是int 、 預設值是0
假如系統能幫忙生成以下邏輯方法,那麼效率會是最好的
另外上面例子可以看出對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按照順序讀取我們要的資料,其中代碼邏輯 :
最後得出以下Exprssion版本代碼
查詢效果圖 : 
最後查看Expression.Lambda > DebugView(注意是非公開屬性)驗證代碼 :

8. Strongly Typed Mapping 原理 Part5 : Emit IL反建立C#代碼
有了前面Expression版本概念後,接著可以進到Dapper底層最核心的技術 : Emit。
首先要有個概念,MSIL(CIL)目的是給JIT編譯器看的,所以可讀性會很差、難Debug,但比起Expression來說可以做到更細節的邏輯操作。
在實際環境開發使用Emit,一般會先寫好C#代碼後 > 反編譯查看IL > 使用Emit建立動態方法,舉例 :
1.首先建立一個簡單打印例子 :
2.反編譯查看IL
3.使用DynamicMethod + Emit建立動態方法

但是對已經寫好的專案來說就不是這樣流程了,開發者不一定會好心的告訴你當初設計的邏輯,所以接著討論此問題。
如果像是Dapper只有Emit IL沒有C# Source Code專案怎麼辦?
我的解決方式是 : 「既然只有Runtime才能知道IL,那麼將IL保存成靜態檔案再反編譯查看」
這邊可以使用MethodBuild + Save方法將IL保存成靜態exe檔案 > 反編譯查看,但需要特別注意
請對應好參數跟返回類別,否則會編譯錯誤。
netstandard不支援此方式,Dapper需要使用
region if 指定版本來做區分,否則不能使用,如圖片
代碼如下 :
接著使用此方式在GetTypeDeserializerImpl方法反編譯Dapper Query Mapping IL,可以得出C#代碼 :

有了C#代碼後再來了解Emit邏輯會快很多,接著就可以進到Emit版本Query實作部分。
9.Strongly Typed Mapping 原理 Part6 : Emit版本
以下代碼是Emit版本,我把C#對應IL部分都寫在註解。
這邊Emit的細節概念非常的多,這邊無法全部都講解,先挑出重要概念講解
Emit Label
在Emit if/else需要使用Label定位,告知編譯器條件為true/false時要跳到哪個位子,舉例 : 「boolean轉整數」,假設要簡單將Boolean轉換成Int,C#代碼可以用「如果是True返回1否則返回0」邏輯來寫:
當轉成Emit寫法的時候,需要以下邏輯 :
考慮Label動態定位問題
先要建立好Label讓Brtrue_S知道符合條件時要去哪個Label位子
(注意,這時候Label位子還沒確定)繼續按順序由上而下建立IL
等到了
符合條件要運行區塊的前一行,使用MarkLabel方法標記Label的位子。
最後寫出的C# Emit代碼 :
這邊可以發現Emit版本 優點 :
能做更多細節的操作
因為細節顆粒度小,可以優化的效率更好
缺點 :
難以Debug
可讀性差
代碼量變大、複雜度增加
接著來看Dapper作者的建議,現在一般專案當中沒有必要使用Emit,使用Expression + Func/Action已經可以解決大部分動態方法的需求,尤其是Expression支援Block等方法情況。連結 c# - What's faster: expression trees or manually emitting IL

話雖如此,但有一些厲害的開源專案就是使用Emit管理細節,如果想看懂它們,就需要基礎的Emit IL概念。
10.Dapper 效率快關鍵之一 : Cache 緩存原理
為何Dapper可以這麼快?
前面介紹到動態使用 Emit IL 建立 ADO.NET Mapping 方法,但單就這功能無法讓 Dapper 被稱為輕量ORM效率之王。
因為動態建立方法是需要成本、並耗費時間的動作,單純使用反而會拖慢速度。但當配合 Cache 後就不一樣,將建立好的方法保存在 Cache 內,可以用『空間換取時間』概念加快查詢的效率,也就是俗稱查表法。
接著追蹤Dapper源碼,這次需要特別關注的是QueryImpl方法下的Identity、GetCacheInfo 
Identity、GetCacheInfo
Identity主要封裝各緩存的比較Key屬性 :
sql : 區分不同SQL字串
type : 區分Mapping類別
commandType : 負責區分不同數據庫
gridIndex : 主用用在QueryMultiple,後面講解。
connectionString : 主要區分同數據庫廠商但是不同DB情況
parametersType : 主要區分參數類別
typeCount : 主要用在Multi Query多映射,需要搭配override GetType方法,後面講解
接著搭配GetCacheInfo方法內Dapper使用的緩存類別ConcurrentDictionary<Identity, CacheInfo>,使用TryGetValue方法時會去先比對HashCode接著比對Equals特性,如圖片源碼。 
將Key類別Identity藉由override Equals方法實現緩存比較算法,可以看到以下Dapper實作邏輯,只要一個屬性不一樣就會建立一個新的動態方法、緩存。
以此概念拿之前Emit版本修改成一個簡單Cache Demo讓讀者感受:
效果圖 : 
11.錯誤SQL字串拼接方式,會導致效率慢、內存洩漏
了解實作邏輯後,接著延伸一個Dapper使用的重要觀念,SQL字串為緩存重要Key值之一,假如不同的SQL字串,Dapper會為此建立新的動態方法、緩存,所以使用不當情況下就算使用StringBuilder也會造成效率慢、內存洩漏問題。

至於為何要以SQL字串當其中一個關鍵Key,而不是單純使用Mapping類別的Handle,其中原因之一是跟查詢欄位順序有關,在前面有講到,Dapper使用「結果反推程式碼」方式建立動態方法,代表說順序跟資料都必須要是固定的,避免SQL Select欄位順序不一樣又使用同一組動態方法,會有A欄位值給B屬性錯值大問題。
最直接解決方式,對每個不同SQL字串建立不同的動態方法,並保存在不同的緩存。
舉例,以下代碼只是簡單的查詢動作,查看Dapper Cache數量卻達到999999個,如Gif動畫顯示

要避免此問題,只需要保持一個原則重複利用SQL字串,而最簡單方式就是參數化, 舉例 : 將上述代碼改成以下代碼,緩存數量降為1,達到重複利用目的 :

12.Dapper SQL正確字串拼接方式 : Literal Replacement
假如遇到必要拼接SQL字串需求的情況下,舉例 : 有時候值使用字串拼接會比不使用參數化效率好,特別是該欄位值只會有幾種固定值。
這時候Dapper可以使用Literal Replacements功能,使用方式 : 將要拼接的值字串以{=屬性名稱}取代,並將值保存在Parameter參數內,舉例 :
為什麼Literal Replacement可以避免緩存問題
首先追蹤源碼GetCacheInfo下GetLiteralTokens方法,可以發現Dapper在建立緩存之前會抓取SQL字串內符合{=變量名稱}規格的資料。
接著在CreateParamInfoGenerator方法生成Parameter參數化動態方法,此段方法IL如下 :
接著再生成Mapping動態方法,要了解此段邏輯我這邊做一個模擬例子方便讀者理解 :
看完以上例子,可以發現Dapper Literal Replacements底層原理就是字串取代,同樣屬於字串拼接方式,為何可以避免緩存問題?
這是因為取代的時機點在SetParameter動態方法內,所以Cache的SQL Key是沒有變動過的,可以重複利用同樣的SQL字串、緩存。
也因為是字串取代方式,所以只支持基本Value類別,假如使用String類別系統會告知The type String is not supported for SQL literals.,避免SQL Injection問題。
13.Query Multi Mapping 使用方式
接著講解Dapper Multi Mapping(多對應)實作跟底層邏輯,畢竟工作當中不可能都是一對一概念。
使用方式 :
需要自己編寫Mapping邏輯,使用方式 :
Query<Func邏輯>(SQL,Parameter,Mapping邏輯Func)需要指定泛型參數類別,規則為
Query<Func第一個類別,Func第二個類別,..以此類推,Func最後返回類別>(最多支持六組泛型參數)指定切割欄位名稱,預設使用
ID,假如不一樣需要特別指定 (這段後面特別講解)以上順序都是
由左至右
舉例 : 有訂單(Order)跟會員(User)表格,關係是一對多關係,一個會員可以有多個訂單,以下是C# Demo代碼 :

支援dynamic Multi Mapping
在初期常變動表格結構或是一次性功能不想宣告Class,Dapper Multi Mapping也支援dynamic方式

SplitOn區分類別Mapping組別
Split預設是用來切割主鍵,所以預設切割字串是Id,假如當表格結構PK名稱為Id可以省略參數,舉例 
假如主鍵名稱是其他名稱,請指定splitOn字串名稱,並且對應多個可以使用,做區隔,舉例,添加商品表格做Join :
14.Query Multi Mapping 底層原理
Multiple Mapping 底層原理
這邊先以一個簡單Demo帶讀者了解Dapper Multi Mapping 概念
按照泛型類別參數數量建立對應數量的Mapping Func集合
Mapping Func建立邏輯跟Query Emit IL一樣
呼叫使用者的Custom Mapping Func,其中參數由前面動態生成的Mapping Func而來
以上概念就是此方法的主要邏輯,接著講其他細節部分
支持多組類別 + 強型別返回值
Dapper為了強型別多類別Mapping使用多組泛型參數方法方式,這方式有個小缺點就是沒辦法動態調整,需要以寫死方式來處理。
舉例,可以看到圖片GenerateMapper方法,依照泛型參數數量,寫死強轉型邏輯,這也是為何Multiple Query有最大組數限制,只能支持最多6組的原因。

多類別泛型緩存算法
這邊Dapper使用
泛型類別來強型別保存多類別的資料
並配合繼承共用Identity大部分身分驗證邏輯
提供可
override的GetType方法,來客製泛型比較邏輯,避免造成跟Non Multi Query緩存衝突。


Dapper Query Multi Mapping的Select順序很重要
因為SplitOn分組基礎依賴於Select的順序,所以順序一錯就有可能屬性值錯亂情況。
舉例 : 假如上面例子的SQL改成以下,會發生User的ID變成Order的ID;Order的ID會變成User的ID。
原因可以追究到Dapper的切割算法
首先
倒序方式處理欄位分組(GetNextSplit方法可以看到從DataReader Index大到小查詢)
接著
倒序方式處理類別的Mapping Emit IL Func最後反轉為
正序,方便後面Call Func對應泛型使用

15.QueryMultiple 底層原理
使用方式例子 :
使用QueryMultiple優點 :
主要
減少Reqeust次數可以將多個查詢
共用同一組Parameter參數
QueryMultiple的底層實作邏輯 :
底層技術是ADO.NET - DataReader - MultipleResult
QueryMultiple取得DataReader並封裝進GridReader
呼叫Read方法時才會建立Mapping動態方法,Emit IL動作跟Query方法一樣
接著使用ADO.NET技術呼叫
DataReader NextResult取得下一組查詢結果假如
沒有下一組查詢結果才會將DataReader釋放
緩存算法
緩存的算法多增加gridIndex判斷,主要對每個result mapping動作做一個緩存,Emit IL的邏輯跟Query一樣。

沒有延遲查詢特性
注意Read方法使用的是buffer = true = 返回結果直接ToList保存在內存,所以沒有延遲查詢特性。


記得管理DataReader的釋放
Dapper 呼叫QueryMultiple方法時會將DataReader封裝在GridReader物件內,只有當最後一次Read動作後才會回收DataReader

所以沒有讀取完再開一個GridReader > Read會出現錯誤:已經開啟一個與這個 Command 相關的 DataReader,必須先將它關閉。

要避免以上情況,可以改成using區塊方式,運行完區塊代碼後就會自動釋放DataReader
閒話 :
感覺Dapper GridReader好像有機會可以實作是否有NextResult方法,這樣就可以配合while方法一次讀取完多組查詢資料,等之後有空來想想有沒有機會做成。
概念代碼 :
16.TypeHandler 自訂Mapping邏輯使用、底層邏輯
遇到想要客製某些屬性Mapping邏輯時,在Dapper可以使用TypeHandler
使用方式 :
建立類別繼承
SqlMapper.TypeHandler將要客製的類別指定給
泛型,e.g :JsonTypeHandler<客製類別> : SqlMapper.TypeHandler<客製類別>查詢的邏輯使用override實作Parse方法,增刪改邏輯實作SetValue方法假如多個類別Parse、SetValue共用同樣邏輯,可以將實作類別改為
泛型方式,客製類別在AddTypeHandler時指定就可以,可以避免建立一堆類別,e.g :JsonTypeHandler<T> : SqlMapper.TypeHandler<T> where T : class
舉例 : 想要特定屬性成員在數據庫保存Json,在AP端自動轉成對應Class類別,這時候可以使用SqlMapper.AddTypeHandler<繼承實作TypeHandler的類別>。
以下例子是User資料變更時會自動在Log欄位紀錄變更動作。
效果圖 : 
接著追蹤TypeHandler源碼邏輯,需要分兩個部份來追蹤 : SetValue,Parse
SetValue底層原理
AddTypeHandlerImpl方法管理緩存的添加
在CreateParamInfoGenerator方法Emit建立動態AddParameter方法時,假如該Mapping類別TypeHandler緩存內有資料,Emit添加呼叫SetValue方法動作。
在Runtime呼叫AddParameters方法時會使用LookupDbType,判斷是否有自訂TypeHandler

接著將建立好的Parameter傳給自訂TypeHandler.SetValue方法

最後查看IL轉成的C#代碼
可以發現生成的Emit IL會去從TypeHandlerCache取得我們實作的TypeHandler,接著呼叫實作SetValue方法運行設定的邏輯,並且TypeHandlerCache特別使用泛型類別依照不同泛型以Singleton方式保存不同handler,這樣有以下優點 :
只要傳遞泛型類別參數就可以取得同一個handler
避免重複建立物件因為是泛型類別,取handler時可以避免了反射動作,
提升效率



Parse對應底層原理
主要邏輯是在GenerateDeserializerFromMap方法Emit建立動態Mapping方法時,假如判斷TypeHandler緩存有資料,以Parse方法取代原本的Set屬性動作。 
查看動態Mapping方法生成的IL代碼 :
轉成C#代碼來驗證 :
17. CommandBehavior的細節處理
這篇將帶讀者了解Dapper如何在底層利用CommandBehavior優化查詢效率,如何選擇正確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
SequentialAccess、SingleResult優化邏輯
首先可以看到每個方法都使用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提早關閉情況
QueryFirst搭配SingleRow,
有時候我們會遇到select top 1知道只會讀取一行資料的情況,這時候可以使用QueryFirst。它使用CommandBehavior.SingleRow可以避免浪費資源只讀取一行資料。
另外可以發現此段除了while (reader.NextResult()){}外還有while (reader.Read()) {},同樣是避免忽略錯誤,這是一些公司自行土炮ORM會忽略的地方。 
與QuerySingle之間的差別
兩者差別在QuerySingle沒有使用CommandBehavior.SingleRow,至於為何沒有使用,是因為需要有多行資料才能判斷是否不符合條件並拋出Exception告知使用者。
這段有一個特別好玩小技巧可以學,錯誤處理直接沿用對應LINQ的Exception,舉例:超過一行資料錯誤,使用new int[2].Single(),這樣不用另外維護Exceptiono類別,還可以擁有i18N多國語言化。

18.Parameter 參數化底層原理
接著進到Dapper的另一個關鍵功能 : 「Parameter 參數化」
主要邏輯 : GetCacheInfo檢查是否緩存內有動態方法 > 假如沒有緩存,使用CreateParamInfoGenerator方法Emit IL建立AddParameter動態方法 > 建立完後保存在緩存內
接著重點來看CreateParamInfoGenerator方法內的底成邏輯跟「精美細節處理」,使用了結果反推代碼方法,忽略「沒使用的欄位」不生成對應IL代碼,避免資源浪費情況。這也是前面緩存算法要去判斷不同SQL字串的原因。
以下是我挑出的源碼重點部分 :
接著查看IL來驗證,查詢代碼如下
CreateParamInfoGenerator AddParameter 動態方法IL代碼如下 :
IL轉成對應C#代碼:
可以發現雖然傳遞Age參數,但是SQL字串沒有用到,Dapper不會去生成該欄位的SetParameter動作IL。這個細節處理真的要給Dapper一個讚!
19. IN 多集合參數化底層原理
為何ADO.NET不支援IN 參數化,Dapper支援 ?
原理
判斷參數的屬性是否為IEnumerable類別子類別
假如是,以該參數名稱為主 + Parameter正則格式找尋SQL內的參數字串 (正則格式 :
([?@:]參數名)(?!\w)(\s+(?i)unknown(?-i))?)將找到的字串以
()+ 多個屬性名稱+流水號方式替換依照流水號順序依序CreateParameter > SetValue
關鍵程式部分 
以下用sys.objects來查SQL Server的表格跟視圖當追蹤例子 :
Dapper會將SQL字串改成以下方式執行
查看Emit IL可以發現跟之前的參數化IL很不一樣,非常的簡短
轉成C#代碼來看,會很驚訝地發現:「這段根本不需要使用Emit IL簡直多此一舉」
沒錯,是多此一舉,甚至 IDataParameterCollection parameter = P_0.Parameters;這段代碼根本不會用到。
Dapper這邊做法是有原因的,因為要能跟非集合參數配合使用,像是前面例子加上找出訂單Orders名稱的資料邏輯
對應生成的IL轉換C#代碼就會是以下代碼,達到能搭配使用目的 :
另外為何Dapper這邊Emit IL會直接呼叫工具方法PackListParameters,是因為IN的參數化數量是不固定,所以不能由固定結果反推程式碼方式動態生成方法。
該方法裡面包含的主要邏輯:
判斷集合參數的類型是哪一種 (假如是字串預設使用4000大小)
正則判斷SQL參數以流水號參數字串取代
DbCommand的Paramter的創建
SQL參數字串的取代邏輯也寫在這邊,如圖片 
20.DynamicParameter 底層原理、自訂實作
這邊用個例子帶讀者了解DynamicParameter原理,舉例現在有一段代碼如下 :
前面已經知道String型態Dapper會自動將轉成數據庫Nvarchar並且長度為4000的參數,數據庫實際執行的SQL如下 :
這是一個方便快速開發的貼心設計,但假如遇到欄位是varchar型態的情況,有可能會因為隱性轉型導致索引失效,導致查詢效率變低。
這時解決方式可以使用Dapper DynamicParamter指定數據庫型態跟大小,達到優化效能目的
接著往底層來看如何實現,首先關注GetCacheInfo方法,可以看到DynamicParameters建立動態方法方式代碼很簡單,就只是呼叫AddParameters方法
代碼可以這麼簡單的原因,是Dapper在這邊特別使用「依賴於介面」設計,增加程式的彈性,讓使用者可以客制自己想要的實作邏輯。這點下面會講解,首先來看Dapper預設的實作類別DynamicParameters中AddParameters方法的實作邏輯
可以發現Dapper在AddParameters為了方便性跟兼容其他功能,像是Literal Replacement、EnumerableMultiParameter功能,做了許多判斷跟動作,所以代碼量會比以前使用ADO.NET版本多,所以效率也會比較慢。
假如有效率苛求的需求,可以自己實作想要的邏輯,因為Dapper此段特別設計成「依賴於介面」,只需要實作IDynamicParameters介面就可以。
以下是我做的一個Demo,可以使用ADO.NET SqlParameter建立參數跟Dapper配合

21. 單次、多次 Execute 底層原理
查詢、Mapping、參數講解完後,接著講解在增、刪、改情況Dapper我們會使用Execute方法,其中Execute Dapper分為單次執行、多次執行。
單次Execute
以單次執行來說Dapper Execute底層是ADO.NET的ExecuteNonQuery的封裝,封裝目的為了跟Dapper的Parameter、緩存功能搭配使用,代碼邏輯簡潔明瞭這邊就不做多說明,如圖片 
「多次」Execute
這是Dapper一個特色功能,它簡化了集合操作Execute之間的操作,簡化了代碼,只需要 : connection.Execute("sql",集合參數);。
至於為何可以這麼方便,以下是底層的邏輯 :
確認是否為集合參數

建立
一個共同DbCommand提供foreach迭代使用,避免重複建立浪費資源
假如是集合參數,建立Emit IL動態方法,並放在緩存內利用

動態方法邏輯是
CreateParameter > 對Parameter賦值 > 使用Parameters.Add添加新建的參數,以下是Emit IL轉成的C#代碼 :
foreach該集合參數 > 除了第一次外,每次迭代清空DbCommand的Parameters > 重新呼叫同一個動態方法添加Parameter > 送出SQL查詢
實作方式簡潔明瞭,並且細節考慮共用資源避免浪費(e.g共用同一個DbCommand、Func),但遇到大量執行追求效率需求情況,需要特別注意此方法每跑一次對數據庫送出一次reqesut,效率會被網路傳輸拖慢,所以這功能被稱為「多次執行」而不是「批量執行」的主要原因。
舉例,簡單Execute插入十筆資料,查看SQL Profiler可以看到系統接到10次Reqeust:

22. ExecuteScalar應用
ExecuteScalar因為其只能讀取第一組結果、第一筆列、第一筆資料特性,是一個常被遺忘的功能,但它在特定需求下還是能派上用場,底下用「查詢資料是否存在」例子來做說明。
首先,Entity Framwork如何高效率判斷資料是否存在?
假如有EF經驗的讀者會答使用Any而不是Count() > 1。
使用Count系統會幫轉換SQL為 :
SQL Count 是一個匯總函數,會迭代符合條件的資料行判斷每列該資料是否為null,並返回其行數。
而Any語法轉換SQL使用EXISTS,它只在乎是否有沒有資料,代表不用檢查到每列,只需要其中一筆有資料就有結果,所以效率快。
Dapper如何做到同樣效果?
SQL Server可以使用SQL格式select top 1 1 from [表格] where 條件 搭配 ExecuteScalar 方法,接著在做一個擴充方法,如下 :
效果圖 : 
使用如此簡單原因,是利用Dapper ExecuteScalar會去呼叫ExecuteScalarImpl其底層Parse邏輯
使用 Convert.ChangeType 轉成 bool : 「0=false,非0=true」 特性,讓系統可以簡單轉型為bool值。
注意
不要QueryFirstOrDefault代替,因為它需要在SQL額外做Null的判斷,否則會出現「NullReferenceException」。 
這原因是兩者Parse實作方式不一樣,QueryFirstOrDefault判斷結果為null時直接強轉型 
而ExecuteScalar的Parce實作多了為空時使用default值的判斷 
23.總結
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優點開發專案
可以輕鬆面對Dapper的面試,比起一般使用Dapper工程師回答出更深層的概念
從最簡單Reflection到常用Expression到最細節Emit從頭建立Mapping方法,帶讀者
漸進式了解Dapper底層強型別Mapping邏輯了解動態建立方法的重要概念
「結果反推程式碼」有基本IL能力,可以利用IL反推C#代碼方式看懂其他專案的底層Emit邏輯
了解Dapper因為緩存的算法邏輯,所以
不能使用錯誤字串拼接SQL
Last updated