Unity 容器的可配置方面





0/5 (0投票)
通过配置使用动态装饰器(Dynamic Decorator)为 Unity 容器添加 AOP 功能。
引言
在文章 MEF 的可配置方面 中,我介绍了一种基于 动态装饰器(Dynamic Decorator) 的、用于 MEF 的可配置 AOP 模型。该模型同样适用于其他 IoC 容器,如 Unity 或 Windsor 等。
在本文中,我将介绍适用于 Unity IoC 容器的 AOP 模型。使用该模型,您可以在应用程序配置文件中为对象配置方面,并使用自定义的 Unity 容器来创建具有已附加方面(aspects)的对象。
背景
动态装饰器(Dynamic Decorator)是一种通过将行为附加到对象而不是修改类或创建新类来扩展对象功能的工具。它很有吸引力,因为它避免了设计或重新设计类的麻烦。请阅读文章 动态装饰器模式 和 使用动态装饰器为对象添加方面,以更深入地了解动态装饰器、它要解决的问题、如何解决这些问题以及它的局限性。
还有几篇其他文章讨论了动态装饰器如何用于应用程序开发,并将其与其他类似技术进行比较。请参阅本文的“参考文献”部分。
一旦您阅读了一些文章,您就会发现动态装饰器使用简单且功能强大。在本文中,我将介绍动态装饰器的一个可配置模型,该模型使其使用更加简便,同时保持其强大的功能。然后,该模型与 Unity 容器集成,使得从 Unity 容器中出来的对象已经附加了方面。
使用此模型,您可以在应用程序配置文件中为对象配置方面。然后,一个自定义的 Unity 容器将创建您的对象,并根据配置将方面与之绑定。当您使用这些对象时,由方面定义的相应功能已经附加到对象上。
示例
假设您有一个简单的组件 Employee
,它实现了 IEmployee
和 INotifyPropertyChanged
两个接口,如下所示。
public interface IEmployee
{
System.Int32? EmployeeID { get; set; }
System.String FirstName { get; set; }
System.String LastName { get; set; }
System.DateTime DateOfBirth { get; set; }
System.Int32? DepartmentID { get; set; }
System.String DetailsByLevel(int iLevel);
//1-full name; 2-full name plus birth day;
//3-full name plus birth day and department id.
}
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
public class Employee : IEmployee, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#region Properties
public System.Int32? EmployeeID { get; set; }
public System.String FirstName { get; set; }
public System.String LastName { get; set; }
public System.DateTime DateOfBirth { get; set; }
public System.Int32? DepartmentID { get; set; }
#endregion
public Employee(
System.Int32? employeeid
, System.String firstname
, System.String lastname
, System.DateTime bDay
, System.Int32? departmentID
)
{
this.EmployeeID = employeeid;
this.FirstName = firstname;
this.LastName = lastname;
this.DateOfBirth = bDay;
this.DepartmentID = departmentID;
}
public Employee() { }
public System.String DetailsByLevel(int iLevel)
{
System.String s = "";
switch (iLevel)
{
case 1:
s = "Name:" + FirstName + " " + LastName + ".";
break;
case 2:
s = "Name: " + FirstName + " " + LastName +
", Birth Day: " + DateOfBirth.ToShortDateString() + ".";
break;
case 3:
s = "Name: " + FirstName + " " + LastName +
", Birth Day: " + DateOfBirth.ToShortDateString() +
", Department:" + DepartmentID.ToString() + ".";
break;
default:
break;
}
return s;
}
}
定义方面
方面是横切关注点。对于动态装饰器,方面是一个方法,它接受一个 AspectContext
类型的对象作为第一个参数,一个 object[]
类型的对象作为第二个参数,并返回 void
。
您可以以更通用的方式设计您的方面,以便在各种情况下使用它们。您也可以为某些特定情况设计方面。例如,您可以以通用方式定义您的进入/退出日志方面,并将它们放在 SysConcerns
类中,如下所示。
public class SysConcerns
{
static SysConcerns()
{
ConcernsContainer.runtimeAspects.Add(
"DynamicDecoratorAOP.SysConcerns.EnterLog",
new Decoration(SysConcerns.EnterLog, null));
ConcernsContainer.runtimeAspects.Add(
"DynamicDecoratorAOP.SysConcerns.ExitLog",
new Decoration(SysConcerns.ExitLog, null));
}
public static void EnterLog(AspectContext ctx, object[] parameters)
{
StackTrace st = new StackTrace(new StackFrame(4, true));
Console.Write(st.ToString());
IMethodCallMessage method = ctx.CallCtx;
string str = "Entering " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
public static void ExitLog(AspectContext ctx, object[] parameters)
{
IMethodCallMessage method = ctx.CallCtx;
string str = "Exiting " + ctx.Target.GetType().ToString() +
"." + method.MethodName + "(";
int i = 0;
foreach (object o in method.Args)
{
if (i > 0)
str = str + ", ";
str = str + o.ToString();
}
str = str + ")";
Console.WriteLine(str);
Console.Out.Flush();
}
}
正如您所见,这些方法以通用方式访问 Target
和 CallCtx
,并且可以被各种类型的对象共享,以写入进入/退出日志。
另一方面,某些方面可能需要访问更具体的信息。例如,您只想在 Employee
对象的属性被设置时为其附加一个更改通知功能。以下代码定义了一些特定的方面。
class LocalConcerns
{
static LocalConcerns()
{
ConcernsContainer.runtimeAspects.Add(
"ConsoleUtil.LocalConcerns.NotifyChange",
new Decoration(LocalConcerns.NotifyChange, null));
ConcernsContainer.runtimeAspects.Add(
"ConsoleUtil.LocalConcerns.SecurityCheck",
new Decoration(LocalConcerns.SecurityCheck, null));
}
public static void NotifyChange(AspectContext ctx, object[] parameters)
{
((Employee)ctx.Target).NotifyPropertyChanged(ctx.CallCtx.MethodName);
}
public static void SecurityCheck(AspectContext ctx, object[] parameters)
{
Exception exInner = null;
try
{
if (parameters != null && parameters[0] is WindowsPrincipal &&
((WindowsPrincipal)parameters[0]).IsInRole(
"BUILTIN\\" + "Administrators"))
{
return;
}
}
catch ( Exception ex)
{
exInner = ex;
}
throw new Exception("No right to call!", exInner);
}
}
在上面的代码中,NotifyChange
方法只能由 Employee
类型的目标使用,而 SecurityCheck
需要一个 WindowsPrincipal
对象作为参数。
注意:您可能已经注意到,在 SysConcerns
和 LocalConcerns
的每个类中都有一个静态构造函数。在静态类内部,类中定义的每个方面方法都用于创建一个 Decoration
实例,然后将其添加到名为 ConcernsContainer.runtimeAspects
的字典中。ConcernsContainer
的定义如下。
public class ConcernsContainer
{
static public Dictionary<string, Decoration>
runtimeAspects = new Dictionary<string, Decoration>();
}
这个 Dictionary
和静态构造函数的目的是根据应用程序中定义的方面方法,维护一个所有 Decoration
对象的清单,并通过相应的方法名访问它们。这使得可以通过在应用程序配置文件中指定相应的方法名来配置方面。
配置方面
在配置文件中,您可以指定方面如何与对象关联。以下是一些示例来说明如何配置方面。
<configuration>
<configSections>
<section name="DynamicDecoratorAspect"
type="DynamicDecoratorAOP.Configuration.DynamicDecoratorSection,
DynamicDecoratorAOP.Configuration" />
</configSections>
<DynamicDecoratorAspect>
<objectTemplates>
<add name="1"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="DetailsByLevel,get_EmployeeID"
predecoration="SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
postdecoration=""/>
<add name="2"
type="ThirdPartyHR.Employee"
interface="ThirdPartyHR.IEmployee"
methods="set_EmployeeID"
predecoration="ConsoleUtil.LocalConcerns.SecurityCheck"
postdecoration="ConsoleUtil.LocalConcerns.NotifyChange"/>
</objectTemplates>
</DynamicDecoratorAspect>
</configuration>
首先,您需要在配置文件中添加一个 <DynamicDecoratorAspect>
部分。然后,在 <DynamicDecoratorAspect>
的 <objectTemplates>
中,添加各个元素。对于 <objectTemplates>
中的每个元素,都需要指定以下属性:
type
- 目标类型interface
- 返回的接口methods
- 将附加到predecoration
和postdecoration
指定的方面目标方法的名称predecoration
- 预处理方面postdecoration
- 后处理方面
注释
methods
属性值中的名称用逗号分隔。例如,"DetailsByLevel,get_EmployeeID"
。predecoration
属性的值包含两部分,由逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称,例如:"SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
。如果未指定第二部分,则假定该方面定义在入口程序集中,例如:"ConsoleUtil.LocalConcerns.SecurityCheck"
。postdecoration
属性的值包含两部分,由逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称。如果未指定第二部分,则假定该方面定义在入口程序集中。
使用 AOPUnityContainer
AOPUnityContainer
类负责读取方面配置、构造目标对象以及将方面绑定到目标对象。以下代码演示了如何使用它。
class Program
{
static void Main(string[] args)
{
using (IUnityContainer container = new UnityContainer())
{
container.RegisterType<IEmployee, Employee>(new InjectionConstructor());
AOPUnityContainer aopcontainer = new AOPUnityContainer(container);
IEmployee emp = aopcontainer.Resolve<Employee, IEmployee>();
emp.EmployeeID = 1;
emp.FirstName = "John";
emp.LastName = "Smith";
emp.DateOfBirth = new DateTime(1990, 4, 1);
emp.DepartmentID = 1;
emp.DetailsByLevel(2);
Employee target = null;
IEmployee emp1 = aopcontainer.Resolve<Employee,
IEmployee>("set_EmployeeID");
try
{
//Commenting out this line will throw out an exception
Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
Decoration dec = ConcernsContainer.runtimeAspects[
"ConsoleUtil.LocalConcerns.SecurityCheck"];
dec.Parameters = new object[] { Thread.CurrentPrincipal };
target = aopcontainer.GetTarget<Employee>();
target.PropertyChanged +=
new PropertyChangedEventHandler(PropertyChanged_Listener);
emp1.EmployeeID = 2;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Console.ReadLine();
}
static void PropertyChanged_Listener(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine(e.PropertyName.ToString() + " has changed.");
}
}
您创建一个 UnityContainer
的对象 container
,并像往常一样使用它来注册类型等。然后,不是使用它来解析业务对象,而是通过将 container
对象传递给其构造函数来创建一个 AOPUnityContainer
的 aopcontainer
。此时,您可以通过指定目标类型 T
和返回的接口类型 V
来使用 Resolve<T, V>()
。例如,在上面的代码中,您指定 Employee
作为目标类型,IEmployee
作为返回的接口类型。它返回一个 IEmployee
类型的代理。就是这样。现在,当使用 emp
时,它将开始写入进入日志。
Resolve<T, V>()
通过查找 type
属性为 T
且 interface
属性为 V
的第一个匹配项来查找配置文件中的第一个匹配元素,然后使用该元素设置来创建一个代理。对于 Resolve<Employee, IEmployee>()
,它会尝试在配置文件中查找 type
属性为 "ThirdPartyHR.Employee"
且 interface
属性为 "ThirdPartyHR.IEmployee"
的元素。配置文件中的第一个元素匹配。因此,使用 methods
属性的值 "DetailsByLevel,get_EmployeeID"
、predecoration
属性的值 "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"
和 postdecoration
属性的值 ""
来创建代理。当使用 emp
时,只有 DetailsByLevel
和 get_EmployeeID
方法会写入进入日志。
Resolve<T, V>(string methods)
通过查找 type
属性为 T
、interface
属性为 V
且 methods
属性为 methods
的第一个匹配项来查找配置文件中的第一个匹配元素。例如,代码 Resolve<Employee, IEmployee>("set_EmployeeID")
尝试匹配 type
属性为 "ThirdPartyHR.Employee"
、interface
属性为 "ThirdPartyHR.IEmployee"
且 methods
属性为 "set_EmployeeID"
,并且配置文件中的第二个元素匹配。因此,使用 predecoration
属性的值 "ConsoleUtil.LocalConcerns.SecurityCheck"
和 postdecoration
属性的值 "ConsoleUtil.LocalConcerns.NotifyChange"
来创建代理。当使用 emp1
时,只有 set_EmployeeID
方法在调用之前会进行安全检查,并在调用之后引发通知。
还有几点值得注意。在 emp1
调用方面 LocalConcerns.SecurityCheck
之前,其 paramters
参数需要更新为 WindowsPrincipal
对象。这可以通过使用 ConcernsContainer
的 Dictionary
获取与该方面关联的 Decoration
对象,然后设置其 Parameters
属性来实现。上面代码 try
块中的前三行完成了这个操作。
为了捕获在 set_EmployeeID
方法设置属性后,由方面 LocalConcerns.NotifyChange
引发的事件,您需要在调用 set_EmployeeID
方法之前向 Employee
的目标对象注册一个监听器。要实现这一点,您可以使用 AOPUnityContainer
的 GetTarget<T>()
方法获取目标对象,然后向目标对象注册监听器。try
块中的最后三行代码完成了这个操作。
执行程序时,您会看到以下输出:
如果注释掉 Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal)
这一行,您会看到以下输出:
摘要
可配置的方面与 Unity 容器相结合,使得 AOP 非常简单。您定义您的方面方法,然后在应用程序配置文件中添加元素,将您的业务对象与方面关联起来。大多数情况下,这就是您需要做的。在某些特殊情况下,您仍然可以通过代码更新方面参数参数并获取目标对象。
参考文献
以下文章将帮助您了解动态装饰器及其功能。
以下文章讨论了动态装饰器如何帮助您改进应用程序开发。
- 组件、方面和动态装饰器
- 用于 ASP.NET 应用程序的组件、方面和动态装饰器
- 用于 ASP.NET MVC 应用程序的组件、方面和动态装饰器
- 用于 Silverlight / WCF 服务应用程序的组件、方面和动态装饰器
- 用于 MVC/AJAX/REST 应用程序的组件、方面和动态装饰器
以下文章将动态装饰器与其他一些类似工具进行了比较。
以下文章讨论了关于动态装饰器和 AOP 的一些杂项主题。