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

2012年11月8日 星期四

EF with Repository Pattern - 簡易版

情境:
上一篇中我們有使用到一個NorthwindReposity類別來存取資料庫,不過我們也發現到這樣的寫法與呼叫端的耦合度還是有點高。若我要使用的資料所要做的事情是固定的,像是瀏覽產品資訊時的產品資料是從主資料庫中找到產品的資料表並取出指定產品的資料,那麼其實對這個部分來說其實他就只關心產品得資料如何存取和操作而已。一個簡單的想法是,我們讓呼叫端用一個介面來針對他所感興趣的部份去做操作。

實作:
我們用的資料庫依然是北風資料庫,而且和上一篇是一樣的!避免頁面切來切去這邊就直接寫囉~
using System.Data.Entity;
System.ComponentModel.DataAnnotations.Schema;
public class Northwind : DbContext
{
    public DbSet<product> Products { get; set; }

    public DbSet<category> Categories { get; set; }
}
public class Product
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public Decimal? UnitPrice { get; set; }
    public bool Discontinued { get; set; }
    [ForeignKey("Category")]
    public int CategoryID { get; set; }

    public virtual Category Category { get; set; }
}
public class Category
{
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }
    public byte[] Picture { get; set; }

    public virtual ICollection<product> Products { get; set; }
}
接下來就是我們上一篇用來存取資料庫的主要類別,也是我們這次的修改標的物。
public class NorthwindRepository : IDisposable
    {
        private Northwind _db;
        
        public NorthwindRepository()
        {
            _db = new Northwind();
        }

        public ICollection<string> ListAllProductName()
        {
            return _db.Products.Select(a => a.ProductName).ToList();
        }

        public ICollection<string> ListAllCategoryName()
        {
            return _db.Categories.Select(a => a.CategoryName).ToList();
        }

        public void Dispose()
        {
            if (_db != null)
                _db.Dispose();
        }
    }
依照先前所說,我們先來做一個讓呼叫端使用的介面吧~我們這邊是以Product資料為例子
public interface IProductRepository
{
     IList<Product> GetProducts();
     Product GetProductByID(int productId);
     Product GetProductByName(string productName);
     void InsertProduct(Product product);
     void DeleteProduct(int productId);
     void UpdateProduct(Product porduct);
}
接著我們要寫一個實作這個介面的類別,並且讓他實際去取出資料。
public class ProductReposity : IRepository
{
     private Northwind _ db = null;
     public ProductReposity(Northwind context)
     {
          if(context == null) throw new ArgumentNullException("context");
          this._db = context;
     }
     public IList<Product> GetProducts()
     {
          return _db.Products.ToList();
     }
     public Product GetProductByID(int productId)
     {
          return _db.Products.Where(a => a.ProductID == productId).Select(a => a).FirstOrDefault();
     }
     public Product GetProductByName(string productName)
     {
          return _db.Products.Where(a => a.ProductName == productName).Select(a => a).FirstOrDefault();
     }
     public void InsertProduct(Product product)
     {
          _db.Products.Add(product);
          _db.SaveChanges();
     }
     public void DeleteProduct(int productId)
     {
          Product product = _db.Products.Find(productId);
          _db.Products.Remove(product);
          _db.SaveChanges();
     }
     public void UpdateProduct(Product product)
     {
          Product target = _db.Products.First(a => a.ProductID == product.ProductID);
          target = product;
          _db.SaveChanges();
     }
以上~在呼叫端時我們就可以這樣使用:
IProductRepository db = new ProductRepository(new Northwind());
var items = db.GetProducts();
foerach(var p in items)
{
     Console.WriteLine("Product Name {0}", p.ProductName);
}
這樣我們就進一步的把呼叫端跟資料庫分離開一點了,不過寫到這邊就打住的話實在會很虛...那我們就把這個情境稍微擴大,變成「有多個資料的操作都是類似的」。依照這樣的寫法我們需要新增一個IRepository類別和與之相對應的Repository類別,既然操作都是類似的能不能介面只寫一份就好了呢?可以的!那我們就來改改吧~
public interface IRepository<T> : IDisposable
{
     IList<T> GetProducts();
     T GetProductByID(int itemId);
     T GetProductByName(string itemName);
     void InsertProduct(T item);
     void DeleteProduct(int itemId);
     void UpdateProduct(T item);
}
而把ProductRepository改成如下:
public class ProductReposity : IRepository<Product>
{
     // 判定是否已經呼叫Dispos()方法。
     private bool disposed = false;

