1. 程式人生 > >在.NET Core中使用DispatchProxy“實現”非公開的介面

在.NET Core中使用DispatchProxy“實現”非公開的介面

原文地址:“Implementing” a non-public interface in .NET Core with DispatchProxy
原文作者:Filip W.
譯文地址:https://www.cnblogs.com/lwqlun/p/11575686.html
譯者:Lamond Lu

簡介

反射是.NET中一個非常強大的概念,對於每一個C#開發人員來說,遲早都會使用到這個它。在許多場景中,反射都非常有用,例如程式集掃描,型別發現或者各種程式組合使用。

然而,它經常被用來繞過你正在使用的依賴項的public介面 - 修改它們或者訪問依賴項做著未曾預想的內容。這就是說,這種“黑客入侵”的方式對於C#開發來說非常的典型,儘管有一定的風險,但是它可能有時候是讓你擺脫編碼困境的唯一方法。

如果你被迫公開一個非public(例如可能是internal的)介面的實現,那麼事情就開始變得有趣了。針對這個問題,“基本的”反射已經不能帶來任何幫助了,所以讓我們來一起看一下我們應該如何實現這個需求。

示例問題

想想一下,你正在使用一個第三方庫,在這個庫中包含一下的內部類Greeter

internal class Greeter
{
    public static void Greet(IGreeting greeting)
    {
        Console.WriteLine(greeting.Message);
    }
}

現在呢,我們希望通過反射,使用這個型別,執行它其中定義的Greet

方法。為了實現這個需求,你需要一個實現IGreeting介面的實現類例項,因為它是Greet方法所需的引數。IGreeting介面的程式碼如下:

internal interface IGreeting
{
    string Message { get; }
}

這裡需要注意的是,這裡沒有任何一個你可以直接使用的IGreeting介面的實現。相反的,要使用Greeter類,你就必須自己提供一個IGreeting介面的實現。

當然,使用C#實現一個介面很簡單 - 但是如何實現一個通過反射提取到的介面?好吧,這有一點問題,不是麼?下面的程式碼,也對此進行了說明,注意該示例程式碼中的類與Greeter

IGreeting型別存在於不同的程式集中。

class Program
{
    static void Main(string[] args)
    {
        // 查詢非公開Greeter型別                
        var greeterType = Assembly.Load("Library").GetType("Library.Greeter");
        
        // 提取Greet方法
        var greetMethod = internalType.GetMethod("Greet", BindingFlags.Public | BindingFlags.Static);
 
        // 嘗試執行方法,然而...
        // ...我們需要一個IGreeting介面型別的例項,我們該怎麼辦?
        var greeting = greetMethod.Invoke(null, new[] { ??? });
        Console.WriteLine();
    }
}

DispatchProxy

下面讓我們來使用DispatchProxy類。這個型別自.NET Core誕生之日起,就已經存在了,它提供了例項化代理物件和處理器方法分發的機制。DispatchProxy類的典型用法如下:

var proxy = DispatchProxy.Create<IFoo, FooProxy>();

這我們的示例中,IFoo是我們需要實現的介面。DispatchProxy的強大功能如下:它允許我們建立一個FooProxy型別,該型別可以像IFoo一樣被使用,且不需要真正"實現它"。(或者它也可以轉發給另一個實際上模擬IFoo介面的型別)

但是,當使用以上API的時候,代理類實現的介面型別需要在編譯時被知曉,這對於我們當前的用例不太理想 - 因為在非public的介面情況下,我們只能在執行時才能抓住它。不過不用擔心,我們將使用反射解決這個問題。以下的程式碼說明了我們的做法(假設IFoo是非public的):

var internalType = Assembly.Load("Library").GetType("IFoo");
var proxy = typeof(DispatchProxy)
    .GetMethod(nameof(DispatchProxy.Create))
    .MakeGenericMethod(internalType, typeof(FooProxy))
    .Invoke(null, null);

最後就很簡單了,我們使用與之前相同的Api, 但是我們可以動態的提供必要的引數型別,而不必在編譯時才知道它們才能在泛型中使用。

在我們特定的Greeting例子中,用於建立代理的方法如下:(為了更清晰的分離,我們將它封裝在一個工廠類中)。

public class GreetingFactory
{
    public static object Create()
    {
        var internalType = Assembly.Load("Library").GetType("Library.IGreeting");
        return typeof(DispatchProxy)
            .GetMethod(nameof(DispatchProxy.Create))
            .MakeGenericMethod(internalType, typeof(GreetingProxy))
            .Invoke(null, null);
    }
}

現在,謎題的最後一塊碎片就是實現GreetingProxy了。如下的程式碼展示了GreetingProxy類的實現,它是DispatchProxy的一個子類。

public class GreetingProxy : DispatchProxy
{
    private GreetingImpl _impl;
 
    public GreetingProxy()
    {
        _impl = new GreetingImpl();
    }
 
    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        return _impl.GetType().GetMethod(targetMethod.Name).Invoke(_impl, args);
    }
 
    private class GreetingImpl // : 不實現IGreeting, 但是模擬了它
    {
        public string Message => "hello world";
    }
}

如你所見,這個類充當了潛在呼叫者與IGreeting實際實現之間的閘道器,畢竟這就是代理的主要作用。這個"實現"(我用引號引起來,因為我們並不是真正實現非public介面),或者更確切的說,使用私有類GreetingImpl的形式模仿介面型別,幷包含了必要的public屬性Message。這裡並不是必須要要這麼做,這只是我自己喜歡的一種實現方式。

每當代用代理類的時候,我們都會可以根據請求的介面成員獲得MethodInfo資訊 - 因此,我們只需將其重定向到結構相同的隱藏實現GreetingImpl的相應成員即可。

最後,我們的程式碼看起來應該是這樣的。

class Program
{
    static void Main(string[] args)
    {
        var internalType = Assembly.Load("Library").GetType("Library.Greeter");
        var greetMethod = internalType.GetMethod("Greet", BindingFlags.Public | BindingFlags.Static);
 
        var proxy = GreetingFactory.Create();
        Console.WriteLine(greetMethod.Invoke(null, new[] { proxy }));
    }
}

那麼,這個方法到底是用來做什麼的呢?它通過代理物件,呼叫了GreetingImpl中定義的方法,打印出了"Hello World"。當然,最終的結果是我們設法“實現”並使用了非公開API中的非public介面。

這在真實需求中有用麼?

就像其他所有東西一樣,我覺著答案 - 取決於 -畢竟它是一個高度專業化的API。這種技術(代理物件)經常會在ORM和其他Mock框架中使用。另外,如果你使用的是複雜的第三方框架或庫,並且需要使用大量的反射,那麼你遲早會用到DispatchProxy

實際上,如果你對真實需求的例子感興趣, 你可以來看看我們的OmniSharp專案。OmniSharp專案使用Roslyn編譯器為VSCode等許多程式碼編輯器提供程式碼感知功能。但是不幸的是,Roslyn並不會提供大量public API, 所以我們不得不大量使用反射。實際上,我們還必須在許多地方使用DispatchProxy才能向用戶提供一些特定功能,例如從型別中提取介面。一方面,這不是很友好,因為東西很容易崩潰,但是對於客戶的價值是毋庸置疑的,所以我們還是選擇這樣做