项目
版本

Autofac 类型拦截器

Castle.Core 是 Castle 框架 的一部分,它提供了一个名为 "DynamicProxy" 的方法拦截框架。

Autofac.Extras.DynamicProxy 集成包允许对 Autofac 组件的方法调用进行拦截,常见的使用场景包括事务处理、日志记录和声明式安全。你可以使用 Autofac.Extras.DynamicProxy2 与 Autofac 4.0.0 之前的版本配合使用。

启用拦截

要使 DynamicProxy 集成工作,基本步骤如下:

  • create_interceptors
  • register_interceptors
  • enable_type_interception
  • associate_interceptors

创建拦截器

拦截器实现 Castle.DynamicProxy.IInterceptor 接口。下面是一个简单的拦截器示例,用于记录方法调用,包括输入和输出:

public class CallLogger : IInterceptor
{
    TextWriter _output;

    public CallLogger(TextWriter output)
    {
        _output = output;
    }

    public void Intercept(IInvocation invocation)
    {
        _output.Write("Calling method {0} with parameters {1}... ",
            invocation.Method.Name,
            string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));

        invocation.Proceed();

        _output.WriteLine("Done: result was {0}.", invocation.ReturnValue);
    }
}

注册拦截器

拦截器必须在容器中注册。你可以将其注册为类型服务或命名服务。如果你选择命名服务,它们必须命名为 IInterceptor 注册项。

选择哪种方式取决于你如何决定将拦截器与被拦截的类型关联起来。

// 命名注册
builder.Register(c => new CallLogger(Console.Out))
       .Named<IInterceptor>("log-calls");

// 类型注册
builder.Register(c => new CallLogger(Console.Out));

为类型启用拦截

当你注册一个被拦截的类型时,需要在注册时标记该类型,以便 Autofac 知道要为此设置拦截。你可以使用 EnableInterfaceInterceptors()EnableClassInterceptors() 注册扩展来实现这一点。

var builder = new ContainerBuilder();
builder.RegisterType<SomeType>()
       .As<ISomeInterface>()
       .EnableInterfaceInterceptors();
builder.Register(c => new CallLogger(Console.Out));
var container = builder.Build();
var willBeIntercepted = container.Resolve<ISomeInterface>();

背后,EnableInterfaceInterceptors() 会创建一个执行拦截的接口代理,而 EnableClassInterceptors() 则会动态子类化目标组件,以拦截虚拟方法。

这两种技术都可以与 Assembly 扫描功能结合使用,因此你可以使用相同的方法配置一组组件。

特殊情况:WCF 代理和远程对象 虽然 WCF 代理对象看起来像接口,但 EnableInterfaceInterceptors() 机制无法工作,因为 .NET 在幕后实际上是使用了行为像接口的 System.Runtime.Remoting.TransparentProxy 对象。如果要在 WCF 代理上启用拦截,请使用 InterceptTransparentProxy() 方法。

var cb = new ContainerBuilder();
cb.RegisterType<TestServiceInterceptor>();
cb.Register(c => CreateChannelFactory()).SingleInstance();
cb
  .Register(c => c.Resolve<ChannelFactory<ITestService>>().CreateChannel())
  .InterceptTransparentProxy(typeof(IClientChannel))
  .InterceptedBy(typeof(TestServiceInterceptor))
  .UseWcfSafeRelease();

将拦截器与待拦截类型关联

为了选择与你的类型关联的拦截器,你有两种选择。

你的第一种选择是为类型添加一个属性,如下所示:

// 这个属性将查找具有特定类型的 TYPED 拦截器注册:
[Intercept(typeof(CallLogger))]
public class First
{
    public virtual int GetValue()
    {
        // 执行一些计算并返回一个值
    }
}

// 这个属性将查找具有特定名称的 NAMED 拦截器注册:
[Intercept("log-calls")]
public class Second
{
    public virtual int GetValue()
    {
        // 执行一些计算并返回一个值
    }
}

当使用属性将拦截器关联起来时,你不需要在注册时指定拦截器。只需启用拦截,拦截器类型将自动被发现。

