使用ImpromptuInterface反射庫方便的建立自定義DfaGraphWriter

依樂祝發表於2020-07-21

在本文中,我為建立的自定義的DfaGraphWriter實現奠定了基礎。DfaGraphWriter是公開的,因此您可以如上一篇文章中所示在應用程式中使用它,但它使用的所有類均已標記為internal。這使得建立自己的版本成為問題。要解決此問題,我使用了一個開源的反射庫ImpromptuInterface,使建立自定義的DfaGraphWriter實現更加容易。

作者:依樂祝
原文地址:https://andrewlock.net/creating-a-custom-dfagraphwriter-using-impromptuinterface-and reflection/
譯文地址:https://www.cnblogs.com/yilezhu/p/13336066.html

我們將從檢視現有的DfaGraphWriter開始,以瞭解其使用的internal類以及導致我們的問題。然後,我們來看一下使用一些自定義介面和ImpromptuInterface庫來允許我們呼叫這些類。在下一篇文章中,我們將研究如何使用自定義介面建立的自定義版本DfaGraphWriter

探索現有的 DfaGraphWriter

DfaGraphWriter類是存在於ASP.NET Core中的一個“pubternal”資料夾中的。它已註冊為單例,並使用注入的IServiceProvider來解析DfaMatcherBuilder

 public class DfaGraphWriter
{
    private readonly IServiceProvider _services;
    public DfaGraphWriter(IServiceProvider services)
    {
        _services = services;
    }

    public void Write(EndpointDataSource dataSource, TextWriter writer)
    {
        // retrieve the required DfaMatcherBuilder
        var builder = _services.GetRequiredService<DfaMatcherBuilder>();

        // loop through the endpoints in the dataSource, and add them to the builder
        var endpoints = dataSource.Endpoints;
        for (var i = 0; i < endpoints.Count; i++)
        {
            if (endpoints[i] is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>()?.SuppressMatching ?? false) == false)
            {
                builder.AddEndpoint(endpoint);
            }
        }

        // Build the DfaTree. 
        // This is what we use to create the endpoint graph
        var tree = builder.BuildDfaTree(includeLabel: true);

        // Add the header
        writer.WriteLine("digraph DFA {");

        // Visit each node in the graph to create the output
        tree.Visit(WriteNode);

        //Close the graph
        writer.WriteLine("}");

        // Recursively walks the tree, writing it to the TextWriter
        void WriteNode(DfaNode node)
        {
            // Removed for brevity - we'll explore it in the next post
        }
    }
}

上面的程式碼顯示了圖形編寫者Write方法的所有操作,終結如下:

  • 獲取一個 DfaMatcherBuilder
  • 寫入所有的端點EndpointDataSourceDfaMatcherBuilder
  • 呼叫DfaMatcherBuilderBuildDfaTree。這將建立一個DfaNode的 圖。
  • 訪問DfaNode樹中的每一個,並將其寫入TextWriter輸出。我們將在下一篇文章中探討這種方法。

建立我們自己的自定義編寫器的目的是通過控制如何將不同的節點寫入輸出來定製最後一步,因此我們可以建立更多的描述性的圖形,如我先前所示:

對端點圖使用不同的樣式

我們的問題是兩個重點類,DfaMatcherBuilderDfaNode,是internal所以我們不能輕易例項化它們,或者使用它們的寫入方法。這給出了兩個選擇:

  • 重新實現這些internal類,包括它們依賴的其他任何internal類。
  • 使用反射在現有類上建立和呼叫方法。

這些都不是很好的選擇,但是鑑於端點圖不是效能關鍵的東西,我決定使用反射將是最簡單的。為了使事情變得更加簡單,我使用了開源庫ImpromptuInterface

ImpromptuInterface使反射更容易

ImpromptuInterface是一個庫它使呼叫動態物件或呼叫儲存在物件引用中的底層物件上的方法變得更加容易。它本質上增加了簡單的duck/structural型別,允許您為物件使用stronlgy型別化介面。它使用Dynamic Language RuntimeReflection.Emit來實現。

例如,讓我們獲取我們要使用的現有DfaMatcherBuilder類。即使我們不能直接引用它,我們仍然可以從DI容器中獲取此類的例項,如下所示:

// get the DfaMatcherBuilder type - internal, so needs reflection :(
Type matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly
    .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder");

object rawBuilder = _services.GetRequiredService(matcherBuilder);

rawBuilder是一個object引用,但它包含了一個DfaMatcherBuilder的例項。我們不能直接在呼叫它的方法,但是我們可以通過直接構建MethodInfo和直接呼叫invoke來使用反射來呼叫它們。。

ImpromptuInterface通過提供一個可以直接呼叫方法的靜態介面,使該過程更加容易。例如,對於DfaMatcherBuilder,我們只需要呼叫兩個方法AddEndpointBuildDfaTree。原始類如下所示:

