web-dev-qa-db-de.com

Abhängigkeitseingabe mehrerer Instanzen desselben Typs in ASP.NET Core 2

In ASP.NET Core 2 Web Api möchte ich die Abhängigkeitsinjektion verwenden, um eine httpClientA-Instanz von HttpClient in ControllerA und eine Instanz httpClientB der HttpClient in ControllerB zu injizieren.

Der DI-Registrierungscode würde ungefähr so ​​aussehen:

HttpClient httpClientA = new HttpClient();
httpClientA.BaseAddress = endPointA;
services.AddSingleton<HttpClient>(httpClientA);

HttpClient httpClientB = new HttpClient();
httpClientB.BaseAddress = endPointB;
services.AddSingleton<HttpClient>(httpClientB);

Ich weiß, ich könnte HttpClient in eine Unterklasse einordnen, um einen eindeutigen Typ für jeden Controller zu erstellen, aber das lässt sich nicht gut skalieren.

Was ist ein besserer Weg?

UPDATE Insbesondere in Bezug auf HttpClient scheint Microsoft etwas in Arbeit zu haben 

https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 - Danke an @ mountain-traveller (Dylan) für den Hinweis.

13
Bryan

Hinweis: Diese Antwort verwendet HttpClient und ein HttpClientFactory als Beispiel aber trifft leicht auf irgendeine andere Art von Sache zu. Insbesondere für HttpClient wird nter Verwendung des neuen IHttpClientFactory from Microsoft.Extensions.Http bevorzugt.


Der integrierte Dependency Injection Container unterstützt keine benannten Dependency-Registrierungen, und es gibt derzeit keine Pläne, dies hinzuzufügen .

Ein Grund dafür ist, dass es bei der Abhängigkeitsinjektion keine typsichere Möglichkeit gibt, anzugeben, welche Art von benannter Instanz gewünscht wird. Sie könnten sicherlich Parameterattribute für Konstruktoren (oder Attribute für Eigenschaften zur Eigenschaftsinjektion) verwenden, aber das wäre eine andere Art von Komplexität, die sich wahrscheinlich nicht lohnt. und es würde mit Sicherheit nicht unterstützt werden durch das Typsystem, was ein wichtiger Teil der Funktionsweise der Abhängigkeitsinjektion ist.

Im Allgemeinen sind benannte Abhängigkeiten ein Zeichen dafür, dass Sie Ihre Abhängigkeiten nicht ordnungsgemäß entwerfen. Wenn Sie zwei verschiedene Abhängigkeiten desselben Typs haben, sollte dies bedeuten, dass sie möglicherweise austauschbar sind. Wenn dies nicht der Fall ist und eines davon gültig ist, während das andere nicht der Fall ist, ist dies ein Zeichen dafür, dass Sie möglicherweise das Liskov-Substitutionsprinzip verletzen.

Wenn Sie sich außerdem die Abhängigkeitsinjektion ansehen, die do support benannte Abhängigkeiten enthält, werden Sie feststellen, dass der einzige Weg, diese Abhängigkeiten abzurufen, nicht die Abhängigkeitsinjektion, sondern das Service Locator-Muster) ist stattdessen ist dies das genaue Gegenteil von mkehrung der Kontrolle , das DI ermöglicht.

Simple Injector, einer der größeren Abhängigkeitsinjektionscontainer, erklärt das Fehlen benannter Abhängigkeiten wie folgt :

Das Auflösen von Instanzen mit einem Schlüssel ist eine Funktion, die in Simple Injector absichtlich nicht berücksichtigt wird, da sie immer zu einem Entwurf führt, bei dem die Anwendung häufig zahlreiche Abhängigkeiten vom DI-Container selbst aufweist. Um eine verschlüsselte Instanz aufzulösen, müssen Sie wahrscheinlich direkt die Container -Instanz aufrufen, und dies führt zu dem Service Locator-Anti-Pattern .

