مطالب
Asp.Net Identity #2
پیشتر در اینجا در مورد تاریخچه‌ی سیستم Identity مطالبی را عنوان کردیم. در این مقاله می‌خواهیم نحوه‌ی برپایی سیستم Identity را بحث کنیم.
ASP.NET Identity مانند ASP.NET Membership به اسکیمای SQL Server وابسته نیست؛ اما Relational Storage همچنان واحد ذخیره سازی پیش فرض و آسانی می‌باشد. بدین جهت که تقریبا بین همه‌ی توسعه دهندگان جا افتاده است. ما در این نوشتار از LocalDB جهت ذخیره سازی جداول استفاده می‌کنیم. ذکر این نکته ضروری است که سیستم Identity از Entity Framework Code First جهت ساخت اسکیما استفاده می‌کند.
پیش از هر چیز، ابتدا یک پروژه‌ی وب را ایجاد کنید؛ مانند شکل زیر:

در مرحله‌ی بعد سه پکیج زیر را باید نصب کنیم :

- Microsoft.AspNet.Identity.EntityFramework

-   Microsoft.AspNet.Identity.OWIN

-  Microsoft.Owin.Host.SystemWeb

بعد از اینکه پکیج‌های بالا را نصب کردیم، باید فایل Web.config را بروز کنیم. اولین مورد، تعریف یک Connection String می‌باشد:
<connectionStrings>
 <add name="IdentityDb" 
      providerName="System.Data.SqlClient"
      connectionString="Data Source=(localdb)\v11.0;Initial Catalog=IdentityDb;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False;MultipleActiveResultSets=True"/>
 </connectionStrings>
بعد از آن، تعریف یک کلید در قسمت AppSettings تحت عنوان Owin:AppStartup است:
<appSettings>
 <add key="webpages:Version" value="3.0.0.0" />
 <add key="webpages:Enabled" value="false" />
 <add key="ClientValidationEnabled" value="true" />
 <add key="UnobtrusiveJavaScriptEnabled" value="true" />
 <add key="owin:AppStartup" value="Users.IdentityConfig" />
 </appSettings>
Owin مدل شروع برنامه (Application Startup Model) خودش را تعریف می‌کند که از کلاس کلی برنامه (منظور Global.asax) جداست. AppSetting تعریف شده با نام owin:Startup شناخته می‌شود و مشخص کننده کلاسی است که Owin وهله سازی خواهد کرد. وقتی برنامه شروع به کار می‌کند، تنظیمات خودش را از این کلاس دریافت خواهد کرد (در این نوشتار کلاس IdentityConfig می‌باشد).

ساخت کلاس User
مرحله‌ی بعد ساخت کلاس User می‌باشد. این کلاس بیانگر یک کاربر می‌باشد. کلاس User باید از کلاس IdentityUser ارث بری کند که این کلاس در فضای نام Microsoft.AspNet.Identity.EntityFramework قرار دارد. کلاس IdentityUser فراهم کننده‌ی یک کاربر عمومی و ابتدایی است. اگر بخواهیم اطلاعات اضافی مربوط به کاربر را ذخیره کنیم باید آنها در کلاسی که از کلاس IdentityUser ارث بری می‌کند قرار دهیم. کلاس ما در اینجا AppUser نام دارد.
using System;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Users.Models 
{
   public class AppUser : IdentityUser 
  {
      // پروپرتی‌های اضافی در اینجا
  }
}

ساخت کلاس Database Context برنامه
مرحله‌ی بعد ساخت کلاس DbContext برنامه می‌باشد که بر روی کلاس AppUser عمل میکند. کلاس Context برنامه که ما در اینجا آن را AppIdentityDbContext تعریف کرده‌ایم، از کلاس <IdentityDbContext<T ارث بری می‌کند که T همان کلاس User می‌باشد (در مثال ما AppUser) .
using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Models;
namespace Users.Infrastructure {
 public class AppIdentityDbContext : IdentityDbContext<AppUser> 
{
   public AppIdentityDbContext() 
              : base("IdentityDb") { }

  static AppIdentityDbContext() 
 {
    Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());
  }
 public static AppIdentityDbContext Create() {
 return new AppIdentityDbContext();
 }
 }
public class IdentityDbInit
 : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> {
 protected override void Seed(AppIdentityDbContext context) {
 PerformInitialSetup(context);
 base.Seed(context);
 }
 public void PerformInitialSetup(AppIdentityDbContext context) {
 // initial configuration will go here
 }
 }
}

ساخت کلاس User Manager
یکی از مهمترین کلاسهای Identity کلاس User Manager (مدیر کاربر) می‌باشد که نمونه‌هایی از کلاس User را مدیریت می‌کند. کلاسی را که تعریف می‌کنیم، باید از کلاس <UserManager<T ارث بری کند که T همان کلاس User می‌باشد. کلاس UserManager خاص EF نمی‌باشد و ویژگی‌های عمومی بیشتری برای ساخت و انجام عملیات بر روی داده‌های کاربر را فراهم می‌نماید.

کلاس UserManager حاوی متدهای بالا است. اگر دقت کنید، می‌بینید که تمامی متدهای بالا به کلمه‌ی Async ختم می‌شوند. زیرا Asp.Net Identity تقریبا کل ویژگیهای برنامه نویسی Async را پیاده سازی کرده است و این بدین معنی است که عملیات میتوانند به صورت غیر همزمان اجرا شده و دیگر فعالیت‌ها را بلوکه نکنند.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models;

namespace Users.Infrastructure 
{
 public class AppUserManager : UserManager<AppUser> 
{

 public AppUserManager(IUserStore<AppUser> store)
 : base(store) {
 }
 public static AppUserManager Create( IdentityFactoryOptions<AppUserManager> options, IOwinContext context) 
{
 AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
 AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
 return manager;
 }
 }
}

زمانی که Identity نیاز به وهله‌ای از کلاس AppUserManager داشته باشد، متد استاتیک Create را صدا خواهد زد. این عمل زمانی اتفاق می‌افتد که عملیاتی بر روی داده‌های کاربر انجام گیرد. برای ساخت وهله‌ای از کلاس AppUserManager نیاز به کلاس <UserStor<AppUser دارد. کلاس <UserStore<T یک پیاده سازی از رابط <IUserStore<T  توسط  EF میباشد که وظیفه‌ی آن فراهم کننده‌ی پیاده سازی Storage-Specific متدهای تعریف شده در کلاس User Manager (که در اینجا AppUserManager ) می‌باشد. برای ساخت <UserStore<T نیاز به وهله‌ای از کلاس AppIdentityDbContext می‌باشد که از طریق Owin به صورت زیر قابل دریافت است:

AppIdentityDbContext db = context.Get<AppIdentityDbContext>();

یک پیاده سازی از رابط IOwinContext، به عنوان پارامتر به متد Create پاس داده می‌شود. در این پیاده سازی، یک تابع جنریک به نام Get تعریف شده که اقدام به برگشت وهله ای از اشیای ثبت شده‌ی در کلاس شروع Owin می‌نماید.


ساخت کلاس شروع Owin

اگر خاطرتان باشد یک کلید در قسمت AppSettings فایل Web.config به صورت زیر تعریف کردیم:

<add key="owin:AppStartup" value="Users.IdentityConfig" />

قسمت Value کلید بالا از دو قسمت تشکیل شده است: Users بیانگر فضای نام برنامه‌ی شماست و IdentityConfig بیانگر کلاس شروع می‌باشد. (البته شما می‌توانید هر نام دلخواهی را برای کلاس شروع بگذارید. فقط دقت داشته باشید که نام کلاس شروع و مقدار، با کلیدی که تعریف می‌کنید یکی باشد)

Owin مستقل از ASP.NET اعلام شده است و قراردادهای خاص خودش را دارد. یکی از این قراردادها تعریف یک کلاس و وهله سازی آن به منظور بارگذاری و پیکربندی میان افزار و انجام دیگر کارهای پیکربندی که نیاز است، می‌باشد. به طور پیش فرض این کلاس Start نام دارد و در پوشه‌ی App_Start تعریف می‌شود. این کلاس حاوی یک متد به نام Configuration می‌باشد که بوسیله زیرساخت Owin فراخوانی می‌شود و یک پیاده سازی از رابط Owin.IAppBuilder به عنوان آرگومان به آن پاس داده می‌شود که کار پشتیبانی از Setup میان افزار مورد نیاز برنامه را برعهده دارد.

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Users.Infrastructure;
namespace Users 
{
 public class IdentityConfig 
{
 public void Configuration(IAppBuilder app) 
{
 app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);
 app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
 app.UseCookieAuthentication(new CookieAuthenticationOptions {
 AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
 LoginPath = new PathString("/Account/Login"),
 });
 }
 }
}

رابط IAppBuilder بوسیله تعدادی متد الحاقی که در کلاسهایی که در فضای نام Owin تعریف شده‌اند، تکمیل شده است. متد CreatePerOwinContext کار ساخت وهله‌ای از کلاس AppUserManager و کلاس AppIdentityDbContext را برای هر درخواست بر عهده دارد. این مورد تضمین می‌کند که هر درخواست، به یک داده‌ی تمیز از Asp.Net Identity دسترسی خواهد داشت و نگران اعمال همزمانی و یا کش ضعیف داده نخواهد بود. متد UseCookieAuthentication به Asp.Net Identity می‌گوید که چگونه از کوکی‌ها برای تصدیق هویت کاربران استفاده کند که Optionهای آن از طریق کلاس CookieAuthenticationOptions مشخص می‌شود. مهمترین قسمت در اینجا پروپرتی LoginPath می‌باشد و مشخص می‌کند که کلاینت‌های تصدیق هویت نشده، هنگام دسترسی به یک منبع محافظت شده، به کدام URL هدایت شوند که توسط یک رشته به متد PathString پاس داده می‌شود.

خوب دوستان برپایی سیستم Identity به پایان رسید. انشالله در قسمت بعدی به چگونگی استفاده‌ی از این سیستم خواهیم پرداخت.

مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت چهارم - شاخص‌های مهم اطلاعات آماری کوئری‌ها
تا اینجا با روش‌های مختلف جمع آوری اطلاعات آماری مرتبط با کوئری‌های اجرا شده‌ی در SQL Server آشنا شدیم. در این قسمت قصد داریم بررسی کنیم این اطلاعات جمع آوری شده، چه مفاهیمی را در بر دارند و مهم‌ترین‌های آن‌ها کدامند؟


شاخص‌های مهم بررسی کارآیی کوئری‌ها

در ابتدای بررسی هر کوئری، باید 4 شاخص بسیار مهم، مدنظر باشند:
- مدت زمان اجرای کوئری: هرچند بررسی مدت زمان اجرای کوئری، شاخص مهمی‌است، اما الزاما حاوی اطلاعات مفیدی در مورد آن کوئری نیست. برای مثال اگر یک کوئری زیاد طول می‌کشد، حتما به معنای وجود مشکلی با آن نیست؛ ممکن است اطلاعات زیادی را واکشی می‌کند یا ممکن است توسط عاملی سد شده‌است. در این موارد هرچند مشکلاتی وجود دارند، اما مستقیما مرتبط با آن کوئری نیستند.
- میزان مصرف CPU: میزان کاری که باید توسط CPU انجام شود تا کوئری به نتیجه برسد.
- I/O: در SQL Server می‌توان هم physical I/O و هم logical I/O را بررسی کرد. برای مثال اگر اطلاعات مورد درخواست توسط کوئری هم اکنون در حافظه موجود باشند، نیازی به physical I/O پرهزینه نخواهد بود و در مقابل آن logical I/O کم هزینه‌تر است.
- میزان مصرف حافظه

در کل هر کدام از این شاخص‌ها اگر دارای مقدار بالایی باشند، بیانگر وجود مشکلی است.


مروری بر ابزارهای مختلف اندازه‌گیری شاخص‌های کارآیی

