, What Every JavaScript Developer Should Know About ECMAScript 2015 is the book I'd like to read about the new features in the JavaScript language. The book isn't a reference manual or an exhaustive list of everything in the ES2015 specification. Instead, I purposefully selected what I think are the important features we will use in everyday programming.
معرفی API استاندارد بومی سازی JavaScript
Internationalization یا به صورت خلاصه i18n (یعنی یک i به همراه 18 حرف و یک n)، پروسهای که در آن برنامه به نحوی طراحی میشود تا خروجی آن قابلیت استفادهی برای انواع و اقسام فرهنگها را داشته باشد. برای مثال دو متد زیر را در نظر بگیرید:
function formatDate(d) { var month = d.getMonth() + 1; var date = d.getDate(); var year = d.getFullYear(); return month + "/" + date + "/" + year; } function formatMoney(amount) { return "$" + amount.toFixed(2); }
پیشتر جاوا اسکریپت برای مدیریت یک چنین مواردی (i18n-aware formatting) از متد toLocaleString استفاده میکرد (و هنوز هم برای پشتیبانی از برنامههای قدیمی، از API عمومی آن حذف نشدهاست) و خروجی آن از هر مرورگر و پیاده سازی خاصی، به مرورگر دیگری میتواند متفاوت باشد؛ حتی اگر جزئیات دقیقی هم درخواست شود. برای رفع این مشکل، استاندارد ECMAScript Internationalization API ارائه شد تا قابلیتهای توکار i18n جاوا اسکریپت را بهبود بخشیده و همچنین یکدست کند. توسط آن امکان انتخاب یک یا چند منطقهی خاص و سپس فرمت کردن تاریخ، اعداد و یا حتی مرتب سازی واژهها و عبارات با معرفی collations، میسر میباشد. در اینجا حتی امکان سفارشی سازی این فرمتها نیز پیشبینی شدهاست.
معرفی اینترفیس Intl
i18n API در یک شیء سراسری به نام Intl قابل دسترسی است و تعدادی از سازندههای آن Intl.Collator ،Intl.DateTimeFormat و Intl.NumberFormat نام دارند؛ مانند:
const result = new Intl.NumberFormat("fa").format(123456)
تمام این سازندهها میتوانند با یک فرهنگ و یا آرایهای از فرهنگهای مدنظر شروع شوند:
const portugueseTime = new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);
پارامتر اختیاری دوم آنها نیز تنظیماتی است که جهت سفارشی سازی این بومی سازی میتوان تعریف کرد.
نمایش شمسی تاریخ میلادی توسط i18n API
پس از معرفی i18n API، اکنون میخواهیم در طی مثالهایی، تمام کتابخانههای ثالث تبدیل تاریخ میلادی به شمسی را کنار گذاشته و با استفاده از جاوا اسکریپت استاندارد، این تبدیل را انجام دهیم. پارامتر دوم سازندهی new Intl.DateTimeFormat که تنظیمات آنرا مشخص میکند، میتواند به همراه ترکیبی از موارد زیر باشد که مقادیر مجاز برای آنها را نیز مشاهده میکنید:
{ weekday: 'narrow' | 'short' | 'long', era: 'narrow' | 'short' | 'long', year: 'numeric' | '2-digit', month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long', day: 'numeric' | '2-digit', hour: 'numeric' | '2-digit', minute: 'numeric' | '2-digit', second: 'numeric' | '2-digit', timeZoneName: 'short' | 'long', // Time zone to express it in timeZone: 'Asia/Shanghai', // Force 12-hour or 24-hour hour12: true | false, // Rarely-used options hourCycle: 'h11' | 'h12' | 'h23' | 'h24', formatMatcher: 'basic' | 'best fit' }
var dateFormat = new Intl.DateTimeFormat("fa"); console.log(dateFormat.format(Date.now()));
نمایش تاریخ شمسی با فرمت «۱۳۹۸ اسفند ۱, پنجشنبه»
برای تبدیل تاریخ میلادی به شمسی میتوان از سازندهی new Intl.DateTimeFormat با فرهنگ fa استفاده کرد. در اینجا ذکر مقدار long برای نام روز هفته، سبب درج نام روز میشود. نمایش سال به صورت عددی تنظیم شدهاست، ماه را به صورت بلند و نام کامل نمایش میدهد و مقدار روز را به صورت عددی درج میکند. این اعداد نیز فارسی هستند:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200)); const faDate = new Intl.DateTimeFormat("fa", { weekday: "long", year: "numeric", month: "long", day: "numeric" }).format(date); console.log(faDate);
اگر فقط نیاز به نمایش «۱ اسفند ۱۳۹۸» بود، میتوان از تنظیمات زیر که در آن ماه، روز و سال ذکر شدهاند و در آن، ماه به صورت کامل و بلند نمایش داده میشود، استفاده کرد:
const isoString = new Date().toISOString(); const date = new Date(isoString); console.log( new Intl.DateTimeFormat("fa", { month: "long", day: "numeric", year: "numeric" }).format(date) );
یک نکته: همین خروجی را با متد قدیمی toLocaleDateString نیز میتوان به دست آورد؛ اما روش توصیه شده برای برنامههای جدید، همان استفاده از new Intl است.
console.log( new Date().toLocaleDateString("fa", { month: "long", day: "numeric", year: "numeric" }) );
نمایش تاریخ شمسی با فرمت «۹۸/۱۲/۱، ۶:۳۰»
برای اینکار پس از ذکر فرهنگ fa، تمام اجزای تاریخ را به صورت عددی مشخص میکنیم و سال را نیز دو رقمی نمایش خواهیم داد:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200)); const fmt = new Intl.DateTimeFormat("fa", { year: "2-digit", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric" }); console.log(fmt.format(date));
const faDateTime = new Intl.DateTimeFormat("fa", { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", timeZoneName: "short" }).format; const now = Date.now(); console.log(faDateTime(now));
و یا اگر «۱ اسفند ۱۳۹۸، ۰۹:۲۹ (UTC)» مدنظر بود، میتوان ماه را به long تنظیم کرد و مقدار timeZone را صریحا ذکر نمود (که البته ذکر تنظیمات timeZone اختیاری است):
const faTime = new Intl.DateTimeFormat("fa", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", timeZoneName: "short", timeZone: "UTC" }); console.log(faTime.format(now));
const rtf = new Intl.RelativeTimeFormat("en", { localeMatcher: "best fit", // other values: "lookup" numeric: "always", // other values: "auto" style: "long", // other values: "short" or "narrow" }); console.log(rtf.format(-1, "day")); console.log(rtf.format(1, "day"));
احتمالا برای تبدیل اعداد انگلیسی به فارسی و نمایش آنها، متدهایی را برای replace حروف و اعداد طراحی کردهاید. به کمک شیء استاندارد Intl.NumberFormat دیگر نیازی به آنها نخواهید داشت!
خروجی شیء Intl.NumberFormat به همراه ذکر فرهنگ فارسی و هیچ تنظیم اضافهتری
console.log(new Intl.NumberFormat("fa").format(123456));
اگر میخواهید این جداکنندهی هزارگان نمایش داده نشود، نیاز است تنظیمات آنرا به همراه useGrouping: false، به صورت زیر ذکر کرد:
console.log( new Intl.NumberFormat("fa", { useGrouping: false }).format(123456) );
این شیء یک مقدار غیرعددی را
console.log(new Intl.NumberFormat("fa").format("تست"));
و یا برای نمایش واحد پولی، میتوان حالت نمایش را به currency و نوع currency را به IRR که ریال است، تنظیم کرد:
const gasPrice = new Intl.NumberFormat("fa", { style: "currency", currency: "IRR", minimumFractionDigits: 3 }); console.log(gasPrice.format(5.2597));
برای نمایش درصد پس از اعداد میتوان از تنظیم زیر استفاده کرد:
const faPercent = new Intl.NumberFormat("fa", { style: "percent", minimumFractionDigits: 2 }).format; console.log(faPercent(0.438));
و یا برای نمایش ممیز به همراه تنظیم دقت آن داریم:
const persianDecimal = new Intl.NumberFormat("fa", { minimumIntegerDigits: 2, maximumFractionDigits: 2 }); console.log(persianDecimal.format(3.1416));
دریافت کدهای کامل این پروژه
کدهای کامل پروژهای که نیازمندیهای فوق را پیاده سازی میکند، در اینجا میتوانید مشاهده و دریافت کنید. در این مطلب از قرار دادن مستقیم این کدها صرفنظر شده و سعی خواهد شد بجای آن، نقشهی ذهنی درک کدهای آن توضیح داده شود.
پیشنیازها
در پروژهی فوق برای شروع به کار، از اطلاعات مطرح شدهی در سلسله مطالب زیر استفاده شدهاست:
- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- «مدیریت مرکزی شماره نگارشهای بستههای NuGet در پروژههای NET Core.»
- «کاهش تعداد بار تعریف usingها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامهی ASP.NET Core»
نیاز به علامتگذاری صفحات امن شدهی سمت کلاینت، جهت نمایش خودکار آنها
صفحات امن سازی شدهی سمت کلاینت، با ویژگی Authorize مشخص میشوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا میتوان یک نمونهی سفارشی سازی شدهی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارثبری کرده و دقیقا مانند آن عمل میکند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمتهای بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافهتری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شدهی در منوی اصلی برنامهاست و Title همان عنوان صفحه که در این منو نمایش داده میشود. اگر صفحهی محافظت شدهای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شدهاند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.
نمونهای از استفادهی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 میتوانید مشاهده کنید که خلاصهی آن به صورت زیر است:
@attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]
ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.
نیاز به لیست کردن صفحات علامتگذاری شدهی با ویژگی ProtectedPage
پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آنها است. اینکار توسط سرویس ProtectedPagesProvider صورت میگیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننتهای تعریف شدهی در برنامه را از اسمبلی جاری استخراج میکند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آنهایی که دارای RouteAttribute هستند، پردازش میشوند؛ یعنی کامپوننتهایی که به همراه مسیریابی هستند. پس از آن بررسی میشود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.
نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شدهی با ویژگی ProtectedPage
اکنون که لیست صفحات امن سازی شدهی توسط ویژگی ProtectedPage را در اختیار داریم، میتوانیم آنها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش میدهد.
تهیهی جداول و سرویسهای ثبت دسترسیهای پویای سمت کلاینت
جداول و فیلدهای مورد استفادهی در این پروژه را در تصویر فوق ملاحظه میکنید که در پوشهی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقشهای کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، میتوان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:
در اینجا نقشها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستمهای اعتبارسنجی آن بر همین اساس کار میکنند، قابل مشاهدهاست. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شدهی سمت کلاینت و ::DynamicServerPermission:: جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شدهی سمت سرور نیز تعریف شدهاند. رابطهای این اطلاعات با جدول کاربران، many-to-many است.
به این ترتیب است که مشخص میشود کدام کاربر، به چه claimهایی دسترسی دارد.
برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شدهاند. UserTokens اطلاعات توکنهای صادر شدهی توسط برنامه را ذخیره میکند و توسط آن میتوان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکنهای او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شدهاست.
اطلاعات این سرویسها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامهی کلاینت قرار میگیرند.
نیاز به قسمت مدیریتی ثبت دسترسیهای پویای سمت کلاینت و سرور
قبل از اینکه بتوان قسمتهای مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفادهی از لیست ProtectedPageها نیز تهیه کرد:
در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آنرا از کنترلر UsersAccountManagerController دریافت میکند، انجام میدهد. در مقابل نام هر کاربر، دو دکمهی ثبت اطلاعات پویای دسترسیهای سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت میشود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شدهی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه میدهد. اگر در اینجا صفحهای انتخاب شد، اطلاعات آن به سمت سرور ارسال میشود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.
شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش میدهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری میشود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آنها را در مسیر src\Server\Controllers\Tests میتوانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission:: به کاربر انتخابی انتساب داده میشود.
بازگشت اطلاعات پویای دسترسیهای سمت کلاینت از API
تا اینجا کامپوننتهای امن سازی شدهی سمت کلاینت و اکشن متدهای امن سازی شدهی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویسهای سمت سرور، اطلاعات آنها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفادهی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال میشوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقشهای او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشدهاست، برنامه میتواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال میشود. این اطلاعات به توکن دسترسی اضافه نشدهاند تا بیجهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده میکند.
اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شدهاند. میشد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شدهی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آنرا مشکل میکند.
این سه توکن توسط سرویس BearerTokensStore، در برنامهی سمت کلاینت ذخیره و بازیابی میشوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواستهای ارسالی توسط برنامه الصاق خواهد شد.
مدیریت خودکار اجرای Refresh Token در برنامههای Blazor WASM
دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحلهی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.
- برای مدیریت حالت الف، یک Policy ویژهی Polly طراحی شدهاست که آنرا در کلاس ClientRefreshTokenRetryPolicy مشاهده میکنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شدهاست. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتنهای تکراری، در هر جائیکه از سرویسهای HttpClient استفاده میشود، میباشد.
- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده میکنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمهی F5) و در کلاس آغازین ClientAuthenticationStateProvider میباشد.
نیاز به پیاده سازی Security Trimming سمت کلاینت
از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج میشود. بنابراین مرحلهی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شدهی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکنهای دریافتی پس از لاگین را انجام میدهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده میکنیم تا توسط آن بتوان زمانیکه قرار است آیتمهای منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژهی SecurityTrim.razor، با استفاده از نقشها و claims یک کاربر، میتواند تعیین کند که آیا قسمت محصور شدهی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده میکنند، استفاده میکند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor میتوانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده میشود.
نحوهی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت میدهد. این لیست پس از لاگین موفقیت آمیز دریافت شدهاست. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقهای از سرویس ClientSecurityTrimmingService عبور میدهیم. یعنی مسیر صفحه و همچنین دسترسیهای پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتمهای منوی پویای برنامه، نمایش داده نمیشود.
نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آنها دسترسی ندارد
با استفاده از ClientSecurityTrimmingService، در حلقهای که آیتمهای منوی سایت را نمایش میدهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرسها را به صورت مستقیم در مرورگر وارد کند، به آنها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده میکنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیشفرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحهای اعمال شد، از این سیاست دسترسی استفاده میکند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده میکند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال میکنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟
نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آنها دسترسی ندارد
شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission:: است که در صفحهی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده میکند نیز DynamicServerPermissionsAuthorizationHandler میباشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شدهاست. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شدهی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» است؛ اما بدون استفادهی از ASP.NET Core Identity.
روش اجرای برنامه
چون این برنامه از نوع Blazor WASM هاست شدهاست، نیاز است تا برنامهی Server آنرا در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه میشود. لیست کاربران پیشفرض آنرا در اینجا میتوانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آنها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.
کتابخانه 101
1) 101 will be maintained to minimize overlap with vanilla JS.
- 101 utils are made to work well with vanilla JS methods.
- 101 will only duplicate vanilla JS to provide functional programming paradigms or if the method is not available in a widely supported JS version (currently ES5).
- Underscore/lodash - duplicates a lot of ES5: forEach, map, reduce, filter, sort, and more.
2) No need for custom builds.
- With 101, import naturally, and what you use will be bundled.
- Each util method is a module that can be required require('101/<util>').
- Currently node/browserify is supported, I will add other module system support on request.
- Underscore/lodash is large, and you have to manually create custom builds when if you're trying to optimize for size.
در Asp.net دو چرخهی حیات مهم
وجود دارند که اساس چارچوب MVC را تشکیل میدهند
:
- چرخهی حیات برنامه (Application)؛ از لحظهای که برنامه برای اولین بار اجرا میشود و تا لحظهی خاتمهی آن را شامل میشود.
- چرخهی حیات یک درخواست ( Request )؛ مسیری که یک درخواست طی میکند، اصطلاحا PipeLine نامیده میشود که همان چرخهی حیات یک درخواست نیز هست و از لحظهای که درخواست تحویل asp.net شده، تا زمانیکه درخواست ارسال میشود را شامل میشود.
تمرکز بنده بیشتر بر روی روند و مسیری است که یک درخواست طی میکند و قصد دارم با بهره گیری از کتاب Pro Asp.net Mvc 5 و دیگر منابع، چرخهی حیات درخواست را در برنامههای Mvc بررسی کرده و در مقالات آتی ماژولها و هندلرها را بررسی کنم.
در asp.net ، برنامه global فایلهای شامل دو فایل Global.asax , Global.asax.cs است.
فایل Global.asax که هیچ گاه نیاز به ویرایش آن نداریم محتویاتی مانند زیر دارد:
<%@ Application Codebehind="Global.asax.cs" Inherits="YourAppName.MvcApplication" Language="C#" %>
در این مقاله منظور از فایل global فایل Global.asax.cs است که مشتق شده از کلاس System.Web.HttpApplication است:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ...// } }
Asp.net برای پاسخگویی به درخواستهای واصله، وهلههایی از کلاس MvcApplication را میسازد ولی این دو متد صرفا در نقاط شروع و پایان برنامه فراخوانی شده و عملا در وهلههای یاد شده صدا زده نخواهند شد و به جای آنها رویدادهایی را که در ذیل آنها را معرفی میکنیم، فراخوانی شده و چرخهی حیات درخواست را برای ما مشخص میسازند .
BeginRequest : به عنوان اولین رویداد، به محض وصول یک درخواست جدید رخ خواهد داد.AuthenticateRequest ,PostAuthenticateRequest : رویداد AuthenticateRequest برای شناسایی کاربر ارسال کننده درخواست، کاربرد دارد و پس از پردازش کلیهی توابع، رویداد PostAuthenticateRequest صدا زده میشود.
AuthorizeRequest :بههنگام صدور مجوزهای یک درخواست رخ میدهد و مشابه رویداد بالا پس از پردازش کلیهی توابع، رویداد PostAuthorizeRequest صدا زده خواهد شد.ResolveRequestCache : پس از صدور مجوزهای یک درخواست در رویداد authorization زمانیکه ماژولهای کش میخواهند اطلاعاتی را از کش سرور مطالبه کنند، رخ میدهد و به مانند دو رخداد قبلی، PostResolveRequestCache نیز پس از اتمام پردازش توابع رویداد رخ میدهد.
MapRequestHandler : زمانی که Asp.net میخواهد هندلری را برای پاسخگویی به درخواست واصله انتخاب کند رخ میدهد و PostMapRequestHandler نیز پس از این انتخاب، تریگر میشود.AcquireRequestState : جهت بدست آوردن دادههایی نظیر سشن و ... مرتبط با درخواست جاری کاربرد داشته و PostAcquireRequestState نیز پس از پردازش توابع رویداد رخ خواهد داد.
PreRequestHandlerExecute : بالافاصله قبل و همچنین بلافاصله بعد از این که یک هندلر بخواهد درخواستی را پردازش کند، رخ میدهد. PostRequestHandlerExecute نیز همانند دیگر رویدادهای گذشته، پس از اتمام پردازش توابع، این رویداد رخ خواهد داد.ReleaseRequestState : زمانی رخ میدهد که دادههای مرتبط با درخواست جاری، در ادامهی روند پردازش درخواست مورد نیاز نباشند و پس از پردازش توابع رویداد، PostReleaseRequestState رخ خواهد داد .
UpdateRequestCache : به جهت اینکه ماژولهای مسئول کش، توانایی به روز رسانی دادههای خود، برای پاسخگویی به درخواستهای بعدی را داشته باشند، این رویداد رخ میدهد.
LogRequest : قبل از انجام عملیات لاگین برای درخواست جاری رخ میدهد و پس از پردازش توابع رویداد نیز PostLogRequest تریگر میشود.EndRequest : پس از پایان کار پردازش درخواست جاری و مهیا شدن پاسخ مرتبط جهت ارسال به مرورگر تریگر خواهد شد.
PreSendRequestHeaders : قبل از ارسال HTTP headers به مرورگر این رویداد رخ خواهد داد.
PreSendRequestContent : بعد از ارسال شدن هدرها و قبل از ارسال محتوای صفحه به مرورگر رخ میدهد.Error : هر زمان و در هر مرحله از پردازش درخواست، چنانچه خطایی صورت پذیرد این رویداد رخ خواهد داد.
فریم ورک Asp.net جهت مدیریت بهتر یک درخواست، در تمام مسیر پردازش، رویدادهای بالا را مهیا کرده است. در ادامه نحوهی هندل کردن رویدادهای چرخهی حیات درخواست را در فایل global توضیح میدهم. هر چند که استفاده از این فایل بدین منظور، صرفا برای مدیریت مسائل ابتدایی مناسب بوده و در یک پروژهی بزرگ موجب به هم ریختگی فایل global با کدهای زیاد و خوانایی پایین بوده که قابلیت استفاده مجدد در دیگر پروژهها را نیز ندارد.
Asp.net این مشکل را با معرفی ماژولها که در مقالات آتی توضیح خواهم داد، مرتفع کرده است.
در فایل global هر گاه متدی را با پیشوند Application_ و نام یکی از رویدادهای بالا بنویسید Asp.net آن را به عنوان هندلری برای رویداد مذکور میشناسد. به عنوان مثال متدی با نام Application_BeginRequest متد رویداد BeginRequest میباشد.
ابتدا یک پروژهی MVC جدید را به نام SimpleApp ایجاد کرده و فایل global آن را مطابق ذیل تغییر میدهیم:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace SimpleApp { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } protected void Application_BeginRequest() { RecordEvent("BeginRequest"); } protected void Application_AuthenticateRequest() { RecordEvent("AuthenticateRequest"); } protected void Application_PostAuthenticateRequest() { RecordEvent("PostAuthenticateRequest"); } private void RecordEvent(string name) { List<string> eventList = Application["events"] as List<string>; if (eventList == null) { Application["events"] = eventList = new List<string>(); } eventList.Add(name); } } }
در اینجا متدی به نام RecordEvent را در کدهای ذکر شده مشاهده میکنید که نام یک رویداد را دریافت و جهت در دسترس قرار دادن در کل برنامه به خاصیت Application از کلاس HttpApplication نسبت داده و متد مذکور را از سه متد دیگر فراخوانی کردهایم. این متدها در زمان رخ دادن رویدادهای BeginRequest, AuthenticateRequest, PostAuthenticateRequest صدا زده خواهند شد.
حال جهت نمایش اطلاعات رویداد نیاز است تغییراتی مشابه ذیل در کنترلر Home ایجاد نماییم.
using System.Web.Mvc; namespace SimpleApp.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(HttpContext.Application["events"]); } } }
ویوی مرتبط با اکشن متد index را مطابق کدهای ذیل بازنویسی میکنیم:
@model List<string> @{ ViewBag.Title = "Events List"; } <h5>Events</h5> <table> @foreach (string eventName in Model) { <tr> <td>@eventName</td> </tr> } </table>
در این مقاله سعی کردیم ابتدا چرخهی حیات یک Request را فرا گرفته و سپس از طریق فایل global و توسط متدهایی با پیشوند Application_ +نام رویداد (اصطلاحا متدهای ویژه نامیده میشوند) چرخه حیات یک درخواست را مدیریت کنیم.
PM> Install-Package snap.structuremap
StructureMap (≥ 2.6.4.1) CommonServiceLocator.StructureMapAdapter (≥ 1.1.0.3) SNAP (≥ 1.8) fasterflect (≥ 2.1.2) Castle.Core (≥ 3.1.0) CommonServiceLocator (≥ 1.0)
تنظیمات SNAP
namespace Framework.UI.Asp { public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { initSnap(); initStructureMap(); } private static void initSnap() { SnapConfiguration.For<StructureMapAspectContainer>(c => { // Tell Snap to intercept types under the "Framework.ServiceLayer..." namespace. c.IncludeNamespace("Framework.ServiceLayer.*"); // Register a custom interceptor (a.k.a. an aspect). c.Bind<Framework.ServiceLayer.Aspects.AuthorizationInterceptor>() .To<Framework.ServiceLayer.Aspects.AuthorizationAttribute>(); }); } void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } private static void initStructureMap() { var thread = StructureMap.Pipeline.Lifecycles.GetLifecycle(InstanceScope.HttpSession); ObjectFactory.Configure(x => { x.For<IUserManager>().Use<EFUserManager>(); x.For<IAuthorizationManager>().LifecycleIs(thread) .Use<EFAuthorizationManager>().Named("_AuthorizationManager"); x.For<Framework.DataLayer.IUnitOfWork>() .Use<Framework.DataLayer.Context>(); x.SetAllProperties(y => { y.OfType<IUserManager>(); y.OfType<Framework.DataLayer.IUnitOfWork>(); y.OfType<Framework.Common.Web.IPageHelpers>(); }); }); } } }
namespace Framework.ServiceLayer.Aspects { public class AuthorizationInterceptor : MethodInterceptor { public override void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute) { var AuthManager = StructureMap.ObjectFactory .GetInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>(); var FullName = GetMethodFullName(method); if (!AuthManager.IsActionAuthorized(FullName)) throw new Common.Exceptions.UnauthorizedAccessException(""); invocation.Proceed(); // the underlying method call } private static string GetMethodFullName(MethodBase method) { var TypeName = (((System.Reflection.MemberInfo)(method)).DeclaringType).FullName; return TypeName + "." + method.Name; } } public class AuthorizationAttribute : MethodInterceptAttribute { }
namespace Snap { public abstract class MethodInterceptor : IAttributeInterceptor, IInterceptor, IHideBaseTypes { protected MethodInterceptor(); public int Order { get; set; } public Type TargetAttribute { get; set; } public virtual void AfterInvocation(); public virtual void BeforeInvocation(); public void Intercept(IInvocation invocation); public abstract void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute); public bool ShouldIntercept(IInvocation invocation); } }
یک نکته
private void Application_PreRequestHandlerExecute(object source, EventArgs e) { var page = HttpContext.Current.Handler as BasePage; // The Page handler if (page == null) return; WireUpThePage(page); WireUpAllUserControls(page); var UsrCod = HttpContext.Current.Session["UsrCod"]; if (UsrCod != null) { var _AuthorizationManager = ObjectFactory .GetNamedInstance<Framework.ServiceLayer.UserManager.IAuthorizationManager>("_AuthorizationManager"); ((Framework.ServiceLayer.UserManager.EFAuthorizationManager)_AuthorizationManager) .AuditUserId = UsrCod.ToString(); } }
namespace Framework.ServiceLayer.UserManager { public class EFUserManager : IUserManager { IUnitOfWork _uow; IDbSet<User> _users; public EFUserManager(IUnitOfWork uow) { _uow = uow; _users = _uow.Set<User>(); } [Framework.ServiceLayer.Aspects.Authorization] public List<User> GetAll() { return _users.ToList<User>(); } } }
- Visual Studio Express 2013 RC for Web یا Visual Studio 2013 RC
- یک حساب کاربری در Windows Azure. میتوانید یک حساب رایگان بسازید.
یک مدیر کلی به حساب کاربری Active Directory خود اضافه کنید
- وارد Windows Azure Portal شوید.
- یک حساب کاربری (Windows Azure Active Directory (AD انتخاب یا ایجاد کنید. اگر قبلا حساب کاربری ساخته اید از همان استفاده کنید در غیر اینصورت یک حساب جدید ایجاد کنید. مشترکین Windows Azure یک AD پیش فرض با نام Default Directory خواهند داشت.
- در حساب کاربری AD خود یک کاربر جدید در نقش Global Administrator بسازید. اکانت AD خود را انتخاب کنید و Add User را کلیک کنید. برای اطلاعات کاملتر به Managing Windows Azure AD from the Windows Azure Portal 1– Sign Up with an Organizational Account مراجعه کنید.
یک نام کاربری انتخاب کرده و به مرحله بعد بروید.
نام کاربری را وارد کنید و نقش Global Administrator را به آن اختصاص دهید. مدیران کلی به یک آدرس ایمیل متناوب هم نیاز دارند. به مرحله بعد بروید.
بر روی Create کلیک کنید و کلمهی عبور موقتی را کپی کنید. پس از اولین ورود باید کلمه عبور را تغییر دهید.
یک اپلیکیشن ASP.NET بسازید
گزینه Organizational Accounts را انتخاب کنید. نام دامنه خود را وارد کنید و سپس گزینه Single Sign On, Read directory data را انتخاب کنید. به مرحله بعد بروید.
نکته: در قسمت More Options می توانید قلمرو اپلیکیشن (Application ID URI) را تنظیم کنید. تنظیمات پیش فرض برای اکثر کاربران مناسب است اما در صورت لزوم میتوانید آنها را ویرایش کنید، مثلا از طریق Windows Azure Portal دامنههای سفارشی خودتان را تنظیم کنید.
اگر گزینه Overwrite را انتخاب کنید اپلیکیشن جدیدی در Windows Azure برای شما ساخته خواهد شد. در غیر اینصورت فریم ورک سعی میکند اپلیکیشنی با شناسه یکسان پیدا کند (در پست متدهای احراز هویت در VS 2013 به تنظیمات این قسمت پرداخته شده).
اطلاعات مدیر کلی دامنه در Active Directory خود را وارد کنید (Credentials) و پروژه را با کلیک کردن بر روی Create Project بسازید.
با کلیدهای ترکیبی Ctrl + F5، اپلیکیشن را اجرا کنید. مرورگر شما باید یک اخطار SSL Certificate به شما بدهد. این بدین دلیل است که مدرک استفاده شده توسط IIS Express مورد اعتماد (trusted) نیست. این اخطار را بپذیرید و اجازه اجرا را به آن بدهید. پس از آنکه اپلیکیشن خود را روی Windows Azure منتشر کردید، این پیغام دیگر تولید نمیشود؛ چرا که Certificateهای استفاده شده trusted هستند.
با حساب کاربری سازمانی (organizational account) که ایجاد کردهاید، وارد شوید.
همانطور که مشاهده میکنید هم اکنون به سایت وارد شده اید.
توزیع اپلیکیشن روی Windows Azure
اپلیکیشن را روی Windows Azure منتشر کنید. روی پروژه کلیک راست کرده و Publish را انتخاب کنید. در مرحله تنظیمات (Settings) مشاهده میکنید که احراز هویت حسابهای سازمانی (organizational accounts) فعال است. همچنین سطح دسترسی به خواندن تنظیم شده است. در قسمت Database رشته اتصال دیتابیس را تنظیم کنید.
حال به وب سایت Windows Azure خود بروید و توسط حساب کاربری ایجاد شده وارد سایت شوید. در این مرحله دیگر نباید خطای امنیتی SSL را دریافت کنید.
خواندن اطلاعات پروفایل کاربران توسط Graph API
[Authorize] public async Task<ActionResult> UserProfile() { string tenantId = ClaimsPrincipal.Current.FindFirst(TenantSchema).Value; // Get a token for calling the Windows Azure Active Directory Graph AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId)); ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey); AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential); string authHeader = assertionCredential.CreateAuthorizationHeader(); string requestUrl = String.Format( CultureInfo.InvariantCulture, GraphUserUrl, HttpUtility.UrlEncode(tenantId), HttpUtility.UrlEncode(User.Identity.Name)); HttpClient client = new HttpClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); request.Headers.TryAddWithoutValidation("Authorization", authHeader); HttpResponseMessage response = await client.SendAsync(request); string responseString = await response.Content.ReadAsStringAsync(); UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(responseString); return View(profile); }
کلیک کردن لینک UserProfile اطلاعات پروفایل کاربر جاری را نمایش میدهد.
اطلاعات بیشتر
معرفی ELMAH
عموما کاربران نمیتوانند گزارش خطای خوبی را ارائه بدهند و البته انتظاری هم از آنان نیست. تنها گزارشی که از یک کاربر دریافت میکنید این است: "برنامه کار نمیکنه!" و همین!
روشهای متعددی برای لاگ کردن خطاهای یک برنامه ASP.Net موجود است؛ چه خودتان آنها را توسعه دهید و یا از ASP.NET health monitoring استفاده کنید.
روش دیگری که این روزها در وبلاگهای متعددی در مورد آن مطلب منتشر میشود، استفاده از ELMAH است. (البته ELMAH به تازگی منتشر نشده ولی تا کیفیت محصولی به عموم ثابت شود مدتی زمان میبرد)
ELMAH یک ماژول رایگان و سورس باز لاگ کردن خطاهای مدیریت نشده برنامههای ASP.Net است. برای استفاده از این ماژول نیازی نیست تا تغییری در برنامه خود ایجاد کنید یا حتی آنرا کامپایل مجدد نمائید. یک فایل dll دارد به همراه کمی تغییر در web.config برنامه جهت معرفی آن و این تمام کاری است که برای برپایی آن لازم است صورت گیرد. این ماژول تمامی خطاهای مدیریت نشدهی برنامه شما را لاگ کرده (در حافظه سرور، در یک فایل xml ، در یک دیتابیس اس کیوال سرور یا اوراکل ، در یک دیتابیس اکسس و یا در یک دیتابیس اس کیوال لایت) و برای مرور آنها یک صفحهی وب سفارشی یا فیدی مخصوص را نیز در اختیار شما قرار میدهد. همچنین این قابلیت را هم دارد که به محض بروز خطایی یک ایمیل را نیز به شما ارسال نماید.
با توجه به اینکه این ماژول در Google code قرار گرفته احتمالا دسترسی به آن مشکل خواهد بود. سورس و فایلهای کامپایل شده آنرا از آدرسهای زیر نیز میتوان دریافت نمود:
نحوه استفاده از ELMAH :
برای استفاده از ELMAH دو کار را باید انجام دهید:
الف) کپی کردن فایل Elmah.dll در پوشه bin برنامه
ب) تنظیم وب کانفیگ برنامه
بهترین مرجع برای آشنایی با نحوه بکار گیری این ماژول، مراجعه به فایل web.config موجود در پوشه samples آن است. بر اساس این فایل نمونه:
- ابتدا باید configSections آن را به وب کانفیگ خود اضافه کنید.
- سپس تگ elmah باید اضافه شود. در این تگ موارد زیر مشخص میشوند:
الف) آیا خطاها توسط آدرس elmah.axd توسط کاربران راه دور قابل مشاهده شود یا خیر.
ب) خطاها کجا ذخیره شوند؟ موارد زیر پشتیبانی میشوند:
دیتابیسهای اس کیوال سرور ، اوراکل ، حافظه سرور، فایلهای xml ، دیتابیس SQLite ، دیتابیس اکسس و یا دیتابیسی از نوع VistaDB
ج) آیا خطاها ایمیل هم بشوند؟ اگر بلی، تگ مربوطه را تنظیم کنید.
د) آیا خطاها به اکانت twitter شما نیز ارسال شوند؟
- در ادامه تگ مربوط به معرفی این httpModules باید تنظیم شود.
- سپس httpHandlers ایی به نام elmah.axd که جهت مرور خطاها میتوان از آن استفاده نمود معرفی میگردد.
- از IIS7 استفاده میکنید؟ قسمت system.webServer را نیز باید اضافه نمائید.
- و در آخر نحوهی دسترسی به elmah.axd مشخص میشود. اگر اجازه دسترسی از راه دور را داده باشید، به این طریق میشود دسترسی را فقط به کاربران مجاز و تعیین اعتبار شده، اعطاء کرد و یا به نقشی مشخص مانند ادمین و غیره.
برای نمونه، اگر بخواهید از دیتابیس SQLite جهت ذخیره سازی خطاهای حاصل شده استفاده نمائید و نیز از ارسال ایمیل صرفنظر کنید، وب کانفیگ برنامه شما باید به شکل زیر تغییر یابد:
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>
<elmah>
<security allowRemoteAccess="1" />
<errorLog type="Elmah.SQLiteErrorLog, Elmah" connectionStringName="cn1" />
</elmah>
<appSettings/>
<connectionStrings>
<add name="cn1" connectionString="data source=~/ErrorsLog/Errors.db" />
</connectionStrings>
<system.web>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</httpModules>
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<compilation debug="true"/>
<authentication mode="Windows" />
</system.web>
</configuration>
سادهترین تنظیم این ماژول استفاده از حالت xml است که به ازای هر خطا یک فایل xml را تولید کرده و نیاز به اسمبلی دیگری بجز ماژول مربوطه نخواهد داشت.
تذکر:
از لحاظ امنیتی مثال فوق توصیه نمیشود زیرا allowRemoteAccess آن 1 است و قسمت authorization ذکر نشده است. این مثال فقط جهت راه اندازی و آزمایش اولیه ارائه گردیده است. (همچنین بهتر است این نام پیش فرض را به نامی دیگر مثلا myloggermdl.axd تغییر داده و در قسمت httpHandlers تنظیم نمائید. سپس این نام را به تگ location نیز اضافه کنید)
اکنون برای مشاهده خروجی این ماژول به انتهای آدرس سایت خود، elmah.axd را اضافه کرده و سپس enter کنید:
همانطور که در تصویر مشخص است، تمامی خطاهای لاگ شده گزارش داده شدهاند. همچنین دو نوع فید به همراه امکان دریافت خطاها به صورت CSV نیز موجود است. با کلیک بر روی لینک details ، صفحهی بسیار ارزندهای ارائه میشود که تقریبا نحوهی وقوع ماجرا را بازسازی میکند.
نکتهی مهمی که در صفحهی جزئیات ارائه میشود (علاوه بر stack trace و مشخصات کاربر)، مقادیر تمامی فیلدهای یک صفحه هنگام بروز خطا است (قسمت Raw/Source data in XML or in JSON در این صفحه) :
به این صورت دیگر نیازی نیست از کاربر بپرسید چه چیزی را وارد کرده بودید که خطا حاصل شد. دقیقا مقادیر فیلدهای همان صفحهی زمان بروز خطا نیز برای شما لاگ میگردد.
نکته:
ماژول SQLite ایی که به همراه مجموعه ELMAH ارائه میشود 32 بیتی کامپایل شده (64 بیتی آن نیز موجود است که باید از آن در صورت لزوم استفاده شود). بنابراین برای اینکه در یک سرور 64 بیتی به مشکل برنخورید و خطای BadImageFormat را دریافت نکنید نیاز است تا به این نکته دقت داشت.
برای مطالعه بیشتر:
Error Logging Modules And Handlers
Sending ELMAH Errors Via GMail
Exception-Driven Development
Using HTTP Modules and Handlers to Create Pluggable ASP.NET Components