115 messaggi dal 26 febbraio 2007
Ciao,

Ho letto un interessante articolo su come NON utilizzate HttpClient, e visto che la mia applicazione si basa totalmente su chiamate con HttpClient, devo per forza ottimizzare questa parte.


Il mio problema è questo:

Ho una lista dinamica di EndPoint che saranno chiamati con HttpClient, Questa lista può avere autenticazione anonima oppure con http basic. In più è possibile settare un Proxy (anche diverso per ogni Item EndPoint configurato. Ho pensato di creare nello startup un client Http per ogni Item EndPoint, però questi EndPoint potrebbero essere modificati in qualsiasi momento oppure aggiungi\rimossi. E'possibile modifcare a runtime i miei client (al massimo potrei anche cancellare e ricreare l'endpoint se non è possibile modificarlo)


public void createHttpClients(IServiceCollection services)
        {
            services.AddHttpClient("MyServiceProxy1").ConfigurePrimaryHttpMessageHandler(() =>
            {
                return new HttpClientHandler
                {
                    Proxy = new WebProxy("http://127.0.0.1:8888"),
                    UseProxy = true
                };
            });

            services.AddHttpClient("MyServiceProxy2").ConfigurePrimaryHttpMessageHandler(() =>
            {
                return new HttpClientHandler
                {
                    Proxy = new WebProxy("http://127.0.0.1:8889"),
                    UseProxy = true
                };
            });
        }


In realtà alla fine si tratterebbe di creare 1 Client HTTP senza Proxy, e poi N client con proxy quanti sono i diversi proxy inseriti nel file di configurazione. (più che N Client tanti quanti sono gli EndPoint configurati).
Modificato da Federico.C il 02 maggio 2019 15:24 -
11.724 messaggi dal 09 febbraio 2002
Contributi
Ciao Federico,
mi sa che non è possibile modificare l'elenco dei servizi a runtime, soprattutto con il container minimale che ASP.NET Core mette a disposizione.
Potrebbe essere possibile con un container di terze parti, tipo Autofac, StructureMap o altri. Bisognerebbe provare.


Ho letto un interessante articolo su come NON utilizzate HttpClient,

Va bé, non succede niente se fai un new HttpClient. Per prima cosa cerca di farlo funzionare, poi si troverà il modo di ottimizzarlo. L'importante è che non ti blocchi a causa di ottimizzazioni premature.
Comunque, posta il link all'articolo, vediamo che dicono.

Il mio suggerimento in questo momento è quello di realizzare un servizio-factory che contenga un metodo tipo CreateClients() che ti restituisce indietro un elenco di HttpClient coerente con la configurazione. Esempio:

public interface IClientFactory
{
  public IEnumerable<HttpClient> CreateClients();
}


La configurazione, se ce l'hai in un file json (es. appsettings.json), è aggiornabile a caldo quindi non sarà necessario riavviare l'applicazione.
L'importante è che nell'implementazione concreta di IClientFactory tu riceva un IOptionsMonitor<ClasseOpzioni> che ti darà sempre i valori corretti della configurazione. Esempio:

public class MyClientFactory : IClientFactory
{
   private readonly IOptionsMonitor<ClasseOpzioni> opzioni;
   public MyClientFactory(IOptionsMonitor<ClasseOpzioni> opzioni)
   {
     this.opzioni = opzioni;
   }
   public IEnumerable<HttpClient> CreateClients()
   {
     foreach (var opzione in opzioni.CurrentValue) 
     {
       using (var handler new HttpClientHandler(...))
       {
         using (var client = new HttpClient(handler))
         {
           yield return client;
         }
       }
     }
   }
}


Delle info su come usare il servizio IOptionsMonitor le trovi qui:
https://forum.aspitalia.com/forum/post/420254/Accedere-AppSettings-Classe.aspx

ciao,
Moreno

Enjoy learning and just keep making
115 messaggi dal 26 febbraio 2007
https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests


