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

面向 .NET 应用程序的横切关注点

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2015 年 11 月 27 日

CPOL

11分钟阅读

viewsIcon

51873

downloadIcon

527

如何使用 Microsoft Unity Interception 作为 .NET 应用程序中横切关注点的解决方案

Cross

引言

横切关注点是由应用程序的所有层使用的特定服务提供的常见方法;“贯穿整个应用程序”。
例如

  • 日志记录
  • 缓存
  • 授权
  • 错误处理
  • 审计

本文描述了在 .NET 应用程序中实现横切关注点的各种示例。
对于每个示例,都列出了优点和缺点。
结论是,在灵活性、可维护性、工作量以及遵循 SOLID 软件设计原则方面,使用 Microsoft Unity 实现横切关注点可能是最合适的方式。

示例

向使用者类添加代码

当横切关注点通过在使用者类中简单地添加代码来实现时,代码将被充斥着依赖项,当代码处于测试状态或开发团队进行并行开发时,这些依赖项将成为负担。

一个例子是

public class ProductRepository    
{
 public void Update(string productName)
 {
  Update(productName);
  new EventLog("MyLog").WriteEntry(string.Format("{0} is updated in the database"));
 }
}

上面的代码可以正常工作,但存在一些缺点。
它违反了单一职责原则,同时具有更新和日志记录功能。
由于 `update` 方法在不触发日志记录服务的情况下无法调用,因此 `update` 方法无法以隔离的方式进行测试。
日志记录器 (`EventLog` 类) 无法轻易地被存根或假日志记录器替换。
`ProductRepository` 拖带着日志记录 DLL 的依赖项,这意味着每当日志记录代码库出现问题时,`ProductRepository` 也会出现问题。

使用依赖注入

使用依赖注入是一个更好的解决方案,但仍然需要将其注入到每个想要使用该关注点的类中,因此可能非常重复。

此外,接口现在呈现了类的不特定行为,例如,日志记录不是 `ProductRepository` 的内在行为,但它在接口中(过于)明显地存在。

public class ProductRepository
{
        ILogger _logger;
 
        public ProductRepository(ILogger logger)
        {
            _logger = logger;
        }
 
        public void Update(string productName)
        {
            Update(productName);
            logger.WriteEntry(string.Format("{0} is updated in the database"));
        }
}

使用环境上下文

环境上下文由一组定义的属性或方法表示,这些属性或方法可以像应用程序变量(或全局变量)一样被使用。

实现这一点的一种方法是使用带有可写单例类的 `static` 属性或 `static` 方法。
当环境上下文中的 `public` 方法返回一个值供使用者使用时,环境上下文的最佳使用方式。
当 `public` 方法返回 `void` 时,例如,方法写入日志、缓存等,当原始类不使用返回值时,就没有必要“污染”原始代码。
在这种情况下,使用拦截是一个更好的解决方案,因为不需要将返回值传递给使用者。
下一个示例显示了 `abstract` 类 `TimeProvider`。

public abstract class TimeProvider
    {
        private static TimeProvider current;
 
        static TimeProvider()
        {
            TimeProvider.current = new DefaultTimeProvider();
        }
 
        public static TimeProvider Current
        {
            get { return TimeProvider.current; }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException("value");
                }
                TimeProvider.current = value;
            }
        }
 
        public abstract DateTime UtcNow { get; }
 
        public static void Reset()
        {
            TimeProvider.current = new DefaultTimeProvider();
        }
    } 
 
    public class DefaultTimeProvider : TimeProvider
    {
        public override DateTime UtcNow
        {
            get { return DateTime.UtcNow; }
        }
    }

环境上下文的正确用法

  • 确保它具有内在的默认值,它应该始终在不分配自定义上下文的情况下工作。
  • 上下文不能为 null,使用者应该能够始终查询环境上下文。
  • 这可以通过一个卫语句来实现,该卫语句检查设置器值是否不为 `null`。

环境上下文的优点

  • 始终可用
  • `timeprovider` 类可以在应用程序的任何地方使用,它始终可用,而无需扩展接口/API(遵守接口隔离原则)

可模拟

`timeprovider` 类可以使用模拟框架进行模拟(以下示例中使用 `NSubstitute`)

var timeProviderStub = Substitute.For<TimeProvider>();
timeProviderStub.UtcNow.Returns(DateTime.UtcNow.AddYears(-1));
Assert.AreEqual(DateTime.UtcNow.AddYears(-1).Year, timeProviderStub.UtcNow.Year);

环境上下文的缺点

