در این مقاله موضوعی را مطرح خواهم کرد که شاید برای خیلیها این نوع کد نویسی خوشایند نباشد. حتی برای خود من هم خوشایند نیست؛ ولی نهایتا در بهبود Performance تاثیر خیلی زیادی دارد. به کد زیر دقت کنید.
توضیح کد: فرض کنید سناریوی پروژه ما به این صورت هست که ما یک لیست داریم، شامل 3 فیلد که header، title و footer را در تنظیمات میتوانیم مشخص کنیم که header و footer در شرایطی نمایش داده شود و در شرایطی نمایش داده نشود و یا حالتهای دیگر.
درست است من هم با شما موافق هستم که خوشایند نیست. در این کد ما همهی حالتها را پیش بینی و قالب مناسب هر شرط را درست کردهایم. حجم کد چند برابر شده، ولی از لحاظ Performance در ساخت لیست در View در حد 98% بهبود پیدا کردهاست. همان مثال قبلی را در نظر بگیرید. ng-if مربوط به header و footer در این کد فقط 4 بار بررسی میشود. چه 100 رکورد باشد، چه 1000 تا، چه 10 تا رکورد.
<div ng-repeat="item in items"> <div ng-if="setting.header">{{item.header}}</div> <div>{{item.title}}</div> <div ng-if="setting.footer">{{item.footer}}</div> </div>
خوب مشکل چیست و راهکار چیست؟
فرض کنید لیست ما شامل 100 رکورد هست و در تنظیمات مشخص کردهایم که header نمایش داده شود و footer نمایش داده نشود. اما اتفاقی بدی که میفتد این است که وقتی لیست در View ساخته میشود، 100 بار ng-if مربوط به header و footer چک میشود؛ در جمع 200 بار میشود. چه این مقادیر true باشند چه false فرقی نمیکند و 200 بار بررسی میشود.
راهکار این مشکل به این صورت است که ما باید از ng-if داخل ng-repeat استفاده نکنیم. اما برای پیاده سازی تنظیمات باید ng-ifها را قبل از ng-repeat بررسی کنیم. پس مسلما ng-repeatها باید قالب پیش بینی کرده ما را نسبت به ng-ifها درست کند. نتیجهی کار به صورت کد زیر است که شاید برای شما هم خوشایند نباشد:
<div ng-if="setting.header && setting.footer"> <div ng-repeat="item in items"> <div>{{item.header}}</div> <div>{{item.title}}</div> <div>{{item.footer}}</div> </div> </div> <div ng-if="setting.header && setting.footer==false"> <div ng-repeat="item in items"> <div>{{item.header}}</div> <div>{{item.title}}</div> </div> </div> <div ng-if="setting.header==false && setting.footer"> <div ng-repeat="item in items"> <div>{{item.title}}</div> <div>{{item.footer}}</div> </div> </div> <div ng-if="setting.header==false && setting.footer==false"> <div ng-repeat="item in items"> <div>{{item.title}}</div> </div> </div>
در مورد ng-repeatها هم نگران نباشید فقط یک بار اجرا میشوند. اگر کارکرد ng-if را در مقالهی قبلی من ، خوانده باشید، متوجهی این موضوع میشوید که elementهای داخلی و directionهای AngularJS داخلی ng-if زمانی پردازش میشوند که شرط true باشد. از این روش زمانی استفاده کنید که تعداد دادهها و حالتهای زیادی دارید و Performance اهمیت بیشتری دارد. امیدوارم مقالهی مفیدی باشد.
همیشه نمیتوان کاربران را وادار به استفادهی از صفحهی لاگین برنامهی IDP کرد. ممکن است کاربران بخواهند توسط سطوح دسترسی خود در یک شبکهی ویندوزی به سیستم وارد شوند و یا از Social identity providers مانند تلگرام، گوگل، فیسبوک، توئیتر و امثال آنها برای ورود به سیستم استفاده کنند. برای مثال شاید کاربری بخواهد توسط اکانت گوگل خود به سیستم وارد شود. همچنین مباحث two-factor authentication را نیز باید مدنظر داشت؛ برای مثال ارسال یک کد موقت از طریق ایمیل و یا SMS و ترکیب آن با روش فعلی ورود به سیستم جهت بالا بردن میزان امنیت برنامه.
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
زمانیکه کاربر بر روی یکی از تامین کنندههای لاگین خارجی در صفحهی لاگین کلیک میکند، اکشن Challenge، نام provider مدنظر را دریافت کرده و پس از آن returnUrl را به اکشن متد Callback به صورت query string ارسال میکند. اینجا است که کاربر به تامین کنندهی هویت خارجی مانند گوگل منتقل میشود. البته مدیریت حالت Windows Authentication و استفاده از اکانت ویندوزی در اینجا متفاوت است؛ از این جهت که از returnUrl پشتیبانی نمیکند. در اینجا اطلاعات کاربر از اکانت ویندوزی او به صورت خودکار استخراج شده و به لیست Claims او اضافه میشود. سپس یک کوکی رمزنگاری شده از این اطلاعات تولید میشود تا در ادامه از محتویات آن استفاده شود.
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
بنابراین در ابتدا آنرا با IUsersService تعویض خواهیم کرد:
و سپس تمام ارجاعات قبلی به users_ را نیز توسط امکانات این سرویس اصلاح میکنیم:
الف) در متد FindUserFromExternalProvider
سطر قدیمی
به این صورت تغییر میکند:
در این حالت امضای این متد نیز باید اصلاح شود تا async شده و همچنین User را بجای TestUser بازگشت دهد:
ب) متد AutoProvisionUser قبلی
نیز باید حذف شود؛ زیرا در ادامه آنرا با صفحهی ثبت نام کاربر، جایگزین میکنیم.
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
اکنون نیاز است اطلاعات Provider و ProviderUserId را نیز در اینجا دریافت کرد. به همین جهت ViewModel زیر را به برنامه اضافه میکنیم:
سپس با داشتن اطلاعات FindUserFromExternalProvider که آنرا در قسمت الف اصلاح کردیم، اگر خروجی آن null باشد، یعنی کاربری که از سمت تامین کنندهی هویت خارجی به برنامهی ما وارد شدهاست، دارای اکانتی در سمت IDP نیست. به همین جهت او را به صفحهی ثبت نام کاربر هدایت میکنیم. همچنین پس از پایان کار ثبت نام نیاز است مجددا به همینجا، یعنی متد Callback که فراخوان FindUserFromExternalProvider است، بازگشت:
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
به این ترتیب اطلاعات provider نیز علاوه بر ReturnUrl در اختیار View آن قرار خواهد گرفت. البته RegisterUserViewModel هنوز شامل این خواص اضافی نیست. به همین جهت با ارث بری از RegistrationInputModel، این خواص در اختیار RegisterUserViewModel نیز قرار میگیرند:
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
- با توجه به اینکه کاربر از طریق یک تامین کنندهی هویت خارجی وارد شدهاست، دیگر نیازی به ورود کلمهی عبور ندارد. به همین جهت خاصیت آنرا در ViewModel مربوطه به صورت Required تعریف نکردهایم:
مابقی این فرم ثبت نام مانند قبل خواهد بود.
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
که اینکار را با مقدار دهی UserLogins کاربر در حال ثبت، انجام دادهایم.
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
در این مطلب نحوهی یکپارچه سازی Windows Authentication دومینهای ویندوزی را با IdentityServer بررسی میکنیم.
کار با تامین کنندههای هویت خارجی
اغلب کاربران، دارای اکانت ثبت شدهای در جای دیگری نیز هستند و شاید آنچنان نسبت به ایجاد اکانت جدیدی در IDP ما رضایت نداشته باشند. برای چنین حالتی، امکان یکپارچه سازی IdentityServer با انواع و اقسام IDPهای دیگر نیز پیش بینی شدهاست. در اینجا تمام اینها، روشهای مختلفی برای ورود به سیستم، توسط یک کاربر هستند. کاربر ممکن است توسط اکانت خود در شبکهی ویندوزی به سیستم وارد شود و یا توسط اکانت خود در گوگل، اما در نهایت از دیدگاه سیستم ما، یک کاربر مشخص بیشتر نیست.
نگاهی به شیوهی پشتیبانی از تامین کنندههای هویت خارجی توسط Quick Start UI
Quick Start UI ای را که در «قسمت چهارم - نصب و راه اندازی IdentityServer» به IDP اضافه کردیم، دارای کدهای کار با تامین کنندههای هویت خارجی نیز میباشد. برای بررسی آن، کنترلر DNT.IDP\Controllers\Account\ExternalController.cs را باز کنید:
[HttpGet] public async Task<IActionResult> Challenge(string provider, string returnUrl) [HttpGet] public async Task<IActionResult> Callback()
در اکشن متد Callback، اطلاعات کاربر از کوکی رمزنگاری شدهی متد Challenge استخراج میشود و بر اساس آن هویت کاربر در سطح IDP شکل میگیرد.
فعالسازی Windows Authentication برای ورود به IDP
در ادامه میخواهیم برنامه را جهت استفادهی از اکانت ویندوزی کاربران جهت ورود به IDP تنظیم کنیم. برای این منظور باید نکات مطلب «فعالسازی Windows Authentication در برنامههای ASP.NET Core 2.0» را پیشتر مطالعه کرده باشید.
پس از فعالسازی Windows Authentication در برنامه، اگر برنامهی IDP را توسط IIS و یا IIS Express و یا HttpSys اجرا کنید، دکمهی جدید Windows را در قسمت External Login مشاهده خواهید کرد:
یک نکته: برچسب این دکمه را در حالت استفادهی از مشتقات IIS، به صورت زیر میتوان تغییر داد:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<IISOptions>(iis => { iis.AuthenticationDisplayName = "Windows Account"; iis.AutomaticAuthentication = false; });
اتصال کاربر وارد شدهی از یک تامین کنندهی هویت خارجی به کاربران بانک اطلاعاتی برنامه
سازندهی کنترلر DNT.IDP\Controllers\Account\ExternalController.cs نیز همانند کنترلر Account که آنرا در قسمت قبل تغییر دادیم، از TestUserStore استفاده میکند:
public ExternalController( IIdentityServerInteractionService interaction, IClientStore clientStore, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _events = events; }
private readonly IUsersService _usersService; public ExternalController( // ... IUsersService usersService) { // ... _usersService = usersService; }
الف) در متد FindUserFromExternalProvider
سطر قدیمی
var user = _users.FindByExternalProvider(provider, providerUserId);
var user = await _usersService.GetUserByProviderAsync(provider, providerUserId);
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)> FindUserFromExternalProvider(AuthenticateResult result)
private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable<Claim> claims) { var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); return user; }
مفهوم «Provisioning a user» در اینجا به معنای درخواست از کاربر، جهت ورود اطلاعاتی مانند نام و نام خانوادگی او است که پیشتر صفحهی ثبت کاربر جدید را برای این منظور در قسمت قبل ایجاد کردهایم و از آن میشود در اینجا استفادهی مجدد کرد. بنابراین در ادامه، گردش کاری ورود کاربر از طریق تامین کنندهی هویت خارجی را به نحوی اصلاح میکنیم که کاربر جدید، ابتدا به صفحهی ثبت نام وارد شود و اطلاعات تکمیلی خود را وارد کند؛ سپس به صورت خودکار به متد Callback بازگشته و ادامهی مراحل را طی نماید:
در اکشن متد نمایش صفحهی ثبت نام کاربر جدید، متد RegisterUser تنها آدرس بازگشت به صفحهی قبلی را دریافت میکند:
[HttpGet] public IActionResult RegisterUser(string returnUrl)
namespace DNT.IDP.Controllers.UserRegistration { public class RegistrationInputModel { public string ReturnUrl { get; set; } public string Provider { get; set; } public string ProviderUserId { get; set; } public bool IsProvisioningFromExternal => !string.IsNullOrWhiteSpace(Provider); } }
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد.
در ادامه نیاز است امضای متد نمایش صفحهی ثبت نام را نیز بر این اساس اصلاح کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpGet] public IActionResult RegisterUser(RegistrationInputModel registrationInputModel) { var vm = new RegisterUserViewModel { ReturnUrl = registrationInputModel.ReturnUrl, Provider = registrationInputModel.Provider, ProviderUserId = registrationInputModel.ProviderUserId }; return View(vm); }
namespace DNT.IDP.Controllers.UserRegistration { public class RegisterUserViewModel : RegistrationInputModel {
اکنون نیاز است RegisterUser.cshtml را اصلاح کنیم:
- ابتدا دو فیلد مخفی دیگر Provider و ProviderUserId را نیز به این فرم اضافه میکنیم؛ از این جهت که در حین postback به سمت سرور به مقادیر آنها نیاز داریم:
<inputtype="hidden"asp-for="ReturnUrl"/> <inputtype="hidden"asp-for="Provider"/> <inputtype="hidden"asp-for="ProviderUserId"/>
@if (!Model.IsProvisioningFromExternal) { <div> <label asp-for="Password"></label> <input type="password" placeholder="Password" asp-for="Password" autocomplete="off"> </div> }
پس از آن نیاز است اطلاعات اکانت خارجی این کاربر را در حین postback و ارسال اطلاعات به اکشن متد RegisterUser، ثبت کنیم:
namespace DNT.IDP.Controllers.UserRegistration { public class UserRegistrationController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> RegisterUser(RegisterUserViewModel model) { // ... if (model.IsProvisioningFromExternal) { userToCreate.UserLogins.Add(new UserLogin { LoginProvider = model.Provider, ProviderKey = model.ProviderUserId }); } // add it through the repository await _usersService.AddUserAsync(userToCreate); // ... } }
همچنین در ادامهی این اکشن متد، کار لاگین خودکار کاربر نیز انجام میشود. با توجه به اینکه پس از ثبت اطلاعات کاربر نیاز است مجددا گردش کاری اکشن متد Callback طی شود، این لاگین خودکار را نیز برای حالت ورود از طریق تامین کنندهی خارجی، غیرفعال میکنیم:
if (!model.IsProvisioningFromExternal) { // log the user in // issue authentication cookie with subject ID and username var props = new AuthenticationProperties { IsPersistent = false, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; await HttpContext.SignInAsync(userToCreate.SubjectId, userToCreate.Username, props); }
بررسی ورود به سیستم توسط دکمهی External Login -> Windows
پس از این تغییرات، اکنون در حین ورود به سیستم (تصویر ابتدای بحث در قسمت فعالسازی اعتبارسنجی ویندوزی)، گزینهی External Login -> Windows را انتخاب میکنیم. بلافاصله به صفحهی ثبتنام کاربر هدایت خواهیم شد:
همانطور که مشاهده میکنید، IDP اکانت ویندوزی جاری را تشخیص داده و فعال کردهاست. همچنین در اینجا خبری از ورود کلمهی عبور هم نیست.
پس از تکمیل این فرم، بلافاصله کار ثبت اطلاعات کاربر و هدایت خودکار به برنامهی MVC Client انجام میشود.
در ادامه از برنامهی کلاینت logout کنید. اکنون در صفحهی login مجددا بر روی دکمهی Windows کلیک نمائید. اینبار بدون پرسیدن سؤالی، لاگین شده و وارد برنامهی کلاینت خواهید شد؛ چون پیشتر کار اتصال اکانت ویندوزی به اکانتی در سمت IDP انجام شدهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
یک نکته: برای آزمایش برنامه جهت فعالسازی Windows Authentication بهتر است برنامهی IDP را توسط IIS Express اجرا کنید و یا اگر از IIS Express استفاده نمیکنید، نیاز است UseHttpSys فایل program.cs را مطابق توضیحات «یک نکتهی تکمیلی: UseHttpSys و استفادهی از HTTPS» فعال کنید.
مدتی است که حالت READ_COMMITTED_SNAPSHOT بسیار مورد توجه واقع شده:
- در سایت Stack overflow از آن استفاده میشود (^).
- در SQL Server Azure حالت پیش فرض ایجاد دیتابیسها و تراکنشهای جدید است (^).
- در Entity framework 6 حالت پیش فرض تراکنشهای ایجاد شده، قرار گرفته است (^ ).
و ... در Oracle، تنها حالت مدیریت مسایل همزمانی است! (البته به نام MVCC، اما با همین عملکرد)
اما READ_COMMITTED_SNAPSHOT در SQL Server چیست و کاربرد آن کجا است؟
اگر استفاده گسترده و سنگینی از SQL Server داشته باشید، حتما به پیغامهای خطای deadlock آن برخوردهاید:
روش پیش فرض مدیریت مسایل همزمانی در SQL Server، حالت READ COMMITTED است. به این معنا که اگر در طی یک تراکنش مشغول به تغییر اطلاعاتی باشیم، سایر کاربران از خواندن نتیجه آن (اصطلاحا به آن Dirty read گفته میشود) منع خواهند شد؛ تا زمانیکه این تراکنش با موفقیت به پایان برسد. هرچند در این حالت سایر تراکنشها امکان ویرایش یا حذف اطلاعات را خواهند داشت. به علاوه اگر در طی این تراکنش، اطلاعاتی خوانده شوند، سایر تراکنشها تا پایان تراکنش جاری، قادر به تغییر این اطلاعات خوانده شده نخواهند بود (منشاء بروز خطاهای deadlock یاد شده در سیستمهای پرترافیک).
در SQL Server 2005 برای بهبود مقیاس پذیری SQL Server و کاهش خطاهای deadlock، مکانیزم READ_COMMITTED_SNAPSHOT معرفی گشت.
به صورت خلاصه زمانیکه که تراکنش مورد نظر تحت حالت READ COMMITTED SNAPSHOT انجام میشود، optimistic reads and pessimistic writes خواهیم داشت (خواندنهای خوشبینانه و نوشتنهای بدبینانه). در این حالت تضمین میشود که خواندن اطلاعات داخل یک تراکنش، شامل اطلاعات تغییر یافته توسط سایر تراکنشهای همزمان نخواهد بود. همچنین زمانیکه در این بین، اطلاعاتی خوانده میشود، بر روی این اطلاعات برخلاف حالت READ COMMITTED قفل قرار داده نمیشود. بنابراین تراکنشهایی که درحال خواندن اطلاعات هستند، تراکنشهای همزمانی را که در حال نوشتن اطلاعات میباشند، قفل نخواهد کرد و برعکس.
نحوه فعال سازی READ_COMMITTED_SNAPSHOT
فعال سازی READ_COMMITTED_SNAPSHOT باید ابتدا در سطح یک بانک اطلاعاتی SQL Server انجام شود:
کاری که در اینجا انجام خواهد شد، ایجاد یک snapshot یا یک کپی فقط خواندنی، از بانک اطلاعاتی کاری شما میباشد. بنابراین در این حالت، زمانیکه یک عبارت Select را فراخوانی میکنید، این خواندن، از بانک اطلاعاتی فقط خواندنی تشکیل شده، صورت خواهد گرفت. اما تغییرات بر روی دیتابیس اصلی کاری درج شده و سپس این snapshot به روز میشود.
حالت READ_COMMITTED_SNAPSHOT خصوصا برای برنامههای وبی که تعداد بالایی Read در مقابل تعداد کمی Write دارند، به شدت بر روی کارآیی و بالا رفتن سرعت و مقیاس پذیری آنها تاثیر خواهد داشت؛ به همراه حداقل تعداد deadlockهای حاصل شده.
در Entity framework وضعیت به چه صورتی است؟
EF از حالت پیش فرض مدیریت مسایل همزمانی در SQL Server یا همان حالت READ COMMITTED در زمان فراخوانی متد SaveChanges استفاده میکند.
در EF 6 این حالت پیش فرض به READ_COMMITTED_SNAPSHOT تغییر کرده است. البته همانطور که عنوان شد، پیشتر باید بانک اطلاعاتی را نیز جهت پذیرش این نوع تراکنشها آماده ساخت.
اگر از نگارشهای پایینتر از EF 6 استفاده میکنید، برای استفاده از حالت READ_COMMITTED_SNAPSHOT باید صراحتا IsolationLevel را مشخص ساخت:
- در سایت Stack overflow از آن استفاده میشود (^).
- در SQL Server Azure حالت پیش فرض ایجاد دیتابیسها و تراکنشهای جدید است (^).
- در Entity framework 6 حالت پیش فرض تراکنشهای ایجاد شده، قرار گرفته است (^ ).
و ... در Oracle، تنها حالت مدیریت مسایل همزمانی است! (البته به نام MVCC، اما با همین عملکرد)
اما READ_COMMITTED_SNAPSHOT در SQL Server چیست و کاربرد آن کجا است؟
اگر استفاده گسترده و سنگینی از SQL Server داشته باشید، حتما به پیغامهای خطای deadlock آن برخوردهاید:
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
در SQL Server 2005 برای بهبود مقیاس پذیری SQL Server و کاهش خطاهای deadlock، مکانیزم READ_COMMITTED_SNAPSHOT معرفی گشت.
به صورت خلاصه زمانیکه که تراکنش مورد نظر تحت حالت READ COMMITTED SNAPSHOT انجام میشود، optimistic reads and pessimistic writes خواهیم داشت (خواندنهای خوشبینانه و نوشتنهای بدبینانه). در این حالت تضمین میشود که خواندن اطلاعات داخل یک تراکنش، شامل اطلاعات تغییر یافته توسط سایر تراکنشهای همزمان نخواهد بود. همچنین زمانیکه در این بین، اطلاعاتی خوانده میشود، بر روی این اطلاعات برخلاف حالت READ COMMITTED قفل قرار داده نمیشود. بنابراین تراکنشهایی که درحال خواندن اطلاعات هستند، تراکنشهای همزمانی را که در حال نوشتن اطلاعات میباشند، قفل نخواهد کرد و برعکس.
نحوه فعال سازی READ_COMMITTED_SNAPSHOT
فعال سازی READ_COMMITTED_SNAPSHOT باید ابتدا در سطح یک بانک اطلاعاتی SQL Server انجام شود:
ALTER DATABASE testDatabase SET ALLOW_SNAPSHOT_ISOLATION ON; ALTER DATABASE testDatabase SET READ_COMMITTED_SNAPSHOT ON;
حالت READ_COMMITTED_SNAPSHOT خصوصا برای برنامههای وبی که تعداد بالایی Read در مقابل تعداد کمی Write دارند، به شدت بر روی کارآیی و بالا رفتن سرعت و مقیاس پذیری آنها تاثیر خواهد داشت؛ به همراه حداقل تعداد deadlockهای حاصل شده.
در Entity framework وضعیت به چه صورتی است؟
EF از حالت پیش فرض مدیریت مسایل همزمانی در SQL Server یا همان حالت READ COMMITTED در زمان فراخوانی متد SaveChanges استفاده میکند.
در EF 6 این حالت پیش فرض به READ_COMMITTED_SNAPSHOT تغییر کرده است. البته همانطور که عنوان شد، پیشتر باید بانک اطلاعاتی را نیز جهت پذیرش این نوع تراکنشها آماده ساخت.
اگر از نگارشهای پایینتر از EF 6 استفاده میکنید، برای استفاده از حالت READ_COMMITTED_SNAPSHOT باید صراحتا IsolationLevel را مشخص ساخت:
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel= IsolationLevel.Snapshot })) { // update some tables using entity framework context.SaveChanges(); transactionScope.Complete(); }
NuGet چیست؟
روش متداول استفاده از کتابخانههای موجود دات نتی در Visual studio عموما به این صورت است: مراجعه به سایت مربوطه، دریافت بسته مورد نظر، باز کردن آن و سپس افزودن ارجاعی به اسمبلیهای آن کتابخانه. در این حالت زمانیکه نسخهی جدیدی از کتابخانهی مورد استفاده ارائه شود (و عموما تا مدتها شاید از آن بیاطلاع باشیم) تمام این مراحل باید از ابتدا تکرار شوند و همینطور الی آخر.
برای رفع این نقیصه، تیم ASP.NET، افزونهای سورس باز و رایگان را به نام NuGet جهت VS.Net 2010 طراحی کردهاند که کار مدیریت بستههای کتابخانههای مورد استفاده را بسیار ساده کرده است. امکانات این افزونه پس از نصب، در دو حالت استفاده از رابط گرافیکی کاربری آن و یا با استفاده از خط فرمان PowerShell ویندوز در دسترس خواهد بود. این افزونه در زمان بارگذاری، با مراجعه به فید سایت مرکزی خود، لیست بستههای مهیا را در اختیار علاقمندان قرار میدهد. درب این سایت مرکزی به روی تمام توسعه دهندهها جهت افزودن بستههای خود باز است.
و ... فراگیری کار با NuGet برای تمام برنامه نویسان دات نت لازم و ضروری است! از این جهت که پیغام "این بسته تنها برای NuGet عرضه شده است" کم کم در حال متداول شدن میباشد و دیگر سایتهای مرتبط، لینک مستقیمی را جهت دریافت کتابخانههای خود ارائه نمیدهند. حتی خبر به روز شدن محصولات خود را هم شاید دیگر به صورت منظم ارائه ندهند؛ زیرا NuGet کار مدیریت آنها را به عهده خواهد داشت.
دریافت و نصب NuGet
NuGet را حداقل به سه طریق میتوان دریافت و نصب کرد:
الف) با مراجعه به سایت CodePlex : (+)
ب) دریافت آن از سایت گالریهای آن : (+)
ج) با استفاده از امکانات VS.NET
هر سه روش فوق به دریافت و نصب فایل NuGet.Tools.vsix منتهی میشوند. برای مثال در روش (ج) باید به منوی Tools و گزینهی Extension Manager مراجعه کنید. سپس برگهی Online Gallery را گشوده و اندکی صبر کنید تا اطلاعات آن دریافت و نمایش داده شود. سپس NuGet را در Search box بالای صفحه نوشته و NuGet Package manager ظاهر شده را انتخاب و نصب کنید.
نحوه استفاده از NuGet
فرض کنید یک پروژه جدید ASP.NET را ایجاد کردهاید و نیاز است تا کتابخانهی ELMAH به آن اضافه شود. روش انجام اینکار را به کمک NuGet در ادامه بررسی خواهیم کرد (کمتر از یک دقیقه زمان خواهد برد):
الف) با کمک امکانات رابط گرافیکی کاربر آن
سادهترین روش استفاده از NuGet ، کلیک راست بر روی پوشه References در Solution explorer و سپس انتخاب گزینهی Add Library Package Reference میباشد:
در صفحهی باز شده، برگهی Online را باز کنید و مدتی صبر نمائید تا اطلاعات لازم دریافت گردد (در زمان نگارش این مطلب، 1135 بسته در این مخزن موجود است):
سپس در جعبهی جستجوی سمت راست بالای صفحه، نام کتابخانهی مورد نظر را نوشته و اندکی صبر کنید تا اطلاعات آن نمایش داده شود:
اکنون با کلیک بر روی دکمه Install ، بسته مرتبط با این کتابخانه دریافت شده و سپس به صورت خودکار ارجاعی به آن نیز افزوده خواهد شد. همچنین تنظیمات مرتبط با فایل Config برنامه هم اضافه میشوند.
روش دیگر ظاهر کردن این صفحه، مراجعه به منوی Tools و گزینهی Library Package Manager میباشد:
جهت دریافت به روز رسانیهای بستههای نصب شده تنها کافی است به برگهی Updates این صفحه مراجعه کرده و موارد لیست شده را نصب نمائیم:
نکته: NuGet در SharpDevelop 4.1 به بعد هم پشتیبانی میشود:
ب) با استفاده از امکانات خط فرمان PowerShell ویندوز
برای استفاده از امکانات پاورشل ویندوز نیاز است تا پاورشل نگارش 2 بر روی سیستم شما نصب باشد (نیاز به Windows XP with Service Pack 3 به بعد دارد). سپس به منوی Tools ، قسمت Library Package Manager ، گزینهی Package Manager Console آن جهت فعال سازی کنسول پاور شل در VS.NET مراجعه نمائید:
نکته: در تصویر فوق پس از نوشتن el ، دکمه tab فشرده شده است. در این حالت منوی پکیجهای مهیای شروع شده با el، از سایت مرکزی NuGet ظاهر گردیده است.
فرامین مهمی که در اینجا در دسترس هستند شامل: List-Package ، Uninstall-Package ، Update-Package و Get-Package میباشند. برای مثال اگر قصد جستجو در بین بستههای موجود را داشته باشید Get-Package بسیار مفید است:
برای مثال جهت یافتن بستههای مرتبط با wpf و silverlight به صورت زیر میتوان عمل کرد:
PM> get-package -remote -filter wpf
PM> get-package -remote -filter silverlight
نکته: روش دیگر جستجو در بین بستههای مهیا، مراجعه به سایت گالری آن است : (+) . در اینجا دستور پاورشل نصب هر بستهی یافت شده نیز نمایش داده میشود.
ج) استفاده از برنامه NuGet.exe
برنامه NuGet.exe از سایت CodePlex قابل دریافت است. این روش هم جهت علاقمندان به خط فرمان تدارک دیده شده است!
پس از دریافت آن فرض کنید میخواهیم تمام بستههای شروع شده با nhi را لیست کنیم. برای این منظور دستور خط فرمان ذیل را صادر کنید:
D:\Test>nuget list nhi
D:\Test>nuget install NHibernate
به این صورت کتابخانه NHibernate به همراه تمام وابستگیهای آن دریافت خواهد شد.
به روز رسانی خودکار NuGet
برای به روز رسانی برنامه nuget.exe دستور زیر را میتوان صادر نمود:
D:\Test>NuGet.exe u
Tools | Options, then Environment | Extension Manager and click "Automatically check for updates to installed extensions."
ادامه دارد ...
مطلب مفید و کاربردی میباشد ، ممنون.
آیا سنارویو زیر با این کتابخانه قابل انجام میباشد :
بنده میخوام بعد از اینکه کاربر به درگاه پرداخت فرستاده شده ، تا نیم ساعت بعد از آن اگر پاسخی از بانک دریافت نشد تا سه مرتبه هر ده دقیه یک بار وضعیت پرداخت رو بررسی کنم .
آیا سنارویو زیر با این کتابخانه قابل انجام میباشد :
بنده میخوام بعد از اینکه کاربر به درگاه پرداخت فرستاده شده ، تا نیم ساعت بعد از آن اگر پاسخی از بانک دریافت نشد تا سه مرتبه هر ده دقیه یک بار وضعیت پرداخت رو بررسی کنم .
روش متداول کار با کتابخانهی iTextSharp ، ایجاد شیء Document ، سپس ایجاد PdfWriter برای نوشتن در آن، گشودن سند و ... افزودن اشیایی مانند Paragraph ، PdfPTable ، PdfPCell و غیره به آن است و در نهایت بستن سند. راه میانبری هم برای کار با این کتابخانه وجود دارد و آن هم استفاده از امکانات فضای نام iTextSharp.text.html.simpleparser آن میباشد. به این ترتیب میتوان به صورت خودکار، یک محتوای HTML را تبدیل به فایل PDF کرد.
مثال : نمایش یک متن HTML ساده انگلیسی
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;
namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();
var html = @"<span style='color:blue'><b>Testing</b></span>
<i>iTextSharp's</i> <u>HTML to PDF capabilities</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), null);
foreach (var htmlElement in parsedHtmlElements)
{
pdfDoc.Add(htmlElement);
}
}
//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}
نکتهی جدید کد فوق، استفاده از متد HTMLWorker.ParseToList است. به این ترتیب parser کتابخانهی iTextSharp وارد عمل شده و html تعریف شده را به معادل المانهای بومی خودش تبدیل میکند؛ مثلا تبدیل به chunk یا pdfptable و امثال آن. در نهایت در طی یک حلقه، این عناصر به صفحه اضافه میشوند.
البته باید دقت داشت که HTMLWorker امکان تبدیل عناصر پیچیده، تودرتو و چندلایه HTML را ندارد؛ اما بهتر از هیچی است!
همهی اینها خوب! اما به درد ما فارسی زبانها نمیخورد. همین متغیر html فوق را با یک متن فارسی جایگزین کنید، چیزی نمایش داده نخواهد شد. البته این هم نکته دارد که در ادامه ذکر خواهد شد.
جهت نمایش متون فارسی نیاز است تا نکات ذکر شده در مطلب «فارسی نویسی و iTextSharp» رعایت شوند که شامل:
- تعیین صریح قلم
- تعیین encoding
- استفاده از عناصر دربرگیرندهای است که خاصیت RunDirection را پشتیبانی میکنند؛ مانند PdfPCell و غیره
به این ترتیب خواهیم داشت:
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.html.simpleparser;
using iTextSharp.text.pdf;
using iTextSharp.text.html;
namespace HeadersAndFooters
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();
//روش صحیح تعریف فونت
FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");
StyleSheet styles = new StyleSheet();
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.FONTFAMILY, "tahoma");
styles.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, "Identity-H");
var html = @"<span style='color:blue'><b>آزمایش</b></span>
کتابخانه <i>iTextSharp</i> <u>جهت بررسی فارسی نویسی</u>";
var parsedHtmlElements = HTMLWorker.ParseToList(new StringReader(html), styles);
PdfPCell pdfCell = new PdfPCell { Border = 0 };
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
foreach (var htmlElement in parsedHtmlElements)
{
pdfCell.AddElement(htmlElement);
}
var table1 = new PdfPTable(1);
table1.AddCell(pdfCell);
pdfDoc.Add(table1);
}
//open the final file with adobe reader for instance.
Process.Start("Test.pdf");
}
}
}
همانطور که ملاحظه میکنید ابتدا قلمی در cache قلمهای این کتابخانه ثبت میشود (FontFactory.Register). سپس نوع قلم و encoding آن توسط یک StyleSheet تعریف شده و به HTMLWorker.ParseToList ارسال میگردد و در نهایت به کمک یک المان دارای RunDirection، در صفحه نمایش داده میشود.
نکته:
ممکن است که به متغیر html ، یک table ساده html را نسبت دهید. در این حالت پس از تنظیم style یاد شده، در هر سلول این html table ، متون فارسی به صورت معکوس نمایش داده خواهند شد که این هم یک نکتهی کوچک دیگر دارد:
foreach (var htmlElement in parsedHtmlElements)
{
if (htmlElement is PdfPTable)
{
var table = (PdfPTable)htmlElement;
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
foreach (var row in table.Rows)
{
foreach (var cell in row.GetCells())
{
cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
}
}
}
pdfCell.AddElement(htmlElement);
}
در قسمتی که قرار است المانهای معادل به pdfCell اضافه شوند، آنها را بررسی کرده و RunDirection آنها را RTL خواهیم کرد.
کاربردها:
بدیهی است این حالت برای تهیه گزارشات پیشرفتهتر برای مثال تهیه قالبهایی که در حین تهیه PDF ، قسمتهایی از آنها توسط برنامه نویس Replace میشوند، بسیار مناسب است.
همچنین مطلب «بارگذاری یک یوزرکنترل با استفاده از جیکوئری» و متد RenderUserControl مطرح شده در آن که در نهایت یک قطعه کد HTML را به صورت رشته به ما تحویل میدهد، میتواند جهت تهیه گزارشهای پویایی که برای مثال قسمتی از آن یک GridView بایند شده حاصل از یک یوزر کنترل است، مورد استفاده قرار گیرد.
نظرات مطالب
ASP.NET MVC #12
- از Html.RenderAction استفاده کنید.
+ و یا همچنین layout، مدل محتوای خودش را به ارث میبرد. یعنی مدلی که در View تنظیم میشود، همان مدلی است که layout به آن دسترسی خواهد داشت. به همین جهت مثلا میتونید توسط ViewBag، عنوان صفحه را که در layout تعریف شده، مقدار دهی کنید.
اگر میخواهید Strongly typed کار کنید، روش Html.RenderAction یک راه حل است و روش دوم به صورت زیر است:
یک کلاس پایه abstract تعریف کنید:
بعد تمام مدلها یا ViewModelهایی که قرار است در برنامه شما به Viewها ارسال شوند، باید از این کلاس پایه ارث بری کنند. مثلا:
در این حالت و با رعایت این شرط، میتونید در فایل layout، نوع مدل را بجای حالت dynamic فعلی، تبدیل کنید به نوع کلاس پایهایی که ذکر شد:
الان در layout، نوع کلاس پایه، به عنوان نوع مدل اصلی تعریف شده. بنابراین در این فایل layout مشترک بین تمام Viewها، خواص قرار گرفته شده در کلاس پایهای که توسط ViewModelها به Viewها ارسال میشوند، به صورت strongly typed قابل دسترسی خواهند بود.
+ و یا همچنین layout، مدل محتوای خودش را به ارث میبرد. یعنی مدلی که در View تنظیم میشود، همان مدلی است که layout به آن دسترسی خواهد داشت. به همین جهت مثلا میتونید توسط ViewBag، عنوان صفحه را که در layout تعریف شده، مقدار دهی کنید.
اگر میخواهید Strongly typed کار کنید، روش Html.RenderAction یک راه حل است و روش دوم به صورت زیر است:
یک کلاس پایه abstract تعریف کنید:
public abstract class BaseViewModel { public string Name { get; set; } }
public class HomeViewModel : BaseViewModel { public int Data1 { set; get;} // ... }
@model BaseViewModel <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Test</title> </head> <body> <header> Hello @Model.Name </header> <div> @this.RenderBody() </div> </body> </html>
ممنون از پاسخگویی شما.
بله درسته بسته نیوگت سیگنال آر نسخه قدیمیتر بود.
بله درسته بسته نیوگت سیگنال آر نسخه قدیمیتر بود.