     private Northwind _ db = null;
     public ProductReposity(Northwind context)
     {
          if(context == null) throw new ArgumentNullException("context");
          this._db = context;
     }
     public IList<Product> GetProducts()
     {
          return _db.Products.ToList();
     }
     public Product GetProductByID(int productId)
     {
          return _db.Products.Where(a => a.ProductID == productId).Select(a => a).FirstOrDefault();
     }
     public Product GetProductByName(string productName)
     {
          return _db.Products.Where(a => a.ProductName == productName).Select(a => a).FirstOrDefault();
     }
     public void InsertProduct(Product product)
     {
          _db.Products.Add(product);
          _db.SaveChanges();
     }
     public void DeleteProduct(int productId)
     {
          Product product = _db.Products.Find(productId);
          _db.Products.Remove(product);
          _db.SaveChanges();
     }
     public void UpdateProduct(Product product)
     {
          Product target = _db.Products.First(a => a.ProductID == product.ProductID);
          target = product;
          _db.SaveChanges();
     }
     // 用戶端主動呼叫表明要釋放資源的函式。
     public void Dispose()
     {
         if (_db != null)
             this.Dispose(true);
         //通知GC不用呼叫 finalize 來釋放資源。
         GC.SuppressFinalize(this);
     }
     /// <remarks>
     /// 無法被外部呼叫。
     /// 若是true表示是被用戶端呼叫,manage resource和unmanage resource都可以釋放。
     /// 若是false表示是被GC呼叫,此時應該只釋放unmanage resource。
     /// </remarks>
     protected virtual void Dispose(bool disposing)
     {
         if (!disposed)
         {
             if (disposing)
             {
                 _db.Dispose();
             }
             disposed = true;
         }
     }

     /// <summary>
     /// finalizer。這個無法自行呼叫是由GC來使用的。
     /// </summary>
     ~ProductReposity()
     {
         //以false告知Dispose函數是從垃圾回收器(GC)在呼叫Finalize。
         Dispose(false);
     }
}
ps(這邊的Dispose()寫法的好處是,若呼叫端忘記呼叫Dispose()方法還有Finalize讓GC可以幫忙把物件使用的manage/unmanage resource回收,若呼叫端呼叫Dispose()方法時可以即時把資源進行回收動作,而且不用GC又呼叫一次可以比較有效率點。)
好哩~這樣新的IRepository介面就套用成功了!其實沒什麼變嘛~不過...這樣的話之後若對資料操作的邏輯是相似的話用同一個介面就好了,不需要產生那麼多的介面,也算是省了那麼一點工吧(?) XD

2012年10月5日 星期五

獨立開發Model-使用EF Code First with existing database

前言:
在開發專案時很多時候若有連接到資料庫,通常會把Model這個部分先切割出來開發,這樣可以讓Model獨立在主project之外讓其他projects可以一起使用,在維護的時候其實也會比較方便只需要抽換DLL就好(這也算關注點分離嗎),另外就是Model天生就比較能夠獨立出來開發。不過在獨立開發Model的時候也要注意一些小東西。今天使用Entity Framework (version 5)的Code First with existing database的方式來做個示範。

實作部分:
我們選用的Database是赫赫有名的Northwind資料庫,我想這個資料庫大家應該都比我還熟了。這次我們選出Products和Categories這兩個資料表來做示範的Table吧~!順便提醒一下用Code First with existing database時會有些小地方需要注意!
首先~我們就來把Northwind database bring online吧~


把資料庫online後,接下來就切到Visual Studio中創一個新的Class Library專案吧~
專案名稱就隨便給個Project.Model來代表這個DLL是我們的Model。而因為要用Entity Framework的Code First功能,我們要引用一個EF的DLL:EntityFramework,若是用Nuget來安裝的話會連同System.Data.Entity這個DLL一起裝,不過若是沒有要用視覺設計工具來輔助的話其實是可以不用這個DLL的。
使用Code First的方式來與資料庫做互動的話需要撰寫一個繼承DbContext的類別。這裡我們就把這個類別名稱取為Northwind。

using System.Data.Entity;
///這邊我們只用到Products與Categories這兩個資料表做示範。
///DbSet這個類別代表一組在Context中的實體資料集合,但要注意這個集合內的型別(class)都是相同的,
///不能接受複合型別,像是DbSet<Product,Category>(這邊型別相當於資料庫中的資料表的概念)。
public class Northwind : DbContext
{
    // 這裡的屬性名稱要注意,需要與資料表的名稱相同!
    // 2012-11-16更正!
    // 這邊要注意的應該是DbSet裡的名稱,(也就是Product與Catagory)
    // Case 1:沒有貼Table標籤
    // Code-First預設會依據DbContext中DbSet的泛型名稱(這裡就是Products, Catagories)作為
    // 資料庫中對應的表格名稱。若資料庫中沒有這個表格Code First會幫你產生(很貼心吧,千萬小心)。
    // Case 2:Product有貼Table標籤
    // Code-First會依據標籤所給予的名稱來做為對應到資料庫中的表格名稱。
    public DbSet<Product> Products { get; set; }