Considera che la mia applicazione è una specie di HUB che riceve le singole chiamate REST dal client e per ogni singola richiesta utilizza 3 HttpClient che vengono sempre utilizzate su 3 EndPoint diversi, a seconda della configurazione passata. Quindi fa un massiccio uso delle HttpClient perchè di per se la mia app non fa altro che riceve richieste dal client e smistarle ad alcuni server che conosce solo la mia APP (e che può accederci solo lei).
115 messaggi dal 26 febbraio 2007
Credo di aver trovato un buon compromesso

Creo questo per tutte le chiamate senza proxy
services.AddHttpClient("DefaultClient").ConfigurePrimaryHttpMessageHandler(() =>
            {
                return new HttpClientHandler
                {
                    UseDefaultCredentials = true,
                    Proxy = WebRequest.DefaultWebProxy
                };
            });


Le configurazioni che richiedono proxy (e saranno sempre lette con la Option di cui mi dicevi sopra) saranno settate in _httpClientHandler (NULL nel caso in cui non sia necessario un proxy).
if (_httpClientHandler != null)
            {
                newHttpClient = new HttpClient(handler: _httpClientHandler, disposeHandler: true);
            }
            else
            {
                newHttpClient = httpClientFactory.CreateClient("DefaultClient");
            }


Non copre tutti i casi, ma almeno le chiamate senza proxy sono ottimizzate (che alla fine saranno nel 99% dei casi)
11.724 messaggi dal 09 febbraio 2002
Contributi
Quindi fa un massiccio uso delle HttpClient

Ok, allora merita un po' più attenzione.

Ho preparato un progetto di esempio da cui puoi partire:
https://github.com/BrightSoul/option-dependent-httpclient-factory

Questo progetto non usa IHttpClientFactory ma comunque crea una istanza di HttpClient per endpoint e la tiene in cache.
Le istanze così generate vengono rimosse dalla cache quando modifichi la configurazione nel file appsettings.json. Così, la prossima volta che dovrai inviare una richiesta, verrà ricreata una nuova istanza di HttpClient in accordo con la nuova configurazione.

Io non conosco tutti i dettagli del tuo progetto ma vedi se ci puoi trarre qualcosa.
Per prima cosa, ho presupposto che la tua configurazione fosse come la seguente. Quindi, in appsettings.json troverai:
{
  "Endpoints": [
    {
      "name": "Richieste a foo.it",
      "selector": "//foo.it",
      "proxy": "http://127.0.0.1:8888"
    },
    {
      "name": "Richieste a bar.com",
      "selector": "//bar1.com",
      "proxy": "http://127.0.0.1:8889"
    },
    {
      "name": "Tutte le altre richieste",
      "selector": "",
      "proxy": null
    }
  ]
}

Come vedi, ogni endpoint ha:
  • Un nome, usato solo ai fini di logging
  • Un selettore, che serve a determinare se la richiesta deve essere gestita da quell'endpoint. Banalmente viene verificato se il selettore è contenuto nell'URL della richiesta. Tu qui dovrai mettere la tua logica di selezione;
  • Un proxy (se necessario, altrimenti null);

Eventualmente ad ogni endpoint potrai aggiungere anche le informazioni che li differenziano, tipo le default header.

Ho creato un servizio IRequestSender fatto così:
public interface IRequestSender
{
   Task<HttpResponseMessage> SendRequest(HttpRequestMessage request);
}

In questo modo, chi usa questa classe resta completamente isolato da tutte le menate che riguardano l'HttpClient. Semplicemente, passa la sua istanza di HttpRequestMessage e il servizio pensa ad ottenere l'istanza e ad inviare la richiesta.
Così non c'è neanche pericolo che il codice che utilizza questo servizio vada a fare il Dispose dell'HttpClient o che tocchi una delle sue proprietà, col rischio di introdurre problemi legati alla thread-safety.

L'implementazione concreta di questo servizio la trovi qui:
https://github.com/BrightSoul/option-dependent-httpclient-factory/blob/master/Models/Services/Infrastructure/RequestSender.cs

