Norns.Urd 中的一些設計

victor.x.qu發表於2020-12-09

Norns.Urd 是什麼?

Norns.Urd 是一個基於emit實現動態代理的輕量級AOP框架.
版本基於 netstandard2.0. 所以哪些.net 版本能用你懂的。
完成這個框架的目的主要出自於個人以下意願:

  • 靜態AOP和動態AOP都實現一次
  • 如果不實現DI,怎麼將AOP框架實現與其他現有DI框架整合
  • 一個AOP 如何將 sync 和 async 方法同時相容且如何將實現選擇權完全交予使用者

希望該庫能對大家有些小小的作用
中文文件在:https://fs7744.github.io/Norns.Urd/zh-cn/index.html
順便求個star, github 地址:https://github.com/fs7744/Norns.Urd

對了,如果不瞭解AOP的同學,可以看看這些文章:
面向切面的程式設計
什麼是面向切面程式設計AOP?
AOP 有幾種實現方式?

Norns.Urd 中的一些設計

Norns.Urd的實現前提

由於Norns.Urd的實現基於以下兩點前提

  1. 將 sync 和 async 方法同時相容且如何將實現選擇權完全交予使用者

    • 其實這點還好,工作量變成兩倍多一些就好,sync 和 async 完全拆分成兩套實現。
    • 提供給使用者的Interceptor介面要提供 sync 和 async 混合在一套實現程式碼的方案,畢竟不能強迫使用者實現兩套程式碼,很多場景使用者不需要為sync 和 async 的差異而實現兩套程式碼
  2. 不包含任何內建DI,但要整體都為支援DI而作

    • 其實如果內建DI容器可以讓支援 generic 場景變得非常簡單,畢竟從DI容器中例項化物件時必須有明確的型別,但是呢,現在已經有了那麼多實現的庫了,我就不想為了一些場景而實現很多功能(我真的懶,否則這個庫也不會寫那麼久了)
    • 但是DI容器確實解耦非常棒,我自己都常常因此受益而減少了很多程式碼修改量,所以做一個aop庫必須要考慮基於DI容器做支援,這樣的話,di 支援的 open generic / 自定義例項化方法都要做支援,並且aop裡面還得提供使用者呼叫DI的方法,否則還不好用了 (這樣算下來,我真的偷懶了嗎?我是不是在給自己挖坑呀?)

如何設計解決的?

目前方案不一定完美,暫時算解決了問題而已 (有更好方案請一定要告訴我,我迫切需要學習)

提供什麼樣的攔截器編寫模式給使用者?

以前接觸一些其他aop實現框架,很多都需要將攔截程式碼分為 方法前 / 方法後 / 有異常等等,個人覺得這樣的形式還是一定程度上影響攔截器實現的程式碼思路,總覺得不夠順滑

但是像 ASP.NET Core Middleware就感覺非常不錯,如下圖和程式碼:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/index/_static/request-delegate-pipeline.png?view=aspnetcore-5.0

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello, World!");
});

攔截器也應該可以像這樣做,所以攔截器的程式碼應該可以像這樣:

public class ConsoleInterceptor 
{
    public async Task InvokeAsync(Context context, Delegate next)
    {
        Console.WriteLine("Hello, World!");
        await next(context);
    }
}

sync 和 async 方法如何拆分?又如何能合併在一起呢?使用者有怎麼自己選擇實現sync 還是 async 或者兩個都都實現呢?


public delegate Task AsyncAspectDelegate(AspectContext context);

public delegate void AspectDelegate(AspectContext context);

// 拆分: 
// 由AspectDelegate 和 AsyncAspectDelegate 建立兩套完全區分 sync 和 async 的Middleware呼叫鏈,具體使用哪個由具體被攔截的方法本身決定

public abstract class AbstractInterceptor : IInterceptor
{
    public virtual void Invoke(AspectContext context, AspectDelegate next)
    {
        InvokeAsync(context, c =>
        {
            next(c);
            return Task.CompletedTask;
        }).ConfigureAwait(false)
                    .GetAwaiter()
                    .GetResult();
    }

// 合併:
// 預設實現轉換方法內容,這樣各種攔截器都可以混在一個Middleware呼叫鏈中

    public abstract Task InvokeAsync(AspectContext context, AsyncAspectDelegate next);

// 使用者自主性選擇:
// 同時提供sync 和 async 攔截器方法可以過載,使用者就可以自己選擇了
// 所以使用者在 async 中可以呼叫專門的未非同步優化程式碼了,也不用說在 sync 中必須 awit 會影響效能了,
// 你認為影響效能,你在乎就自己都過載,不在乎那就自己選
}

沒有內建DI,如何相容其他DI框架呢?

DI框架都有註冊型別,我們可以通過 emit 生成代理類,替換原本的註冊,就可以做到相容。

當然每種DI框架都需要定製化的實現一些程式碼才能支援(唉,又是工作量呀)

AddTransient<IMTest>(x => new NMTest()), 類似這樣的例項化方法怎麼支援呢?

由於這種DI框架的用法,無法通過Func函式拿到實際會使用的型別,只能根據IMTest定義通過emit 生成 橋接代理型別,其偽碼類似如下:


interface IMTest
{
    int Get(int i);
}

class IMTestProxy : IMTest
{
    IMTest instance = (x => new NMTest())();

    int Get(int i) => instance.Get(i);
}

.AddTransient(typeof(IGenericTest<,>), typeof(GenericTest<,>)) 類似這樣的 Open generic 怎麼支援呢?

其實對於泛型,我們通過 emit 生成泛型型別一點問題都沒有,唯一的難點是不好生成 Get<T>() 這樣的方法呼叫, 因為IL需要反射找到的具體方法,比如Get<int>() Get<bool>() 等等,不能是不明確的 Get<T>()

要解決這個問題就只能將實際的呼叫延遲到執行時呼叫再生成具體的呼叫,偽碼大致如下:


interface GenericTest<T,R>
{
    T Get<T>(T i) => i;
}

class GenericTestProxy<T,R> : GenericTest<T,R>
{
    T Get<T>(T i) => this.GetType().GetMethod("Get<T>").Invoke(i);
}

相關文章