Dies bedeutet nicht, dass das Auflösen von Instanzen mit einem Schlüssel niemals nützlich ist. Das Auflösen von Instanzen mit einem Schlüssel ist normalerweise ein Job für eine bestimmte Factory und nicht für den Container. Durch diesen Ansatz wird das Design viel übersichtlicher, Sie müssen nicht mehr viele Abhängigkeiten von der DI-Bibliothek eingehen, und es werden viele Szenarien möglich, die von den DI-Containerautoren einfach nicht berücksichtigt wurden.


Abgesehen davon möchten Sie manchmal wirklich so etwas und es ist einfach nicht möglich, eine Vielzahl von Untertypen und separaten Registrierungen zu haben. In diesem Fall gibt es jedoch geeignete Methoden, um dies zu erreichen.

Ich kann mir eine bestimmte Situation vorstellen, in der der Framework-Code von ASP.NET Core etwas Ähnliches enthält: Benannte Konfigurationsoptionen für das Authentifizierungs-Framework. Lassen Sie mich versuchen, das Konzept schnell zu erklären (bitte mit mir):

Der Authentifizierungsstapel in ASP.NET Core unterstützt die Registrierung mehrerer Authentifizierungsanbieter desselben Typs. Beispielsweise kann es vorkommen, dass Ihre Anwendung mehrere OpenID Connect-Anbieter verwendet. Obwohl sie alle die gleiche technische Implementierung des Protokolls haben, müssen sie in der Lage sein, unabhängig zu arbeiten und die Instanzen individuell zu konfigurieren.

Dies wird gelöst, indem jedem "Authentifizierungsschema" ein eindeutiger Name zugewiesen wird. Wenn Sie ein Schema hinzufügen, registrieren Sie im Grunde genommen einen neuen Namen und teilen der Registrierung mit, welcher Handlertyp verwendet werden soll. Außerdem konfigurieren Sie jedes Schema mit IConfigureNamedOptions<T> , das bei der Implementierung im Grunde ein nicht konfiguriertes Optionsobjekt erhält, das dann konfiguriert wird - wenn der Name übereinstimmt. Für jeden Authentifizierungstyp T wird es schließlich mehrere Registrierungen für IConfigureNamedOptions<T> Geben, die ein einzelnes Optionsobjekt für ein Schema konfigurieren können.

Irgendwann wird ein Authentifizierungshandler für ein bestimmtes Schema ausgeführt und benötigt das tatsächlich konfigurierte Optionsobjekt. Dazu hängt es von IOptionsFactory<T> Ab, welches Standardimplementierung Ihnen die Möglichkeit gibt, ein konkretes Optionsobjekt zu erstellen, das dann von all diesen IConfigureNamedOptions<T> - Handlern konfiguriert wird.

Und genau diese Logik der Options Factory können Sie nutzen, um eine Art „benannte Abhängigkeit“ zu erreichen. Übersetzt in Ihr spezielles Beispiel könnte das zum Beispiel so aussehen:

// container type to hold the client and give it a name
public class NamedHttpClient
{
    public string Name { get; private set; }
    public HttpClient Client { get; private set; }

    public NamedHttpClient (string name, HttpClient client)
    {
        Name = name;
        Client = client;
    }
}

// factory to retrieve the named clients
public class HttpClientFactory
{
    private readonly IDictionary<string, HttpClient> _clients;

    public HttpClientFactory(IEnumerable<NamedHttpClient> clients)
    {
        _clients = clients.ToDictionary(n => n.Key, n => n.Value);
    }

    public HttpClient GetClient(string name)
    {
        if (_clients.TryGet(name, out var client))
            return client;

        // handle error
        throw new ArgumentException(nameof(name));
    }
}


// register those named clients
services.AddSingleton<NamedHttpClient>(new NamedHttpClient("A", httpClientA));
services.AddSingleton<NamedHttpClient>(new NamedHttpClient("B", httpClientB));

Sie würden dann den HttpClientFactory irgendwo einfügen und seine GetClient -Methode verwenden, um einen benannten Client abzurufen.

