روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

اگر در کدهای خود قطعه کد ذیل را دارید:
using(var client = new HttpClient())
{
   // do something with http client
}
استفاده‌ی از using در اینجا، نه‌تنها غیرضروری و اشتباه است، بلکه سبب از کار افتادن زود هنگام برنامه‌ی شما با صدور استثنای ذیل خواهد شد:
 Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.


HttpClient خود را Dispose نکنید

کلاس HttpClient اینترفیس IDisposable را پیاده سازی می‌کند. بنابراین روش استفاده‌ی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
using (var client = new HttpClient())
{
       var result = await client.GetAsync("http://example.com/");
}
اما در این حال فرض کنید به همین روش تعدادی درخواست را ارسال کرده‌اید:
for (int i = 0; i < 10; i++)
{
      using (var client = new HttpClient())
      {
            var result = await client.GetAsync("http://example.com/");
            Console.WriteLine(result.StatusCode);
      }
}
مشکل این روش، در ایجاد سوکت‌های متعددی است که حتی پس از بسته شدن برنامه نیز باز، باقی خواهند ماند:
  TCP    192.168.1.6:13996      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13997      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13998      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:13999      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14000      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14001      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14002      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14003      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14004      93.184.216.34:http     TIME_WAIT
  TCP    192.168.1.6:14005      93.184.216.34:http     TIME_WAIT
این یک نمونه‌ی خروجی برنامه‌ی فوق، توسط دستور netstat «پس از بسته شدن کامل برنامه» است.

بنابراین اگر برنامه‌ی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال می‌کند، سیستم عامل به حد اشباع ایجاد سوکت‌های جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفاده‌ی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکت‌های ایجاد شده‌است. TIME_WAIT ایی را که در اینجا ملاحظه می‌کنید، به معنای بسته شدن اتصال از طرف برنامه‌ی ما است؛ اما سیستم عامل هنوز منتظر نتیجه‌ی نهایی، از طرف دیگر اتصال است که آیا قرار است بسته‌ی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشته‌است. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم می‌شود:
 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]

بنابراین روش توصیه شده‌ی کار با HttpClient، داشتن یک وهله‌ی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شده‌است و دسترسی به یک شیء سراسری آن در برنامه‌های چند ریسمانی مشکلی را ایجاد نمی‌کند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.


تمام اجزای HttpClient به صورت Thread-safe طراحی نشده‌اند

تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهله‌ی Singleton از آن‌را در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکت‌های سیستم عامل و ناپایدار کردن تمام برنامه‌هایی که از آن سرویس می‌گیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفاده‌ی از متدهای ذیل این کلاس thread-safe هستند:
CancelPendingRequests
DeleteAsync
GetAsync
GetByteArrayAsync
GetStreamAsync
GetStringAsync
PostAsync
PutAsync
SendAsync
اما تغییر این خواص در کلاس HttpClient به هیچ عنوان thread-safe نبوده و در برنامه‌های چند ریسمانی و چند کاربری، مشکل ساز می‌شوند:
BaseAddress
DefaultRequestHeaders
MaxResponseContentBufferSize
Timeout
بنابراین در طراحی کلاس مدیریت کننده‌ی HttpClient برنامه‌ی خود نیاز است به ازای هر BaseAddress‌، یک HttpClient خاص آن‌را ایجاد کرد و HttpClientهای سراسری نمی‌توانند BaseAddress‌های خود را نیز به اشتراک گذاشته و تغییری را در آن ایجاد کنند.


استفاده‌ی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمی‌شود

با طراحی یک کلاس مدیریت کننده‌ی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفاده‌ی مجدد می‌شوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجاره‌ی اتصال آن‌را دقیقا مشخص کنیم:
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com"));
sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds
با این‌کار هرچند هنوز هم از اتصالات استفاده‌ی مجدد می‌شود، اما این استفاده‌ی مجدد، نامحدود نبوده و مدت معینی را پیدا می‌کند.


طراحی یک کلاس، برای مدیریت سراسری وهله‌های HttpClient‌

تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهله‌ی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان می‌برد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شده‌است؛ اما تعدادی از خواص آن مانند BaseAddress‌  اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمی‌یابند)، نیاز است timeout آن‌را تنظیم کرد.

بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
using System;
using System.Collections.Generic;
using System.Net.Http;

