[.NET Core] .NET Core 的 Interceptor 解決方案 - AspectCore

AspectCore 是一個基於 AOP 概念設計的框架,可以在不更改主要流程的程式碼下,動態的從中添加功能。這個套件可以用在 .NET Framework 以及 .NET Core,這裡主要將會以 .NET 6 做為示範,簡單展示幾個 Interceptor 的應用方式。

 

 

前言

.NET Core 官方的 DI 功能太基礎,沒有提供攔截器(Interceptor)這種功能,沒有辦法善用 AOP 的設計一直讓我覺得很可惜,後來我在偶然的機會得知有一個叫 AspectCore 的套件,便想說研究看看,殊不知雖然他的 Nuget 官方說明看似很簡單,但實際使用之後卻發現有很多寫法不如預期的好用 😟,官方 Doc 又寫得很陽春,很多內容都沒補充,在看到 GitHub Issues 上哀鴻遍野的狀況,讓我對這個套件滿頭問號,真的有可能有人寫出這麼難用的套件嗎???

後來我在經歷了一些摸索後,終於搞懂這個套件的使用方式,驚覺其實有很多設定並沒有很難用,只是官方 Doc 描述上容易讓人混淆,導致使用上白白遭受許多困境,覺得很可惜,便決定寫出這篇,希望可以幫助到跟我有相同困擾的夥伴們~!

Interceptor 跟 AOP 的原理息息相關,不過這篇不會對這方面解釋太多,所以不了解 AOP 的小夥伴可以先去搜尋 AOP 後再回來這篇文章喔~~

 

 

示範專案

TargetFramework:.NET 6

IOC:.NET 原生

專案類型:Web API

AspectCore 套件:dotnetcore/AspectCore-Framework: AspectCore is an AOP-based cross platform framework for .NET Standard. (github.com)

GitHub example project:KittyChen913/NET6_AspectCore_Demo (github.com)

 

 

Install AspectCore

所需的套件有 3 個

  • AspectCore.Core
  • Microsoft.Extension.DependencyInjection
  • AspectCore.Extensions.DependencyInjection

不過可以只安裝這一個就好 👇

AspectCore.Extensions.DependencyInjection 2.4.0

他就會把需要的其他相依套件一起載入了

 

 

撰寫自定義 Interceptor

CustomInterceptorAttribute.cs

public class CustomInterceptorAttribute : AbstractInterceptorAttribute
{
    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
        try
        {
            Console.WriteLine("Invoking Interceptor");
            await next(context);
            Console.WriteLine("Invoked Interceptor");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Interceptor Exception{ex}");
            throw;
        }
    }
}

新增一個 class 繼承 AbstractInterceptorAttribute 抽象類別,去實作Invoke 方法,就可以在await next(context) 上下撰寫攔截器前後所需的處理。

 

 

配置 AspectCore Interceptor

Program.cs

builder.Services.ConfigureDynamicProxy();

//Replace the default IOC container with the AspectCore one.
builder.Host.UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());

添加完這 2 行,即完成基本的 AspectCore 設定,接著就是怎麼使用的問題了

 

Interceptor 主要有兩種使用方式:

以下將會各自示範一遍,並沿途補充相關的注意事項~

 

 

Interceptor Attribute 版

如果只是要 for 單個 method 或是 class,可以採用默認的 Attribute 用法即可

use example IWeatherService.cs

[CustomInterceptor] // Add for all implementations under the interface
public interface IWeatherService
{
    [CustomInterceptor] // Add for single implementation
    IEnumerable<WeatherForecast> GetWeatherForecast();
    string GetUserName();
}

像上面的範例中[CustomInterceptor] 可以掛在 interface 單個方法上,這樣的話此方法的進出就會經過這個 Interceptor

也可以將 [CustomInterceptor] 掛在 interface 上面,則代表這個 interface 底下的所有實作方法都要使用這個 Interceptor

 

AbstractInterceptorAttributeF12進去也可以看到,他總共能適用於 Class / Method / Interface

掛完 Attribute 後,就已經完成第一版的 Interceptor 了~

 

 

AspectCore 的 Interceptor DI

上面基礎的 Interceptor 只有做簡單的Console.WriteLine(),但實務情境上有可能還需要注入 ILogger 寫 Log,或是注入其他東西來做更多設定,所以 Interceptor 也需要可以支援 DI 才行

AspectCore 的 Interceptor DI 主要有支援 3 種方式:

如果 Interceptor 是 Attribute 版的,目前本人實測只能使用建構子注入,所以以下先示範建構子注入的寫法

 

 

讓 Interceptor 支援建構子注入

Program.cs

builder.Services.AddScoped<CustomInterceptorAttribute>();

如果要讓相依可以由 DI 提供,Interceptor 就必須先註冊

 

use example IWeatherService.cs

public interface IWeatherService
{
    [ServiceInterceptor(typeof(CustomInterceptorAttribute))]
    IEnumerable<WeatherForecast> GetWeatherForecast();
    string GetUserName();
}

Attribute 要再加掛 [ServiceInterceptor],他才有辦法從 DI 擷取 CustomInterceptorAttribute 所需的 instance

 

CustomInterceptorAttribute.cs

public class CustomInterceptorAttribute : AbstractInterceptorAttribute
{
    private readonly ILogger<SingleInterceptorAttribute> logger;
    
    public SingleInterceptorAttribute(ILogger<SingleInterceptorAttribute> logger)
    {
        this.logger = logger;
    }

    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
        try
        {
            logger.LogInformation("Invoking Interceptor");
            await next(context);
            logger.LogInformation("Invoked Interceptor");
        }
        catch (Exception ex)
        {
            logger.LogInformation($"Interceptor Exception{ex}");
            throw;
        }
    }
}