Wenn Sie über diese Implementierung und über das, was ich zuvor geschrieben habe, nachdenken, wird dies offensichtlich einem Service-Locator-Muster sehr ähnlich sein. In gewisser Weise handelt es sich in diesem Fall tatsächlich um eine solche, die jedoch auf dem vorhandenen Abhängigkeitsinjektionsbehälter aufbaut. Macht es das besser? Wahrscheinlich nicht, aber es ist eine Möglichkeit, Ihre Anforderungen mit dem vorhandenen Container umzusetzen. Für die vollständige Verteidigung ist übrigens im obigen Fall der Authentifizierungsoptionen die Optionsfactory eine real - Factory. Sie erstellt also tatsächliche Objekte und verwendet keine vorhandenen vorregistrierten Instanzen, also technisch = nicht ein Servicestandortmuster dort.


Offensichtlich besteht die andere Alternative darin, das, was ich oben geschrieben habe, vollständig zu ignorieren und einen anderen Abhängigkeitsinjektionscontainer mit ASP.NET Core zu verwenden. Beispiel: Autofac unterstützt benannte Abhängigkeiten und kann ersetzt problemlos den Standardcontainer für ASP.NET Core .

25
poke

Benannte Registrierungen verwenden

Dies ist genau das, wofür benannte Registrierungen sind.

Registrieren Sie sich wie folgt:

container.RegisterInstance<HttpClient>(new HttpClient(), "ClientA");
container.RegisterInstance<HttpClient>(new HttpClient(), "ClientB");

Und diesen Weg abrufen:

var clientA = container.Resolve<HttpClient>("ClientA");
var clientB = container.Resolve<HttpClient>("ClientB");

Wenn Sie möchten, dass ClientA oder ClientB automatisch in einen anderen registrierten Typ eingefügt werden, siehe diese Frage . Beispiel:

container.RegisterType<ControllerA, ControllerA>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientA") // Resolve parameter of type HttpClient using name "ClientA"
    )
);
container.RegisterType<ControllerB, ControllerB>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientB") // Resolve parameter of type HttpClient using name "ClientB"
    )
);

Verwenden Sie eine Fabrik

Wenn Ihr IoC-Container nicht in der Lage ist, benannte Registrierungen zu verarbeiten, können Sie eine Factory einspritzen und den Controller entscheiden lassen, wie er die Instanz erhält. Hier ist ein wirklich einfaches Beispiel:

class HttpClientFactory : IHttpClientFactory
{
    private readonly Dictionary<string, HttpClient> _clients;

    public void Register(string name, HttpClient client)
    {
        _clients[name] = client;
    }

    public HttpClient Resolve(string name)
    {
        return _clients[name];
    }
}

Und in deinen Controllern:

class ControllerA
{
    private readonly HttpClient _httpClient;

    public ControllerA(IHttpClientFactory factory)
    {
        _httpClient = factory.Resolve("ClientA");
    }
}

Und in deiner Kompositionswurzel:

var factory = new HttpClientFactory();
factory.Register("ClientA", new HttpClient());
factory.Register("ClientB", new HttpClient());
container.AddSingleton<IHttpClientFactory>(factory);
4
John Wu

Eigentlich sollte sich der Verbraucher des Dienstes nicht darum kümmern, wo die Implementierung der von ihm verwendeten Instanz ist. In Ihrem Fall sehe ich keinen Grund, viele verschiedene Instanzen von HttpClient manuell zu registrieren. Sie können den Typ einmal registrieren, und jede konsumierende Instanz, die eine Instanz benötigt, erhält ihre eigene Instanz von HttpClient. Das kann man mit AddTransient machen.

Die AddTransient-Methode wird verwendet, um abstrakte Typen konkreten Services zuzuordnen, die für jedes Objekt, für das dies erforderlich ist, separat instanziiert werden

services.AddTransient<HttpClient, HttpClient>();
0
Igor

Eine andere Option ist zu

  • verwenden Sie einen zusätzlichen generischen Typparameter für die Schnittstelle oder eine neue Schnittstelle, die die nicht generische Schnittstelle implementiert.
  • implementieren Sie eine Adapter-/Interceptor-Klasse, um den Markertyp hinzuzufügen
  • benutze den generischen Typ als "Name"

Ich habe einen Artikel mit weiteren Details geschrieben: Abhängigkeitsinjektion in .NET: Eine Möglichkeit, fehlende benannte Registrierungen zu umgehen

0
Rico Suter