namespace HttpClientTips
{
    public interface IHttpClientFactory : IDisposable
    {
        HttpClient GetOrCreate(
            Uri baseAddress,
            IDictionary<string, string> defaultRequestHeaders = null,
            TimeSpan? timeout = null,
            long? maxResponseContentBufferSize = null,
            HttpMessageHandler handler = null);
    }
}

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;

namespace HttpClientTips
{
    /// <summary>
    /// Lifetime of this class should be set to `Singleton`.
    /// </summary>
    public class HttpClientFactory : IHttpClientFactory
    {
        // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than
        // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
        // threads but only one of the objects succeeds in creating the HttpClient.
        private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients =
                         new ConcurrentDictionary<Uri, Lazy<HttpClient>>();
        private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute

        public HttpClientFactory()
        {
            // Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx
            ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
            // Increases the concurrent outbound connections
            ServicePointManager.DefaultConnectionLimit = 1024;
        }

        public HttpClient GetOrCreate(
           Uri baseAddress,
           IDictionary<string, string> defaultRequestHeaders = null,
           TimeSpan? timeout = null,
           long? maxResponseContentBufferSize = null,
           HttpMessageHandler handler = null)
        {
            return _httpClients.GetOrAdd(baseAddress,
                             uri => new Lazy<HttpClient>(() =>
                             {
                                 // Reusing a single HttpClient instance across a multi-threaded application means
                                 // you can't change the values of the stateful properties (which are not thread safe),
                                 // like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout.
                                 // So you can only use them if they are constant across your application and need their own instance if being varied.
                                 var client = handler == null ? new HttpClient { BaseAddress = baseAddress } :
                                               new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress };
                                 setRequestTimeout(timeout, client);
                                 setMaxResponseBufferSize(maxResponseContentBufferSize, client);
                                 setDefaultHeaders(defaultRequestHeaders, client);
                                 setConnectionLeaseTimeout(baseAddress, client);
                                 return client;
                             },
                             LazyThreadSafetyMode.ExecutionAndPublication)).Value;
        }

        public void Dispose()
        {
            foreach (var httpClient in _httpClients.Values)
            {
                httpClient.Value.Dispose();
            }
        }

        private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client)
        {
            // This ensures connections are used efficiently but not indefinitely.
            client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client
            ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely.
        }

        private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client)
        {
            if (defaultRequestHeaders == null)
            {
                return;
            }
            foreach (var item in defaultRequestHeaders)
            {
                client.DefaultRequestHeaders.Add(item.Key, item.Value);
            }
        }

        private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client)
        {
            if (maxResponseContentBufferSize.HasValue)
            {
                client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value;
            }
        }

        private static void setRequestTimeout(TimeSpan? timeout, HttpClient client)
        {
            if (timeout.HasValue)
            {
                client.Timeout = timeout.Value;
            }
        }
    }
}
در اینجا به ازای هر baseAddress جدید، یک HttpClient خاص آن ایجاد می‌شود تا در کل برنامه مورد استفاده‌ی مجدد قرار گیرد. برای مدیریت thread-safe ایجاد HttpClientها نیز از نکته‌ی مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary» استفاده شده‌است. همچنین نکات تنظیم ConnectionLeaseTimeout و سایر خواص غیر thread-safe کلاس HttpClient نیز در اینجا لحاظ شده‌اند.

پس از تدارک این کلاس، نحوه‌ی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده می‌کنید، آن‌را به صورت ذیل ثبت کنید:
namespace HttpClientTips.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IHttpClientFactory, HttpClientFactory>();
            services.AddMvc();
        }

اکنون، یک نمونه، نحوه‌ی استفاده‌ی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل می‌باشد:
namespace HttpClientTips.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IHttpClientFactory _httpClientFactory;
        public HomeController(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public async Task<IActionResult> Index()
        {
            var host = new Uri("http://localhost:5000");
            var httpClient = _httpClientFactory.GetOrCreate(host);
            var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false);
            var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
            return Content(responseContent);
        }
سرویس IHttpClientFactory یک HttpClient را به ازای host درخواستی ایجاد کرده و در طول عمر برنامه از آن استفاده‌ی مجدد می‌کند. به همین جهت دیگر مشکل اشباع سوکت‌ها در این سیستم رخ نخواهند داد.