    public DbSet<Category> Categories { get; set; }
}
而另外兩個資料表則如下撰寫:
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
    // Code-First 的預設欄位對應會自動把ID結尾的屬性辨識為PrimaryKey,但可以用[Key]標籤來輔助。
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public Decimal? UnitPrice { get; set; }
    public bool Discontinued { get; set; }
    /// 注意!使用 Code First 連接既有資料庫沒有加上這個 Schema 標籤會出錯誤!
    /// 因為使用Category與Product資料表,因此需要將這兩張表格的關係寫明。
    /// 若是只引用Product資料表則不用寫也沒關係。
    /// 當然若兩張表格彼此沒有直接關係的話也不用寫這個。
    [ForeignKey("Category")]
    public int CategoryID { get; set; }

    public virtual Category Category { get; set; }
}
另外一張資料表(Category)如下:
using System.Collections.Generic;
public class Category
{
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }
    public byte[] Picture { get; set; }

    public virtual ICollection<product> Products { get; set; }
}
另外我們撰寫另外一個類別來使用Northwind至這個類別(或是說資料庫)~
public class NorthwindRepository : IDisposable
    {
        private Northwind _db;
        //這裡的資料存取為求簡單沒有做依賴注入的動作。
        public NorthwindRepository()
        {
            _db = new Northwind();
        }

        public ICollection<string> ListAllProductName()
        {
            return _db.Products.Select(a => a.ProductName).ToList();
        }

        public ICollection<string> ListAllCategoryName()
        {
            return _db.Categories.Select(a => a.CategoryName).ToList();
        }

        public void Dispose()
        {
            if (_db != null)
                _db.Dispose();
        }
    }

接下來就是要注意的就是使用Code First with existing database時並不會自動幫我們把連線字串準備好,這必須得自行撰寫。因此我們在App.config(這個檔案會在安裝Entity Framework的時候幫我們把基本的東西宣告好)中,需要加入一段<connectionStrings>標籤~
<add name="Northwind"
         connectionString="Data Source=your source;Initial Catalog=Northwind;User Id=uid;Password=pwd" 
         providerName="System.Data.SqlClient"/>
若不確定連線字串如何撰寫可以參考這個網頁,裡面有很多資料庫的連線字串撰寫方法。
以上都寫好了之後其實就可以來使用我們的Northwind資料庫了~這邊我們新創一個空的web form專案,首先我們要先把我們的連線字串放置web.config中,然後就是引用Project.Model.dll了~!不過這邊要注意,引用的dll不能把copy local設為false,否則在create物件時會出現錯誤!
另外使用於console專案時,也可以直接把App.config複製過去,一樣參考DLL時copy local設為true就可以直接拿來用了!像是這樣~
using Project.Model;
static void Main(string[] args)
{
    // 能用using時儘量用,至少沒有害處。
    using (NorthwindRepository db = new NorthwindRepository())
    {
       foreach (var item in db.ListAllProductName())
       {
            Console.WriteLine("item name: {0}", item);
       }
    }
    Console.WriteLine("\nFinish!");
    Console.Read();
}

2012年9月26日 星期三

Using jQuery.ajax in a "Hello World" way

前言:

通常在寫 ASP.NET Web Form 的時候我們很習慣會把一些 html tag 以 Server control 來使用。

