مقدمه
در Net Core 3. نوعهای جدیدی با عنوانهای <IAsyncEnumerable<T>,IAsyncEnumerator<T> در فضای نام System.Collections.Generic معرفی شدند. همانطور که مشخص است این نوعهای جدید کاملا با نوعهای synchronous خود هم پوشانی دارند و مفاهیم قبلی را به پیاده سازی میکنند.
نوع <IAsyncEnumerable<T متد GetAsyncEnumerator را معرفی میکند تا عملیات enumeration را به صورت async انجام دهد و در خروجی این متد، نوع <IAsyncEnumerator<T را برگشت میدهد؛ بهطوریکه این نوع disposable و دو عضو MoveNextAsync و Current را در خود دارد. اولی برای رسیدن به مقدار بعدی و دومی برای دریافت مقدار فعلی استفاده میشود. این در حالی است که MoveNextAsync بجای برگشت دادن یک bool یک <ValueTask<bool را برگشت میدهد. همچنین این متد، مقدار CancelationToken را همانند سایر فرآیندهایی که به صورت async تعریف میشوند، به صورت اختیاری از ورودی دریافت میکند، تا در صورت لزوم، عملیات جاری را کنسل کند. از طرفی به دلیل اینکه IAsyncEnumerator اینترفیس IAsyncDisposable را پیاده سازی میکند، متد DisposeAsync را نیز در اختیار دارد بهطوریکه بجای void یک ValueTask را برگشت میدهد.
static async IAsyncEnumerable<int> RangeAsync(int start, int count) { for (int i = 0; i < count; i++) { await Task.Delay(i); yield return start + i; } }
در مرحله اول، یک وب سرویس REST را بدون استفاده از IAsyncEnumerable ایجاد میکنیم تا متوجه مشکلات آن شویم و سپس در مرحله بعدی همین وب سرویس را با نوع IAsyncEnumerable بازنویسی میکنیم.
[ApiController] [Route("[controller]")] public class CustomerController : ControllerBase { private readonly IDictionary<int, Customer> _customers; private void FillCustomerFromMemory(int countOfCustomer) { for (int CustomerId = 1; CustomerId <= countOfCustomer; CustomerId++) { _customers.Add(key: CustomerId, new Customer($"name_{CustomerId}", CustomerId)); } } public CustomerController() { _customers = new Dictionary<int, Customer>(); FillCustomerFromMemory(countOfCustomer : 100); } [HttpGet] public async Task<IEnumerable<Customer>> Get() { var output = new List<Customer>(); while (_customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); output.Add(new Customer(customer.Value.Name, customer.Key)); await Task.Delay(500); _customers.Remove(customer); } return output; } public class Customer { public int Id { get; private set; } public string Name { get; private set; } public Customer(string name, int id) { Name = name; Id = id; } } }
[HttpGet] public async IAsyncEnumerable<Customer> Get() { while (_customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); yield return new Customer(customer.Value.Name, customer.Key); _customers.Remove(customer); await Task.Delay(500); } }
در قسمت قبل تلاش کردیم تا یک وب سرویس با قابلیت stream را پیاده سازی کنیم. حال در این بخش کد کلاینت را به صورتی ایجاد میکنیم تا هر سری صرفا یک بلاک ارسال شده توسط سرور را دریافت و آن را Deserialize کند. برای این کار از کتابخانه Newtonsoft.Json استفاده میکنیم.
const int TARGET = 80; var _httpClient = new HttpClient(); using (var response = await _httpClient.GetAsync( "https://localhost:7284/customer", HttpCompletionOption.ResponseHeadersRead)) { var stream = await response.Content.ReadAsStreamAsync(); var _jsonSerializerSettings = new JsonSerializerSettings(); var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings); using TextReader textReader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(textReader); await using (stream.ConfigureAwait(false)) { await jsonReader.ReadAsync().ConfigureAwait(false); while (await jsonReader.ReadAsync().ConfigureAwait(false) && jsonReader.TokenType != JsonToken.EndArray) { Customer customer = _serializer!.Deserialize<Customer>(jsonReader); if (customer.Id == TARGET) { Console.WriteLine(customer.Id + " : " + customer.Name); break; } } } }
استفاده از CancelationToken در جهت استفاده بهینه از منابع
فرض کنید به هر دلیلی، برای مثال خطای داخلی برنامهی کلاینت و یا بسته شدن مرورگر، ارتباط کلاینت با سرور قطع شود. در این صورت سرور از این ماجرا خبردار نمیشود و به کار خود جهت ارسال اطلاعات ادامه میدهد. همانطور که گفته شد، کلاینت به هر دلیلی از دریافت اطلاعات منصرف شده و یا به خطا خورده. پس فرستادن اطلاعات هیچ کاربردی ندارد و سرور در هر مرحله ای از ارسال که باشد، باید به کار خود خاتمه دهد.
برای برطرف کردن مشکل، این سناریو کد سمت سرور را مجدد باز نویسی میکنیم :
[HttpGet] public async IAsyncEnumerable<Customer> Get(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested && _customers.Any(_ => _.Key % 10 == 0)) { var customer = _customers.First(_ => _.Key % 10 == 0); yield return new Customer(customer.Value.Name, customer.Key); _customers.Remove(customer); await Task.Delay(500,cancellationToken); } }
کلاینت در صورتیکه به اطلاعات مورد نظر از طریق وب سرویس دسترسی پیدا کرد، دیگر تمایلی به ادامه خواندن از جریان داده یا stream را ندارد و از حلقه خواندن اطلاعات خارج میشود. اما سرور همچنان درگیر ارسال اطلاعات است. برای رفع این مشکل کد سمت کلاینت را بازنویسی میکنیم:
const int TARGET = 80; var _httpClient = new HttpClient(); var _cancelationTokenSource = new CancellationTokenSource(); using (var response = await _httpClient.GetAsync( "https://localhost:7284/customer", HttpCompletionOption.ResponseHeadersRead, _cancelationTokenSource.Token)) { var stream = await response.Content.ReadAsStreamAsync(_cancelationTokenSource.Token); var _jsonSerializerSettings = new JsonSerializerSettings(); var _serializer = Newtonsoft.Json.JsonSerializer.Create(_jsonSerializerSettings); using TextReader textReader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(textReader); await using (stream.ConfigureAwait(false)) { await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false); while (await jsonReader.ReadAsync(_cancelationTokenSource.Token).ConfigureAwait(false) && jsonReader.TokenType != JsonToken.EndArray) { Customer customer = _serializer!.Deserialize<Customer>(jsonReader); if (customer.Id == TARGET) { Console.WriteLine(customer.Id + " : " + customer.Name); _cancelationTokenSource.Cancel(); break; } } } }
https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8
https://code-maze.com/csharp-async-enumerable-yield
Github Link : https://github.com/Ershad95/Stream_REST_API