2013年10月20日 星期日

DI patterns (DI 模式)

前言:
這篇文章主要講的是DI常見的方式,共四種模式這邊首先介紹最為常見的建構子注入。其實DI這個技巧我們常常都會使用到,甚至就算你不知道也會用它,雖然是什麼相當偉大的技巧但是卻是很實用也很重要的技巧。DI(Dependency Injection, 依賴注入),講的是針對抽象來撰寫程式而非實體。底下會有範例來展示這項技巧。另外,這篇文章主要是依照Dependency Injection in .NET這本書的內容來撰寫的,但有部分觀點屬於個人。

內文:
Constructor Injection (建構子注入):
這個模式應用很廣泛也是很容易實現的一個DI模式,在使用這個模式時會需要類別提供一個公開(public)並且需要一個實體作為參數的的建構子,通常的情況下這個類別只會有這麼一個建構子的存在,另外,請儘量避免overload建構子,保持只有單一一個建構子的情況。
//實現 Constructor Injection 的類別
public class MyFirstDIClass
{
   private readonly AnimalRepository repository;// DI 欄位是readonly可以在類別初始化後避免被修改。
   public MyFirstDIClass(AnimalRepository repository)
   {
      if(repository==null)// 確保注入的實體是存在的
      {
         throw new ArgumentNullException("repository");
      }
      this.repository=repository;// 注入依賴的實體
   }
}
通常所要求輸入的參數會是一個抽象類別或是一個介面(abstract class or interface),之所以使用這兩種Type是因為建構子可以不用去管真正實現的類別究竟為何,只要這個類別繼承或是實作指定的Type即可,因為對於這個注入的實體也只會使用到指定Type所提供的公開方法而已。
在使用這個方法時有個需要注意的地方,要儘量避免在建構子中撰寫其他的邏輯。建構子裡面最好就是只有建構這個類別時所需要的邏輯即可,所有初始化這個類別以外的邏輯都不應該出現在這裡,簡單來講就是維持建構子的單純。
一般來說 Constructor Injection 這個方式的優缺點為
優點:
1)能確保依賴確實的注入到類別中
2)容易實現
缺點:
1)在一些架構下很難去實現(ex. 像是在ASP.NET Web Form 這個 Framework 中就很難去實現這個依賴注入的方法,有些架構就是天生不適合使用DI)

小結:
Constructor Injection這個方法在面對依賴注入是必要的情況下很好使用。而從建構子注入依賴也可以滿足大多數的使用情況,儘量使用這個方法做為首選依賴注入的方法。

Method Injection(方法注入)
---

當遇到Dependency會依據每一次的呼叫而有所不同時,就很適合使用這種依賴注入的方式。

public class UserInfoService
{
public IEnumerable<AdvertisementEntity> ShowAdvertisement(IUserGradeService userGradeService)
{
//// 依據傳入的會員等級不同,顯示不同的優惠
return userGradeService.GetAdvertisements();
}
}
有兩種比較常見的使用時機

* 像是 add-in 一類,主程式提供統一介面注入,實作該介面的實體就會被主程式使用。