Management studio
درون Management studio می‌توان اطلاعات مرتبط با یک کوئری را به صورت زنده مشاهده کرد. البته این اطلاعات صرفا مرتبط با یک کوئری و یا تعدادی مشخص هستند؛ چون باید کوئری را به صورت دستی درون این برنامه اجرا کرد و سپس اطلاعات اجرای کوئری‌ها را دریافت نمود. اطلاعات آماری که توسط آن نیز ارائه می‌شود محدودیت‌هایی دارد. برای مثال مدت زمان اجرای کوئری و یا تعداد رکوردهای تحت تاثیر قرار گرفته شده را می‌توان مشاهده کرد. اما به اندازه‌ی اطلاعات ارائه شده‌ی در یک execution plan کامل نیست. به علاوه بازگشت اطلاعات حاصل از اجرای کوئری‌ها درون این برنامه، سربار خودش را داشته و سبب کند شدن برنامه می‌شود. در آخر اطلاعات ارائه شده‌ی توسط آن‌را نیز باید از قسمت‌های مختلفی جمع آوری و به صورت دستی ذخیره کرد.

Extended Events
توسط Extended Events نیز می‌توان همانند Management studio، اطلاعات آماری یک تک کوئری و یا یک batch را جمع آوری کرد؛ اما پس از ایجاد و تنظیم آن، به صورت خودکار اجرا می‌شود. در حین تعریف یک سشن Extended Events می‌توان شاخص‌های خاصی را انتخاب کرد و یا شرط‌های دقیقی را اعمال کرد. خروجی آن نیز به صورت خودکار در یک فایل ذخیره می‌شود.

Dynamic management objects
با استفاده از DMO's از نتایج آماری مرتبط با تک کوئری‌ها، به نتایج تجمعی حاصل از اجرای آن‌ها می‌رسیم. این نتایج نیز در plan cache ذخیره می‌شوند. به این معنا که اگر کش، تخلیه (با اجرای دستور DBCC FREEPROCCACHE) و یا سرور ری‌استارت شود، این اطلاعات از دست خواهند رفت. هدف آن بیشتر رفع اشکال کوئری‌هایی است که هم اکنون در حال اجرا هستند. اگر نیاز به اطلاعات دوره‌ای را داشته باشید، نیاز خواهید داشت تا با تهیه‌ی snapshotهایی از بانک اطلاعاتی، این تاریخچه را تکمیل کنید. به همین جهت Query Store ارائه شده‌است تا نیازی به اینکار نباشد.

Query Store
Query Store کار ذخیره سازی متن plan و آمار تجمعی مرتبط با آن‌را به صورت خودکار انجام می‌دهد و آن‌را درون بانک اطلاعاتی کاربر ذخیره می‌کند. به همین جهت با خالی شدن کش، برخلاف DMO's، اطلاعات آن حذف نمی‌شود.


مثالی از روش‌های مختلف جمع آوری اطلاعات آماری حاصل از اجرای کوئری‌ها در SQL Server

در ادامه قصد داریم با مثالی، خلاصه‌ای را از سه قسمتی که تاکنون بررسی کردیم، ارائه دهیم. برای این منظور ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetPersonInfo];
GO

CREATE PROCEDURE [Application].[usp_GetPersonInfo]
    (@PersonID INT)
AS

SELECT
    [p].[FullName],
    [p].[EmailAddress],
    [c].[FormalName]
FROM [Application].[People] [p]
    LEFT OUTER JOIN [Application].[Countries] [c]
    ON [p].[PersonID] = [c].[LastEditedBy]
WHERE [p].[PersonID] = @PersonID;
GO
کار آن دریافت اطلاعات یک کاربر بر اساس ID او می‌باشد.

سپس یک سشن Extended event را با نام QueryPerf ایجاد می‌کنیم:
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf]
ON SERVER
ADD EVENT sqlserver.sp_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.query_post_execution_showplan
ADD TARGET package0.event_file(
SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF);
GO
این سشن به رخ‌دادهای sql_statement_completed، sp_statement_completed و query_post_execution_showplan، اگر طول مدت آن کوئری بیش از 1 میلی ثانیه باشد، واکنش نشان می‌دهد. نتیجه‌ی نهایی را نیز در پوشه‌ی C:\Temp\QueryPerf ذخیره می‌کند (این پوشه را باید به صورت دستی ایجاد کنید).

در ادامه Query Store را نیز بر روی بانک اطلاعاتی WideWorldImporters فعال کرده و همچنین اگر اطلاعاتی از پیش در آن وجود دارند، پاک می‌شود.
USE [master];
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON;
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
DATA_FLUSH_INTERVAL_SECONDS = 60,
INTERVAL_LENGTH_MINUTES = 5,
MAX_STORAGE_SIZE_MB = 100,
QUERY_CAPTURE_MODE = ALL,
SIZE_BASED_CLEANUP_MODE = AUTO,
MAX_PLANS_PER_QUERY = 200);
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR;
GO

سپس هر آنچه را که در plan cache نیز وجود دارد، حذف می‌کنیم:
DBCC FREEPROCCACHE;
GO

اکنون سشن QueryPerf را که پیشتر ایجاد کردیم، آغاز می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = START;
GO
نتیجه‌ی آن‌را در قسمت management->extended events، با سبز شدن آیکن QueryPerf می‌توانید مشاهده کنید.


در ادامه چون می‌خواهیم نتایج آماری را در management studio نیز مشاهده کنیم، ابتدا جمع آوری شاخص‌های آماری را در یک پنجره‌ی جدید new query، فعال می‌کنیم:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
GO
SET STATISTICS XML ON;
GO

همچنین در منوی Query، گزینه‌ی Include client statistics را نیز انتخاب می‌کنیم تا مشخص شود که آیا عملیات insert/update/delete انجام شده‌است. چه تعداد ردیف تحت تاثیر اجرای این کوئری قرار گرفته‌اند. چه تعداد تراکنش انجام شده‌است. همچنین اطلاعات آماری شبکه و زمان نیز ارائه شوند.

پس از این تنظیمات، اکنون نوبت به اجرای کوئری‌های زیر رسیده‌است که یکی پارامتری است و دیگری AdHoc:
USE [WideWorldImporters];
GO

EXECUTE [Application].[usp_GetPersonInfo] 1234;
GO

SELECT
    [s].[StateProvinceName],
    [s].[SalesTerritory],
    [s].[LatestRecordedPopulation],
    [s].[StateProvinceCode]
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = 'United States';
GO
با اجرای آن، در management studio، برگه‌های messages و client statistics ظاهر می‌شوند که هر کدام اینبار اطلاعات آماری اجرای این کوئری را به همراه دارند. همچنین در قسمت results، امکان مشاهده‌ی query plan، به علت فعال بودن اطلاعات آماری XML، وجود دارد.




سپس سشن QueryPerf را متوقف و حذف می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = STOP;
GO

DROP EVENT SESSION [QueryPerf] ON SERVER;
GO
فایل خروجی با پسوند xel آن را که در پوشه‌ی C:\Temp\QueryPerf ذخیره شده‌است، می‌توان در management studio مشاهده کرد. البته در ابتدای نمایش آن، صرفا دو ستون name و timestamp را نمایش می‌دهد که می‌توان با انتخاب هر ردیف آن و سپس انتخاب و کلیک راست بر روی ردیف‌های details آن، گزینه‌ی Show Column in table را انتخاب کرد تا شاخص مدنظر، در ستون‌های گزارش نیز ظاهر شود.


اگر بخواهیم از عملیات صورت گرفته توسط DMO's کوئری بگیریم:
SELECT
    [qs].[last_execution_time],
    [qs].[execution_count],
    [qs].[total_elapsed_time],
    [qs].[total_elapsed_time]/[qs].[execution_count] [AvgDuration],
    [qs].[total_logical_reads],
    [qs].[total_logical_reads]/[qs].[execution_count] [AvgLogicalReads],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_query_stats [qs]
CROSS APPLY sys.dm_exec_sql_text([qs].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([qs].[plan_handle]) [p]
WHERE [t].[text] LIKE '%Countries%';
GO
که در آن تنها ردیف‌هایی که متن کوئری آن‌ها حاوی Countries است، فیلتر شده، به یک چنین خروجی خواهیم رسید:


همانطور که مشاهده می‌کنید، شاخص‌های چهارگانه‌ای که در ابتدای بحث معرفی شدند، در مورد کوئری پارامتری نوشته شده، وضعیت بسیار بهتری نسبت به کوئری AdHoc دوم دارند.

از Query Store هم می‌توان به صورت زیر کوئری گرفت (علاوه بر قسمت رابط کاربری Query Store که ذیل اشیاء مرتبط با بانک اطلاعاتی WideWorldImporters در management studio قابل مشاهده‌است):
USE [WideWorldImporters];
GO

SELECT
    [qsq].[query_id],
    [qst].[query_sql_text],
    CASE
WHEN [qsq].[object_id] = 0 THEN N'Ad-hoc'
ELSE OBJECT_NAME([qsq].[object_id])
END AS [ObjectName],
    [qsp].[plan_id],
    [rs].[count_executions],
    [rs].[avg_logical_io_reads],
    [rs].[avg_duration],
    TRY_CONVERT(XML, [qsp].[query_plan]),
    [rs].[last_execution_time],
    (DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())),
[rs].[last_execution_time])) AS [LocalLastExecutionTime]
FROM [sys].[query_store_query] [qsq]
    JOIN [sys].[query_store_query_text] [qst]
    ON [qsq].[query_text_id] = [qst].[query_text_id]
    JOIN [sys].[query_store_plan] [qsp]
    ON [qsq].[query_id] = [qsp].[query_id]
    JOIN [sys].[query_store_runtime_stats] [rs]
    ON [qsp].[plan_id] = [rs].[plan_id]
WHERE [qst].[query_sql_text] LIKE '%Countries%';
GO

مطالب
ASP.NET MVC #14

آشنایی با نحوه معرفی تعاریف طرحبندی سایت به کمک Razor

ممکن است یک سری از اصطلاحات را در قسمت‌های قبل مانند master page در لابلای توضیحات ارائه شده، مشاهده کرده باشید. این نوع مفاهیم برای برنامه نویس‌های ASP.NET Web forms آشنا است (و اگر با Web forms view engine‌ در ASP.NET MVC کار کنید، دقیقا یکی است؛ البته با این تفاوت که فایل code behind آن‌ها حذف شده است). به همین جهت در این قسمت برای تکمیل بحث، مروری خواهیم داشت بر نحوه‌ی معرفی جدید آن‌ها توسط Razor.
در یک پروژه جدید ASP.NET MVC و در پوشه Views\Shared\_Layout.cshtml آن، فایل Layout آن،‌ مفهوم master page را دارد. در این نوع فایل‌ها، زیر ساخت مشترک تمام صفحات سایت قرار می‌گیرند:

<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>

<body>
@RenderBody()
</body>
</html>

اگر دقت کرده باشید، در هیچکدام از فایل‌های Viewایی که تا این قسمت به پروژه‌های مختلف اضافه کردیم، تگ‌هایی مانند body، title و امثال آن وجود نداشتند. در ASP.NET مرسوم است کلیه اطلاعات تکراری صفحات مختلف سایت را مانند تگ‌های یاد شده به همراه منویی که باید در تمام صفحات قرار گیرد یا footer‌ مشترک بین تمام صفحات سایت، به یک فایل اصلی به نام master page که در اینجا layout نام گرفته، Refactor کنند. به این ترتیب حجم کدها و markup تکراری که باید در تمام Viewهای سایت قرار گیرند به حداقل خواهد رسید.
برای مثال محل قرار گیری تعاریف Content-Type تمام صفحات و همچنین favicon سایت، بهتر است در فایل layout باشد و نه در تک تک Viewهای تعریف شده:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />


در کدهای فوق یک نمونه پیش فرض فایل layout را مشاهده می‌کنید. در اینجا توسط متد RenderBody، محتوای رندر شده یک View درخواستی، به داخل تگ body تزریق خواهد شد.
تا اینجا در تمام مثال‌های قبلی این سری، فایل layout در Viewهای اضافه شده معرفی نشد. اما اگر برنامه را اجرا کنیم باز هم به نظر می‌رسد که فایل layout اعمال شده است. علت این است که در صورت عدم تعریف صریح layout در یک View، این تعریف از فایل Views\_ViewStart.cshtml دریافت می‌گردد:

@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

