全新升級的AOP框架Dora.Interception[4]: 基於Lambda表示式的攔截器註冊方式

2022-06-24 12:00:53

如果攔截器應用的目標型別是由自己定義的,Dora.Interception(github地址,覺得不錯不妨給一顆星)可以在其型別或成員上標註InterceptorAttribute特性來應用對應的攔截器。如果對那個的程式集是由第三方提供的呢?此時我們可以採用提供的第二種基於表示式的攔截器應用方式。這裡的攔截器是一個呼叫目標型別某個方法或者提取某個屬性的Lambda表示式,我們採用這種強型別的程式設計方式得到目標方法,並提升程式設計體驗。(拙著《ASP.NET Core 6框架揭祕》於日前上市,加入讀者群享6折優惠)

目錄
一、IInterceptorRegistry
二、將攔截器應用到某個型別
三、應用到指定的方法和屬性
四、指定構建攔截器的引數
五、攔截圖蔽
六、兩個後備方法

一、IInterceptorRegistry

以表示式採用強型別的方式將指定型別的攔截器應用到目標方法上是藉助如下這個IInterceptorRegistry介面完成的。IInterceptorRegistry介面提供了一個For<TInterceptor>方法以待註冊的攔截器型別關聯,引數arguments用來提供構建攔截器物件的引數。該方法會返回一個IInterceptorRegistry<TInterceptor>物件,它提供了一系列的方法幫助我們將指定的攔截器應用到指定目標型別(通過泛型引數型別TTarget表示)相應的方法上。

public interface IInterceptorRegistry
{

    IInterceptorRegistry<TInterceptor> For<TInterceptor>(params object[] arguments);
    ...
}

public interface IInterceptorRegistry<TInterceptor>
{
    IInterceptorRegistry<TInterceptor> ToAllMethods<TTarget>(int order);
    IInterceptorRegistry<TInterceptor> ToMethod<TTarget>(int order, Expression<Action<TTarget>> methodCall);
    IInterceptorRegistry<TInterceptor> ToMethod(int order, Type targetType, MethodInfo method);
    IInterceptorRegistry<TInterceptor> ToGetMethod<TTarget>(int order, Expression<Func<TTarget, object?>> propertyAccessor);
    IInterceptorRegistry<TInterceptor> ToSetMethod<TTarget>(int order, Expression<Func<TTarget, object?>> propertyAccessor);
    IInterceptorRegistry<TInterceptor> ToProperty<TTarget>(int order, Expression<Func<TTarget, object?>> propertyAccessor);
}

封裝了IServiceCollection集合的InterceptionBuilder提供了一個RegisterInterceptors擴充套件方法,我們可以利用該方法定義的Action<IInterceptorRegistry>型別的引數來使用上述的這個IInterceptorRegistry介面。不論是IServiceCollection介面的BuildInterceptableServiceProvider擴充套件方法,還是IHostBuilder介面的UseInterception方法均提供了一個可選的Action<InterceptionBuilder>委託型別的引數。

public sealed class InterceptionBuilder { public IServiceCollection Services { get; }
public InterceptionBuilder(IServiceCollection services); } public static class Extensions { public static InterceptionBuilder RegisterInterceptors(this InterceptionBuilder builder, Action<IInterceptorRegistry> register); public static IServiceProvider BuildInterceptableServiceProvider(this IServiceCollection services, Action<InterceptionBuilder>? setup = null); public static IHostBuilder UseInterception(this IHostBuilder hostBuilder, Action<InterceptionBuilder>? setup = null); }

二、將攔截器應用到某個型別

類似與將InterceptorAttribute標註到某個型別上,我們也可以採用這種方式將指定的攔截器應用到目標型別上,背後的含義就是應用到該型別可以被攔截的所以方法上(含屬性方法)。

public class FoobarInterceptor
{
    public ValueTask InvokeAsync(InvocationContext invocationContext)
    {
        var method = invocationContext.MethodInfo;
        Console.WriteLine($"{method.DeclaringType!.Name}.{method.Name} is intercepted.");
        return invocationContext.ProceedAsync();
    }
}

public class Foobar
{
    public virtual void M() { }
    public virtual object? P { get; set; }
}

