April 27, 2013

TortoiseSVN設定排除簽入特定資料夾或檔案

在commit資料到repository時,我們可能會想排除特定的檔案或資料夾,如bin或obj資料夾,以避免不必要同步的資料也一併進了repository。如下圖中可以看到在簽入資料時,視窗會列出bin及obj資料夾中的檔案。雖然可以手動去取消勾選,但每次要commit時就得再做一次。


TortoiseSVN中允許我們做全域設定,只要符合排除名單,就不會將資料簽入到repository裡。在Windows桌面點選右鍵進入TortoiseSVN-> Settings-> General


Global ignore pattern中加入bin obj並按下OK


接下來試著重新commit,就可以看到bin和obj資料夾已不在commit清單中。如果要排除特定副檔名的檔案,可使用萬用字元的方式,如*.suo。另外有一點要注意的是,Global ignore pattern是case-sensitive,所以大小寫要和欲排除的資料夾或檔案名稱一致

April 21, 2013

使用Legit Log Viewer讀取NLog紀錄檔

Legit Log Viewer是一套可以讀取NLog紀錄檔的免費軟體。在系統開發期間,我們常會把例外訊息或是除錯用的資訊寫入到文字檔中以便檢查問題所在。雖說用一般的文字編輯軟體即可開啟NLog紀錄檔,但紀錄檔內容一多,要從中找出想要的資訊恐怕也得多花些時間。

下載安裝成功後開啟Legit Log Viewer,可以看到一個展示用的log檔Demo Log.tmp.log,位於資料夾C:\Users\Pete\AppData\Local\Temp\底下。從畫面上我們也可以看到所使用的Log Format為NLog (Legit Log custom layout)。


用文字編輯軟體開啟這個log檔可以發現每一列的資料都含有|符號。Legit Log Viewer會把每個以|符號區隔開來的資料當作是一個欄位。以下面的log檔來說,每列以5個|符號區隔資料,所以在Legit Log Viewer上會顯示6個欄位。


Legit Log Viewer在載入log檔時會先檢查目前所有的Log Format中的定義檔(例如NLog (Legit Log custom layout))哪個符合log檔的格式,找到的話便會依照定義檔內所定義的方式顯示log檔。如果要顯示符合自訂格式的log檔,我們需先建立一個Log Format定義檔

點選Tools-> Edit Log Formats,可以看到目前所有的定義檔。


點選NLog (Legit Log custom layout)並按下Save for Edit按鈕,將內建的定義檔重新命名另存新檔。

開啟剛儲存的定義檔可以看到其內容為XML格式。
<?xml version="1.0" encoding="utf-8" ?>
<LogFormat xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:xsd="http://www.w3.org/2001/XMLSchema"
           xmlns="http://www.legitlog.com/LogFormatSchema.xsd"
           FormatName="NLog (Legit Log custom layout)">
  <Description>
    Log format for Legit Log NLog layout.
    ${processid}|${longdate}|${level:uppercase=true}|${event-context:item=Context}|${logger}|${message}
  </Description>
  <LogLine>
    <Fields>
      <LogFieldFormat Name="ProcessId" FieldType="Int" Delimiter="|" FilterColumn="true" HighlightRows="true" />
      <LogFieldFormat Name="DateTime" FieldType="DateTime" Delimiter="|" Format="yyyy-MM-dd HH:mm:ss.ffff" />
      <LogFieldFormat Name="Level" FieldType="String" Delimiter="|" FilterColumn="true" HighlightRows="true" />
      <LogFieldFormat Name="Context" FieldType="String" Delimiter="|" FilterColumn="true" />
      <LogFieldFormat Name="Logger" FieldType="String" Delimiter="|" />
      <LogFieldFormat Name="Message" FieldType="String" Delimiter="&#xD;&#xA;" Multiline="true" />
    </Fields>
  </LogLine>
</LogFormat>

