Вибір між HttpClient та WebClient


218

Наш веб-додаток працює у .Net Framework 4.0. Користувальницький інтерфейс викликає методи контролера через дзвінки ajax.

Нам потрібно споживати послугу REST від нашого постачальника. Я оцінюю найкращий спосіб зателефонувати в службу REST в. Net 4.0. Службі REST потрібна основна схема аутентифікації, і вона може повертати дані як у XML, так і в JSON. Немає вимоги до завантаження / завантаження величезних даних, і я нічого не бачу в майбутньому. Я переглянув кілька проектів з відкритим кодом для споживання REST і не знайшов у них жодної цінності, яка б виправдовувала додаткову залежність у проекті. Почав оцінювати WebClientі HttpClient. Я завантажив HttpClient для .Net 4.0 з NuGet.

Я шукав відмінності між WebClientі, HttpClientі цей сайт зазначив, що один HttpClient може обробляти одночасні дзвінки, і він може повторно використовувати вирішені DNS, конфігурацію файлів cookie та автентифікацію. Я ще не бачу практичних цінностей, які ми можемо отримати через розбіжності.

Я зробив швидкий тест на ефективність, щоб виявити, як WebClient(синхронізувати дзвінки), HttpClient(синхронізація та асинхронізація). і ось результати:

Використання одного і того ж HttpClientекземпляра для всіх запитів (min - max)

Синхронізація WebClient: 8 мс - 167 мс
Синхронізація HttpClient: 3 мс - 7228 мс
Асинхронізація HttpClient: 985 - 10405 мс

Використання нового HttpClientдля кожного запиту (min - max)

Синхронізація WebClient: 4 мс - 297 мс
Синхронізація HttpClient: 3 мс - 7953 мс
Асинхронізація HttpClient: 1027 - 10834 мс

Код

public class AHNData
{
    public int i;
    public string str;
}

public class Program
{
    public static HttpClient httpClient = new HttpClient();
    private static readonly string _url = "http://localhost:9000/api/values/";

    public static void Main(string[] args)
    {
       #region "Trace"
       Trace.Listeners.Clear();

       TextWriterTraceListener twtl = new TextWriterTraceListener(
           "C:\\Temp\\REST_Test.txt");
       twtl.Name = "TextLogger";
       twtl.TraceOutputOptions = TraceOptions.ThreadId | TraceOptions.DateTime;

       ConsoleTraceListener ctl = new ConsoleTraceListener(false);
       ctl.TraceOutputOptions = TraceOptions.DateTime;

       Trace.Listeners.Add(twtl);
       Trace.Listeners.Add(ctl);
       Trace.AutoFlush = true;
       #endregion

       int batchSize = 1000;

       ParallelOptions parallelOptions = new ParallelOptions();
       parallelOptions.MaxDegreeOfParallelism = batchSize;

       ServicePointManager.DefaultConnectionLimit = 1000000;

       Parallel.For(0, batchSize, parallelOptions,
           j =>
           {
               Stopwatch sw1 = Stopwatch.StartNew();
               GetDataFromHttpClientAsync<List<AHNData>>(sw1);
           });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                Stopwatch sw1 = Stopwatch.StartNew();
                GetDataFromHttpClientSync<List<AHNData>>(sw1);
            });
       Parallel.For(0, batchSize, parallelOptions,
            j =>
            {
                using (WebClient client = new WebClient())
                {
                   Stopwatch sw = Stopwatch.StartNew();
                   byte[] arr = client.DownloadData(_url);
                   sw.Stop();

                   Trace.WriteLine("WebClient Sync " + sw.ElapsedMilliseconds);
                }
           });

           Console.Read();
        }

        public static T GetDataFromWebClient<T>()
        {
            using (var webClient = new WebClient())
            {
                webClient.BaseAddress = _url;
                return JsonConvert.DeserializeObject<T>(
                    webClient.DownloadString(_url));
            }
        }

        public static void GetDataFromHttpClientSync<T>(Stopwatch sw)
        {
            HttpClient httpClient = new HttpClient();
            var response = httpClient.GetAsync(_url).Result;
            var obj = JsonConvert.DeserializeObject<T>(
                response.Content.ReadAsStringAsync().Result);
            sw.Stop();

            Trace.WriteLine("HttpClient Sync " + sw.ElapsedMilliseconds);
        }

        public static void GetDataFromHttpClientAsync<T>(Stopwatch sw)
        {
           HttpClient httpClient = new HttpClient();
           var response = httpClient.GetAsync(_url).ContinueWith(
              (a) => {
                 JsonConvert.DeserializeObject<T>(
                    a.Result.Content.ReadAsStringAsync().Result);
                 sw.Stop();
                 Trace.WriteLine("HttpClient Async " + sw.ElapsedMilliseconds);
              }, TaskContinuationOptions.None);
        }
    }
}