我們可以採用如下的方式將呼叫IInterceptorRegistry<TInterceptor>的ToAllMethods<TTarget>方法將上面定義的攔截器FoobarInterceptor應用到Foobar型別的所有方法上。

var foobar = new ServiceCollection()
    .AddSingleton<Foobar>()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService<Foobar>();

foobar.M();
foobar.P = null;
_ = foobar.P;

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    var foobar = registry.For<FoobarInterceptor>();
    foobar.ToAllMethods<Foobar>(order: 1);
}

從如下所示的執行結果可以看出,Foobar型別的M方法和P屬性均被FoobarInterceptor攔截下來(原始碼)。

image

三、應用到指定的方法和屬性

我們可以通過指定呼叫方法或者獲取屬性的表示式來指定攔截器應用的目標方法。我們將目標型別Foobar定義成如下的形式,兩個過載的M方法和三個屬性均是可以攔截的。

public class Foobar
{
    public virtual void M(int x, int y) { }
    public virtual void M(double x, double y) { }
    public virtual object? P1 { get; set; }
    public virtual object? P2 { get; set; }
    public virtual object? P3 { get; set; }
}

我們利用如下的程式碼將上面定義的FoobarInterceptor應用到Foobar型別相應的成員上。具體來說,我們呼叫ToMethod<TTarget>方法應用到兩個過載的M方法,呼叫ToProperty<TTarget>方法應用到P1屬性的Get和Set方法上,呼叫ToGetMethod<TTarget>和ToSetMethod<TTarget>方法應用到P2屬性的Get方法和P3屬性的Set方法。

var provider = new ServiceCollection()
    .AddSingleton<Foobar>()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors));

var foobar = provider.GetRequiredService<Foobar>();

foobar.M(1, 1);
foobar.M(3.14, 3.14);
foobar.P1 = null;
_ = foobar.P1;
foobar.P2 = null;
_ = foobar.P2;
foobar.P3 = null;
_ = foobar.P3;
Console.ReadLine();

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    var foobar = registry.For<FoobarInterceptor>();
    foobar
        .ToMethod<Foobar>(order: 1, it => it.M(default(int), default(int)))
        .ToMethod<Foobar>(order: 1, it => it.M(default(double), default(double)))
        .ToProperty<Foobar>(order: 1, it => it.P1)
        .ToGetMethod<Foobar>(order: 1, it => it.P2)
        .ToSetMethod<Foobar>(order: 1, it => it.P3)
        ;
}

程式執行後,針對Foobar相應成員的攔截體現在如下所示的輸出結果上(原始碼)。

image

四、指定構建攔截器的引數

如果應用的攔截器型別建構函式指定了引數,我們採用這種註冊方式的時候也可以指定引數。以如下這個FoobarInterceptor為例,其建構函式中指定了兩個引數,一個是代表攔截器名稱的name引數,另一個是IFoobar物件。

public class FoobarInterceptor
{
    public FoobarInterceptor(string name, IFoobar foobar)
    {
        Name = name;
        Foobar = foobar;
    }

    public string Name { get; }
    public IFoobar Foobar { get; }
    public ValueTask InvokeAsync(InvocationContext invocationContext)
    {
        Console.WriteLine($"{invocationContext.MethodInfo.Name} is intercepted by FoobarInterceptor {Name}.");
        Console.WriteLine($"Foobar is '{Foobar.GetType()}'.");
        return invocationContext.ProceedAsync();
    }
}
public interface IFoobar { }
public class Foo : IFoobar { }
public class Bar: IFoobar { }

public class Invoker
{
    public virtual void M1() { }
    public virtual void M2() { }
}

由於字串引數name無法從依賴注入容器提取,所以在註冊FoobarInterceptor是必須顯式指定。如果容器能夠提供IFoobar物件,但是希望指定一個不通過的物件,也可以在註冊的時候顯式指定一個IFoobar物件。我們按照如下的方式將兩個不同的FoobarInterceptor物件分別應用到Invoker型別的Invoke1和Invoke2方法上,並分別將名稱設定為Interceptor1和Interceptor2,第二個攔截器還指定了一個Bar物件作為引數(容器預設提供的IFoobar物件的型別為Foo)。