設定檔的格式其實蠻直覺的,每個<LogFieldFormat>代表一個欄位,屬性Name為欄位標頭名稱,FieldType為欄位類型,Delimiter為資料分隔符號。<LogFormat>FormatName屬性為定義檔的名稱,你可以修改為較有意義的名稱,例如專案名稱。<Description>內的文字則是用來當我們在Legit Log Viewer中點選此定義檔時要顯示的描述文字,例如顯示此定義檔使用的log檔layout格式(設定在NLog設定檔中<target>裡的layout屬性)為何,如範例中的${processid}|${longdate}|${level:uppercase=true}|${event-context:item=Context}|${logger}|${message}。

修改完定義檔後,點選Add new format按鈕加入定義檔便可在列表中看到自訂的定義檔。我們可以利用Move up把自定義檔移到最上方做為最優先被Legit Log Viewer選擇到的定義檔。


以下有幾點需額外注意
  1. Multiline屬性只能出現在最後一個LogFieldFormat中,否則載入定義檔時會出現錯誤訊息。因此也建議如果要顯示多行資料的話最好放在最後一欄。

  2. 載入定義檔後,若之後定義檔有任何修改,必須先刪除原來的定義檔後再重新載入才會生效,這部份算是使用上較為不方便的地方

接下來至Logs-> Open Log載入要檢視的log檔便可顯示在Legit Log Viewer裡。載入後我們可以透過Message filter功能輸入關鍵字來過濾出我們想要的資訊。


若log檔內容有更新,Legit Log Viewer也會提醒使用者是否顯示更新過後的內容。


April 14, 2013

解密Network Monitor擷取的TLS/SSL封包

在「使用Network Monitor擷取TLS/SSL封包」中介紹了Network Monitor擷取SSL網站封包的方式。因為資料在傳輸過程中被加密的關係,我們無法看到資料的原始內容。例如我登入了GMail,我的使用者帳號和密碼在傳輸至GMail的過程中是加密過的,必須有GMail的SSL憑證私密金鑰才能解密這些資料。如果我有私密金鑰的話該怎麼來解密呢?

在實際進行資料解密之前,我們需先至 Network Monitor Open Source Parsers網站下載並安裝Network Monitor所使用最新的parser,以防解密的過程中出現錯誤而解密失敗。

接下來我們在已擷取完SSL封包的視窗中至選單Experts-> NmDecrypt-> Run Expert


Network Monitor會要我們先儲存目前已擷取到的資料以利之後的解密


儲存後會開啟另一個視窗


Encrypted Capture File是指我們剛剛所儲存已擷取的封包資料
Server Certificate Path是指SSL憑證的私密金鑰路徑,指定路徑後需要再輸入金鑰的密碼
Decrypted File Path是指解密後資料存放的路徑
Debug Log File Path是指解密過程中的log檔儲存路徑


輸入完畢後按下Start開始解密,解密完後可至存放解密資料的路徑以Network Monitor開啟資料。在Frame Summary視窗中可以使用Find功能來搜尋我們要的資料,接下來便可看到資料已經成功被解密


參考

使用Network Monitor擷取TLS/SSL封包

Network Monitor是一套免費又強大的網路封包分析工具,本篇文章將介紹如何使用Network Monitor來擷取安裝有SSL憑證的網站封包。

管理者模式開啟Network Monitor,點選New Capture


在視窗下方Display Filter輸入IPv4.Address == {欲監控的網站IP位址} AND TLS,例如IPv4.Address == 74.125.31.83 AND TLS。

IPv4.Address代表要監控來源或目的端的IP位址,因為我們想要監控的是有安裝SSL憑證的網站,所以只要專心在要監控的網站IP位址,避免擷取到不必要的封包
TLS為SSL所使用的通訊協定

點選Apply套用封包過濾規則。


接下來按下Start進行封包擷取


瀏覽欲監控的網站,可以看到Network Monitor成功擷取到SSL封包,如下方可以看到SSL handshake的過程


在這個範例中,我是擷取登入GMail時的封包,如果仔細檢查擷取到的frame(Description為TLS:TLS Rec Layer-1 SSL Application Data的frame)會發現資料都是有加密過的,如果要解密這些資料的話則必需要有SSL憑證的私密金鑰。