Мої запитання

  1. Виклики REST повертаються через 3-4 секунди, що прийнятно. Дзвінки в службу REST ініціюються методами контролера, який отримує виклик від дзвінків ajax Для початку дзвінки виконуються в іншому потоці і не блокують інтерфейс користувача. Отже, чи можу я просто дотримуватися синхронізації дзвінків?
  2. Вищевказаний код запускався в моєму локальному ящику. У налаштуваннях prod буде задіяно DNS та пошук проксі. Чи є якась перевага використання HttpClientнад WebClient?
  3. Чи HttpClientпаралельність краще, ніж WebClient? З результатів тестування я бачу, що WebClientвиклики синхронізації працюють краще.
  4. Чи HttpClientстане кращим вибором дизайну, якщо ми перейдемо до .Net 4.5? Продуктивність є ключовим фактором дизайну.

5
Ваш тест є несправедливим, GetDataFromHttpClientAsyncоскільки він запускається першим, інші виклики отримують користь від потенційно кешованих даних (будь то на локальній машині або будь-якого прозорого проксі між вами та пунктом призначення) і будуть швидшими. Крім того, за правильних умов var response = httpClient.GetAsync("http://localhost:9000/api/values/").Result;може виникнути глухий кут через вас виснажливих ниток басейну. Ніколи не слід блокувати активність, яка залежить від пулу потоків у потоках ThreadPool, awaitнатомість слід повернути нитку назад у пул.
Скотт Чемберлен

1
HttpClient з клієнтом Web API є фантастичним для клієнта JSON / XML REST.
Кори Нельсон

@Scott Chamberlain - Дякую за вашу відповідь. Оскільки всі тестові дзвінки виконуються в Parallel.Foreach, немає гарантії, який із них був би запущений першим. Крім того, якщо перший дзвінок у службу був від GetDataFromHttpClientAsync, усі наступні дзвінки з GetDataFromHttpClientAsync повинні були скористатися кешем і запустити швидше. Я не бачив цього в результаті. Rgd чекаємо, ми все ще використовуємо 4.0. Я погоджуюсь з вами, що HttpClient синхронізуючий спосіб призведе до тупикової ситуації, і я виключаю цей варіант з мого розгляду дизайну.
user3092913

@CoryNelson Ви можете, будь ласка, пояснити, чому HttpClient з клієнтом Web API є фантастичним для клієнта JSON / XML REST?
користувач3092913

2
Ось кілька слів про різницю між HttpClient та WebClient: blogs.msdn.com/b/henrikn/archive/2012/02/11/…
JustAndrei

Відповіді:


243

Я живу як у світі F #, так і у веб-API.

З веб-API відбувається багато хороших речей, особливо у формі обробників повідомлень для безпеки тощо.

Я знаю, що у мене є лише одна думка, але я б рекомендував використовувати лише HttpClientдля будь-якої майбутньої роботи . Можливо, є якийсь спосіб скористатися деякими іншими частинами, що виходять із цього місця, System.Net.Httpне використовуючи безпосередньо цю збірку, але я не можу уявити, як би це працювало в цей час.

Якщо говорити про порівняння цих двох

  • HttpClient ближче до HTTP, ніж до WebClient.
  • HttpClient не мав бути повноцінною заміною веб-клієнта, оскільки є такі речі, як хід звіту, спеціальна схема URI та здійснення FTP-дзвінків, які надає WebClient - але HttpClient цього не робить.
