Dart کتابخانه ای است که توسط شرکت گوگل ارائه شده است و گفته میشود، قرار است جایگزین جاوا اسکریپت گردد و از آدرس https://www.dartlang.org قابل دسترسی میباشد. این کتابخانه، دارای انعطاف پذیری فوق العاده بالایی است و کد نویسی Java Script را راحتتر میکند. در حال حاضر هیچ مرورگری به غیر از Chromium از این تکنولوژی پشتیبانی نمیکند و جهت تسهیل در کدنویسی، باید از ویرایشگر Dart Editor استفاده کنید. این ویرایشگر کدهای نوشته شده را به دو صورت Native و JavaScript Compiled در اختیار مرورگر قرار میدهد. در ادامه با نحوهی کار و راه اندازی Dart آشنا خواهید شد.
ابتدا Dart و ویرایشگر مربوط به آن را توسط لینکهای زیر دانلود کنید:
دانلود نسخه 64 بیتی دارت + ویرایشگر
دانلود نسخه 32 بیتی دارت + ویرایشگر
بعد از اینکه فایلهای فوق را از حالت فشرده خارج کردید، پوشه ای با نام dart ایجاد مینماید. وارد پوشه dart شده و DartEditor را اجرا کنید.
توجه: جهت اجرای dart به JDK 6.0 یا بالاتر نیاز دارید
در مرحله بعد نمونه کدهای Dart را از لینک زیر دانلود نمایید و از حالت فشرده خارج کنید. پوشه ای با نام one-hour-codelab ایجاد میگردد.
از منوی File > Open Existing Folder… پوشه one-hour-codelab را باز کنید .
توضیحات
- پوشه packages و همچنین فایلهای pubspec.yaml و pubspec.lock شامل پیش نیازها و Package هایی هستند که جهت اجرای برنامههای تحت Dart مورد نیاز هستند. Dart Editor این نیازمندیها را به صورت خودکار نصب و تنظیم میکند.
توجه: اگر پوشه Packages را مشاهده نکردید و یا در سمت چپ فایلها علامت X قرمز رنگ وجود داشت، بدین معنی است که package ها به درستی نصب نشده اند. برای این منظور بر روی pubspec.yaml کلیک راست نموده و گزینه Get Pub را انتخاب کنید. توجه داشته باید که بدلیل تحریم ایران توسط گوگل باید از ابزارهای عبور از تحریم استفاده کنید.
- 6 پوشه را نیز در تصویر فوق مشاهده میکنید که نمونه کد piratebadge را بصورت مرحله به مرحله انجام داده و به پایان میرساند.
- Dart SDK شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاس هایی است که توسط کیت توسعه نرم افزاری Dart ارائه شده است.
- Installed Packages شامل سورس کد مربوط به تمامی توابع، متغیرها و کلاسهای کتابخانههای اضافهتری است که Application به آنها وابسته است.
گام اول: اجرای یک برنامه کوچک
در این مرحله سورس کدهای آماده را مشاهده میکنید و با ساختار کدهای Dart و HTML آشنا میشوید و برنامه کوچکی را اجرا مینمایید.
در Dart Editor پوشه 1-blankbadge را باز کنید و فایلهای piratebadge.html و piratebadge.dart را مشاهده نمایید.
کد موجود در فایل piratebadge.html
<html> <head> <meta charset="utf-8"> <title>Pirate badge</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="piratebadge.css"> </head> <body> <h1>Pirate badge</h1> <div> TO DO: Put the UI widgets here. </div> <div> <div> Arrr! Me name is </div> <div> <span id="badgeName"> </span> </div> </div> <script type="application/dart" src="piratebadge.dart"></script> <script src="packages/browser/dart.js"></script> </body> </html>
توضیحات
- در کد HTML ، اولین تگ <script> ، فایل piratebadge.dart را جهت پیاده سازی دستورات dart به صفحه ضمیمه مینماید
- Dart Virtual Machine (Dart VM) کدهای Dart را بصورت Native یا بومی ماشین اجرا میکند. Dart VM کدهای خود را در Dartium که یک ویرایش ویژه از مرورگر Chromium میباشد اجرا میکند که میتواند برنامههای تحت Dart را بصورت Native اجرا کند.
- فایل packages/browser/dart.js پشتیبانی مرورگر از کد Native دارت را بررسی میکند و در صورت پشتیبانی، Dart VM را راه اندازی میکند و در غیر این صورت JavaScript کامپایل شده را بارگزاری مینماید.
کد موجود در piratebadge.dart
void main() { // Your app starts here. }
- این فایل شامل تابع main میباشد که تنها نقطه ورود به application است. تگ <script> موجود در piratebadge.html برنامه را با فراخوانی این تابع راه اندازی میکند.
- تابع main() یک تابع سطح بالا یا top-level میباشد.
- متغیرها و توابع top-level عناصری هستند که خارج از ساختار تعریف کلاس ایجاد میشوند.
جهت اجرای برنامه در Dart Editor بر روی piratebadge.html کلیک راست نمایید و گزینه Run in Dartium را اجرا کنید. این فایل توسط Dartium اجرا میشود و تابع main() را فراخوانی میکند و صفحه ای همانند شکل زیر را نمایش میدهد.
گام دوم: افزودن فیلد input
توجه داشته باشید که در این مرحله یا میتوانید تغییرات مورد نظر خود را در طی آموزش بر روی پوشهی 1-blankbadge اعمال کنید و یا به پوشههای تهیه شده در نمونه کد موجود در همین پروژه مراجعه نمایید.
در این مرحله یک تگ <input> به تگ <div class=”widgets”> اضافه کنید.
... <div> <div> <input type="text" id="inputName" maxlength="15"> </div> </div> ...
سپس کتابخانه dart:html را به ابتدای فایل piratebadge.dart اضافه کنید.
import 'dart:html';
توضیحات
- دستور فوق کلاسها و Resource های موجود در کتابخانه dart:html را اضافه میکند.
- از حجیم شدن کدهای خود نگران نباشید، زیرا فرایند کامپایل کدهای اضافی را حذف خواهد کرد.
- کتابخانه dart:html شامل کلاسهایی جهت کار با عناصر DOM و توابعی جهت دسترسی به این عناصر میباشد.
- در مباحث بعدی یاد میگیرید که با استفاده از کلمه کلیدی show فقط کلاسهایی را import کنید که به آن نیاز دارید.
- اگر کتابخانه ای در هیچ بخش کد استفاده نشود، خود Dart Editor به صورت warning اخطار میدهد و میتوانید آن را حذف کنید.
دستور زیر را در تابع main بنویسید تا رویداد مربوط به ورود اطلاعات در فیلد input را مدیریت نمایید.
void main() { querySelector('#inputName').onInput.listen(updateBadge); }
توضیحات
- تابع querySelector() در کتابخانه dart:html تعریف شده است و یک المنت DOM را جستجو مینماید. پارامتر ورودی آن یک selector میباشد که در اینجا فیلد input را توسط #inputName جستجو نمودیم که یک ID Selector میباشد.
- نوع خروجی این متد یک شی از نوع DOM میباشد.
- تابع onInput.Listen() رویدادی را برای پاسخگویی به ورود اطلاعات در فیلد input تعریف میکند. زمانی که کاربر اطلاعاتی را وارد نماید، تابع updateBadge فراخوانی میگردد.
- رویداد input زمانی رخ میدهد که کاربر کلیدی را از صفحه کلید فشار دهد.
- رشتهها همانند جاوا اسکریپت میتوانند در " یا ' قرار بگیرند.
تابع زیر را به صورت top-level یعنی خارج از تابع main تعریف کنید.
... void updateBadge(Event e) { querySelector('#badgeName').text = e.target.value; }
توضیحات
- این تابع محتوای المنت badgeName را به محتوای وارد شده در فیلد input تغییر میدهد.
- پارامتر ورودی این تابع شی e از نوع Event میباشد و به همین دلیل میتوانیم این تابع را یک Event Handler بنامیم.
- e.target به شی ای اشاره میکند که موجب رخداد رویداد شده است و در اینجا همان فیلد input میباشد
- با نوشتن کد فوق یک warning را مشاهده میکنید که بیان میکند ممکن است خصوصیت value برای e.target وجود نداشته باشد. برای حل این مسئله کد را بصورت زیر تغییر دهید.
... void updateBadge(Event e) { querySelector('#badgeName').text = (e.target as InputElement).value; }
توضیحات
- کلمه کلیدی as به منظور تبدیل نوع استفاده میشود که e.target را به یک InputElement تبدیل میکند.
همانند گام اول برنامه را اجرا کنید و نتیجه را مشاهده نمایید. با تایپ کردن در فیلد input به صورت همزمان در کادر قرمز رنگ نیز نتیجه تایپ را مشاهده مینمایید.
الف) ابتدا افزونه Razor Generator را از اینجا دریافت و نصب کنید.
ب) سپس به پروژه MVC خود بسته NuGet زیر را اضافه نمائید:
PM> Install-Package RazorGenerator.Mvc
با اضافه کردن این بسته NuGet تغییرات زیر به پروژه جاری اعمال خواهند شد:
- ارجاعی به اسمبلی RazorGenerator.Mvc.dll به پروژه اضافه خواهد شد.
- در پوشه App_Start، فایلی به نام RazorGeneratorMvcStart.cs اضافه گردیده و کار تنظیم موتور View مخصوص کار با Viewهای کامپایل شده را انجام میدهد.
ج) پس از نصب بسته NuGet یاد شده در همان خط فرمان پاورشل نوگت دستور زیر را صادر کنید:
PM> Enable-RazorGenerator
پس از انجام اینکار، دو کار صورت خواهد گرفت:
- برای تمام Viewهای برنامه، فایل cs متناظری تولید میشود که ذیل فایلهای View قابل مشاهده است.
- گزینه Custom tool این Viewها نیز به RazorGenerator تنظیم میشود.
بدیهی است اگر از دستور Enable-RazorGenerator استفاده نکنید، نیاز خواهید داشت تا تنظیم گزینه Custom tool به RazorGenerator کلیه Viewها را دستی انجام داده و اگر فایل cs متناظر با View تولید نشد روی فایل view کلیک راست کرده و گزینه run custom tool را انتخاب کنید. اما دستور Enable-RazorGenerator کار را بسیار ساده میکند.
مزایا:
- در عمل موتور ASP.NET همین کارها را در زمان اولین بار اجرای Viewها(ی کامپایل نشده) در پشت صحنه انجام میدهد. بنابراین با این روش زمان آغاز برنامه سریعتر میشود.
- دیگر نیازی به توزیع فایلهای cshtml نخواهید داشت.
- خطایابی Viewها نیز سادهتر میشود. از این جهت که کامپایل آنها به زمان اجرا موکول نخواهد شد.
یک سری قابلیتهای دیگر نیز به همراه این پروژه است مانند انتقال Viewها به یک اسمبلی دیگر و یا استفاده از MSBuild برای انجام عملیات که میتوانید آنها را در Wiki پروژه Razor Generator مطالعه کنید. انتقال Viewها به یک اسمبلی دیگر هرچند در این روش کاملا ممکن شده و کار میکند اما صفحه dialog افزودن یک view جدید مهیا در کلیک راست بر روی اکشن متدهای یک کنترلر را غیرفعال میکند که در عمل آنچنان جالب نیست.
یک نکته مهم:
اگر در آینده بسته NuGet و افزونه یاد شده را به روز کردید نیاز است دستور زیر را اجرا کنید:
PM> Redo-RazorGenerator
فایلهای Helper قرار گرفته در پوشه App_Code
اگر HTML Helperهای خود را توسط فایلهای Razor قرار گرفته در پوشه App_Code تولید میکنید، پس از اجرای دستور Enable-RazorGenerator، برای این موارد نیز فایلهای cs متناظری تولید میشود. با این تفاوت که Build Action آنها بر روی Compile قرار ندارند که این مورد را باید دستی تنظیم کنید. همچنین حین استفاده از این توابع کمکی نیاز است فضای نام مرتبط را نیز در ابتدای فایل View خود ذکر کنید مثلا:
@using MvcViewGenTest2.app_code
حذف فایل RazorGeneratorMvcStart.cs
اگر علاقمند به استفاده از فایل پیش فرض RazorGeneratorMvcStart.cs نیستید و میخواهید موتورهای View اضافی را حذف کنید، ابتدا فایل RazorGeneratorMvcStart.cs را حذف کرده و سپس در فایل global.asax.cs تغییر زیر را اعمال نمائید:
using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; using RazorGenerator.Mvc; namespace MvcViewGenTest2 { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); // Adding PrecompiledMvcEngine var engine = new PrecompiledMvcEngine(typeof(MvcApplication).Assembly); ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(engine); VirtualPathFactoryManager.RegisterVirtualPathFactory(engine); } } }
روش افزودن میانافزار RateLimiter به برنامههای ASP.NET Core
شبیه به سایر میانافزارها، جهت فعالسازی میانافزار RateLimiter، ابتدا باید سرویسهای متناظر با آنرا به برنامه معرفی کرد و پس از فعالسازی میانافزار مسیریابی، آنرا به زنجیرهی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواستها، به 10 درخواست در دقیقه، محدود میشود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شدهاست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); }); // ... var app = builder.Build(); // ... app.UseRouting(); app.UseRateLimiter(); app.MapGet("/", () => "Hello World!"); app.Run();
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویسهای میانافزار rate limiter به سیستم تزریق وابستگیهای ASP.NET Core میشود.
- در اینجا میتوان برای مثال خاصیت options.GlobalLimiter تنظیمات آنرا نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کنندهی سراسری نرخ، برای تمام درخواستهای رسیدهی به برنامه میشود.
- GlobalLimiter را میتوان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شدهاست تا بتوان «الگوریتمهای بازهی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده میکنید که سبب فعالسازی میانافزار، بر اساس تنظیمات صورت گرفته میشود.
برای آزمایش برنامه، آنرا اجرا کرده و سپس به سرعت شروع به refresh کردن صفحهی اصلی آن کنید. پس از 10 بار ریفرش، پیام 503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میانافزار rate limiter است.
بررسی تنظیمات رد درخواستها توسط میانافزار rate limiter
اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، میتوان آنرا هم تغییر داد:
builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = 429; // ... });
علاوه بر آن در اینجا گزینهی OnRejected نیز پیش بینی شدهاست تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شدهاست، ارائه داد:
builder.Services.AddRateLimiter(options => { options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = 429; if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { await context.HttpContext.Response.WriteAsync( $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " + $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } else { await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later. " + "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } }; // ... });
یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن میتوان برای مثال سرویس ILogger را از آن درخواست کرد و رخداد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کردهاست؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()? .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
طراحی فعلی میانافزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه میدهد و همچنین مانند GitHub مشخص نمیکند که در لحظهی جاری چند درخواست دیگر را میتوان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میانافزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!
الگوریتمهای پشتیبانی شدهی توسط میانافزار rate limiter
در قسمت قبل با چند الگوریتم استاندارد طراحی میانافزارهای rate limiter آشنا شدیم که میانافزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آنها را پشتیبانی میکند:
- Concurrency limit: سادهترین نوع محدود سازی نرخ درخواستها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواستهای همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن میتوان محدودیتهایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیتها میسر میکند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواستهای انفجاری نیز پشتیبانی میکند (این مفاهیم در قسمت قبل بررسی شدند).
علاوه بر اینها امکان ترکیب گزینههای فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.CreateChained( PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 600, Window = TimeSpan.FromMinutes(1) })), PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 6000, Window = TimeSpan.FromHours(1) }))); // ... });
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامهی خود از کلید پارتیشن بندی بهتر و معقولتری استفاده کنید.
امکان در صف قرار دادن درخواستها بجای رد کردن آنها
در تنظیمات مثالهای فوق، در کنار PermitLimit، میتوان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواستها در صف قرار میگیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 6, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, Window = TimeSpan.FromSeconds(1) })));
این تنظیم، تجربهی کاربری بهتری را برای استفاده کنندگان از برنامهی شما به همراه خواهد داشت؛ بجای رد قاطع درخواستهای ارسالی توسط آنها.
یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازههای زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر اینطور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیهی کوتاه در صف بودن برای کاربران معنا دارد.
امکان ایجاد سیاستهای محدود سازی سفارشی
اگر الگوریتمهای توکار میانافزار rate limiter برای کار شما مناسب نیستند، میتوانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونهی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز میگرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواستها را میسر میکند.
در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده میکنید که نحوهی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشدهی به سیستم است. در اینجا کاربر وارد شدهی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string> { public RateLimitPartition<string> GetPartition(HttpContext httpContext) { if (httpContext.User.Identity?.IsAuthenticated == true) { return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!, partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 1_000, Window = TimeSpan.FromMinutes(1), }); } return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, Window = TimeSpan.FromMinutes(1), }); } public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = (context, _) => { context.HttpContext.Response.StatusCode = 418; // I'm a 🫖 return new ValueTask(); }; }
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");
امکان تعریف سیاستهای محدود سازی نرخ دسترسی به گروهی از endpoints
تا اینجا روشهای سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامهای بخواهیم محدودیتهای متفاوتی را به گروههای خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آنها را محدود کنیم:
builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("Api", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); options.AddFixedWindowLimiter("Web", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); // ... });
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه میکنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام میدهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, Window = TimeSpan.FromSeconds(1) }));
محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API
پس از تعریف نامی برای سیاستهای دسترسی، اکنون میتوان از آنها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آنها استفاده کرد:
// Endpoint app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api"); // Group app.MapGroup("/api/orders").RequireRateLimiting("Api");
// Endpoint app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting(); // Group app.MapGroup("/api/orders").DisableRateLimiting();
محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC
میتوان سیاستهای نرخ دسترسی تعریف شده را بر اساس نام آنها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")] public class Orders : Controller { [DisableRateLimiting] public IActionResult Index() { return View(); } [EnableRateLimiting("ApiListing")] public IActionResult List() { return View(); } }
و یا حتی میتوان این سیاستهای محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints => { endpoints.MapRazorPages() .DisableRateLimiting(); endpoints.MapControllers() .RequireRateLimiting("UserBasedRateLimiting"); });
- برنامه هایی که مقدار زیادی کد مدیریت شده قبل از نمایش فرم برنامه دارند. مانند برنامه Blend که مقدار زیادی کد در شروع برنامه دارد. استفاده از ngen میتواند باعث افزایش کارایی و سرعت اجرای برنامه شود
- فریم ورک ها، dllها و کامپوننتهای عمومی: کدهای تولید شده توسط JIT قابل اشتراک بین برنامههای مختلف نیستند ولی NGEN قابل اشتراک مابین برنامههای مختلف میباشد. بنابراین اگر کامپوننتی دارید که در بین برنامههای مختلف مشترک استفاده میشود، این کار میتواند سرعت شروع برنامهها را بالا برده استفاده از منابع سیستم را کاهش دهد
- برنامه هایی که در terminal serverها استفاده میشوند: توضیح فوق در مورد این برنامهها نیز صادق است.
- برنامههای کوچک: عملا سرعت JIT آن قدر بالا است که NGEN کار را کندتر خواهد کرد!
- برنامههای سروری که سرعت شروع آن مهم نیست: برنامهها یا dll هایی که سرعت شروع آنها مهم نیست، اگر NGEN نشوند سرعت بیشتری برای شما به ارمغان خواهند داشت چون JIT در هنگام اجرا، کد را بهینه میکند ولی NGEN این کار را انجام نمیدهد.
سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا
5 دلیل برای استفاده از یک ابزار ORM
- امیدی به ادامهی آن باشد (نگن امروز به به! فردا ... خوب دیگه ... تموم شد! صرف نمیکنه، دیگه توسعه نمیدیم! خیلی از سیاستهای مایکروسافت همینطوری است. مثلا همون کاری که با LINQ to SQL کرد)
- چند هزار نفری پیرو و دنبال مباحث آن باشند (حداقل 2 تا فوروم رفع اشکال بتونید پیدا کنید)
- دو تا کتاب در موردش باشه
- 4 تا وبلاگ در موردش مطلب بنویسند.
و مسایلی از این دست.
به همین جهت یا روی EF یا NH سرمایه گذاری کنید.
به شخصه NH رو ترجیح میدم چون سورس باز است، به همین جهت مرگ برای آن معنی ندارد (این گروه نخواست ادامه بده ... گروههای دیگر هستند)، رایگان است، مجوزش اجازه استفاده در کارهای تجاری سورس بسته را میدهد. چندتا کتاب در موردش هست و ...
به EF شک دارم. نمیدونم مایکروسافت مثلا 4 سال دیگه آیا این را هم بازنشسته اعلام میکند یا نه.
OData چهار قسمت اصلی دارد:
- OData data model که یک راه عمومی برای مدیریت و توصیف دادهها را فراهم مینماید
- OData protocol که به کلاینت اجازه ایجاد درخواست و پاسخ از سرویس دهنده OData را میدهد.
- OData client libraries که امکان ساخت سادهتر نرم افزارها برای دسترسی به دادهها با قرارداد OData را میدهد.
- OData service سرویس دهنده و امکان دسترسی به دادهها را فراهم میسازد.
- ساده و انعطاف پذیر
- سورس باز بودن
- امکان استفاده در سیستمهای با دادههای رابطه ای و غیر رابطه ای
- امکان استفاده از دادهها با منابع ای که آدرس پذیر هستند یعنی دسترسی از طریق Url
- امکان دسترسی هر نوع گیرنده ای به داده ها
- امکان نمایش خروجی با فرمت Json یا Xml
- ...
کنترل DatePicker شمسی مخصوص Silverlight 4
context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity });
کامپایل افزونهی spell fix1
افزونهی spell fix، به همراه هیچکدام از توزیعهای باینری SQLite ارائه نمیشود. ارائهی آن فقط به صورت سورس کد است و باید خودتان آنرا کامپایل کنید!
برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آنرا دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحهی بعدی، لینک download آن هم وجود دارد.
سپس به صفحهی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بستهی amalgamation آنرا دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونههای آن قرار گیرند تا بتوان این افزونهها را کامپایل کرد. بنابراین پس از دریافت بستهی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشهی آن کپی کنید:
اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آنرا به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای میتوانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بستهی codeblocks-20.03mingw-setup.exe را دریافت کنید (بستهای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin میتوانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن میتوان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll
روش معرفی افزونههای SQLite به Microsoft.Data.Sqlite
EF Core، از بستهی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده میکند و در اینجا هم برای معرفی افزونهی کامپایل شده، باید ابتدا آنرا به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونههایش را بر اساس معرفی مستقیم مسیر فایل dll آنها بارگذاری نمیکند. بلکه path ویندوز را برای جستجوی آنها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آنرا بارگذاری میکند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا میتوان توسط قطعه کد زیر، آنرا به صورت پویایی معرفی کرد:
using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace EFCoreSQLiteFTS.DataLayer { public static class LoadSqliteExtensions { public static void AddToSystemPath(string extensionsDirectory) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app."); } var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)); if (path.Add(extensionsDirectory)) { Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path)); } } } }
در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشهی افزونهها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونهی spellfix1 به آن معرفی شدهاست:
LoadSqliteExtensions.AddToSystemPath("path to .dll file"); services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) => { var connection = new SqliteConnection(connectionString); connection.Open(); connection.LoadExtension("spellfix1"); // Passing in an already open connection will keep the connection open between requests. optionsBuilder.UseSqlite(connection); });
ایجاد جداول ویژهی spell fix در برنامه
در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونهی spell fix به صورت زیر تکمیل میکنیم:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ... // EditCost for unicode support context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');"); context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);"); context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);"); }
- همانطور که مشاهده میکنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد میشود. کار آن استخراج توکنهای FTS و آماده سازی آنها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژهی EditCost را مشاهده میکنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار میکند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده میکند، ایجاد شدهاست.
اجرای این دستورات، جداول زیر را ایجاد میکنند (که ساختار آنها استاندارد است و باید مطابق فرمولهای مستندات آنها باشد):
به روز رسانی جدول واژه نامهی غلط یابی برنامه
آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت میکند:
هر بار که بانک اطلاعاتی را به روز میکنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank) SELECT term, cnt FROM Chapters_FTS_Vocab WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");
کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix
تا اینجا افزونهی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامهی آنرا نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئریهای آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities { public class SpellCheck { public string Word { get; set; } public decimal Rank { get; set; } public decimal Distance { get; set; } public decimal Score { get; set; } public decimal Matchlen { get; set; } } }
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<SpellCheck>().HasNoKey().ToView(null); } //... } }
در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw( @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix WHERE word MATCH {0} and top=6", "فارشی")) { Console.WriteLine($"Word: {item.Word}"); Console.WriteLine($"Distance: {item.Distance}"); }
top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.
یک نکته: اگر میخواهید کوئری فوق را توسط برنامهی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینهی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.