參考

Effective C# - Prefer readonly to const

Effective C#第2版中的Prefer readonly to const章節中建議開發人員使用readonly取代const的常數宣告方式,如
public const string ConstVersion = "2.0";
改用
public static readonly string ReadonlyVersion = "2.0";
在此章節還有幾個要點
  1. 用const宣告常數的方式稱為compile-time constant,而用readonly的方式稱為runtime constant
  2. compile-time constant會在編譯程式碼時就被嵌入IL中,而runtime constant是在執行期時透過readonly變數來取得實際的常數值
  3. compile-time constant的效能比runtime constant來得好一點,因為它是在編譯期就被嵌入IL中
  4. compile-time constant是binary incompatible,也就是當compile-time constant的值有變動時,參考它的程式碼也要重新編譯及佈署,新的常數值才會被嵌入到IL中;runtime constant是在執行期時透過readonly變數來取得實際的常數值所以是binary compatible,因此在維護的彈性上runtime constant較佳
  5. 在使用具名參數(named parameter)或選擇性參數(optional parameter)時需要注意上述binary compatibility的問題
以下為簡單的測試範例

建立一library專案ConstvsReadonly.Const並新增下方類別。ConstVersion為compile-time constant,而ReadonlyVersion為runtime constant
public class Constants
{
    public const string ConstVersion = "1.0";
    public static readonly string ReadonlyVersion = "2.0";
}

建立一console app專案ConstvsReadonly,參考並新增下方類別
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Const:{0}", Constants.ConstVersion);
        Console.WriteLine("Readonly:{0}", Constants.ReadonlyVersion);
        Console.ReadKey(true);
    }
}

編譯、佈署並執行console app,可以看到如下結果


透過ILSpy檢視console app的執行檔可以看到Constants.ConstVersion已被置換為常數值1.0


接下來將Constants.cs修改成下方並重新編譯程式及佈署ConstvsReadonly.Const專案
public class Constants
{
    public const string ConstVersion = "2.0";
    public static readonly string ReadonlyVersion = "3.0";
}

再次執行console app可以看到以下結果


可以看到雖然重新佈署了ConstvsReadonly.Const專案,但Const的值保持不變(1.0)而Readonly的值更新了(3.0),這就是所謂的binary incompatible。我們重新佈署了修改的地方(ConstvsReadonly.Const專案),但因為ConstvsReadonly專案沒重新編譯佈署,造成嵌入至IL的ConstVersion常數值在兩個專案中不相同。

April 9, 2013

Network Monitor無法顯示系統已安裝的網路

前幾天在研究SSL的運作機制,試著使用Network Monitor擷取SSL封包,安裝完後開啟卻找不到可以使用的網路


此問題的原因在於權限不足,改由管理者權限來開啟Network Monitor之後就可以正常顯示



April 3, 2013

將Logging Framework去耦(decouple)的必要性

大多數的人在決定將系統導入log機制時通常會先選擇幾個較為知名且開源的logging framework如Enterprise Library的Logging Application Block、log4netNLog等,除了避免重複造輪子浪費時間外,這些framework也提供了不少強大的功能還附上原始碼。這裡不談如何去選擇一個合適的logging framework,畢竟個人偏好及政治因素可能佔上大部份,也就是那句老話,it depends。

上述framework都有各自的抽象層及實作,通常很直覺地當我們安裝好時,就直接在程式碼中使用起來,例如NLog使用factory method pattern去建立一個Logger實作類別
Logger logger = LogManager.GetCurrentClassLogger();
logger.Debug("this is a Debug level");
而log4net則使用factory method pattern建立一個ILog介面
ILog log = LogManager.GetLogger(typeof(Program));
log.Debug("this is just a debug message");

接下來諸如上面的程式碼片段便開始散佈在各個使用logging framework的類別中。這樣會有什麼問題嗎?