+--------------------------------------------+--------------------------------------------+
|               WebClient                    |               HttpClient                   |
+--------------------------------------------+--------------------------------------------+
| Available in older versions of .NET        | .NET 4.5 only.  Created to support the     |
|                                            | growing need of the Web API REST calls     |
+--------------------------------------------+--------------------------------------------+
| WinRT applications cannot use WebClient    | HTTPClient can be used with WinRT          |
+--------------------------------------------+--------------------------------------------+
| Provides progress reporting for downloads  | No progress reporting for downloads        |
+--------------------------------------------+--------------------------------------------+
| Does not reuse resolved DNS,               | Can reuse resolved DNS, cookie             |
| configured cookies                         | configuration and other authentication     |
+--------------------------------------------+--------------------------------------------+
| You need to new up a WebClient to          | Single HttpClient can make concurrent      |
| make concurrent requests.                  | requests                                   |
+--------------------------------------------+--------------------------------------------+
| Thin layer over WebRequest and             | Thin layer of HttpWebRequest and           |
| WebResponse                                | HttpWebResponse                            |
+--------------------------------------------+--------------------------------------------+
| Mocking and testing WebClient is difficult | Mocking and testing HttpClient is easy     |
+--------------------------------------------+--------------------------------------------+
| Supports FTP                               | No support for FTP                         |
+--------------------------------------------+--------------------------------------------+
| Both Synchronous and Asynchronous methods  | All IO bound methods in                    |
| are available for IO bound requests        | HTTPClient are asynchronous                |
+--------------------------------------------+--------------------------------------------+

Якщо ви використовуєте .NET 4.5, будь ласка, використовуйте користь асинхронізації з HttpClient, яку Microsoft надає розробникам. HttpClient дуже симетричний відносно серверів, які брати HTTP, це HttpRequest і HttpResponse.

Оновлення: 5 причин використання нового API HttpClient:

  • Сильно набрані заголовки.
  • Спільні кеші, файли cookie та облікові дані
  • Доступ до файлів cookie та спільних файлів cookie
  • Контроль кешування та спільного кешу.
  • Введіть свій кодовий модуль у трубопровід ASP.NET. Чистіший і модульний код.

Довідково

C # 5.0 Джозеф Альбахарі

(Channel9 - Video Build 2013)

П'ять великих причин використовувати новий HttpClient API для підключення до веб-служб

WebClient vs HttpClient проти HttpWebRequest


4
Слід зазначити, що HttpClient також доступний для .NET 4.0 .
Тодд Меньє

2
Це не пояснює, чому WebClient здається набирає величини швидше, ніж HttpClient. Також, WebClientсхоже, зараз є методи асинхронізації.
розчавити

8
@crush це тому, що ОП створює новий екземпляр HttpClient для кожного запиту. Натомість ви повинні використовувати один екземпляр HttpClient протягом усього часу роботи вашої програми. Дивіться stackoverflow.com/a/22561368/57369
Габріель

6
Варто зауважити, що WebClientвона не доступна, .Net Coreале HttpClientє.
Пранав Сінгх

3
Оскільки .Net Core 2.0 WebClient (серед тисяч інших API) повернувся і доступний.
CoderBang

56

HttpClient - це новіший API, і він має переваги

  • має гарну модель програмування async
  • над якою працював Генрік Ф Нільсон, який в основному є одним із винахідників HTTP, і він розробив API, щоб вам було легко слідувати стандарту HTTP, наприклад, генеруючи відповідні стандартам заголовки
  • знаходиться в .Net Framework 4.5, тому він має певний гарантований рівень підтримки в осяжному майбутньому
  • також є версія xcopyable / portable-Framework бібліотеки, якщо ви хочете використовувати її на інших платформах - .Net 4.0, Windows Phone тощо.

Якщо ви пишете веб-службу, яка здійснює дзвінки REST до інших веб-служб, вам слід захотіти використовувати модель програмування асинхронізації для всіх своїх REST-дзвінків, щоб у вас не було голодного потоку. Можливо, ви також хочете використовувати найновіший компілятор C #, який має підтримку async / очікує.

