善用 System.Attribute,讓你的元件更具彈性

在類別中使用中介資料宣告,讓開發人員在使用你的類別時可簡單的以宣告方式即可設定元件資訊,而不需要寫程式。

System.Attribute,這個類別可能很少人聽到,不過你可能經常看到在類別或屬性或方法上放上一段宣告的程式碼,例如在 WCF 中宣告服務與方法合約時,會使用下列的程式碼:

// Define the IMath contract.
[ServiceContract]
public interface IMath
{
    [OperationContract] 
    double Add(double A, double B);

    [OperationContract]
    double Multiply (double A, double B);
}

再讓你看一個例子,通常在存取 Active Directory 讀取使用者資料時,都要一個一個指名屬性來讀取,例如:

DirectoryEntry entry = new DirectoryEntry(ldapBindingStr, ldapUserName, ldapPassword);
string employeeSAMName = entry.Properties["sAMAccountName"].Value.ToString();

試想,如果我們可以直接使用屬性來存取,而讓這個常數值自動由程式去設定,像是這樣:

// class declaration
public class ActiveDirectoryUser
{
    [Schema(Name="sAMAccountName", type=typeof(string)]
    public string UserName;
}

// access program
DirectoryEntry entry = new DirectoryEntry(ldapBindingStr, ldapUserName, ldapPassword);
Schema schemaAttr = System.Attribute.GetCustomAttribute(this.GetType(), typeof(SchemaAttribute)) as SchemaAttrubute;
string employeeSAMName = entry.Properties[schemaAttr.Name].Value.ToString();

這樣外部程式只要透過 ActiveDirectoryUser.UserName 即可取得使用者的登入名稱,不是輕鬆自在嗎?如果可以善用這個特性,那麼對於元件或框架開發人員來說,寫出來的類別會具有相當的彈性,也可以簡化使用它的開發人員的設定方式,亦可對部份設定做強制性的限制(搭配列舉來做)。

System.Attribute 是用來建立程式碼中的中介資訊(metadata)所使用的類別,它在編譯時會轉換成描述類別的資料,儲存在執行檔中,而程式可以利用 System.Attribute 提供的方法來讀取儲存在執行檔中的中介資訊,這個技法在 .NET Framework 中被廣泛的應用,WCF, WF 就是最好的例子,而在 ASP.NET MVC 中,也用到了很多這樣子的技法。它可以搭配 Framework 或類別庫的基礎類別來組成完整的元件服務,以提供簡易的元件應用能力。

你可以使用 System.Attribute 建立中介資料,以取得下列優勢:

  • 簡化元件特性宣告的使用。
  • 強制要求元件特性,例如可以在子元件中要求加入某段宣告,在父類別檢查這個宣告存不存在,不存在則擲出例外。
  • 強制要求設定值表,利用列舉功能來達到此功能。
  • 配合基底類別,達到高彈性的功能。

若要使用 System.Attribute 建立自訂中介資訊,請依下列步驟:

  1. 建立類別,其名稱一定要是 "類別名稱 + Attribute",例如要宣告 [MyConfig] 的話,類別名稱要用 MyConfigAttribute,並且繼承 System.Attribute 類別。
  2. 在類別中建立屬性值,這些屬性值會在中介資訊中以具名名稱來顯現
  3. 使用 AttributeUsage 宣告這個中介資訊可套用的層次與限制。
  4. 在程式中使用它。

例如下列的程式碼(作為 Active Directory 屬性資訊定義的屬性):

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=true, Inherited=false)] // 強制要求此屬性只能套用在類別的成員變數或屬性,允許可多重套用,但不允許在衍生類別中使用
public class SchemaMappingAttribute : System.Attribute
{
   private string _schemaName = null;
   private bool _readonly = false;
   private SchemaTypeEnums _type = SchemaTypeEnums.DirectoryString;

   public string SchemaName
   {
      get { return this._schemaName; }
      set { this._schemaName = value; }
   }

   public bool ReadOnly
   {
      get { return this._readonly; }
      set { this._readonly = value; }
   }

   public SchemaTypeEnums Type // 強制設定 AD 的 schema 資料型別
   {
      get { return this._type; }
      set { this._type = value; }
   }
}

利用這個屬性,套用到使用者類別的容器中:

public class SchemaUser
{
   [SchemaMapping(SchemaName = "displayName", ReadOnly = false)]
   public string DisplayName = null;
   [SchemaMapping(SchemaName = "name", ReadOnly = false)]
   public string Name = null;
   [SchemaMapping(SchemaName = "distinguishedName", ReadOnly = false)]
   public string DNName = null;
   [SchemaMapping(SchemaName = "postalAddress", ReadOnly = false)]
   public string Address = null;
   [SchemaMapping(SchemaName = "telephoneNumber", ReadOnly = false)]
   public string Phone = null;
   [SchemaMapping(SchemaName = "mail", ReadOnly = false)]
   public string Email = null;
   [SchemaMapping(SchemaName = "mobile", ReadOnly = false)]
   public string MobilePhone = null;
   [SchemaMapping(SchemaName = "department", ReadOnly = false)]
   public string Department = null;
}

然後在讀取 Active Directory 的程式中,使用 System.Attribute 取得被宣告在 SchemaUser 中各成員的屬性設定:

DirectoryEntry userEntry = new DirectoryEntry(ldapBindingStr, ldapUserName, ldapPassword);
SchemaUser user = new SchemaUser();

foreach (FieldInfo fieldInfo in user.GetType().GetFields())
{
   SchemaMappingAttribute attribute =
   (SchemaMappingAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(SchemaMappingAttribute)); // 取得類別中的宣告資料。

   if (attribute != null)
       fieldInfo.SetValue(user, userEntry.Properties[attribute.SchemaName].Value);
}

userEntry.Dispose();

而使用這個類別的程式員,只要用這樣的程式就可以得到資料:

string displayName = SchemaUser.DisplayName;

這個技法還有很多的應用方式,尤其是在 Framework 或元件架構的設計上,未來有機會會在這裡與大家分享。