0%

[DotnetCore]AOP初體驗

前情提要

筆者負責的專案,是那種到處要跟第三方串接那種,第三方不管是內部或多個外部,串接方式不外乎就是WebService或是Restful API,或者提供dll檔案,多種形式見怪不怪,串接這時候釐清問題是最重要的,因此必須要確保我方系統上保有RequestResponse以釐清問題,也是自保的一種概念,因為你無法保證串接的Method跟金額無關,這時候唯有留下系統軌跡才能保證你的清白(被害妄想症上身中),筆者相信留下紀錄這件事,不管事不是跟別的系統串接,仍是很重要的課題。

然而對於Restful API這種串接方式,最方便留下紀錄了,只要共用一個HttpClientRepository,將發出HttpRequest集中在某一個Method中,方便事後增加其往來紀錄的相關程式碼,當然我方系統是被呼叫方的話,也是可以透過DotnetCore內建的Middleware能搞定。至於呼叫WebService或者dll檔案中的Method則比較傷腦筋一點,動用到今天的主角,AOP Logging,能夠輕鬆地不留痕跡地做到留下紀錄。

AOP Logging選項

AOP: Aspect Oriented Programming,筆者這邊就不贅述了,網路上文章太多了,基本上dotnetcore上的Middleware應該就算是AOP的實作,且還有Pipeline的概念,一個接一個執行,筆者當初的想法也是如此,有沒有辦法找到類似像Middleware套用在一般的Service Method上,筆者現在都跳過Google,直接丟問題給ChatGPT

1
List the nuget package that can be logged method input and output like middleware, AOP pattern using dotnet core DI framework

還真的找到滿像樣的回答

ChatGPT回答來說,筆者大概猜得出來,除了第一個之外,其他都是第三方DI套件的附加功能,筆者專案皆是透過dotnet core內建的DI功能在注入使用所有相關Service,固然選第一個套件來survey了,這時候再用google搜尋該套件,找到該套件的github一探究竟,找到document,有了document就好辦事了。

安裝對應套件

筆者就使用ChatGPT推薦的第一個套件了,因為等等會分兩個部分註冊

  • Interceptor
  • DynamicProxyProvider

以筆者的專案配置來說,會是Service專案及Web專案皆要安裝對應的套件

1
dotnet add package AspectCore.Extensions.DependencyInjection

製作LoggingInterceptor

首先要先製作一個LoggingInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ServiceMethodLoggingInterceptor : AbstractInterceptorAttribute
{
private readonly ILogger<ServiceMethodLoggingInterceptor> _logger;
private readonly ApiRequestPipelineModel _apiRequestPipelineModel;
private readonly GenericRepository<ApiLog> _apiLogRepo;
private readonly ITimeWrapper _timeWrapper;
public ServiceMethodLoggingInterceptor(ILogger<ServiceMethodLoggingInterceptor> logger
, ApiRequestPipelineModel apiRequestPipelineModel
, GenericRepository<ApiLog> apiLogRepo
, ITimeWrapper timeWrapper)
{
_logger = logger;
_apiRequestPipelineModel = apiRequestPipelineModel;
_apiLogRepo = apiLogRepo;
_timeWrapper = timeWrapper;
}

public override async Task Invoke(AspectContext context, AspectDelegate next)
{
// Step1: Get method infos
var methodName = context.ImplementationMethod.Name;
var inputParams = context.GetParameters();
var parameterObject = JsonHelper.SerializeObjectWithCamelCase(inputParams.Select(x => x.Value));

// Step2: Add ApiLog
// 初始化ApiLog物件
var apiLog = new ApiLog(_apiRequestPipelineModel.RequestId, $"{methodName}", parameterObject, _timeWrapper.Now);
// 這邊透過Repository的AddAsync方法新增一筆ApiLog
await _apiLogRepo.AddAsync(apiLog);

// Step3: Execute method
await next(context);

// Step4: Update return value
var output = context.ReturnValue;
var response = output.Adapt<BaseResponseModel>();
apiLog.UpdateResponseInfo(response.ResponseCode, JsonHelper.SerializeObjectWithCamelCase(output), _timeWrapper.Now);
await _apiLogRepo.UpdateAsync(apiLog);
}
}

依上面的Code說明一下對應關係,因為跟本文無關就只列出概念性的解釋,想要參考的各位,請務必換成自己專案對應的寫法

  • ApiLog,對應筆者專案使用的Log資料表(因為都是屬於第三方串接,與Restful API紀錄共用)
  • GenericRepository,筆者專案使用的CRUD的Repository(單檔都透過它完成)
  • Adapt方法,則是Mapster套件的功能,與AutoMapper功能相似,簡單用,效能更好
  • 筆者這邊將物件變更都集中在該物件類別中,因此設計UpdateResponseInfo,透過此方法變更其值
  • ApiRequestPipelineModel,筆者這邊是偷用AddScoped的好處,該物件建構式中初始化一個GUID放著,一個Request中經過的任何Service,只要注入該物件,都會拿到相同的GUID,以拿來關聯各種資料為同一個Request的追查依據

註冊Interceptor

接著要透過註冊的方式註冊其上面寫的ServiceMethodLoggingInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using AspectCore.Configuration;
using AspectCore.Extensions.DependencyInjection;
// 其他Using省略

public static class ServiceRegistration
{
public static IServiceCollection AddServices(this IServiceCollection services
, IConfiguration config)
{
// 以上忽略
services.AddSingleton<ServiceMethodLoggingInterceptor>();
services.ConfigureDynamicProxy(config =>
{
config.Interceptors.AddServiced<ServiceMethodLoggingInterceptor>(Predicates.ForService("XXXService"));
});
return services;
}
}

註冊方式有很多種,參考中的使用指南都有示範,筆者用慣建構式注入的方式引用其對應的實作,因此

  • 透過AddSingleton的方式註冊其Interceptor
  • 透過AddServiced的方式新增其InterceptorDynamicProxyProvider
  • 筆者這邊有指定某XXXService,因筆者把呼叫Dll對應的方法都集中在該Service Class中,若全域套用則不需增加此設定
  • 筆者這邊習慣在某一個Project中的Service註冊集中在該Project的某一個Extension中,相關註冊對應程式碼可直接宣告在Startup中的ConfigureService即可

註冊DynamicProxyProvider

最後需要在Program那邊要作手腳,才算大功告成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using AspectCore.Extensions.DependencyInjection;
// 其他Using省略

public class Program
{
public async static Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.RunAsync();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
webBuilder
.ConfigureLogging(logging =>
{
logging.ClearProviders();
})
.UseNLog()
.UseStartup<Startup>())
// 主要是以下這行
.UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());
}

結論

筆者喜歡AOP設計的原因就是在此文章中根本沒看到商業邏輯程式,意思就是完全不用動到主程式,就能做到紀錄Input, Output的功能,超棒的,且因為該套件設計是可以指定套用該Interceptor的服務名稱,更上一層樓了,只要改動註冊的地方,其他商業邏輯主程式也輕易地享有Logging的功能,這就是AOP設計的魅力所在,今天的分享就到這了,下篇見。

參考