فایل ViewStart، محل تعریف کدهای تکراری است که باید پیش از اجرای هر View مقدار دهی یا اجرا شوند. برای مثال در اینجا می‌شود بر اساس نوع مرورگر،‌ layout خاصی را به تمام Viewها اعمال کرد. مثلا یک layout‌ ویژه برای مرورگرهای موبایل‌ها و layout ایی دیگر برای مرورگرهای معمولی. امکان دسترسی به متغیرهای تعریف شده در یک View در فایل ViewStart از طریق ViewContext.ViewData میسر است.
ضمن اینکه باید درنظر داشت که می‌توان فایل ViewStart را در زیر پوشه‌های پوشه اصلی View نیز قرار داد. مثلا اگر فایل ViewStart ایی در پوشه Views/Home قرار گرفت، این فایل محتوای ViewStart اصلی قرار گرفته در ریشه پوشه Views را بازنویسی خواهد کرد.
برای معرفی صریح فایل layout، تنها کافی است مسیر کامل فایل layout را در یک View مشخص کنیم:

@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

اهمیت این مساله هم در اینجا است که یک سایت می‌تواند چندین layout یا master page داشته باشد. برای نمونه یک layout برای صفحات ورود اطلاعات؛ یک layout خاص هم مثلا برای صفحات گزارش گیری نهایی سایت.
همانطور که پیشتر نیز ذکر شد، در ASP.NET حرف ~ به معنای ریشه سایت است که در اینجا ابتدای محل جستجوی فایل layout را مشخص می‌کند.
به این ترتیب زمانیکه یک کنترلر، View خاصی را فراخوانی می‌کند، کار از فایل Views\Shared\_Layout.cshtml شروع خواهد شد. سپس View درخواستی پردازش شده و محتوای نهایی آن، جایی که متد RenderBody قرار دارد، تزریق خواهد شد.
همچنین مقدار ViewBag.Title ایی که در فایل View تعریف شده، در فایل layout جهت رندر مقدار تگ title استفاده می‌شود (انتقال یک متغیر از View به یک فایل master page؛ کلاس layout، مدل View ایی را که قرار است رندر کند به ارث می‌برد).

یک نکته:
در نگارش سوم ASP.NET MVC امکان بکارگیری حرف ~ به صورت مستقیم در حین تعریف یک فایل js یا css وجود ندارد و حتما باید از متد سمت سرور Url.Content کمک گرفت. در نگارش چهارم ASP.NET MVC، این محدودیت برطرف شده و دقیقا همانند متغیر Layout ایی که در بالا مشاهده می‌کنید، می‌توان بدون نیاز به متد Url.Content، مستقیما از حرف ~ کمک گرفت و به صورت خودکار پردازش خواهد شد.


تزریق نواحی ویژه یک View در فایل layout

توسط متد RenderBody، کل محتوای View درخواستی در موقعیت تعریف شده آن در فایل Layout، رندر می‌شود. این ویژگی به نحو یکسانی به تمام Viewها اعمال می‌شود. اما اگر نیاز باشد تا view بتواند محتوای markup قسمت ویژه‌ای از master page را مقدار دهی کند، می‌توان از مفهومی به نام Sections استفاده کرد:
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
<div id="menu">
@if (IsSectionDefined("Menu"))
{
RenderSection("Menu", required: false);
}
else
{
<span>This is the default ...!</span>
}
</div>
<div id="body">
@RenderBody()
</div>
</body>
</html>

در اینجا ابتدا بررسی می‌شود که آیا قسمتی به نام Menu در View جاری که باید رندر شود وجود دارد یا خیر. اگر بله، توسط متد RenderSection، آن قسمت نمایش داده خواهد شد. در غیراینصورت، محتوای پیش فرضی را در صفحه قرار می‌دهد. البته اگر از متد RenderSection با آرگومان required: false استفاده شود، درصورتیکه View جاری حاوی قسمتی به نام مثلا menu نباشد، تنها چیزی نمایش داده نخواهد شد. اگر این آرگومان را حذف کنیم، یک استثنای عدم یافت شدن ناحیه یا قسمت مورد نظر صادر می‌گردد.
نحوه‌ی تعریف یک Section در Viewهای برنامه به شکل زیر است:
@{
ViewBag.Title = "Index";
//Layout = null;
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
Index</h2>
@section Menu{
<ul>
<li>item 1</li>
<li>item 2</li>
</ul>
}

برای مثال فرض کنید که یکی از Viewهای شما نیاز به دو فایل اضافی جاوا اسکریپت برای اجرای صحیح خود دارد. می‌توان تعاریف الحاق این دو فایل را در قسمت header فایل layout قرار داد تا در تمام Viewها به صورت خودکار لحاظ شوند. یا اینکه یک section سفارشی را به نحو زیر در آن View خاص تعریف می‌کنیم:

@section JavaScript
{
<script type="text/javascript" src="@Url.Content("~/Scripts/SomeScript.js")" />;
<script type="text/javascript" src="@Url.Content("~/Scripts/AnotherScript.js")" />;
}

سپس کافی است جهت تزریق این کدها به header تعریف شده در master page مورد نظر، یک سطر زیر را اضافه کرد:

@RenderSection("JavaScript", required: false)

به این ترتیب، اگر view ایی حاوی تعریف قسمت JavaScript نبود، به صورت خودکار شامل تعاریف الحاق اسکریپت‌های یاد شده نیز نخواهد گردید. در نتیجه دارای حجمی کمتر و سرعت بارگذاری بالاتری نیز خواهد بود.



مدیریت بهتر فایل‌ها و پوشه‌های یک برنامه ASP.NET MVC به کمک Areas

به کمک قابلیتی به نام Areas می‌توان یک برنامه بزرگ را به چندین قسمت کوچکتر تقسیم کرد. هر کدام از این نواحی، دارای تعاریف مسیریابی، کنترلرها و Viewهای خاص خودشان هستند. به این ترتیب دیگر به یک برنامه‌ی از کنترل خارج شده ASP.NET MVC که دارای یک پوشه Views به همراه صدها زیر پوشه است، نخواهیم رسید و کنترل این نوع برنامه‌های بزرگ ساده‌تر خواهد شد.
برای مثال یک برنامه بزرگ را درنظر بگیرید که به کمک قابلیت Areas، به نواحی ویژه‌ای مانند گزارشگیری، قسمت ویژه مدیریتی، قسمت کاربران، ناحیه بلاگ سایت، ناحیه انجمن سایت و غیره، تقسیم شده است. به علاوه هر کدام از این نواحی نیز هنوز می‌توانند از اطلاعات ناحیه اصلی برنامه مانند master page آن استفاده کنند. البته باید درنظر داشت که فایل viewStart به پوشه جاری و زیر پوشه‌های آن اعمال می‌شود. اگر نیاز باشد تا اطلاعات این فایل به کل برنامه اعمال شود، فقط کافی است آن‌را به یک سطح بالاتر، یعنی ریشه سایت منتقل کرد.


نحوه افزودن نواحی جدید
افزودن یک Area جدید هم بسیار ساده است. بر روی نام پروژه در VS.NET کلیک راست کرده و سپس گزینه Add|Area را انتخاب کنید. سپس در صفحه باز شده، نام دلخواهی را وارد نمائید. مثلا نام Reporting را وارد نمائید تا ناحیه گزارشگیری برنامه از قسمت‌های دیگر آن مستقل شود. پس از افزودن یک Area جدید، به صورت خودکار پوشه جدیدی به نام Areas به ریشه سایت اضافه می‌شود. سپس داخل آن، پوشه‌ی دیگری به نام Reporting اضافه خواهد شد. پوشه reporting اضافه شده هم دارای پوشه‌های Model، Views و Controllers خاص خود می‌باشد.
اکنون که پوشه Areas به ریشه سایت اضافه شده است، با کلیک راست بر روی این پوشه نیز گزینه‌ی Add|Area در دسترس می‌باشد. برای نمونه یک ناحیه جدید دیگر را به نام Admin به سایت اضافه کنید تا بتوان امکانات مدیریتی سایت را از سایر قسمت‌های آن مستقل کرد.


نحوه معرفی تعاریف مسیریابی نواحی تعریف شده
پس از اینکه کار با Areas را آغاز کردیم، نیاز است تا با نحوه‌ی مسیریابی آن‌ها نیز آشنا شویم. برای این منظور فایل Global.asax.cs قرار گرفته در ریشه اصلی برنامه را باز کنید. در متد Application_Start، متدی به نام AreaRegistration.RegisterAllAreas، کار ثبت و معرفی تمام نواحی ثبت شده را به فریم ورک، به عهده دارد. کاری که در پشت صحنه انجام خواهد شد این است که به کمک Reflection تمام کلاس‌های مشتق شده از کلاس پایه AreaRegistration به صورت خودکار یافت شده و پردازش خواهند شد. این کلاس‌ها هم به صورت پیش فرض به نام SomeNameAreaRegistration.cs در ریشه اصلی هر Area توسط VS.NET تولید می‌شوند. برای نمونه فایل ReportingAreaRegistration.cs تولید شده، حاوی اطلاعات زیر است:

using System.Web.Mvc;

namespace MvcApplication11.Areas.Reporting
{
public class ReportingAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Reporting";
}
}

public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Reporting_default",
"Reporting/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}

توسط AreaName، یک نام منحصربفرد در اختیار فریم ورک قرار خواهد گرفت. همچنین از این نام برای ایجاد پیوند بین نواحی مختلف نیز استفاده می‌شود.
سپس در قسمت RegisterArea، یک مسیریابی ویژه خاص ناحیه جاری مشخص گردیده است. برای مثال تمام آدرس‌های ناحیه گزارشگیری سایت باید با http://localhost/reporting آغاز شوند تا مورد پردازش قرارگیرند. سایر مباحث آن هم مانند قبل است. برای مثال در اینجا نام اکشن متد پیش فرض، index تعریف شده و همچنین ذکر قسمت id نیز اختیاری است.
همانطور که ملاحظه می‌کنید، تعاریف مسیریابی و اطلاعات پیش فرض آن منطقی هستند و آنچنان نیازی به دستکاری و تغییر ندارند. البته اگر دقت کرده باشید مقدار نام controller پیش فرض، مشخص نشده است. بنابراین بد نیست که مثلا نام Home یا هر نام مورد نظر دیگری را به عنوان نام کنترلر پیش فرض در اینجا اضافه کرد.


تعاریف کنترلر‌های هم نام در نواحی مختلف
در ادامه مثال جاری که دو ناحیه Admin و Reporting به آن اضافه شده، به پوشه‌های Controllers هر کدام، یک کنترلر جدید را به نام HomeController اضافه کنید. همچنین این HomeController را در ناحیه اصلی و ریشه سایت نیز اضافه نمائید. سپس برای متد پیش فرض Index هر کدام هم یک View جدید را با کلیک راست بر روی نام متد و انتخاب گزینه Add view، اضافه کنید. اکنون برنامه را به همین نحو اجرا نمائید. اجرای برنامه با خطای زیر متوقف خواهد شد:

Multiple types were found that match the controller named 'Home'. This can happen if the route that services this
request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.

The request for 'Home' has found the following matching controllers:
MvcApplication11.Areas.Admin.Controllers.HomeController
MvcApplication11.Controllers.HomeController

فوق العاده خطای کاملی است و راه حل را هم ارائه داده است! برای اینکه مشکل ابهام یافتن HomeController برطرف شود، باید این جستجو را به فضاهای نام هر قسمت از نواحی برنامه محدود کرد (چون به صورت پیش فرض فضای نامی برای آن مشخص نشده، کل ناحیه ریشه سایت و زیر مجموعه‌های آن‌را جستجو خواهد کرد). به همین جهت فایل Global.asax.cs را گشوده و متد RegisterRoutes آن‌را مثلا به نحو زیر اصلاح نمائید:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
, namespaces: new[] { "MvcApplication11.Controllers" }
);
}

آرگومان چهارم معرفی شده، آرایه‌ای از نام‌های فضاهای نام مورد نظر را جهت یافتن کنترلرهایی که باید توسط این مسیریابی یافت شوند، تعریف می‌کند.
اکنون اگر مجددا برنامه را اجرا کنیم، بدون مشکل View متناظر با متد Index کنترلر Home نمایش داده خواهد شد.
البته این مشکل با نواحی ویژه و غیر اصلی سایت وجود ندارد؛ چون جستجوی پیش فرض کنترلرها بر اساس ناحیه است.
در ادامه مسیر http://localhost/Admin/Home را نیز در مرورگر وارد کنید. سپس بر روی صفحه در مروگر کلیک راست کرده و سورس صفحه را بررسی کنید. مشاهده خواهید کرد که master page یا فایل layout ایی به آن اعمال نشده است. علت را هم در ابتدای بحث Areas مطالعه کردید. فایل Views\_ViewStart.cshtml در سطحی که قرار دارد به ناحیه Admin اعمال نمی‌شود. آن‌را به ریشه سایت منتقل کنید تا layout اصلی سایت نیز به این قسمت اعمال گردد. البته بدیهی است که هر ناحیه می‌تواند layout خاص خودش را داشته باشد یا حتی می‌توان با مقدار دهی خاصیت Layout نیز در هر view، فایل master page ویژه‌ای را انتخاب و معرفی کرد.