但是這些控制項若放在表單中,每次的POST動作都會把這些控制項的內容和狀態一起送回伺服器端,若是有需要的話倒還好,偏偏很多控制項的狀態是根本不需要維護的。像是 Button 啦~ Literal 啦~這些東西其實大多數的時候是死都不會改變的,不過因為天生架構的關係所以每次 POST 回去都會要把這些東西一起送回 Server,實在有點麻煩。所以若是這個時候可以用jQuery來取代 form 傳回 Server 並處理的話可以減輕不少流量的負擔。



實作:

一開始我們先開啟一個空的 Web From 專案~


然後新增一個Index.aspx頁面並設定為起始頁面。

在body中新增下列標籤:

<input type="text" id="textbox1" /><input type="button" id="button1" value="jQuery way" />
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="formTextbox1" runat="server"></asp:TextBox>
        <asp:Button ID="formbutton1" runat="server" Text="form way" OnClick="formbutton1_Click" />
    </div>
    </form>


並且在body中新增下列script:

<script type="text/javascript">
    function jqueryAjax() {
        $.ajax({
            url: 'ajaxhandler.ashx',
            type: 'POST',
            dataType: 'text',
            cache: 'false',
            success: function (result) {
                $('#textbox1').val(result);
            }
        })
    };
    $(function () {
        $('#button1').bind('click', jqueryAjax)
    });
</script>


這段script的作用就在於,把id為button1的按鈕的click事件與jqueryAjax函式進行綁定。而jqueryAjax函式的工作就只是呼叫名為ajaxhendler.ashx的泛型處理常式來回應client端的呼叫。

而這次我們在泛型處理常式就直接用最原始的樣貌(就是新增泛型處理常式後直接用),不做任何修改。像是底下這樣:

public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "text/plain";
    context.Response.Write("Hello World");
}


是的這次就是個標準的 hello world 級別的程式,不過其實這邊是可以進行資料庫的存取動作的。藉由把資料庫的存取放到這邊來我們可以讓UI的部份動的更順利(ps.若我富堅的毛病沒犯的話,這會在下回分享 = =)

以上這些就算是做好了使用jQuery.ajax方法來與 Server溝通的最簡單方式。底下就是使用jQuery的ajax方法來與Server互動的截圖,附上傳輸量:



另外,我們也要比較一下若是用web form來進行server的溝通的話傳輸量又會是如何:


這邊我們可以發現到,web form就算是簡單的"Hello Word"文字的傳送也需要1.27kb的傳送量,反觀若是用jQuery.ajax來溝通的話只需要344byte的傳輸量,若是傳輸的頁面更複雜的話,兩者的差距會越來越誇張!

所以囉~若是要與Servre溝通的話會建議儘量用ajax來傳輸,減輕網路流量的負擔,但這個範例是用web form,難免會有要用到控制項或是UpdatePanel這種恐怖的東西,若是轉換到ASP.NET MVC架構的話就可以避免和這種怪物正面對決的機會。

不過...若是在做專案的話是要和web form戰鬥還是要跟MVC快樂的舞蹈應該就不是我們工程師能決定的事了....

(ps.以後有機會會分享web form中使用jQuery.ajax的心得...一樣,若我富堅病沒犯的話...)

2012年9月3日 星期一

Single Login in ASP.NET Web Form

前言: 會有這樣的題目主要是因為業主的要求(廢話)。明確的說是這樣的,同一時間同一帳號只能在一個地方登入,之後登入的可以把前面登入的剔除。這個機制其實之前我並沒有想到要怎麼解,或是說沒想到不用資料庫的方式要怎麼解...(超遜的啦!)。不過認真想過後才發現這個機制其實不會太難,或是說我的解法其實很簡單,而且應該也可以用在MVC架構裡。

研究方法:
1)Session。 ASP.NET提供Session讓我們可對工作狀態進行管理,可以讓我們跨越多個要求儲存與瀏覽器工作階段關連的訊息,Session提供Key-value pairs的方式來存取這些資料。雖然我們很常拿Session來儲存使用者的資訊,像是使用者的帳號或是一些其他資訊,不過Session是每個使用者各自有的,這裡面的資訊並不會共享,因此利用Session來達成需求顯然是不行。我們需要的應用系統的全域變數,或是資料結構來對整個系統的使用者進行管理。這樣的需求我想全域應用程式類別(預設檔名為Global.asax)就是我這邊提出的解答!