* 像是一個領域模型(Domain Entity)(常見於[領域驅動開發](https://en.wikipedia.org/wiki/Domain-driven_design)) 混合資料與商業邏輯。

優點:

1. 允許呼叫者依據情境提供相應的實體
2. 讓依賴得以注入以資料為中心的物件,而非使用組合(composition)的方式

缺點:

1. 使用限制比較多
2. 會讓依賴暴露於 API 層級,向外部的呼叫者透露較多訊息

--全文尚未完結--

2013年4月19日 星期五

using Strong Name Tool to encryption message

前言:

一般來說在.net中實現加密時我們會使用RSACryptoServiceProvider這個類別來實現產生公私鑰進行加解密的行為,而且一個簡單的ToXMLString()方法就能把公鑰(或甚至是私鑰,但實務上很不建議)給吐出來。不過除了使用這個類別來產生鑰匙外我們其實還可以利用強式名稱工具來幫我們產生鑰匙,好處是不但可以作為加解密的鑰匙來源,沒有金鑰容器名稱以及存取層級(User Layer, Machine Layer)的管理issue簡單用,還可以順便用它來簽署組件,一檔多用資源徹底利用。以下就是利用強式名稱工具來實作的方式。

內文:

強式名稱工具(Strong Name Tool)是安裝Visual Studio之後就會附上的開發用工具我們可以開啟"Visual Studio x64 Win64命令提示字元",鍵入"sn.exe /?"來觀看可以使用的參數,這裡我們會用到的就是最簡單的 -k 參數而且就是使用default值。我們就利用這個工具產生一組全新的snk檔案放在D槽吧~




我們已經可以在D槽的跟目錄下面看到產生好的Strong Name Key File了~

要注意的是使用預設的話key size長度就是1024 bit這長度與能拿來加密的文字長度會有直接關係使用預設長度的話一次能夠加密的文字長度為128bits,不過要小心在.net中會有padding的issue造成實際上可以加密的文字長度會更短(註一),這個問題的解決方式我們會在稍後的範例中看到。

準備好之後我們就來開啟一個專案試試看這snk檔要怎麼幫我們加密資料吧~

我們這邊就使用最單純的Console專案來進行測試即可~

ps.這邊記得把D槽根目錄中的snk檔案放在專案的bin資料夾底下。

ps2.為了保持目的單純不小心類別就被搞大了...

using System;
using System.Collections;
using System.IO;
using System.Security.Cryptography;

class Program
{
   private static string _strongNameKeyFile = "Test.snk";
   public static string GetPublicKey
   {
      get;
      private set;
   }
   private static string GetPrivateKey
   {
      get;
      set;
   }

   public static string Data(string plaintext)
   {
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         rsa.ImportCspBlob(File.ReadAllBytes(_strongNameKeyFile));
         GetPublicKey = rsa.ToXmlString(false);
         GetPrivateKey = rsa.ToXMLString(true);

         // base-64 encoded.
         return Convert.ToBase64String(EncryptData(plaintext));
      }
   }

   // 若資料量很大的話(資料量小以預設來說就是資料量小於117 bits,那可以直接呼叫Encrypt()來進行加密及可)
   // 就需要分段加密不然一定會炸掉,吐一個Bad Length的例外訊息。
   private static byte[] EncryptData(string rawdata)
   {
      // 憑證的金鑰預設就是1024 bit
      // buffer扣去11 bits是為了padding使用,請參考註一的連結。
      const int encryptionBufferSize = (1024 / 8) - 11;
      const int DecryptionBufferSize = 1024 / 8;

      RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
      // 這邊利用公鑰來加密,因此解密的一方需要取得密鑰才能解密,這樣可以確保有私鑰的人才有辦法看到資訊。
      rsa.FromXmlString(GetPublicKey);

      // 進行加密時都需要把字串轉成byte陣列來操作,這邊需要注意轉換的問題!
      byte[] dataEncoded = System.Text.Encoding.UTF8.GetBytes(rawdata);
      using (MemoryStream ms = new MemoryStream())
      {
         byte[] buffer;
         int pos = 0;
         int copyLength = encryptionBufferSize;
         while (true)
         {
            if (pos + copyLength > dataEncoded.Length)
               copyLength = dataEncoded.Length - pos;
            buffer = new byte[copyLength];
            Array.Copy(dataEncoded, pos, buffer, 0, copyLength);
            pos += copyLength;

            // 這邊不使用OEAP padding,使用 PKCS#1 v1.5 padding(填補法的差異請參考註一)
            // 另外也要注意雖然加密的資料需要扣除 padding 量,但是寫入的長度依然不變。
            ms.Write(rsa.Encrypt(buffer, false), 0, DecryptionBufferSize);
            Array.Clear(buffer, 0, copyLength);
            if (pos >= dataEncoded.Length)
               break;
         }
         return ms.ToArray();
      }
   }

   public static string GetPlainDataBack(string encryptdata)
   {
      string result = string.Empty;
      using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
      {
         const int decryptionBufferSize = 1024 / 8;

         // 使用公鑰加密就必須用私鑰解密。
         rsa.FromXmlString(GetPrivateKey);
         byte[] encryptdataarray = Convert.FromBase64String(encryptdata);
         using (MemoryStream ms = new MemoryStream(encryptdataarray.Length))
         {
            byte[] buffer = new byte[decryptionBufferSize];
            int pos = 0;
            int copyLength = buffer.Length;
            while (true)
            {
               Array.Copy(encryptdataarray, pos, buffer, 0, copyLength);
               pos += copyLength;
               byte[] resp = rsa.Decrypt(buffer, false);
               ms.Write(resp, 0, resp.Length);
               Array.Clear(resp, 0, resp.Length);
               Array.Clear(buffer, 0, buffer.Length);

               if (pos >= encryptdataarray.Length)
                  break;
            }
            result = System.Text.Encoding.UTF8.GetString((ms.ToArray()));
         }
      }
      return result;
   }

   static void Main(string[] args)
   {
      var plaintext = "This is a plain text!";
      var result = Data(plaintext);
      Console.WriteLine("Orignal Text:{0}", plaintext);
      Console.WriteLine("After Encryption:{0}", result);

      result = GetPlainDataBack(result);
      Console.WriteLine("After Decryption:{0}",result);
      Console.Read();
   }
}


結語:
以上就是使用強式名稱工具來達成資訊加密、解密的需求,我們可以發現若對方要可以解讀這個密文那snk檔是對方勢必得取得的檔案,因此該檔案的存取是一定要控管的,這是不方便之處。另外其實實務上除了加解密之外還會順手做數位簽章....這就留到下次吧...


參考資料:

註一:http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt.aspx

參考1:http://www.dotblogs.com.tw/huanlin/archive/2008/04/23/3309.aspx

參考2:http://msdn.microsoft.com/zh-tw/library/system.security.cryptography.rsacryptoserviceprovider%28v=vs.110%29.aspx

參考3:http://msdn.microsoft.com/zh-tw/library/k5b5tt23%28v=vs.110%29.aspx

參考4:http://en.wikipedia.org/wiki/RSA_%28algorithm%29

2012年11月28日 星期三

ASP.NET Page-Life-Cycle

前言:
開發 ASP.NET Web Form 的工程師對於網頁生命週期(Page Life Cycle)一定要熟記在心中,也要知道每個循環中的會發生的個事件的主要目的,才不會在需求要修改或是出現Bug時不知道該往哪裡找問題。不過,講是這樣講其實ASP.NET的網頁生命週期需要注意的細節是滿多的,而且一個週期內會發生的事件可能比你想像中的還要多。

內文:
一般來說網頁生命週期會經過 "網頁要求"=>"開始"=>"初始化"=>"載入"=>"回傳事件處理"=>"呈現"=>"卸載"

階段 說明
網頁要求 在網頁生命週期開始前會發生網頁要求。當使用者要求網頁時,ASP.NET 會判斷是否需要剖析和編譯網頁 (因此開始網頁的生命週期),或是可以在不執行網頁的情況下,傳送網頁的快取版本做為回應。
開始 在開始階段,會先設定網頁屬性,如 Request 與 Response。在這個階段中,網頁也會判斷要求是否為回傳或是新的要求,然後設定 IsPostBack 屬性。網頁同時也會設定 UICulture 屬性。
初始化 在網頁初始化期間,可以使用網頁上的控制項,並且設定每個控制項的 UniqueID 屬性。如果適用,也會將主版頁面與佈景主題套用到網頁。如果目前的要求是回傳,則尚未載入回傳資料,並且控制項屬性值並未還原至檢視狀態提供的值。
載入 在載入期間,如果目前的要求是回傳,就會使用從檢視狀態和控制項狀態復原的資訊載入控制項屬性。
回傳事件處理 如果是回傳要求,會先呼叫控制項事件處理常式,然後才會呼叫所有驗證程式控制項的 Validate 方法,以設定個別驗證程式控制項與網頁的 IsValid 屬性。
呈現 在呈現前,會儲存網頁和所有控制項的檢視狀態。在呈現階段,網頁會呼叫每個控制項的 Render 方法,藉此提供文字寫入器將其輸出寫入網頁之 Response 屬性的 OutputStream 物件。
卸載 完整呈現網頁之後,會引發 Unload 事件,然後傳送至用戶端予以捨棄。此時將會執行網頁屬性 (如 Response 與 Request) 的卸載及清除工作。
資料來源 表一:MSDN 一般網頁生命週期階段 Page Life Cycle

而在這樣的生命週期階段中會有數十個事件依序發生,而不論是作為網頁容器的 Page 類別或是使用者控制項(User Control)或是伺服器控制項(Server Constrol)幾乎都有相對應的事件。
Page 頁面是每個 Web Form 都會繼承的類別,依據需求上面也可以放置自行撰寫的使用者控制項(User Control),或是裡面也可以直接擺上 Server Control,因此我們可以把這個類別是為一個容器,裡面可以擺放很多顯示用的控制項,而這個控制項裡會有很多事件會在一個 request 進來後依序引發,以下就是網頁生命週期的「生命週期事件」:

OnPreInit:
這個事件會在初始化之前就引發,通常會拿來檢查 PostBack 屬性、建立或是重建動態控制項、動態設定主版頁面、動態設定 Theme 屬性、讀取貨設定設定黨屬性值。

OnInit:
初始化所有控制項並套用任何面板設定後引發。個別控制項的 OnInit 事件會在容器前執行。可以在這個時候讀取或是初始化控制項的屬性。

OnInitComplete:
發生在網頁初始化結束時。

OnPreLoad:
這邊會於載入本身以及所包含的控制項的 ViewState,以及處理 request 執行個體中所附的回傳資料後引發。
若有自定驗證的話通常我會在這邊來進行,就不用載入太多東西。

OnLoad:
這邊容器會呼叫本身的 OnLoad 方法,然後以遞迴的方式對每一個控制項執行相同的動作。(因此控制項的 Load 會比較晚執行)通常我會於此設定控制項中的屬性,以及重建資料庫連線。

OnLoadComplete:
於 OnLoad 事件處理完畢後引發。

OnPreRender:
容器會呼叫自己的 PreRender 事件,然後也是以遞迴的方式對每一個控制項執行相同的動作。這個事件可以在呈現(Render)階段前對控制項的屬性做最後的變更。
通常我會在這邊把需要的 javascript 或 CSS 給引入或是對顯示邏輯進行修改。

OnSaveStateComplete:
這個事件是在儲存網頁所有控制項的 ViewState 後引發,因為是在呼叫Render方法之前,因此所做的任何變更依舊會影響控制項的呈現,但是因為 ViewState 已經儲存了,所以下一次的 PostBack 並不會擷取這些變更!
(因此PreRender才會被視為呈現前最後一個可以變更控制項的事件,因為此時變更後會被儲存下來!)

Render:
這不是個事件...在呈現這個階段每個控制項都會呼叫自己的Render方法來產生各自的HTML Tag。

OnUnload:
會先由控制項引發,控制項都引發完畢後才是容器引發。通常在這邊會對頁面使用到的特定資源進行釋放,像是與資料庫的連線或是關閉開啟的檔案。

以上資料來源 MSDN 生命週期事件 + 我的胡說八道

當然上面這些說明的文件看完之後還是需要手動來驗證一下才比較有感覺!
就讓我們來驗證一下吧!

驗證:

實驗一:
1)首先我們先新增一個空白ASP.NET Web Form專案,並且新增項目選擇Web Form,命名為Index.aspx。