黑箱
  • `TimeProvider` 的使用者是(更加)黑箱的。
  • 从其接口来看,使用者正在使用 `TimeProvider` 获取时间,这一点并不清楚。
  • 如果 `TimeProvider` 的行为发生更改,所有使用者都会随之更改,而更改的直接来源可能很难找到。
棘手
  • 与任何应用程序范围的实例/变量一样,如果实现不当,值或引用可能会因不同的并行线程问题而改变。

装饰器 (Decorator)

装饰器模式是遵守 SOLID 编程原则中最明显的方法之一。良好的实现将展示所有原则。

  • 装饰器通过使用一个类并同时实现该类的相同接口来充当该类的包装器。
  • 这使得替换原始类并覆盖该类中的方法成为可能。
  • 覆盖可以调用注入的类并将自己的额外功能添加进来,例如日志记录或缓存。

代码示例

public interface IProductRepository
{
   void Update(string productname);
}

public class ProductRepository : IProductRepository
{
  public void Update(string productname)
  {
    //write to database
  }
}

public class ProductRepositoryWithLog : IProductRepository
    {
        private IProductRepository _productRepository;
 
        public ProductRepositoryWithLog(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }
 
        public void Update(string productName)
        {
            //Delegate to original update method
            _productRepository.Update(productName);
            //Decoration with logging feature
            new EventLog("MyLog").WriteEntry(string.Format("{0} is updated in the database"));
        }
    }

使用装饰器的优势

  • 遵循 SOLID 原则,具有所有带来的优势:代码更具可读性、可测试性、可扩展性、可维护性和松耦合性。

缺点

  • 对于大型项目,它可能冗长且重复,违反了 DRY 原则。

面向方面编程

AOP 是一种自动将装饰器连接到原始类的机制,无需编写实际的装饰器。以方面形式呈现的横切关注点功能通过自定义属性附加到原始类。

PostSharp 就是这样一个 AOP 框架,它在编译时使用代码织入,在编译过程中拦截并用横切关注点方面来装饰原始类。
使用 PostSharp 这样的 AOP 框架有利有弊,这完全取决于项目和情况。

缺点之一是大多数 AOP 机制使用属性来声明方面。
方面是 AOP 框架在编译后步骤中添加的代码片段。这可能导致调试问题和所谓的供应商锁定反模式。此外,使用属性会使您的代码与所使用的 AOP 框架之间产生硬依赖,从而降低代码的松耦合性。

使用 Unity 进行拦截

  • 拦截是一种设计模式,用于以透明、非侵入性的方式修改或增强代码。
  • 使用 (Microsoft) Unity,可以在运行时使用(动态)拦截,而无需修改编译后的代码。
  • Unity 提供了一种定义装饰器的方式,该装饰器可以被任意类使用,而无需该类实现此装饰器作为接口。
  • 简而言之,Unity 提供两种类型的拦截:实例拦截和类型拦截。

拦截类型:实例

  • Unity 根据目标对象实现的接口创建一个代理对象。
  • 要使用 Unity 实现实例拦截,Microsoft 提供了两种拦截器类型
    • `InterfaceInterceptor`:一种实例拦截器,它通过动态生成单个接口的代理类来实现。
    • `TransparentProxyInterceptor`:一种实例拦截器,它使用远程代理进行拦截。
      此拦截器的优点是它可以拦截多个接口,但速度较慢是其缺点。

实现

  • 创建一个实现 `IinterceptionBehavior` 的类。此类将充当装饰器。
  • 当成员被拦截时,将调用 `Invoke` 方法。在 `Invoke` 方法中,定义所需的横切关注点,例如日志记录
public class LoggingInterceptionBehavior : IInterceptionBehavior
    {
        public IMethodReturn Invoke(IMethodInvocation input,
        GetNextInterceptionBehaviorDelegate getNext)
        {
            // Before invoking the method on the original target.
            WriteToLog(String.Format(
            "Invoking method {0} at {1}",
            input.MethodBase, DateTime.Now.ToLongTimeString()));
            // Invoke the next behavior in the chain.
            var result = getNext()(input, getNext);
            // After invoking the method on the original target.
 
            if (result.Exception != null)
            {
                WriteToLog(String.Format(
                "Method {0} threw exception {1} at {2}",
                input.MethodBase, result.Exception.Message,
                DateTime.Now.ToLongTimeString()));
            }
 
            else
            {
                WriteToLog(String.Format(
                "Method {0} returned {1} at {2}",
                input.MethodBase, result.ReturnValue,
                DateTime.Now.ToLongTimeString()));
            }
            return result;
        }
 
        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return Type.EmptyTypes;
        }
 
        public bool WillExecute
        {
            get { return true; }
        }
 
        private void WriteToLog(string message)
        {
            EventLog eventLog = new EventLog("");
            eventLog.Source = "Interception";
            eventLog.WriteEntry(string.Format("{0}", message, EventLogEntryType.Information));
        }
    }

