跳转到内容

AOP 与拦截器

[UnitOfWork][Cacheable]、审计留痕这些横切能力,底层都是同一套机制:Castle 动态代理 + IXiHanInterceptor。理解它能解释一大类「特性标了没生效」的问题,也是写自己横切逻辑的入口。

一条必须先记住的限制

拦截只对「以接口为服务类型」的注册生效

AddCastleDynamicProxy 只处理 ServiceType.IsInterface 为真的服务描述器(用的是 CreateInterfaceProxyWithTarget)。

csharp
services.AddScoped<MyService>();               // ❌ 注册为自身类型 → 不会被代理
services.AddScoped<IMyService, MyService>();   // ✅ 接口 → 会被代理

把类注册为它自己,[UnitOfWork] / [Cacheable] 全部静默失效——不报错、不记日志。要用 AOP 就走「接口 + 实现」。

其次检查:类型有没有被列进 DynamicProxyIgnoreTypes

写一个自己的拦截器

1. 拦截器

csharp
using XiHan.Framework.Core.DynamicProxy;

public class BillingAuditInterceptor(ILogger<BillingAuditInterceptor> logger) : IXiHanInterceptor
{
    public async Task InterceptAsync(IXiHanMethodInvocation invocation)
    {
        var sw = Stopwatch.StartNew();
        await invocation.ProceedAsync();     // ← 必须调用,否则目标方法不会执行
        logger.LogInformation("{Method} 耗时 {Ms}ms", invocation.Method.Name, sw.ElapsedMilliseconds);
    }
}

IXiHanMethodInvocation 能拿到什么:

成员说明
MethodMethodInfo,可读特性
Arguments / ArgumentsDictionary入参
GenericArguments泛型实参
TargetObject真实目标实例
ReturnValue返回值,可读可写
ProceedAsync()调用下一环

2. 注册器:决定拦谁

csharp
public static class BillingAuditInterceptorRegistrar
{
    public static void RegisterIfNeeded(IOnServiceRegistredContext context)
    {
        if (!DynamicProxyIgnoreTypes.Contains(context.ImplementationType)
            && context.ImplementationType.IsDefined(typeof(BillingAuditAttribute), inherit: true))
        {
            context.Interceptors.TryAdd<BillingAuditInterceptor>();
        }
    }
}

3. 挂上

csharp
public override void PreConfigureServices(ServiceConfigurationContext context)
{
    context.Services.OnRegistered(BillingAuditInterceptorRegistrar.RegisterIfNeeded);
}

OnRegistered 登记的回调会在每个服务描述器上跑一遍;只要往 context.Interceptors 塞了拦截器,该服务就会被包成代理。框架自己的 XiHanUowModuleXiHanCachingModule 用的就是这个 API。

挂在 PreConfigureServices

注册器要在其他模块注册服务之前登记好,否则先注册的服务不会被扫到。

拿到未被代理的真实实例

某些场景下代理会坏事——最典型的是匿名端点

匿名端点调被代理的服务会永久挂起

匿名端点(Minimal API、OAuth 回调等)不经过工作单元中间件,而服务被代理包着,UoW 拦截器会急切开事务从而死锁。

解法是绕开代理:

  • 注入真正的依赖直连,或
  • ProxyHelper.UnProxy(service) 取真实目标实例再调。

框架的 OAuth 回调端点就是这么处理的。

执行顺序

多个拦截器按登记顺序串成管道:

text
拦截器 A 前段 → 拦截器 B 前段 → 目标方法 → B 后段 → A 后段

先登记的在外层。要控制顺序就控制 OnRegistered 的调用顺序(即模块的拓扑序)。

常见问题

现象原因
特性标了没生效服务注册类型不是接口(最常见);或类型在 DynamicProxyIgnoreTypes
目标方法没执行拦截器忘了调 ProceedAsync()
匿名端点调用挂起走了代理,用 ProxyHelper.UnProxy
自定义拦截器不生效OnRegistered 挂在了 ConfigureServices(太晚),改到 PreConfigureServices

下一步

Released under The MIT License