Quando avvii l'applicazione, ti troverai una casella per inserire l'url della richiesta da inviare. Nell'output console dell'applicazione, vedrai che l'istanza di HttpClient viene creata solo la prima volta che invii una richiesta a quell'url.
RequestSender:Information: Created a new instance of HttpClient for endpoint 'Richieste a foo.it'

Dopodiché la stessa istanza viene riottenuta dalla cache evitando il problema dell'accumularsi di connessioni socket.

A questo punto, se vai a toccare la configurazione in appsettings.json e risalvi il file, vedrai che le istanze presenti in cache vengono rimosse proattivamente. Ancora una volta, ne trovi evidenza nell'output in console.
Disposed HttpClient for key 'endpoint-//foo.it' after eviction for reason: 'TokenExpired'


Se il progetto ti è di qualche utilità, mi raccomando non copiare-incollare il codice ma cerca di capire cosa fa. In caso di dubbi chiedi. E' una roba scritta in un'ora e sicuramente andranno fatte modifiche prima che sia pronto per la produzione.

ciao,
Moreno
Modificato da BrightSoul il 03 maggio 2019 20:50 -

Enjoy learning and just keep making
115 messaggi dal 26 febbraio 2007
Ciao,

Grazie mille, l'esempio si adatta molto bene al mio caso.

Ho fatto le dovute modifiche per supportare il mio caso d'uso e lo sto testando in questo momento.

Sembra funzionare perfettamente...


Grazie
115 messaggi dal 26 febbraio 2007
Mi è venuto solo un dubbio, nel caso di HttpClient in Cache, che cosa succede se due utenti mandano la stessa richiesta in contemporanea?

Devo gestire la concorrenza?


EDIT: ad occhio mi sembra che non ci sia problema, perchè con Fiddler ho lanciato 500 richieste nel giro di qualche secondo ed è andato tutto correttamente. (si nota chiaramente l'aumento di performance rispetto a quanto veniva creata una HttpClient per ogni singola richiesta)
Modificato da Federico.C il 06 maggio 2019 14:26 -
11.724 messaggi dal 09 febbraio 2002
Contributi
Ciao Federico,


Devo gestire la concorrenza?

No, non c'è bisogno.

Puoi leggere questo:
https://medium.com/@nuno.caneco/c-httpclient-should-not-be-disposed-or-should-it-45d2a8f568bc


Michael Taylor did some investigation and concluded that the HttpClient is NOT thread safe. Essentially because of the BaseAddress and DefaultRequestHeaders properties.

Quindi il problema ci sarebbe solo se da due richieste concorrenti si andassero a modificare le proprietà DefaultRequestHeaders e/o BaseAddress. Ma nel progetto di esempio questo non si verifica.

Le altre cose a cui fare attenzione sono:

Ensure the creation and initialization of the client is done on a single thread, preferably through a factory.

Sì, è proprio questo il caso. L'istanza dell'HttpClient viene appunto creata da una factory (la lambda passata al metodo GetOrCreate) e in maniera thread-safe. Ecco qui:
https://github.com/BrightSoul/option-dependent-httpclient-factory/blob/master/Models/Services/Infrastructure/RequestSender.cs#L41


Ensure that no code outside the initialization tries to modify any of the properties.

Le proprietà delle istanze di HttpClient NON vengono modificate. Infatti, il servizio RequestSender NON espone direttamente le istanze di HttpClient proprio per evitare che ciò si verifichi.


Create a new client for each unique base address and request header combinations.

Come dicevo in precedenza, DefaultRequestHeaders e BaseAddress non vengono modificate.

Inoltre, a ulteriore garanzia, vedi nella documentazione che c'è l'elenco dei metodi di HttpClient che sono thread-safe.
https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?redirectedfrom=MSDN&view=netcore-2.2#Anchor_5
Il SendAsync, che è quello che viene usato qui dal progetto demo, è tra quelli.

ciao,
Moreno
Modificato da BrightSoul il 07 maggio 2019 10:17 -

Enjoy learning and just keep making

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.