نحوه ایجاد پیوند بین نواحی مختلف سایت
زمانیکه پیوندی را به شکل زیر تعریف می‌کنیم:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home")

یعنی ایجاد لینکی در ناحیه جاری. برای اینکه پیوند تعریف شده به ناحیه‌ای خارج از ناحیه جاری اشاره کند باید نام Area را صریحا ذکر کرد:

@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home",
routeValues: new { Area = "Admin" } , htmlAttributes: null)


همین نکته را باید حین کار با متد RedirectToAction نیز درنظر داشت:
public ActionResult Index()
{
return RedirectToAction("Index", "Home", new { Area = "Admin" });
}


مطالب
انتشار VS2010

یکی دو روزی هست که VS2010 منتشر شده و در این مطلب خلاصه‌ای از لینک‌های مفید مرتبط را جمع آوری کرده‌ام که در ادامه ملاحظه خواهید کرد:

دریافت فایل ISO اصلی
این فایل ISO منتشر شده، نسخه‌ی نهایی است و تنها تفاوت آن با نگارش اصلی منتشر شده برای دارندگان اکانت‌های MSDN ، عدم درج سریال در فایل setup.sdb آن است. تصاویر زیر متعلق است به دارندگان اکانت‌های MSDN و همانطور که مشاهده می‌کنید، محل وارد کردن سریالی که یافته‌اید، در فایل setup.sdb است. پس از آن نگارش Trial به صورت کامل نصب خواهد شد.








خلاصه‌ای از تازه‌ها و موارد مرتبط با VS2010 و دات نت 4

راستی! ReSharper 5.0 هم در همین زمان منتشر شده است.

مطالب
آپلود فایل‌های Excel در ASP.NET MVC توسط ExcelDataReader

در برنامه‌های تحت وب، در بعضی موارد نیاز داریم تا برای کاربر، امکان ثبت داده‌هایش را با آپلود فایل‌های Excel فراهم کنیم. برای مثال در مطلب خواندن اطلاعات از فایل اکسل با استفاده از LinqToExcel ، امکان خواندن از Excel توضیح داده شده، اما نقطه ضعف این روش‌ها، وابستگی به Providerهای مایکروسافت است که در صورت عدم نصب آن ها:

Microsoft.Jet.OLEDB.4.0 provider --> Excel 97-2003 format (.xls)
Microsoft.ACE.OLEDB.12.0 provider --> Excel 2007+ format (.xlsx)
با خطاهای زیر روبرو می‌شویم:
The ‘Microsoft.Jet.OLEDB.4.0’ provider is not registered on the local machine
The ‘Microsoft.ACE.OLEDB.12.0’ provider is not registered on the local machine

البته راه حل، نصب  Office 2007 Data Connectivity Components یا Office 2010 Database Engine بر روی سرور می‌باشد. اما اگر هاست اشتراکی بوده و اجازه نصب نداشته باشیم؟

در این مقاله به بررسی کتابخانه ExcelDataReader می‌پردازیم که امکان خواندن فایل‌های اکسل را بدون نیاز به نصب هرگونه پیش نیازی بر روی سرور، برای ما فراهم می‌کند.

برای این کار:

1-  ابتدا یک پروژه خالی Asp.Net MVC  را ایجاد می‌کنیم.
2-  با استفاده از دستورات زیر در Package Manager Console بسته‌های ExcelDataReader و ExcelDataReader.DataSet را نصب می‌کنیم:

PM> Install-Package ExcelDataReader
PM> Install-Package ExcelDataReader.DataSet
توجه: دو روش برای خواندن از فایل‌های اکسل در این کتابخانه وجود دارد که نصب بسته دوم مربوط به روش دوم آن است.


3-  سپس کنترلر مورد نظر (در اینجا HomeController) را ایجاد نموده و اکشن Upload را بصورت زیر در آن قرار می‌دهیم:

public ActionResult Upload()
{
      return View();
}

4- برای آپلود فایل، اکشن دیگری را با نام Upload نیاز داریم که آن را بصورت زیر ایجاد می‌کنیم:

توجه: در قطعه کد زیر سعی شده از حداقل کانفیگ کتابخانه استفاده شود. کانفیگ بیشتر

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Upload(HttpPostedFileBase upload)
{  
           // اعتبار سنجی فایل آپلود شده
            if (upload != null && upload.ContentLength > 0 && (upload.FileName.EndsWith(".xls") || upload.FileName.EndsWith(".xlsx")))
            {
                // با خواندن فایل به صورت باینری، این کتابخانه نیازی به نصب پیش نیازهای آفیس ندارد
                Stream stream = upload.InputStream;

                //  نیازی به نگرانی در مورد پسوند فایل نیست
                // کتابخانه به صورت خودکار کلاس مورد نظر برای پسوند مربوطه را استفاده می‌کند
                // ExcelDataReader.ExcelBinaryReader یا ExcelDataReader.ExcelOpenXmlReader
                IExcelDataReader reader = ExcelReaderFactory.CreateReader(stream);

                // روش ذکر شده در قسمت دوم برای خواندن کل اطلاعات بصورت یکجا
                DataSet result = reader.AsDataSet(new ExcelDataSetConfiguration()
               {
                    ConfigureDataTable = (tableReader) => new ExcelDataTableConfiguration()
                    {
                        // true: ردیف اول از فایل را به عنوان هدر در نظر می‌گیرد
                        // مقدار پیش فرض: false
                        UseHeaderRow = true
                    }
                });

                reader.Close();
                return View(result.Tables[0]);
            }
      ModelState.AddModelError("File", "Please upload Excel file ...");
      return View();
 }  

  روش دیگر خواندن اطلاعات:
do {
        while (reader.Read()) {
       // reader.GetDouble(0);
        }
    } while (reader.NextResult());


5-  خب حالا از یک View (با نام Upload) هم برای ارسال فایل و همچنین نمایش محتویات آپلود شده بصورت زیر استفاده می‌کنیم:
@model System.Data.DataTable
@using System.Data;

<h2>Upload File</h2>

@using (Html.BeginForm("Upload", "Home", null, FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary()

    <div>
        <input type="file" id="dataFile" name="upload" />
    </div>

    <div>
        <input type="submit" value="Upload" />
    </div>

    if (Model != null)
    {
        <table>
            <thead>
                <tr>
                    @foreach (DataColumn col in Model.Columns)
                    {
                        <th>@col.ColumnName</th>
                    }
                </tr>
            </thead>
            <tbody>
                @foreach (DataRow row in Model.Rows)
                {
                    <tr>
                        @foreach (DataColumn col in Model.Columns)
                        {
                            <td>@row[col.ColumnName]</td>
                        }
                    </tr>
                }
            </tbody>
        </table>
    }
}
توجه داشته باشید که این مثال آموزشی است و در پروژه واقعی، قطعا روش‌های بهتری برای پردازش، پیمایش و نمایش محتوا وجود دارد.


6-  حالا پروژه را اجرا می‌کنیم تا خروجی را مشاهده کنیم.

ابتدا فایل مورد نظر را انتخاب و آپلود می‌کنیم:


انتخا فایل اکسل

و خروجی به صورت زیر خواهد بود:

خروجی

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: UploadExcelFiles.rar

منابع: ^ و ^

مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- پیشنیار بحث «معرفی JSON Web Token»

پیاده سازی‌های زیادی را در مورد JSON Web Token با ASP.NET Web API، با کمی جستجو می‌توانید پیدا کنید. اما مشکلی که تمام آن‌ها دارند، شامل این موارد هستند:
- چون توکن‌های JWT، خودشمول هستند (در پیشنیاز بحث مطرح شده‌است)، تا زمانیکه این توکن منقضی نشود، کاربر با همان سطح دسترسی قبلی می‌تواند به سیستم، بدون هیچگونه مانعی لاگین کند. در این حالت اگر این کاربر غیرفعال شود، کلمه‌ی عبور او تغییر کند و یا سطح دسترسی‌های او کاهش یابند ... مهم نیست! باز هم می‌تواند با همان توکن قبلی لاگین کند.
- در روش JSON Web Token، عملیات Logout سمت سرور بی‌معنا است. یعنی اگر برنامه‌ای در سمت کاربر، قسمت logout را تدارک دیده باشد، چون در سمت سرور این توکن‌ها جایی ذخیره نمی‌شوند، عملا این logout بی‌مفهوم است و مجددا می‌توان از همان توکن قبلی، برای لاگین به سرور استفاده کرد. چون این توکن شامل تمام اطلاعات لازم برای لاگین است و همچنین جایی هم در سرور ثبت نشده‌است که این توکن در اثر logout، باید غیرمعتبر شود.
- با یک توکن از مکان‌های مختلفی می‌توان دسترسی لازم را جهت استفاده‌ی از قسمت‌های محافظت شده‌ی برنامه یافت (در صورت دسترسی، چندین نفر می‌توانند از آن استفاده کنند).

به همین جهت راه حلی عمومی برای ذخیره سازی توکن‌های صادر شده از سمت سرور، در بانک اطلاعاتی تدارک دیده شد که در ادامه به بررسی آن خواهیم پرداخت و این روشی است که می‌تواند به عنوان پایه مباحث Authentication و Authorization برنامه‌های تک صفحه‌ای وب استفاده شود. البته سمت کلاینت این راه حل با jQuery پیاده سازی شده‌است (عمومی است؛ برای طرح مفاهیم پایه) و سمت سرور آن به عمد از هیچ نوع بانک اطلاعات و یا ORM خاصی استفاده نمی‌کند. سرویس‌های آن برای بکارگیری انواع و اقسام روش‌های ذخیره سازی اطلاعات قابل تغییر هستند و الزامی نیست که حتما از EF استفاده کنید یا از ASP.NET Identity یا هر روش خاص دیگری.


نگاهی به برنامه


در اینجا تمام قابلیت‌های این پروژه را مشاهده می‌کنید.
- امکان لاگین
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize
- امکان دسترسی به یک کنترلر مزین شده‌ی با فلیتر Authorize جهت کاربری با نقش Admin
- پیاده سازی مفهوم ویژه‌ای به نام refresh token که نیاز به لاگین مجدد را پس از منقضی شدن زمان توکن اولیه‌ی لاگین، برطرف می‌کند.
- پیاده سازی logout


بسته‌های پیشنیاز برنامه

پروژه‌ای که در اینجا بررسی شده‌است، یک پروژه‌ی خالی ASP.NET Web API 2.x است و برای شروع به کار با JSON Web Tokenها، تنها نیاز به نصب 4 بسته‌ی زیر را دارد:
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.Jwt
PM> Install-Package structuremap
PM> Install-Package structuremap.web
بسته‌ی Microsoft.Owin.Host.SystemWeb سبب می‌شود تا کلاس OwinStartup به صورت خودکار شناسایی و بارگذاری شود. این کلاسی است که کار تنظیمات اولیه‌ی JSON Web token را انجام می‌دهد و بسته‌ی Microsoft.Owin.Security.Jwt شامل تمام امکاناتی است که برای راه اندازی توکن‌های JWT نیاز داریم.
از structuremap هم برای تنظیمات تزریق وابستگی‌های برنامه استفاده شده‌است. به این صورت قسمت تنظیمات اولیه‌ی JWT ثابت باقی خواهد ماند و صرفا نیاز خواهید داشت تا کمی قسمت سرویس‌های برنامه را بر اساس بانک اطلاعاتی و روش ذخیره سازی خودتان سفارشی سازی کنید.


دریافت کدهای کامل برنامه

کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید. در ادامه صرفا قسمت‌های مهم این کدها را بررسی خواهیم کرد.


بررسی کلاس AppJwtConfiguration

کلاس AppJwtConfiguration، جهت نظم بخشیدن به تعاریف ابتدایی توکن‌های برنامه در فایل web.config، ایجاد شده‌است. اگر به فایل web.config برنامه مراجعه کنید، یک چنین تعریفی را مشاهده خواهید کرد:
<appJwtConfiguration
    tokenPath="/login"
    expirationMinutes="2"
    refreshTokenExpirationMinutes="60"
    jwtKey="This is my shared key, not so secret, secret!"
    jwtIssuer="http://localhost/"
    jwtAudience="Any" />
این قسمت جدید بر اساس configSection ذیل که به کلاس AppJwtConfiguration اشاره می‌کند، قابل استفاده شده‌است (بنابراین اگر فضای نام این کلاس را تغییر دادید، باید این قسمت را نیز مطابق آن ویرایش کنید؛ درغیراینصورت، appJwtConfiguration قابل شناسایی نخواهد بود):
 <configSections>
    <section name="appJwtConfiguration" type="JwtWithWebAPI.JsonWebTokenConfig.AppJwtConfiguration" />
</configSections>
- در اینجا tokenPath، یک مسیر دلخواه است. برای مثال در اینجا به مسیر login تنظیم شده‌است. برنامه‌ای که با Microsoft.Owin.Security.Jwt کار می‌کند، نیازی ندارد تا یک قسمت لاگین مجزا داشته باشد (مثلا یک کنترلر User و بعد یک اکشن متد اختصاصی Login). کار لاگین، در متد GrantResourceOwnerCredentials کلاس AppOAuthProvider انجام می‌شود. اینجا است که نام کاربری و کلمه‌ی عبور کاربری که به سمت سرور ارسال می‌شوند، توسط Owin دریافت و در اختیار ما قرار می‌گیرند.
- در این تنظیمات، دو زمان منقضی شدن را مشاهده می‌کنید؛ یکی مرتبط است به access tokenها و دیگری مرتبط است به refresh tokenها که در مورد این‌ها، در ادامه بیشتر توضیح داده خواهد شد.
- jwtKey، یک کلید قوی است که از آن برای امضاء کردن توکن‌ها در سمت سرور استفاده می‌شود.
- تنظیمات Issuer و Audience هم در اینجا اختیاری هستند.

یک نکته
جهت سهولت کار تزریق وابستگی‌ها، برای کلاس AppJwtConfiguration، اینترفیس IAppJwtConfiguration نیز تدارک دیده شده‌است و در تمام تنظیمات ابتدایی JWT، از این اینترفیس بجای استفاده‌ی مستقیم از کلاس AppJwtConfiguration استفاده شده‌است.


بررسی کلاس OwinStartup

شروع به کار تنظیمات JWT و ورود آن‌ها به چرخه‌ی حیات Owin از کلاس OwinStartup آغاز می‌شود. در اینجا علت استفاده‌ی از SmObjectFactory.Container.GetInstance انجام تزریق وابستگی‌های لازم جهت کار با دو کلاس AppOAuthOptions و AppJwtOptions است.
- در کلاس AppOAuthOptions تنظیماتی مانند نحوه‌ی تهیه‌ی access token و همچنین refresh token ذکر می‌شوند.
- در کلاس AppJwtOptions تنظیمات فایل وب کانفیگ، مانند کلید مورد استفاده‌ی جهت امضای توکن‌های صادر شده، ذکر می‌شوند.


حداقل‌های بانک اطلاعاتی مورد نیاز جهت ذخیره سازی وضعیت کاربران و توکن‌های آن‌ها

همانطور که در ابتدای بحث عنوان شد، می‌خواهیم اگر سطوح دسترسی کاربر تغییر کرد و یا اگر کاربر logout کرد، توکن فعلی او صرفنظر از زمان انقضای آن، بلافاصله غیرقابل استفاده شود. به همین جهت نیاز است حداقل دو جدول زیر را در بانک اطلاعاتی تدارک ببینید:
الف) کلاس User
در کلاس User، بر مبنای اطلاعات خاصیت Roles آن است که ویژگی Authorize با ذکر نقش مثلا Admin کار می‌کند. بنابراین حداقل نقشی را که برای کاربران، در ابتدای کار نیاز است مشخص کنید، نقش user است.
همچنین خاصیت اضافه‌تری به نام SerialNumber نیز در اینجا درنظر گرفته شده‌است. این مورد را باید به صورت دستی مدیریت کنید. اگر کاربری کلمه‌ی عبورش را تغییر داد، اگر مدیری نقشی را به او انتساب داد یا از او گرفت و یا اگر کاربری غیرفعال شد، مقدار خاصیت و فیلد SerialNumber را با یک Guid جدید به روز رسانی کنید. این Guid در برنامه با Guid موجود در توکن مقایسه شده و بلافاصله سبب عدم دسترسی او خواهد شد (در صورت عدم تطابق).

