زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حلهای جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که بهدنبال کمترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم نحوهی نوشتن منطقهای موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ها شاید قدمی مهم در مورد بهینه کردن کدها باشد؛ چرا استفاده درست و بجای این دو مورد میتواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.
Task<T> چیست؟
Task یک کلاس در فضای نام System.Threading.Tasks است؛ بهطوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. بهبیان دیگر میتواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحلهی اجرایی به مرحلهای دیگر منتقل میکند. همچنین هر Task میتواند یک مقدار برگشتی نیز داشته باشد.
این درحالیاست که میتواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. بهعبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از Task<int> و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی دادههای قدیمی به جدید را انجام میدهد، میتواند از نوع Task باشد.
همانطور که اشاره شد، Task یک کلاس است که شامل متدها و فیلدهای مختلفی میباشد. با استفاده از این اعضا میتوان نحوهی اجرای کدها و وضعیتهای مختلف اجرای آن را مدیریت کرد، تا در نهایت اجرای آن کامل شود.
به دلیل اینکه Task یک class است و class ها از نوع ReferenceType میباشند، روی حافظهی Heap ذخیره میشوند و بهازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.
نحوه استفاده از Task<T>
برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :
public static class DummyWeatherProvider { public static async Task<Weather> Get(string city) { await Task.Delay(10); var weather = new Weather { City = city, Date = DateTime.Now, AvgTempratureF = new Random().Next(5, 70) }; return weather; } }
static async Task CheckTaskStatus() { var task = DummyWeatherProvider.Get("Stockholm"); LogTaskStatus(task.Status); await task; LogTaskStatus(task.Status); } static void LogTaskStatus(TaskStatus status) { Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}"); }
ValueTask<T> چیست؟
همانند Task ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ها از نوع struct هستند. بهطوریکه نحوهی ذخیره سازی آنها در حافظه به نسبت class ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل میکنند. برای درک بهتر عملکرد ValueTask ها کد زیر را بررسی میکنیم :
public class WeatherService { private readonly ConcurrentDictionary<string, Weather> _cache; public WeatherService() { _cache = new(); } public async Task<Weather> GetWeatherTask(string city) { if (!_cache.ContainsKey(city)) { var weather = await DummyWeatherProvider.Get(city); _cache.TryAdd(city, weather); } return _cache[city]; } public async ValueTask<Weather> GetWeatherValueTask(string city) { if (!_cache.ContainsKey(city)) { var weather = await DummyWeatherProvider.Get(city); _cache.TryAdd(city, weather); } return _cache[city]; }
کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده میکنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد GetWeatherTask و GetWeatherValueTask کاملا شبیه به هم است؛ بهطوریکه اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده میشود؛ در غیر این صورت DummyWeatherProvider.Get() فراخوانی خواهد شد.
در قدم بعدی اطلاعات بهدست آمده را در _cache ذخیره میکنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن میباشد؛ یکی از Taskو دیگری از ValueTask استفاده میکند.
برای مقایسهی مصرف حافظهی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کدهای زیر داریم :
[MemoryDiagnoser] public class TaskAndValueTaskBenchmark { private readonly WeatherService _weatherService; public TaskAndValueTaskBenchmark() { _weatherService = new(); } [Benchmark] [Arguments("Denver")] public async Task<Weather> TaskBenchmark(string city) { return await _weatherService.GetWeatherTask(city); } [Benchmark] [Arguments("London")] public async ValueTask<Weather> ValueTaskBenchmark(string city) { return await _weatherService.GetWeatherValueTask(city); } }
نتیجه به دست آمده به شرح زیر است :
Allocated | Gen0 | Method |
144 B | 0.0229 | TaskBenchmark |
------ | ---- | ValueTaskBenchmark |
مزیت ValueTask<T>
بهدلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره میشوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف میشوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش میدهد .
علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا میشوند، در این مواقع استفاده از ValueTask، بهتر از Task میباشد .
این سری متد GetWeatherValueTask
را جهت تشخص اینکه اغلب کدها به صورت sync یا async اجرا میشوند، بررسی میکنیم. در
متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا میشود و اگر شهر وجود
نداشته باشد، کار به صورت async اجرا میشود. با بررسی دقیقتر متوجه میشویم اکثر مواقع در این متد کار به صورت sync
اجرا میشود؛ چرا که بعد ازدریافت
اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان _cache ) .
محدودیتهای استفاده از ValueTask<T>
1. در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await میکنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.
2. اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).
3. نمیتوانیم به یک ValueTask به صورت هم زمان در حالت Multi threads دسترسی داشته باشیم.
4. به صورت پیش فرض خروجی عملیات async، نوع Task میباشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.
منابع :
در این بلاگ میتوان:
- یک مطلب جدید را ارسال کرد.
- مطالب قابل ویرایش و یا حذف هستند.
- مطالب بلاگ قسمت ارسال نظرات دارند.
- امکان گزارشگیری از آخرین نظرات ارسالی وجود دارد.
- سایت صفحات درباره و تماس با ما را نیز دارا است.
ساختار پوشههای برنامه
در تصویر ذیل، ساختار پوشههای برنامه بلاگ را ملاحظه میکنید. چون قسمت سمت کلاینت این برنامه کاملا جاوا اسکریپتی است، پوشههای App، Controllers، Libs، Models، Routes و Templates آن در پوشهی Scripts تعریف شدهاند و به این ترتیب میتوان تفکیک بهتری را بین اجزای تشکیل دهندهی یک برنامهی تک صفحهای وب Emeber.js پدید آورد.
فایل CSS بوت استرپ نیز به پوشهی Content اضافه شدهاست.
دریافت پیشنیازهای سمت کاربر برنامه
در ساختار پوشههای فوق، از پوشهی Libs برای قرار دادن کتابخانههای پایه برنامه مانند jQuery و Ember.js استفاده خواهیم کرد. به این ترتیب:
- نیاز به آخرین نگارشهای Ember.js و همچنین افزونهی Ember-Data آن برای کار سادهتر با دادهها و سرور وجود دارد. این فایلها را از آدرس ذیل میتوانید دریافت کنید (نسخههای نیوگت به دلیل قدیمی بودن و به روز نشدن مداوم آنها توصیه نمیشوند):
http://emberjs.com/builds/#/beta
برای حالت آزمایش برنامه، استفاده از فایلهای دیباگ آن توصیه میشوند (فایلهایی با نام اصلی و بدون پسوند prod یا min). زیرا این فایلها خطاها و اطلاعات بسیار مفصلی را از اشکالات رخ داده، در کنسول وب مرورگرها، فایرباگ و یا Developer tools آنها نمایش میدهند. نسخهی min برای حالت ارائهی نهایی برنامه است. نسخهی prod همان نسخهی دیباگ است با حذف اطلاعات دیباگ (نسخهی production فشرده نشده). نسخهی فشرده شدهی prod آن، فایل min نهایی را تشکیل میدهد.
- دریافت جیکوئری
- آخرین نگارش handlebars.js را از سایت رسمی آن دریافت کنید: http://handlebarsjs.com
- Ember Handlebars Loader: https://github.com/michaelrkn/ember-handlebars-loader
- Ember Data Local Storage Adapter: https://github.com/kurko/ember-localstorage-adapter
ترتیب تعریف و قرارگیری این فایلها را پس از دریافت، در فایل index.html قرار گرفته در ریشهی سایت، در کدهای ذیل مشاهده میکنید:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Ember Blog</title> <link href="Content/bootstrap.css" rel="stylesheet" /> <link href="Content/bootstrap-theme.css" rel="stylesheet" /> <link href="Content/styles.css" rel="stylesheet" /> <script src="Scripts/Libs/jquery-2.1.1.min.js" type="text/javascript"></script> <script src="Scripts/Libs/bootstrap.min.js" type="text/javascript"></script> <script src="Scripts/Libs/handlebars-v2.0.0.js" type="text/javascript"></script> <script src="Scripts/Libs/ember.js" type="text/javascript"></script> <script src="Scripts/Libs/ember-handlebars-loader-0.0.1.js" type="text/javascript"></script> <script src="Scripts/Libs/ember-data.js" type="text/javascript"></script> <script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script> </head> <body> </body> </html>
اصلاح فایل ember-handlebars-loader-0.0.1.js
اگر به فایل ember-handlebars-loader-0.0.1.js مراجعه کنید، مسیر فایلهای قالب handlebars قسمتهای مختلف برنامه را از پوشهی templates واقع در ریشهی سایت میخواند. با توجه به تصویر ساختار پوشهی پروژهی جاری، پوشهی template به داخل پوشهی Scripts منتقل شدهاست و نیاز به یک چنین اصلاحی دارد:
url: "Scripts/Templates/" + name + ".hbs",
<system.webServer> <staticContent> <mimeMap fileExtension=".hbs" mimeType="text/x-handlebars-template" /> </staticContent> </system.webServer>
مزیت استفاده از نسخهی دیباگ ember.js در حین توسعهی برنامه
نسخهی دیباگ ember.js علاوه بر به همراه داشتن خطاهای بسیار جامع و توضیح علل مشکلات، مواردی را که در آینده منسوخ خواهند شد نیز توضیح میدهد:
برای مثال در اینجا عنوان شدهاست که دیگر از linkTo استفاده نکنید و آنرا به link-to تغییر دهید.
همچنین اگر از مرورگر کروم استفاده میکنید، افزونهی Ember Inspector را نیز میتوانید نصب کنید تا اطلاعات بیشتری را از جزئیات مسیریابیهای تعریف شده و قالبهای Ember.js بتوان مشاهده کرد. این افزونه به صورت یک برگهی جدید در Developer tools آن ظاهر میشود.
ایجاد شیء Application
همانطور که در قسمتهای پیشین نیز عنوان شد (^ و ^ )، یک برنامهی Ember.js با تعریف وهلهای از شیء Application آن آغاز میشود. برای این منظور به پوشهی App مراجعه کرده و فایل جدید Scripts\App\blogger.js را اضافه کنید؛ با این محتوا:
Blogger = Ember.Application.create();
<script src="Scripts/App/blogger.js" type="text/javascript"></script>
تعاریف مسیریابی و قالبها
اکنون در ادامه قصد داریم لیستی از عناوین مطالب ارسالی را نمایش دهیم. در ابتدا این عناوین را از یک آرایهی ثابت جاوا اسکریپتی دریافت خواهیم کرد و پس از آن از یک Web API کنترلر، جهت دریافت اطلاعات از سرور کمک خواهیم گرفت.
کار router در Ember.js، نگاشت آدرس درخواستی توسط کاربر، به یک route یا مسیریابی تعریف شدهاست. به این ترتیب مدل، کنترلر و قالب آن route به صورت خودکار بارگذاری و پردازش خواهند.
با مراجعه به ریشهی سایت، فایل index.html بارگذاری میشود.
در اینجا تصویری از صفحهی آغازین بلاگ را مشاهده میکنید. در آن صفحهی posts همان ریشهی سایت نیز میباشد. بنابراین نیاز است ابتدا مسیریابی آنرا تعریف کرد. برای این منظور، فایل جدید Scripts\App\router.js را به پوشهی App اضافه کنید؛ با این محتوا:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); });
همچنین مدخل آنرا نیز در فایل index.html تعریف نمائید:
<script src="Scripts/App/blogger.js" type="text/javascript"></script> <script src="Scripts/App/router.js" type="text/javascript"></script>
افزودن مسیریابی و قالب posts
در ادامه فایل جدید Scripts\Templates\posts.hbs را اضافه کنید. به این ترتیب قالب خالی مطالب به پوشهی templates اضافه میشود. محتوای این فایل را به نحو ذیل تنظیم کنید:
<div class="container"> <h1>Emeber.js blog</h1> <ul> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </ul> </div>
برای بارگذاری این قالب نیاز است آنرا به template loader توضیح داده شده در ابتدای بحث، در فایل index.html اضافه کنیم:
<script> EmberHandlebarsLoader.loadTemplates([ 'posts' ]); </script>
افزودن مسیریابی و قالب about
در ادامه قصد داریم صفحهی about را اضافه کنیم. مجددا با افزودن مسیریابی آن به فایل Scripts\App\router.js شروع میکنیم:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); });
<h1>About Ember Blog</h1> <p>Bla bla bla!</p>
<script> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about' ]); </script>
<script type="text/x-handlebars" data-template-name="about"> </script>
برای آزمایش این مسیر و قالب جدید آن، آدرس http://localhost/#/about را بررسی کنید.
اضافه کردن منوی ثابت بالای سایت
روش اول این است که به ابتدای هر دو قالب about.hbs و posts.hbs، تعاریف منو را اضافه کنیم. مشکل اینکار، تکرار کدها و پایین آمدن قابلیت نگهداری برنامه است. روش بهتر، افزودن کدهای مشترک بین صفحات، در قالب application برنامه است. نمونهی آنرا در مثال قسمت قبل مشاهده کردهاید. در اینجا چون قصد نداریم به صورت دستی قالبها را به صفحه اضافه کنیم و همچنین برای ساده شدن نگهداری برنامه، قالبها را در فایلهای مجزایی قرار دادهایم، تنها کافی است فایل جدید Scripts\Templates\application.hbs را به پوشهی قالبهای برنامه اضافه کنیم؛ با این محتوا:
<div class='container'> <nav class='navbar navbar-default' role='navigation'> <ul class='nav navbar-nav'> <li>{{#link-to 'posts'}}Posts{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li> </ul> </nav> {{outlet}} </div>
<script> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application' ]); </script>
افزودن مسیریابی و قالب contact
برای افزودن صفحهی تماس با مای سایت، ابتدا مسیریابی آنرا در فایل Scripts\App\router.js تعریف میکنیم:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact'); });
<h1>Contact</h1> <ul> <li>Phone: ...</li> <li>Email: ...</li> </ul>
<script> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact' ]); </script>
<div class='container'> <nav class='navbar navbar-default' role='navigation'> <ul class='nav navbar-nav'> <li>{{#link-to 'posts'}}Posts{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li> <li>{{#link-to 'contact'}}Contact{{/link-to}}</li> </ul> </nav> {{outlet}} </div>
تعریف مسیریابی تو در تو در صفحهی contact
در حال حاضر صفحهی Contact، ایمیل و شماره تماس را در همان بار اول نمایش میدهد. میخواهیم این دو را تبدیل به دو لینک جدید کنیم که با کلیک بر روی هر کدام، محتوای مرتبط، در قسمتی از همان صفحه بارگذاری شده و نمایش داده شود.
برای اینکار نیاز است مسیریابی را تو در تو تعریف کنیم:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); });
پس از آن دو فایل قالب جدید Scripts\Templates\email.hbs را با محتوای:
<h2>Email</h2> <p> <span></span> Email name@site.com. </p>
<h2>Phone</h2> <p> <span></span> Call 12345678. </p>
<script> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone' ]); </script>
<h1>Contact</h1> <div class="row"> <div class="col-md-6"> <p> Want to get in touch? <ul> <li>{{#link-to 'phone'}}Phone{{/link-to}}</li> <li>{{#link-to 'email'}}Email{{/link-to}}</li> </ul> </p> </div> <div class="col-md-6"> {{outlet}} </div> </div>
در اینجا نحوهی پردازش مسیریابی contact را ملاحظه میکنید. ابتدا درخواستی جهت مشاهدهی آدرس http://localhost/#/contact دریافت میشود. سپس router این درخواست را به مسیریابی همنامی منتقل میکند. این مسیریابی ابتدا قالب عمومی application را رندر کرده و سپس قالب اصلی و همنام مسیریابی جاری یا همان contact.hbs را رندر میکند. در این صفحه چون مسیریابی تو در تویی تعریف شدهاست، اگر درخواستی برای مشاهدهی http://localhost/#/contact/phone دریافت شود، محتوای آنرا در {{outlet}} قالب contact.hbs جاری رندر میکند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_01.zip
سوال
یک جدول ایجاد کنید با نام Image که بین جدول محصولات و جدول Image یک ارتباط یک به چند وجود داره
در مورد خرید شما میتونید برای پیاده سازی سبد خرید از کوکی استفاده کنید که کاربر بدون عضویت هم بتونه محصولات رو به سبد خرید اضافه کنه ولی موقع پرداخت نهایی کاربر حتما باید عضو سایت باشه(یا لاگین کنه یا ثبت نام)
1- اندازه گیری تعداد Transactionها در واحد زمان روی یک Database خاص در SQL Server
جهت بدست آوردن تعداد Transactionها در واحد زمان( Transactions Per Second ) روی یک Database خاص در یک سیستم عملیاتی، جهت ارتقاء سخت افزاری ، تست فشار و ... میتوانید از یک DMV با نام sys.dm_os_performance_counters به طریق زیر استفاده نمائید:declare @cntr_value bigint Select @cntr_value=cntr_value from sys.dm_os_performance_counters where instance_name='AdventureWorks' and counter_name='Write Transactions/sec' /* ایجاد یک تاخیر مثلاً یک ثانیه */ waitfor delay '00:00:01' Select cntr_value -@cntr_value from sys.dm_os_performance_counters where instance_name='AdventureWorks' and counter_name='Write Transactions/sec'
2- sys.sp_MSforeachtable
از رویههای ذخیره شده UnDocumented در SQL Server میباشد و این قابلیت را دارا است که برای هر یک از جداول موجود در یک بانک اطلاعاتی، یک رویهای را اجرا کند. برای مثال با استفاده از دستور زیر، میتوانید تعداد سطرها، اندازهی دادهها و ایندکسهای یک جدول را بدست آورید
EXEC sys.sp_MSforeachtable 'sp_spaceused ''?''';
USE [AdventureWorksDW2008R2] GO CREATE TABLE #TableSpaceUsed( [name] [nvarchar](120) NULL, [rows] [nvarchar](120) NULL, [reserved] [nvarchar](120) NULL, [data] [nvarchar](120) NULL, [index_size] [nvarchar](120) NULL, [unused] [nvarchar](120) NULL ) ON [PRIMARY] Insert Into #TableSpaceUsed EXEC sys.sp_MSforeachtable 'sp_spaceused ''?'''; Select * from #TableSpaceUsed Order by CAST([rows] as int) desc Drop table #TableSpaceUsed
- آدرسی که کار میکند، یعنی جزو مسیریابی سیستم ثبت شدهاست.
- برای فعال سازی CORS این مراحل باید طی شوند:
در فایل WebApiConfig.cs، در ابتدای متد Register این کدها باید اضافه شوند:
var cors = new EnableCorsAttribute("*", "*", "*"); config.EnableCors(cors); config.MessageHandlers.Add(new PreflightRequestsHandler());
public class PreflightRequestsHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Headers.Contains("Origin") && request.Method.Method == "OPTIONS") { var response = new HttpResponseMessage {StatusCode = HttpStatusCode.OK}; response.Headers.Add("Access-Control-Allow-Origin", "*"); response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); response.Headers.Add("Access-Control-Allow-Methods", "*"); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } return base.SendAsync(request, cancellationToken); } }
jQuery.support.cors = true;
بررسی Claims Transformations
میخواهیم Claims بازگشت داده شدهی توسط IDP را به یکسری Claims که کار کردن با آنها در برنامهی MVC Client سادهتر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میانافزار oidc دریافت میشود، ابتدا بررسی میشود که آیا دیکشنری نگاشتها وجود دارد یا خیر؟ اگر بله، کار نگاشتها از یک claim type به claim type دیگر انجام میشود.
برای مثال لیست claims اصلی بازگشت داده شدهی توسط IDP، پس از تبدیلات و نگاشتهای آن در برنامهی کلاینت، یک چنین شکلی را پیدا میکند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1 Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd Claim type: given_name - Claim value: Vahid Claim type: family_name - Claim value: N
namespace ImageGallery.MvcClient.WebApp { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); }
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شدهاست:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb • Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: idp - Claim value: local • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
کار با مجموعهی User Claims
تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آنها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider میباشد. حذف آنها حجم کوکی نگهداری کنندهی آنها را کاهش میدهد. همچنین میخواهیم تعدادی دیگر را نیز به آنها اضافه کنیم.
علاوه بر اینها میانافزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف میکند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آنها نیازی نداریم. اما میتوان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن میتوان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.ClaimActions.Remove("amr"); options.ClaimActions.DeleteClaim("sid"); options.ClaimActions.DeleteClaim("idp"); }); }
اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیشفرض حذف نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
افزودن Claim جدید آدرس کاربر
برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace DNT.IDP { public static class Config { // identity-related resources (scopes) public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Address() }; }
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address },
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Main Road 1") } }, new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Big Street 2") } } }; }
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه میکنیم تا درخواست این claim را به لیست scopes میانافزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.Scope.Add("address"); // … options.ClaimActions.DeleteClaim("address"); }); }
یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و میشود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میانافزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت میکند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از اینها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمیخواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.
دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint
همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامهی کلاینت» عنوان شد، میانافزار oidc با UserInfo Endpoint کار میکند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچکتر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر میشود.
درخواستی که به سمت UserInfo Endpoint ارسال میشود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo Authorization: Bearer R9aty5OPlk
اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بستهی نیوگت IdentityModel را به پروژهی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { public async Task<IActionResult> OrderFrame() { var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]); var metaDataResponse = await discoveryClient.GetAsync(); var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint); var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var response = await userInfoClient.GetAsync(accessToken); if (response.IsError) { throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception); } var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value; return View(new OrderFrameViewModel(address)); }
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels { public class OrderFrameViewModel { public string Address { get; } = string.Empty; public OrderFrameViewModel(string address) { Address = address; } } }
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel <div class="container"> <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div> <div class="text bottomMarginSmall">We've got this address on record for you:</div> <div class="text text-info bottomMarginSmall">@Model.Address</div> <div class="text">If this isn't correct, please contact us.</div> </div>
سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه میکنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:
همانطور که ملاحظه میکنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شدهاست.
بررسی Authorization مبتنی بر نقشها
تا اینجا مرحلهی Authentication را که مشخص میکند کاربر وار شدهی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج میشود. مرحلهی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی میباشد. به این مرحله Authorization میگویند و روشهای مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقشهای کاربر
در این حالت claim ویژهی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص میشوند. برای مثال کاربر وارد شدهی به سیستم میتواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شدهی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آنرا نسبت به حالت الف ترجیح میدهند که آنرا در قسمتهای بعدی بررسی خواهیم کرد.
در اینجا روش «تعیین سطوح دسترسی بر اساس نقشهای کاربر» را بررسی میکنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف میکنیم:
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "PayingUser") } }, new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "FreeUser") } } }; }
سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP { public static class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { // ... new IdentityResource( name: "roles", displayName: "Your role(s)", claimTypes: new List<string>() { "role" }) }; }
همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آنرا به AllowedScopes مشخصات Client نیز اضافه میکنیم:
namespace DNT.IDP { public static class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { // ... AllowedScopes = { // ... "roles" } // ... } }; }
در ادامه قرار است تنها کاربری که دارای نقش PayingUser است، امکان دسترسی به سفارش نگارش قاب شدهی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی کلاینت مراجعه کرده و درخواست scope نقشهای کاربر را به متد تنظیمات AddOpenIdConnect اضافه میکنیم:
options.Scope.Add("roles");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحهی consent، از کاربر مجوز دسترسی به نقشهای او نیز سؤال پرسیده میشود:
اما اگر به لیست موجود در this.User.Claims در برنامهی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
همانطور که در نکتهای پیشتر نیز ذکر شد، چون role جزو لیست نگاشتهای OpenID Connect Options مایکروسافت نیست، آنرا به صورت خودکار به لیست claims اضافه نمیکند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، میتوان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N • Claim type: role - Claim value: PayingUser
استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده
در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.GivenName, RoleClaimType = JwtClaimTypes.Role, };
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شدهی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.
البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کردهایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود میکنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public async Task<IActionResult> OrderFrame() {
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:
همانطور که مشاهده میکنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحهی Account/AccessDenied که هنوز در برنامهی کلاینت تعریف نشدهاست، هدایت شدهایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شدهاست.
برای تغییر مقدار پیشفرض صفحهی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد میکنیم:
using Microsoft.AspNetCore.Mvc; public class AuthorizationController : Controller { public IActionResult AccessDenied() { return View(); } }
<div class="container"> <div class="h3">Woops, looks like you're not authorized to view this page.</div> <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div> </div>
اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { // ... }).AddCookie("Cookies", options => { options.AccessDeniedPath = "/Authorization/AccessDenied"; }) // ...
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شدهاست نمایش داده خواهد شد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup>
این ایده نیز از npm و ابزارهای محلی و مختص به یک پروژهی آن گرفته شدهاست. اما npm امکان نصب این ابزارها را به صورت سراسری نیز دارد که امکان وجود linters ، test runners و یا development web servers را میسر کردهاست و در این حالت نیازی نیست یک چنین ابزارهایی را به ازای هر پروژه نیز یکبار نصب کرد.
معرفی ابزارهای سراسری در NET Core 2.1.
اگر SDK جدید NET Core 2.1 را نصب کرده باشید، پس از Build یک پروژهی مبتنی بر NET Core 2.0. (که توسط فایل global.json، شماره SDK آن محدود و مقید نشدهاست) یک چنین پیامهای اخطاری را مشاهده خواهید کرد:
warning : Using DotNetCliToolReference to reference 'Microsoft.EntityFrameworkCore.Tools.DotNet' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK. warning : Using DotNetCliToolReference to reference 'Microsoft.DotNet.Watcher.Tools' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK.
پیامهای اخطار فوق نیز به این معنا هستند که دیگر نیازی نیست تا برای اجرای دستور dotnet watch run، حتما ابزار پروژهی Microsoft.DotNet.Watcher.Tools را به صورت دستی به تمام فایلهای csproj خود اضافه کنید. ابزار Watcher و یا EntityFrameworkCore.Tools اکنون جزو ابزارهای سراسری NET Core 2.1. هستند و بدون نیازی به افزودن ارجاع خاصی به آنها، هم اکنون در تمام پروژههای NET Core. شما قابل دسترسی و استفاده هستند. بنابراین ارجاعات مستقیم به آنها را حذف کنید؛ چون غیرضروری میباشند.
روش نصب ابزارهای سراسری در NET Core.
روش نصب ابزارهای سراسری NET Core. به صورت زیر است:
dotnet tool install -g example
%USERPROFILE%\.dotnet\toolspkgs (Windows) $HOME/.dotnet/toolspkgs (macOS/Linux)
%USERPROFILE%\.dotnet\tools
~/.dotnet/tools
در حال حاضر برای عزل این برنامهها باید به یکی از این مسیرها مراجعه و آنها را دستی حذف کرد (در هر دو مسیر toolspkgs و tools باید حذف شوند).
یک نمونه از این ابزارها را که dotnet-dev-certs نام دارد، پس از نصب SDK جدید، در مکانهای یاد شده، خواهید یافت. کار این ابزار سراسری، تولید یک self signed certificate مخصوص برنامههای ASP.NET Core 2.1 است که پیشتر در مطلب «اجبار به استفادهی از HTTPS در حین توسعهی برنامههای ASP.NET Core 2.1» آنرا بررسی کردیم.
نکته 1: بر اساس تصویر فوق، در خط فرمان، دستور dotnet-dev-certs را صادر کنید. اگر پیام یافت نشدن این دستور یا ابزار را مشاهده کردید، به معنای این است که مسیر نصب آنها به PATH سیستم اضافه نشدهاست. با استفاده از دستورات ذیل میتوانید این مسیر را به PATH سیستم اضافه کنید:
Windows PowerShell: setx PATH "$env:PATH;$env:USERPROFILE/.dotnet/tools" Linux/macOS: echo "export PATH=\"\$PATH:\$HOME/.dotnet/tools\"" >> ~/.bash_profile
نکته 2: اگر به این مسیرها دقت کنید، این ابزارها صرفا برای کاربر جاری سیستم نصب میشوند و مختص به او هستند؛ به عبارتی user-specific هستند و نه machine global.
روش ایجاد ابزارهای سراسری NET Core.
همانطور که عنوان شد، ابزارهای سراسری NET Core. در اصل برنامههای کنسول آن هستند. به همین جهت پس از نصب SDK جدید، در یک پوشهی جدید، دستور dotnet new console را اجرا کنید تا یک برنامهی کنسول جدید مطابق آن ایجاد شود. سپس فایل csproj آنرا به صورت زیر ویرایش کنید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <IsPackable>true</IsPackable> <PackAsTool>true</PackAsTool> <TargetFramework>netcoreapp2.1</TargetFramework> </PropertyGroup> </Project>
پس از آن برای تهیهی یک بستهی نیوگت از آن، دستور زیر را اجرا کنید:
dotnet pack -c Release
dotnet tool install -g package-name
بهینه سازی ضروری تصاویر
We should all be automating our image compression.
In 2017, image optimization should be automated. It’s easy to forget, best practices change, and content that doesn’t go through a build pipeline can easily slip. To automate: Use imagemin or libvips for your build process. Many alternatives exist.
Most CDNs (e.g. Akamai) and third-party solutions like Cloudinary, imgix, Fastly’s Image Optimizer, Instart Logic’s SmartVision or ImageOptim API offer comprehensive automated image optimization solutions.