.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;
}
}
}
用于参数的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在拦截机制上的应用
从这一节开始我们讨论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:定义了消息接收器的接口,一个消息接收器必须实现这个接口。
还有几个接口,我们将在下一节结合拦截构架的实现原理中进行介绍。
我们的自定义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的数据访问类的代码。