internal class DfaMatcherBuilder : MatcherBuilder
{
    public override void AddEndpoint(RouteEndpoint endpoint) { /* body */ }
    public DfaNode BuildDfaTree(bool includeLabel = false)
}

我們可以建立一個暴露這些方法的介面:

public interface IDfaMatcherBuilder
{
    void AddEndpoint(RouteEndpoint endpoint);
    object BuildDfaTree(bool includeLabel = false);
}

然後,我們可以使用ImpromptuInterface ActLike<>方法建立實現了IDfaMatcherBuilder的代理物件。此代理包裝rawbuilder物件,因此當您在介面上呼叫方法時,它將在底層呼叫DfaMatcherBuilder中的等效的方法:

使用ImpromptuInterface新增包裝代理

在程式碼中,如下所示:

// An instance of DfaMatcherBuilder in an object reference
object rawBuilder = _services.GetRequiredService(matcherBuilder);

// wrap the instance in the ImpromptuInterface interface
IDfaMatcherBuilder builder = rawBuilder.ActLike<IDfaMatcherBuilder>();

// we can now call methods on the builder directly, e.g. 
object rawTree =  builder.BuildDfaTree();

原始DfaMatcherBuilder.BuildDfaTree()方法和介面版本之間有一個重要區別:原始方法返回一個DfaNode,但這是另一個internal類,因此我們無法在介面中引用它。

相反,我們為DfaNode建立另一個ImpromptuInterface,暴露我們將需要的屬性(在接下來的文章中你就會明白為什麼我們需要他們):

public interface IDfaNode
{
    public string Label { get; set; }
    public List<Endpoint> Matches { get; }
    public IDictionary Literals { get; } // actually a Dictionary<string, DfaNode>
    public object Parameters { get; } // actually a DfaNode
    public object CatchAll { get; } // actually a DfaNode
    public IDictionary PolicyEdges { get; } // actually a Dictionary<object, DfaNode>
}

在下一篇文章中,我們將在WriteNode的方法中使用這些屬性,但是有一些複雜性。在原始DfaNode類中,ParametersCatchAll屬性返回DfaNode物件。在我們IDfaNode版本的屬性中,我們必須返回object。我們無法引用DfaNode(因為是internal)並且我們不能返回IDfaNode,因為DfaNode 它沒有實現IDfaNode,因此您不能將object引用隱式轉換為IDfaNode。你必須使用ImpromptuInterface顯式地新增一個實現了介面的代理,。

例如:

// Wrap the instance in the ImpromptuInterface interface
IDfaMatcherBuilder builder = rawBuilder.ActLike<IDfaMatcherBuilder>();

// We can now call methods on the builder directly, e.g. 
object rawTree =  builder.BuildDfaTree();
// Use ImpromptuInterface to add an IDfaNode wrapper
IDfaNode tree = rawTree.ActLike<IDfaNode>();

// We can now call methods and properties on the node...
object rawParameters = tree.Parameters;
// ...but they need to be wrapped using ImpromptuInterface too
IDfaNode parameters = rawParameters.ActLike<IDfaNode>();

返回Dictionary型別的屬性還有另一個問題:LiteralsPolicyEdges。實際返回的型別分別為Dictionary<string, DfaNode>Dictionary<object, DfaNode>,但是我們需要使用一個包含該DfaNode型別的型別。不幸的是,這意味著我們不得不退回到.NET 1.1 IDictionary介面!

您不能將一個Dictionary<string, DfaNode>強制轉換為IDictionary<string, object>,因為這樣做將是不安全的協方差形式

IDictionary是一個非泛型介面,因此keyvalue僅作為object公開。對於string鍵,您可以直接進行轉換,對於,DfaNode我們可以使用ImpromptuInterface為我們建立代理包裝器:

// Enumerate the key-value pairs as DictinoaryEntrys
foreach (DictionaryEntry dictEntry in node.Literals)
{
    // Cast the key value to a string directly
    var key = (string)dictEntry.Key;
    // Use ImpromptuInterface to add a wrapper
    IDfaNode value = dictEntry.Value.ActLike<IDfaNode>();
}

現在,我們已經擁有了通過實現WriteNode來建立自定義DfaWriter實現所需的一切物件,但是這篇文章已經有點長了,所以我們將在下一篇文章中探討如何實現這一點!

摘要

在本文中,我探討了DfaWriter在ASP.NET Core 中的實現以及它使用的兩個internal類:DfaMatcherBuilderDfaNode。這些類是內部類的事實使得建立我們自己的DfaWriter實現非常棘手。為了乾淨地實現它,我們將不得不重新實現這兩種型別以及它們所依賴的所有類。

作為替代,我使用ImpromptuInterface庫建立了一個包裝器代理,該代理實現與被包裝的物件擁有類似的方法。這使用反射來呼叫包裝屬性上的方法,但允許我們使用強型別介面。在下一篇文章中,我將展示如何使用這些包裝器建立一個定製的DfaWriter來進行端點圖的自定義。

相關文章