接下来,在容器中定义您想要拦截的类和接口(您通常会为其编写装饰器的类),并通过该语句定义拦截类型和哪个装饰器(例如,`LoggingInterceptionBehavior`)

container.AddNewExtension<Interception>();
//class to decorate
container.RegisterType<IProductRepository, ProductRepository>(
//interceptor type
new Interceptor<InterfaceInterceptor>(),
//decorator
new InterceptionBehavior<LoggingInterceptionBehavior>());

拦截类型:类型

Unity 根据目标对象的类型创建一个对象,实际上是目标对象的子类型。使用的拦截器称为 `VirtualMethodInterceptor`。

VirtualMethodInterceptor

  • 拦截所有方法,包括内部方法
  • 只能在创建对象时使用,不能用于现有实例
  • 例如,您不能对通道工厂为您创建的 WCF 服务代理对象使用类型拦截
  • 这些拦截类型的缺点是,您无法对类的拦截内容进行细粒度控制。

实现

创建一个实现:`IinterceptionBehavior` 的类

接下来,像接口拦截一样在容器中注册,不同之处在于您使用 `VirtualMethodInterceptor`

container.AddNewExtension<Interception>();
container.RegisterType<ProductRepository>(
new Interceptor<VirtualMethodInterceptor>(),
new InterceptionBehavior<LoggingInterceptionBehavior>());

概述

下图显示了两种拦截类型的优点和缺点

拦截类型

优点

缺点

(实例)

 

Transparent Proxy Interceptor

 

可以拦截目标对象的所有方法(虚拟、非虚拟或接口)。

 

对象必须实现接口或继承自 `System.MarshalByRefObject`。如果远程对象不是基类,则只能代理接口方法。透明代理进程比常规方法调用慢得多。

 

(实例)

 

Interface Interceptor

 

允许拦截实现目标接口的任何对象。它比 `TransparentProxyInterceptor` 快得多。

 

它只拦截单个接口上的方法。它不能将代理强制转换为目标对象的类或其他接口。

 

(类型)

 

Virtual Method Interceptor

 

调用速度比 Transparent Proxy Interceptor 快得多。

 

拦截仅发生在虚拟方法上。您必须在对象创建时设置拦截,而不能拦截现有对象。

 

Unity 配置

有多种方法可以配置 Unity。您可以使用策略和属性。

策略注入

策略注入是一种拦截器,它捕获通过(unity)容器解析的对象上的调用,并应用一个策略,该策略使用从 Unity 继承的调用处理程序和匹配规则,根据每个方法的策略注入行为。

调用处理程序是实际的装饰器,应用程序配置文件中定义的行为规则描述了装饰器如何以及何时使用。
策略注入通过行为规则让您更精确地控制哪些类型的类或方法(成员)被拦截。策略注入不限于特定类或接口,行为规则以声明方式描述了哪些成员被拦截。

匹配示例

匹配规则 描述
程序集匹配规则

选择指定程序集中的类

自定义属性匹配规则

选择具有任意属性的类或成员

成员名称匹配规则

根据成员名称选择类成员

方法签名匹配规则

选择具有特定签名的类方法

命名空间匹配规则

根据命名空间名称选择类

参数类型匹配规则

根据目标对象成员的参数类型名称选择类成员

属性匹配规则

按名称和访问器类型选择类属性

返回类型匹配规则

选择返回指定类型对象的类成员

Tag 属性匹配规则

选择带有指定名称 `Tag` 属性的类成员

类型匹配规则

选择指定类型的类

实现

创建一个实现 `IcallHandler` 的类

  • 此类将充当装饰器。
  • 调用处理程序可以为特定方法编写得非常具体,也可以非常通用,以处理任何成员。
  • 当成员被拦截时,将调用 `Invoke` 方法。
  • 在 `Invoke` 方法中,定义所需的横切关注点,例如日志记录