2)打開 Package Manager Console 安裝NLog Install-Package NLog Install-Package NLog.Config
這邊安裝NLog是為了用來記錄每個事件的觸發順序使用。

3)修改NLog.config
<targets>
    <target xsi:type="File" name="test" fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} ${message}"/>
  </targets>

  <rules>
    <logger name="test" minlevel="Trace" writeTo="test" />
  </rules>

4)在Index.aspx.cs中寫下日誌
using System;
using NLog;
public partial class Index : System.Web.UI.Page
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnPreInit(EventArgs e)
    {
        _logger.Trace("OnPreInit event execute!");
        base.OnPreInit(e);
    }

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnInitComplete(EventArgs e)
    {
        _logger.Trace("OnInitComplete event execute!");
        base.OnInitComplete(e);
    }

    protected override void OnPreLoad(EventArgs e)
    {
        _logger.Trace("OnPreLoad event execute!");
        base.OnPreLoad(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnLoadComplete(EventArgs e)
    {
        _logger.Trace("OnLoadComplete event execute!");
        base.OnLoadComplete(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnPreRenderComplete(EventArgs e)
    {
        _logger.Trace("OnPreRenderComplete event execute!");
        base.OnPreRenderComplete(e);
    }

    protected override void OnSaveStateComplete(EventArgs e)
    {
        _logger.Trace("OnSaveStateComplete event execute!");
        base.OnSaveStateComplete(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("OnUnload event execute!");
        base.OnUnload(e);
        _logger.Trace("-------------------");
    }
}

5)按下F5成功執行後直接關起來

6)觀察寫下來的Log檔

這邊我們可以發現頁面的事件順序大致上就是先初始化、載入、呈現最後就是卸載。
但若我們在頁面上面多加一個使用者控制項(User Constrol)呢?

實驗二:

1)在原本的專案中新增一個ascx檔,並且附上一個 Server Control。

2)新增如下內容
<asp:TextBox ID="ucTextBox1" runat="server" 
             OnInit="ucTextBox1_Init"
             OnLoad="ucTextBox1_Load"
             OnPreRender="ucTextBox1_PreRender"
             OnUnload="ucTextBox1_Unload"></asp:TextBox>