ب) کلاس UserToken
در کلاس UserToken کار نگهداری ریز اطلاعات توکن‌های صادر شده صورت می‌گیرد. توکن‌های صادر شده دارای access token و refresh token هستند؛ به همراه زمان انقضای آن‌ها. به این ترتیب زمانیکه کاربری درخواستی را به سرور ارسال می‌کند، ابتدا token او را دریافت کرده و سپس بررسی می‌کنیم که آیا اصلا چنین توکنی در بانک اطلاعاتی ما وجود خارجی دارد یا خیر؟ آیا توسط ما صادر شده‌است یا خیر؟ اگر خیر، بلافاصله دسترسی او قطع خواهد شد. برای مثال عملیات logout را طوری طراحی می‌کنیم که تمام توکن‌های یک شخص را در بانک اطلاعاتی حذف کند. به این ترتیب توکن قبلی او دیگر قابلیت استفاده‌ی مجدد را نخواهد داشت.


مدیریت بانک اطلاعاتی و کلاس‌های سرویس برنامه

در لایه سرویس برنامه، شما سه سرویس را مشاهده خواهید کرد که قابلیت جایگزین شدن با کدهای یک ORM را دارند (نوع آن ORM مهم نیست):
الف) سرویس TokenStoreService
public interface ITokenStoreService
{
    void CreateUserToken(UserToken userToken);
    bool IsValidToken(string accessToken, int userId);
    void DeleteExpiredTokens();
    UserToken FindToken(string refreshTokenIdHash);
    void DeleteToken(string refreshTokenIdHash);
    void InvalidateUserTokens(int userId);
    void UpdateUserToken(int userId, string accessTokenHash);
}
کار سرویس TokenStore، ذخیره سازی و تعیین اعتبار توکن‌های صادر شده‌است. در اینجا ثبت یک توکن، بررسی صحت وجود یک توکن رسیده، حذف توکن‌های منقضی شده، یافتن یک توکن بر اساس هش توکن، حذف یک توکن بر اساس هش توکن، غیرمعتبر کردن و حذف تمام توکن‌های یک شخص و به روز رسانی توکن یک کاربر، پیش بینی شده‌اند.
پیاده سازی این کلاس بسیار شبیه است به پیاده سازی ORMهای موجود و فقط یک SaveChanges را کم دارد.

یک نکته:
در سرویس ذخیره سازی توکن‌ها، یک چنین متدی قابل مشاهده است:
public void CreateUserToken(UserToken userToken)
{
   InvalidateUserTokens(userToken.OwnerUserId);
   _tokens.Add(userToken);
}
استفاده از InvalidateUserTokens در اینجا سبب خواهد شد با لاگین شخص و یا صدور توکن جدیدی برای او، تمام توکن‌های قبلی او حذف شوند. به این ترتیب امکان لاگین چندباره و یا یافتن دسترسی به منابع محافظت شده‌ی برنامه در سرور توسط چندین نفر (که به توکن شخص دسترسی یافته‌اند یا حتی تقاضای توکن جدیدی کرده‌اند)، میسر نباشد. همینکه توکن جدیدی صادر شود، تمام لاگین‌های دیگر این شخص غیرمعتبر خواهند شد.


ب) سرویس UsersService
public interface IUsersService
{
    string GetSerialNumber(int userId);
    IEnumerable<string> GetUserRoles(int userId);
    User FindUser(string username, string password);
    User FindUser(int userId);
    void UpdateUserLastActivityDate(int userId);
}
از کلاس سرویس کاربران، برای یافتن شماره سریال یک کاربر استفاده می‌شود. در مورد شماره سریال پیشتر بحث کردیم و هدف آن مشخص سازی وضعیت تغییر این موجودیت است. اگر کاربری اطلاعاتش تغییر کرد، این فیلد باید با یک Guid جدید مقدار دهی شود.
همچنین متدهای دیگری برای یافتن یک کاربر بر اساس نام کاربری و کلمه‌ی عبور او (جهت مدیریت مرحله‌ی لاگین)، یافتن کاربر بر اساس Id او (جهت استخراج اطلاعات کاربر) و همچنین یک متد اختیاری نیز برای به روز رسانی فیلد آخرین تاریخ فعالیت کاربر در اینجا پیش بینی شده‌اند.

ج) سرویس SecurityService
public interface ISecurityService
{
   string GetSha256Hash(string input);
}
در قسمت‌های مختلف این برنامه، هش SHA256 مورد استفاده قرار گرفته‌است که با توجه به مباحث تزریق وابستگی‌ها، کاملا قابل تعویض بوده و برنامه صرفا با اینترفیس آن کار می‌کند.


پیاده سازی قسمت لاگین و صدور access token