Примітка. Це не більш ефективний AFAIK. Це, мабуть, дещо аналогічно, якщо створити справедливий тест.


Якби у нього був спосіб переключити проксі, це було б божевільно
ed22

3

По-перше, я не є владою щодо WebClient проти HttpClient, зокрема. По-друге, з ваших коментарів вище, здається, випливає думка, що WebClient синхронізується ТІЛЬКО, тоді як HttpClient - це обоє.

Я зробив швидкий тест на працездатність, щоб виявити, як виконують WebClient (Sync дзвінки), HttpClient (Sync та Async). і ось результати.

Я розглядаю це як величезну різницю, коли думаєте про майбутнє, тобто тривалі процеси, чуйний графічний інтерфейс тощо (додайте до переваг, які ви пропонуєте в рамках 4.5 - що в моєму реальному досвіді набагато швидше на IIS)


4
WebClientСхоже, має можливості асинхронізації в останніх версіях .NET. Мені хотілося б знати, чому він, схоже, перевершує HttpClient в таких масштабних масштабах.
розчавити

1
Згідно stackoverflow.com/a/4988325/1662973 , схоже, це те саме, окрім того, що одне є абстракцією іншого. Можливо, це залежить від того, як об’єкти використовуються / завантажуються. Мінімальний час підтримує твердження про те, що webclient насправді є абстракцією HttpClient, тому великі витрати на мільйонну секунду. Рамка може бути "підлістю" в тому, як це насправді об'єднання або розміщення веб-клієнтів.
Ентоні Хорн

3

HttpClientFactory

Важливо оцінити різні способи створення HttpClient, і частиною цього є розуміння HttpClientFactory.

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

Це я не є прямою відповіддю - але краще починати тут, ніж закінчувати new HttpClient(...)всюди.


2

Я маю орієнтир між HttpClient, WebClient, HttpWebResponse, а потім зателефонуйте Rest Web Api

і результат виклику відпочинкового веб-Api еталону

--------------------- Етап 1 ---- 10 Запит

{00: 00: 17.2232544} ====> HttpClinet

{00: 00: 04.3108986} ====> Веб-запит

{00: 00: 04.5436889} ====> WebClient

--------------------- Етап 1 ---- 10 Запит - Невеликий розмір

{00: 00: 17.2232544} ====> HttpClinet

{00: 00: 04.3108986} ====> Веб-запит

{00: 00: 04.5436889} ====> WebClient

--------------------- Етап 3 ---- 10 Запит на синхронізацію - невеликий розмір

{00: 00: 15.3047502} ====> HttpClinet

{00: 00: 03.5505249} ====> Веб-запит

{00: 00: 04.0761359} ====> WebClient

--------------------- Етап 4 ---- 100 Запит на синхронізацію - невеликий розмір

{00: 03: 23.6268086} ====> HttpClinet

{00: 00: 47.1406632} ====> Веб-запит

{00: 01: 01.2319499} ====> WebClient

--------------------- Етап 5 ---- 10 Запит на синхронізацію - Максимальний розмір

{00: 00: 58.1804677} ====> HttpClinet

{00: 00: 58.0710444} ====> Веб-запит

{00: 00: 38.4170938} ====> WebClient

--------------------- Етап 6 ---- 10 Запит на синхронізацію - Максимальний розмір

{00: 01: 04.9964278} ====> HttpClinet

{00: 00: 59.1429764} ====> Веб-запит

{00: 00: 32.0584836} ====> WebClient

_____ WebClient швидше ()

var stopWatch = new Stopwatch();
        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetHttpClient();
            CallPostHttpClient();
        }

        stopWatch.Stop();

        var httpClientValue = stopWatch.Elapsed;

        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {
            CallGetWebRequest();
            CallPostWebRequest();
        }

        stopWatch.Stop();

        var webRequesttValue = stopWatch.Elapsed;


        stopWatch = new Stopwatch();

        stopWatch.Start();
        for (var i = 0; i < 10; ++i)
        {

            CallGetWebClient();
            CallPostWebClient();

        }

        stopWatch.Stop();

        var webClientValue = stopWatch.Elapsed;

