//search for person with ID = 1 in year 92. using (var context = new TestContextNew()) { // در اینجا هم باید بنحوی بتوان با مشخص کردن سال مورد نظر اطلاعات از جدول مربوطه لود شود //Info_92 مثلا برای سال 92 از جدول var result = from h in context.Info_News where h.ID == 1 select h; dataGridView1.DataSource = result.ToList(); }
مراحل تنظیم Let's Encrypt در IIS
یک نکتهی تکمیلی
ACME V1 تا چند ماه دیگر به پایان خواهد رسید:In June of 2020 we will stop allowing new domains to validate via ACMEv1.
A simple Windows ACMEv2 client (WACS) Software version 2.1.3.671 (RELEASE, PLUGGABLE) IIS version 7.5 Running with administrator credentials Scheduled task not configured yet Please report issues at https://github.com/PKISharp/win-acme N: Create new certificate (simple for IIS) M: Create new certificate (full options) L: List scheduled renewals R: Renew scheduled S: Renew specific A: Renew *all* O: More options... Q: Quit Please choose from the menu: m Running in mode: Interactive, Advanced Please specify how the list of domain names that will be included in the certificate should be determined. If you choose for one of the "all bindings" options, the list will automatically be updated for future renewals to reflect the bindings at that time. 1: IIS 2: Manual input 3: CSR created by another program C: Abort How shall we determine the domain(s) to include in the certificate?: 1 Please select which website(s) should be scanned for host names. You may input one or more site identifiers (comma separated) to filter by those sites, or alternatively leave the input empty to scan *all* websites. 1: Default Web Site (2 bindings) Site identifier(s) or <ENTER> to choose all: 1 1: dotnettips.info (Site 1) 2: www.dotnettips.info (Site 1) You may either choose to include all listed bindings as host names in your certificate, or apply an additional filter. Different types of filters are available. 1: Pick specific bindings from the list 2: Pick bindings based on a search pattern 3: Pick bindings based on a regular expression 4: Pick *all* bindings How do you want to pick the bindings?: 4 1: dotnettips.info (Site 1) 2: www.dotnettips.info (Site 1) Please pick the most important host name from the list. This will be displayed to your users as the subject of the certificate. Common name: 2 1: dotnettips.info (Site 1) 2: www.dotnettips.info (Site 1) Continue with this selection? (y*/n) - yes Target generated using plugin IIS: www.dotnettips.info and 1 alternatives Suggested friendly name '[IIS] Default Web Site, (any host)', press <ENTER> to accept or type an alternative: <Enter> The ACME server will need to verify that you are the owner of the domain names that you are requesting the certificate for. This happens both during initial setup *and* for every future renewal. There are two main methods of doing so: answering specific http requests (http-01) or create specific dns records (dns-01). For wildcard domains the latter is the only option. Various additional plugins are available from https://github.com/PKISharp/win-acme/. 1: [http-01] Save verification files on (network) path 2: [http-01] Serve verification files from memory (recommended) 3: [http-01] Upload verification files via FTP(S) 4: [http-01] Upload verification files via SSH-FTP 5: [http-01] Upload verification files via WebDav 6: [dns-01] Create verification records manually (auto-renew not possible) 7: [dns-01] Create verification records with acme-dns (https://github.com/joohoi/acme-dns) 8: [dns-01] Create verification records with your own script 9: [tls-alpn-01] Answer TLS verification request from win-acme C: Abort How would you like prove ownership for the domain(s) in the certificate?: 2 After ownership of the domain(s) has been proven, we will create a Certificate Signing Request (CSR) to obtain the actual certificate. The CSR determines properties of the certificate like which (type of) key to use. If you are not sure what to pick here, RSA is the safe default. 1: Elliptic Curve key 2: RSA key What kind of private key should be used for the certificate?: 2 When we have the certificate, you can store in one or more ways to make it accessible to your applications. The Windows Certificate Store is the default location for IIS (unless you are managing a cluster of them). 1: IIS Central Certificate Store (.pfx per domain) 2: PEM encoded files (Apache, nginx, etc.) 3: Windows Certificate Store C: Abort How would you like to store the certificate?: 3 1: IIS Central Certificate Store (.pfx per domain) 2: PEM encoded files (Apache, nginx, etc.) 3: No additional storage steps required C: Abort Would you like to store it in another way too?: 3 With the certificate saved to the store(s) of your choice, you may choose one or more steps to update your applications, e.g. to configure the new thumbprint, or to update bindings. 1: Create or update https bindings in IIS 2: Create or update ftps bindings in IIS 3: Start external script or program 4: Do not run any (extra) installation steps Which installation step should run first?: 1 Use different site for installation? (y/n*) - no 1: Create or update ftps bindings in IIS 2: Start external script or program 3: Do not run any (extra) installation steps Add another installation step?: 3 Enter email(s) for notifications about problems and abuse (comma seperated): name@site.com Terms of service: C:\ProgramData\win-acme\acme-v02.api.letsencrypt.org\LE-SA-v1.2-November-15-2017.pdf Open in default application? (y/n*) - no Do you agree with the terms? (y*/n) - yes Authorize identifier: dotnettips.info Authorizing dotnettips.info using http-01 validation (SelfHosting) Authorization result: valid Authorize identifier: www.dotnettips.info Authorizing www.dotnettips.info using http-01 validation (SelfHosting) Authorization result: valid Requesting certificate [IIS] Default Web Site, (any host) Store with CertificateStore... Installing certificate in the certificate store Adding certificate [IIS] Default Web Site, (any host) @ 2020/2/1 9:43:55 to store My Installing with IIS... Updating existing https binding www.dotnettips.info:443 (flags: 0) Updating existing https binding dotnettips.info:443 (flags: 0) Committing 2 https binding changes to IIS Adding Task Scheduler entry with the following settings - Name win-acme renew (acme-v02.api.letsencrypt.org) - Path C:\Programs\win-acme.v2.1.3.671.x64.pluggable - Command wacs.exe --renew --baseuri "https://acme-v02.api.letsencrypt.org/" - Start at 09:00:00 - Time limit 02:00:00 Do you want to specify the user the task will run as? (y/n*) - no
اگر شما یک فرم تماس با ما داشته باشید استفاده از کپچا یک مکانیزم امنیتی معقول میباشد و همچنین اگر فرمی جهت ارسال پست داشته باشید. اما در برخی مواقع مانند فرمهای ارسال کامنت، پاسخ، چت و ... امکان استفاده از این روش وجود ندارد و باید به فکر راه حلی مناسب برای مقابل با درخواستهای مخرب باشیم.
اگر شما هم به دنبال تامین امنیت سایت خود هستید و دوست ندارید که وب سایت شما (به دلیل کمبود پهنای باند یا ارسال مطالب نامربوط که گاهی اوقات به صدها هزار مورد میرسد) از دسترس خارج شود این آموزش را دنبال کنید.
برای این منظور ما از یک ActionFilter برای امضای ActionMethodهایی استفاده میکنیم که باید با ارسالهای متعدد از سوی یک کاربر مقابله کنند. این ActionFilter باید قابلیت تنظیم حداقل زمان بین درخواستها را داشته باشد و اگر درخواستی در زمانی کمتر از مدت مجاز تعیین شده برسد، به نحوی مطلوبی به آن رسیدگی کند.
پس از آن ما نیازمند مکانیزمی هستیم تا درخواستهای رسیدهی از سوی هرکاربر را به شکلی کاملا خاص و یکتا شناسایی کند. راه حلی که قرار است در این ActionFilter از آن استفاده کنم به شرح زیر است:
ما به دنبال آن هستیم که یک شناسهی منحصر به فرد را برای هر درخواست ایجاد کنیم. لذا از اطلاعات شیئ Request جاری برای این منظور استفاده میکنیم.
1) IP درخواست جاری (قابل بازیابی از هدر HTTP_X_FORWARDED_FOR یا REMOTE_ADDR)
2) مشخصات مرورگر کاربر (قابل بازیابی از هدر USER_AGENT)
3) آدرس درخواست جاری (برای اینکه شناسهی تولیدی کاملا یکتا باشد، هرچند میتوانید آن را حذف کنید)
اطلاعات فوق را در یک رشته قرار میدهیم و بعد Hash آن را حساب میکنیم. به این ترتیب ما یک شناسه منحصر فرد را از درخواست جاری ایجاد کردهایم.
مرحله بعد پیاده سازی مکانیزمی برای نگهداری این اطلاعات و بازیابی آنها در هر درخواست است. ما برای این منظور از سیستم Cache استفاده میکنیم؛ هرچند راه حلهای بهتری هم وجود دارند.
بنابراین پس از ایجاد شناسه یکتای درخواست، آن را در Cache قرار میدهیم و زمان انقضای آن را هم پارامتری که ابتدای کار گفتم قرار میدهیم. سپس در هر درخواست Cache را برای این مقدار یکتا جستجو میکنیم. اگر شناسه پیدا شود، یعنی در کمتر از زمان تعیین شده، درخواست مجددی از سوی کاربر صورت گرفته است و اگر شناسه در Cache موجود نباشد، یعنی درخواست رسیده در زمان معقولی صادر شده است.
باید توجه داشته باشید که تعیین زمان بین هر درخواست به ازای هر ActionMethod خواهد بود و نباید آنقدر زیاد باشد که عملا کاربر را محدود کنیم. برای مثال در یک سیستم چت، زمان معقول بین هر درخواست 5 ثانیه است و در یک سیستم ارسال نظر یا پاسخ، 10 ثانیه. در هر حال بسته به نظر شما این زمان میتواند قابل تغییر باشد. حتی میتوانید کاربر را مجبور کنید که در روز فقط یک دیدگاه ارسال کند!
قبل از پیاده سازی سناریوی فوق، در مورد نقش گزینهی سوم در شناسهی درخواست، لازم است توضیحاتی بدهم. با استفاده از این خصوصیت (یعنی آدرس درخواست جاری) شدت سختگیری ما کمتر میشود. زیرا به ازای هر آدرس، شناسهی تولیدی متفاوت خواهد بود. اگر فرد مهاجم، برنامهای را که با آن اسپم میکند، طوری طراحی کرده باشد که مرتبا درخواستها را به آدرسهای متفاوتی ارسال کند، مکانیزم ما کمتر با آن مقابله خواهد کرد.
برای مثال فرد مهاجم میتواند در یک حلقه، ابتدا درخواستی را به AddComment بدهد، بعد AddReply و بعد SendMessage. پس همانطور که میبینید اگر از پارامتر سوم استفاده کنید، عملا قدرت مکانیزم ما به یک سوم کاهش مییابد.
نکتهی دیگری که قابل ذکر است اینست که این روش راهی برای تشخیص زمان بین درخواستهای صورت گرفته از کاربر است و به تنهایی نمیتواند امنیت کامل را برای مقابله با اسپمها، مهیا کند و باید به فکر مکانیزم دیگری برای مقابله با کاربری که درخواستهای نامعقولی در مدت زمان کمی میفرستد پیاده کنیم (پیاده سازی مکانیزم تکمیلی را در آینده شرح خواهم داد).
اکنون نوبت پیاده سازی سناریوی ماست. ابتدا یک کلاس ایجاد کنید و آن را از ActionFilterAttribute مشتق کنید و کدهای زیر را وارد کنید:
using System; using System.Linq; using System.Web.Mvc; using System.Security.Cryptography; using System.Text; using System.Web.Caching; namespace Parsnet.Core { public class StopSpamAttribute : ActionFilterAttribute { // حداقل زمان مجاز بین درخواستها برحسب ثانیه public int DelayRequest = 10; // پیام خطایی که در صورت رسیدن درخواست غیرمجاز باید صادر کنیم public string ErrorMessage = "درخواستهای شما در مدت زمان معقولی صورت نگرفته است."; //خصوصیتی برای تعیین اینکه آدرس درخواست هم به شناسه یکتا افزوده شود یا خیر public bool AddAddress = true; public override void OnActionExecuting(ActionExecutingContext filterContext) { // درسترسی به شئی درخواست var request = filterContext.HttpContext.Request; // دسترسی به شیئ کش var cache = filterContext.HttpContext.Cache; // کاربر IP بدست آوردن var IP = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress; // مشخصات مرورگر var browser = request.UserAgent; // در اینجا آدرس درخواست جاری را تعیین میکنیم var targetInfo = (this.AddAddress) ? (request.RawUrl + request.QueryString) : ""; // شناسه یکتای درخواست var Uniquely = String.Concat(IP, browser, targetInfo); //در اینجا با کمک هش یک امضا از شناسهی درخواست ایجاد میکنیم var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(Uniquely)).Select(s => s.ToString("x2"))); // ابتدا چک میکنیم که آیا شناسهی یکتای درخواست در کش موجود نباشد if (cache[hashValue] != null) { // یک خطا اضافه میکنیم ModelState اگر موجود بود یعنی کمتر از زمان موردنظر درخواست مجددی صورت گرفته و به filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage); } else { // اگر موجود نبود یعنی درخواست با زمانی بیشتر از مقداری که تعیین کردهایم انجام شده // پس شناسه درخواست جدید را با پارامتر زمانی که تعیین کرده بودیم به شیئ کش اضافه میکنیم cache.Add(hashValue, true, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } base.OnActionExecuting(filterContext); } } }
[HttpPost] [StopSpam(DelayRequest = 5)] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> SendFile(HttpPostedFileBase file, int userid = 0) { } [HttpPost] [StopSpam(DelayRequest = 30, ErrorMessage = "زمان لازم بین ارسال هر مطلب 30 ثانیه است")] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> InsertPost(NewPostModel model) { }
همانطور که گفتم این مکانیزم تنها تا حدودی با درخواستهای اسپم مقابله میکند و برای تکمیل آن نیاز به مکانیزم دیگری داریم تا بتوانیم از ارسالهای غیرمجاز بعد از زمان تعیین شده جلوگیری کنیم.
به توجه به دیدگاههای مطرح شده اصلاحاتی در کلاس صورت گرفت و قابلیتی به آن اضافه گردید که بتوان مکانیزم اعتبارسنجی را کنترل کرد.
برای این منظور خصوصیتی به این ActionFilter افزوده شد تا هنگامیکه دادههای فرم معتبر نباشند و در واقع هنوز چیزی ثبت نشده است این مکانیزم را بتوان کنترل کرد. خصوصیت CheckResult باعث میشود تا اگر دادههای مدل ما در اعتبارسنجی، معتبر نبودند کلید افزوده شده به کش را حذف تا کاربر بتواند مجدد فرم را ارسال کند. مقدار آن به طور پیش فرض true است و اگر برابر false قرار بگیرد تا اتمام زمان تعیین شده در مکانیزم ما، کاربر امکان ارسال مجدد فرم را ندارد.
همچنین باید بعد از اتمام عملیات در صورت عدم موفقیت آمیز بودن آن به ViewBag یک خصوصیت به نام ExecuteResult اضافه کنید و مقدار آن را برابر false قرار دهید. تا کلید از کش حذف گردد.
نحوه استفاده آن هم به شکل زیر میباشد:
[HttpPost] [StopSpam(AddAddress = true, DelayRequest = 20)] [ValidateAntiForgeryToken] public Task<ActionResult> InsertPost(NewPostModel model) { if (ModelState.IsValid) { var newPost = dbContext.InsertPost(model); if (newPost != null) { ViewBag.ExecuteResult = true; } } if (ModelState.IsValidField("ExcessiveRequests") == true)
{
ViewBag.ExecuteResult = false;
}
return View(); }
فایل ضمیمه را میتوانید از زیر دانلود کنید:
StopSpamAttribute.rar
Kendo UI
- معرفی Kendo UI
- بررسی ساختار ویجتهای وب Kendo UI
- کار با Kendo UI DataSource
- صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
- استفاده از Kendo UI templates
- فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid
- فعال سازی عملیات CRUD در Kendo UI Grid
- استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
- اعتبار سنجی ورودیهای کاربر در Kendo UI
- Kendo UI MVVM
- یکپارچه سازی سیستم اعتبارسنجی ASP.NET MVC با Kendo UI validator
- بررسی ویجت Kendo UI File Upload
- استفاده از ویجت آپلود KendoUI بصورت پاپ آپ
- رسم نمودار توسط Kendo Chart
- استفاده از Kendo UI TreeView به همراه یک منبع داده راه دور
- ایجاد Drop Down Listهای آبشاری توسط Kendo UI
- فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
یک اپلیکیشن با SQL Membership بسازید
حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.
نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.
بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.
پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده میشوند، در جداول aspnet_Users و aspnet_Membership ذخیره میشوند. دادههای مربوط به نقشها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقشها نیز در جدول aspnet_UsersInRoles ذخیره میشود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.
برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت میکند.
مهاجرت به Visual Studio 2013
- برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
- حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده میشود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده میکند. برای رفع این مساله میتوانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
- فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.
- پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و دادهها قابل رویت هستند.
- سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالاتر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.
پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.
نصب پکیجهای NuGet
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity
در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.
ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف میکنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، میتوانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.
لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان میدهد.
- aspnet_Roles --> AspNetRoles
- aspnet_Users, aspnet_Membership --> AspNetUsers
- aspnet_UsersInRoles --> AspNetUserRoles
ساختن مدلها و صفحات عضویت
کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمیبینید.
public class User : IdentityUser { public User() { CreateDate = DateTime.Now; IsApproved = false; LastLoginDate = DateTime.Now; LastActivityDate = DateTime.Now; LastPasswordChangedDate = DateTime.Now; LastLockoutDate = DateTime.Parse("1/1/1754"); FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754"); FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754"); } public System.Guid ApplicationId { get; set; } public string MobileAlias { get; set; } public bool IsAnonymous { get; set; } public System.DateTime LastActivityDate { get; set; } public string MobilePIN { get; set; } public string Email { get; set; } public string LoweredEmail { get; set; } public string LoweredUserName { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime CreateDate { get; set; } public System.DateTime LastLoginDate { get; set; } public System.DateTime LastPasswordChangedDate { get; set; } public System.DateTime LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } }
حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را میپذیرد. TUser میتواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش میدهد.
در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده میکند.
public class ApplicationDbContext : IdentityDbContext<User> { }
مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام میشود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری میکند و آن را طوری توسعه میدهد که از کلاس User استفاده کند.
در پوشه Models کلاس جدیدی با نام UserManager بسازید.
public class UserManager : UserManager<User> { }
کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره میشوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد میشوند، کلمه عبورشان را توسط الگوریتمهای قدیمی SQL Membership رمزگشایی میکنیم، اما کاربران جدید از الگوریتمهای ASP.NET Identity استفاده خواهند کرد.
کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره میکند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنشهای احراز هویت کاربران استفاده میشود تا کلمههای عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده میکنید.
public class SQLPasswordHasher : PasswordHasher { public override string HashPassword(string password) { return base.HashPassword(password); } public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { string[] passwordProperties = hashedPassword.Split('|'); if (passwordProperties.Length != 3) { return base.VerifyHashedPassword(hashedPassword, providedPassword); } else { string passwordHash = passwordProperties[0]; int passwordformat = 1; string salt = passwordProperties[2]; if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase)) { return PasswordVerificationResult.SuccessRehashNeeded; } else { return PasswordVerificationResult.Failed; } } } //This is copied from the existing SQL providers and is provided only for back-compat. private string EncryptPassword(string pass, int passwordFormat, string salt) { if (passwordFormat == 0) // MembershipPasswordFormat.Clear return pass; byte[] bIn = Encoding.Unicode.GetBytes(pass); byte[] bSalt = Convert.FromBase64String(salt); byte[] bRet = null; if (passwordFormat == 1) { // MembershipPasswordFormat.Hashed HashAlgorithm hm = HashAlgorithm.Create("SHA1"); if (hm is KeyedHashAlgorithm) { KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm; if (kha.Key.Length == bSalt.Length) { kha.Key = bSalt; } else if (kha.Key.Length < bSalt.Length) { byte[] bKey = new byte[kha.Key.Length]; Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length); kha.Key = bKey; } else { byte[] bKey = new byte[kha.Key.Length]; for (int iter = 0; iter < bKey.Length; ) { int len = Math.Min(bSalt.Length, bKey.Length - iter); Buffer.BlockCopy(bSalt, 0, bKey, iter, len); iter += len; } kha.Key = bKey; } bRet = kha.ComputeHash(bIn); } else { byte[] bAll = new byte[bSalt.Length + bIn.Length]; Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length); Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length); bRet = hm.ComputeHash(bAll); } } return Convert.ToBase64String(bRet); } }
دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.
متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری میکند. این الگوریتم از System.Web گرفته میشود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده میکرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنامهای HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمههای عبور و تایید آنها در دیتابیس استفاده میکنند.
سیستم SQL Membership برای رمزنگاری (Hash) کلمههای عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده میکرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده میکنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمههای عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.
کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.
public UserManager() : base(new UserStore<User>(new ApplicationDbContext())) { this.PasswordHasher = new SQLPasswordHasher(); }
ایجاد صفحات جدید مدیریت کاربران
- فایلهای Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده میکنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
- همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
- لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار میتوانید پیش از ساختن حسابهای کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
private Guid GetApplicationID() { using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString)) { string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database SqlCommand command = new SqlCommand(queryString, connection); command.Connection.Open(); var reader = command.ExecuteReader(); while (reader.Read()) { return reader.GetGuid(0); } return Guid.NewGuid(); } }
var currentApplicationId = GetApplicationID(); User user = new User() { UserName = Username.Text, ApplicationId=currentApplicationId, …};
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!"
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!" alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
<script type="text/javascript"> // a globally-scoped variable var a = 1; // global scope function one() { alert(a); } // local scope function two(a) { alert(a); } // local scope again function three() { var a = 3; alert(a); } // Intermediate: no such thing as block scope in javascript function four() { if (true) { var a = 4; } alert(a); // alerts '4', not the global value of '1' } // Intermediate: object properties function Five() { this.a = 5; } // Advanced: closure var six = function () { var foo = 6; return function () { // javascript "closure" means I have access to foo in here, // because it is defined in the function in which I was defined. alert(foo); } }() // Advanced: prototype-based scope resolution function Seven() { this.a = 7; } // [object].prototype.property loses to [object].property in the lookup chain Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above. Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor. // These will print 1-8 one(); two(2); three(); four(); alert(new Five().a); six(); alert(new Seven().a); alert(new Seven().b); </Script>
var obj = { value: 0, increment: function() { this.value+=1; } }; obj.increment(); //Method invocation
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { this.value++; var innerFunction = function() { alert(this.value); } innerFunction(); //Function invocation pattern } } obj.increment(); //Method invocation pattern <script type="text/javascript"> Result : 500
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { var that = this; that.value++; var innerFunction = function() { alert(that.value); } innerFunction(); //Function invocation pattern } } obj.increment(); <script type="text/javascript"> Result : 1
var Dog = function(name) { //this == brand new object ({}); this.name = name; this.age = (Math.random() * 5) + 1; }; var myDog = new Dog('Spike'); //myDog.name == 'Spike' //myDog.age == 2 var yourDog = new Dog('Spot'); //yourDog.name == 'Spot' //yourDog.age == 4
var createCallBack = function(init) { //First function return new function() { //Second function by Constructor Invocation var that = this; this.message = init; return function() { //Third function alert(that.message); } } } window.addEventListener('load', createCallBack("First Message")); window.addEventListener('load', createCallBack("Second Message"));
myFunction.apply(thisContext, arrArgs); myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
var contextObject = { testContext: 10 } var otherContextObject = { testContext: "Hello World!" } var testContext = 15; // Global variable function testFunction() { alert(this.testContext); } testFunction(); // This will alert 15 testFunction.call(contextObject); // Will alert 10 testFunction.apply(otherContextObject); // Will alert "Hello World”
var o = { i : 0, F : function() { var a = function() { this.i = 42; }; a(); document.write(this.i); } }; o.F(); Result :0
var p = { i : 0, F : function() { var a = function() { this.i = 42; }; a.apply(this); document.write(this.i); } }; p.F(); Result :42
var q = { i: 0, F: function F() { var that = this; var a = function () { that.i = 42; } a(); document.write(this.i); } } q.F();
using System.Threading.Tasks; namespace Async05 { class Program { static void Main(string[] args) { var res = doSomethingAsync().Result; } static async Task<int> doSomethingAsync() { await Task.Delay(1); return 1; } } }
البته باید دقت داشت، زمانیکه از خاصیت Result استفاده میشود، این متد همزمان عمل خواهد کرد و نه غیرهمزمان (ترد جاری را بلاک میکند؛ یکی از موارد مجاز استفاده از آن در متد Main برنامههای کنسول است). همچنین اگر در متد doSomethingAsync استثنایی رخ داده باشد، این استثناء زمان استفاده از Result، به صورت یک AggregateException مجددا صادر خواهد شد. وجود کلمهی Aggregate در اینجا به علت امکان استفادهی تجمعی و ترکیب چندین Task باهم و داشتن چندین شکست و استثنای ممکن است.
همچنین اگر از کلمهی کلیدی await بر روی یک faulted task استفاده کنیم، AggregateException صادر نمیشود. در این حالت کامپایلر AggregateException را بررسی کرده و آنرا تبدیل به یک Exception متداول و معمول کدهای دات نت میکند. به عبارتی سعی شدهاست در این حالت، رفتار کدهای async را شبیه به رفتار کدهای متداول همزمان شبیه سازی کنند.
یک مثال
در اینجا توسط متد getTitleAsync، اطلاعات یک صفحهی وب به صورت async دریافت شده و سپس عنوان آن استخراج میشود. در متد showTitlesAsync نیز از آن استفاده شده و در طی یک حلقه، چندین وب سایت مورد بررسی قرار خواهند گرفت. چون متد getTitleAsync از نوع async تعریف شدهاست، فراخوان آن نیز باید async تعریف شود تا بتوان از واژهی کلیدی await برای کار با آن استفاده کرد.
نهایتا در متد Main برنامه، وظیفهی غیرهمزمان showTitlesAsync اجرا شده و تا پایان عملیات آن صبر میشود. چون خروجی آن از نوع Task است و نه Task of T، در اینجا دیگر خاصیت Result قابل دسترسی نیست. متد Wait نیز ترد جاری را همانند خاصیت Result بلاک میکند.
using System; using System.Collections.Generic; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Async05 { class Program { static void Main(string[] args) { var task = showTitlesAsync(new[] { "http://www.google.com", "https://www.dntips.ir" }); task.Wait(); Console.WriteLine(); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } static async Task showTitlesAsync(IEnumerable<string> urls) { foreach (var url in urls) { var title = await getTitleAsync(url); Console.WriteLine(title); } } static async Task<string> getTitleAsync(string url) { var data = await new WebClient().DownloadStringTaskAsync(url); return getTitle(data); } private static string getTitle(string data) { const string patternTitle = @"(?s)<title>(.+?)</title>"; var regex = new Regex(patternTitle); var mc = regex.Match(data); return mc.Groups.Count == 2 ? mc.Groups[1].Value.Trim() : string.Empty; } } }
برنامه را در حالت عدم اتصال به اینترنت اجرا کنید. استثنای صادر شده، در متد task.Wait ظاهر میشود (چون متدهای async ترد جاری را خالی کردهاند):
و اگر در اینجا بر روی لینک View details کلیک کنیم، در inner exception حاصل، خطای واقعی قابل مشاهده است:
همانطور که ملاحظه میکنید، استثنای صادر شده از نوع System.AggregateException است. به این معنا که میتواند حاوی چندین استثناء باشد که در اینجا تعداد آنها با عدد یک مشخص شدهاست. بنابراین در این حالات، بررسی inner exception را فراموش نکنید.
در ادامه داخل حلقهی foreach متد showTitlesAsync، یک try/catch قرار میدهیم:
static async Task showTitlesAsync(IEnumerable<string> urls) { foreach (var url in urls) { try { var title = await getTitleAsync(url); Console.WriteLine(title); } catch (Exception ex) { Console.WriteLine(ex); } } }
System.Net.WebException: The remote server returned an error: (502) Bad Gateway. System.Net.WebException: The remote server returned an error: (502) Bad Gateway. Press any key to exit...
مدیریت تمام inner exceptionهای رخ داده در پردازشهای موازی
همانطور که عنوان شد، await تنها یک استثنای حاصل از Task در حال اجرا را به کد فراخوان بازگشت میدهد. در این حالت اگر این Task، چندین شکست را گزارش دهد، چطور باید برای دریافت تمام آنها اقدام کرد؟ برای مثال استفاده از Task.WhenAll میتواند شامل چندین استثنای حاصل از چندین Task باشد، ولی await تنها اولین استثنای دریافتی را بازگشت میدهد. اما اگر از خاصیتی مانند Result یا متد Wait استفاده شود، یک AggregateException حاصل تمام استثناءها را دریافت خواهیم کرد. بنابراین هرچند await تنها اولین استثنای دریافتی را بازگشت میدهد، اما میتوان به Taskهای مرتبط مراجعه کرد و سپس بررسی نمود که آیا استثناهای دیگری نیز وجود دارند یا خیر؟
برای نمونه در مثال فوق، حلقهی foreach تشکیل شده آنچنان بهینه نیست. از این جهت که هر بار تنها یک سایت را بررسی میکند، بجای اینکه مانند مرورگرها چندین ترد را به یک یا چند سایت باز کرده و نتایج را دریافت کند.
البته انجام کارها به صورت موازی همیشه ایدهی خوبی نیست ولی حداقل در این حالت خاص که با یک یا چند سرور راه دور کار میکنیم، درخواستهای همزمان دریافت اطلاعات، سبب کارآیی بهتر برنامه و بالا رفتن سرعت اجرای آن میشوند. اما مثلا در حالتیکه با سخت دیسک سیستم کار میکنیم، اجرای موازی کارها نه تنها کمکی نخواهد کرد، بلکه سبب خواهد شد تا مدام drive head در مکانهای مختلفی مشغول به حرکت شده و در نتیجه کارآیی آن کاهش یابد.
برای ترکیب چندین Task، ویژگی خاصی به زبان سیشارپ اضافه نشده، زیرا نیازی نبوده است. برای این حالت تنها کافی است از متد Task.WhenAll، برای ساخت یک Task مرکب استفاده کرد. سپس میتوان واژهی کلیدی await را بر روی این Task مرکب فراخوانی کرد.
همچنین میتوان از متد ContinueWith یک Task مرکب نیز برای جلوگیری از بازگشت صرفا اولین استثنای رخ داده توسط کامپایلر، استفاده کرد. در این حالت امکان دسترسی به خاصیت Result آن به سادگی میسر میشود که حاوی AggregateException کاملی است.
اعتبارسنجی آرگومانهای ارسالی به یک متد async
زمان اعتبارسنجی آرگومانهای ارسالی به متدهای async مهم است. بعضی از مقادیر را نمیتوان بلافاصله اعتبارسنجی کرد؛ مانند مقادیری که نباید نال باشند. تعدادی دیگر نیز پس از انجام یک Task زمانبر مشخص میشوند که معتبر بودهاند یا خیر. همچنین فراخوانهای این متدها انتظار دارند که متدهای async بلافاصله بازگشت داده شده و ترد جاری را خالی کنند. بنابراین اعتبارسنجیهای آنها باید با تاخیر انجام شود. در این حالات، دو نوع استثنای آنی و به تاخیر افتاده را شاهد خواهیم بود. استثنای آنی زمان شروع به کار متد صادر میشود و استثنای به تاخیر افتاده در حین دریافت نتایج از آن دریافت میگردد. باید دقت داشت کلیه استثناهای صادر شده در بدنهی یک متد async، توسط کامپایلر به عنوان یک استثنای به تاخیر افتاده گزارش داده میشود. بنابراین اعتبارسنجیهای آرگومانها را بهتر است در یک متد سطح بالای غیر async انجام داد تا بلافاصله بتوان استثناءهای حاصل را دریافت نمود.
از دست دادن استثناءها
فرض کنید مانند مثال قسمت قبل، دو وظیفهی async آغاز شده و نتیجهی آنها پس از await هر یک، با هم جمع زده میشوند. در این حالت اگر کل عملیات را داخل یک قطعه کد try/catch قرار دهیم، اولین await ایی که یک استثناء را صادر کند، صرفنظر از وضعیت await دوم، سبب اجرای بدنهی catch میشود. همچنین انجام این عملیات بدین شکل بهینه نیست. زیرا ابتدا باید صبر کرد تا اولین Task تمام شود و سپس دومین Task شروع گردد و به این ترتیب پردازش موازی Taskها را از دست خواهیم داد. در یک چنین حالتی بهتر است از متد await Task.WhenAll استفاده شود. در اینجا دو Task مورد نیاز، تبدیل به یک Task مرکب میشوند. این Task مرکب تنها زمانی خاتمه مییابد که هر دوی Task اضافه شده به آن، خاتمه یافته باشند. به این ترتیب علاوه بر اجرای موازی Taskها، امکان دریافت استثناءهای هر کدام را نیز به صورت تجمعی خواهیم داشت.
مشکل! همانطور که پیشتر نیز عنوان شد، استفاده از await در اینجا سبب میشود تا کامپایلر تنها اولین استثنای دریافتی را بازگشت دهد و نه یک AggregateException نهایی را. روش حل آنرا نیز عنوان کردیم. در این حالت بهتر است از متد ContinueWith و سپس استفاده از خاصیت Result آن برای دریافت کلیه استثناءها کمک گرفت.
حالت دوم از دست دادن استثناءها زمانیاست که یک متد async void را ایجاد میکنید. در این حالات بهتر است از یک Task بجای بازگشت void استفاده شود. تنها علت وجودی async voidها، استفاده از آنها در روالهای رویدادگردان UI است (در سایر حالات code smell درنظر گرفته میشود).
public async Task<double> GetSum2Async() { try { var task1 = GetNumberAsync(); var task2 = GetNumberAsync(); var compositeTask = Task.WhenAll(task1, task2); await compositeTask.ContinueWith(x => { }); return compositeTask.Result[0] + compositeTask.Result[1]; } catch (Exception ex) { //todo: log ex throw; } }
در این مثال دیگر مانند مثال قسمت قبل
public async Task<double> GetSumAsync() { var leftOperand = await GetNumberAsync(); var rightOperand = await GetNumberAsync(); return leftOperand + rightOperand; }
با کمک متد Task.WhenAll ترکیب آنها ایجاد و سپس با فراخوانی await، سبب اجرای موازی چندین Task با هم شدهایم.
مدیریت خطاهای مدیریت نشده
ابتدا مثال زیر را در نظر بگیرید:
using System; using System.Threading.Tasks; namespace Async01 { class Program { static void Main(string[] args) { Test2(); Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.ReadLine(); } public static async Task Test() { throw new Exception(); } public static async void Test2() { throw new Exception(); } } }
اگر برنامه را کامپایل کنید، کامپایلر بر روی سطر فراخوانی متد Test اخطار زیر را صادر میکند. البته برنامه بدون مشکل کامپایل خواهد شد.
Warning 1 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
همچنین اگر برنامه را اجرا کنید استثنای صادر شده در متد async void سبب کرش برنامه میشود؛ اما نه استثنای صادر شده در متد async Task. متدهای async void چون دارای Synchronization Context نیستند، استثنای صادره را به Thread pool برنامه صادر میکنند. به همین جهت در همان لحظه نیز سبب کرش برنامه خواهند شد. اما در حالت async Task به این نوع استثناءها اصطلاحا Unobserved Task Exception گفته شده و سبب بروز faulted state در Task تعریف شده میگردند.
برای مدیریت آنها در سطح برنامه باید در ابتدای کار و در متد Main، توسط TaskScheduler.UnobservedTaskException روال رخدادگردانی را برای مدیریت اینگونه استثناءها تدارک دید. زمانیکه GC شروع به آزاد سازی منابع میکند، این استثناءها نیز درنظر گرفته شده و سبب کرش برنامه خواهند شد. با استفاده از متد SetObserved همانند قطعه کد زیر، میتوان از کرش برنامه جلوگیری کرد:
using System; using System.Threading.Tasks; namespace Async01 { class Program { static void Main(string[] args) { TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; //Test2(); Test(); Console.ReadLine(); GC.Collect(); GC.WaitForPendingFinalizers(); Console.ReadLine(); } private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { e.SetObserved(); Console.WriteLine(e.Exception); } public static async Task Test() { throw new Exception(); } public static async void Test2() { throw new Exception(); } } }
به عبارتی رفتار قطعه کد زیر در دات نت 4 و 4.5 متفاوت است:
Task.Factory.StartNew(() => { throw new Exception(); }); Thread.Sleep(100); GC.Collect(); GC.WaitForPendingFinalizers();
<configuration> <runtime> <ThrowUnobservedTaskExceptions enabled="true"/> </runtime> </configuration>
یک نکتهی تکمیلی: ممکن است عبارات lambda مورد استفاده، از نوع async void باشد.
همانطور که عنوان شد باید از async void منهای مواردی که کار مدیریت رویدادهای عناصر UI را انجام میدهند (مانند برنامههای ویندوز 8)، اجتناب کرد. چون پایان کار آنها را نمیتوان تشخیص داد و همچنین کامپایلر نیز اخطاری را در مورد استفاده ناصحیح از آنها بدون await تولید نمیکند (چون نوع void اصطلاحا awaitable نیست). به علاوه بروز استثناء در آنها، بلافاصله سبب خاتمه برنامه میشود. بنابراین اگر جایی در برنامه متد async void وجود دارد، قرار دادن try/catch داخل بدنهی آن ضروری است.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState) { try { ClickMeButton.Tapped += async (sender, args) => { throw new Exception(); }; } catch (Exception ex) { // This won’t catch exceptions! TextBlock1.Text = ex.Message; } }
public delegate void TappedEventHandler(object sender, TappedRoutedEventArgs e);
سفارشی کردن صفحه بندی WebGrid در ASP.NET MVC
mode: WebGridPagerModes.All,
@{ var grid = new WebGrid( source: null, canPage: true, rowsPerPage: 10, canSort: true, defaultSort: "Title" ); grid.Bind(Model, rowCount: (int)ViewBag.PageCount, autoSortAndPage: false); var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1); } @grid.Table( tableStyle: "table table-striped table-hover", headerStyle: "webgrid-header", footerStyle: "webgrid-footer", alternatingRowStyle: "webgrid-alternating-row", selectedRowStyle: "webgrid-selected-row", rowStyle: "webgrid-row-style", columns: grid.Columns( grid.Column(header: "#", style: "text-align-center-col", format: @<text>@(rowIndex++)</text>), grid.Column(columnName: "Title", header: "عنوان", style: "myfont"), grid.Column(columnName: "URL", header: "آدرس", style: "myfont"), grid.Column(header: "", style: "text-align-center-col smallcell", format: item => @Html.ActionLink(linkText: "ویرایش", actionName: "Edit", controllerName: "Link", routeValues: new { area = "Admin", id = item.Code }, htmlAttributes: new { @class = "btn-sm btn-info vertical-center" })), grid.Column(header: "", format: @<form action="Link/Delete/@item.Code" method="post"> <input type="submit" class="btn-sm btn-danger submitlink" onclick="return confirm('آیا از حذف این آیتم مطمئن هستید ؟');" value="حذف" /> </form>))) <div class="text-center"> @grid.PagerList(mode: WebGridPagerModes.All) </div>
@{ var grid = new WebGrid( source: Model, canPage: true, rowsPerPage: 10, canSort: true, defaultSort: "Title" ); // grid.Bind(Model, rowCount: (int)ViewBag.PageCount, autoSortAndPage: false); // Delete this Line var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1); }
پیاده سازی ساده Google Recaptcha در ASP.NET MVC
متد async کتابخانهای که Result بر روی آن فراخوانی میشود، نیاز به ConfigureAwait(false) دارد (در تمام فریم ورکهای دات نتی، غیر از NET Core.)؛ در غیراینصورت منتظر وقوع یک deadlock باشید. اطلاعات بیشتر
متدهای توکار استفاده از نوع دادهای XML - قسمت اول
- query : xml را به عنوان ورودی گرفته و نهایتا یک خروجی XML دیگر را بر میگرداند.
- exist : خروجی bit دارد؛ true یا false. ورودی آن یک XQuery است.
- value : یک خروجی SQL Type را ارائه میدهد.
- nodes : خروجی جدولی دارد.
- modify : برای تغییر اطلاعات بکار میرود.
استفاده از متد exist به عنوان جایگزین سبک وزن XML Schema
یکی از کاربردهای متد exist، تعریف قید بر روی یک ستون XML ایی جدول است. این روش، راه حل دوم و سادهای است بجای استفاده از XML Schema برای ارزیابی و اعتبارسنجی کل سند. پیشنیاز اینکار، تعریف قید مدنظر توسط یک تابع جدید است:
CREATE FUNCTION dbo.checkPerson(@data XML) RETURNS BIT WITH SCHEMABINDING AS BEGIN RETURN @data.exist('/people/person') END GO CREATE TABLE tblXML ( id INT PRIMARY KEY, doc XML CHECK(dbo.checkPerson(doc)=1) ) GO
اکنون برای آزمایش آن خواهیم داشت:
INSERT INTO tblXML (id, doc) VALUES ( 1, '<people><person name="Vahid"/></people>' ) INSERT INTO tblXML (id, doc) VALUES ( 2, '<people><emp name="Vahid"/></people>' )
The INSERT statement conflicted with the CHECK constraint "CK__tblXML__doc__060DEAE8". The conflict occurred in database "testdb", table "dbo.tblXML", column 'doc'. The statement has been terminated.
استفاده از متد value برای دریافت اطلاعات
با کاربرد مقدماتی متد value در بازگشت یک مقدار scalar در قسمتهای قبل آشنا شدیم. در ادامه مثالهای کاربردیتر را بررسی خواهیم کرد.
ابتدا جدول زیر را با یک ستون XML در آن درنظر بگیرید:
CREATE TABLE xml_tab ( id INT IDENTITY PRIMARY KEY, xml_col XML )
INSERT INTO xml_tab VALUES ('<people><person name="Vahid"/></people>') INSERT INTO xml_tab VALUES ('<people><person name="Farid"/></people>')
SELECT id, xml_col.value('(/people/person/@name)[1]', 'varchar(50)') AS name FROM xml_tab
یک نکته
اگر نیاز به خروجی از نوع XML است، بهتر است از متد query که در دو قسمت قبل بررسی شد، استفاده گردد. خروجی متد query همیشه یک untyped XML است یا نال. البته میتوان خروجی آنرا به یک typed XML دارای Schema نیز نسبت داد. در اینجا اعتبارسنجی در حین انتساب صورت خواهد گرفت.
استفاده از متد value برای تعریف قیود
از متد value همچنین میتوان برای تعریف قیود پیشرفته نیز استفاده کرد. برای مثال فرض کنیم میخواهیم ویژگی Id سند XML در حال ذخیره شدن، حتما مساوی ستون Id جدول باشد. برای این منظور ابتدا نیاز است همانند قبل یک تابع جدید را ایجاد نمائیم:
CREATE FUNCTION getIdValue(@doc XML) RETURNS int WITH SCHEMABINDING AS BEGIN RETURN @doc.value('/*[1]/@Id', 'int') END
سپس از این تابع در عبارت check برای مقایسه ویژگی Id سند XML در حال ذخیره شدن و id ردیف جاری استفاده میشود:
CREATE TABLE docs_tab ( id INT PRIMARY KEY, doc XML, CONSTRAINT id_chk CHECK(dbo.getIdValue(doc)=id) )
در ادامه برای آزمایش آن خواهیم داشت:
INSERT INTO docs_tab (id, doc) VALUES ( 1, '<Invoice Id="1"/>' ) INSERT INTO docs_tab (id, doc) VALUES ( 2, '<Invoice Id="1"/>' )
The INSERT statement conflicted with the CHECK constraint "id_chk". The conflict occurred in database "testdb", table "dbo.docs_tab". The statement has been terminated.
استفاده از متد value برای تعریف primary key
پیشتر عنوان شد که از فیلدهای XML نمیتوان به عنوان کلید یک جدول استفاده کرد؛ چون امکان مقایسهی محتوای کل آنها وجود ندارد. اما با استفاده از متد value میتوان مقدار دریافتی را به عنوان یک کلید اصلی محاسبه شده، ثبت کرد:
CREATE TABLE Invoices ( doc XML, id AS dbo.getIdValue(doc) PERSISTED PRIMARY KEY )
برای آزمایش آن سعی میکنیم دو رکورد را که حاوی ویژگی id برابری هستند، ثبت کنیم:
INSERT INTO Invoices VALUES ( '<Invoice Id="1"/>' ) INSERT INTO Invoices VALUES ( '<Invoice Id="1"/>' )
Violation of PRIMARY KEY constraint 'PK__Invoices__3213E83F145C0A3F'. Cannot insert duplicate key in object 'dbo.Invoices'. The duplicate key value is (1). The statement has been terminated.
توابع دسترسی به مقدار دادهها در XQuery
تابع data ، string و text برای دسترسی به مقدار دادهها در XQuery پیش بینی شدهاند.
اگر سعی کنیم مثال زیر را اجرا نمائیم:
DECLARE @doc XML SET @doc = '<foo bar="baz" />' SELECT @doc.query('/foo/@bar')
XQuery [query()]: Attribute may not appear outside of an element
DECLARE @doc XML SET @doc = '<foo bar="baz" />' SELECT @doc.query('data(/foo/@bar)')
DECLARE @x XML SET @x = '<x>hello<y>world</y></x><x>again</x>' SELECT @x.query('data(/*)')
اما اگر همین مثال را با متد string اجرا کنیم:
DECLARE @x XML SET @x = '<x>hello<y>world</y></x><x>again</x>' SELECT @x.query('string(/*)')
XQuery [query()]: 'string()' requires a singleton (or empty sequence), found operand of type 'element(*,xdt:untyped) *'
SELECT @x.query('string(/*[1])')
برای دریافت تمام کلمات توسط متد string میتوان از اسلش کمک گرفت:
SELECT @x.query('string(/)')
نمونهی دیگر آن مثال زیر است:
DECLARE @x XML = '<age>12</age>' SELECT @x.query('string(/age[1])')
متد text اندکی متفاوت عمل میکند. برای بررسی آن، ابتدا یک schema collection جدید را تعریف میکنیم که داری تک المانی رشتهای است به نام Root.
CREATE XML SCHEMA COLLECTION root_el AS '<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:geo"> <xs:element name="Root" type="xs:string" /> </xs:schema> ' GO
DECLARE @xmlDoc XML SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; /g:Root/text() ')
DECLARE @xmlDoc XML(root_el) SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; /g:Root[1]/text() ')
XQuery [query()]: 'text()' is not supported on simple typed or 'http://www.w3.org/2001/XMLSchema#anyType' elements, found 'element(g{urn:geo}:Root,xs:string) *'.
DECLARE @xmlDoc XML(root_el) SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>' SELECT @xmlDoc.query(' declare namespace g="urn:geo"; data(/g:Root[1]) ')
data(/age/text())