February 23, 2014

以Castle DynamicProxy實現AOP Caching機制

AOP (Aspect-Oriented Programming)是一種用來解決Cross-Cutting Concern混雜在業務邏輯程式碼的一種技術。常見的Cross-Cutting Concern有Logging, Caching, Validation等。以下程式碼在實作cache時相當常見
public class CachedRepository : IRepository
{
    IList<string> IRepository.GetLanguages()
    {
        Debug.WriteLine("Enter IRepository.GetLanguages()...");

        ObjectCache cache = MemoryCache.Default;
        IList<string> item = cache.Get("GetLanguages") as IList<string>;

        if (item == null)
        {
            Debug.WriteLine("Cache key GetLanguages does not exists");

            IList<string> languages = Database.GetLanguages();
            cache.Set("GetLanguages", languages, new CacheItemPolicy());
            return languages;
        }

        Debug.WriteLine("Cache key GetLanguages exists");
        return item;
    }

    IList<Setting> IRepository.GetSettings()
    {
        Debug.WriteLine("Enter IRepository.GetSettings()...");

        ObjectCache cache = MemoryCache.Default;
        IList<Setting> item = cache.Get("GetSettings") as IList<Setting>;

        if (item == null)
        {
            Debug.WriteLine("Cache key GetSettings does not exists");

            IList<Setting> settings = Database.GetSettings();
            cache.Set("GetSettings", settings, new CacheItemPolicy());
            return settings;
        }

        Debug.WriteLine("Cache key GetSettings exists");
        return item;
    }
}

可以看到在需要cache的地方,我們就必須宣告ObjectCache物件,檢查相對應的cache是否已存在,若不存在則從資料庫取得資料並儲存在cache裡,存在的話則直接回傳cache物件。如果需要加入cache的方法越來越多,則類似的程式便需要重覆撰寫在各個方法中,造成不少code duplication。除此之外,處理cache的程式碼也會和資料存取的程式碼混合在一起而降低可讀性。AOP就是為了解決前述問題,提高程式碼可讀性並將不相關的程式邏輯切割開來,達到SoC (Separation of Concerns)的概念。

在引入AOP後,Repository類別將不再包含存取cache的程式碼,如
public class Repository : IRepository
{
    IList<string> IRepository.GetLanguages()
    {
        Debug.WriteLine("Enter IRepository.GetLanguages()...");
        return Database.GetLanguages();
    }

    IList<Setting> IRepository.GetSettings()
    {
        Debug.WriteLine("Enter IRepository.GetSettings()...");
        return Database.GetSettings();
    }
}

這樣是不是很簡潔?但存取cache的程式碼要寫到哪呢?

在這邊要借助Castle Project裡的Castle DynamicProxy來實作AOP機制。首先先透過nuget取得Castle Project的核心函式庫。執行以下指令

Install-Package Castle.Core

DynamicProxy主要是以Proxy Pattern來實作代理類別,在run-time時將存取cache的程式碼注入至原始類別的方法中,如上述的Repository類別。當用戶端存取Repository類別時,實際上存取到的是DynamicProxy產生的代理類別,也就是加料過的Repository類別。


要讓DynamicProxy能注入程式碼至目標類別裡,只要實作它所提供的IInterceptor介面即可,而IInterceptor介面只有一個名為Intercept的方法。
public class CacheInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Debug.WriteLine("Enter interceptor...");
        Debug.WriteLine("Invoke {0}.{1}", invocation.Method.DeclaringType.FullName, invocation.Method.Name);

        string key = string.Format("{0}.{1}", invocation.Method.DeclaringType.FullName, invocation.Method.Name);
        ObjectCache cache = MemoryCache.Default;
        object item = cache.Get(key);

        if (item == null)
        {
            invocation.Proceed();
            Debug.WriteLine("Set cache...");
            cache.Set(key, invocation.ReturnValue, new CacheItemPolicy());
            return;
        }

        Debug.WriteLine("Return cache...");
        invocation.ReturnValue = item;
        Debug.WriteLine("Leave interceptor...");
    }
}

接下來只要宣告ProxyGenerator物件並加入CacheInterceptor類別即可。
[TestMethod]