public class LoggingCallHandler : ICallHandler
    {
        public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextHandlerDelegate getNext)
        {
            System.Diagnostics.Debug.WriteLine("Begin");
 
            string message =
                string.Format(
               "Assembly : {0}.{1}Location : {2}.{3}Calling Class : {4}",
                input.MethodBase.DeclaringType.Assembly.FullName,
                Environment.NewLine,
                input.MethodBase.DeclaringType.Assembly.Location,
                Environment.NewLine,
                input.MethodBase.DeclaringType.FullName
                );
 
            WriteToLog(string.Format("{0} is called with parameters '{1}'", 
                input.MethodBase.Name,
                input.Inputs[0]));
 
            WriteToLog(message);
            
            IMethodReturn result = getNext().Invoke(input, getNext);
            WriteToLog(string.Format("{0} is ended", input.MethodBase.Name));
            System.Diagnostics.Debug.WriteLine("End");
            return result;
        }
 
        // order in which handler will be invoked
        public int Order { get; set; }
 
        private void WriteToLog(string message)
        {
            EventLog eventLog = new EventLog("");
            eventLog.Source = "Interception";
            eventLog.WriteEntry(string.Format("{0}", 
        message,EventLogEntryType.Information));
        }
    }

接下来,您只需要在应用程序配置文件中描述策略,并在容器中加载此行为。

示例

<container name="Demo">
      <extension type="Interception"/>
      <register type="Logger.CallHandler.LoggingCallHandler, Logger"/>
      <register type="DynamicInterception.IProductRepository, DynamicInterception" 
       mapTo="DynamicInterception.ProductRepository, DynamicInterception"/>
      <register type="DynamicInterception.ProductRepository, DynamicInterception">
        <interceptor type="VirtualMethodInterceptor"/>
        <interceptionBehavior type="PolicyInjectionBehavior"/>
      </register>
      <interception>
        <policy name="LogPolicy">
 
       <matchingRule type="NamespaceMatchingRule" name=" NameSpaceRule">
            <constructor>
              <param name="namespaceName" value="DynamicInterception"/>
            </constructor>
          </matchingRule>
         
          
       <matchingRule type="MemberNameMatchingRule" name="MemberMatch">
            <constructor>
              <param name="nameToMatch" value="Update" />
            </constructor>
          </matchingRule>
          
          <callHandler type="Logger.CallHandler.LoggingCallHandler, 
          Logger" name="logging handler"/>
        </policy>
      </interception>
   </container>

在容器中加载这些规则

container.LoadConfiguration("Demo");

结果将是 `DynamicInterception` 命名空间中所有名为“`update`”的方法都将被拦截,从而具有日志记录功能。

策略注入和属性

使用属性无需配置任何策略,但您需要定义自己的属性。属性声明成员是否必须被拦截。
使用属性时,违反 SOLID 原则很重要,属性会强制您引用提供横切关注点服务的 DLL。

如果您将属性应用于您希望拦截的方法,您实际上是在创建对该方面(aspect)的硬依赖。假设您想处理一组方法的安全性。
声明安全属性到您的方法上时,您就创建了对该安全实现的硬依赖。

实现

首先,在容器中注册带有属性的策略注入

container.AddNewExtension<Interception>();
container.RegisterType<IProductRepository, ProductRepository>(
new Interceptor<InterfaceInterceptor>(),
new InterceptionBehavior<PolicyInjectionBehavior>());

接下来,创建一个实现 `HandlerAttribute` 的属性。
`AttributeUsage` 属性指定属性可以应用的语言元素。

示例

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method)]
    public class LogCallBeginEndAttribute : HandlerAttribute
    {
        public override ICallHandler CreateHandler(IUnityContainer container)
        {
            return container.Resolve<LoggingCallHandler>();
        }
    }

Use the attribute on members you want to intercept :
[LogCallBeginEndAttribute()]
public virtual void Delete(string productname)
{
   //delete in database
}

关注点

所需工具

为了使用 Unity 进行拦截,必须满足以下要求

  • 操作系统:Microsoft Windows® 7 Professional、Enterprise 或 Ultimate;Windows Server 2003 R2;Service Pack 2 的 Windows Server 2008;Windows Server 2008 R2;Service Pack 2 的 Windows Vista;或 Service Pack 3 的 Windows XP。
  • Microsoft .NET Framework 3.5 Service Pack 1 或 Microsoft .NET Framework 4.0。或更高版本
  • .NET 的 Unity Application Block (可在 nuget.org 上获取)
  • .NET 的 Unity Interception Extension (可在 nuget.org 上获取)

依赖项

  • 除了使用必需的工具外,没有其他依赖项。

准则

  • 最佳实践是使用策略注入,以避免在实现 `IinterceptionBehavior` 接口的类中使用过滤。
  • 使用策略注入时,请从狭窄的规则开始,以避免项目中所有成员都被拦截。

历史

  • 首次发布
© . All rights reserved.