public partial class UserControl1 : System.Web.UI.UserControl
{
    private Logger _logger = LogManager.GetLogger("test");

    protected override void OnInit(EventArgs e)
    {
        _logger.Trace("User Control OnInit event execute!");
        base.OnInit(e);
    }

    protected override void OnLoad(EventArgs e)
    {
        _logger.Trace("User Control OnLoad event execute!");
        base.OnLoad(e);
    }

    protected override void OnPreRender(EventArgs e)
    {
        _logger.Trace("User Constrol OnPreRender event execute!");
        base.OnPreRender(e);
    }

    protected override void OnUnload(EventArgs e)
    {
        _logger.Trace("User Constrol OnUnload event execute!");
        base.OnUnload(e);
    }

    protected override void OnDataBinding(EventArgs e)
    {
        _logger.Trace("User Control OnDataBinding event execute!");
        base.OnDataBinding(e);
    }

    #region 伺服器控制項的事件
    protected void ucTextBox1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Unload event execute!");
    }

    protected void ucTextBox1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 PreRender event execute!");
    }

    protected void ucTextBox1_Load(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Load event execute!");
    }

    protected void ucTextBox1_Init(object sender, EventArgs e)
    {
        _logger.Trace("ucTextBox1 Init event execute!");
    } 
    #endregion
}

