wake-up-neo.com

Injizieren von Single Instance HttpClient mit einem bestimmten HttpMessageHandler

Als Teil eines ASP.Net Core-Projekts, an dem ich arbeite, muss ich über mein WebApi mit einer Reihe verschiedener restbasierter API-Endpunkte kommunizieren. Um dies zu erreichen, verwende ich eine Reihe von Serviceklassen, die jeweils ein statisches HttpClient instanziieren. Grundsätzlich habe ich für jeden Rest-basierten Endpunkt, mit dem das WebApi eine Verbindung herstellt, eine Serviceklasse.

Ein Beispiel, wie das statische HttpClient in jeder der Serviceklassen instanziiert wird, ist unten zu sehen.

private static HttpClient _client = new HttpClient()
{
    BaseAddress = new Uri("http://endpointurlexample"),            
};

Das oben Genannte funktioniert zwar gut, ermöglicht jedoch keinen effektiven Unit-Test der Serviceklassen, die HttpClient verwenden. Damit ich Unit-Tests durchführen kann, habe ich ein falsches HttpMessageHandler, das ich für das HttpClient in meinen Unit-Tests verwenden möchte, während das HttpClient wie oben jedoch instanziiert wird Ich kann die Fälschung HttpMessageHandler im Rahmen meiner Komponententests nicht anwenden.

Wie kann HttpClient in den Serviceklassen in der gesamten Anwendung am besten eine einzige Instanz bleiben (eine Instanz pro Endpunkt), während der Komponententests kann jedoch eine andere HttpMessageHandler angewendet werden? ?

Ein Ansatz, an den ich gedacht habe, wäre, kein statisches Feld zu verwenden, um das HttpClient in den Serviceklassen zu halten, sondern es über die Konstruktorinjektion unter Verwendung eines Singleton-Lebenszyklus zu injizieren, wodurch ich ein angeben könnte HttpClient mit dem gewünschten HttpMessageHandler während Unit-Tests, die andere Option, die ich mir vorgestellt habe, wäre die Verwendung einer HttpClient Factory-Klasse, die die HttpClients in statischen Feldern instanziiert Dies könnte dann durch Injizieren der Factory HttpClient in die Serviceklassen erreicht werden, wodurch wiederum eine andere Implementierung mit dem entsprechenden HttpMessageHandler in Komponententests zurückgegeben werden kann. Keiner der oben genannten fühlt sich jedoch besonders sauber an und es scheint, als gäbe es einen besseren Weg?

Fragen, lassen Sie es mich wissen.

15
Chris Lawrence

Das Hinzufügen von Kommentaren zu der Konversation sieht so aus, als würden Sie eine HttpClient -Factory benötigen

public interface IHttpClientFactory {
    HttpClient Create(string endpoint);
}

und die Implementierung der Kernfunktionalität könnte ungefähr so ​​aussehen.

static IDictionary<string, HttpClient> cache = new Dictionary<string, HttpClient>();

public HttpClient Create(string endpoint) {
    HttpClient client = null;
    if(cache.TryGetValue(endpoint, out client)) {
        return client;
    }

    client = new HttpClient {
        BaseAddress = new Uri(endpoint),
    };
    cache[endpoint] = client;

    return client;
}

Das heißt, wenn Sie mit dem obigen Design nicht besonders zufrieden sind. Sie können die HttpClient - Abhängigkeit hinter einem Service abstrahieren, damit der Client nicht zum Implementierungsdetail wird.

Die Verbraucher des Dienstes müssen nicht genau wissen, wie die Daten abgerufen werden.

18
Nkosi

Du denkst zu kompliziert. Alles, was Sie benötigen, ist eine HttpClient-Factory oder ein Accessor mit einer HttpClient -Eigenschaft, und verwenden Sie diese so, wie der ASP.NET Core das Injizieren von HttpContext ermöglicht

public interface IHttpClientAccessor 
{
    HttpClient Client { get; }
}

public class DefaultHttpClientAccessor : IHttpClientAccessor
{
    public HttpClient Client { get; }

    public DefaultHttpClientAccessor()
    {
        Client = new HttpClient();
    }
}