public void Cache_With_AOP()
{
    ProxyGenerator proxyGenerator = new ProxyGenerator();
    IRepository repository = new Repository();
    IRepository repositoryProxy = proxyGenerator.CreateInterfaceProxyWithTarget<IRepository>(repository, new CacheInterceptor());

    Debug.WriteLine("First run...");
    IList<string> languages = repositoryProxy.GetLanguages();
    Assert.AreEqual<string>("English", languages[0]);
    Assert.AreEqual<string>("Japanese", languages[1]);
    Assert.AreEqual<string>("Simplified Chinese", languages[2]);
    Assert.AreEqual<string>("Traditional Chinese", languages[3]);

    Debug.WriteLine("Second run...");
    IList<string> cachedLanguages = repositoryProxy.GetLanguages();
    Assert.AreEqual<string>("English", cachedLanguages[0]);
    Assert.AreEqual<string>("Japanese", cachedLanguages[1]);
    Assert.AreEqual<string>("Simplified Chinese", cachedLanguages[2]);
    Assert.AreEqual<string>("Traditional Chinese", cachedLanguages[3]);
}

可以看到在第一次呼叫Repository物件時,程式還會執行到GetLanguages方法,且cache不存在所以建立新cache,但第二次呼叫GetLanguages方法時,cache則直接被回傳。


到這裡已經達到以AOP的方式來實作cache存取機制,但要注意的是CacheInterceptor要記得做例外處理。若Intercept方法內出現了例外,程式不應該因此而中斷,而必須將例外攔截,並執行原本Repository類別內的GetLanguages方法。

完整程式碼可參考https://github.com/petekcchen/blog/tree/master/AOPCaching

February 18, 2014

工商服務 - Project Manager(台中)

公司名稱: Asia Fusion Technology

公司網站: http://www.afusion.com/

工作職缺: Project Manager

需求人數: 1人

工作地點: 台中市西區(辦公室籌備中,預計四月正式上線!)

徵求條件:
  • 新產品開發、規劃及導入。 
  • 對市場競爭者分析其技術能力及產品利基。 
  • 新產品專案時程規劃、管理、溝通協調。 
  • 制定新產品規格,評估開發的可行性,進行導入並控管時程。 
  • 執行市場與產品趨勢調查,提供研發部門做為新產品開發的參考。 
  • 進行通路推廣,統籌產品行銷活動、文宣製作與客戶說明會之規劃。 
  • 熟研發時程、成本估計者。 
  • 善溝通協調、個性積極主動、重視團隊合作。 
  • 具英文聽說讀寫能力。

加分條件:
  • 具技術背景
  • 具帶領web-based專案經驗

基本福利制度:
  • 勞保、健保、團保、三節禮金/禮品、周休二日
  • 燈光美氣氛佳,舒適的工作環境,並附有撞球桌及多項健身器材
  • 自由發揮的成長空間
  • 相關課程訓練及進修學習補助、進修假
  • 零食、飲料不定期供應
  • 不定期的聚餐、生日餐會、尾牙活動、電影欣賞、健康檢查等
  • 優於勞基法的年假及請休假制度,包括全薪病假、長輩照護假、幼兒照護假等

薪資待遇: 40,000+

聯絡人: Jonathan

請將您的中英文履歷寄至hrtw@afusion.com

合適者通知面試。面試分兩個階段,第一階段與台北公司主管面試,合適者將與加拿大公司CTO進行第二階段面試。

工商服務 - .NET Team Lead(台中)

公司名稱: Asia Fusion Technology

公司網站: http://www.afusion.com/

工作職缺: .NET Team Lead

需求人數: 2人

工作地點: 台中市西區(辦公室籌備中,預計四月正式上線!)

