web-dev-qa-db-fra.com

Meilleure façon d'utiliser HTTPClient dans ASP.Net Core en tant que DI Singleton

J'essaie de comprendre comment utiliser au mieux la classe HttpClient dans ASP.Net Core.

Selon la documentation et plusieurs articles, la classe est mieux instanciée une fois pour la durée de vie de l'application et partagée pour plusieurs requêtes. Malheureusement, je n’ai pas pu trouver d’exemple sur la manière de procéder correctement dans Core. Je propose donc la solution suivante. 

Mes besoins particuliers nécessitent l'utilisation de 2 points de terminaison différents (j'ai un serveur APIServer pour la logique métier et un serveur ImageServer piloté par une API). Je pense donc avoir deux singletons HttpClient que je peux utiliser dans l'application.

J'ai configuré mes points de service dans le fichier appsettings.json comme suit:

"ServicePoints": {
"APIServer": "http://localhost:5001",
"ImageServer": "http://localhost:5002",
}

Ensuite, j'ai créé un HttpClientsFactory qui instancie mes 2 clients http et les conserve dans un dictionnaire statique.

public class HttpClientsFactory : IHttpClientsFactory
{
    public static Dictionary<string, HttpClient> HttpClients { get; set; }
    private readonly ILogger _logger;
    private readonly IOptions<ServerOptions> _serverOptionsAccessor;

    public HttpClientsFactory(ILoggerFactory loggerFactory, IOptions<ServerOptions> serverOptionsAccessor) {
        _logger = loggerFactory.CreateLogger<HttpClientsFactory>();
        _serverOptionsAccessor = serverOptionsAccessor;
        HttpClients = new Dictionary<string, HttpClient>();
        Initialize();
    }

    private void Initialize()
    {
        HttpClient client = new HttpClient();
        // ADD imageServer
        var imageServer = _serverOptionsAccessor.Value.ImageServer;
        client.BaseAddress = new Uri(imageServer);
        HttpClients.Add("imageServer", client);

        // ADD apiServer
        var apiServer = _serverOptionsAccessor.Value.APIServer;
        client.BaseAddress = new Uri(apiServer);
        HttpClients.Add("apiServer", client);
    }

    public Dictionary<string, HttpClient> Clients()
    {
        return HttpClients;
    }

    public HttpClient Client(string key)
    {
        return Clients()[key];
    }
  } 

Ensuite, j'ai créé l'interface que je peux utiliser pour définir ma DI ultérieurement. Notez que la classe HttpClientsFactory hérite de cette interface.

public interface IHttpClientsFactory
{
    Dictionary<string, HttpClient> Clients();
    HttpClient Client(string key);
}

Je suis maintenant prêt à injecter cela dans mon conteneur Dependency comme suit dans la classe Startup de la méthode ConfigureServices.

// Add httpClient service
        services.AddSingleton<IHttpClientsFactory, HttpClientsFactory>();

Tout est maintenant configuré pour commencer à utiliser ceci dans mon contrôleur.
Premièrement, je prends la dépendance. Pour ce faire, j'ai créé une propriété de classe privée pour la conserver, puis je l'ajoute à la signature du constructeur et je termine en affectant l'objet entrant à la propriété de classe locale.

private IHttpClientsFactory _httpClientsFactory;
public AppUsersAdminController(IHttpClientsFactory httpClientsFactory)
{
   _httpClientsFactory = httpClientsFactory;
}

Enfin, nous pouvons maintenant utiliser la fabrique pour demander un client htpp et exécuter un appel. Ci-dessous, un exemple où je demande une image du serveur d'images à l'aide de httpclientsfactory:

[HttpGet]
    public async Task<ActionResult> GetUserPicture(string imgName)
    {
        // get imageserver uri
        var imageServer = _optionsAccessor.Value.ImageServer;

        // create path to requested image
        var path = imageServer + "/imageuploads/" + imgName;

        var client = _httpClientsFactory.Client("imageServer");
        byte[] image = await client.GetByteArrayAsync(path);

        return base.File(image, "image/jpeg");
    }

Terminé!