// 使用 TYPED 属性:
var builder = new ContainerBuilder();
builder.RegisterType<First>()
       .EnableClassInterceptors();
builder.Register(c => new CallLogger(Console.Out));

// 使用 NAMED 属性:
var builder = new ContainerBuilder();
builder.RegisterType<Second>()
       .EnableClassInterceptors();
builder.Register(c => new CallLogger(Console.Out))
       .Named<IInterceptor>("log-calls");

第二种选择是在 Autofac 注册时间声明拦截器。你可以使用 InterceptedBy() 注册扩展来实现:

var builder = new ContainerBuilder();
builder.RegisterType<SomeType>()
       .EnableClassInterceptors()
       .InterceptedBy(typeof(CallLogger));
builder.Register(c => new CallLogger(Console.Out));

提示

使用公共接口

接口拦截要求接口是公开的(至少对动态生成的代理程序集可见)。非公共接口类型无法被拦截。

如果你想代理 internal 接口,必须在包含接口的程序集中添加注解 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

使用虚方法

类拦截要求被拦截的方法是虚方法,因为它使用子类化作为代理技术。

与表达式一起使用

使用表达式创建的组件或作为实例注册的组件,不能被 DynamicProxy2 引擎子类化。在这种情况下,需要使用基于接口的代理。

接口注册

要通过接口启用代理,组件必须只通过接口提供其服务。为了获得最佳性能,所有此类服务接口都应包含在注册中,即包含在 As<X>() 子句中。

WCF 代理

如前所述,WCF 代理和其他远程类型是特殊情况,不能使用标准的接口或类拦截。你需要为这些类型使用 InterceptTransparentProxy()

类拦截器和 UsingConstructor

如果你通过 EnableClassInterceptors() 使用类拦截器,请避免同时使用 UsingConstructor()。当启用类拦截时,生成的代理会添加一些新的构造函数,这些构造函数也会接受你想要使用的拦截器集合。当你指定 UsingConstructor() 时,会跳过此逻辑,导致你的拦截器未被使用。

已知问题

异步方法拦截

Castle 拦截器仅提供了同步方法拦截的机制——没有显式的 async/await 支持。然而,由于 async/await 只是返回 Task 对象的语法糖,你可以在拦截器中使用 TaskContinueWith() 类似的方法。这个 issue 中有一个示例。另一种选择是使用 辅助库,它们可以使异步操作更方便。

Castle.Core 版本管理

从 Castle.Core 4.2.0 开始,Castle.CoreNuGet 包版本 更新了,但 Assembly 版本 没有更新。此外,Castle.Core 4.1.0 的 Assembly 版本 与包(4.1.0.0)匹配,而 4.2.0 包回退到 4.0.0.0。在完整 .NET 框架项目中,关于 Castle.Core 版本的任何混淆可以通过添加一个 assembly 绑定重定向来解决,强制使用 Castle.Core 4.0.0.0。

不幸的是,.NET Core 不支持 assembly 绑定重定向。如果你直接依赖 Castle.Core,同时又通过库(如 Autofac.Extras.DynamicProxy)间接依赖 Castle.Core,你可能会看到类似这样的错误:

System.IO.FileLoadException: 无法加载文件或程序集 'Castle.Core, Version=4.1.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc'。已加载的程序集与程序集中引用的程序集的定义不匹配。 (异常来自 HRESULT: 0x80131040)

这是因为回退的 assembly。

确保你使用了最新版的 Autofac.Extras.DynamicProxy。我们会尽最大努力从 Autofac 方面解决问题。更新该库或 Castle.Core 可能会有帮助。

如果这还不行,有两个解决方案:

一是删除你的直接 Castle.Core 引用。间接引用应该会自行解决。

二是如果你不能删除直接引用,或者删除后不起作用……所有直接依赖项都需要更新到 Castle.Core 4.2.0 或更高版本。你需要向这些项目报告问题;这不是 Autofac 可以为你解决的问题。

这里有关于这一挑战的 Castle.Core issue。

在本文档中