آموزش کار با HttpClient در ASP.NET Core 2x

دسته‌بندی: آموزش
تاریخ انتشار: ۱۳۹۸ فروردین ۲۵, یکشنبه ساعت ۱۱:۱۹۰ رأیبدون امتیاز
آموزش کار با HttpClient در ASP.NET Core 2x

پیشتر مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» را مطالعه کرده بودید. پس از ارائه‌ی NET Core 2.1.، این مجموعه به همراه یک IHttpClientFactory نیز ارائه می‌شود که در اینجا قصد داریم این مورد و همچنین سایر موارد مشابه را بررسی کنیم.\ \ \ صورت مساله\ قصد داریم اطلاعاتی را با فرمت JSON...

پیشتر مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» را مطالعه کرده بودید. پس از ارائه‌ی NET Core 2.1.، این مجموعه به همراه یک IHttpClientFactory نیز ارائه می‌شود که در اینجا قصد داریم این مورد و همچنین سایر موارد مشابه را بررسی کنیم.

صورت مساله

قصد داریم اطلاعاتی را با فرمت JSON، از یک API خارجی، توسط HttpClient دریافت و سپس آن‌را به یک DTO فرضی، به نام GitHubRepositoryDto نگاشت کنیم.

راه حل ۱

در این روش از وهله سازی مستقیم HttpClient به همراه استفاده‌ی از یک عبارت using کمک گرفته شده‌است. همچنین چون عملیات async است، نتیجه‌ی آن‌را به کمک خاصیت Result دریافت کرده‌ایم که پس از آن، کل اطلاعات دریافتی را به صورت یک رشته، در اختیار خواهیم داشت:

public class GitHubClient { public IReadOnlyCollection GetRepositories() { using (var httpClient = new HttpClient) { var result = httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).Result; return JsonConvert.DeserializeObject(result); } } }

مشکلات این راه حل:

– استفاده از خاصیت Result، هیچگاه ایده‌ی خوبی نبوده است و یک عملیات async را تبدیل به عملیاتی Blocking می‌کند که حتی می‌تواند سبب بروز dead-lock نیز شود.

– HttpClient نباید Dispose شود. علت آن‌را در مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» مفصل بررسی کرده‌ایم.

– دریافت کل response یک API به صورت یک رشته‌ی بزرگ، یک Large object heap را به‌وجود می‌آورد که باز هم ایده‌ی خوبی نیست.

راه حل ۲

اگر خاصیت Result راه حل ۱ را حذف کنیم، به راه حل ۲ خواهیم رسید:

public class GitHubClient : IGitHubClient { public async Task GetRepositories() { using (var httpClient = new HttpClient ) { var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath); return JsonConvert.DeserializeObject(result); } } }

مزایا:

– اینبار از دسترسی asynchronous واقعی استفاده شده‌است.

معایب:

– ایجاد و تخریب یک HttpClient جدید به ازای هر فراخوانی.

– دریافت و ذخیره سازی کل response به صورت یک رشته.

راه حل ۳

در این نگارش، HttpClient از طریق وهله سازی در سازنده‌ی کلاس دریافت شده و به این ترتیب امکان استفاده‌ی مجدد را پیدا می‌کند:

public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; public GitHubClient() { _httpClient = new HttpClient ; } public async Task GetRepositories() { var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject(result); } }

طول عمر GitHubClient نیز Singleton معرفی می‌شود.

services.AddSingleton();

مزایا:

– دسترسی asynchronous واقعی به API مدنظر.

– استفاده‌ی مجدد از HttpClient

معایب:

– دریافت و ذخیره سازی کل response به صورت یک رشته.

– چون طول عمر GitHubClient از نوع Singleton است و برای همیشه از یک وهله‌ی سراسری استفاده می‌کند، از تغییرات DNS آگاه نخواهد شد.

راه حل ۴

تا اینجا همانطور که ملاحظه کردید، به سادگی می‌توان HttpClient را به نحو نادرستی مورد استفاده قرار داد. ایجاد مجدد آن به علت عدم رها شدن بلافاصله‌ی سوکت‌های لایه‌ی زرین آن توسط سیستم عامل، مشکل حادی را به نام sockets exhaustion پدید می‌آورد. به همین جهت، این کلاس باید یکبار نمونه سازی شده و در طول عمر برنامه از همین تک وهله‌ی آن استفاده شود. یک روش اینکار تعریف آن به صورت اشیاء singleton و یا static است. مشکلی که این روش به همراه دارد، عدم باخبر شدن آن از تغییرات DNS است. برای رفع این مسایل، از NET Core 2.1. به بعد، خود مایکروسافت با ارائه‌ی یک IHttpClientFactory، روش استانداری را برای مدیریت وهله‌های HttpClient ارائه کرده‌است:

public class GitHubClient : IGitHubClient { private readonly IHttpClientFactory _httpClientFactory; public GitHubClient(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); } public async Task GetRepositories() { var httpClient = _httpClientFactory.CreateClient("GitHub"); var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject(result); } }

با این روش ثبت

services.AddHttpClient("GitHub", x => ); services.AddSingleton();

در این روش، IHttpClientFactory به سازنده‌ی کلاس تزریق می‌شوند و از آن برای دسترسی به یک HttpClient جدید، هربار که این متد فراخوانی خواهد شد، استفاده می‌کنیم. بله … در این حالت نیز یک HttpClient هربار ایجاد خواهد شد؛ اما چون از IHttpClientFactory استفاده می‌کنیم، مشکلی به شمار نمی‌رود. از این جهت که مطابق مستندات آن، هر HttpClient‌ای که به این نحو تولید می‌شود، یک HttpMessageHandler را در پشت صحنه مورد استفاده قرار می‌دهد که عملیات pooling و استفاده‌ی مجدد از آن‌ها، صورت می‌گیرد. یعنی IHttpClientFactory از HttpClient خود، به نحو بهینه‌ای استفاده‌ی مجدد می‌کند و در این حالت سیستم با مشکل کمبود منابع مواجه نخواهد شد و همچنین سربار ایجاد HttpClient‌های جدید نیز به حداقل می‌رسند.

