《CLR Via C#》学习笔记系列05---C# 定制特性
一、初识特性
特性(attribute)是被指定给某一声明的一则附加的声明性信息。
设计类型的时候可以使用各种成员来描述该类型的信息,但有时候我们可能不太愿意将一些附加信息放到类的内部,因为这样,可能会给类型本身的信息描述带来麻烦或误解。我们想为类型、属性、方法及返回值附加额外的信息,这些附加信息可以更明确的表达类及其对象成员的状态,怎么办?定制特性Attribute可以做到。
在C#中,有一个小的预定义特性集合。在学习如何建立我们自己的定制特性(custom attributes)之前,我们先来看看在我们的代码中如何使用预定义特性。
1 using System;
2 public class AnyClass
3 {
4 [Obsolete("Don't use Old method, use New method", true)]
5 static void Old( ) { }
6 static void New( ) { }
7 public static void Main( )
8 {
9 Old( );
10 }
11 }
我们先来看一下上面这个例子,在这个例子中我们使用了Obsolete特性,它标记了一个不应该再被使用的程序实体。第一个参数是一个字符串,它解释了为什么该实体是过时的以及应该用什么实体来代替它。实际上,你可以在这里写任何文本。第二个参数告诉编译器应该把使用这个过时的程序实体当作一种错误。它的默认值是false,也就是说编译器对此会产生一个警告。
当我们尝试编译上面这段程序的时候,我们将会得到一个错误:
AnyClass.Old()' is obsolete: 'Don't use Old method, use New method'
二、特性是什么
定制特性其实是一个类型的实例,为了符合“公共语言规范”CLS的要求,定制特性类必须直接或间接从公共抽象类System.Atrribute派生。查看文档发现StructLayoutAttribute,MarshalAsAttribute,DllImportAttribute,InAttribute和OutAttribute,这些类都是从System.Attribute派生。所有符合CLS规范的特性类都肯定从这个类派生。
三、定义自己的特性
开发定制特性(custom attributes)
现在让我们来看看如何开发我们自己的特性。
首先我们要从System.Attribute派生出我们自己的特性类(一个从System.Attribute抽象类继承而来的类,不管是直接还是间接继承,都会成为一个特性类。特性类的声明定义了一种可以被放置在声明之上新的特性)。
1 using System;
2 public class HelpAttribute : Attribute
3 {
4 }
不管你是否相信,我们已经建立了一个定制特性,现在我们可以用它来装饰现有的类就好像上面我们使用Obsolete attribute一样。
1 [Help()]
2 public class AnyClass
3 {
4 }
注意:对一个特性类名使用Attribute后缀是一个惯例。然而,当我们把特性添加到一个程序实体,是否包括 Attribute后缀是我们的自由。编译器会首先在System.Attribute的派生类中查找被添加的特性类。如果没有找到,那么编译器会添加 Attribute后缀继续查找。
到目前为止,这个特性还没有起到什么作用。下面我们来添加些东西给它使它更有用些。
1 using System;
2 public class HelpAttribute : Attribute
3 {
4 public HelpAttribute(String Descrition_in)
5 {
6 this.description = Description_in;
7 }
8 protected String description;
9 public String Description
10 {
11 get
12 {
13 return this.description;
14 }
15 }
16 }
17 [Help("this is a do-nothing class")]
18 public class AnyClass
19 {
20 }
在上面的例子中,我们给HelpAttribute特性类添加了一个属性并且在后续的部分中我们会在运行时环境中查寻它。
四、限制特性
在定义特性时,我们有时会希望他只应用于枚举,或只应用于类,有时我们希望他多少出现,或只出现一次,为了告诉编译器空上特性的合法应用范围,需要向特性类应用System.AttributeUsageAttribute类的实例
AttributeUsage类是另外一个预定义特性类,它帮助我们控制我们自己的定制特性的使用。它描述了一个定制特性如和被使用。
AttributeUsage有三个属性,我们可以把它放置在定制属性前面。
ValidOn
通过这个属性,我们能够定义定制特性应该在何种程序实体前放置。一个属性可以被放置的所有程序实体在AttributeTargets enumerator中列出。通过OR操作我们可以把若干个AttributeTargets值组合起来。
AllowMultiple
这个属性标记了我们的定制特性能否被重复放置在同一个程序实体前多次。
Inherited
我们可以使用这个属性来控制定制特性的继承规则。它标记了我们的特性能否被继承。
下面让我们来做一些实际的东西。我们将会在刚才的Help特性前放置AttributeUsage特性以期待在它的帮助下控制Help特性的使用。
1 using System;
2 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
3 public class HelpAttribute : Attribute
4 {
5 public HelpAttribute(String Description_in)
6 {
7 this.description = Description_in;
8 }
9 protected String description;
10 public String Description
11 {
12 get
13 {
14 return this.description;
15 }
16 }
17 }
先让我们来看一下AttributeTargets.Class。它规定了Help特性只能被放在class的前面。这也就意味着下面的代码将会产生错误:
1 [Help("this is a do-nothing class")]
2 public class AnyClass
3 {
4 [Help("this is a do-nothing method")] //error
5 public void AnyMethod()
6 {
7 }
8 }
编译器报告错误如下:
AnyClass.cs: Attribute 'Help' is not valid on this declaration type.
It is valid on 'class' declarations only.
我们可以使用AttributeTargets.All来允许Help特性被放置在任何程序实体前。可能的值是:
Assembly,Module,Class,Struct,Enum,Constructor,Method,Property,Field,Event,Interface,Parameter,Delegate
All = Assembly | Module | Class | Struct | Enum | Constructor | Method | Property | Field | Event | Interface | Parameter | Delegate
ClassMembers = Class | Struct | Enum | Constructor | Method | Property | Field | Event | Delegate | Interface
下面考虑一下AllowMultiple = false。它规定了特性不能被重复放置多次。
1 [Help("this is a do-nothing class")]
2 [Help("it contains a do-nothing method")]
3 public class AnyClass
4 {
5 [Help("this is a do-nothing method")] //error
6 public void AnyMethod()
7 {
8 }
9 }
它产生了一个编译期错误。
AnyClass.cs: Duplicate 'Help' attribute
Ok,现在我们来讨论一下最后的这个属性。Inherited, 表明当特性被放置在一个基类上时,它能否被派生类所继承。
1 [Help("BaseClass")]
2 public class Base
3 {
4 }
5 public class Derive : Base
6 {
7 }
五、检测定制特性
仅仅定义特性类没有用,确实可以定义自己想要的所有特性类,并应用自己想要的所有实例,但这样除了在程序集中生成额外的元数据,没有其它任何意义。应用程序代码的行为不会有任何改变。现在我们用一种称为反射的技术检测特性的存在,假定你现在让你
根据AnyClass类中是否应用了Help类型的实例来区别重写toString()方法:
public override string ToString() { if (this.GetType().IsDefined(typeof(HelpAttribute), false)) { //如果是,就执行代码 } else { //如果不是,就执行代码 } }
上述代码调用Type的IsDefined方法,要求系统查看枚举类型的元数据,检查是否关联了HelpAttribute类的实例,如果IsDefined返回true,表明HelpAttribute的一个实例已与枚举类型关联
六、定制特性的使用场景------“数据修改日志”
1、假设有对象person,我们需求是:记录(最终)人员信息中Address的修改情况,并且能够查询出所有status="valid"的人员的Address信息的变化情况;
public class Person { public int Id; public string status; public Address; ... }
2、基本原理是:通过给实体类的定制的Attribute修饰,而在数据DAO的时候注入保存日志的动作来实现.
为了修饰实体类,区分需要保存的修改日志的实体类,系统中定义了两个Attribute类:AuditLogClassAttribute,AuditLogAttribute类,分别用于修饰实体类和类的属性,其构造函数和属性包括:
public AuditLogClassAttribute:Attribute { AuditLogClassAttribute(string name, string idProperty,bool logInserting,bool logDeleting,bool logChanging) { ... } Public string Name{...} public string IDProperty{} } //用于修饰属性的Attribute [AttributeUsage( AttributeTargets.Property,AllowMultiple=false)] public class AuditLogAttribute : Attribute { /// <summary> /// 构造函数 /// </summary> /// <param name="name">保存到日志库时属性的名称</param> /// <param name="alwaysLogging">是否总是记录该属性的值,无论它是否发生变更</param> public AuditLogAttribute(string name,bool alwaysLogging) { ... } }
其中 AuditLogClassAttribute用于修饰类,Name:保存修改日志时该实体的名称,IDProperty:标志该实体的唯一性的属性名称。bool logInserting,bool logDeleting,bool logChanging用于标识是否在插入,删除,修改的时候是否记录。 AuditLogAttribute用于修饰属性.AlwaysLogging属性在用于类的属性时,是否总是记录它的值(无论是否发生了变化),缺省为false
3、例子如下:
[AuditLogClassAttribute("人员库","Xmbm",true,true,true] public class Person { public int Xmbm { ... } [AuditLogAttribute("姓名")] public string Name { ... } [AuditLogAttribute("单位") public UnitInfoBase Unit { ... } }
在修饰属性Property时,如果该属性是复杂类型(类),比如上例子中的Unit,那么此类必须满足下面的两个要求:
- 类必须重载ToString()方法,以便在数据库中能够正确的保存其值;
- 类必须重载Equal(object )方法,用于比较修改前后值,以判断是否该实例的值是否需要记录到日志中;
4、注入DAO:如何在每一个实体类的Dao中注入保存日志的方法是关键点,由于项目中使用NHibernate来实现dao,这就给实现增加了很多便利,NHibernate有接口IInterceptor,可以直接注入代码
在NHibernateSessionModule模块中开始事务之前增加,注入Interceptor的代码:
public void Init(HttpApplication context) { context.BeginRequest += new EventHandler(BeginTransaction); context.EndRequest += new EventHandler(CommitAndCloseSession); } /// Opens a session within a transaction at the beginning of the HTTP request. /// This doesn't actually open a connection to the database until needed. private void BeginTransaction(object sender, EventArgs e) { //增加自己的注入代码,InterceptorWithAuditLog类的实例 NHibernateSessionManager.Instance.RegisterInterceptor(new InterceptorWithAuditLog()); NHibernateSessionManager.Instance.BeginTransaction(); //NHibernateSessionManager.Instance.GetSession().FlushMode = NHibernate.FlushMode.Never; } /// Commits and closes the NHibernate session provided by the supplied /// Assumes a transaction was begun at the beginning of the request; but a transaction or session does /// not *have* to be opened for this to operate successfully. private void CommitAndCloseSession(object sender, EventArgs e) { try { NHibernateSessionManager.Instance.CommitTransaction(); } finally { NHibernateSessionManager.Instance.CloseSession(); } } public void Dispose() { } }
5、web配置
<sectionGroup name="system.web"> <section name="auditLogService" type="OilDigital.Common.Log.AuditLogServiceSection, Common" allowDefinition="MachineToApplication" restartOnExternalChanges="true"/> </sectionGroup> ... <system.web> <auditLogService defaultProvider="sqlAuditLogProvider"> <providers> <add name="sqlAuditLogProvider" type="OilDigital.Common.Log.SQLAuditLogProvider, Common" connectionStringName="LocalSqlServer" /> </providers> </auditLogService> </system.web>
主要配置保存修改日志的Provider,在Common.dll中提供了一个缺省的保存日志Provider:SQLAuditLogProvider,该类实现了AuditLogProvider接口(抽象类),把日志WebAuditLog实例保存到数据库中.使用该类必须配置其connectionStringName属性为数据库连接串的名字,此外他还提供了 另一个配置选项enabled,如果enabled="false",那么表明此保存控件被跳过. 主要配置保存修改日志的Provider,在Common.dll中提供了一个缺省的保存日志Provider:SQLAuditLogProvider,该类实现了AuditLogProvider接口(抽象类),把日志WebAuditLog实例保存到数据库中.使用该类必须配置其connectionStringName属性为数据库连接串的名字,此外他还提供了另一个配置选项enabled,如果enabled="false",那么表明此Provider处于disabled状态,不执行任何操作。
6、如果使用SQLAuditLogProvider,那么必须在数据库中添加表AuditLog:
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[AuditLog]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [dbo].[AuditLog] GO CREATE TABLE [dbo].[AuditLog] ( [id] [bigint] IDENTITY (1, 1) NOT NULL , [userid] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [ip] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [actiontype] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [classname] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [propertyName] [nvarchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [originalvalue] [nvarchar] (200) COLLATE Chinese_PRC_CI_AS NULL , [currentvalue] [nvarchar] (200) COLLATE Chinese_PRC_CI_AS NULL , [entityId] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL , [updatetime] [datetime] NULL , [ThreadId] bigint not null default 0 ) ON [PRIMARY] GO ALTER TABLE [dbo].[AuditLog] WITH NOCHECK ADD CONSTRAINT [PK_AuditLog] PRIMARY KEY CLUSTERED ( [id] ) ON [PRIMARY] GO
7、创建存储过程
CREATE procedure usp_auditlog_insert ( @userid varchar(50), @ip varchar(50), @actiontype varchar(50), @classname nvarchar(50), @entityid varchar(50), @propertyname nvarchar(50), @originalvalue nvarchar(200), @currentvalue nvarchar(200), @updatetime datetime, @threadid bigint ) as begin insert into auditlog (userid ,ip, actiontype, classname, entityid, propertyname, originalvalue, currentvalue, updatetime, threadid ) values ( @userid, @ip, @actiontype, @classname, @entityid, @propertyname, @originalvalue, @currentvalue, @updatetime, @threadid ) end GO
8、重点是 InterceptorWithAuditLog() 这个类的代码如何写呢?