3)按下F5成功執行後直接關起來

4)觀察日誌檔

這邊我們可以觀察到初始化時會是控制項的初始化先進行,然後才是 User Control 的初始化,最後是 Page 的初始化(容器的初始化會比較晚)。
而在載入的時候會先由 Page 先行載入,然後是 User Control 的載入,最後才是伺服器控制項的載入(容器會先行載入)。
而在卸載的時候則是控制項先卸載,然後是 User Constrol,最後才是 Page(容器的卸載的比較晚)。

實驗三:
現在我們在 Page 頁面也新增兩個 button 來觀察看看各控制項的事件引發順序。

1)我們分別在使用者控制項(User Control)的前後增加一個伺服器控制項(Server Control)。現在Index.aspx就會變成下面這樣:
<form id="form1" runat="server">
    <div>
        <asp:Button ID="button1" runat="server" Text="Firstbutton" 
                    OnInit="button1_Init"
                    OnLoad="button1_Load"
                    OnPreRender="button1_PreRender"
                    OnUnload="button1_Unload" />
        <uc:UserControl1 ID="uc1" runat="server" Visible="true" />
        <asp:Button ID="button2" runat="server" Text="Secondbutton" 
                    OnInit="button2_Init"
                    OnLoad="button2_Load"
                    OnPreRender="button2_PreRender"
                    OnUnload="button2_Unload" />
    </div>
</form>

2)在兩個 button 的事件中寫下日誌...方法就跟上面的寫法一樣...
#region 伺服器控制項的事件
    protected void button1_Init(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Init event execute!");
    }

    protected void button1_Load(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Load event execute!");
    }

    protected void button1_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton PreRender event execute!");
    }

    protected void button1_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Fisrtbutton Unload event execute!");
    }
    protected void button2_Init(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Init event execute!");
    }

    protected void button2_Load(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Load event execute!");
    }

    protected void button2_PreRender(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton PreRender event execute!");
    }

    protected void button2_Unload(object sender, EventArgs e)
    {
        _logger.Trace("Secondbutton Unload event execute!");
    } 
#endregion

3)按下F5之後直接關掉觀察日誌檔...

初始化>>
這邊我們可以觀察到初始化的時候會依照控制項出現的順序依序初始化,因此第一個 button 會先被初始化,然後是使用者控制項中的 textbox 初始化,接下來就是使用者控制項的初始化!接下來才是第二個 button 的初始化,最後才是 Page 頁面(基底容器) 的初始化。(個別控制項的初始化會在它的容器前執行)

載入>>
載入時依據MSDN官方說法是容器會先控制項載入,我們來驗證吧~!這邊我們可以看到 Page 頁面的載入確實先觸發了!然後是第一個 button 的載入,接下來是使用者控制項的載入,接著使用者控制項中的 textbox 載入,最後是第二的 button 的載入。載入就一這樣的順序結束了!

呈現(Render)>>
這邊很特別,我們可以看到這個 Render 的順序根本就是頁面從上到下的順序依序呼叫 PreRender,不過也難怪啦~畢竟這邊是要產生 HTML Tag 是最後要呈現在瀏覽器的樣子,會有這樣的行為其實不意外,要是順序不是這樣最後呈現可能也會怪怪的。

卸載>>
卸載這邊則會讓控制項先行卸載,最後才是容器卸載。所以這邊第一個 button 就會先執行卸載,接下來是使用者控制項中的 textbox 卸載,接著是使用者控制項,然後是第二個 button,這些控制項都卸載後最後才是 Page 頁面的卸載。

