原文地址:“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
才能向用戶提供一些特定功能,例如從類型中提取接口。一方面,這不是很友好,因為東西很容易崩潰,但是對於客戶的價值是毋庸置疑的,所以我們還是選擇這樣做。