// ------------------------- Функції

private void CallPostHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.PostAsync("PostJson", null);
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private void CallGetHttpClient()
    {
        var httpClient = new HttpClient();
        httpClient.BaseAddress = new Uri("https://localhost:44354/api/test/");
        var responseTask = httpClient.GetAsync("getjson");
        responseTask.Wait();

        var result = responseTask.Result;
        var readTask = result.Content.ReadAsStringAsync().Result;

    }
    private string CallGetWebRequest()
    {
        var request = (HttpWebRequest)WebRequest.Create("https://localhost:44354/api/test/getjson");

        request.Method = "GET";
        request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;

        var content = string.Empty;

        using (var response = (HttpWebResponse)request.GetResponse())
        {
            using (var stream = response.GetResponseStream())
            {
                using (var sr = new StreamReader(stream))
                {
                    content = sr.ReadToEnd();
                }
            }
        }

        return content;
    }
    private string CallPostWebRequest()
    {

        var apiUrl = "https://localhost:44354/api/test/PostJson";


        HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(apiUrl));
        httpRequest.ContentType = "application/json";
        httpRequest.Method = "POST";
        httpRequest.ContentLength = 0;

        using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse())
        {
            using (Stream stream = httpResponse.GetResponseStream())
            {
                var json = new StreamReader(stream).ReadToEnd();
                return json;
            }
        }

        return "";
    }

    private string CallGetWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/getjson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.DownloadString(apiUrl);


        return json;
    }

    private string CallPostWebClient()
    {
        string apiUrl = "https://localhost:44354/api/test/PostJson";


        var client = new WebClient();

        client.Headers["Content-type"] = "application/json";

        client.Encoding = Encoding.UTF8;

        var json = client.UploadString(apiUrl, "");


        return json;
    }

1
Дивіться коментар Габріеля вище. Коротше кажучи, HttpClient набагато швидше, якщо створити один екземпляр HttpClient і використовувати його повторно.
LT Dan

1

Можливо, ви могли б подумати про проблему по-іншому. WebClientі HttpClientпо суті є різними реалізаціями одного і того ж. Що я рекомендую, це реалізувати схему введення залежностей із контейнером IoC протягом усієї програми. Ви повинні побудувати клієнтський інтерфейс з більш високим рівнем абстракції, ніж передача HTTP низького рівня. Ви можете написати конкретні класи, які використовують і, WebClientі HttpClient, а потім використовувати контейнер IoC для ін'єкції реалізації через config.

Це дозволило б вам перейти між собою HttpClientта WebClientлегко, щоб ви могли об'єктивно перевірити виробничі умови.

Отже такі питання, як:

Чи стане HttpClient кращим вибором дизайну, якщо ми перейдемо до .Net 4.5?

Насправді можна об'єктивно відповісти, перемикаючись між двома реалізаціями клієнта за допомогою контейнера IoC. Ось приклад інтерфейсу, від якого ви можете залежати, який не містить деталей про HttpClientабо WebClient.

/// <summary>
/// Dependency Injection abstraction for rest clients. 
/// </summary>
public interface IClient
{
    /// <summary>
    /// Adapter for serialization/deserialization of http body data
    /// </summary>
    ISerializationAdapter SerializationAdapter { get; }

    /// <summary>
    /// Sends a strongly typed request to the server and waits for a strongly typed response
    /// </summary>
    /// <typeparam name="TResponseBody">The expected type of the response body</typeparam>
    /// <typeparam name="TRequestBody">The type of the request body if specified</typeparam>
    /// <param name="request">The request that will be translated to a http request</param>
    /// <returns></returns>
    Task<Response<TResponseBody>> SendAsync<TResponseBody, TRequestBody>(Request<TRequestBody> request);

    /// <summary>
    /// Default headers to be sent with http requests
    /// </summary>
    IHeadersCollection DefaultRequestHeaders { get; }

    /// <summary>
    /// Default timeout for http requests
    /// </summary>
    TimeSpan Timeout { get; set; }