結語:
在撰寫 ASP.NET Web Form 時了解網頁生命週期是不可或缺的知識,他可以幫助你理解 Web Form 架構時的一些眉角,能夠在正確的時間點做正確的事情。雖然現在在強大的 javascript library 或 framework 的幫助下,已經讓網頁的使用者體驗愈趨近 desktop 導致 Web Form 的式微,但是偶而還是會有需要使用到的時候,到時候就會需要了解這方面的知識了。

參考資料:
MSDN
ASP.NET Page Life Cycle Overview
dotblog
In 91 [ASP.NET]Page Lify Cycle整理
m@rcus學習筆記 [.NET]ASP.NET網頁生命週期Page Life-Cycle

2012年11月20日 星期二

Entity Framework Migrations - 入門使用

前言:
使用 Entity Framework 的 Code First 來做資料存取層與既存資料庫溝通時多少都會遇到資料庫 Schema 變更的時候,只是這個時候若是有用到 Code First 來新增表格的話就會遇到像上一篇一樣的問題;執行時期使用資料庫的話會引發錯誤訊息,而且會通知因為資料庫的 Schema 變更需要使用 Migrations 來解決問題。因此這篇文章就是要來說明一般來說使用 Code First 來開發遇到資料庫 Schema 要變更時的正規作法應該怎麼做。

內文:
我們就用上一篇所生出來的資料庫來實作看看吧~


不過為了方便起見,我們就直接先把 Products 這個資料表先刪除,我們來看看用 Code First 的方式開發的話若要新增一個資料表的正規方式是什麼?(這邊正規也只是我在講而已,只是這麼做的話可以免除之後的麻煩)
首先我們要先打開套件管理員的命令列視窗「Tools => Library Package Manager => Package Manager Console」,然後要用 Migrations 的話就要先把功能打開,需要下這樣的指令:「PM >> Enable-Migrations」,成功的同時我們的專案會新增一個Migrations資料夾,並且會給你一個 Configrations.cs 類別,若沒有要使用自動Migrations的話就可以先不用動這個類別的內容。接下來因為需求異動的關係我們需要多一個 Products 資料表來儲存產品的資訊。Products 的資料表如下
/// 
/// 產品的實體。
/// 
[Table("Products")]
public class Product
{
    /// 
    /// 產品的識別號。
    /// 
    [Key]
    [ScaffoldColumn(false)]
    public int ID { get; set; }
    /// 
    /// 產品的名稱。
    /// 
    public string Name { get; set; }
    /// 
    /// 產品的價格。
    /// 
    public double Price { get; set; }
    /// 
    /// 生產產品的工廠名稱。
    /// 
    public string FactoryName { get; set; }
}

對於資料庫的 Schema 的異動我們可以在 Package Manager Console 中輸入 Add-Migrations xxxxxx
xxxxxx就是新增的類別的名稱,通常會是這次異動的表格、欄位的名稱像是AddProducts之類的。這邊我是用AddTableProducts
using System;
using System.Data.Entity.Migrations;

public partial class AddTableProducts : DbMigration
{
    // Operations to be performed during the upgrade process.
    public override void Up()
    {
    }

    // Operations to be performed during the downgrade process.
    public override void Down()
    {
    }
}
使用 Migrations 時一定要實作這兩個方法,而且內容要互相對應。
什麼意思呢?就是說若今天是要新增一個 Table 的話,那我們就要在 Up() 中放置新增 Table 的程式,而在 Down() 中放置捨棄該 Table 的程式像是底下這樣:
public partial class AddTableProducts : DbMigration
{
    public override void Up()
    {
        // 新增一個資料表就呼叫CreateTable方法,需指名資料表明稱,以及資料表各欄位的資料型態
        CreateTable(
            "dbo.Products",
            c => new
                {
                    ID = c.Int(nullable: false, identity: true),
                    Name = c.String(),
                    Price = c.Double(nullable: false),
                    FactoryName = c.String(),
                })
            .PrimaryKey(t => t.ID);
            
    }
        
    public override void Down()
    {
        // Down 這邊要放是 CreateTable的相反方法,這邊就是 DropTable方法
        DropTable("dbo.Products");
    }
}
寫完之後我們在 Package Manage Console 中下個 Update-Database 指令,順利執行完畢之後我們去資料庫中就可以看到新增後的資料表了~
至於新增一筆欄位我們也可以如法炮製,若我們現在 Products 資料表又多了一筆欄位的需求,要把產品的庫存數量記錄下來,首先我們先在 Model 中修改新的 Products 資料表的 Schema (我們在最後面新增了一個屬性"Quantity"):
/// 
/// 產品的實體。
/// 
[Table("Products")]
public class Product
{
    /// 
    /// 產品的識別號。
    /// 
    [Key]
    [ScaffoldColumn(false)]
    public int ID { get; set; }
    /// 
    /// 產品的名稱。
    /// 
    public string Name { get; set; }