برای مطالعه‌ی بیشتر

You're using HttpClient wrong and it is destabilizing your software
Disposable, Finalizers, and HttpClient
Using HttpClient as it was intended (because you’re not)
Singleton HttpClient? Beware of this serious behaviour and how to fix it
Beware of the .NET HttpClient
Effectively Using HttpClient
  • #
    ‫۶ سال و ۸ ماه قبل، شنبه ۲۳ دی ۱۳۹۶، ساعت ۱۷:۰۷
    با سلام و احترام. با توجه به این که ASP.NET Core کدها را با ماهیت SynchronizationContext اجرا نمی‌کند، نوشتن یا ننوشتن ConfigureAwait(false) چه تفاوتی ایجاد می‌کند؟
    دوم اینکه اگر فرض کنیم این مثال برای ASP.NET MVC غیر ASP.NET Core ای نوشته شده باشد که در آن نوشتن ConfigureAwait(false) با ننوشتن اش تفاوت ایجاد می‌کند، این نوع استفاده از ConfigureAwait ایجاد مشکل می‌کند، زیرا به علت Restore نشدن Sync Context، عملا مواردی مثل HttpContext.Current مقدار درستی را در خط بعد از await نخواهند داشت.
    در مجموع جای ConfigureAwait(false) چه در ASP.NET و چه در ASP.NET Core در Controller و Action نیست. در ASP.NET Core که عملا تفاوتی ندارد و در ASP.NET هم در لایه‌های قبلی مثل Service و Repository اگر از ConfigureAwait(false) استفاده بشه، به بهبود عملکرد سیستم کمک می‌کنه، به شرطی که وقتی کد داره توی Controller و Action فراخونی می‌شه دیگه ConfigureAwait نداشته باشه.
    سپاس 
    • #
      ‫۶ سال و ۸ ماه قبل، شنبه ۲۳ دی ۱۳۹۶، ساعت ۱۸:۳۹
      HttpClient یک کتابخانه‌ی چندسکویی و پرتابل هست و همه‌جا قابل استفاده‌است. بنابراین ارائه‌ی نحوه‌ی استفاده‌ای که سبب تفکر شود، روش مناسبی برای ارائه‌ی مطلب است. همچنین ممکن است کتابخانه‌هایی را تولید کنید بر همین مبنا که بر اساس NET Standard. تهیه شده باشند و در همه جا (در برنامه‌های WPF تا ASP.NET‌های مختلف و غیره) قابل استفاده باشند، به همین جهت یک best practice است که بهتر است رعایت شود.
      + علت‌های استفاده از ConfigureAwait(false):
      جلوگیری از deadlock در برنامه‌های async
        بهبود کارآیی برنامه
      1.   با حذف callbackهای فراخوان ترد جاری. هر چقدر تعداد این callbackها کمتر باشد، کارآیی برنامه بیشتر می‌شود. یک مثال
      2.   با اجازه دادن به CLR جهت اجرای این قطعه کد در هر تردی که صلاح می‌داند (و نه اجبار به اجرای نهایی آن در ترد اصلی).

      «... زیرا به علت Restore نشدن Sync Context، عملا مواردی مثل HttpContext.Current مقدار درستی را در خط بعد از await نخواهند داشت ...»
      اینطور نیست. درست است که سطرهای پس از ConfigureAwait(false) بر روی Thread pool اجرا می‌شوند که با ترد اصلی شروع کننده‌ی پردازش اکشن متد یکی نیست، اما context اصلی ترد جاری از حفظ اطلاعات مرتبط با ASP.NET در آن‌ها اطمینان حاصل می‌کند:

      If multiple operations complete at once for the same application, AspNetSynchronizationContext will ensure that they execute one at a time. They may execute on any thread, but that thread will have the identity and culture of the original page

      • #
        ‫۶ سال و ۸ ماه قبل، شنبه ۲۳ دی ۱۳۹۶، ساعت ۲۰:۰۶
        اگر هدف ارائه دادن نکات مهم باشد که موارد زیادی را می‌توان گنجاند. مثل گرفتن Cancellation token درخواست جاری به عنوان ورودی action و پاس دادن آن به متدهای http client تا در صورتی که درخواست اصلی منتفی شد، الکی http client کلی کار اضافه انجام نده و ضمن کنسل شدن کار، از ادامه کار سرور مقصد http client هم جلوگیری بشه. اما اگر هدف ارائه دادن مهم‌ترین نکات نبوده بلکه تمرکز بر روی قطعه کد جاری و اصل این مطلب است نیز دو مهم وجود دارد:
        ۱- چرا بعد از await دوم در asp.net، مقدار System.Web.HttpContext.Current نال است، در صورتی که بدون ConfigureAwait این اتفاق نمی‌افتد؟
        ۲- آیا بنچمارک ای دارید که نشان دهد عملکرد این کد در asp.net core با و بدون configure await تفاوت می‌کند؟
        اساسا جای configure await اگر هم قصد آموزش اش وجود داشته باشد، در mvc/web api actions & middlewares نیست، چون در بهترین حالت تفاوتی نمی‌کند و در بدترین حالت ایجاد مشکل می‌کند. در واقع وقتی کد در web api actions نوشته می‌شود، دیگر چند سکویی و ... معنی نمی‌دهد، بلکه مشخصا مثلا داریم برای asp.net core 2 mvc کد می‌زنیم. توصیه شده در جاهای دیگر مانند service‌ها و repository‌ها در صورتی که معلوم نباشد کجا قرار است استفاده شوند، مثلا asp.net باشد یا wpf، بد نیست configure await استفاده شود که اولا این کم رخ می‌دهد و دوما حتما باز باید در Web API/MVC action موقع فراخوانی آن متد Async از ConfigureAwait استفاده نشود. به همین علت فراخوانی Configure Await در اکثر پروژه‌های نرم افزاری عملا به دست فراموشی سپرده شده و در سطح فریمورک‌های مطرح مانند ASP.NET Core هم دیگر استفاده نمی‌شود.
        با توجه به این که عموما استفاده از ConfigureAwait  به شکل صحیح Use case‌های کمی دارد، این قسمت از کد بیشتر مشکل زا و گمراه کننده می‌دانم تا مفید و آموزنده.
        سپاس 
        • #
          ‫۶ سال و ۸ ماه قبل، شنبه ۲۳ دی ۱۳۹۶، ساعت ۲۲:۵۲
          کار کردن مستقیم با System.Web.HttpContext.Current در یک برنامه‌ی اصولی ASP.NET هیچگاه توصیه نمی‌شود؛ چون نه فقط قابلیت آزمون پذیری آن‌را پایین می‌آورد، همچنین معادل OWIN ایی ندارد و thread-safe هم طراحی نشده‌است. اگر بحث کار با اکشن متدهای ASP.NET MVC 5.x هست، بجای آن از this.HttpContext در یک اکشن متد استفاده می‌شود که پس از ConfigureAwait(false) نال نیست و قابل استفاده است؛ چون این خاصیت عضو کلاس پایه AsyncController هست. برای نمونه اگر قطعه کد اکشن متد Index فوق را در ASP.NET MVC 5.x هم اجرا کنید، کار می‌کند؛ چون  return Content آن در پشت صحنه از همین this.HttpContext برای نوشتن در Response استفاده می‌کند.
  • #
    ‫۶ سال و ۷ ماه قبل، شنبه ۱۴ بهمن ۱۳۹۶، ساعت ۱۲:۴۵
    یک نکته‌ی تکمیلی

    به همراه NET Core 2.1.، یک HttpClientFactory توکار توسط مایکروسافت ارائه شده‌است:

    به این ترتیب برای مثال جهت کار با یک آدرس مشخص، می‌توان تنظیمات آن‌را یکبار در آغاز برنامه ثبت کرد:
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient("github", c =>
        {
            c.BaseAddress = new Uri("https://api.github.com/");
            c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent
        });
        services.AddHttpClient();
    }
    و بعد برای استفاده‌ی سراسری از آن توسط سیستم ترزیق وابستگی‌ها، می‌توان به صورت زیر عمل کرد:
    IHttpClientFactory _httpClientFactory;
    public MyController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    public IActionResult Index()
    {
        //This client doesn’t have any special configuration applied
        var defaultClient = _httpClientFactory.CreateClient();
        //This client has the header and base address configured for the “github” client above.
        var gitHubClient = _httpClientFactory.CreateClient("github");
        return View();
    }
  • #
    ‫۶ سال و ۵ ماه قبل، چهارشنبه ۲۹ فروردین ۱۳۹۷، ساعت ۱۸:۳۷
    معادل این مطلب در برنامه‌های ASP.NET Core

    ابتدا بسته‌ی نیوگت DNTCommon.Web.Core را نصب کنید:
    PM> Install-Package DNTCommon.Web.Core
    سپس مثالی از ICommonHttpClientFactory آن‌را در اینجا می‌توانید مشاهده کنید.  
    • #
      ‫۶ سال قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۷:۰۷
      چگونه می‌توان این کتابخانه را در لایه سرویس استفاده کرد ؟
       مشکلم این است که وقتی می‌خواهم از این کتابخانه در لایه‌های دیگر  مثل لایه Service استفاده کنم ، چون فایل Startup ندارم نمی‌دانم چگونه آن را صدا بزنم؟
      • #
        ‫۶ سال قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۷:۲۸
        « ... چون فایل Startup ندارم ... »
        چه نوع پروژه‌ای هست؟
        • #
          ‫۶ سال قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۸:۲۳
          پروژ ه ام از نوع asp.net core 2.1 است.
          در لایه WebApp مشکلی ندارم و در کلاس Startup آن را فراخوانی کرده ام و به خوبی کار می‌کند.
          لایه سرویسم از نوع .Net Standard می‌باشد می‌خواهم این کتابخانه را در لایه سرویس استفاده کنم
          • #
            ‫۶ سال قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۸:۳۲
            مشکلی ندارد. همان تنظیم اولیه‌ی تزریق وابستگی‌های آن، برای تمام لایه‌های برنامه‌ی وب کفایت می‌کند. همچنین این کتابخانه با حداقل net standard 1.6 و 2.0 سازگار است. این لایه‌هایی که تعریف می‌شوند مستقل از کل برنامه نیستند و در نهایت در پروسه‌ی برنامه‌ی اصلی بارگذاری و اجرا می‌شوند و جزئی از آن هستند. دقیقا مانند اینکه فضای نام جدیدی را به پروژه‌ی وب خود اضافه کرده باشید.
    • #
      ‫۲ سال و ۶ ماه قبل، شنبه ۲۳ بهمن ۱۴۰۰، ساعت ۱۹:۳۰
      ممنون میشم مزایای استفاده از CommonHttpClientFactory نسبت به HttpClientFactory در حال حاضر چیست؟ آیا با وجود     HttpClientFactory  نیازی به استفاده از CommonHttpClientFactory  هست؟
  • #
    ‫۶ سال و ۱ ماه قبل، چهارشنبه ۱۷ مرداد ۱۳۹۷، ساعت ۱۷:۴۹
    ممنونم از مطلب خوبتون
    بنده در پروژه‌ی خودم لایه‌ی سرویس رو با Web Api پیاده سازی کردم، لایه‌ی اپلیکیشن با MVC
    احراز هویت در لایه‌ی سرویس با JWT انجام می‌شود و نیاز است تا token دریافتی در DefaultRequestHeaders.Authorization تنظیم شود و سپس درخواست HTTP ارسال شود.( این موضوع در MVC انجام می‌پذیرد.)
    با توجه به گفته‌ی خودتون Thread Safe ،DefaultRequestHeaders نیست. پس نمیتونم به ازای تمامی درخواست‌ها از یک HttpClient استفاده کنم و مجبورم به ازای هرکاربر HttpClient جدیدی ایجاد کنم تا token در header رو برای هر کاربر جداگانه تنظیم کنم.
    پس کلید دیکشنری رو از Uri baseAddress به string token تغییر دادم.
    حالا مشکلی که برام پیش اومده اینه که تعداد HttpClient‌های منقضی شده در دیکشنری زیاد میشه و باید با روشی اونها رو از دیکشنری حذف کنم که سعی میکنم حلش کنم اما سوالم اینه که:
    آیا تغییری که در کلاس شما دادم اصولی بود ؟ 
    • #
      ‫۶ سال و ۱ ماه قبل، چهارشنبه ۱۷ مرداد ۱۳۹۷، ساعت ۱۸:۲۰
      هنوز هم می‌توانید از تک client استفاده کنید به شرطی‌که از متد ویژه‌ی زیر استفاده کنید:
      public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
      در این متد می‌توان هدرها را به ازای هر درخواست مشخص، جداگانه تنظیم و ارسال کرد (بدون تاثیر گذاشتن بر روی سایر درخواست‌ها):
       var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
       httpRequestMessage.Headers.Authorization = ...
       httpClient.SendAsync(httpRequestMessage);
  • #
    ‫۶ سال قبل، چهارشنبه ۳۱ مرداد ۱۳۹۷، ساعت ۰۲:۰۷
    سلام. من به این شکل و طبق الگو، پیاده سازی کردم
    private async Task<(Token Token, Dictionary<string, string> AppCookies)> LoginAsync(string requestUri, string username, string password)
            {
                var viewmodel = new LoginViewModel { Username = username, Password = password };
                var host = new Uri("http://localhost:5000");
                var httpClient = _httpClientFactoryService.GetOrCreate(host);
                var responseMessage = await httpClient.PostAsJsonAsync("api/account/login", viewmodel).ConfigureAwait(false);
                var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
               // return Content(responseContent);
    ...
    }
    اما این خطا رو میده:
    No connection could be made because the target machine actively refused it
    تو استک هم سرچ کردم میگه از فایر وال و آنتی ویروسه که جفتش هم خاموشه. چه باید بکنم؟
    • #
      ‫۶ سال قبل، چهارشنبه ۳۱ مرداد ۱۳۹۷، ساعت ۰۳:۰۷
      این خطا یعنی سرور یا آدرس مدنظر در دسترس نیست. اگر قصد اجرای مثال ASPNETCore2JwtAuthentication.ConsoleClient  را دارید، ابتدای آن نوشتم:
       //Note: First you should run the `ASPNETCore2JwtAuthentication.WebApp` project and then run the `ConsoleClient` project.
      یعنی ابتدا مراجعه کنید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp و فایل _1-dotnet_run.bat آن‌را اجرا کنید تا سرور در آدرس http://localhost:5000 راه اندازی شود. سپس این برنامه‌ی کنسول را جداگانه اجرا کنید تا به سرور در حال اجرا متصل شود.
      • #
        ‫۶ سال قبل، چهارشنبه ۳۱ مرداد ۱۳۹۷، ساعت ۰۳:۵۰
        بسیار عالی. درست شد. اما این در حالت لوکال هست. وقتی پروژه رو پابلیش می‌کنم و تو سرور قرار میدم باید به همین شکل کار کنه؟ یا خودش میفهمه؟
        • #
          ‫۶ سال قبل، چهارشنبه ۳۱ مرداد ۱۳۹۷، ساعت ۰۴:۰۹
          بررسی کنید آیا مسیر http://localhost:5000/api/account/login یا مسیر مشابه آن در مرورگر قابل دسترسی هست یا خیر. اگر بله، با HttpClient هم قابل دسترسی است.
  • #
    ‫۱۰ روز قبل، جمعه ۱۶ شهریور ۱۴۰۳، ساعت ۱۲:۱۷

    یک نکته‌ی تکمیلی: امکان کار همزمان هم با HttpClient وجود دارد!

    تا پیش از ارائه‌ی NET Core.، روش متداول دریافت فایل‌ها، عموما به صورت زیر و همزمان/synchronous بود:

    var client = new WebClient();
    client.DownloadFile(downloadUrl, filePath);

    هرچند ... WebClient امکان دریافت فایل‌ها را به صورت غیرهمزمان هم دارد، اما API آن با async/await هماهنگ نیست و طراحی آن قدیمی است.

    پس از آن،‌ HttpClient ارائه شد که از روز اول، async بود و کاملا هماهنگ با async/await و روش کدنویسی جدید آن. اما ... شاید در قسمت‌هایی نیاز باشد تا بتوان کدهای قدیمی را بدون تبدیل کردن آن‌ها به نمونه‌های async، به همان شکل همزمان، بازنویسی کنیم. برای رفع این مشکل، از زمان دات‌نت 5، متد Send همزمان هم به API آن اضافه شده‌است:

    var response = httpClient.Send(new HttpRequestMessage(HttpMethod.Post, "http://site.com"));
    using var reader = new StreamReader(response.Content.ReadAsStream());            
    var content = reader.ReadToEnd();