徵求條件:
  • 五年以上.NET(C#)開發經驗
  • 具ASP.NET MVC開發經驗
  • 會使用jQuery
  • 會使用至少一套測試框架(NUnit/MSTest)
  • 會使用至少一套版本控制工具(Git/SVN)
  • 會使用SQL Server並撰寫T-SQL或Stored Procedure
  • 具物件導向基本觀念
  • 具三(多)層式架構觀念
  • 具帶領團隊經驗
  • 良好的團隊溝通能力
  • 具Dependency Injection/Inversion of Control經驗
  • 具Design Pattern經驗
  • 具Continuous Integration經驗

加分條件:
  • 具Domain Driven Design經驗
  • 具Test Driven Development經驗
  • 具Selenium Testing經驗
  • 具Agile/Scrum經驗
  • 具英文溝通能力
  • 具跨國團隊開發經驗

基本福利制度:
  • 勞保、健保、團保、三節禮金/禮品、周休二日
  • 燈光美氣氛佳,舒適的工作環境,並附有撞球桌及多項健身器材
  • 自由發揮的成長空間
  • 相關課程訓練及進修學習補助、進修假
  • 零食、飲料不定期供應
  • 不定期的聚餐、生日餐會、尾牙活動、電影欣賞、健康檢查等
  • 優於勞基法的年假及請休假制度,包括全薪病假、長輩照護假、幼兒照護假等


薪資待遇: 60,000+

聯絡人: Jonathan

請將您的中英文履歷和過去自行撰寫過的程式碼參考(web application專案佳)寄至hrtw@afusion.com

合適者通知面試。面試分兩個階段,第一階段與台北公司Team Lead面試,合適者將與加拿大公司CTO進行第二階段面試。

February 14, 2014

工商服務 - Junior/Mid-level/Senior .NET Developer(台中)

公司名稱: Asia Fusion Technology

公司網站: http://www.afusion.com/

工作職缺: Junior/Mid-level/Senior .NET Developer

需求人數: 17人

工作地點: 台中市西區(辦公室籌備中,預計四月正式上線!)

徵求條件:
  • 一年以上.NET(C#)開發經驗
  • 具ASP.NET MVC開發經驗
  • 會使用jQuery
  • 會使用至少一套測試框架(NUnit/MSTest)
  • 會使用至少一套版本控制工具(Git/SVN)
  • 會使用SQL Server並撰寫T-SQL或Stored Procedure
  • 具物件導向基本觀念
  • 具三(多)層式架構觀念
  • 良好的團隊溝通能力

加分條件:
  • 具Domain Driven Design經驗
  • 具Design Pattern經驗
  • 具Dependency Injection/Inversion of Control經驗
  • 具Test Driven Development經驗
  • 具Selenium Testing經驗
  • 具Continuous Integration經驗
  • 具Agile/Scrum經驗
  • 具跨國團隊開發經驗
  • 具英文溝通能力

基本福利制度:
  • 勞保、健保、團保、三節禮金/禮品、周休二日
  • 燈光美氣氛佳,舒適的工作環境,並附有撞球桌及多項健身器材
  • 自由發揮的成長空間
  • 相關課程訓練及進修學習補助、進修假
  • 零食、飲料不定期供應
  • 不定期的聚餐、生日餐會、尾牙活動、電影欣賞、健康檢查等
  • 優於勞基法的年假及請休假制度,包括全薪病假、長輩照護假、幼兒照護假等

薪資待遇:
Junior: 25,000 ~ 40,000
Mid-level: 35,000 ~ 50,000
Senior: 45,000 ~ 60,000

聯絡人: Jonathan

請將您的中英文履歷和過去自行撰寫過的程式碼參考(web application專案佳)寄至hrtw@afusion.com

合適者通知面試。面試分兩個階段,第一階段與台北公司Team Lead面試,合適者將與加拿大公司CTO進行第二階段面試。

February 8, 2014

與Jenkins共舞 - 自動化測試整合

與Jenkins共舞 - 建置Visual Studio專案中,我們成功地讓Jenkins建置Visual Studio專案,然而這只是在做持續整合最基本的一項要求。在成功建置程式碼後,就可以開始導入更進階的整合功能,例如自動化測試。本篇文章將介紹如何設定Jenkins來執行專案中的測試程式,並顯示測試結果。

安裝NUnit
由於我主要是以NUnit來撰寫測試程式,所以安裝NUnit以便讓Jenkins呼叫NUnit來執行測式程式。至http://www.nunit.org/index.php?p=download下載NUnit並解壓縮至特定路徑如C:\tools\NUnit-2.6.3。


安裝MSBuild Extension Pack
MSBuild Extension Pack提供大量的MSBuild Task,其中就含有執行NUnit的Task並產生測試結果。至http://msbuildextensionpack.codeplex.com/下載MSBuild Extension Pack。


這邊下載的是MSBuild Extension Pack 4.0版本,因為我的專案是由.NET 4.0所開發。下載後解壓縮後可以看到還有3個壓縮檔,分別是MSBuild Extension Pack 4.0.8.0 Binaries.zipMSBuild Extension Pack 4.0.8.0 Help.zipMSBuild Extension Pack 4.0.8.0 Installers.zip。將MSBuild Extension Pack 4.0.8.0 Installers.zip解壓縮並安裝其中的MSBuild Extension Pack 4.0.msi。安裝後可以在C:\Program Files (x86)\MSBuild\ExtensionPack\4.0路徑下看到MSBuild Extension Pack所提供的函式庫。當然你也可以直接將MSBuild Extension Pack 4.0.8.0 Binaries.zip解壓縮後放置到相同的路徑下。



建立Targets檔案
接下來我們建立一份NUnit.targets檔案(檔案名稱可自訂),這份檔案的內容格式為XML。
<Project ToolsVersion="4.0" DefaultTargets="NUnit" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="$(MSBuildExtensionsPath)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks" Condition="Exists('$(MSBuildExtensionsPath)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks')" />
    <PropertyGroup>
        <ToolPath>C:\tools\NUnit-2.6.3\bin</ToolPath>
    </PropertyGroup>
    <Target Name="NUnit">
        <ItemGroup>
            <Assemblies Include="$(MSBuildStartupDirectory)\src\Demo\Demo.Service.IntegrationTests\bin\Release\*.dll" />
        </ItemGroup>
        <!--- Run NUnit over a collection of assemblies -->
        <MSBuild.ExtensionPack.CodeQuality.NUnit Assemblies="@(Assemblies)" ToolPath="$(ToolPath)" OutputXmlFile="$(MSBuildStartupDirectory)\NUnit.xml">
            <Output TaskParameter="Total" PropertyName="ResultTotal" />
            <Output TaskParameter="NotRun" PropertyName="ResultNotRun" />
            <Output TaskParameter="Failures" PropertyName="ResultFailures" />
            <Output TaskParameter="Errors" PropertyName="ResultErrors" />
            <Output TaskParameter="Inconclusive" PropertyName="ResultInconclusive" />
            <Output TaskParameter="Ignored" PropertyName="ResultIgnored" />
            <Output TaskParameter="Skipped" PropertyName="ResultSkipped" />
            <Output TaskParameter="Invalid" PropertyName="ResultInvalid" />
        </MSBuild.ExtensionPack.CodeQuality.NUnit>
        <Message Text="ResultTotal: $(ResultTotal)" />
        <Message Text="ResultNotRun: $(ResultNotRun)" />
        <Message Text="ResultFailures: $(ResultFailures)" />
        <Message Text="ResultErrors: $(ResultErrors)" />
        <Message Text="ResultInconclusive: $(ResultInconclusive)" />
        <Message Text="ResultIgnored: $(ResultIgnored)" />
        <Message Text="ResultSkipped: $(ResultSkipped)" />
        <Message Text="ResultInvalid: $(ResultInvalid)" />
    </Target>
</Project>

以上有幾點需注意
  1. $(MSBuildExtensionsPath)\ExtensionPack\4.0\為MSBuild Extension Pack的安裝路徑,$(MSBuildExtensionsPath)指的則是MSBuild的預設位置,如C:\Program Files (x86)\MSBuild。
  2. C:\tools\NUnit-2.6.3\bin為NUnit執行檔路徑。
  3. $(MSBuildStartupDirectory)\src\Demo\Demo.Service.IntegrationTests\bin\Release\*.dll為測試程式執行時所有會使用到的函式庫,基本上就是指向測試專案產出的bin資料夾下所有的DLL檔案。$(MSBuildStartupDirectory)為呼叫MSBuild的目錄路徑,此一路徑為所執行的Build Job下的workspace路徑,如C:\Jenkins\jobs\Demo\workspace。
  4. $(MSBuildStartupDirectory)\NUnit.xml為測試結果的輸出路徑,此檔案將被Jenkins讀取做為測試結果報表的呈現。
我們將NUnit.targets放在Jenkins所設定的workspace路徑下,如C:\Jenkins\jobs\Demo\workspace\NUnit.targets。Demo在這裡指的是在Jenkins中建立的Job名稱。


安裝Jenikns NUnit Plugin
最後一個步驟就是讓Jenkins能執行測試專案,讀取測試結果並呈現出報表。這裡我們將借助於Jenkins NUnit Plugin。至Plugin Manager中安裝Jenkins NUnit Plugin。


安裝完成後,在Build Job內新增一個Build如下,且這個Build要放在建置專案的Build之後,如此才能確保測試是在專案能成功地建置之後才執行的。


接著在Post-build Actions中加入Publish NUnit test result report,讓Jenkins能讀取到測試結果。


設定完成後執行Build Job,即可看到測試結果報表。



值得注意是,當測試不通過時,Build Job是亮黃燈(Unstable)而不是紅燈(Failed)。


參考
  1. MSBuild Extension Pack Online Help
  2. MSBuild Reserved and Well-Known Properties