خالق Null در زبانهای برنامه نویسی، آنرا یک اشتباه چند میلیارد دلاری میداند! و به عنوان یک توسعه دهندهی دات نت، غیرممکن است که در حین اجرای برنامههای خود تابحال به null reference exception برخورد نکرده باشید. هدف از ارائهی قابلیت جدید «نوعهای ارجاعی نالنپذیر» در C# 8.0، مقابلهی با یک چنین مشکلاتی است و خصوصا غنی سازی IDEها برای ارائهی اخطارهایی پیش از کامپایل برنامه، در مورد قسمتهایی از کد که ممکن است سبب بروز null reference exception شوند.
فعالسازی «نوعهای ارجاعی نالنپذیر»
قابلیت «نوعهای ارجاعی نالنپذیر» به صورت پیشفرض غیرفعال است. برای فعالسازی آن میتوان فایل csproj را به صورت زیر، با افزودن خاصیت NullableContextOptions، ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </PropertyGroup> </Project>
CS8632: The annotation for nullable reference types should only be used in code within a ‘#nullable’ context. CS8627: A nullable type parameter must be known to be a value-type or non-nullable reference type. Consider adding a ‘class’, ‘struct’ or type constraint.
<PropertyGroup> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup>
تغییر ماهیت نوعهای ارجاعی #C با فعالسازی NullableContextOptions
در #C ای که ما میشناسیم، رشتهها قابلیت پذیرش نال را دارند و همچنین ذکر آنها به صورت nullable بیمعنا است. اما پس از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، اکنون عکس آن رخ میدهد. رشتهها نالنپذیر میشوند؛ اما میتوان در صورت نیاز، آنها را nullable نیز تعریف کرد.
یک مثال: بررسی تاثیر فعالسازی NullableContextOptions بر روی یک پروژه
کلاس زیر را در نظر بگیرید:
public class Person { public string FirstName { get; set; } public string MiddleName { get; set; } public string LastName { get; set; } public Person(string first, string last) => (FirstName, LastName) = (first, last); public Person(string first, string middle, string last) => (FirstName, MiddleName, LastName) = (first, middle, last); public override string ToString() => $"{FirstName} {MiddleName} {LastName}"; }
در این کلاس، دو سازنده وجود دارند که یکی MiddleName را دریافت میکند و دیگری خیر. در اینجا کامپایلر تشخیص دادهاست که چون در سازندهی اولی که MiddleName را دریافت نمیکند، مقدار پیشفرض خاصیت MiddleName، نال خواهد بود و همچنین ما NullableContextOptions را نیز فعال کردهایم، بنابراین این خاصیت دیگر به صورت معمول و متداول یک نوع ارجاعی نالپذیر عمل نمیکند و دیگر نمیتوان نال را به عنوان مقدار پیشفرض آن، به آن نسبت داد. به همین جهت اخطار فوق ظاهر شدهاست.
برای رفع این مشکل:
به کامپایلر اعلام میکنیم: «میدانیم که MiddleName میتواند نال هم باشد» و آنرا در این زمینه راهنمایی میکنیم:
public string? MiddleName { get; set; }
public static class NullableReferenceTypes { //#nullable enable // Toggle to enable public static string Exemplify() { var vahid = new Person("Vahid", "N"); var length = GetLengthOfMiddleName(vahid); return $"{vahid.FirstName}'s middle name has {length} characters in it."; static int GetLengthOfMiddleName(Person person) { string middleName = person.MiddleName; return middleName.Length; } } }
در اینجا در متد محلی (local function) تعریف شده، سعی در دسترسی به خاصیت MiddleName وجود دارد و اکنون با تغییر جدیدی که اعمال کردیم، به صورت نالپذیر تعریف شدهاست.
همچنین در سطر بعدی آن نیز نتیجهی نهایی middleName، مورد استفاده قرار گرفتهاست که آن نیز مشکلدار تشخیص داده شدهاست.
مشکل اولین سطر را به این صورت میتوانیم برطرف کنیم:
var middleName = person.MiddleName;
اما مشکل سطر دوم هنوز باقی است:
علت اینجا است که متغیر middleName نیز اکنون ممکن است مقدار نال را داشته باشد. برای رفع این مشکل میتوان از اپراتور .? استفاده کرد و سپس اگر مقدار نهایی این عبارت نال بود، مقدار صفر را بازگشت میدهیم:
static int GetLengthOfMiddleName(Person person) { var middleName = person.MiddleName; return middleName?.Length ?? 0; }
امکان خاموش و روشن کردن ویژگی نوعهای ارجاعی نالنپذیر به صورت موضعی
زمانیکه خاصیت NullableContextOptions را فعال میکنیم، بر روی کل پروژه تاثیر میگذارد. برای مثال اگر یک چنین قابلیتی را بر روی پروژههای قدیمی خود فعال کنید، با صدها اخطار مواجه خواهید شد. به همین جهت است که این ویژگی حتی با فعالسازی C# 8.0 و انتخاب آن، به صورت پیشفرض غیرفعال است. بنابراین برای اینکه بتوان پروژههای قدیمی را قدم به قدم و سر فرصت، «مقاومتر» کرد، میتوان تعیین کرد که کدام قسمت، تحت تاثیر این ویژگی قرار بگیرد و کدام قسمتها خیر:
public static class NullableReferenceTypes { #nullable disable // Toggle to enable
مجبور ساختن خود به «مقاوم سازی» برنامه
اگر NullableContextOptions را فعال کنید، کامپایلر صرفا یکسری اخطار را در مورد مشکلات احتمالی صادر میکند؛ اما برنامه هنوز کامپایل میشود. برای اینکه خود را مقید به «مقاوم سازی» برنامه کنیم، میتوانیم با فعالسازی ویژگی TreatWarningsAsErrors در فایل csprj، این اخطارها را تبدیل به خطای کامپایلر کرده و از کامپایل برنامه جلوگیری کنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> </Project>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
آیا اگر برنامهای با C# 7.0 کامپایل شود، کتابخانههای تهیه شدهی با C# 8.0 را میتواند استفاده کند؟
پاسخ: بله. از دیدگاه برنامههای قدیمی، کتابخانههای تهیه شدهی با C# 8.0، تفاوتی با سایر کتابخانه ندارند. آنها نوعهای نالپذیر جدید را مانند ?string مشاهده نمیکنند؛ آنها فقط string را مشاهده میکنند و روش کار کردن با آنها نیز همانند قبل است. بدیهی است در این حالت از مزایای کامپایلر C# 8.0 در تشخیص زود هنگام مشکلات برنامه محروم خواهند بود؛ اما عملکرد برنامه تفاوتی نمیکند.
وضعیت برنامهی C# 8.0 ای که از کتابخانههای C# 7.0 و یا قبل از آن استفاده میکند، چگونه خواهد بود؟
چون کتابخانههای قدیمیتر از مزایای کامپایلر C# 8.0 استفاده نمیکنند، خروجیهای آن بدون بروز خطایی توسط کامپایلر C# 8.0 استفاده میشوند؛ چون حجم اخطارهای صادر شدهی در این حالت بیش از حد خواهد بود. یعنی این بررسیهای کامپایلر صرفا برای کتابخانههای جدید فعال هستند و نه برای کتابخانههای قدیمی.
مهارتهای مواجه شدن با اخطارهای ناشی از فعالسازی NullableContextOptions
در مثالی که بررسی شد، یک نمونه از روشهای مواجه شدن با اخطارهای ناشی از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر را بررسی کردیم. در ادامه روشهای تکمیلی دیگری را بررسی میکنیم.
1- هرجائیکه قرار است متغیر ارجاعی نالپذیر باشد، آنرا صراحتا اعلام کنید.
string name = null; // ERROR string? name = null; // OK!
نمونهی دیگر آن مثال زیر است:
public class Person { public Address? Address { get; set; }; public string Country => Address?.Country; // ERROR! }
public class Person { public Address? Address { get; set; }; public string? Country => Address?.Country; // OK! }
البته در این حالت باید به مثال زیر دقت داشت:
var node = this; // Initialize non-nullable variable while (node != null) { node = null; // ERROR! }
Node? node = this; // Initialize nullable variable while (node != null) { node = null; // OK! }
2- نوعهای خود را مقدار دهی اولیه کنید.
در مثال زیر:
public class Person { public string Name { get; set; } // ERROR! }
public class Person { public string? Name { get; set; } }
یا یک استثناء را صادر کنید:
public class Person { public string Name { get; set; } public Person(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } }
البته در این حالت برای مقدار دهی اولیهی Name، حتما نیاز به تعریف یک سازندهاست و در این حالت کدهایی را که از سازندهی پیشفرض استفاده کرده بودند (مانند new Person { Name = "Vahid" })، باید تغییر دهید.
راهحل دیگر، مقدار دهی اولیهی این خواص بدون تعریف یک سازندهی اضافی است:
public class Person { public string Name { get; set; } = string.Empty; // -or- public string Name { get; set; } = ""; }
String.Empty Array.Empty<T>() Enumerable.Empty<T>()
public class Person { public Address Address { get; set; } = new Address(); }
روش افزودن میانافزار RateLimiter به برنامههای ASP.NET Core
شبیه به سایر میانافزارها، جهت فعالسازی میانافزار RateLimiter، ابتدا باید سرویسهای متناظر با آنرا به برنامه معرفی کرد و پس از فعالسازی میانافزار مسیریابی، آنرا به زنجیرهی مدیریت یک درخواست معرفی نمود. برای نمونه در مثال زیر، امکان دسترسی به تمام درخواستها، به 10 درخواست در دقیقه، محدود میشود که پارتیشن بندی آن (در مورد پارتیشن بندی در قسمت قبل بیشتر بحث شد)، بر اساس username کاربر اعتبارسنجی شده و یا hostname یک کاربر غیراعتبارسنجی شدهاست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 0, Window = TimeSpan.FromMinutes(1) })); }); // ... var app = builder.Build(); // ... app.UseRouting(); app.UseRateLimiter(); app.MapGet("/", () => "Hello World!"); app.Run();
- فراخوانی builder.Services.AddRateLimiter، سبب معرفی سرویسهای میانافزار rate limiter به سیستم تزریق وابستگیهای ASP.NET Core میشود.
- در اینجا میتوان برای مثال خاصیت options.GlobalLimiter تنظیمات آنرا نیز مقدار دهی کرد. GlobalLimiter، سبب تنظیم یک محدود کنندهی سراسری نرخ، برای تمام درخواستهای رسیدهی به برنامه میشود.
- GlobalLimiter را میتوان با هر نوع PartitionedRateLimiter مقدار دهی کرد که در اینجا از نوع FixedWindowLimiter انتخاب شدهاست تا بتوان «الگوریتمهای بازهی زمانی مشخص» را به برنامه اعمال نمود تا برای مثال فقط امکان پردازش 10 درخواست در هر دقیقه برای هر کاربر، وجود داشته باشد.
- در پایان کار، فراخوانی app.UseRateLimiter را نیز مشاهده میکنید که سبب فعالسازی میانافزار، بر اساس تنظیمات صورت گرفته میشود.
برای آزمایش برنامه، آنرا اجرا کرده و سپس به سرعت شروع به refresh کردن صفحهی اصلی آن کنید. پس از 10 بار ریفرش، پیام 503 Service Unavailable را مشاهده خواهید کرد که به معنای مسدود شدن دسترسی به برنامه توسط میانافزار rate limiter است.
بررسی تنظیمات رد درخواستها توسط میانافزار rate limiter
اگر پس از محدود شدن دسترسی به برنامه توسط میان افزار rate limiter از status code = 503 دریافتی راضی نیستید، میتوان آنرا هم تغییر داد:
builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = 429; // ... });
علاوه بر آن در اینجا گزینهی OnRejected نیز پیش بینی شدهاست تا بتوان response ارائه شده را در حالت رد درخواست، سفارشی سازی کرد تا بتوان پیام بهتری را به کاربری که هم اکنون دسترسی او محدود شدهاست، ارائه داد:
builder.Services.AddRateLimiter(options => { options.OnRejected = async (context, token) => { context.HttpContext.Response.StatusCode = 429; if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) { await context.HttpContext.Response.WriteAsync( $"Too many requests. Please try again after {retryAfter.TotalMinutes} minute(s). " + $"Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } else { await context.HttpContext.Response.WriteAsync( "Too many requests. Please try again later. " + "Read more about our rate limits at https://example.org/docs/ratelimiting.", cancellationToken: token); } }; // ... });
یک نکته: باتوجه به اینکه در اینجا به HttpContext دسترسی داریم، یعنی به context.HttpContext.RequestServices نیز دسترسی خواهیم داشت که توسط آن میتوان برای مثال سرویس ILogger را از آن درخواست کرد و رخداد واقع شده را برای بررسی بیشتر لاگ نمود؛ برای مثال چه کاربری مشکل پیدا کردهاست؟
context.HttpContext.RequestServices.GetService<ILoggerFactory>()? .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware") .LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
طراحی فعلی میانافزار rate limiter، کمی محدود است. برای مثال «retry after»، تنها metadata مفیدی است که جهت بازگشت ارائه میدهد و همچنین مانند GitHub مشخص نمیکند که در لحظهی جاری چند درخواست دیگر را میتوان ارسال کرد و امکان دسترسی به اطلاعات آماری درونی آن وجود ندارد. اگر نیاز به یک چنین اطلاعاتی دارید شاید استفاده از میانافزار ثالث دیگری به نام AspNetCoreRateLimit برای شما مفیدتر باشد!
الگوریتمهای پشتیبانی شدهی توسط میانافزار rate limiter
در قسمت قبل با چند الگوریتم استاندارد طراحی میانافزارهای rate limiter آشنا شدیم که میانافزار توکار rate limiter موجود در ASP.NET Core 7x، اکثر آنها را پشتیبانی میکند:
- Concurrency limit: سادهترین نوع محدود سازی نرخ درخواستها است و کاری به زمان ندارد و فقط برای آن، تعداد درخواستهای همزمان مهم است. برای مثال پیاده سازی «مجاز بودن تنها 10 درخواست همزمان».
- Fixed window limit: توسط آن میتوان محدودیتهایی مانند «مجاز بودن تنها 60 درخواست در دقیقه» را اعمال کرد که به معنای امکان ارسال یک درخواست در هر ثانیه در هر دقیقه و یا حتی ارسال یکجای 60 درخواست در یک ثانیه است.
- Sliding window limit: این محدودیت بسیار شبیه به حالت قبل است اما به همراه قطعاتی که کنترل بیشتری را بر روی محدودیتها میسر میکند؛ مانند مجاز بودن 60 درخواست در هر دقیقه که فقط در این حالت یک درخواست در هر ثانیه مجاز باشد.
- Token bucket limit: امکان کنترل نرخ سیلان را میسر کرده و همچنین از درخواستهای انفجاری نیز پشتیبانی میکند (این مفاهیم در قسمت قبل بررسی شدند).
علاوه بر اینها امکان ترکیب گزینههای فوق توسط متد کمکی PartitionedRateLimiter.CreateChained نیز میسر است:
builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.CreateChained( PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 600, Window = TimeSpan.FromMinutes(1) })), PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 6000, Window = TimeSpan.FromHours(1) }))); // ... });
در این مثال فرضی، متد الحاقی ResolveClientIpAddress اهمیتی ندارد. بهتر است برای برنامهی خود از کلید پارتیشن بندی بهتر و معقولتری استفاده کنید.
امکان در صف قرار دادن درخواستها بجای رد کردن آنها
در تنظیمات مثالهای فوق، در کنار PermitLimit، میتوان QueueLimit را نیز مشخص کرد. به این ترتیب با رسیدن به PermitLimit، به تعداد QueueLimit، درخواستها در صف قرار میگیرند، بجای اینکه کاملا رد شوند:
PartitionedRateLimiter.Create<HttpContext, string>(httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, QueueLimit = 6, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, Window = TimeSpan.FromSeconds(1) })));
این تنظیم، تجربهی کاربری بهتری را برای استفاده کنندگان از برنامهی شما به همراه خواهد داشت؛ بجای رد قاطع درخواستهای ارسالی توسط آنها.
یک نکته: بهتر است QueueLimitهای بزرگی را انتخاب نکنید؛ خصوصا برای بازههای زمانی طولانی. چون یک مصرف کننده نیاز دارد تا سریع، پاسخی را دریافت کند و اگر اینطور نباشد، دوباره سعی خواهد کرد. تنها چند ثانیهی کوتاه در صف بودن برای کاربران معنا دارد.
امکان ایجاد سیاستهای محدود سازی سفارشی
اگر الگوریتمهای توکار میانافزار rate limiter برای کار شما مناسب نیستند، میتوانید با پیاده سازی <IRateLimiterPolicy<TPartitionKey، یک نمونهی سفارشی را ایجاد کنید. پیاده سازی این اینترفیس، نیاز به دو متد را دارد:
الف) متد GetPartition که بر اساس HttpContext جاری، یک rate limiter مخصوص را باز میگرداند.
ب) متد OnRejected که امکان سفارشی سازی response رد درخواستها را میسر میکند.
در مثال زیر پیاده سازی یک rate limiter سفارشی را مشاهده میکنید که نحوهی پارتیشن بندی آن بر اساس user-name کاربر اعتبارسنجی شده و یا host-name کاربر وارد نشدهی به سیستم است. در اینجا کاربر وارد شدهی به سیستم، محدودیت بیشتری دارد:
public class ExampleRateLimiterPolicy : IRateLimiterPolicy<string> { public RateLimitPartition<string> GetPartition(HttpContext httpContext) { if (httpContext.User.Identity?.IsAuthenticated == true) { return RateLimitPartition.GetFixedWindowLimiter(httpContext.User.Identity.Name!, partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 1_000, Window = TimeSpan.FromMinutes(1), }); } return RateLimitPartition.GetFixedWindowLimiter(httpContext.Request.Headers.Host.ToString(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 100, Window = TimeSpan.FromMinutes(1), }); } public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = (context, _) => { context.HttpContext.Response.StatusCode = 418; // I'm a 🫖 return new ValueTask(); }; }
options.AddPolicy<string, ExampleRateLimiterPolicy>("myPolicy");
امکان تعریف سیاستهای محدود سازی نرخ دسترسی به گروهی از endpoints
تا اینجا روشهای سراسری محدود سازی دسترسی به منابع برنامه را بررسی کردیم؛ اما ممکن است در برنامهای بخواهیم محدودیتهای متفاوتی را به گروههای خاصی از endpoints اعمال کنیم و یا شاید اصلا نخواهیم تعدادی از آنها را محدود کنیم:
builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter("Api", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); options.AddFixedWindowLimiter("Web", options => { options.AutoReplenishment = true; options.PermitLimit = 10; options.Window = TimeSpan.FromMinutes(1); }); // ... });
البته باید درنظر داشت که متدهای الحاقی Add داری را که در اینجا ملاحظه میکنید، محدود سازی را بر اساس نام درنظر گرفته شده انجام میدهند. یعنی درحقیقت یک محدودسازی سراسری بر اساس گروهی از endpoints هستند و امکان تعریف پارتیشنی را به ازای یک کاربر یا آدرس IP خاص، ندارند. اگر نیاز به اعمال این نوع پارتیشن بندی را دارید، باید از متدهای AddPolicy استفاده کنید:
options.AddPolicy("Api", httpContext => RateLimitPartition.GetFixedWindowLimiter(httpContext.ResolveClientIpAddress(), partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = 10, Window = TimeSpan.FromSeconds(1) }));
محدود سازی نرخ دسترسی به منابع در ASP.NET Core Minimal API
پس از تعریف نامی برای سیاستهای دسترسی، اکنون میتوان از آنها به صورت زیر جهت محدود سازی یک endpoint و یا گروهی از آنها استفاده کرد:
// Endpoint app.MapGet("/api/hello", () => "Hello World!").RequireRateLimiting("Api"); // Group app.MapGroup("/api/orders").RequireRateLimiting("Api");
// Endpoint app.MapGet("/api/hello", () => "Hello World!").DisableRateLimiting(); // Group app.MapGroup("/api/orders").DisableRateLimiting();
محدود سازی نرخ دسترسی به منابع در ASP.NET Core MVC
میتوان سیاستهای نرخ دسترسی تعریف شده را بر اساس نام آنها به کنترلرها و یا اکشن متدها اعمال نمود:
[EnableRateLimiting("Api")] public class Orders : Controller { [DisableRateLimiting] public IActionResult Index() { return View(); } [EnableRateLimiting("ApiListing")] public IActionResult List() { return View(); } }
و یا حتی میتوان این سیاستهای محدود سازی نرخ دسترسی را به تمام کنترلرها و صفحات razor نیز به صورت زیر اعمال کرد:
app.UseConfiguredEndpoints(endpoints => { endpoints.MapRazorPages() .DisableRateLimiting(); endpoints.MapControllers() .RequireRateLimiting("UserBasedRateLimiting"); });
چگونگی نمایش 10 سایت پر بازدید در IE 5
خلاصه اشتراکهای روز جمعه 4 آذر 1390
من این ویدیو مقیاسه Silverlight vs HTML5 رو دانلود کردم و دیدم
تو این ویدیو بیشتر تمرکز روی HTML5 بود و کمتر از Silverlight صحبت کرد. البته تو ویدیو گفت که HTML5 در همه بروزرها نتایجش یکسان نیست ولی خوب از امکانات خوب HTML5 هم نمیشه گذشت. مخصوصا ساپورت اکثر سیستم عاملهای موبایل از HTML5.
تو این ویدیو سخنران هیچ اشارهای به بیزینس اپلیکیشنها نکرد (شاید من متوجه نشدم) .
خلاصه کلام نتیجه این ویدیو برای من این بود که به فکر یادگیری ASP.net MVC باشم و یه جورایی Silverlight رو کم کم بزارم کنار.
تیم ASP.NET Identity پروژه نمونه ای را فراهم کرده است که میتواند بعنوان نقطه شروعی برای اپلیکیشنهای MVC استفاده شود. پیکربندیهای لازم در این پروژه انجام شدهاند و برای استفاده از فریم ورک جدید آماده است.
شروع به کار : پروژه نمونه را توسط NuGet ایجاد کنید
برای شروع یک پروژه ASP.NET خالی ایجاد کنید (در دیالوگ قالبها گزینه Empty را انتخاب کنید). سپس کنسول Package Manager را باز کرده و دستور زیر را اجرا کنید.
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پس از اینکه NuGet کارش را به اتمام رساند باید پروژه ای با ساختار متداول پروژههای ASP.NET MVC داشته باشید. به تصویر زیر دقت کنید.
همانطور که میبینید ساختار پروژه بسیار مشابه پروژههای معمول MVC است، اما آیتمهای جدیدی نیز وجود دارند. فعلا تمرکز اصلی ما روی فایل IdentityConfig.cs است که در پوشه App_Start قرار دارد.
اگر فایل مذکور را باز کنید و کمی اسکرول کنید تعاریف دو کلاس سرویس را مشاهده میکنید: EmailService و SmsService.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. return Task.FromResult(0); } } public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your sms service here to send a text message. return Task.FromResult(0); } }
اگر دقت کنید هر دو کلاس قرارداد IIdentityMessageService را پیاده سازی میکنند. میتوانید از این قرارداد برای پیاده سازی سرویسهای اطلاع رسانی ایمیلی، پیامکی و غیره استفاده کنید. در ادامه خواهیم دید چگونه این دو سرویس را بسط دهیم.
یک حساب کاربری مدیریتی پیش فرض ایجاد کنید
پیش از آنکه بیشتر جلو رویم نیاز به یک حساب کاربری در نقش مدیریتی داریم تا با اجرای اولیه اپلیکیشن در دسترس باشد. کلاسی بنام ApplicationDbInitializer در همین فایل وجود دارد که هنگام اجرای اولیه و یا تشخیص تغییرات در مدل دیتابیس، اطلاعاتی را Seed میکند.
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } //Create User=Admin@Admin.com with password=Admin@123456 in the Admin role public static void InitializeIdentityForEF(ApplicationDbContext db) { var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>(); var roleManager = HttpContext.Current.GetOwinContext().Get<ApplicationRoleManager>(); const string name = "admin@admin.com"; const string password = "Admin@123456"; const string roleName = "Admin"; //Create Role Admin if it does not exist var role = roleManager.FindByName(roleName); if (role == null) { role = new IdentityRole(roleName); var roleresult = roleManager.Create(role); } var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = name }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } // Add user admin to Role Admin if not already added var rolesForUser = userManager.GetRoles(user.Id); if (!rolesForUser.Contains(role.Name)) { var result = userManager.AddToRole(user.Id, role.Name); } } }
تایید حسابهای کاربری : چگونه کار میکند
بدون شک با تایید حسابهای کاربری توسط ایمیل آشنا هستید. حساب کاربری ای ایجاد میکنید و ایمیلی به آدرس شما ارسال میشود که حاوی لینک فعالسازی است. با کلیک کردن این لینک حساب کاربری شما تایید شده و میتوانید به سایت وارد شوید.
اگر به کنترلر AccountController در این پروژه نمونه مراجعه کنید متد Register را مانند لیست زیر مییابید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Register(RegisterViewModel model) { if (ModelState.IsValid) { var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action( "ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync( user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>"); ViewBag.Link = callbackUrl; return View("DisplayEmail"); } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model); }
public static ApplicationUserManager Create( IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager( new UserStore<ApplicationUser>( context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application // uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. manager.RegisterTwoFactorProvider( "PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider( "EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>( dataProtectionProvider.Create("ASP.NET Identity")); } return manager; }
در قطعه کد بالا کلاسهای EmailService و SmsService روی وهله ApplicationUserManager تنظیم میشوند.
manager.EmailService = new EmailService(); manager.SmsService = new SmsService();
درست در بالای این کدها میبینید که چگونه تامین کنندگان احراز هویت دو مرحله ای (مبتنی بر ایمیل و پیامک) رجیستر میشوند.
// Register two factor authentication providers. This application // uses Phone and Emails as a step of receiving a code for verifying the user // You can write your own provider and plug in here. manager.RegisterTwoFactorProvider( "PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider( "EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" });
تایید حسابهای کاربری توسط ایمیل و احراز هویت دو مرحله ای توسط ایمیل و/یا پیامک نیاز به پیاده سازی هایی معتبر از قراردارد IIdentityMessageService دارند.
پیاده سازی سرویس ایمیل توسط ایمیل خودتان
پیاده سازی سرویس ایمیل نسبتا کار ساده ای است. برای ارسال ایمیلها میتوانید از اکانت ایمیل خود و یا سرویس هایی مانند SendGrid استفاده کنید. بعنوان مثال اگر بخواهیم سرویس ایمیل را طوری پیکربندی کنیم که از یک حساب کاربری Outlook استفاده کند، مانند زیر عمل خواهیم کرد.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Credentials: var credentialUserName = "yourAccount@outlook.com"; var sentFrom = "yourAccount@outlook.com"; var pwd = "yourApssword"; // Configure the client: System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient("smtp-mail.outlook.com"); client.Port = 587; client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network; client.UseDefaultCredentials = false; // Creatte the credentials: System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(credentialUserName, pwd); client.EnableSsl = true; client.Credentials = credentials; // Create the message: var mail = new System.Net.Mail.MailMessage(sentFrom, message.Destination); mail.Subject = message.Subject; mail.Body = message.Body; // Send: return client.SendMailAsync(mail); } }
پیاده سازی سرویس ایمیل با استفاده از SendGrid
سرویسهای ایمیل متعددی وجود دارند اما یکی از گزینههای محبوب در جامعه دات نت SendGrid است. این سرویس API قدرتمندی برای زبانهای برنامه نویسی مختلف فراهم کرده است. همچنین یک Web API مبتنی بر HTTP نیز در دسترس است. قابلیت دیگر اینکه این سرویس مستقیما با Windows Azure یکپارچه میشود.
می توانید در سایت SendGrid یک حساب کاربری رایگان بعنوان توسعه دهنده بسازید. پس از آن پیکربندی سرویس ایمیل با مرحله قبل تفاوت چندانی نخواهد داشت. پس از ایجاد حساب کاربری توسط تیم پشتیبانی SendGrid با شما تماس گرفته خواهد شد تا از صحت اطلاعات شما اطمینان حاصل شود. برای اینکار چند گزینه در اختیار دارید که بهترین آنها ایجاد یک اکانت ایمیل در دامنه وب سایتتان است. مثلا اگر هنگام ثبت نام آدرس وب سایت خود را www.yourwebsite.com وارد کرده باشید، باید ایمیلی مانند info@yourwebsite.com ایجاد کنید و توسط ایمیل فعالسازی آن را تایید کند تا تیم پشتیبانی مطمئن شود صاحب امتیاز این دامنه خودتان هستید.
تنها چیزی که در قطعه کد بالا باید تغییر کند اطلاعات حساب کاربری و تنظیمات SMTP است. توجه داشته باشید که نام کاربری و آدرس فرستنده در اینجا متفاوت هستند. در واقع میتوانید از هر آدرسی بعنوان آدرس فرستنده استفاده کنید.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Credentials: var sendGridUserName = "yourSendGridUserName"; var sentFrom = "whateverEmailAdressYouWant"; var sendGridPassword = "YourSendGridPassword"; // Configure the client: var client = new System.Net.Mail.SmtpClient("smtp.sendgrid.net", Convert.ToInt32(587)); client.Port = 587; client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network; client.UseDefaultCredentials = false; // Creatte the credentials: System.Net.NetworkCredential credentials = new System.Net.NetworkCredential(credentialUserName, pwd); client.EnableSsl = true; client.Credentials = credentials; // Create the message: var mail = new System.Net.Mail.MailMessage(sentFrom, message.Destination); mail.Subject = message.Subject; mail.Body = message.Body; // Send: return client.SendMailAsync(mail); } }
آزمایش تایید حسابهای کاربری توسط سرویس ایمیل
ابتدا اپلیکیشن را اجرا کنید و سعی کنید یک حساب کاربری جدید ثبت کنید. دقت کنید که از آدرس ایمیلی زنده که به آن دسترسی دارید استفاده کنید. اگر همه چیز بدرستی کار کند باید به صفحه ای مانند تصویر زیر هدایت شوید.
همانطور که مشاهده میکنید پاراگرافی در این صفحه وجود دارد که شامل لینک فعالسازی است. این لینک صرفا جهت تسهیل کار توسعه دهندگان درج میشود و هنگام توزیع اپلیکیشن باید آن را حذف کنید. در ادامه به این قسمت باز میگردیم. در این مرحله ایمیلی حاوی لینک فعالسازی باید برای شما ارسال شده باشد.
پیاده سازی سرویس SMS
برای استفاده از احراز هویت دو مرحله ای پیامکی نیاز به یک فراهم کننده SMS دارید، مانند Twilio . مانند SendGrid این سرویس نیز در جامعه دات نت بسیار محبوب است و یک C# API قدرتمند ارائه میکند. میتوانید حساب کاربری رایگانی بسازید و شروع به کار کنید.
پس از ایجاد حساب کاربری یک شماره SMS، یک شناسه SID و یک شناسه Auth Token به شما داده میشود. شماره پیامکی خود را میتوانید پس از ورود به سایت و پیمایش به صفحه Numbers مشاهده کنید.
شناسههای SID و Auth Token نیز در صفحه Dashboard قابل مشاهده هستند.
اگر دقت کنید کنار شناسه Auth Token یک آیکون قفل وجود دارد که با کلیک کردن روی آن شناسه مورد نظر نمایان میشود.
حال میتوانید از سرویس Twilio در اپلیکیشن خود استفاده کنید. ابتدا بسته NuGet مورد نیاز را نصب کنید.
PM> Install-Package Twilio
public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { string AccountSid = "YourTwilioAccountSID"; string AuthToken = "YourTwilioAuthToken"; string twilioPhoneNumber = "YourTwilioPhoneNumber"; var twilio = new TwilioRestClient(AccountSid, AuthToken); twilio.SendSmsMessage(twilioPhoneNumber, message.Destination, message.Body); // Twilio does not return an async Task, so we need this: return Task.FromResult(0); } }
حال که سرویسهای ایمیل و پیامک را در اختیار داریم میتوانیم احراز هویت دو مرحله ای را تست کنیم.
آزمایش احراز هویت دو مرحله ای
پروژه نمونه جاری طوری پیکربندی شده است که احراز هویت دو مرحله ای اختیاری است و در صورت لزوم میتواند برای هر کاربر بصورت جداگانه فعال شود. ابتدا توسط حساب کاربری مدیر، یا حساب کاربری ای که در قسمت تست تایید حساب کاربری ایجاد کرده اید وارد سایت شوید. سپس در سمت راست بالای صفحه روی نام کاربری خود کلیک کنید. باید صفحه ای مانند تصویر زیر را مشاهده کنید.
در این قسمت باید احراز هویت دو مرحله ای را فعال کنید و شماره تلفن خود را ثبت نمایید. پس از آن یک پیام SMS برای شما ارسال خواهد شد که توسط آن میتوانید پروسه را تایید کنید. اگر همه چیز بدرستی کار کند این مراحل چند ثانیه بیشتر نباید زمان بگیرد، اما اگر مثلا بیش از 30 ثانیه زمان برد احتمالا اشکالی در کار است.
حال که احراز هویت دو مرحله ای فعال شده از سایت خارج شوید و مجددا سعی کنید به سایت وارد شوید. در این مرحله یک انتخاب به شما داده میشود. میتوانید کد احراز هویت دو مرحله ای خود را توسط ایمیل یا پیامک دریافت کنید.
پس از اینکه گزینه خود را انتخاب کردید، کد احراز هویت دو مرحله ای برای شما ارسال میشود که توسط آن میتوانید پروسه ورود به سایت را تکمیل کنید.
حذف میانبرهای آزمایشی
همانطور که گفته شد پروژه نمونه شامل میانبرهایی برای تسهیل کار توسعه دهندگان است. در واقع اصلا نیازی به پیاده سازی سرویسهای ایمیل و پیامک ندارید و میتوانید با استفاده از این میانبرها حسابهای کاربری را تایید کنید و کدهای احراز هویت دو مرحله ای را نیز مشاهده کنید. اما قطعا این میانبرها پیش از توزیع اپلیکیشن باید حذف شوند.
بدین منظور باید نماها و کدهای مربوطه را ویرایش کنیم تا اینگونه اطلاعات به کلاینت ارسال نشوند. اگر کنترلر AccountController را باز کنید و به متد ()Register بروید با کد زیر مواجه خواهید شد.
if (result.Succeeded) { var code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme); await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>"); // This should not be deployed in production: ViewBag.Link = callbackUrl; return View("DisplayEmail"); } AddErrors(result);
نمایی که این متد باز میگرداند یعنی DisplayEmail.cshtml نیز باید ویرایش شود.
@{ ViewBag.Title = "DEMO purpose Email Link"; } <h2>@ViewBag.Title.</h2> <p class="text-info"> Please check your email and confirm your email address. </p> <p class="text-danger"> For DEMO only: You can click this link to confirm the email: <a href="@ViewBag.Link">link</a> Please change this code to register an email service in IdentityConfig to send an email. </p>
متد دیگری که در این کنترلر باید ویرایش شود ()VerifyCode است که کد احراز هویت دو مرحله ای را به صفحه مربوطه پاس میدهد.
[AllowAnonymous] public async Task<ActionResult> VerifyCode(string provider, string returnUrl) { // Require that the user has already logged in via username/password or external login if (!await SignInHelper.HasBeenVerified()) { return View("Error"); } var user = await UserManager.FindByIdAsync(await SignInHelper.GetVerifiedUserIdAsync()); if (user != null) { ViewBag.Status = "For DEMO purposes the current " + provider + " code is: " + await UserManager.GenerateTwoFactorTokenAsync(user.Id, provider); } return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl }); }
همانطور که میبینید متغیری بنام Status به ViewBag اضافه میشود که باید حذف شود.
نمای این متد یعنی VerifyCode.cshtml نیز باید ویرایش شود.
@model IdentitySample.Models.VerifyCodeViewModel @{ ViewBag.Title = "Enter Verification Code"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() @Html.ValidationSummary("", new { @class = "text-danger" }) @Html.Hidden("provider", @Model.Provider) <h4>@ViewBag.Status</h4> <hr /> <div class="form-group"> @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <div class="checkbox"> @Html.CheckBoxFor(m => m.RememberBrowser) @Html.LabelFor(m => m.RememberBrowser) </div> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <input type="submit" class="btn btn-default" value="Submit" /> </div> </div> }
در این فایل کافی است ViewBag.Status را حذف کنید.
از تنظیمات ایمیل و SMS محافظت کنید
در مثال جاری اطلاعاتی مانند نام کاربری و کلمه عبور، شناسههای SID و Auth Token همگی در کد برنامه نوشته شده اند. بهتر است چنین مقادیری را بیرون از کد اپلیکیشن نگاه دارید، مخصوصا هنگامی که پروژه را به سرویس کنترل ارسال میکند (مثلا مخازن عمومی مثل GitHub). بدین منظور میتوانید یکی از پستهای اخیر را مطالعه کنید.
دکمه F5 را بزنید و برنامه را اجرا کنید. یک دکمه میبینید که Text آن عبارت + است.
اصولا خوندن کتاب کاغذی را نسبت به PDF ترجیح میدم اگه فارسی هم باشه چه بهتر
به نظر من کتابهای فارسی :
اکثرا ترجمه روانی ندارند یا بعضی از لغتها بهتره ترجمه نشه که ترجمه شده (بعضی وقتها فکر میکنی گوگل ترجمه کرده یا یک فرد غیر متخصص که تجربه ای در این رشته نداره)
این کتابها بومی نشده اند (اگه تالیف یک کتاب سخته حداقل میتونه لابه لای مطالب اون نکات و یا ابزارهایی که برای خواننده ایرانی مهم است باشه )
نمونه بد اون
مثلا کتاب Visual C# 2010 ترجمه احمد پهلوان انتشارات ناقوس که فصل آخر اون که راجب وب سرویس هست هر جایی که باید مینوشته WCF به اشتباه نوشته WPF و خیلی مشکلات دیگه
و یه نمونه خوب که مشکلات بالا رو نداره
سری آموزشی ASP.NET MVC وحید نصیری که اگه چاپ شده بود حتما میخریدمش