web-dev-qa-db-fra.com

Injection d'une instance unique HttpClient avec un HttpMessageHandler spécifique

Dans le cadre d'un projet ASP.Net Core sur lequel je travaille, j'ai besoin de communiquer avec un certain nombre de points de terminaison d'API basés sur Rest à partir de mon WebApi. Pour ce faire, j'utilise un certain nombre de classes de service qui instancient chacune un HttpClient statique. Essentiellement, j'ai une classe de service pour chacun des points de terminaison reposés auxquels le WebApi se connecte.

Un exemple de la façon dont le HttpClient statique est instancié dans chacune des classes de service peut être vu ci-dessous.

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

Bien que ce qui précède fonctionne bien, il ne permet pas de tests unitaires efficaces des classes de service qui utilisent HttpClient. Pour me permettre d'effectuer des tests unitaires, j'ai un faux HttpMessageHandler que j'aimerais utiliser pour le HttpClient dans mes tests unitaires, tandis que le HttpClient est instancié comme ci-dessus cependant Je ne peux pas appliquer le faux HttpMessageHandler dans le cadre de mes tests unitaires.

Quelle est la meilleure façon pour que le HttpClient dans les classes de service reste une seule instance dans toute l'application (une instance par point de terminaison), mais pour permettre l'application d'un HttpMessageHandler différent pendant les tests unitaires ?

Une approche à laquelle j'ai pensé serait de ne pas utiliser un champ statique pour contenir le HttpClient dans les classes de service, plutôt de lui permettre d'être injecté via l'injection de constructeur en utilisant un cycle de vie singleton, ce qui me permettrait de spécifier un HttpClient avec le HttpMessageHandler souhaité pendant les tests unitaires, l'autre option à laquelle j'ai pensé serait d'utiliser une classe d'usine HttpClient qui instancie les HttpClients dans les champs statiques qui pourrait ensuite être récupéré en injectant la fabrique HttpClient dans les classes de service, permettant à nouveau une implémentation différente avec le HttpMessageHandler pertinent à renvoyer dans des tests unitaires. Cependant, rien de ce qui précède ne semble particulièrement propre et il semble qu'il doit y avoir un meilleur moyen?

Toutes les questions, faites le moi savoir.

15
Chris Lawrence

L'ajout à la conversation à partir des commentaires semble avoir besoin d'une usine HttpClient

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

et la mise en œuvre de la fonctionnalité de base pourrait ressembler à ceci.

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;
}

Cela dit, si vous n'êtes pas particulièrement satisfait du design ci-dessus. Vous pouvez supprimer la dépendance HttpClient derrière un service afin que le client ne devienne pas un détail d'implémentation.

Les consommateurs du service n'ont pas besoin de savoir exactement comment les données sont récupérées.

18
Nkosi

Vous pensez trop compliqué. Tout ce dont vous avez besoin est une fabrique ou un accesseur HttpClient avec une propriété HttpClient et utilisez-le de la même manière qu'ASP.NET Core permet à HttpContext d'être injecté

public interface IHttpClientAccessor 
{
    HttpClient Client { get; }
}

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

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

et l'injecter dans vos services

public class MyRestClient : IRestClient
{
    private readonly HttpClient client;

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

inscription dans Startup.cs:

services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();

Pour les tests unitaires, il suffit de se moquer

// 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

Ma préférence actuelle est de dériver de HttpClient une fois par domaine de noeud final cible et d'en faire un singleton en utilisant l'injection de dépendances plutôt que d'utiliser HttpClient directement.

Disons que je fais des requêtes HTTP à example.com, j'aurais un ExampleHttpClient qui hérite de HttpClient et qui a la même signature de constructeur que HttpClient vous permettant de passer et de vous moquer du HttpMessageHandler comme d'habitude.

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

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

J'ai ensuite défini ExampleHttpClient comme singleton dans mon enregistrement d'injection de dépendance et j'ajoute un enregistrement pour HttpMessageHandler comme transitoire car il ne sera créé qu'une seule fois par type de client http. En utilisant ce modèle, je n'ai pas besoin d'avoir plusieurs enregistrements compliqués pour HttpClient ou des usines intelligentes pour les construire en fonction du nom d'hôte de destination.

Tout ce qui doit parler à example.com devrait prendre une dépendance de constructeur sur ExampleHttpClient, puis ils partagent tous la même instance et vous obtenez le pool de connexions comme prévu.

De cette façon, vous disposez également d'un meilleur emplacement pour placer des éléments tels que les en-têtes par défaut, les types de contenu, l'autorisation, l'adresse de base, etc., et aide à empêcher la configuration http pour un service de fuir vers un autre service.

3
alastairtree

Je suis peut-être en retard à la fête, mais j'ai créé un package de nuget Helper qui vous permet de tester les points de terminaison HttpClient dans des tests unitaires.

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

L'idée de base est que vous créez la charge utile de réponse fausse et transmettez cette instance a FakeHttpMessageHandler à votre code, qui inclut cette fausse réponse charge utile. Ensuite, lorsque votre code essaie réellement de frapper ce point de terminaison URI, il ne le fait pas ... et renvoie simplement la fausse réponse à la place. LA MAGIE!

et voici un exemple très simple:

[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