就可以支援注入ILogger

 

 

Interceptor Global 版

如果要設置能夠多個地方一起使用的 Global Interceptor 就比較多 需要注意的事項了

 

Program.cs

builder.Services.ConfigureDynamicProxy(config => 
{ 
	config.Interceptors.AddServiced<GlobalInterceptorAttribute>(); 
});

只要在ConfigureDynamicProxy內配置你寫的自定義 Interceptor,他就會變成 Global 的版本

但是這裡要注意的是 ❗   他不能什麼條件都沒有的直接像上面這樣寫 ⬆️

他會直接變成整個專案的 Global Interceptor,任何方法都會被這個 Interceptor 攔截到 😰

 

必須像這樣 👇 在 parameters 裡添加一些條件

builder.Services.ConfigureDynamicProxy(config => 
{ 
	config.Interceptors.AddServiced<GlobalInterceptorAttribute>(Predicates.ForService("*Repository")); 
});

他可以支援 for namespace、service、method 的條件判定

  • Predicates.ForNameSpace()
  • Predicates.ForService()
  • Predicates.ForMethod()

像是

 Predicates.ForMethod("GetCityList") 

代表只有名稱叫 GetCityList 的 method 會使用此 Interceptor

 

他也支援萬用字元 *

 Predicates.ForMethod("*List") 

代表名稱是 List 結尾的 method 會使用此 Interceptor

 

 

Global Interceptor 支援屬性注入

Global 的 Interceptor 也可以支援上面提到的建構子注入,但是屬性注入的寫法,本人目前實測發現只有在 Global 版本的 Interceptor 可以使用,這裡也示範一下用法以及補充一下注意事項

 

GlobalInterceptorAttribute.cs

public class GlobalInterceptorAttribute : AbstractInterceptorAttribute
{
    [FromServiceContext]
    public ILogger<GlobalInterceptorAttribute> logger { get; set; }

    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
        try
        {
            logger.LogInformation("Invoking Interceptor");
            await next(context);
            logger.LogInformation("Invoked Interceptor");
        }
        catch (Exception ex)
        {
            logger.LogInformation($"Interceptor Exception{ex}");
            throw;
        }
    }
}

若採用屬性注入的寫法,就必須要在 Property 上掛[FromServiceContext] Attribute

 

Program.cs

// builder.Services.AddScoped<GlobalInterceptorAttribute>(); // no need

builder.Services.ConfigureDynamicProxy(config => 
{ 
	config.Interceptors.AddTyped<GlobalInterceptorAttribute>(Predicates.ForService("*Repository"));
});

如果採用屬性注入,Interceptor 就不用像建構子注入那樣要先在 Program 註冊

然後要特別注意的是,如果要支援屬性注入的寫法的話,在 Program 的配置要改成用AddTyped()AddServiced()只支援建構子注入

 

 

Global Interceptor 忽略配置

AspectCore 是有提供[NonAspect]Attribute

public interface IWeatherService
{
    [NonAspect]
    IEnumerable<WeatherForecast> GetWeatherForecast();
}

或是在 Program 配置的排除寫法

config.NonAspectPredicates.AddMethod("GetWeatherForecast");

都可以讓某個 NameSpace / Service / Method 不會被 Interceptor 攔截

 

但是我實測後發現這 2 種設置方式只能是 Global 的設定,這很奇怪

因為實務情境上我可能有些方法沒有要被 A Interceptor 攔截,但也許我需要他被 B Interceptor 攔截啊!😟

這個功能讓我覺得非常詭異,我覺得沒有很好用,所以後來我自己試寫了一種 for 單個 Interceptor 排除的寫法….

 

 

Single Interceptor 忽略配置

GlobalInterceptorAttribute.cs

public class GlobalInterceptorAttribute : AbstractInterceptorAttribute
{
    [FromServiceContext]
    public ILogger<GlobalInterceptorAttribute> logger { get; set; }

    public override async Task Invoke(AspectContext context, AspectDelegate next)
    {
    	if (context.ServiceMethod.GetCustomAttributes<DisableGlobalInterceptor>(inherit: true).Any())
        {
            await next(context);
            return;
        }
        
        try
        {
            logger.LogInformation("Invoking Interceptor");
            await next(context);
            logger.LogInformation("Invoked Interceptor");
        }
        catch (Exception ex)
        {
            logger.LogInformation($"Interceptor Exception{ex}");
            throw;
        }
    }
}

public sealed class DisableGlobalInterceptor : Attribute { }

我這裡添加了[DisableGlobalInterceptor]Attribute,裡面什麼都不做,單純用來給 Interceptor Invoke()內判定用的

 

if (context.ServiceMethod.GetCustomAttributes<DisableGlobalInterceptor>(inherit: true).Any())
{
    await next(context);
    return;
}

如果 method 有帶[DisableGlobalInterceptor]Attribute,就直接做完直接出去,不要走 Interceptor 內的其他處理,Done

 

use example IWeatherService.cs

public interface IWeatherService
{
    [DisableGlobalInterceptor]
    IEnumerable<WeatherForecast> GetWeatherForecast();
}

 

 

 

 

以上範例的 Source Code 我已放到 GitHub,請大家儘管服用,如果對上述寫法有其他想法的人也歡迎與我交流~😆

GitHub:KittyChen913/NET6_AspectCore_Demo (github.com)