und injizieren Sie dies in Ihre Dienste

public class MyRestClient : IRestClient
{
    private readonly HttpClient client;

    public MyRestClient(IHttpClientAccessor httpClientAccessor)
    {
        client = httpClientAccessor.Client;
    }
}

registrierung in Startup.cs:

services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();

Machen Sie sich zum Testen von Einheiten einfach lustig

// Moq-esque

// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
var httpHandler = new HttpMessageHandler(..) { ... };
var httpContext = new HttpContext(httpHandler);

httpClientAccessor.SetupGet(a => a.Client).Returns(httpContext);

// Act
var restClient = new MyRestClient(httpClientAccessor.Object);
var result = await restClient.GetSomethingAsync(...);

// Assert
...
12
Tseng

Meine derzeitige Präferenz ist, einmal pro Zielendpunktdomäne von HttpClient abzuleiten und es mit Abhängigkeitsinjektion zu einem Singleton zu machen, anstatt HttpClient direkt zu verwenden.

Angenommen, ich mache HTTP-Anfragen an example.com. Ich hätte ein ExampleHttpClient, das von HttpClient erbt und die gleiche Konstruktorsignatur wie HttpClient hat, mit der Sie das übergeben und verspotten können HttpMessageHandler wie gewohnt.

public class ExampleHttpClient : HttpClient
{
   public ExampleHttpClient(HttpMessageHandler handler) : base(handler) 
   {
       BaseAddress = new Uri("http://example.com");

       // set default headers here: content type, authentication, etc   
   }
}

Ich setze dann ExampleHttpClient als Singleton in meiner Abhängigkeitsinjektionsregistrierung und füge eine Registrierung für HttpMessageHandler hinzu, die so vorübergehend ist, wie sie nur einmal pro http-Client-Typ erstellt wird. Unter Verwendung dieses Musters muss ich nicht mehrere komplizierte Registrierungen für HttpClient oder intelligente Fabriken haben, um sie basierend auf dem Zielhostnamen zu erstellen.

Alles, was mit example.com gesprochen werden muss, sollte eine Konstruktorabhängigkeit von ExampleHttpClient haben, und dann verwenden alle dieselbe Instanz, und Sie erhalten das geplante Verbindungspooling.

Auf diese Weise können Sie auch Dinge wie Standardheader, Inhaltstypen, Autorisierung, Basisadresse usw. besser platzieren und verhindern, dass http config für einen Service an einen anderen Service verliert.

3
alastairtree

Ich komme vielleicht zu spät zur Party, aber ich habe ein Helper-Nuget-Paket erstellt, mit dem Sie HttpClient-Endpunkte in Komponententests testen können.

NuGet: install-package WorldDomination.HttpClient.Helpers
Repo: https://github.com/PureKrome/HttpClient.Helpers

Die Grundidee ist, dass Sie die gefälschte Antwortnutzlast erstellt und die Instanz a FakeHttpMessageHandler an Ihren Code weitergibt, der diese gefälschte Antwortnutzlast enthält. Wenn Ihr Code dann versucht, diesen URI-Endpunkt tatsächlich zu treffen, tut er dies nicht ... und gibt stattdessen nur die gefälschte Antwort zurück. MAGIE!

und hier ist ein wirklich einfaches Beispiel:

[Fact]
public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo()
{
    // Arrange.

    // Fake response.
    const string responseData = "{ \"Id\":69, \"Name\":\"Jane\" }";
    var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData);

    // Prepare our 'options' with all of the above fake stuff.
    var options = new HttpMessageOptions
    {
        RequestUri = MyService.GetFooEndPoint,
        HttpResponseMessage = messageResponse
    };

    // 3. Use the fake response if that url is attempted.
    var messageHandler = new FakeHttpMessageHandler(options);

    var myService = new MyService(messageHandler);

    // Act.
    // NOTE: network traffic will not leave your computer because you've faked the response, above.
    var result = await myService.GetSomeFooDataAsync();

    // Assert.
    result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync.
    result.Baa.ShouldBeNull();
    options.NumberOfTimesCalled.ShouldBe(1);
}
1
Pure.Krome