J'ai testé cela et cela fonctionne très bien dans mon environnement de développement. Cependant, je ne suis pas sûr que ce soit le meilleur moyen de mettre cela en œuvre. Je reste avec les questions suivantes:

  1. Ce fil de solution est-il sûr? (Selon la documentation MS: "Tous les membres statiques publics (Shared en Visual Basic) de ce type sont thread-safe.")
  2. Cette configuration sera-t-elle capable de supporter une charge lourde sans ouvrir plusieurs connexions séparées?
  3. Que faire dans le noyau ASP.Net pour traiter le problème DNS décrit dans ‘Singleton HttpClient? Méfiez-vous de ce comportement sérieux et de la façon de le réparer. ”Situé à http://byterot.blogspot.be/2016/07/singleton-httpclient-dns.html
  4. D'autres améliorations ou suggestions?
9
Laobu

En réponse à une question de @MuqeetKhan concernant l'utilisation de l'authentification avec la demande httpclient.

Premièrement, ma motivation à utiliser DI et une usine était de me permettre d’étendre facilement mon application à différentes API et de l’avoir facilement dans tout mon code. C’est un modèle que j’espère pouvoir réutiliser plusieurs fois. 

Dans le cas de mon contrôleur ‘GetUserPicture’ décrit dans la question initiale ci-dessus, j’ai effectivement supprimé l’authentification pour des raisons de simplicité. Honnêtement cependant, je doute toujours d’en avoir besoin pour récupérer simplement une image du serveur d’images. Quoi qu'il en soit, dans d'autres contrôleurs, j'en ai vraiment besoin, alors…

J'ai implémenté Identityserver4 en tant que serveur d'authentification. Cela me fournit l'authentification au-dessus de ASP Identity. Pour l’autorisation (en utilisant des rôles dans ce cas), j’ai implémenté IClaimsTransformer dans mes projets MVC 'et' API (pour en savoir plus à ce sujet, cliquez sur Comment insérer des rôles d’identité ASP.net dans le jeton d’identité Identityserver4 ). 

À présent, dès que j'entre dans mon contrôleur, j'ai un utilisateur authentifié et autorisé pour lequel je peux récupérer un jeton d'accès. J'utilise ce jeton pour appeler mon API, qui appelle bien sûr la même instance de identityserver pour vérifier si l'utilisateur est authentifié. 

La dernière étape consiste à permettre à mon API de vérifier si l'utilisateur est autorisé à appeler le contrôleur api demandé. Comme expliqué précédemment, dans le pipeline de demandes de l'API utilisant IClaimsTransformer, je récupère l'autorisation de l'utilisateur appelant et l'ajoute aux revendications entrantes. Notez que dans le cas d’un appel MVC et d’une API, je récupère donc l’autorisation 2 fois; une fois dans le pipeline de demandes MVC et une fois dans le pipeline de demandes d'API. 

En utilisant cette configuration, je peux utiliser mon HttpClientsFactory avec autorisation et authentification. 

Le gros problème de sécurité qui me manque est HTTPS bien sûr. J'espère que je peux en quelque sorte l'ajouter à mon usine. Je vais le mettre à jour une fois que je l'ai mis en œuvre. 

Comme toujours, toutes les suggestions sont les bienvenues.

Ci-dessous, un exemple où je télécharge une image sur le serveur d'images à l'aide de l'authentification (l'utilisateur doit être connecté et avoir le rôle admin). 

Mon contrôleur MVC appelle «UploadUserPicture»:

    [Authorize(Roles = "Admin")]
    [HttpPost]
    public async Task<ActionResult> UploadUserPicture()
    {
        // collect name image server
        var imageServer = _optionsAccessor.Value.ImageServer;

        // collect image in Request Form from Slim Image Cropper plugin
        var json = _httpContextAccessor.HttpContext.Request.Form["slim[]"];

        // Collect access token to be able to call API
        var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");

        // prepare api call to update image on imageserver and update database
        var client = _httpClientsFactory.Client("imageServer");
        client.DefaultRequestHeaders.Accept.Clear();
        client.SetBearerToken(accessToken);
        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("image", json[0])
        });
        HttpResponseMessage response = await client.PostAsync("api/UserPicture/UploadUserPicture", content);

        if (response.StatusCode != HttpStatusCode.OK)
        {
            return StatusCode((int)HttpStatusCode.InternalServerError);
        }
        return StatusCode((int)HttpStatusCode.OK);
    }

