نظرات مطالب
نظرات مطالب
چک لیست امنیتی پروژه های نرم افزاری تحت وب
ظاهرا مشکلاتی هم دارد این روش .
این کتابخانه هم جالب بود
سلام، آیا میتوان از این کتابخانه خروجی اکسل گرفت؟
نظرات مطالب
EF Code First #12
ار کتابخانه ی EntityFramework.Extended استفاده کنید.
کتابخانه extended ef هم به نظر در این مورد جالب است.
در نگارش فعلی ASP.NET MVC، متد Html.RenderAction جهت تزریق رندر نهایی محتوای یک اکشن متد در یک View کاربرد دارد. مشکل آن شروع چرخهی طول عمر مجدد رندر کردن یک اکشن متد دیگر همزمان با رندر شدن اکشن متد جاری است. برای رفع آن View components معرفی شدهاند که دیگر چنین مشکلی را ندارند.
یکی از محصولات پرکاربرد و حرفه ای مایکروسافت در زمینه تولید گزارش SQL Server Reporting Services یا به اختصار SSRS میباشد . در این پست نحوه ایحاد یک گزارش ساده به صورت والد و فرزندی ، انتشار گزارش روی وب سرور و مدیریت نمایش ستونها با استفاده از Expressionها را در محیط BIDS بیان میکنم
برای انجام این پروسه ، از ابزار BIDS استفاده خواهیم کرد . همچنین برای اطلاعات و دادهها از دیتابیس آزمایشی Adventure works استفاده میکنیم (دانلود )
صورت مسئله : گزارش از دپارتمانهای کاری و تعداد کارمندان مرد و زن هر دپارتمان . که با کلیک روی هر سطر گزارش (بسته به جنسیت) بتوان لیست تمام افراد آن دپارتمان را دید .
بخش اول :
برای شروع BIDS را باز کرده و یک پروژه از نوع Report Server Project استارت بزنید .
نام این پروژه را در اینجا introductionPrj1 قرار دادم .
همانطور که ملاحظه میکنید ، سه پوشه در Solution explorer قرار دارد که برای تعریف پایگاه داده با پوشه Shared Data Sources و برای تعریف گزارشات از پوشه Reports استفاده خواهیم کرد
برای این منظور ابتدا یک data source تعریف میکنیم :
و سپس شروع به ساختن یک گزارش میکنیم :
مراحل را تا رسیدن به مرحله تعریف Query پی میگیریم .
انتخاب دیتا سورس :
در اینجا میخواهیم قسمت اول گزارش یعنی فهرست کردن تعداد کارمندان هر دپارتمان را به تفکیک جنسیت مشاهده کنیم :
SELECT DEPARTMENTNAME, GENDER, COUNT(1) AS COUNT FROM DBO.DIMEMPLOYEE GROUP BY DEPARTMENTNAME,GENDER ORDER BY DEPARTMENTNAME,GENDER
برای مشاهده صحت دستور میتوانید از Query Builder کمک بگیرید :
ادامه تنظیمات را مانند تصویر پی بگیرید (تعریف Tabular بودن گزارش و طراحی جدول و theme و نام گذاری گزارش )
به این ترتیب بخش اول گزارش ایجاد شد . حال باید زیر گزارش مربوطه را ایجاد کنیم :
مجددا مراحل را برای ساخت یک گزارش جدید پیگیری کنید و برای دستور کوئری از دستور زیر استفاده کنید :
SELECT EMPLOYEEKEY,FIRSTNAME,LASTNAME, MIDDLENAME,TITLE,HIREDATE, BIRTHDATE,EMAILADDRESS,PHONE,GENDER FROM DBO.DIMEMPLOYEE WHERE DEPARTMENTNAME=@DEPARTMENTNAME AND GENDER=@GENDER
و تست گزارش :
و بقیه قسمتها مانند قبل :
تا به این مرحله data source و گزارشها ایجاد شدند :
اکنون باید ارتباط بین دو گزارش را برقرار کنیم :
گزارش والد را باز کرده و روی ستون COUNT کلیک راست نموده و گزینه Popperties را انتخاب نمایید :
سپس در تب action گزینه Go to Report را انتخاب نموده و گزارش فرزند را انتخاب نمایید .
در انتها هم باید پارامترها را تعریف کنید . خروجی مانند زیر خواهد بود :
ToolTip از تب General قابل اعمال است .
و در نهایت با کلیک روی این سطر میتوانید گزارش مرتبط را مشاهده کنید :
تا به این مرحله گزارش تکمیل شد که البته برای ظاهر آن هم باید فکری کرد که در این پست اشاره ای نمیشود .
بخش دوم :
گزارش جاری فقط قابل استفاده از طریق BIDS است و با توجه به محدودیت دسترسی باید آن را در جایی قرار داد تا کاربران بتوانند از آن استفاده کننده . برای این منظور باید تنظیمات SSRS Web Application انجام شود تا بتواند روی سرور عملیاتی قرار گیرد .
در صورتی که تنظیمات SSRS برای قرار گرفتن روی وب سرور انجام نشده باشد و ما بخواهیم گزارش را Deploy کنیم خطا دریافت خواهیم کرد .
پس در ادامه نحوه تنظیم وب سرور را بیان میکنم و پس از آن گزارش را روی وب سرور قرار میدهیم :
برای این منظور باید برنامه Reporting Services Configuration Manager که در مسیر نصب SQL Server است برویم
پس از اتصال به سرور به تب Report Manager Url بروید :
در این مرحله باید سرور را تنظیم کنید تا بتوانیم پروژه را روی آن Deploy کنیم . از باز بودن پورت اطیمنان حاصل کنید . سپس وب سرویس را تنظیم کنید که هر دو فقط شامل نام Virtual Directory و Credential آن میشود . (مگر اینکه تنظیمات خاصی داشته باشید).
در صورت اجرا کردن مسیر URL باید بتوانید صفحه خانگی آن را مشاهده کنید :
که البته هنوز هیچ گزارشی روی آن قرار نگرفته است .سپس به گزارش خود باز میگردیم تا تنظیمات سرور را روی BIDS تکمیل کنیم :
برای این منظور روی پروژه کلیک راست کنید و ابتدا روی Properties کلیک کنید .
تنظمات مهم شامل مسیر سرور که برابر با همان سرویسی است که تنظیم کرده اید و مسیر پوشه ای که گزارشها روی سرور در آن قرار خواهند گرفت ، میباشد.دقت کنید که باید حتما پوشه موجود باشد . اگر به حالت پیشفرش رها کنید ، گزارشات در Root قرار خواهند گرفت .
روی OK کلیک کنید و پروژه را Deploy کنید .
اگر به سایت برگردید ، گزارشات را میتوانید مشاهده کنید
بخش سوم :
در این مرحله میخواهیم یکی از ویژگی هایی که در گزارش گیری کاربرد زیادی دارد ، یعنی نمایش ستونهای دلخواه در گزارش را به کمک SSRS و BIDS به کار ببریم.
برای این منظور به پنجره Report Data مراجعه کرده و روی Parameters کلی راست کرده و گزینه Add Parameter را انتخاب کنید :
فیلدها را مانند زیر پر کنید : (در اینجا میخواهیم یک dropdown به کاربر نشان داده شود تا انتخاب کند که ستون نمایش داده شود یا خیر . همچنین مقدار پیشفرض بله است)
گزینههای مورد نظر را انتخاب نمایید
و تعیین مقدار پیش فرض
و نتیجه در BIDS :
اکنون باید تغییر مقدار این ستون را بر روی گزارش اعمال کنیم :
برای همین منظور روی ستون BirthDate در حالت Design کلیک راست کرده و گزینه Column Visibility را انتخاب کنید :
سپس باید تنظیم کنیم که در نمایش و عدم نمایش این ستون باید بر اساس یک عبارت یا expression باشد .
عبارت زیر را وارد کنید (به این معنی که اگر مقدار 1 بود نمایش داده شود در غیر این صورت نمایش داده نشود) برای اطلاعات بیشتر در مورد دستورات Expression به اینجا و اینجا و اینجا مراجعه کنید
=iif(Parameters!ShowBirthDate.Value=1,true,false)
اکنون میتوانید گزارش را deploy کنید و تنظیمات سطوح دسترسی کاربران را انجام دهید
پروژهها
DotNetAuth
یک کتابخانه برای پیاده سازی مصرف کننده پروتوکل OAuth
درباره پروتوکل OAuth
برای اینکه کاربرد این کتابخانه مشخص شود بایستی ابتدا پروتوکل OAuth بحث شود، چون این کتابخانه صرفاً پیاده سازی بخشی از این پروتوکل است.
پروتوکل OAuth پروتوکلی است جهت به اشتراک گذاشتن اطلاعات کاربر با اجازهی خود کاربر به یک موجودیت سوم(دو موجودیت دیگر کاربر و شما که اطلاعات کاربر را دارید هستند). برای مثال سایت facebook اطلاعات کاربر را با اجازه کاربر در اختیار سایت شما قرار میدهد و مثلاً شما از طریق این پروتوکل لیست دوستان کاربر را در facebook بازیابی میکنید. البته تا دور نشدم بگم که این پروتوکل فقط در حد گرفتن اجازه کاربر و تفویض حقوق دسترسی به برنامههای کاربردی میباشد و بعد از آن دیگر در حوزه تعریف شده این پروتوکل نیست. مثلاً اینکه facebook چگونه اطلاعات لیست دوستان را ارائه میکند فرای تعاریف این پروتوکل است.
مطالب
WebStorage: قسمت دوم
در این مقاله قصد داریم نحوهی کدنویسی webstorage را با کتابخانههایی که در مقاله قبل معرفی کردیم بررسی کنیم.
ابتدا روش ذخیره سازی و بازیابی متداول آن را بررسی میکنیم که تنها توسط دو تابع صورت میگیرد. مطلب زیر برگرفته از w3Schools است:
دسترسی به شیء webstorage به صورت زیر امکان پذیر است:
window.localStorage window.sessionStorage
if(typeof(Storage) !== "undefined") { // Code for localStorage/sessionStorage. } else { // Sorry! No Web Storage support.. }
localStorage.setItem("lastname", "Smith"); //======================== localStorage.getItem("lastname");
var a=localStorage.lastname;
//ذخیره مقدار store.set('username', 'marcus') //بازیابی مقدار store.get('username') //حذف مقدار store.remove('username') //حذف تمامی مقادیر ذخیره شده store.clear() //ذخیره ساختار store.set('user', { name: 'marcus', likes: 'javascript' }) //بازیابی ساختار به شکل قبلی var user = store.get('user') alert(user.name + ' likes ' + user.likes) //تغییر مستقیم مقدار قبلی store.getAll().user.name == 'marcus' //بازخوانی تمام مقادیر ذخیر شده توسط یک حلقه store.forEach(function(key, val) { console.log(key, '==', val) })
همچنین بهتر هست از یک فلگ برای بررسی فعال بودن storage استفاده نمایید. به این دلیل که گاهی کاربرها از پنجرههای private استفاده میکنند که ردگیری اطلاعات آن ممکن نیست و موجب خطا میشود.
<script src="store.min.js"></script> <script> init() function init() { if (!store.enabled) { alert('Local storage is not supported by your browser. Please disable "Private Mode", or upgrade to a modern browser.') return } var user = store.get('user') // ... and so on ... } </script>
در صورتیکه بخشی از دادهها را توسط localstorage ذخیره نمایید و بخواهید از طریق storage به آن دسترسی داشته باشید، خروجی string خواهد بود؛ صرف نظر از اینکه شما عدد، شیء یا آرایهای را ذخیره کردهاید.
در صورتیکه ساختار JSON را ذخیره کرده باشید، میتوانید رشته برگردانده شده را با json.stringify و json.parse بازیابی و به روز رسانی کنید.
در حالت cross browser تهیهی یک sessionStorage امکان پذیر نیست. ولی میتوان به روش ذیل و تعیین یک زمان انقضاء آن را محدود کرد:
var storeWithExpiration = { // دریافت کلید و مقدار و زمان انقضا به میلی ثانیه set: function(key, val, exp) { //ایجاد زمان فعلی جهت ثبت تاریخ ایجاد store.set(key, { val:val, exp:exp, time:new Date().getTime() }) }, get: function(key) { var info = store.get(key) //در صورتی که کلید داده شده مقداری نداشته باشد نال را بر میگردانیم if (!info) { return null } //تاریخ فعلی را منهای تاریخ ثبت شده کرده و در صورتی که //از مقدار میلی ثاینه بیشتر باشد یعنی منقضی شده و نال بر میگرداند if (new Date().getTime() - info.time > info.exp) { return null } return info.val } } // استفاده عملی از کد بالا // استفاده از تایمر جهت نمایش واکشی دادهها قبل از نقضا و بعد از انقضا storeWithExpiration.set('foo', 'bar', 1000) setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 500) // -> "bar" setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 1500) // -> null
مورد بعدی استفاده از سورس cross-storage است. اگر به یاد داشته باشید گفتیم یکی از احتمالاتی که برای ما ایجاد مشکل میکند، ساب دومین هاست که ممکن است دسترسی ما به یک webstorage را از ساب دومین دیگر از ما بگیرد.
این کتابخانه به دو جز تقسیم شده است یکی هاب Hub و دیگری Client .
ابتدا نیاز است که هاب را آماده سازی و با ارائه یک الگو از آدرس وب، مجوز عملیات را دریافت کنیم. در صورتیکه این مرحله به فراموشی سپرده شود، انجام هر نوع عمل روی دیتاها در نظر گرفته نخواهد شد.
CrossStorageHub.init([ {origin: /\.example.com$/, allow: ['get']}, {origin: /:\/\/(www\.)?example.com$/, allow: ['get', 'set', 'del']} ]);
valid.example.com
ولی دامنه زیر را نامعتبر اعلام میکند:
invalid.example.com.malicious.com
{ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 'Access-Control-Allow-Headers': 'X-Requested-With', 'Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-WebKit-CSP': "default-src 'unsafe-inline' *", }
پس کار را بدین صورت آغاز میکنیم، یک فایل به نام hub.htm درست کنید و هاب را آماده سازید:
hub.htm
<script type="text/javascript" src="~/Scripts/cross-storage/hub.js"></script> <script> CrossStorageHub.init([ {origin: /.*localhost:300\d$/, allow: ['get', 'set', 'del']} ]); </script>
در فایل دیگر که کلاینت شناخته میشود باید فایل hub معرفی شود تا تنظیمات هاب خوانده شود:
var storage = new CrossStorageClient('http://localhost:3000/example/hub.html'); var setKeys = function () { return storage.set('key1', 'foo').then(function() { return storage.set('key2', 'bar'); }); };
نحوهی ذخیره سازی بدین شکل هم طبق مستندات صحیح است:
storage.onConnect().then(function() { return storage.set('key', {foo: 'bar'}); }).then(function() { return storage.set('expiringKey', 'foobar', 10000); });
برای خواندن دادههای ذخیره شده به نحوه زیر عمل میکنیم:
storage.onConnect().then(function() { return storage.get('key1'); }).then(function(res) { return storage.get('key1', 'key2', 'key3'); }).then(function(res) { // ... });
کد بالا نحوهی خواندن مقادیر را به شکلهای مختلفی نشان میدهد و مقدار بازگشتی آنها یک آرایه از مقادیر است؛ مگر اینکه تنها یک مقدار برگشت داده شود. مقدار بازگشتی در تابع بعدی به عنوان یک آرگومان در دسترس است. در صورتی که خطایی رخ دهد، قابلیت هندل آن نیز وجود دارد:
storage.onConnect() .then(function() { return storage.get('key1', 'key2'); }) .then(function(res) { console.log(res); // ['foo', 'bar'] })['catch'](function(err) { console.log(err); });
برای باقی مسائل چون به دست آوردن لیست کلیدهای ذخیره شده، حذف کلیدهای مشخص شده، پاکسازی کامل دادهها و ... به مستندات رجوع کنید.
در اینجا جهت سازگاری با مرورگرهای قدیمی خط زیر را به صفحه اضافه کنید:
<script src="https://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js"></script>
ذخیرهی اطلاعات به شکل یونیکد، فضایی دو برابر کدهای اسکی میبرد و با توجه به محدود بودن حجم webstorage به 5 مگابایت ممکن است با کمبود فضا مواجه شوید. در صورتیکه قصد فشرده سازی اطلاعات را دارید میتوانید از کتابخانه lz-string استفاده کنید. ولی توجه به این نکته ضروری است که در صورت نیاز، عمل فشرده سازی را انجام دهید و همینطوری از آن استفاده نکنید.
آخرین موردی که بررسی میشود استفاده از IndexedDB API است که با استفاده از آن میتوان با webstorage همانند یک دیتابیس رفتار کرد و به سمت آن کوئری ارسال کرد.
var request = indexedDB.open("library"); request.onupgradeneeded = function() { // The database did not previously exist, so create object stores and indexes. var db = request.result; var store = db.createObjectStore("books", {keyPath: "isbn"}); var titleIndex = store.createIndex("by_title", "title", {unique: true}); var authorIndex = store.createIndex("by_author", "author"); // Populate with initial data. store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); }; request.onsuccess = function() { db = request.result; };
برای انجام عملیات خواندن و نوشتن باید از تراکنشها استفاده کرد:
var tx = db.transaction("books", "readwrite"); var store = tx.objectStore("books"); store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); tx.oncomplete = function() { // All requests have succeeded and the transaction has committed. };
var tx = db.transaction("books", "readonly"); var store = tx.objectStore("books"); var index = store.index("by_author"); var request = index.openCursor(IDBKeyRange.only("Fred")); request.onsuccess = function() { var cursor = request.result; if (cursor) { // Called for each matching record. report(cursor.value.isbn, cursor.value.title, cursor.value.author); cursor.continue(); } else { // No more matching records. report(null); } };
کدهای بالا همه در مستندات معرفی شده وجود دارند و ما پیشتر توضیح ابتدایی در مورد آن دادیم و برای کسب اطلاعات بیشتر میتوانید به همان مستندات معرفی شده رجوع کنید. برای idexedDB هم میتوانید از این منابع + + + استفاده کنید که خود w3c منبع فوق العادهتری است.
نظرات مطالب
C# 8.0 - Async Streams
یک نکتهی تکمیلی: استفاده از IAsyncEnumerable در جهت ایجاد وب سرویسهای REST با قابلیت Stream
مقدمه
در 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 را برگشت میدهد.
برای استفاده از این نوع در نهایت باید از عبارت yield return استفاده کرد. تا مقدار برگشتی مشخص شده در IAsyncEnumerable که در این مثال int است برگشت داده شود. در صورت استفاده نشدن از yield، خطای cannot return value from an iterator داده میشود.
ایجاد یک وب سرویس بدون خروجی IAsyncEnumerable
در مرحله اول، یک وب سرویس REST را بدون استفاده از IAsyncEnumerable ایجاد میکنیم تا متوجه مشکلات آن شویم و سپس در مرحله بعدی همین وب سرویس را با نوع IAsyncEnumerable بازنویسی میکنیم.
ایجاد یک وب سرویس با خروجی IAsyncEnumerable
این بار به محض اینکه یک دیتا ساخته شد، برگشت داده میشود و منتظر تمام دیتا نیستیم. این برگه برنده استفاده از IAsyncEnumerable , yield return است چرا که با ترکیب این دو میتوان وب سرویسی با قابلیت stream را ایجاد کرد. از طرفی حجم payload نیز کمتر شدهاست، چرا که هر بار صرفا یک بلاک مشخص از دیتا را به کلاینت ارسال میکنیم.
پیاده سازی سمت کلاینت
در قسمت قبل تلاش کردیم تا یک وب سرویس با قابلیت stream را پیاده سازی کنیم. حال در این بخش کد کلاینت را به صورتی ایجاد میکنیم تا هر سری صرفا یک بلاک ارسال شده توسط سرور را دریافت و آن را Deserialize کند. برای این کار از کتابخانه Newtonsoft.Json استفاده میکنیم.
همانطورکه در کد بالا مشخص است، ابتدا یک درخواست Get را به آدرس وب سرویس زده و برای اینکه متجوجه شویم به انتهای لیست دادهها رسیدیم از jsonReader.TokenType != JsonToken.EndArray استفاده میکنیم. با این کار در صورتی که به ] نرسیده باشیم، باید عملیات خواندن از stream ادامه داشته باشد و هر سری بلاک جاری را Deserialize میکنیم و در آخر در صورتیکه آیتم مورد نظر را دریافت کردیم، با دستور break از حلقه دریافت بلاکها خارج میشویم.
استفاده از CancelationToken در جهت استفاده بهینه از منابع
سناریو اول - قطع کردن ارتباط توسط کلاینت
فرض کنید به هر دلیلی، برای مثال خطای داخلی برنامهی کلاینت و یا بسته شدن مرورگر، ارتباط کلاینت با سرور قطع شود. در این صورت سرور از این ماجرا خبردار نمیشود و به کار خود جهت ارسال اطلاعات ادامه میدهد. همانطور که گفته شد، کلاینت به هر دلیلی از دریافت اطلاعات منصرف شده و یا به خطا خورده. پس فرستادن اطلاعات هیچ کاربردی ندارد و سرور در هر مرحله ای از ارسال که باشد، باید به کار خود خاتمه دهد.
برای برطرف کردن مشکل، این سناریو کد سمت سرور را مجدد باز نویسی میکنیم :
در کد بالا صرفا یک CancelationToken به ورودی متد اضافه شده و از آن در جهت اطمینان از اتصال کلاینت استفاده شده، به طوری که در حلقه اصلی ارسال اطلاعات شرط cancellationToken.IsCancellationRequested را چک میکند تا کاربر به دلایل مختلفی از دریافت اطلاعات منصرف نشده باشد و در صورت لغو کاربر، سرور به کار خود خاتمه میدهد
کلاینت در صورتیکه به اطلاعات مورد نظر از طریق وب سرویس دسترسی پیدا کرد، دیگر تمایلی به ادامه خواندن از جریان داده یا stream را ندارد و از حلقه خواندن اطلاعات خارج میشود. اما سرور همچنان درگیر ارسال اطلاعات است. برای رفع این مشکل کد سمت کلاینت را بازنویسی میکنیم:
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
مقدمه
در 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 را برگشت میدهد.
نحوه استفاده از IAsyncEnumerable
static async IAsyncEnumerable<int> RangeAsync(int start, int count) { for (int i = 0; i < count; i++) { await Task.Delay(i); yield return start + i; } }
پیاده سازی سمت سرور
در قسمت قبل سعی بر این بود تا با این نوع جدید آشنا شویم. در این قسمت تلاش میکنیم تا با استفاده از این نوع یک وب سرویس stream را ایجاد کنیم .
در مرحله اول، یک وب سرویس 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; } } }
در صورت اجرای این تکه کد و فراخوانی وب سرویس موجود بعد از بارگذاری کامل دیتا، خروجی به کاربر برگشت داده میشود. این در حالی است که ممکن است کاربر فقط به بخشی از این دیتا نیاز داشته باشد؛ برای مثال شاید صرفا به 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 پیاده سازی کردیم. در قسمت بعدی سمت کلاینت را نیز پیاده سازی میکنیم تا دیتا را همانطور که سرور، قسمت به قسمت ارسال میکند، کلاینت نیز آن را به شکل تک قسمتی دریافت کند.
در قسمت قبل تلاش کردیم تا یک وب سرویس با قابلیت 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 در جهت استفاده بهینه از منابع
تا اینجا به هدفی که انتظار داشتیم رسیدیم؛ به این شکل که یک وب سرویس را ایجاد کردیم تا اطلاعات را به صورت بخش بخش ارسال کند و کلاینتی ساختیم تا این اطلاعات را دریافت کند و در صورتیکه اطلاعات مورد نظر را دریافت کرد، به کار خواندن از وب سرویس خاتمه دهد. برای اینکه متوجه اهمیت CanclationToken شویم دو سناریو زیر را با هم بررسی میکنیم :
فرض کنید به هر دلیلی، برای مثال خطای داخلی برنامهی کلاینت و یا بسته شدن مرورگر، ارتباط کلاینت با سرور قطع شود. در این صورت سرور از این ماجرا خبردار نمیشود و به کار خود جهت ارسال اطلاعات ادامه میدهد. همانطور که گفته شد، کلاینت به هر دلیلی از دریافت اطلاعات منصرف شده و یا به خطا خورده. پس فرستادن اطلاعات هیچ کاربردی ندارد و سرور در هر مرحله ای از ارسال که باشد، باید به کار خود خاتمه دهد.
برای برطرف کردن مشکل، این سناریو کد سمت سرور را مجدد باز نویسی میکنیم :
[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