常常有朋友問,Attribute是什麼?它有什麼用?好像沒有這個東東程序也能運行。實際上在.Net中,Attribute是一個很是重要的組成部分,爲了幫助你們理解和掌握Attribute,以及它的使用方法,特意收集了幾個Attribute使用的例子,提供給你們參考。mysql
在具體的演示以前,我想先大體介紹一下Attribute。咱們知道在類的成員中有property成員,兩者在中文中都作屬性解釋,那麼它們究竟是不是同一個東西呢?從代碼上看,明顯不一樣,首先就是它們的在代碼中的位置不一樣,其次就是寫法不一樣(Attribute必須寫在一對方括符中)。sql
首先,咱們確定Attribute是一個類,下面是msdn文檔對它的描述:
公共語言運行時容許你添加相似關鍵字的描述聲明,叫作attributes, 它對程序中的元素進行標註,如類型、字段、方法和屬性等。Attributes和Microsoft .NET Framework文件的元數據保存在一塊兒,能夠用來向運行時描述你的代碼,或者在程序運行的時候影響應用程序的行爲。數據庫
在.NET中,Attribute被用來處理多種問題,好比序列化、程序的安全特徵、防止即時編譯器對程序代碼進行優化從而代碼容易調試等等。下面,咱們先來看幾個在.NET中標準的屬性的使用,稍後咱們再回過頭來討論Attribute這個類自己。(文中的代碼使用C#編寫,但一樣適用全部基於.NET的全部語言)編程
在C#中存在着必定數量的編譯器指令,如:#define DEBUG, #undefine DEBUG, #if等。這些指令專屬於C#,並且在數量上是固定的。而Attribute用做編譯器指令則不受數量限制。好比下面的三個Attribute:設計模式
下面的代碼演示了上述三個屬性的使用:數組
#define DEBUG //這裏定義條件 using System; using System.Runtime.InteropServices; using System.Diagnostics; namespace AttributeDemo { class MainProgramClass { [DllImport("User32.dll")] public static extern int MessageBox(int hParent, string Message, string Caption, int Type); static void Main(string[] args) { DisplayRunningMessage(); DisplayDebugMessage(); MessageBox(0,"Hello","Message",0); Console.ReadLine(); } [Conditional("DEBUG")] private static void DisplayRunningMessage() { Console.WriteLine("開始運行Main子程序。當前時間是"+DateTime.Now); } [Conditional("DEBUG")] [Obsolete] private static void DisplayDebugMessage() { Console.WriteLine("開始Main子程序"); } } }
若是在一個程序元素前面聲明一個Attribute,那麼就表示這個Attribute被施加到該元素上,前面的代碼,[DllImport]施加到MessageBox函數上, [Conditional]施加到DisplayRuntimeMessage方法和DisplayDebugMessage方法,[Obsolete]施加到DisplayDebugMessage方法上。安全
根據上面涉及到的三個Attribute的說明,咱們能夠猜到程序運行的時候產生的輸出:DllImport Attribute代表了MessageBox是User32.DLL中的函數,這樣咱們就能夠像內部方法同樣調用這個函數。服務器
重要的一點就是Attribute就是一個類,因此DllImport也是一個類,Attribute類是在編譯的時候被實例化的,而不是像一般的類那樣在運行時候才實例化。Attribute實例化的時候根據該Attribute類的設計能夠帶參數,也能夠不帶參數,好比DllImport就帶有"User32.dll"的參數。Conditional對知足參數的定義條件的代碼進行編譯,若是沒有定義DEBUG,那麼該方法將不被編譯,讀者能夠把#define DEBUG一行註釋掉看看輸出的結果(release版本,在Debug版本中Conditional的debug老是成立的)。Obsolete代表了DispalyDebugMessage方法已通過時了,它有一個更好的方法來代替它,當咱們的程序調用一個聲明瞭Obsolete的方法時,那麼編譯器會給出信息,Obsolete還有其餘兩個重載的版本。你們能夠參考msdn中關於的ObsoleteAttribute 類的描述。異步
除了.NET提供的那些Attribute派生類以外,咱們能夠自定義咱們本身的Attribute,全部自定義的Attribute必須從Attribute類派生。如今咱們來看一下Attribute 類的細節:分佈式
protected Attribute(): 保護的構造器,只能被Attribute的派生類調用。
三個靜態方法:
static Attribute GetCustomAttribute():這個方法有8種重載的版本,它被用來取出施加在類成員上指定類型的Attribute。
static Attribute[] GetCustomAttributes(): 這個方法有16種重載版本,用來取出施加在類成員上指定類型的Attribute數組。
static bool IsDefined():由八種重載版本,看是否指定類型的定製attribute被施加到類的成員上面。
實例方法:
bool IsDefaultAttribute(): 若是Attribute的值是默認的值,那麼返回true。
bool Match():代表這個Attribute實例是否等於一個指定的對象。
公共屬性: TypeId: 獲得一個惟一的標識,這個標識被用來區分同一個Attribute的不一樣實例。
咱們簡單地介紹了Attribute類的方法和屬性,還有一些是從object繼承來的。這裏就不列出來了。
下面介紹如何自定義一個Attribute: 自定義一個Attribute並不須要特別的知識,其實就和編寫一個類差很少。自定義的Attribute必須直接或者間接地從Attribute這個類派生,如:
public MyCustomAttribute : Attribute { ... }
這裏須要指出的是Attribute的命名規範,也就是你的Attribute的類名+"Attribute",當你的Attribute施加到一個程序的元素上的時候,編譯器先查找你的Attribute的定義,若是沒有找到,那麼它就會查找「Attribute名稱"+Attribute的定義。若是都沒有找到,那麼編譯器就報錯。
對於一個自定義的Attribute,你能夠經過AttributeUsage的Attribute來限定你的Attribute 所施加的元素的類型。代碼形式以下: [AttriubteUsage(參數設置)] public 自定義Attribute : Attribute { ... }
很是有意思的是,AttributeUsage自己也是一個Attribute,這是專門施加在Attribute類的Attribute. AttributeUsage天然也是從Attribute派生,它有一個帶參數的構造器,這個參數是AttributeTargets的枚舉類型。下面是AttributeTargets 的定義:
public enum AttributeTargets { All=16383, Assembly=1, Module=2, class="4", Struct=8, Enum=16, Constructor=32, Method=64, Property=128, Field=256, Event=512, Interface=1024, Parameter=2048, Delegate=4096, ReturnValue=8192 }
做爲參數的AttributeTarges的值容許經過「或」操做來進行多個值得組合,若是你沒有指定參數,那麼默認參數就是All 。 AttributeUsage除了繼承Attribute 的方法和屬性以外,還定義瞭如下三個屬性:
AllowMultiple: 讀取或者設置這個屬性,表示是否能夠對一個程序元素施加多個Attribute 。
Inherited:讀取或者設置這個屬性,表示是否施加的Attribute 能夠被派生類繼承或者重載。
ValidOn: 讀取或者設置這個屬性,指明Attribute 能夠被施加的元素的類型。
using System; namespace AttTargsCS { // 該Attribute只對類有效. [AttributeUsage(AttributeTargets.Class)] public class ClassTargetAttribute : Attribute { } // 該Attribute只對方法有效. [AttributeUsage(AttributeTargets.Method)] public class MethodTargetAttribute : Attribute { } // 該Attribute只對構造器有效。 [AttributeUsage(AttributeTargets.Constructor)] public class ConstructorTargetAttribute : Attribute { } // 該Attribute只對字段有效. [AttributeUsage(AttributeTargets.Field)] public class FieldTargetAttribute : Attribute { } // 該Attribute對類或者方法有效(組合). [AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)] public class ClassMethodTargetAttribute : Attribute { } // 該Attribute對全部的元素有效. [AttributeUsage(AttributeTargets.All)] public class AllTargetsAttribute : Attribute { } //上面定義的Attribute施加到程序元素上的用法 [ClassTarget] //施加到類 [ClassMethodTarget]//施加到類 [AllTargets] //施加到類 public class TestClassAttribute { [ConstructorTarget] //施加到構造器 [AllTargets] //施加到構造器 TestClassAttribute() { } [MethodTarget] //施加到方法 [ClassMethodTarget] //施加到方法 [AllTargets] //施加到方法 public void Method1() { } [FieldTarget] //施加到字段 [AllTargets] //施加到字段 public int myInt; static void Main(string[] args) { } } }
至此,咱們介紹了有關Attribute類和它們的代碼格式。你必定想知道到底如何在你的應用程序中使用Attribute,若是僅僅是前面介紹的內容,仍是不足以說明Attribute有什麼實用價值的話,那麼從後面的章節開始咱們將介紹幾個Attribute的不一樣用法,相信你必定會對Attribute有一個新的瞭解。
.NET Framework中對Attribute的支持是一個全新的功能,這種支持來自它的Attribute類。在你的程序中適當地使用這個類,或者是靈活巧妙地利用這個類,將使你的程序得到某種在以往編程中很難作到的能力。咱們來看一個例子:
假如你是一個項目開發小組中的成員,你想要跟蹤項目代碼檢查的信息,一般你能夠把代碼的檢查信息保存在數據庫中以便查詢;或者把信息寫到代碼的註釋裏面,這樣能夠閱讀代碼的同時看到代碼被檢查的信息。咱們知道.NET的組件是自描述的,那麼是否可讓代碼本身來描述它被檢查的信息呢?這樣咱們既能夠將信息和代碼保存在一塊兒,又能夠經過代碼的自我描述獲得信息。答案就是使用Attribute.
下面的步驟和代碼告訴你怎麼作:
首先,咱們建立一個自定義的Attribute,而且事先設定咱們的Attribute將施加在class的元素上面以獲取一個類代碼的檢查信息。
using System; using System.Reflection; [AttributeUsage(AttributeTargets.Class)] //還記得上一節的內容嗎? public class CodeReviewAttribute : System.Attribute //定義一個CodeReview的Attribute { private string reviewer; //代碼檢查人 private string date; //檢查日期 private string comment; //檢查結果信息 //參數構造器 public CodeReviewAttribute(string reviewer, string date) { this.reviewer=reviewer; this.date=date; } public string Reviewer { get { return reviewer; } } public string Date { get { return date; } } public string Comment { get { return comment; } set { comment=value; } } }
咱們的自定義CodeReviewAttribute同普通的類沒有區別,它從Attribute派生,同時經過AttributeUsage表示咱們的Attribute僅能夠施加到類元素上。
第二步就是使用咱們的CodeReviewAttribute, 假如咱們有一個Jack寫的類MyClass,檢查人Niwalker,檢查日期2003年7月9日,因而咱們施加Attribute以下:
[CodeReview("Niwalker","2003-7-9",Comment="Jack的代碼")] public class MyClass { //類的成員定義 }
當這段代碼被編譯的時候,編譯器會調用CodeReviewAttribute的構造器而且把"Niwalker"和"2003-7-9"分別做爲構造器的參數。注意到參數表中還有一個Comment屬性的賦值,這是Attribute特有的方式,這裏你能夠設置更多的Attribute的公共屬性(若是有的話),須要指出的是.NET Framework1.0容許向private的屬性賦值,但在.NET Framework1.1已經不容許這樣作,只能向public的屬性賦值。
第三步就是取出咱們須要的信息,這是經過.NET的反射來實現的,關於反射的知識,限於篇幅我不打算在這裏進行說明,也許我會在之後另外寫一篇介紹反射的文章。
class test { static void Main(string[] args) { System.Reflection.MemberInfo info=typeof(MyClass); //經過反射獲得MyClass類的信息 //獲得施加在MyClass類上的定製Attribute CodeReviewAttribute att= (CodeReviewAttribute)Attribute.GetCustomAttribute(info,typeof(CodeReviewAttribute)); if(att!=null) { Console.WriteLine("代碼檢查人:{0}",att.Reviewer); Console.WriteLine("檢查時間:{0}",att.Date); Console.WriteLine("註釋:{0}",att.Comment); } } }
在上面這個例子中,Attribute扮演着向一個類添加額外信息的角色,它並不影響MyClass類的行爲。經過這個例子,咱們大體能夠知道如何寫一個自定義的Attribute,以及如何在應用程序使用它。下一節,我將介紹如何使用Attribute來自動生成ADO.NET的數據訪問類的代碼。
用於參數的Attribute
在編寫多層應用程序的時候,你是否爲每次要寫大量相似的數據訪問代碼而感到枯燥無味?好比咱們須要編寫調用存儲過程的代碼,或者編寫T_SQL代碼,這些代碼每每須要傳遞各類參數,有的參數個數比較多,一不當心還容易寫錯。有沒有一種一勞永逸的方法?固然,你可使用MS的Data Access Application Block,也可使用本身編寫的Block。這裏向你提供一種另類方法,那就是使用Attribute。
下面的代碼是一個調用AddCustomer存儲過程的常規方法:
public int AddCustomer(SqlConnection connection, string customerName, string country, string province, string city, string address, string telephone) { SqlCommand command=new SqlCommand("AddCustomer", connection); command.CommandType=CommandType.StoredProcedure; command.Parameters.Add("@CustomerName",SqlDbType.NVarChar,50).Value=customerName; command.Parameters.Add("@country",SqlDbType.NVarChar,20).Value=country; command.Parameters.Add("@Province",SqlDbType.NVarChar,20).Value=province; command.Parameters.Add("@City",SqlDbType.NVarChar,20).Value=city; command.Parameters.Add("@Address",SqlDbType.NVarChar,60).Value=address; command.Parameters.Add("@Telephone",SqlDbType.NvarChar,16).Value=telephone; command.Parameters.Add("@CustomerId",SqlDbType.Int,4).Direction=ParameterDirection.Output; connection.Open(); command.ExecuteNonQuery(); connection.Close(); int custId=(int)command.Parameters["@CustomerId"].Value; return custId; }
上面的代碼,建立一個Command實例,而後添加存儲過程的參數,而後調用ExecuteMonQuery方法執行數據的插入操做,最後返回CustomerId。從代碼能夠看到參數的添加是一種重複單調的工做。若是一個項目有100多個甚至幾百個存儲過程,做爲開發人員的你會不會要想辦法偷懶?(反正我會的:-))。
下面開始咱們的代碼自動生成工程:
咱們的目的是根據方法的參數以及方法的名稱,自動生成一個Command對象實例,第一步咱們要作的就是建立一個SqlParameterAttribute, 代碼以下:
SqlCommandParameterAttribute.cs using System; using System.Data; using Debug=System.Diagnostics.Debug; namespace DataAccess { // SqlParemeterAttribute 施加到存儲過程參數 [ AttributeUsage(AttributeTargets.Parameter) ] public class SqlParameterAttribute : Attribute { private string name; //參數名稱 private bool paramTypeDefined; //是否參數的類型已經定義 private SqlDbType paramType; //參數類型 private int size; //參數尺寸大小 private byte precision; //參數精度 private byte scale; //參數範圍 private bool directionDefined; //是否認義了參數方向 private ParameterDirection direction; //參數方向 public SqlParameterAttribute() { } public string Name { get { return name == null ? string.Empty : name; } set { _name = value; } } public int Size { get { return size; } set { size = value; } } public byte Precision { get { return precision; } set { precision = value; } } public byte Scale { get { return scale; } set { scale = value; } } public ParameterDirection Direction { get { Debug.Assert(directionDefined); return direction; } set { direction = value; directionDefined = true; } } public SqlDbType SqlDbType { get { Debug.Assert(paramTypeDefined); return paramType; } set { paramType = value; paramTypeDefined = true; } } public bool IsNameDefined { get { return name != null && name.Length != 0; } } public bool IsSizeDefined { get { return size != 0; } } public bool IsTypeDefined { get { return paramTypeDefined; } } public bool IsDirectionDefined { get { return directionDefined; } } public bool IsScaleDefined { get { return _scale != 0; } } public bool IsPrecisionDefined { get { return _precision != 0; } } ...
以上定義了SqlParameterAttribute的字段和相應的屬性,爲了方便Attribute的使用,咱們重載幾個構造器,不一樣的重載構造器用於不用的參數:
... // 重載構造器,若是方法中對應於存儲過程參數名稱不一樣的話,咱們用它來設置存儲過程的名稱 // 其餘構造器的目的相似 public SqlParameterAttribute(string name) { Name=name; } public SqlParameterAttribute(int size) { Size=size; } public SqlParameterAttribute(SqlDbType paramType) { SqlDbType=paramType; } public SqlParameterAttribute(string name, SqlDbType paramType) { Name = name; SqlDbType = paramType; } public SqlParameterAttribute(SqlDbType paramType, int size) { SqlDbType = paramType; Size = size; } public SqlParameterAttribute(string name, int size) { Name = name; Size = size; } public SqlParameterAttribute(string name, SqlDbType paramType, int size) { Name = name; SqlDbType = paramType; Size = size; } } }
爲了區分方法中不是存儲過程參數的那些參數,好比SqlConnection,咱們也須要定義一個非存儲過程參數的Attribute:
//NonCommandParameterAttribute.cs using System; namespace DataAccess { [ AttributeUsage(AttributeTargets.Parameter) ] public sealed class NonCommandParameterAttribute : Attribute { } }
咱們已經完成了SQL的參數Attribute的定義,在建立Command對象生成器以前,讓咱們考慮這樣的一個事實,那就是若是咱們數據訪問層調用的不是存儲過程,也就是說Command的CommandType不是存儲過程,而是帶有參數的SQL語句,咱們想讓咱們的方法同樣能夠適合這種狀況,一樣咱們仍然可使用Attribute,定義一個用於方法的Attribute來代表該方法中的生成的Command的CommandType是存儲過程仍是SQL文本,下面是新定義的Attribute的代碼:
//SqlCommandMethodAttribute.cs using System; using System.Data; namespace Emisonline.DataAccess { [AttributeUsage(AttributeTargets.Method)] public sealed class SqlCommandMethodAttribute : Attribute { private string commandText; private CommandType commandType; public SqlCommandMethodAttribute( CommandType commandType, string commandText) { commandType=commandType; commandText=commandText; } public SqlCommandMethodAttribute(CommandType commandType) : this(commandType, null){} public string CommandText { get { return commandText==null ? string.Empty : commandText; } set { commandText=value; } } public CommandType CommandType { get { return commandType; } set { commandType=value; } } } }
咱們的Attribute的定義工做已經所有完成,下一步就是要建立一個用來生成Command對象的類。
SqlCommandGenerator類的設計
SqlCommandGEnerator類的設計思路就是經過反射獲得方法的參數,使用被SqlCommandParameterAttribute標記的參數來裝配一個Command實例。
引用的命名空間:
//SqlCommandGenerator.cs using System; using System.Reflection; using System.Data; using System.Data.SqlClient; using Debug = System.Diagnostics.Debug; using StackTrace = System.Diagnostics.StackTrace;
類代碼:
namespace DataAccess { public sealed class SqlCommandGenerator { //私有構造器,不容許使用無參數的構造器構造一個實例 private SqlCommandGenerator() { throw new NotSupportedException(); } //靜態只讀字段,定義用於返回值的參數名稱 public static readonly string ReturnValueParameterName = "RETURN_VALUE"; //靜態只讀字段,用於不帶參數的存儲過程 public static readonly object[] NoValues = new object[] {}; public static SqlCommand GenerateCommand(SqlConnection connection, MethodInfo method, object[] values) { //若是沒有指定方法名稱,從堆棧幀獲得方法名稱 if (method == null) method = (MethodInfo) (new StackTrace().GetFrame(1).GetMethod()); // 獲取方法傳進來的SqlCommandMethodAttribute // 爲了使用該方法來生成一個Command對象,要求有這個Attribute。 SqlCommandMethodAttribute commandAttribute = (SqlCommandMethodAttribute) Attribute.GetCustomAttribute(method, typeof(SqlCommandMethodAttribute)); Debug.Assert(commandAttribute != null); Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure || commandAttribute.CommandType == CommandType.Text); // 建立一個SqlCommand對象,同時經過指定的attribute對它進行配置。 SqlCommand command = new SqlCommand(); command.Connection = connection; command.CommandType = commandAttribute.CommandType; // 獲取command的文本,若是沒有指定,那麼使用方法的名稱做爲存儲過程名稱 if (commandAttribute.CommandText.Length == 0) { Debug.Assert(commandAttribute.CommandType == CommandType.StoredProcedure); command.CommandText = method.Name; } else { command.CommandText = commandAttribute.CommandText; } // 調用GeneratorCommandParameters方法,生成command參數,同時添加一個返回值參數 GenerateCommandParameters(command, method, values); command.Parameters.Add(ReturnValueParameterName, SqlDbType.Int).Direction =ParameterDirection.ReturnValue; return command; } private static void GenerateCommandParameters( SqlCommand command, MethodInfo method, object[] values) { // 獲得全部的參數,經過循環一一進行處理。 ParameterInfo[] methodParameters = method.GetParameters(); int paramIndex = 0; foreach (ParameterInfo paramInfo in methodParameters) { // 忽略掉參數被標記爲[NonCommandParameter ]的參數 if (Attribute.IsDefined(paramInfo, typeof(NonCommandParameterAttribute))) continue; // 獲取參數的SqlParameter attribute,若是沒有指定,那麼就建立一個並使用它的缺省設置。 SqlParameterAttribute paramAttribute = (SqlParameterAttribute) Attribute.GetCustomAttribute( paramInfo, typeof(SqlParameterAttribute)); if (paramAttribute == null) paramAttribute = new SqlParameterAttribute(); //使用attribute的設置來配置一個參數對象。使用那些已經定義的參數值。若是沒有定義,那麼就從方法 // 的參數來推斷它的參數值。 SqlParameter sqlParameter = new SqlParameter(); if (paramAttribute.IsNameDefined) sqlParameter.ParameterName = paramAttribute.Name; else sqlParameter.ParameterName = paramInfo.Name; if (!sqlParameter.ParameterName.StartsWith("@")) sqlParameter.ParameterName = "@" + sqlParameter.ParameterName; if (paramAttribute.IsTypeDefined) sqlParameter.SqlDbType = paramAttribute.SqlDbType; if (paramAttribute.IsSizeDefined) sqlParameter.Size = paramAttribute.Size; if (paramAttribute.IsScaleDefined) sqlParameter.Scale = paramAttribute.Scale; if (paramAttribute.IsPrecisionDefined) sqlParameter.Precision = paramAttribute.Precision; if (paramAttribute.IsDirectionDefined) { sqlParameter.Direction = paramAttribute.Direction; } else { if (paramInfo.ParameterType.IsByRef) { sqlParameter.Direction = paramInfo.IsOut ? ParameterDirection.Output : ParameterDirection.InputOutput; } else { sqlParameter.Direction = ParameterDirection.Input; } } // 檢測是否提供的足夠的參數對象值 Debug.Assert(paramIndex < values.Length); //把相應的對象值賦於參數。 sqlParameter.Value = values[paramIndex]; command.Parameters.Add(sqlParameter); paramIndex++; } //檢測是否有多餘的參數對象值 Debug.Assert(paramIndex == values.Length); } } }
必要的工做終於完成了。SqlCommandGenerator中的代碼都加上了註釋,因此並不難讀懂。下面咱們進入最後的一步,那就是使用新的方法來實現上一節咱們一開始顯示個那個AddCustomer的方法。
重構新的AddCustomer代碼:
[ SqlCommandMethod(CommandType.StoredProcedure) ] public void AddCustomer( [NonCommandParameter] SqlConnection connection, [SqlParameter(50)] string customerName, [SqlParameter(20)] string country, [SqlParameter(20)] string province, [SqlParameter(20)] string city, [SqlParameter(60)] string address, [SqlParameter(16)] string telephone, out int customerId ) { customerId=0; //須要初始化輸出參數 //調用Command生成器生成SqlCommand實例 SqlCommand command = SqlCommandGenerator.GenerateCommand( connection, null, new object[] {customerName,country,province,city,address,telephone,customerId } ); connection.Open(); command.ExecuteNonQuery(); connection.Close(); //必須明確返回輸出參數的值 customerId=(int)command.Parameters["@CustomerId"].Value; }
代碼中必須注意的就是out參數,須要事先進行初始化,並在Command執行操做之後,把參數值傳回給它。受益於Attribute,使咱們擺脫了那種編寫大量枯燥代碼編程生涯。 咱們甚至還可使用Sql存儲過程來編寫生成整個方法的代碼,若是那樣作的話,可就大大節省了你的時間了,上一節和這一節中所示的代碼,你能夠把它們單獨編譯成一個組件,這樣就能夠在你的項目中不斷的重用它們了。
Attribute在攔截機制上的應用
從這一節開始咱們討論Attribute的高級應用,爲此我準備了一個實際的例子:咱們有一個訂單處理系統,當一份訂單提交的時候,系統檢查庫存,若是庫存存量知足訂單的數量,系統記錄訂單處理記錄,而後更新庫存,若是庫存存量低於訂單的數量,系統作相應的記錄,同時向庫存管理員發送郵件。爲了方便演示,咱們對例子進行了簡化:
//Inventory.cs using System; using System.Collections; namespace NiwalkerDemo { public class Inventory { private Hashtable inventory=new Hashtable(); public Inventory() { inventory["Item1"]=100; inventory["Item2"]=200; } public bool Checkout(string product, int quantity) { int qty=GetQuantity(product); return qty>=quantity; } public int GetQuantity(string product) { int qty=0; if(inventory[product]!=null) qty = (int)inventory[product]; return qty; } public void Update(string product, int quantity) { int qty=GetQuantity(product); inventory[product]=qty-quantity; } } } //Logbook.cs using System; namespace NiwalkerDemo { public class Logbook { public static void Log(string logData) { Console.WriteLine("log:{0}",logData); } } } //Order.cs using System; namespace NiwalkerDemo { public class Order { private int orderId; private string product; private int quantity; public Order(int orderId) { this.orderId=orderId; } public void Submit() { Inventory inventory=new Inventory(); //建立庫存對象 //檢查庫存 if(inventory.Checkout(product,quantity)) { Logbook.Log("Order"+orderId+" available"); inventory.Update(product,quantity); } else { Logbook.Log("Order"+orderId+" unavailable"); SendEmail(); } } public string ProductName { get{ return product; } set{ product=value; } } public int OrderId { get{ return orderId; } } public int Quantity { get{ return quantity;} set{ quantity=value; } } public void SendEmail() { Console.WriteLine("Send email to manager"); } } }
下面是調用程序:
//AppMain.cs using System; namespace NiwalkerDemo { public class AppMain { static void Main() { Order order1=new Order(100); order1.ProductName="Item1"; order1.Quantity=150; order1.Submit(); Order order2=new Order(101); order2.ProductName="Item2"; order2.Quantity=150; order2.Submit(); } } }
程序看上去還不錯,商務對象封裝了商務規則,運行的結果也符合要求。可是我好像聽到你在抱怨了,沒有嗎?當你的客戶的需求改變的時候(客戶老是常常改變他們的需求),好比庫存檢查的規則不是單一的檢查產品的數量,還要檢查產品是否被預訂的多種狀況,那麼你須要改變Inventory的代碼,同時還要修改Order中的代碼,咱們的例子只是一個簡單的商務邏輯,實際的狀況比這個要複雜的多。問題在於Order對象同其餘的對象之間是緊耦合的,從OOP的觀點出發,這樣的設計是有問題的,若是你寫出這樣的程序,至少不會在個人團隊裏面被Pass.
你說了:「No problem! 咱們能夠把商務邏輯抽出來放到一個專門設計的用來處理事務的對象中。」嗯,好主意,若是你是這麼想的,或許我還能夠給你一個提議,使用Observer Design Pattern(觀察者設計模式):你可使用delegate,在Order對象中定義一個BeforeSubmit和AfterSubmit事件,而後建立一個對象鏈表,將相關的對象插入到這個鏈表中,這樣就能夠實現對Order提交事件的攔截,在Order提交以前和提交以後自動進行必要的事務處理。若是你感興趣的話,你能夠本身動手來編寫這樣的一個代碼,或許還要考慮在分佈式環境中(Order和Inventory不在一個地方)如何處理對象之間的交互問題。
幸運的是,.NET Framework中提供了實現這種技術的支持。在.NET Framework中的對象Remoting和組件服務中,有一個重要的攔截機制,在對象Remoting中,不一樣的應用程序之間的對象的交互須要穿越他們的域邊界,每個應用域也能夠細分爲多個Context(上下文環境),每個應用域也至少有一個默認的Context,即便在同一個應用域,也存在穿越不一樣Context的問題。NET的組件服務發展了COM+的組件服務,它使用Context Attribute來實現象COM+同樣的攔截功能。經過對調用對象的攔截,咱們能夠對一個方法的調用進行前處理和後處理,同時也解決了上述的跨越邊界的問題。
須要提醒你,若是你在MSDN文檔查ContextAttribute,我能夠保證你得不到任何有助於瞭解ContextAttribute的資料,你看到的將是這麼一句話:「This type supports the .NET Framework infrastructure and is not intended to be used directly from your code.」——「本類型支持.NET Framework基礎結構,它不打算直接用於你的代碼。」不過,在msdn站點,你能夠看到一些有關這方面的資料(見文章後面的參考連接)。
下面咱們介紹有關的幾個類和一些概念,首先是:
ContextAttribute類
ContextAttribute派生自Attribute,同時它還實現了IContextAttribute和IContextProperty接口。全部自定義的ContextAttribute必須從這個類派生。
構造器:
ContextAttribute:構造器帶有一個參數,用來設置ContextAttribute的名稱。
公共屬性:
Name:只讀屬性。返回ContextAttribute的名稱
公共方法:
GetPropertiesForNewContext:虛擬方法。向新的Context添加屬性集合。
IsContextOK:虛擬方法。查詢客戶Context中是否存在指定的屬性。
IsNewContextOK:虛擬方法。默認返回true。一個對象可能存在多個Context,使用這個方法來檢查新的Context中屬性是否存在衝突。
Freeze:虛擬方法。該方法用來定位被建立的Context的最後位置。
ContextBoundObject類
實現被攔截的類,須要從ContextBoundObject類派生,這個類的對象經過Attribute來指定它所在Context,凡是進入該Context的調用均可以被攔截。該類從MarshalByRefObject派生。
如下是涉及到的接口:
IMessage:定義了被傳送的消息的實現。一個消息必須實現這個接口。
IMessageSink:定義了消息接收器的接口,一個消息接收器必須實現這個接口。
還有幾個接口,咱們將在下一節結合攔截構架的實現原理中進行介紹。
(承上節) .NET Framework攔截機制的設計中,在客戶端和對象之間,存在着多種消息接收器,這些消息接收器組成一個鏈表,客戶端的調用對象的過程以及調用返回實行攔截,你能夠定製本身的消息接收器,把它們插入了到鏈表中,來完成你對一個調用的前處理和後處理。那麼調用攔截是如何構架或者說如何實現的呢?
在.NET中有兩種調用,一種是跨應用域(App Domain),一種是跨上下文環境(Context),兩種調用均經過中間的代理(proxy),代理被分爲兩個部分:透明代理和實際代理。透明代理暴露同對象同樣的公共入口點,當客戶調用透明代理的時候,透明代理把堆棧中的幀轉換爲消息(上一節提到的實現IMessage接口的對象),消息中包含了方法名稱和參數等屬性集,而後把消息傳遞給實際代理,接下去分兩種狀況:在跨應用域的狀況下,實際代理使用一個格式化器對消息進行序列化,而後放入遠程通道中;在跨上下文環境的狀況下,實際代理沒必要知道格式化器、通道和Context攔截器,它只須要在向前傳遞消息以前對調用實行攔截,而後它把消息傳遞給一個消息接收器(實現IMessageSink的對象),每個接收器都知道本身的下一個接收器,當它們對消息進行處理以後(前處理),都將消息傳遞給下一個接收器,一直到鏈表的最後一個接收器,最後一個接收器被稱爲堆棧建立器,它把消息還原爲堆棧幀,而後調用對象,當調用方法結果返回的時候,堆棧建立器把結果轉換爲消息,傳回給調用它的消息接收器,因而消息沿着原來的鏈表往回傳,每一個鏈表上的消息接收器在回傳消息以前都對消息進行後處理。一直到鏈表的第一個接收器,第一個接收器把消息傳回給實際代理,實際代理把消息傳遞給透明代理,後者把消息放回到客戶端的堆棧中。從上面的描述咱們看到穿越Context的消息不須要格式化,CLR使用一個內部的通道,叫作CrossContextChannel,這個對象也是一種消息接收器。
有幾種消息接收器的類型,一個調用攔截能夠在服務器端進行也能夠在客戶端進行,服務器端接收器攔截全部對服務器上下文環境中對象的調用,同時做一些前處理和後處理。客戶端的接收器攔截全部外出客戶端上下文環境的調用,同時也作一些前處理和後處理。服務器負責服務器端接收器的安裝,攔截對服務器端上下文環境訪問的接收器稱爲服務器上下文環境接收器,那些攔截調用實際對象的接收器是對象接收器。經過客戶安裝的客戶端接收器稱爲客戶端上下文環境接受器,經過對象安裝的客戶端接收器則稱爲特使(Envoy)接收器,特使接收器僅攔截那些和它相關的對象。客戶端的最後一個接收器和服務器端的第一個接收器是CrossContextChannel類型的實例。不一樣類型的接收器組成不一樣的段,每一個段的端點都裝上稱爲終結器的接收器,終結器起着把本段的消息傳給下一個段的做用。在服務器上下文環境段的最後一個終結器是ServerContextTerminatorSink。若是你在終結器調用NextSink,它將返回一個null,它們的行爲就像是死端頭,可是在它們內部保存有下一個接收器對象的私有字段。
咱們大體介紹了.NET Framework的對象調用攔截的實現機制,目的是讓你們對這種機制有一個認識,如今是實現咱們代碼的時候了,經過代碼的實現,你能夠看到消息如何被處理的過程。首先是爲咱們的程序定義一個接收器CallTraceSink:
//TraceContext.cs using System; using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Messaging; using System.Runtime.Remoting.Activation; namespace NiwalkerDemo { public class CallTraceSink : IMessageSink //實現IMessageSink { private IMessageSink nextSink; //保存下一個接收器 //在構造器中初始化下一個接收器 public CallTraceSink(IMessageSink next) { nextSink=next; } //必須實現的IMessageSink接口屬性 public IMessageSink NextSink { get { return nextSink; } } //實現IMessageSink的接口方法,當消息傳遞的時候,該方法被調用 public IMessage SyncProcessMessage(IMessage msg) { //攔截消息,作前處理 Preprocess(msg); //傳遞消息給下一個接收器 IMessage retMsg=nextSink.SyncProcessMessage(msg); //調用返回時進行攔截,並進行後處理 Postprocess(msg,retMsg); return retMsg; } //IMessageSink接口方法,用於異步處理,咱們不實現異步處理,因此簡單返回null, //無論是同步仍是異步,這個方法都須要定義 public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return null; } //咱們的前處理方法,用於檢查庫存,出於簡化的目的,咱們把檢查庫存和發送郵件都寫在一塊兒了, //在實際的實現中,可能也須要把Inventory對象綁定到一個上下文環境, //另外,能夠將發送郵件設計爲另一個接收器,而後經過NextSink進行安裝 private void Preprocess(IMessage msg) { //檢查是不是方法調用,咱們只攔截Order的Submit方法。 IMethodCallMessage call=msg as IMethodCallMessage; if(call==null) return; if(call.MethodName=="Submit") { string product=call.GetArg(0).ToString(); //獲取Submit方法的第一個參數 int qty=(int)call.GetArg(1); //獲取Submit方法的第二個參數 //調用Inventory檢查庫存存量 if(new Inventory().Checkout(product,qty)) Console.WriteLine("Order availible"); else { Console.WriteLine("Order unvailible"); SendEmail(); } } } //後處理方法,用於記錄訂單提交信息,一樣能夠將記錄做爲一個接收器 //咱們在這裏處理,僅僅是爲了演示 private void Postprocess(IMessage msg,IMessage retMsg) { IMethodCallMessage call=msg as IMethodCallMessage; if(call==null) return; Console.WriteLine("Log order information"); } private void SendEmail() { Console.WriteLine("Send email to manager"); } } ...
接下來咱們定義上下文環境的屬性,上下文環境屬性必須根據你要建立的接收器類型來實現相應的接口,好比:若是建立的是服務器上下文環境接收器,那麼必須實現IContributeServerContextSink接口。
... public class CallTraceProperty : IContextProperty, IContributeObjectSink { public CallTraceProperty() { } //IContributeObjectSink的接口方法,實例化消息接收器 public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next) { return new CallTraceSink(next); } //IContextProperty接口方法,若是該方法返回ture,在新的上下文環境中激活對象 public bool IsNewContextOK(Context newCtx) { return true; } //IContextProperty接口方法,提供高級使用 public void Freeze(Context newCtx) { } //IContextProperty接口屬性 public string Name { get { return "OrderTrace";} } } ...
最後是ContextAttribute
... [AttributeUsage(AttributeTargets.Class)] public class CallTraceAttribute : ContextAttribute { public CallTraceAttribute():base("CallTrace") { } //重載ContextAttribute方法,建立一個上下文環境屬性 public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg) { ctorMsg.ContextProperties.Add(new CallTraceProperty()); } } }
爲了看清楚調用Order對象的Submit方法如何被攔截,咱們稍微修改一下Order類,同時把它設計爲ContextBoundObject的派生類:
//Inventory.cs //Order.cs using System; namespace NiwalkerDemo { [CallTrace] public class Order : ContextBoundObject { ... public void Submit(string product, int quantity) { this.product=product; this.quantity=quantity; } ... } }
客戶端調用代碼:
... public class AppMain { static void Main() { Order order1=new Order(100); order1.Submit("Item1",150); Order order2=new Order(101); order2.Submit("Item2",150); } } ...
運行結果代表了咱們對Order的Sbumit成功地進行了攔截。須要說明的是,這裏的代碼僅僅是做爲對ContextAttribute應用的演示,它是粗線條的。在具體的實踐中,你們能夠設計的更精妙。
後記:原本想對Attribute進行更多的介紹,發現要講的東西實在是太多了。請容許我在其餘的專題中再來討論它們。十分感謝你們有耐心讀完這個系列。若是這裏介紹的內容在你的編程生涯有所啓迪的話,那麼就是個人莫大榮幸了。再一次謝謝你們。
原文地址:Attribute在.net編程中的應用