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
對AbstractInterceptorAttribute
點F12
進去也可以看到,他總共能適用於 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)