    /// <summary>
    /// Base Uri for the client. Any resources specified on requests will be relative to this.
    /// </summary>
    Uri BaseUri { get; set; }

    /// <summary>
    /// Name of the client
    /// </summary>
    string Name { get; }
}

public class Request<TRequestBody>
{
    #region Public Properties
    public IHeadersCollection Headers { get; }
    public Uri Resource { get; set; }
    public HttpRequestMethod HttpRequestMethod { get; set; }
    public TRequestBody Body { get; set; }
    public CancellationToken CancellationToken { get; set; }
    public string CustomHttpRequestMethod { get; set; }
    #endregion

    public Request(Uri resource,
        TRequestBody body,
        IHeadersCollection headers,
        HttpRequestMethod httpRequestMethod,
        IClient client,
        CancellationToken cancellationToken)
    {
        Body = body;
        Headers = headers;
        Resource = resource;
        HttpRequestMethod = httpRequestMethod;
        CancellationToken = cancellationToken;

        if (Headers == null) Headers = new RequestHeadersCollection();

        var defaultRequestHeaders = client?.DefaultRequestHeaders;
        if (defaultRequestHeaders == null) return;

        foreach (var kvp in defaultRequestHeaders)
        {
            Headers.Add(kvp);
        }
    }
}

public abstract class Response<TResponseBody> : Response
{
    #region Public Properties
    public virtual TResponseBody Body { get; }

    #endregion

    #region Constructors
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response() : base()
    {
    }

    protected Response(
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    TResponseBody body,
    Uri requestUri
    ) : base(
        headersCollection,
        statusCode,
        httpRequestMethod,
        responseData,
        requestUri)
    {
        Body = body;
    }

    public static implicit operator TResponseBody(Response<TResponseBody> readResult)
    {
        return readResult.Body;
    }
    #endregion
}

public abstract class Response
{
    #region Fields
    private readonly byte[] _responseData;
    #endregion

    #region Public Properties
    public virtual int StatusCode { get; }
    public virtual IHeadersCollection Headers { get; }
    public virtual HttpRequestMethod HttpRequestMethod { get; }
    public abstract bool IsSuccess { get; }
    public virtual Uri RequestUri { get; }
    #endregion

    #region Constructor
    /// <summary>
    /// Only used for mocking or other inheritance
    /// </summary>
    protected Response()
    {
    }

    protected Response
    (
    IHeadersCollection headersCollection,
    int statusCode,
    HttpRequestMethod httpRequestMethod,
    byte[] responseData,
    Uri requestUri
    )
    {
        StatusCode = statusCode;
        Headers = headersCollection;
        HttpRequestMethod = httpRequestMethod;
        RequestUri = requestUri;
        _responseData = responseData;
    }
    #endregion

    #region Public Methods
    public virtual byte[] GetResponseData()
    {
        return _responseData;
    }
    #endregion
}

Повний код

Реалізація HttpClient

Ви можете використовувати Task.Runдля WebClientзапуску асинхронно в його реалізації.

Ін'єкція залежностей, коли це зроблено добре, допомагає полегшити проблему необхідності приймати рішення низького рівня заздалегідь. Зрештою, єдиний спосіб дізнатись справжню відповідь - спробувати як у живому середовищі, так і побачити, який із них працює найкраще. Цілком можливо, що WebClientдля деяких клієнтів HttpClientможе працювати краще, а для інших - краще. Ось чому важливо абстрагування. Це означає, що код можна швидко замінити або змінити з конфігурацією без зміни основного дизайну програми.


1

Непопулярна думка з 2020 року:

Коли справа доходить до ASP.NET додатків я віддаю перевагу WebClientбільш , HttpClientтому що:

  1. Сучасна реалізація поставляється з асинхронізованими / очікуваними методами на основі завдань
  2. Має менший слід пам’яті та 2x-5x швидше (інші відповіді вже згадують про це)
  3. Пропонується " повторно використовувати один екземпляр HttpClient протягом життя вашої програми ". Але ASP.NET не має "впродовж життя", а лише весь час запиту.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.