var invoker = new ServiceCollection()
    .AddSingleton<Invoker>()
    .AddSingleton<IFoobar, Foo>()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService<Invoker>();

invoker.M1();
Console.WriteLine();
invoker.M2();

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    registry.For<FoobarInterceptor>("Interceptor1").ToMethod<Invoker>(order: 1, it => it.M1());
    registry.For<FoobarInterceptor>("Interceptor2", new Bar()).ToMethod<Invoker>(order: 1, it => it.M2());
}

程式執行之後,兩個FoobarInterceptor物件的名稱和依賴的IFoobar物件的型別以如下的形式輸出到控制檯上(原始碼)。

image

五、攔截圖蔽

除了用來註冊指定攔截器的For<TInterceptor>方法,IInterceptorRegistry介面還定義瞭如下這些用來遮蔽攔截的SuppressXxx方法。

public interface IInterceptorRegistry
{
    IInterceptorRegistry<TInterceptor> For<TInterceptor>(params object[] arguments);
    IInterceptorRegistry SupressType<TTarget>();
    IInterceptorRegistry SupressTypes(params Type[] types);
    IInterceptorRegistry SupressMethod<TTarget>(Expression<Action<TTarget>> methodCall);
    IInterceptorRegistry SupressMethods(params MethodInfo[] methods);
    IInterceptorRegistry SupressProperty<TTarget>(Expression<Func<TTarget, object?>> propertyAccessor);
    IInterceptorRegistry SupressSetMethod<TTarget>(Expression<Func<TTarget, object?>> propertyAccessor);
    IInterceptorRegistry SupressGetMethod<TTarget>(Expression<Func<TTarget, object?>> propertyAccessor);
}

我們可以採用如下的方式會將遮蔽掉Foobar型別所有成員的攔截特性,雖然攔截器FoobarInterceptor被註冊到了這個型別上(原始碼)。

var foobar = new ServiceCollection()
    .AddSingleton<Foobar>()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService<Foobar>();
...

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    registry.For<FoobarInterceptor>().ToAllMethods<Foobar>(order: 1);
    registry.SupressType<Foobar>();
}

下面的程式明確遮蔽掉Foobar型別如下這些方法的攔截能力:M方法,P1屬性的Get和Set方法(如果有)以及P屬性的Get方法(原始碼)。

var foobar = new ServiceCollection()
    .AddSingleton<Foobar>()
    .BuildInterceptableServiceProvider(interception => interception.RegisterInterceptors(RegisterInterceptors))
    .GetRequiredService<Foobar>();

...

static void RegisterInterceptors(IInterceptorRegistry registry)
{
    registry.For<FoobarInterceptor>().ToAllMethods<Foobar>(order: 1);
    registry.SupressMethod<Foobar>(it=>it.M());
    registry.SupressProperty<Foobar>(it => it.P1);
    registry.SupressGetMethod<Foobar>(it => it.P2);
}

六、兩個後備方法

通過指定呼叫目標方法或者提取屬性的表示式來提供攔截器應用的方法和需要遮蔽的方法提供了較好的程式設計體驗,但是能夠提供這種強型別程式設計模式的前提是目標方法或者屬性是公共成員。對於受保護(protected)的方法和屬性,我們只能使用如下兩個後備方法,指定代表目標方法的MethodInfo物件。

public interface IInterceptorRegistry<TInterceptor>
{
      IInterceptorRegistry<TInterceptor> ToMethods<TTarget>(int order, params MethodInfo[] methods);
}

public interface IInterceptorRegistry
{
    IInterceptorRegistry SupressMethods(params MethodInfo[] methods);
}

全新升級的AOP框架Dora.Interception[1]: 程式設計體驗
全新升級的AOP框架Dora.Interception[2]: 基於約定的攔截器定義方式
全新升級的AOP框架Dora.Interception[3]: 基於「特性標註」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[4]: 基於「Lambda表示式」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[5]: 實現任意的攔截器註冊方式
全新升級的AOP框架Dora.Interception[6]: 框架設計和實現原理