    /// 
    /// 產品的價格。
    /// 
    public double Price { get; set; }

    /// 
    /// 生產產品的工廠名稱。
    /// 
    public string FactoryName { get; set; }

    /// 
    /// 產品於庫存的數量。
    /// 
    public int Quantity { get; set; }
}
好哩~這樣Code就算改好了,接下來我們只要下個確認異動的指令讓資料庫異動就好了~
我們在 Package Manage Console 中鍵入「Update-Database」等待執行完畢我們就可以去資料庫確認一下異動是否成功了(其實只要 Console 沒有吐出例外訊息應該都是沒有問題的)

噹~噹~Great!資料庫果然真的有這麼一個欄位在Products資料表中生出來了!
接下來在執行時期也可以很放心的不會吐出任何例外訊息因為資料庫異動而要你去 Migrations 了。
我們也可以來順便看看使用 Migrations 時會在 Demo 資料庫中的 Products 資料表的系統資料表產生的 _Migrations 資料表的內容。
我們這個範例中總共做了 Migrations 兩次,一次是新增資料表,一次是新增欄位:
嗯~果然在這個資料表中我們也發現了兩筆資料。

小結:
在使用 Entity Framework 的 Code First 時對開發人員真的是很方便,以前需要很麻煩的資料庫與程式碼互相切換,還要學會怎麼寫 SQL 這些麻煩事情,使用 Entity Framework 這種 ORM solution 都可以解決掉。而且這次 EF 所提供的 Migrations 功能又可以讓開發人員直接在程式碼中對資料庫異動,完全不需要離開舒適的開發環境。說真的,現在開發要與資料庫互動而且是 MS SQL 那目前的最佳方案我想就是 EF 的 Code First 了!

2012年11月16日 星期五

Code First with existiong db - 手殘問題

前言:
話說之前那一篇的 Entity Framework 觀念其實還是有很多地方是不大清楚的,像是使用 Code First with existing database 時什麼情況會異動到資料庫?什麼情況下需要做Migrations?什麼情況下會直接吐個 Exception 給你而不是自動幫你多個 Table 或 Column?,所以今天就趁著一點時間來寫一下從接觸至今的處理經驗了...(說的好像經驗老到其實還是個新手)

前置工作:
這次的範例中我們會需要一個無辜的資料庫讓我們(恣意蹂躪)測試。
就讓我們把這個偉大的資料庫命名為...Demo!

唔...其實Products先不用建起來,我們可以來做個實驗看看什麼時候會觸發Migrations...好~建立起這個資料庫之後我們就可以馬上來寫Code玩玩看啦~!
這裡我們就使用 Console Application 就好。我們建立一個名為 EFMigrationDemo 的專案,並在根目錄處新增一個 Model 資料夾裡面放置我們最重要的類別。
在寫 Code 之前我們要先把 EntityFramework 裝起來~就直接打開 Package Manager Console 吧!請在 Console 中直接輸入:
Install-Package EntityFramework 若不放心是否為最新就接著下:
Update-Package EntityFramework 這樣就可以保證你所指定的套件是最新的!
前置工作這樣就算是告一段落了~

寫Code囉:
我們新增一個類別檔案於 Model 資料夾中
using System.Data.Entity;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EFMigrationDemo.Model
{
    public class Customers // 類別名稱與資料庫中的表格名稱一樣
    {
        // 標記該屬性(在資料庫中為欄位)為 Promary key
        //(若屬性結尾為 ID 依照 Conversion naming 規則 EF 會自己判斷出來)
        [Key]
        public int ID { get; set; }

        // 標記該欄位是不能為Null的
        [Required]
        public string Name { get; set; }

        [Required]
        public string Address { get; set; }

        [Required]
        public string PhoneNumber { get; set; }

        [Required]
        public string Email { get; set; }

        // 標記這個屬性對應到資料庫中的欄位名稱為 NickName 而非 TheNickName
        // 而且該欄位的內容長度不超過50
        [Column("NickName")]
        [MaxLength(50, ErrorMessage = "Your Nick Name too long")]
        public string TheNickName { get; set; }
        
        // 標記這個屬性並不會對應到資料庫中的任何欄位
        [NotMapped]
        public string NameplusNickName { get { return Name + NickName; } }

        // 這是個類別所以還是可以有方法,而且不會被認為是欄位或是預儲程序
        public void PrintName() { Console.WriteLine("My name is {0}", this.Name); }
    }

    // 繼承 DbContext 的類別可以當成是資料庫來進行操作,預設會去 Config 檔中抓取 connectionString 的 section 裡與類別名稱相同的項目,像是:
    // <add name="Demo" connectionString="" providerName="">
    // 而類別名稱其實也可以不用跟資料庫名稱一樣
    public class Demo : DbContext
    {
        // 被 DbSet 包起來的類別會被判定為是一個實體(也就是一個表格)
        // 所以屬性名稱並不會影響到表格名稱
        public DbSet<Customers> Customer { get; set; }
    }
} 