2)簡述關於Global.asax的運作。
當ASP.NET應用程式收到第一個要求的時候會先使用ApplicationManager建立應用程式定義域(應用程式定義域可以隔離應用程式間的全域變數,而且能夠允許每個應用程式個別進行卸載。)。在所有核心物件初始化後會藉由建立System.Web.HttpApplication類別的實體來執行應用程式。若應用程式中有Global.asax(繼承自HttpApplication)則會改用衍生自HttpApplication的Global.asax來建立執行應用程式的實體。而當處理要求的時候則會執行下述事件:(注意!這邊僅列達成需求出最主要的兩個)

///當要求ASP.NET應用程式中的第一個資源(如:網頁)時呼叫,
///這個方法在應用程式生命週期中只會被呼叫一次。
///我們讓全域變數在這邊進行宣告。
Application_Start(object sender, EventArgs e) { }

///這個方法在MSDN裡面的解釋有點怪,不過在應用程式生命週期中該事件發生也是只有一次,
///就是當IIS重啟或是應用程式正常關閉時就會發起該事件。
///我們讓全域變數所用到的資源在這裡釋放。
Application_End(object sender, EventArgs e) { }
開始撰寫時我們要先準備一下這個管理使用者的容器類別:
/// 系統中記錄目前線上全部使用者的識別號的容器。
public sealed class SystemUserPool
{
    private static ConcurrentDictionary<string, string> _UserPool =
        new ConcurrentDictionary<string, string>();

    /// 記錄系統中新登入的使用者。
    /// userNumber = 登入者的帳號。          
    /// userIp = 登入者的 IP 位址。
    /// 使用者登入成功與否。
    public Boolean AddUser(String userNumber, String userIp)
    {
        return _UserPool.TryAdd(userNumber, userIp);
    }
 
    /// 剔除系統中指定的使用者。
    /// userNumber = 使用者的帳號。
    /// 剔除成功與否。
    public Boolean DeleteUser(String userNumber)
    {
        string str = String.Empty;
        return _UserPool.TryRemove(userNumber, out str);
    }
 
    /// 列出目前系統中在線上的使用者。
    /// 使用者列表。
    public List<string> ListAllUser()
    {
        return _UserPool.Keys.ToList<string>();
    }
 
    /// 判斷使用者是否仍在線上。
    /// userNumber = 欲查明的使用者。
    /// 若在線上則為 true,反之則為 false。
    public Boolean IsOnLine(String userNumber)
    {
        return _UserPool.Keys.Contains(userNumber);
    }
 
    /// 判斷使用者是否從同一個地方登入。
    /// userNumber = 使用者帳號。
    /// userIp = 使用者目前 IP 位址。
    /// 若 userIp 與系統中使用者的 IP 位址一致表示從同一個地方登入。
    public Boolean IsTheSameIP(String userNumber, String userIp)
    {
        String orignalIP = String.Empty;
        if (_UserPool.TryGetValue(userNumber, out orignalIP))
        {
            if (orignalIP == userIp)
                return true;
            return false;
        }
        return false;//若沒能取得,表示使用者已經離線。
    }
 
    /// 更新使用者的 IP 位址。
    /// userNumber = 使用者帳號。
    /// userIP = 使用者新的 IP 位址。
    public void UpdateUserIP(String userNumber, String userIP)
    {
        if (!IsOnLine(userNumber))
            return;
        _UserPool[userNumber] = userIP;
    }
 
    /// 把系統使用者儲蓄池清空。
    public void ClearUserPool()
    {
        _UserPool.Clear();
    }
}


在Application_Start中就這樣寫:
void Application_Start(object sender, EventArgs e) 
{
    Application["UserPool"] = new SystemUserPool();
}
void Application_End(object sender, EventArgs e)
{
    var pool = Application["UserPool"] as SystemUserPool;
    if (pool != null)
        pool.ClearUserPool();
}

以上就是我這次所提出來的解決方案!不過其實最常加在這邊的應該是記錄系統狀態的Log啦~只是Log並不在這次範圍內,以後有機會再說囉~

參考文獻:

1) HttpSessionState

2) ASP.NET應用程式生命週期 IIS 5.0/IIS6.0 IIS7.0

3) ApplicationManager

4) HttpApplication

5) ASP.NET生命週期概觀 --> 強烈推薦寫 ASP.NET Web Form的人可以去看看「網頁生命週期的其他考量」裡面的事件圖