65.9K
CodeProject 正在变化。 阅读更多。
Home

Unity 容器的可配置方面

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 9 月 8 日

CPOL

8分钟阅读

viewsIcon

20788

downloadIcon

196

通过配置使用动态装饰器(Dynamic Decorator)为 Unity 容器添加 AOP 功能。

引言

在文章 MEF 的可配置方面 中,我介绍了一种基于 动态装饰器(Dynamic Decorator) 的、用于 MEF 的可配置 AOP 模型。该模型同样适用于其他 IoC 容器,如 UnityWindsor 等。

在本文中,我将介绍适用于 Unity IoC 容器的 AOP 模型。使用该模型,您可以在应用程序配置文件中为对象配置方面,并使用自定义的 Unity 容器来创建具有已附加方面(aspects)的对象。

背景

动态装饰器(Dynamic Decorator)是一种通过将行为附加到对象而不是修改类或创建新类来扩展对象功能的工具。它很有吸引力,因为它避免了设计或重新设计类的麻烦。请阅读文章 动态装饰器模式使用动态装饰器为对象添加方面,以更深入地了解动态装饰器、它要解决的问题、如何解决这些问题以及它的局限性。

还有几篇其他文章讨论了动态装饰器如何用于应用程序开发,并将其与其他类似技术进行比较。请参阅本文的“参考文献”部分。

一旦您阅读了一些文章,您就会发现动态装饰器使用简单且功能强大。在本文中,我将介绍动态装饰器的一个可配置模型,该模型使其使用更加简便,同时保持其强大的功能。然后,该模型与 Unity 容器集成,使得从 Unity 容器中出来的对象已经附加了方面。

使用此模型,您可以在应用程序配置文件中为对象配置方面。然后,一个自定义的 Unity 容器将创建您的对象,并根据配置将方面与之绑定。当您使用这些对象时,由方面定义的相应功能已经附加到对象上。

示例

假设您有一个简单的组件 Employee,它实现了 IEmployeeINotifyPropertyChanged 两个接口,如下所示。

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();
    }
}

正如您所见,这些方法以通用方式访问 TargetCallCtx,并且可以被各种类型的对象共享,以写入进入/退出日志。

另一方面,某些方面可能需要访问更具体的信息。例如,您只想在 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 对象作为参数。

注意:您可能已经注意到,在 SysConcernsLocalConcerns 的每个类中都有一个静态构造函数。在静态类内部,类中定义的每个方面方法都用于创建一个 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 - 将附加到 predecorationpostdecoration 指定的方面目标方法的名称
  • predecoration - 预处理方面
  • postdecoration - 后处理方面

注释

  1. methods 属性值中的名称用逗号分隔。例如,"DetailsByLevel,get_EmployeeID"
  2. predecoration 属性的值包含两部分,由逗号分隔。第一部分指定方面名称,第二部分指定定义该方面的程序集名称,例如:"SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"。如果未指定第二部分,则假定该方面定义在入口程序集中,例如:"ConsoleUtil.LocalConcerns.SecurityCheck"
  3. 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 对象传递给其构造函数来创建一个 AOPUnityContaineraopcontainer。此时,您可以通过指定目标类型 T 和返回的接口类型 V 来使用 Resolve<T, V>()。例如,在上面的代码中,您指定 Employee 作为目标类型,IEmployee 作为返回的接口类型。它返回一个 IEmployee 类型的代理。就是这样。现在,当使用 emp 时,它将开始写入进入日志。

Resolve<T, V>() 通过查找 type 属性为 Tinterface 属性为 V 的第一个匹配项来查找配置文件中的第一个匹配元素,然后使用该元素设置来创建一个代理。对于 Resolve<Employee, IEmployee>(),它会尝试在配置文件中查找 type 属性为 "ThirdPartyHR.Employee"interface 属性为 "ThirdPartyHR.IEmployee" 的元素。配置文件中的第一个元素匹配。因此,使用 methods 属性的值 "DetailsByLevel,get_EmployeeID"predecoration 属性的值 "SharedLib.SysConcerns.EnterLog,SharedLib.SysConcerns"postdecoration 属性的值 "" 来创建代理。当使用 emp 时,只有 DetailsByLevelget_EmployeeID 方法会写入进入日志。

Resolve<T, V>(string methods) 通过查找 type 属性为 Tinterface 属性为 Vmethods 属性为 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 对象。这可以通过使用 ConcernsContainerDictionary 获取与该方面关联的 Decoration 对象,然后设置其 Parameters 属性来实现。上面代码 try 块中的前三行完成了这个操作。

为了捕获在 set_EmployeeID 方法设置属性后,由方面 LocalConcerns.NotifyChange 引发的事件,您需要在调用 set_EmployeeID 方法之前向 Employee 的目标对象注册一个监听器。要实现这一点,您可以使用 AOPUnityContainerGetTarget<T>() 方法获取目标对象,然后向目标对象注册监听器。try 块中的最后三行代码完成了这个操作。

执行程序时,您会看到以下输出:

如果注释掉 Thread.GetDomain().SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal) 这一行,您会看到以下输出:

摘要

可配置的方面与 Unity 容器相结合,使得 AOP 非常简单。您定义您的方面方法,然后在应用程序配置文件中添加元素,将您的业务对象与方面关联起来。大多数情况下,这就是您需要做的。在某些特殊情况下,您仍然可以通过代码更新方面参数参数并获取目标对象。

参考文献

以下文章将帮助您了解动态装饰器及其功能。

  1. 动态装饰器模式
  2. 使用动态装饰器为对象添加方面

以下文章讨论了动态装饰器如何帮助您改进应用程序开发。

  1. 组件、方面和动态装饰器
  2. 用于 ASP.NET 应用程序的组件、方面和动态装饰器
  3. 用于 ASP.NET MVC 应用程序的组件、方面和动态装饰器
  4. 用于 Silverlight / WCF 服务应用程序的组件、方面和动态装饰器
  5. 用于 MVC/AJAX/REST 应用程序的组件、方面和动态装饰器

以下文章将动态装饰器与其他一些类似工具进行了比较。

  1. 动态装饰器与 Castle DynamicProxy 对比
  2. 动态装饰器、Unity 和 Castle DynamicProxy 对比

以下文章讨论了关于动态装饰器和 AOP 的一些杂项主题。

  1. 动态装饰器的性能
  2. 泛型动态装饰器
  3. 面向对象的方面 vs. 面向类的方面
© . All rights reserved.