這樣我們的 Model 類別就算準備好了,接下來就是準備呼叫端了~
using EFMigrationDemo.Model;
        
        static void Main(string[] args)
        {
            Program p = new Program();
            p.Run();
            Console.WriteLine("Process Run has been finished");
            Console.Read();
        }

        void Run()
        {
            Console.WriteLine("Code First begining...");
            using (Demo db = new Demo())
            {
                var result = (from p in db.Customers
                              select p).FirstOrDefault();
                if (result != null)
                    Console.WriteLine("Customer Name is {0}", result.NameplusNickName);
            }
        }

執行之後就是會印出來資料庫內的第一筆資料哩~
以上就是很順利的 Code First 運作情況,當然事事不會都這麼順利又美好的,若我Customers類別名稱跟資料庫不同的話呢?像是這樣:
    public class MyCustomers
    {
        [Key]
        public int ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Address { get; set; }

        [Required]
        public string PhoneNumber { get; set; }

        [Required]
        public string Email { get; set; }

        [Column("NickName")]
        [MaxLength(50, ErrorMessage = "Your Nick Name too long")]
        public string TheNickName { get; set; }
        
        [NotMapped]
        public string NameplusNickName { get { return Name + NickName; } }
    }
    
    
    public class Demos : DbContext
    {
        public DbSet<Mycustomersgt; Customer { get; set; }
    }

重新建置後再次執行Program.cs的內容!Console會印出:
Code First begining...
Process Run has been finished
沒了...但是讓我來看看DB有什麼變化吧
噹~噹~!
恭喜你剛剛用 Code First 新建一個 Table 了耶!等等...這不是你要的效果嗎?! Entity Framework 一臉疑惑的問到
這時候你才發現到你剛剛手殘不小心把Table名稱打錯了,改快把名稱改回來重新建置一次,建置成功!讓我們再Run一次吧~
你被殘酷的拒絕了...不過錯誤訊息很貼心的說你應該要用Migration來整合資料庫,因為已經不大一樣了。停!它怎麼知道資料庫不一樣了?
不要急...讓我們看看這張圖吧你就知道為什麼它會知道資料庫被動過了
喔!什麼時候在系統資料表中新增了一個_MigrationHistory資料表了?!而且裡面還有一筆資料!
唔...這就是用 Code First 異動資料庫時它生出來的東西,在這種情況下要重新 run 時就請用 Migration 整合後再說吧!
不過先讓我們看看另外一種錯誤吧!

當我們在撰寫 Customers 類別時實在太粗心了,不小心多加了一個不屬於這個資料表的欄位(假設屬性名稱叫NotExist)也就是說多了一個屬性,而且也沒有貼上[NotMapped]標籤,就直接很開心的 ctr+Alt+b 然後 F5 就下去了。依據經驗法則是不是會在資料庫中多一個欄位呢?讓我們看看吧~
嗯...是個錯誤訊息,表明了這欄位在 Customers 中找不到,所以放心~資料庫中的 Customers 表格也沒有多一個欄位。

最後(ps.Migration怎麼用就留到下一篇吧),預設上 DbContext 會去找跟類別名稱相同的連線字串名稱,但是其實是可以自訂的,這需要參考Syste.Data.Entity.dll。實際方式像是這樣:
public class Demos : DbContext
    {
        public DbSet<customers> Customer { get; set; }
        
        // 這裡呼叫 DbContext 的建構子並且給予一個字串,這個字串就是所指定的連線字串名稱
        // 若有給定的話就不會用預設的方式去找連線字串名稱了
        public Demos() : base("Demo") { }
    }

呼~這篇累積了好多 bug 沒有修啊,但是我打字有點累了...就下回待續吧 XD