在不做unit test和不更換logging framework(例如將log4net換成NLog)的前提下,的確是不會有什麼問題。硬要說的話,就是違反了dependency inversion principle和program to an interface, not an implementation兩個原則,造成client code直接與logging framework相依,耦合度提高。

基於這兩個原則,我們可以利用facade pattern建立一個client code要做log時呼叫用的抽象層,例如
public interface ILogService
{
    void Debug(string message;
    void Info(string message);
    void Warn(string message);
    void Error(string message);
    // 以下省略
}
接下來建立一個類別來實作ILogService介面,而這個實作類別裡直接使用logging framework,等於是將實際log的功能再委託給logging framework,以下面為例是直接呼叫NLog提供的API。
public class LogService : ILogService
    {
        private static Logger _logger = LogManager.GetCurrentClassLogger();

        #region ILogService Members

        public void Debug(string message)
        {
            if (!_logger.IsDebugEnabled)
            {
                return;
            }

            if (string.IsNullOrWhiteSpace(message))
            {
                throw new ArgumentNullException("message");
            }

            _logger.Debug(message);
        }

        // 以下省略

        #endregion
    }

接著在client code只要搭配IoC container就可以反轉相依性讓client code相依於前面建立的抽象層(ILogService)。
ILogService logService = ObjectFactory.GetInstance<ILogService>();
logService.Debug("This is a debug level");

如此便符合上述兩項原則,但為了符合這兩項原則還得大費周章建立一個抽象層來使用,真的值得嗎?以下就兩個面向來討論

Testability
將原本相依於logging framework實作的程式反轉成相依於自行建立的抽象(ILogService)類別可以提高程式碼單元測試的可測性,讓我們可以專注在待測的類別,並使用mocking framework如Rhino Mocks透過mock或stub模擬ILogService的操作,藉以輔助我們實際要測試的功能,如此我們就不必理會ILogService實際上的實作(LogService)如何運作,因為我們可以透過mock/stub模擬出我們要它(ILogService)作的行為。例如下面unit test的程式碼片段
[ExpectedException(typeof(RegisterUserAccountException))]
[TestMethod]
public void Register_ExistedUser_ThrowRegisterUserAccountException()
{
    RegisterRequest request = new RegisterRequest()
    {
        Username = "petechen",
        Password = "petechen"
    };

    IUserAccountRepository userAccountRepository = MockRepository.GenerateStub<IUserAccountRepository>();

    userAccountRepository.Stub(c => c.CreateUserAndAccount(request.Username, request.Password)).IgnoreArguments().Throw(new Exception());

    ILogService logService = MockRepository.GenerateStub<ILogService>();

    IUserAccountService userAccountService = new UserAccountService(userAccountRepository, ILogService logService);

    userAccountService.Register(request);
    logService.AssertWasCalled(c => c.Error("exception occurs");
}
實際要測試的類別為UserAccountService,但因為它相依了兩個介面IUserAccountRepository及ILogService,於是我們使用mock/stub來模擬這兩個介面的行為,這樣我們就可以專注在UserAccountService的功能(Register)是否如我們預期(丟出RegisterUserAccountException)般地執行。

如果沒有建立ILogService這個抽象層的話會有什麼問題嗎?問題會出現在你可能無法預期LogService這個實作類別或是所使用的logging framework會為你帶來的意外效果。舉個例子,當LogService丟出一個ArgumentNullException出來。但以上面的程式碼片段來看,我們要測試的是當執行IUserAccountService.Register時要丟出RegisterUserAccountException而且錯誤訊息還要被記錄到。另一個意外效果則是會發生在logging framework的設定檔沒有正確地被設定,這時也會出現無預期的錯誤,導致在執行unit test之前,必須先確認logging framework的設定檔是否正確。但在這種情況下所做的測試已經不是unit test而是integration test了,因為所做的測試和系統環境設定因素(外在因素)有關。

雖說如此,以NLog來說,即便我們直接相依於它,在沒有任何設定檔的情況下,即使呼叫它所提供的API(如Trace, Debug, Warn等)也不會丟出exception。的確,我也曾經想過是否不要隔著一層抽象層(ILogService),因為只要它不會丟出exception,看起來似乎也不會影響到我的unit test。但如果你的unit test如上面片段一樣,想要在丟出exception時也檢查是否有執行到log錯誤訊息的程式碼(logService.AssertWasCalled(c => c.Error("exception occurs");),這時你還是會需要透過mock去檢查。


Flexibility
相依於抽象層另一個帶來的好處就是讓程式碼更具彈性及可抽換性。為什麼呢?因為程式碼中不再相依於特定的logging framework。當你想把log4net換成NLog時,只需要修改LogService實作類別,或是建立另一個新的LogService實作類別,並在IoC container中將ILogService的實作改指向新對應的實作類別即可一次更新整個系統,因為所有程式碼都是呼叫ILogService,所以根本不需要修改原本的程式碼。
ObjectFactory.Initialize(x =>
{
    //x.For<ILogService>().Use<LogService>();
    x.For<ILogService>().Use<LogServiceForNLog>();
});
此外,修改之後也只需要佈署ILogService新的實作類別即可(也許就只有一支DLL檔),並不需要將整個系統重新佈署,大大地降低佈署的複雜度和出錯的可能性。當然你可能會質疑在開發中或系統已上線再更換logging framework的機率。的確,像log4net或NLog這種等級的logging framework已經是相對穩定許多,除非它們有很嚴重的bug或效能的問題,我想更換的機率是不高,但所謂的彈性就是要預防這種未知的變動。

上述提到的兩點不僅是針對logging framework,也可套用在其它的third-party元件。本篇文章不下任何定論。因為有的人不做unit test或是根本沒時間做,又或是直接就做integration test。有的人認為更換logging framework的事根本不會發生。如果我只做integration test又只使用固定一套logging framework,何必額外花心思和時間在這上頭。所以去除耦合度的必要性?It always depends!

參考

April 2, 2013

使用NLog記錄ASP.NET使用者的網頁請求資訊

在ASP.NET網站中,我們可能會需要記錄使用者的網頁請求資訊以便可以更細部地追蹤使用者提出的問題或是做為audit trail,例如使用者名稱、IP位址及請求的網頁名稱或路徑等。NLog在這方面提供了一些layout renders讓我們可以讀取到ASP.NET的伺服器環境變數。以下摘錄至官方文件
  • ${aspnet-application} - ASP.NET Application variable.
  • ${aspnet-request} - ASP.NET Request variable.
  • ${aspnet-session} - ASP.NET Session variable.
  • ${aspnet-sessionid} - ASP.NET Session ID.
  • ${aspnet-user-authtype} - ASP.NET User variable.
  • ${aspnet-user-identity} - ASP.NET User variable.
然而這幾個layout renders並未被定義在NLog.dll裡,而是定義在NLog的延伸函式庫(NLog.Extended.dll)裡,所以我們必須額外安裝這個延伸函式庫。


透過Package Manager Console執行Install-Package NLog.Extended後,專案中會自動加入NLog.Extended.dll為參考。接下來我們也需要在NLog的設定檔中新增相對應的參考<extensions>元素至<nlog>元素下。

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <extensions>
    <add assembly="NLog.Extended" />
  </extensions>
  
  <!--以下省略-->
</nlog>

設定完後將layout屬性設定為${aspnet-request:serverVariable=AUTH_USER}|${aspnet-request:serverVariable=HTTP_REFERER}|${longdate}|${level:uppercase:true},嘗試登入ASP.NET應用程式及瀏覽其它網頁,可以看到如下資訊被NLog記錄到

|http://localhost:60197/Account/Login|2013-04-02 17:26:26.3267|Debug
pete.chen|http://localhost:60197/Home/About|2013-04-02 17:33:00.8961|Debug

pete.chen是使用者的登入帳號。第一筆紀錄沒有帳號名稱的原因在於使用者尚未登入系統,登入系統後就可以成功擷取到帳號名稱了。

April 1, 2013

避免將NLog設定檔放在library project

為了降低NLog與web app專案間的耦合關係,我建立了一個library專案,加了一層抽象層在NLog與web app之間,而web app只需直接呼叫這個抽象層即可使用NLog所提供的API。於是在library專案中我便直接使用nuget安裝了NLog及其設定檔並完成了抽象層的設計。

就在一切設定好之後發現原本在web app直接使用NLog還能正常輸出紀錄到log檔,將相同的設定檔移到library專案後就不能運作了。嘗試將設定檔中nlog元素的throwExceptions屬性設為true,但沒有看到任何錯誤息出現。最後執行了偵錯模式發現IsDebugEnabled的值居然是false。


但設定檔內容我是原封不動從web app移過來,難道是NLog根本就沒讀取到設定檔(NLog.config)嗎?可是看了一下web app下的bin資料夾,裡面確實有看到NLog.config,所以應該是能讀取到設定檔吧?

研究了一下NLog官方文件可以推斷出設定檔應該是沒有被讀取到,以下是NLog設定檔可被讀取的位置

In case of an ASP.NET application, the following files are searched:
  • standard web application file web.config
  • web.nlog located in the same directory as web.config
  • NLog.config in application’s directory
  • NLog.dll.nlog in a directory where NLog.dll is located
  • file name pointed by the NLOG_GLOBAL_CONFIG_FILE environment variable (if defined, NLog 1.0 only - support removed in NLog 2.0)

我的web app是一個ASP.NET MVC 4專案。根據上述前三項,NLog會讀取web app目錄下的web.config、web.nlog及NLog.config,所以不會讀取到設定檔。而第4項需要設定檔的名稱為NLog.dll.nlog在bin資料夾(NLog.dll所在位置)內,而不是預設的NLog.config名稱,因此當然也讀不到。第5項就更不用說了,在NLog 2.0已經不支援了。

很明顯地,因為我的設定檔是擺在library專案內,所以解決的方式就是將NLog.config重新命名為NLog.dll.nlog。修改完後,重新編譯我的web app並執行,log檔順利寫入。

雖然重新命名設定檔名稱可以解決問題,不過並不建議這麼做。原因是參考library專案的專案可能不只web app,而web app專案也可能有兩個以上,如果各專案間對NLog.config的設定有所不同的話,很顯然就不適合將設定檔放在library專案中。最後,我在各web app專案下放置所屬的NLog.config,移除library專案中相同的設定檔。

NLog設定檔無法使用Intellisense的解決方式

Logging framework - NLog安裝與設定中介紹過NLog提供設定檔的結構描述檔以便支援Visual Studio的Intellisense功能,如此方便我們撰寫NLog設定。


然而在昨天卻出現了Intellisense無法使用的問題,在Error List視窗也出現了不少警告訊息。


看起來像是結構描述檔已經被重複定義了。

出現此問題很有可能是方案內有兩個以上的專案都安裝了NLog的結構描述檔所以造成Visual Studio把兩份以上相同的結構描述檔都cache住了。在我的例子中,我的方案檔的確有兩個web app專案分別都安裝了NLog.xsd,而且其中一個NLog.config的Intellisense可以運作而另一個卻不行。

解決這個問題有兩個方法。第一個方式是先開啟其中一個NLog.config設定檔,至XML-> Schema...找到兩個相同的NLog結構描述檔,將其中一個設定為Do not use this schema



設定完後Error List的警告訊息就會不見,而Intellisense的支援也恢復正常。然而這個方法在每次新加的專案中如果有使用到NLog.xsd的話就要記得去修改設定。

另一個一勞永逸的方式是將NLog.xsd放置到Visual Studio儲存結構描述檔的資料夾。以Visual Studio 2012為例,此資料夾是位於C:\Program Files (x86)\Microsoft Visual Studio 11.0\Xml\Schemas。接著,移除專案中的NLog.xsd,之後新增的專案若需要使用NLog不再需要放入NLog.xsd便可直接使用到Intellisense的支援了。

參考