編寫T4模板無法避免的兩個話題:"Assembly Locking"&"Debug"
在這之前,我寫了一系列關于代碼生成和T4相關的文章,而我現在也試圖將T4引入我們自己的開發框架。在實踐中遇到了一些問題,也解決了不少問題。如果你也在進行T4相關的開發,相信你也一定會遇到這些問題。為此,特意將這些問題和解決方案與朋友們分享,希望在遇到這些問題的時候少走彎路。本篇文章介紹的是兩個重要的話題:程序集鎖定和調試。
目錄
一、程序集引用導致的編譯問題
二、T4引擎對引用程序集的鎖定
三、Debugger.Break導致VS 2010的Crash
四、在Debugger.Break之前加上Debugger.Launch
一、程序集引用導致的編譯問題
為了讓讀者對“程序集鎖定”,以及由它造成的開發上的不便有一個深刻的認識,我特意寫了一個小例子。如右圖所示的解決方案包含兩個項目:Lib和T4。其中我們的T4項目中定義了一個叫作HelloWorld.tt的模板文件,該文件需要使用到定義在Lib項目中的某個類型。所以,HelloWorld.tt模板文件中需要通過<#@Assembly…#>指令引用Lib項目編譯生成的程序集(Artech.T4Template.Lib.dll)。
如果你看過我上一篇文章,你應該知道我們至少具有解決T4模板的程序集引用的五種方案,在這里我們采用的是VS宏的解決方案,即將引用程序集文件的路徑設置成通過$(SolutionDir)表示的解決方案目錄的相對路徑。HelloWorld.tt定義如下,引用的程序集路徑為Lib項目在Debug模式下編譯生成的目錄($(SolutionDir)Lib\Bin\Debug\)。
<#@ output extension=".cs" #>
<#@ Assembly name="$(SolutionDir)Lib\Bin\Debug\Artech.T4Template.Lib.dll" #>
using System;
public class HelloWord
{
static void Main()
{
<# foreach( var person in Artech.T4Template.HelloWorldHelper.GetPersons())
{#>
Console.WriteLine("Hello, {0}!", "<#=person#>");
<# } #>
}
}
當你保存該T4模板,T4引擎將觸發并進行代碼生成工作,但是此時如果你試圖編譯被引用(實際上是生成的程序集被引用)的Lib項目,將會出現如下所示的編譯錯誤。錯誤信息為:“Unable to copy file "obj\Debug\Artech.T4Template.Lib.dll" to "bin\Debug\Artech.T4Template.Lib.dll". The process cannot access the file 'bin\Debug\Artech.T4Template.Lib.dll' because it is being used by another process.”,即之前生成的程序集正在被使用,所以不能將生成的程序集拷貝到編譯目標目錄下。
實際上這個程序集的使用者正是T4引擎。出于提高性能考慮,T4引擎在進行基于代碼生成的模板轉換(Template Transformation)的時候,會始終重用同一個AppDomain。由于該AppDomain不會自動卸載,這就會導致該AppDomain始終鎖定所有被它加載的程序集。如果我們需要釋放程序集,我們不得不重啟VS。但是,對于T4模板的開發調試階段,這種通過重新啟動VS的方式去釋放程序集以確保我們的項目能夠成功編譯是不能接受的。
那么,是否有一種解決方案既能夠確保T4引擎能夠進行正常的模板轉換,又能避免它強行鎖定引用程序集呢?如果你采用T4 ToolBox,你可以通過<#@ VolatileAssembly…#>這個指令輕松地解決這個問題。下面的T4模板中,我們將通過<#@Assembly…#>指令的程序集引用方式替換成了<#@ VolatileAssembly…#>(<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)Lib\Bin\Debug\Artech.T4Template.Lib.dll" #>),我們的Lib項目在任何時候都可以自由地編譯。
<#@ output extension=".cs" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)Lib\Bin\Debug\Artech.T4Template.Lib.dll" #>
using System;
public class HelloWord
{
static void Main()
{
<# foreach( var person in Artech.T4Template.HelloWorldHelper.GetPersons())
{#>
Console.WriteLine("Hello, {0}!", "<#=person#>");
<# } #>
}
}
<#@ VolatileAssembly…#>的實現原理其實挺簡單的,就是在加載的時候并不是直接加載指定的源程序集,而是創建一個新的程序集拷貝。
三、Debugger.Break導致VS 2010的Crash
VS和一些T4編輯器雖然給了基本的智能感知支持,但是在絕大部分我們相當于在編寫純文本的腳本,所以對于一些比較復雜的模板轉換邏輯,我們需要通過Debug的方式去發現一些無法避免的問題。關于T4模板的Debug,你Google一下會搜出一大堆。在這些“大眾化”的Debug解決方案中都包含兩點:
- 在<#@ template…#>指令中將debug屬性設置為true;
- 在需要設置斷點的地方執行Debugger.Break方案
按照這兩點,我們改寫了我們的T4模板,在foreach語句之前加上<# Debugger.Break(); #>,并通過<#@import…#>指令導入System.Diagnostics命名空間。我不知道在VS 2008下這種解決方案是否可行,但是如果你使用的是VS 2010,這肯定會導致整個VS的崩潰。當你保存TT文件的時候,如右圖所示的對話框彈出來,隨之伴隨整個VS的Crash。
<#@ output extension=".cs" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)Lib\Bin\Debug\Artech.T4Template.Lib.dll" #>
<#@ import namespace="System.Diagnostics" #>
using System;
public class HelloWord
{
static void Main()
{
<# Debugger.Break(); #>
<# foreach( var person in Artech.T4Template.HelloWorldHelper.GetPersons())
{#>
Console.WriteLine("Hello, {0}!", "<#=person#>");
<# } #>
}
}
四、在Debugger.Break之前加上Debugger.Launch
為了避免Debugger.Break導致的VS崩潰,只需要在之前多加一句代碼即可,既Debugger.Launch。為此我們對我們的T4模板略加修改:
<#@ output extension=".cs" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="$(SolutionDir)Lib\Bin\Debug\Artech.T4Template.Lib.dll" #>
<#@ import namespace="System.Diagnostics" #>
using System;
public class HelloWord
{
static void Main()
{
<#
Debugger.Launch();
Debugger.Break();
foreach( var person in Artech.T4Template.HelloWorldHelper.GetPersons())
{#>
Console.WriteLine("Hello, {0}!", "<#=person#>");
<#} #>
}
}
現在如果你保存該TT文件,VS會彈出如下一個對話框讓你選在是否進行Debug。如果需要進行Debug,選擇“Yes, debug devenv.exe”。
然后創建一個新的VS實例,或者選擇已經打開的VS程序進行Debug,這個對話框我們應該很熟悉。最后程序將會執行到我們設置的斷點(Debugger.Break),我們就可以像Debug普通托管程序一樣對T4模板進行Debug了。實際上,你也可以直接通過Attach進程的方式進行Debug,不過這里的進程就是VS的進程devenv.exe。