در کلاس AppOAuthProvider کار پیاده سازی قسمت لاگین برنامه انجام شده‌است. این کلاسی است که توسط کلاس AppOAuthOptions به OwinStartup معرفی می‌شود. قسمت‌های مهم کلاس AppOAuthProvider به شرح زیر هستند:
برای درک عملکرد این کلاس، در ابتدای متدهای مختلف آن، یک break point قرار دهید. برنامه را اجرا کرده و سپس بر روی دکمه‌ی login کلیک کنید. به این ترتیب جریان کاری این کلاس را بهتر می‌توانید درک کنید. کار آن با فراخوانی متد ValidateClientAuthentication شروع می‌شود. چون با یک برنامه‌ی وب در حال کار هستیم، ClientId آن‌را نال درنظر می‌گیریم و برای ما مهم نیست. اگر کلاینت ویندوزی خاصی را تدارک دیدید، این کلاینت می‌تواند ClientId ویژه‌ای را به سمت سرور ارسال کند که در اینجا مدنظر ما نیست.
مهم‌ترین قسمت این کلاس، متد GrantResourceOwnerCredentials است که پس از ValidateClientAuthentication بلافاصله فراخوانی می‌شود. اگر به کدهای آن دقت کنید،  خود owin دارای خاصیت‌های user name و password نیز هست.
این اطلاعات را به نحو ذیل از کلاینت خود دریافت می‌کند. اگر به فایل index.html مراجعه کنید، یک چنین تعریفی را برای متد login می‌توانید مشاهده کنید:
function doLogin() {
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            username: "Vahid",
            password: "1234",
            grant_type: "password"
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
url آن به همان مسیری که در فایل web.config تنظیم کردیم، اشاره می‌کند. فرمت data ایی که به سرور ارسال می‌شود، دقیقا باید به همین نحو باشد و content type آن نیز مهم است و owin فقط حالت form-urlencoded را پردازش می‌کند. به این ترتیب است که user name و password توسط owin قابل شناسایی شده و grant_type آن است که سبب فراخوانی GrantResourceOwnerCredentials می‌شود و مقدار آن نیز دقیقا باید password باشد (در حین لاگین).
در متد GrantResourceOwnerCredentials کار بررسی نام کاربری و کلمه‌ی عبور کاربر صورت گرفته و در صورت یافت شدن کاربر (صحیح بودن اطلاعات)، نقش‌های او نیز به عنوان Claim جدید به توکن اضافه می‌شوند.

در اینجا یک Claim سفارشی هم اضافه شده‌است:
 identity.AddClaim(new Claim(ClaimTypes.UserData, user.UserId.ToString()));
کار آن ذخیره سازی userId کاربر، در توکن صادر شده‌است. به این صورت هربار که توکن به سمت سرور ارسال می‌شود، نیازی نیست تا یکبار از بانک اطلاعاتی بر اساس نام او، کوئری گرفت و سپس id او را یافت. این id در توکن امضاء شده، موجود است. نمونه‌ی نحوه‌ی کار با آن‌را در کنترلرهای این API می‌توانید مشاهده کنید. برای مثال در MyProtectedAdminApiController این اطلاعات از توکن استخراج شده‌اند (جهت نمایش مفهوم).

در انتهای این کلاس، از متد TokenEndpointResponse جهت دسترسی به access token نهایی صادر شده‌ی برای کاربر، استفاده کرده‌ایم. هش این access token را در بانک اطلاعاتی ذخیره می‌کنیم (جستجوی هش‌ها سریعتر هستند از جستجوی یک رشته‌ی طولانی؛ به علاوه در صورت دسترسی به بانک اطلاعاتی، اطلاعات هش‌ها برای مهاجم قابل استفاده نیست).

اگر بخواهیم اطلاعات ارسالی به کاربر را پس از لاگین، نمایش دهیم، به شکل زیر خواهیم رسید:


در اینجا access_token همان JSON Web Token صادر شده‌است که برنامه‌ی کلاینت از آن برای اعتبارسنجی استفاده خواهد کرد.

بنابراین خلاصه‌ی مراحل لاگین در اینجا به ترتیب ذیل است:
- فراخوانی متد  ValidateClientAuthenticationدر کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد GrantResourceOwnerCredentials در کلاس AppOAuthProvider . در اینجا کار اصلی لاگین به همراه تنظیم Claims کاربر انجام می‌شود. برای مثال نقش‌های او به توکن صادر شده اضافه می‌شوند.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token را انجام می‌دهد.
- فراخوانی متد CreateAsync در کلاس RefreshTokenProvider. کار این متد صدور توکن ویژه‌ای به نام refresh است. این توکن را در بانک اطلاعاتی ذخیره خواهیم کرد. در اینجا چیزی که به سمت کلاینت ارسال می‌شود صرفا یک guid است و نه اصل refresh token.
- فرخوانی متد TokenEndpointResponse در کلاس AppOAuthProvider . از این متد جهت یافتن access token نهایی تولید شده و ثبت هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی قسمت صدور Refresh token

در تصویر فوق، خاصیت refresh_token را هم در شیء JSON ارسالی به سمت کاربر مشاهده می‌کنید. هدف از refresh_token، تمدید یک توکن است؛ بدون ارسال کلمه‌ی عبور و نام کاربری به سرور. در اینجا access token صادر شده، مطابق تنظیم expirationMinutes در فایل وب کانفیگ، منقضی خواهد شد. اما طول عمر refresh token را بیشتر از طول عمر access token در نظر می‌گیریم. بنابراین طول عمر یک access token کوتاه است. زمانیکه access token منقضی شد، نیازی نیست تا حتما کاربر را به صفحه‌ی لاگین هدایت کنیم. می‌توانیم refresh_token را به سمت سرور ارسال کرده و به این ترتیب درخواست صدور یک access token جدید را ارائه دهیم. این روش هم سریعتر است (کاربر متوجه این retry نخواهد شد) و هم امن‌تر است چون نیازی به ارسال کلمه‌ی عبور و نام کاربری به سمت سرور وجود ندارند.
سمت کاربر، برای درخواست صدور یک access token جدید بر اساس refresh token صادر شده‌ی در زمان لاگین، به صورت زیر عمل می‌شود:
function doRefreshToken() {
    // obtaining new tokens using the refresh_token should happen only if the id_token has expired.
    // it is a bad practice to call the endpoint to get a new token every time you do an API call.
    $.ajax({
        url: "/login", // web.config --> appConfiguration -> tokenPath
        data: {
            grant_type: "refresh_token",
            refresh_token: refreshToken
        },
        type: 'POST', // POST `form encoded` data
        contentType: 'application/x-www-form-urlencoded'
در اینجا نحوه‌ی عملکرد، همانند متد login است. با این تفاوت که grant_type آن اینبار بجای password مساوی refresh_token است و مقداری که به عنوان refresh_token به سمت سرور ارسال می‌کند، همان مقداری است که در عملیات لاگین از سمت سرور دریافت کرده‌است. آدرس ارسال این درخواست نیز همان tokenPath تنظیم شده‌ی در فایل web.config است. بنابراین مراحلی که در اینجا طی خواهند شد، به ترتیب زیر است:
- فرخوانی متد ValidateClientAuthentication در کلاس AppOAuthProvider . طبق معمول چون ClientID نداریم، این مرحله را قبول می‌کنیم.
- فراخوانی متد ReceiveAsync در کلاس RefreshTokenProvider. در قسمت توضیح مراحل لاگین، عنوان شد که پس از فراخوانی متد GrantResourceOwnerCredentials جهت لاگین، متد CreateAsync در کلاس RefreshTokenProvider فراخوانی می‌شود. اکنون در متد ReceiveAsync این refresh token ذخیره شده‌ی در بانک اطلاعاتی را یافته (بر اساس Guid ارسالی از طرف کلاینت) و سپس Deserialize می‌کنیم. به این ترتیب است که کار درخواست یک access token جدید بر مبنای refresh token موجود آغاز می‌شود.
- فراخوانی GrantRefreshToken در کلاس AppOAuthProvider . در اینجا اگر نیاز به تنظیم Claim اضافه‌تری وجود داشت، می‌توان اینکار را انجام داد.
- فراخوانی متد Protect در کلاس AppJwtWriterFormat که کار امضای دیجیتال access token جدید را انجام می‌دهد.
- فراخوانی CreateAsync در کلاس RefreshTokenProvider . پس از اینکه context.DeserializeTicket در متد ReceiveAsync  بر مبنای refresh token قبلی انجام شد، مجددا کار تولید یک توکن جدید در متد CreateAsync شروع می‌شود و زمان انقضاءها تنظیم خواهند شد.
- فراخوانی TokenEndpointResponse در کلاس AppOAuthProvider . مجددا از این متد برای دسترسی به access token جدید و ذخیره‌ی هش آن در بانک اطلاعاتی استفاده می‌کنیم.


پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute

در ابتدای بحث عنوان کردیم که اگر مشخصات کاربر تغییر کردند یا کاربر logout کرد، امکان غیرفعال کردن یک توکن را نداریم و این توکن تا زمان انقضای آن معتبر است. این نقیصه را با طراحی یک AuthorizeAttribute سفارشی جدید به نام JwtAuthorizeAttribute برطرف می‌کنیم. نکات مهم این فیلتر به شرح زیر هستند:
- در اینجا در ابتدا بررسی می‌شود که آیا درخواست رسیده‌ی به سرور، حاوی access token هست یا خیر؟ اگر خیر، کار همینجا به پایان می‌رسد و دسترسی کاربر قطع خواهد شد.
- سپس بررسی می‌کنیم که آیا درخواست رسیده پس از مدیریت توسط Owin، دارای Claims است یا خیر؟ اگر خیر، یعنی این توکن توسط ما صادر نشده‌است.
- در ادامه شماره سریال موجود در access token را استخراج کرده و آن‌را با نمونه‌ی موجود در دیتابیس مقایسه می‌کنیم. اگر این دو یکی نبودند، دسترسی کاربر قطع می‌شود.
- همچنین در آخر بررسی می‌کنیم که آیا هش این توکن رسیده، در بانک اطلاعاتی ما موجود است یا خیر؟ اگر خیر باز هم یعنی این توکن توسط ما صادر نشده‌است.

بنابراین به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت:
- یکبار بررسی جدول کاربران جهت واکشی مقدار فیلد شماره سریال مربوط به کاربر.
- یکبار هم جهت بررسی جدول توکن‌ها برای اطمینان از صدور توکن رسیده توسط برنامه‌ی ما.

و نکته‌ی مهم اینجا است که از این پس بجای فیلتر معمولی Authorize، از فیلتر جدید JwtAuthorize در برنامه استفاده خواهیم کرد:
 [JwtAuthorize(Roles = "Admin")]
public class MyProtectedAdminApiController : ApiController


نحوه‌ی ارسال درخواست‌های Ajax ایی به همراه توکن صادر شده

تا اینجا کار صدور توکن‌های برنامه به پایان می‌رسد. برای استفاده‌ی از این توکن‌ها در سمت کاربر، به فایل index.html دقت کنید. در متد doLogin، پس از موفقیت عملیات دو متغیر جدید مقدار دهی می‌شوند:
var jwtToken;
var refreshToken;
 
function doLogin() {
    $.ajax({
     // same as before
    }).then(function (response) {
        jwtToken = response.access_token;
        refreshToken = response.refresh_token; 
    }
از متغیر jwtToken به ازای هربار درخواستی به یکی از کنترلرهای سایت، استفاده می‌کنیم و از متغیر refreshToken در متد doRefreshToken برای درخواست یک access token جدید. برای مثال اینبار برای اعتبارسنجی درخواست ارسالی به سمت سرور، نیاز است خاصیت headers را به نحو ذیل مقدار دهی کرد:
function doCallApi() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/MyProtectedApi",
        type: 'GET'
    }).then(function (response) {
بنابراین هر درخواست ارسالی به سرور باید دارای هدر ویژه‌ی Bearer فوق باشد؛ در غیراینصورت با پیام خطای 401، از دسترسی به منابع سرور منع می‌شود.


پیاده سازی logout سمت سرور و کلاینت

پیاده سازی سمت سرور logout را در کنترلر UserController مشاهده می‌کنید. در اینجا در اکشن متد Logout، کار حذف توکن‌های کاربر از بانک اطلاعاتی انجام می‌شود. به این ترتیب دیگر مهم نیست که توکن او هنوز منقضی شده‌است یا خیر. چون هش آن دیگر در جدول توکن‌ها وجود ندارد، از فیلتر JwtAuthorizeAttribute رد نخواهد شد.
سمت کلاینت آن نیز در فایل index.html ذکر شده‌است:
function doLogout() {
    $.ajax({
        headers: { 'Authorization': 'Bearer ' + jwtToken },
        url: "/api/user/logout",
        type: 'GET'
تنها کاری که در اینجا انجام شده، فراخوانی اکشن متد logout سمت سرور است. البته ذکر jwtToken نیز در اینجا الزامی است. زیرا این توکن است که حاوی اطلاعاتی مانند userId کاربر فعلی است و بر این اساس است که می‌توان رکوردهای او را در جدول توکن‌ها یافت و حذف کرد.


بررسی تنظیمات IoC Container برنامه

تنظیمات IoC Container برنامه را در پوشه‌ی IoCConfig می‌توانید ملاحظه کنید. از کلاس SmWebApiControllerActivator برای فعال سازی تزریق وابستگی‌ها در کنترلرهای Web API استفاده می‌شود و از کلاس SmWebApiFilterProvider برای فعال سازی تزریق وابستگی‌ها در فیلتر سفارشی که ایجاد کردیم، کمک گرفته خواهد شد.
هر دوی این تنظیمات نیز در کلاس WebApiConfig ثبت و معرفی شده‌اند.
به علاوه در کلاس SmObjectFactory، کار معرفی وهله‌های مورد استفاده و تنظیم طول عمر آن‌ها انجام شده‌است. برای مثال طول عمر IOAuthAuthorizationServerProvider از نوع Singleton است؛ چون تنها یک وهله از AppOAuthProvider در طول عمر برنامه توسط Owin استفاده می‌شود و Owin هربار آن‌را وهله سازی نمی‌کند. همین مساله سبب شده‌است که معرفی وابستگی‌ها در سازنده‌ی کلاس AppOAuthProvider کمی با حالات متداول، متفاوت باشند:
public AppOAuthProvider(
   Func<IUsersService> usersService,
   Func<ITokenStoreService> tokenStoreService,
   ISecurityService securityService,
   IAppJwtConfiguration configuration)
در کلاسی که طول عمر singleton دارد، وابستگی‌های تعریف شده‌ی در سازنده‌ی آن هم تنها یکبار در طول عمر برنامه نمونه سازی می‌شوند. برای رفع این مشکل و transient کردن آن‌ها، می‌توان از Func استفاده کرد. به این ترتیب هر بار ارجاهی به usersService، سبب وهله سازی مجدد آن می‌شود و این مساله برای کار با سرویس‌هایی که قرار است با بانک اطلاعاتی کار کنند ضروری است؛ چون باید طول عمر کوتاهی داشته باشند.
در اینجا سرویس IAppJwtConfiguration  با Func معرفی نشده‌است؛ چون طول عمر تنظیمات خوانده شده‌ی از Web.config نیز Singleton هستند و معرفی آن به همین نحو صحیح است.
اگر علاقمند بودید که بررسی کنید یک سرویس چندبار وهله سازی می‌شود، یک سازنده‌ی خالی را به آن اضافه کنید و سپس یک break point را بر روی آن قرار دهید و برنامه را اجرا و در این حالت چندبار Login کنید.
نظرات مطالب
EF Code First #4
با EF6 هم مشکلی مشاهده نشد:
Sample27.cs
مثال فوق از کلاس DbMigrator استفاده می‌کند. متد Test.RunTests آن‌را اجرا کنید؛ البته بعد از اضافه کردن Connection1 که در سورس ذکر شده. بانک اطلاعاتی جدیدی ساخته می‌شود. سپس به کلاس منو یک فیلد جدید اضافه کنید و مجددا برنامه را اجرا کنید. مشاهده خواهید کرد که این فیلد اضافه شده و خطایی صادر نمی‌شود.
مطالب
آغاز به کار با Twitter Bootstrap در ASP.NET MVC
Twitter Bootstrap یک فریم ورک CSS بسیار محبوب سورس باز تولید برنامه‌های وب به کمک HTML، CSS و جاوا اسکریپت است. این فریم ورک حاوی بسیاری از المان‌های مورد نیاز جهت تولید وب سایت‌هایی زیبا، مانند دکمه‌ها، عناصر فرم‌ها، منوها، ویجت‌ها و غیره است. تمام این‌ها نیز همانطور که عنوان شد برمبنای HTML، CSS و جاوا اسکریپت تهیه شده‌اند؛ بنابراین در هر نوع فناوری سمت سروری مانند ASP.NET، PHP، روبی و امثال آن قابل استفاده است.


دریافت Twitter Bootstrap

محل اصلی دریافت Twitter Bootstrap، آدرس ذیل است:
البته ما از آن در اینجا به شکل خام فوق استفاده نخواهیم کرد؛ زیرا نیاز است قابلیت‌های استفاده در محیط‌های راست به چپ فارسی نیز به آن اضافه شود. برای این منظور می‌توانید از یکی از دو بسته نیوگت ذیل استفاده نمائید:


و یا حتی از منابع سایت http://rbootstrap.ir نیز می‌توان استفاده کرد.
برای نمونه دستور زیر را در کنسول پاورشل ویژوال استودیو وارد نمائید تا اسکریپت‌ها و فایل‌های CSS مورد نیاز به پروژه جاری اضافه شوند:
 PM> Install-Package Twitter.BootstrapRTL
پس از نصب، شاهد افزوده شدن چنین فایل‌هایی به یک پروژه ASP.NET MVC خواهیم بود:


در اینجا فایل‌های min، نگارش‌های فشرده شده فایل‌های js یا css هستند که با توجه به امکانات اضافه شده به ASP.NET MVC4، از آن‌ها استفاده نخواهیم کرد و برای افزودن و تعریف آن‌ها از امکانات Bundling and minification توکار فریم ورک ASP.NET MVC به نحوی که در ادامه توضیح داده خواهد شد، استفاده می‌کنیم.
فایل‌های png اضافه شده، آیکون‌های مخصوص Twitter Bootstrap هستند که اصطلاحا به آن‌ها sprite images نیز گفته می‌شود. در این نوع تصاویر، تعداد زیادی آیکون در کنار هم، برای بهینه سازی تعداد بار رفت و برگشت به سرور جهت دریافت تصاویر، طراحی شده و قرار گرفته‌اند.
فایل‌های js این مجموعه اختیاری بوده و برای استفاده از ویجت‌های Twitter Bootstrap مانند آکاردئون کاربرد دارند. این فایل‌ها برای اجرا، نیاز به jQuery خواهند داشت.


افزودن تعاریف اولیه Twitter Bootstrap به یک پروژه ASP.NET MVC

امکانات Bundling and minification در نوع پروژه‌های نسبتا جامع‌تر ASP.NET MVC به صورت پیش فرض لحاظ شده است. اما اگر یک پروژه خالی را شروع کرده‌اید، نیاز است بسته نیوگت آن‌را نیز نصب کنید:
 PM> Install-Package Microsoft.AspNet.Web.Optimization
پس از آن، کلاس کمکی BundleConfig ذیل را به پروژه جاری اضافه نمائید. به کمک آن قصد داریم امکانات Bundling and minification را فعال کرده و همچنین به آن بگوییم اسکریپت‌ها و فایل‌های CSS تعریف شده را به همان نحوی که معرفی می‌کنیم، پردازش کن (توسط کلاس سفارشی AsIsBundleOrderer).

using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Optimization;

namespace Mvc4TwitterBootStrapTest.Helper
{
    /// <summary>
    /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are 
    /// included in the order you register them.
    /// </summary>
    public class AsIsBundleOrderer : IBundleOrderer
    {
        public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files)
        {
            return files;
        }
    }

    public static class BundleConfig
    {
        private static void addBundle(string virtualPath, bool isCss, params string[] files)
        {
            BundleTable.EnableOptimizations = true;

            var existing = BundleTable.Bundles.GetBundleFor(virtualPath);
            if (existing != null)
                return;

            Bundle newBundle;
            if (HttpContext.Current.IsDebuggingEnabled)
            {
                newBundle = new Bundle(virtualPath);
            }
            else
            {
                newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify());
            }
            newBundle.Orderer = new AsIsBundleOrderer();

            foreach (var file in files)
                newBundle.Include(file);

            BundleTable.Bundles.Add(newBundle);
        }

        public static IHtmlString AddScripts(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Render(virtualPath);
        }

        public static IHtmlString AddStyles(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Render(virtualPath);
        }

        public static IHtmlString AddScriptUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, false, files);
            return Scripts.Url(virtualPath);
        }

        public static IHtmlString AddStyleUrl(string virtualPath, params string[] files)
        {
            addBundle(virtualPath, true, files);
            return Styles.Url(virtualPath);
        }
    }
}
نکته دیگری که در این کلاس سفارشی درنظر گرفته شده، عدم فعال سازی مباحث Bundling and minification در حالت Debug است. اگر در وب کانفیگ، تنظیمات پروژه را بر روی Release قرار دهید فعال خواهد شد (مناسب برای توزیع) و در حالت توسعه برنامه (حالت دیباگ)، این اسکریپت‌ها و فایل‌های CSS، قابلیت دیباگ و بررسی را نیز خواهند داشت.

پس از افزودن کلاس‌های کمکی فوق، به فایل layout پروژه مراجعه کرده و تعاریف ذیل را به ابتدای فایل اضافه نمائید:
@using Mvc4TwitterBootStrapTest.Helper
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    @BundleConfig.AddStyles("~/Content/css",
                            "~/Content/bootstrap.css",
                            "~/Content/bootstrap-responsive.css",
                            "~/Content/Site.css"
                            )
    @BundleConfig.AddScripts("~/Scripts/js",
                            "~/Scripts/jquery-1.9.1.min.js",
                            "~/Scripts/jquery.validate.min.js",
                            "~/Scripts/jquery.unobtrusive-ajax.min.js",
                            "~/Scripts/jquery.validate.unobtrusive.min.js",
                            "~/Scripts/bootstrap.min.js"
                            )
    @RenderSection("JavaScript", required: false)
</head>
و اگر برنامه را اجرا کنید، بجای حداقل 8 مدخل اشاره کننده به فایل‌های اسکریپت و CSS مورد نیاز یک برنامه ASP.NET MVC، فقط دو مدخل ذیل را مشاهده خواهید نمود:
<!DOCTYPE html>
<html>
<head>
    <title>Index</title>
    <link href="/Content/css?v=vsUQD0OJg4AJ-RZH8jSRRCu_rjl2U1nZrmSsaUyxoAc1" rel="stylesheet"/>
    <script src="/Scripts/js?v=GezdoTDiWY3acc3mI2Ujm_7nKKzh6Lu1Wr8TGyyLpW41"></script>
</head>
به این ترتیب تعداد رفت و برگشت‌های مرورگر به سرور، برای دریافت فایل‌های مورد نیاز جهت رندر صحیح یک صفحه، به شدت کاهش خواهد یافت. همچنین این فایل‌ها در حالت release فشرده نیز خواهند شد؛ به همراه تنظیم خودکار هدر کش شدن آن‌ها برای مدتی مشخص، جهت کاهش بار سرور و کاهش پهنای باند مصرفی آن.


مفاهیم پایه‌ای Twitter Bootstrap

الف) Semantic class names
به عبارتی کلاس‌های Twitter Bootstrap دارای نام‌هایی معنا دار و مفهومی می‌باشند؛ مانند کلاس‌های CSS‌ایی، به نام‌های Succes، Error، Info و امثال آن. این نام‌ها مفهومی را می‌رسانند؛ اما در مورد نحوه پیاده سازی آن‌ها جزئیاتی را بیان نمی‌کنند.
برای نمونه می‌توان کلاسی را به نام redText ایجاد کرد. هر چند این نام، توضیحاتی را در مورد علت وجودی‌اش بیان می‌کند، اما بسیار ویژه بوده و در مورد جزئیات پیاده سازی آن نیز اطلاعاتی را ارائه می‌دهد. در این حالت redText معنایی ندارد. چرا یک Text باید قرمز باشد؟ برای مثال این متن قرمز است چون مثلا شخصی، به آن رنگ ویژه علاقه دارد، یا اینکه قرمز است بخاطر نمایش خطایی در صفحه؟ به همین جهت در Twitter Bootstrap از نام‌های مفهومی یاده شده، مانند Error استفاده می‌شود. نام‌هایی معنا دار اما بدون دقیق شدن در مورد ریز جزئیات پیاده سازی آن‌ها. در این حالت می‌توان قالب جدیدی را تدارک دید و با ارائه تعاریف جدیدی برای کلاس Error و نحوه نمایش دلخواهی را به آن اعمال نمود.
یا برای نمونه نام rightside را برای نمایش ستونی در صفحه، درنظر بگیرید. این نام بسیار ویژه است؛ اما Sematic name آن می‌تواند sidebar باشد تا بدون دقیق شدن در جزئیات پیاده سازی آن، در چپ یا راست صفحه قابل اعمال باشد.
Semantic class names کلیدهایی هستند جهت استفاده مجدد از قابلیت‌های یک فریم ورک CSS.

ب) Compositional classes
اکثر کلاس‌های Twitter Bootstrap دارای محدوده کاری کوچکی هستند و به سادگی قابل ترکیب با یکدیگر جهت رسیدن به نمایی خاص می‌باشند. برای مثال به سادگی می‌توان به یک table سه ویژگی color، hover و width برگرفته شده از Twitter Bootstrap را انتساب داد و نهایتا به نتیجه دلخواه رسید؛ بدون اینکه نگران باشیم افزودن کلاس جدیدی در اینجا بر روی سایر کلاس‌های انتساب داده شده، تاثیر منفی دارد.

ج) Conventions
برای استفاده از اکثر قابلیت‌های این فریم ورک CSS یک سری قراردادهای پیش فرضی وجود دارند. برای مثال اگر از کلاس توکار pagination به همراه یک سری ul و li استفاده کنید، به صورت خودکار یک pager شکیل ظاهر خواهد شد. یا برای مثال اگر به یک html table کلاس‌های table table-striped table-hover را انتساب دهیم، به صورت خودکار قراردادهای پیش فرض table مجموعه Twitter Bootstrap به آن اعمال شده، به همراه رنگی ساختن یک درمیان زمینه ردیف‌ها و همچنین فعال سازی تغییر رنگ ردیف‌ها با حرکت ماوس از روی آن‌ها.


طرحبندی صفحات یک سایت به کمک Twitter Bootstrap

بررسی Grid layouts

Layout به معنای طرحبندی و چیدمان محتوا در یک صفحه است. یکی از متداولترین روش‌های طرحبندی صفحات چه در حالت چاپی و چه در صفحات وب، چیدمان مبتنی بر جداول و گریدها است. از این جهت که نحوه سیلان و نمایش محتوا از چپ به راست و یا راست به چپ را به سادگی میسر می‌سازند؛ به همراه اعمال حاشیه‌های مناسب جهت قسمت‌های متفاوت محتوای ارائه شده. Grid در طرحبندی، نمایش بصری نخواهد داشت اما در ساختار صفحه وجود داشته و مباحثی مانند جهت، موقعیت و یکپارچگی و یکدستی طراحی را سبب می‌شود.
به علاوه مرورگرها و مفهوم Grid نیز به خوبی با یکدیگر سازگار هستند. در دنیای HTML و ،CSS طراحی‌ها بر اساس مفهوم ساختار مستطیلی اشیاء صورت می‌گیرد:


برای نمونه در اینجا تصویر CSS Box Model را مشاهده می‌کنید. به این ترتیب، هر المان دارای محدوده‌ای مستطیلی با طول و عرض مشخص، به همراه ویژگی‌هایی مانند Margin، Border و Padding است.
در سال‌های اولیه طراحی وب، عموما کارهای طراحی صفحات به کمک HTML Tables انجام می‌شد. اما با پخته‌تر شدن CSS، استفاده از Tables برای طراحی صفحات کمتر و کمتر گشت تا اینکه نهایتا فریم ورک‌های CSS ایی پدید آمدند تا طراحی‌های مبتنی بر CSS را با ارائه گرید‌ها، ساده‌تر کنند. مانند Blue print، 960 GS و ... Twitter Bootstrap که طراحی مبتنی بر گرید‌های CSS ایی را به مجموعه قابلیت‌های دیگر خود افزوده است.


بررسی Fixed Grids

در اینجا در صفحه layout برنامه، یک Div دربرگیرنده دو Div دیگر را مشاهده می‌کنید:
<body>
    <div>
        <div>
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div>
            @RenderBody()
        </div>
    </div>
</body>
اگر در این حالت صفحه را در مرورگر بررسی کنیم، تمام قسمت‌های نمایش داده شده به همین نحو از بالا به پایین صفحه قرار خواهند گرفت. اما قصد داریم این محتوا را، دو ستونی نمایش دهیم. Div به همراه Title در یک طرف صفحه و Div دربرگیرنده محتوای صفحات، در قسمتی دیگر.
برای اینکار در Twitter Bootstrap از کلاسی به نام row استفاده می‌شود که بیانگر یک ردیف است. این کلاس را به خارجی‌ترین Div موجود اعمال خواهیم کرد. در یک صفحه، هر تعداد row ایی را که نیاز باشد، می‌توان تعریف کرد. داخل این ردیف‌ها، امکان تعریف ستون‌های مختلف و حتی تعریف ردیف‌های تو در تو نیز وجود دارد. هر ردیف Twitter Bootstrap از 12 ستون تشکیل می‌شود و برای تعریف آن‌ها از کلاس span استفاده می‌گردد. در این حالت جمع اعداد ذکر شده پس از span باید 12 را تشکیل دهند.
<body>
    <div class="row">
        <div class="span7">
            <h1>
                Title Title
            </h1>
            Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text
        </div>
        <div  class="span5">
            @RenderBody()
        </div>
    </div>
</body>
برای نمونه در اینجا کلاس row به خارجی‌ترین Div موجود اعمال شده است. دو Div داخلی هر کدام دارای کلاس‌های span ایی جهت تشکیل ستون‌های داخل این ردیف هستند. جمع اعداد بکارگرفته شده پس از آن‌ها، عدد 12 را تشکیل می‌دهد. به این ترتیب به یک طراحی دو ستونی خواهیم رسید.


در این تصویر، قسمت RenderBody کار رندر اکشن متد Index کنترلر Home برنامه را با Viewایی معادل کدهای ذیل، انجام داده است:
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div class="hero-unit">
    <h2>@ViewBag.Message</h2>
    <p>
        This is a template to demonstrate the way to beautify the default MVC template
        using Twitter Bootstrap website. It is pretty simple and easy.</p>
    <p>
        <a href="http://asp.net/mvc" class="btn btn-primary btn-large" style="color: White;">
            To learn more about ASP.NET MVC visit &raquo;</a></p>
</div>

درک نحوه عملکرد Grid در Twitter Bootstrap

در مثال ذیل 5 ردیف را مشاهده می‌کنید:
    <div class="row">
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
        <div class="span1">1</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span4">4</div>
        <div class="span5">5</div>
    </div>
    <div class="row">
        <div class="span5">5</div>
        <div class="span7">7</div>
    </div>
    <div class="row">
        <div class="span3">3</div>
        <div class="span7 offset2">7 offset 2</div>
    </div>
    <div class="row">
        <div class="span12">12</div>
    </div>
در هر ردیف، امکان استفاده از حداکثر 12 ستون وجود دارد که یا می‌توان مانند ردیف اول، 12 ستون را ایجاد کرد و یا مانند ردیف دوم، سه ستون با عرض‌های متفاوت (و یا هر ترکیب دیگری که به جمع 12 برسد).
در ستون چهارم، از کلاس offset نیز استفاده شده است. این مورد سبب می‌شود ستون جاری به تعدادی که مشخص شده است به سمت چپ (با توجه به استفاده از حالت RTL در اینجا) رانده شود و سپس ترسیم گردد.
یا اینکه می‌توان مانند ردیف آخر، یک ستون را به عرض 12 که در حقیقت 940 پیکسل است، ترسیم نمود.
برای اینکه بتوانیم این گرید تشکیل شده و همچنین ستون‌ها را بهتر مشاهده کنیم، به فایل style.css سایت، تنظیم زیر را اضافه کنید:
[class*="span"]
{
background-color: lightblue;
text-align: center;
margin-top: 15px;
}


نکته جالب این گرید، Responsive یا واکنشگرا بودن آن است. در این حالت، عرض مرورگر را کم و زیاد کنید. خواهید دید که ستون‌ها در صورتیکه در عرض نمایشی جاری، قابل ارائه نباشند، به ردیف‌های بعدی منتقل خواهند شد.
البته باید دقت داشت که این گرید هیچگاه یک ستون را نخواهد شکست. برای نمونه ردیف آخر، همواره با همان عرض ثابتش نمایش داده می‌شود و با کوچکتر کردن اندازه مرورگر، یک اسکرول افقی برای نمایش محتوای آن ظاهر خواهد شد.


یک نکته
اگر نمی‌خواهید که چنین رفتار واکنش‌گرایی بروز کند، نیاز است کلیه ردیف‌ها را در div ایی با کلاسی به نام container محصور کنید.
به این ترتیب ابتدا گرید نمایش داده شده، در میانه صفحه ظاهر خواهد شد (پیشتر از سمت راست شروع شده بود). همچنین دیگر با کوچک و بزرگ شدن اندازه مرورگر، ستون‌ها به شکل یک پشته بر روی هم قرار نخواهند گرفت. (اگر پس از این تنظیم، چنین قابلیتی را مشاهده نکردید و هنوز هم طراحی، واکنشگرا بود، تعریف bootstrap-responsive.css را نیاز است برای آزمایش، از هدر صفحه حذف کنید)


بررسی Fluid Grids

به گرید قسمت قبل از این جهت Fixed Grid گفته می‌شود که عرض هر span آن با یک عدد مشخص تعیین گشته است. اما در حالت Fluid Grid، عرض هر span برحسب درصد تعیین می‌شود. بکارگیری درصد در اینجا به معنای امکان تغییر عرض یک ستون بر اساس عرض جاری Container آن است. در اینجا span12 دارای عرض 100 درصد خواهد بود.
در مثال قبل، برای استفاده از Fluid grids، تنها کافی است هرجایی کلاسی مساوی row وجود دارد، به row-fluid تغییر کند. همچنین کلاس container را به container-fluid تغییر دهید.
برای آزمایش آن، اندازه و عرض نمایشی مرورگر خود را تغییر دهید. اینبار مشاهده خواهید کرد که برخلاف حالت Fixed Grid، عرض ستون‌ها به صورت خودکار کم و زیاد می‌شوند. این مورد بر روی محتوای قرار گرفته در این ستون‌ها نیز تاثیر گذار است. برای مثال اگر یک تصویر را در حالت Fluid grid در ستونی قرار دهید، با تغییر عرض مرورگر، اندازه این تصویر نیز تغییر خواهند کرد؛ اما در حالت Fixed Grid خیر.
حالت Fluid، شیوه متداول استفاده از bootstrap در اکثر سایت‌های مهمی است که تابحال از این فریم ورک CSS استفاده کرده‌اند.



مروری بر طراحی واکنشگرا یا Responsive

این روزها تعدادی از کاربران، با استفاده از ابزارهای موبایل و تبلت‌ها از وب سایت‌ها بازدید می‌کنند. هر کدام از این‌ها نیز دارای اندازه نمایشی متفاوتی می‌باشند. بنابراین نیاز خواهد بود تا حالت بهینه‌ای را جهت اینگونه وسایل نیز طراحی نمود. حالت بهینه در اینجا به معنای قابل خواندن بودن متون، امکان استفاده از لینک‌های ورود به صفحات مختلف و همچنین حذف اسکرول و مباحث زوم جهت مشاهده صفحات است.
یکی از ویژگی‌های CSS به نام media چنین قابلیتی را فراهم می‌سازد. برای نمونه قسمتی از فایل bootstrap-responsive.css دارای چنین تعاریفی است:
@media (min-width: 768px) and (max-width: 979px) {
    .hidden-desktop {
        display: inherit !important;
    }
توسط این تنظیمات، شیوه نامه تعیین شده، تنها به صفحاتی با اندازه‌هایی بین 768 تا 979 پیکسل اعمال می‌شود (تعدادی از تبلت‌ها برای نمونه).
Bootstrap برای مدیریت اندازه‌های مختلف وسایل موبایل، شیوه‌نامه‌های خاصی را تدارک دیده است که از اندازه px480 و یا کمتر، تا px1200 و یا بیشتر را پوشش می‌دهد.
به این ترتیب با اندازه px940 که پیشتر در مورد آن بحث شد، اندازه span12 به صورت خودکار به اندازه‌های متناسب با صفحات نمایشی کوچکتر تنظیم می‌گردد. همچنین برای اندازه‌های صفحات کوچکتر از 768px به صورت خودکار از Fluid columns استفاده می‌گردد.
تنها کاری که برای اعمال این قابلیت باید صورت گیرد، افزودن تعاریف فایل bootstrap-responsive.css به هدر صفحه است که در قسمت قبل انجام گردید. این فایل باید پس از فایل اصلی bootstrap.css اضافه شود.


کلاس‌های کمکی طراحی واکنشگرا

Bootstrap شامل تعدادی کلاس کمکی در فایل bootstrap-responsive.css خود می‌باشد شامل visible-phone، visible-tablet و visible-desktop به علاوه hidden-phone، hidden-tablet و hidden-desktop. به این ترتیب می‌توان محتوای خاصی را جهت وسایل ویژه‌ای نمایان یا مخفی ساخت.
برای مثال محتوای مشخص شده با کلاس hidden-desktop، در اندازه وسایل دسکتاپ نمایش داده نخواهد شد.
برای آزمایش آن، شش div را با کلاس‌های یاد شده و محتوایی دلخواه ایجاد کرده و سپس اندازه عرض مرورگر را تغییر دهید تا بهتر بتوان مخفی یا نمایان ساختن محتوا را بر اساس اندازه صفحه جاری درک کرد.
یکی از کاربردهای این قابلیت، قرار دادن تبلیغاتی با اندازه‌های تصاویری مشخص برای وسایل مختلف است؛ بجای اینکه منتظر شویم تا Fluid layout اندازه تصاویر را به صورت خودکار کوچک یا بزرگ کند، که الزاما بهترین کیفیت را حاصل نخواهد ساخت.
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="visible-phone">
                    visible-phone</div>
            </div>
            <div class="span4">
                <div class="visible-tablet">
                    visible-tablet</div>
            </div>
            <div class="span4">
                <div class="visible-desktop">
                    visible-desktop</div>
            </div>
        </div>
    </div>
    <div class="container-fluid">
        <div class="row-fluid">
            <div class="span4">
                <div class="hidden-phone">
                    hidden-phone</div>
            </div>
            <div class="span4">
                <div class="hidden-tablet">
                    hidden-tablet</div>
            </div>
            <div class="span4">
                <div class="hidden-desktop">
                    hidden-desktop</div>
            </div>
        </div>
    </div>