API gérant le téléchargement de l'utilisateur

    [Authorize(Roles = "Admin")]
    [HttpPost]
    public ActionResult UploadUserPicture(String image)
    {
     dynamic jsonDe = JsonConvert.DeserializeObject(image);

        if (jsonDe == null)
        {
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }

        // create filname for user picture
        string userId = jsonDe.meta.userid;
        string userHash = Hashing.GetHashString(userId);
        string fileName = "User" + userHash + ".jpg";

        // create a new version number
        string pictureVersion = DateTime.Now.ToString("yyyyMMddHHmmss");

        // get the image bytes and create a memory stream
        var imagebase64 = jsonDe.output.image;
        var cleanBase64 = Regex.Replace(imagebase64.ToString(), @"^data:image/\w+;base64,", "");
        var bytes = Convert.FromBase64String(cleanBase64);
        var memoryStream = new MemoryStream(bytes);

        // save the image to the folder
        var fileSavePath = Path.Combine(_env.WebRootPath + ("/imageuploads"), fileName);
        FileStream file = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write);
        try
        {
            memoryStream.WriteTo(file);
        }
        catch (Exception ex)
        {
            _logger.LogDebug(LoggingEvents.UPDATE_ITEM, ex, "Could not write file >{fileSavePath}< to server", fileSavePath);
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }
        memoryStream.Dispose();
        file.Dispose();
        memoryStream = null;
        file = null;

        // update database with latest filename and version
        bool isUpdatedInDatabase = UpdateDatabaseUserPicture(userId, fileName, pictureVersion).Result;

        if (!isUpdatedInDatabase)
        {
            return new StatusCodeResult((int)HttpStatusCode.NotModified);
        }

        return new StatusCodeResult((int)HttpStatusCode.OK);
    }
1
Laobu

Si vous utilisez .net core 2.1 ou une version ultérieure, la meilleure approche consiste à utiliser la nouvelle variable HttpClientFactory. Je suppose que Microsoft a compris tous les problèmes que les gens rencontraient et a donc travaillé dur pour nous. Voir ci-dessous pour savoir comment le configurer.

REMARQUE: ajoutez une référence à Microsoft.Extensions.Http.

1 - Ajouter une classe qui utilise HttpClient

public interface ISomeApiClient
{
    Task<HttpResponseMessage> GetSomethingAsync(string query);
}

public class SomeApiClient : ISomeApiClient
{
    private readonly HttpClient _client;

    public SomeApiClient (HttpClient client)
    {
        _client = client;
    }

    public async Task<SomeModel> GetSomethingAsync(string query)
    {
        var response = await _client.GetAsync($"?querystring={query}");
        if (response.IsSuccessStatusCode)
        {
            var model = await response.Content.ReadAsJsonAsync<SomeModel>();
            return model;
        }
        // Handle Error
    }
}

2 - Enregistrez vos clients dans ConfigureServices(IServiceCollection services) dans Startup.cs

var someApiSettings = Configuration.GetSection("SomeApiSettings").Get<SomeApiSettings>(); //Settings stored in app.config (base url, api key to add to header for all requests)
services.AddHttpClient<ISomeApiClient, SomeApiClient>("SomeApi",
                client =>
                {
                    client.BaseAddress = new Uri(someApiSettings.BaseAddress);
                    client.DefaultRequestHeaders.Add("api-key", someApiSettings.ApiKey);
                });

3 - Utilisez le client dans votre code

public class MyController
{
    private readonly ISomeApiClient _client;

    public MyController(ISomeApiClient client)
    {
        _client = client;
    }

    [HttpGet]
    public async Task<IActionResult> GetAsync(string query)
    {
        var response = await _client.GetSomethingAsync(query);

        // Do something with response

        return Ok();
    }
}

Vous pouvez ajouter autant de clients et en enregistrer autant que nécessaire dans votre démarrage avec services.AddHttpClient.

Merci à Steve Gordon et son article ici de m'avoir aidé à l'utiliser dans mon code!

1
garethb