مزیت‌ها:

– استفاده‌ی از یک IHttpClientFactory توکار

معایب:

– استفاده‌ی یک از کلاینت نامدار، بجای یک کلاینت مشخص شده‌ی بر اساس نوع آن.

– دریافت و ذخیره سازی کل response به صورت یک رشته.

روش ثبت services.AddHttpClient را که در اینجا ملاحظه می‌کنید، یک روش ثبت نامدار است و بر اساس نام رشته‌ای GitHub کار می‌کند. همین نام در متد GetRepositories به صورت httpClientFactory.CreateClient(“GitHub”) برای دسترسی به یک HttpClient جدید استفاده شده‌است.

راه حل ۵

در اینجا از یک کلاینت نوع‌دار، بجای یک کلاینت نامدار، استفاده شده‌است:

public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; public GitHubClient(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public async Task GetRepositories() { var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false); return JsonConvert.DeserializeObject(result); } }

با این روش ثبت:

services.AddHttpClient(x => );

برای ثبت یک کلاینت نوع‌دار، از متد AddHttpClient به همراه ذکر نوع کلاس کلاینت، استفاده می‌شود.

مزایا:

– استفاده از IHttpClientFactory

– استفاده از یک کلاینت نوع‌دار، بجای یک نمونه‌ی نامدار

معایب:

– اینبار تمام استفاده کنندگان از IGitHubClient ما باید دارای طول عمر transient باشند (خصوصیت کلاینت‌های نوع‌دار است)؛ برخلاف راه حل‌های پیشین که می‌توانستند singleton تعریف شوند (یا امکان فراخوانی IGitHubClient از سرویس‌های singleton نیز وجود داشت).

– دریافت و ذخیره سازی کل response به صورت یک رشته.

راه حل ۶

اگر در جائی نیاز به استفاده و تزریق یک کلاینت نوع‌دار، در یک سرویس با طول عمر singleton را داشتید، روش آن به صورت زیر است:

public class GitHubClientFactory { private readonly IServiceProvider _serviceProvider; public GitHubClientFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public GitHubClient Create() { return _serviceProvider.GetRequiredService(); } }

با این روش ثبت:

services.AddHttpClient(x => ); services.AddSingleton();

در این روش، یک GitHubClientFactory را داریم که یک GitHubClient را باز می‌گرداند. نکته‌ی اصلی آن، کار با ServiceProvider، جهت دسترسی به GitHubClient است. مابقی آن یعنی تعریف GitHubClient، مانند روش ۵ است.

مزایا:

– استفاده از IHttpClientFactory

– استفاده از یک کلاینت نوع‌دار

– استفاده کننده‌ی از GitHubClientFactory، می‌توانند طول عمر singleton نیز داشته باشد

معایب:

– دریافت و ذخیره سازی کل response به صورت یک رشته.

راه حل ۷

از اینجا به بعد، هدف ما بهینه سازی عملیات است و رفع مشکل کار با یک رشته‌ی بزرگ. برای این منظور بجای متد GetStringAsync، از متد SendAsync که امکان streaming را فراهم می‌کند، استفاده خواهیم کرد. به این ترتیب، بجای ارسال یک رشته‌ی بزرگ به متد Deserialize، امکان دسترسی به استریم response را توسط آن میسر کرده‌ایم.

public class GitHubClient : IGitHubClient { private readonly HttpClient _httpClient; private readonly JsonSerializer _jsonSerializer; public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); } public async Task GetRepositories() { var request = CreateRequest(); var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead); using (var responseStream = await result.Content.ReadAsStreamAsync()) { using (var streamReader = new StreamReader(responseStream)) using (var jsonTextReader = new JsonTextReader(streamReader)) { return _jsonSerializer.Deserialize(jsonTextReader); } } } private static HttpRequestMessage CreateRequest() { return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath); } }

با این روش ثبت:

services.AddHttpClient(x => ); services.AddSingleton(); services.AddSingleton();

مزایا:

– کار با IHttpClientFactory

– استفاده از یک کلاینت نوع‌دار

– کار با استریم response

معایب:

– استفاده از ResponseContentRead

راه حل ۸

در این روش بجای سطر ذیل در راه حل ۷

var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);

از این سطر استفاده خواهیم کرد:

var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

در حالت ResponseContentRead که حالت پیش‌فرض نیز هست، تمام هدرها و کل محتوای بازگشتی از سمت سرور باید خوانده شوند تا در اختیار مصرف کننده قرار گیرند، اما در حالت ResponseHeadersRead، فقط برای دریافت هدرها صبر خواهد شد و مابقی آن سریعا به صورت یک استریم در اختیار مصرف کننده قرار می‌گیرد.

مزایا:

– کار با IHttpClientFactory

– استفاده از یک کلاینت نوع‌دار

– کار با استریم response

– استفاده از ResponseHeadersRead

معایب:

– شاید بتوان از کتابخانه‌ی دیگری برای json deserialization استفاده کرد؟

dotnettips

شاید دیگر مطالب
روژانرا هم دوست داشته باشید

اشتراک‌گذاری

این مطلب برای شما مفید بود؟

بازخورد شما به بهبود کیفیت مطالب کمک می‌کند.

در حال بررسی رأی شما...