مطالب
مهاجرت از SQL Membership به ASP.NET Identity
در این مقاله مهاجرت یک اپلیکیشن وب که توسط SQL Membership ساخته شده است را به سیستم جدید ASP.NET Identity بررسی می‌کنیم. برای این مقاله از یک قالب اپلیکیشن وب (Web Forms) که توسط Visual Studio 2010 ساخته شده است برای ساختن کاربران و نقش‌ها استفاده می‌کنیم. سپس با استفاده از یک SQL Script دیتابیس موجود را به دیتابیسی که ASP.NET Identity نیاز دارد تبدیل می‌کنیم. در قدم بعدی پکیج‌های مورد نیاز را به پروژه اضافه می‌کنیم و صفحات جدیدی برای مدیریت حساب‌های کاربری خواهیم ساخت. بعنوان یک تست، کاربران قدیمی که توسط SQL Membership ساخته شده بودند باید قادر باشند به سایت وارد شوند. همچنین کاربران جدید باید بتوانند بدون هیچ مشکلی در سیستم ثبت نام کنند. سورس کد کامل این مقاله را می‌توانید از این لینک دریافت کنید.


یک اپلیکیشن با SQL Membership بسازید

برای شروع به اپلیکیشنی نیاز داریم که از SQL Membership استفاده می‌کند و دارای داده هایی از کاربران و نقش‌ها است. برای این مقاله، بگذارید پروژه جدیدی توسط VS 2010 بسازیم.

حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.

نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.

بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.

پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده می‌شوند، در جداول aspnet_Users و aspnet_Membership ذخیره می‌شوند. داده‌های مربوط به نقش‌ها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقش‌ها نیز در جدول aspnet_UsersInRoles ذخیره می‌شود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.

برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت می‌کند.

مهاجرت به Visual Studio 2013

  • برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
  • حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده می‌شود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده می‌کند. برای رفع این مساله می‌توانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
  • فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.

  • پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و داده‌ها قابل رویت هستند.
  • سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالا‌تر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.

پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.

نصب پکیج‌های NuGet

در پنجره Solution Explorer روی نام پروژه خود کلیک راست کرده، و گزینه Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده، عبارت "Microsoft.AspNet.Identity.EntityFramework" را وارد کنید. این پکیج را در لیست نتایج انتخاب کرده و آن را نصب کنید. نصب این بسته، نیازمندهای موجود را بصورت خودکار دانلود و نصب می‌کند: EntityFramework و ASP.NET Identity Core. حال پکیج‌های زیر را هم نصب کنید (اگر نمی‌خواهید OAuth را فعال کنید، 4 پکیج آخر را نادیده بگیرید).
  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Facebook
  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.MicrosoftAccount
  • Microsoft.Owin.Security.Twitter

مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity

قدم بعدی مهاجرت دیتابیس فعلی به الگویی است، که سیستم ASP.NET Identity به آن نیاز دارد. بدین منظور ما یک اسکریپت SQL را اجرا می‌کنیم تا جداول جدیدی بسازد و اطلاعات کاربران را به آنها انتقال دهد. فایل این اسکریپت را می‌توانید از لینک https://github.com/suhasj/SQLMembership-Identity-OWIN دریافت کنید.
این اسکریپت مختص این مقاله است. اگر الگوی استفاده شده برای جداول سیستم عضویت شما ویرایش/سفارشی-سازی شده باید این اسکریپت را هم بر اساس این تغییرات بروز رسانی کنید.
پنجره Server Explorer را باز کنید. گره اتصال ApplicationServices را باز کنید تا جداول را مشاهده کنید. روی گره Tables کلیک راست کرده و گزینه New Query را انتخاب کنید.

در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.

ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف می‌کنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، می‌توانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.

لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان می‌دهد.

  • aspnet_Roles --> AspNetRoles
  • aspnet_Users, aspnet_Membership --> AspNetUsers
  • aspnet_UsersInRoles --> AspNetUserRoles

جداول AspNetUserClaims و AspNetUserLogins خالی هستند. فیلد تفکیک کننده (Discriminator) در جدول AspNetUsers باید مطابق نام کلاس مدل باشد، که در مرحله بعدی تعریف خواهد شد. همچنین ستون PasswordHash به فرم 'encrypted password|password salt|password format' می‌باشد. این شما را قادر می‌سازد تا از رمزنگاری برای ذخیره و بازیابی کلمه‌های عبور استفاده کنید. این مورد نیز در ادامه مقاله بررسی شده است.



ساختن مدل‌ها و صفحات عضویت

بصورت پیش فرض سیستم ASP.NET Identity برای دریافت و ذخیره اطلاعات در دیتابیس عضویت از Entity Framework استفاده می‌کند. برای آنکه بتوانیم با جداول موجود کار کنیم، می‌بایست ابتدا مدل هایی که الگوی دیتابیس را نمایندگی می‌کنند ایجاد کنیم. برای این کار مدل‌های ما یا باید اینترفیس‌های موجود در Identity.Core را پیاده سازی کنند، یا می‌توانند پیاده سازی‌های پیش فرض را توسعه دهند. پیاده سازی‌های پیش فرض در Microsoft.AspNet.Identity.EntityFramework وجود دارند.
در نمونه ما، جداول AspNetRoles, AspNetUserClaims, AspNetLogins و AspNetUserRole ستون هایی دارند که شباهت زیادی به پیاده سازی‌های پیش فرض سیستم Identity دارند. در نتیجه می‌توانیم از کلاس‌های موجود، برای Map کردن الگوی جدید استفاده کنیم. جدول AspNetUsers ستون‌های جدیدی نیز دارد. می‌توانیم کلاس جدیدی بسازیم که از IdentityUser ارث بری کند و آن را گسترش دهیم تا این فیلدهای جدید را پوشش دهد.
پوشه ای با نام Models بسازید (در صورتی که وجود ندارد) و کلاسی با نام User به آن اضافه کنید.

کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمی‌بینید.

  public class User : IdentityUser
    {
        public User()
        {
            CreateDate = DateTime.Now;
            IsApproved = false;
            LastLoginDate = DateTime.Now;
            LastActivityDate = DateTime.Now;
            LastPasswordChangedDate = DateTime.Now;
            LastLockoutDate = DateTime.Parse("1/1/1754");
            FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754");
            FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754");
        }

        public System.Guid ApplicationId { get; set; }
        public string MobileAlias { get; set; }
        public bool IsAnonymous { get; set; }
        public System.DateTime LastActivityDate { get; set; }
        public string MobilePIN { get; set; }
        public string Email { get; set; }
        public string LoweredEmail { get; set; }
        public string LoweredUserName { get; set; }
        public string PasswordQuestion { get; set; }
        public string PasswordAnswer { get; set; }
        public bool IsApproved { get; set; }
        public bool IsLockedOut { get; set; }
        public System.DateTime CreateDate { get; set; }
        public System.DateTime LastLoginDate { get; set; }
        public System.DateTime LastPasswordChangedDate { get; set; }
        public System.DateTime LastLockoutDate { get; set; }
        public int FailedPasswordAttemptCount { get; set; }
        public System.DateTime FailedPasswordAttemptWindowStart { get; set; }
        public int FailedPasswordAnswerAttemptCount { get; set; }
        public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; }
        public string Comment { get; set; }
    }

حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را می‌پذیرد. TUser می‌تواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش می‌دهد.

در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده می‌کند.

public class ApplicationDbContext : IdentityDbContext<User>
{
        
}

مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام می‌شود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری می‌کند و آن را طوری توسعه می‌دهد که از کلاس User استفاده کند.

در پوشه Models کلاس جدیدی با نام UserManager بسازید.

public class UserManager : UserManager<User>
{
        
}

کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره می‌شوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد می‌شوند، کلمه عبورشان را توسط الگوریتم‌های قدیمی SQL Membership رمزگشایی می‌کنیم، اما کاربران جدید از الگوریتم‌های ASP.NET Identity استفاده خواهند کرد.

کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره می‌کند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنش‌های احراز هویت کاربران استفاده می‌شود تا کلمه‌های عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده می‌کنید.

public class SQLPasswordHasher : PasswordHasher
{
        public override string HashPassword(string password)
        {
            return base.HashPassword(password);
        }

        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            string[] passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                string passwordHash = passwordProperties[0];
                int passwordformat = 1;
                string salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }

        //This is copied from the existing SQL providers and is provided only for back-compat.
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) // MembershipPasswordFormat.Clear
                return pass;

            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;

            if (passwordFormat == 1)
            { // MembershipPasswordFormat.Hashed 
                HashAlgorithm hm = HashAlgorithm.Create("SHA1");
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length; )
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }

            return Convert.ToBase64String(bRet);
    }
}



دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.

متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری می‌کند. این الگوریتم از System.Web گرفته می‌شود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده می‌کرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنام‌های HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمه‌های عبور و تایید آنها در دیتابیس استفاده می‌کنند.

سیستم SQL Membership برای رمزنگاری (Hash) کلمه‌های عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده می‌کرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده می‌کنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمه‌های عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.

کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.

public UserManager()
            : base(new UserStore<User>(new ApplicationDbContext()))
        {
            this.PasswordHasher = new SQLPasswordHasher();
 }

ایجاد صفحات جدید مدیریت کاربران

قدم بعدی ایجاد صفحاتی است که به کاربران اجازه ثبت نام و ورود را می‌دهند. صفحات قدیمی SQL Membership از کنترل هایی استفاده می‌کنند که با ASP.NET Identity سازگار نیستند. برای ساختن این صفحات جدید به این مقاله مراجعه کنید. از آنجا که در این مقاله پروژه جدید را ساخته ایم و پکیج‌های لازم را هم نصب کرده ایم، می‌توانید مستقیما به قسمت Adding Web Forms for registering users to your application بروید.
چند تغییر که باید اعمال شوند:
  • فایل‌های Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده می‌کنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
  • همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
  • لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار می‌توانید پیش از ساختن حساب‌های کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
مثال: در فایل Register.aspx.cs متد جدیدی تعریف کنید که جدول aspnet_Applications را بررسی میکند و شناسه اپلیکیشن را بر اساس نام اپلیکیشن بدست می‌آورد.
private Guid GetApplicationID()
{
    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString))
            {
                string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database

                SqlCommand command = new SqlCommand(queryString, connection);
                command.Connection.Open();

                var reader = command.ExecuteReader();
                while (reader.Read())
                {
                    return reader.GetGuid(0);
                }

            return Guid.NewGuid();
        }
}


  حال می‌توانید این مقدار را برای آبجکت کاربر تنظیم کنید.
var currentApplicationId = GetApplicationID();

User user = new User() { UserName = Username.Text,
ApplicationId=currentApplicationId, …};
در این مرحله می‌توانید با استفاده از اطلاعات پیشین وارد سایت شوید، یا کاربران جدیدی ثبت کنید. همچنین اطمینان حاصل کنید که کاربران پیشین در نقش‌های مورد نظر وجود دارند.
مهاجرت به ASP.NET Identity مزایا و قابلیت‌های جدیدی را به شما ارائه می‌کند. مثلا کاربران می‌توانند با استفاده از تامین کنندگان ثالثی مثل Facebook, Google, Microsoft, Twitter و غیره به سایت وارد شوند. اگر به سورس کد این مقاله مراجعه کنید خواهید دید که امکانات OAuth نیز فعال شده اند.
در این مقاله انتقال داده‌های پروفایل کاربران بررسی نشد. اما با استفاده از نکات ذکر شده می‌توانید پروفایل کاربران را هم بسادگی منتقل کنید. کافی است مدل‌های لازم را در پروژه خود تعریف کرده و با استفاده از اسکریپت‌های SQL داده‌ها را انتقال دهید.
مطالب
مروری بر چند تجربه‌ی کاری با SQLite




SQLite بانک اطلاعاتی سریع، کم حجم و سورس بازی است که استفاده از آن در دات نت فریم ورک بسیار ساده است. فقط کافی است پروایدر مربوط به آن را دریافت کنید و در کدهای قدیمی خود هر جایی مثلا sqlconnection داشتید آن‌را تبدیل به sqliteconnection کنید و امثال آن (به بیان دیگر، پروایدر تهیه شده از معماری ADO.NET پیروی می‌کند و عملا دانش قبلی شما به سادگی قابل استفاده و ارتقاء است). علاوه بر آن پروایدر ADO.NET‌ تهیه شده برای آن، پشتیبانی از Entity framework را هم ارائه می‌دهد.
این دیتابیس تحت سیستم عامل‌های مختلف مهیا است و مهم‌ترین مزیت آن عدم نیاز به نصب آن می‌باشد.

مزایا:
- سورس باز و رایگان
- مهیا بودن آن در سایر پلتفرم‌ها (ویندوز، لینوکس و ...)
- نیازی به نصب ندارد و فقط یک DLL بومی است. این مورد برای کاربرانی که در مدیریت بانک‌های اطلاعاتی پیچیده مشکل دارند، یک مزیت مهم است.
- امکان تشیکل دیتابیس در حافظه. این نکته و توانایی، در آزمون‌های واحد بسیار جالب توجه است. می‌توانید با سرعت بالا دیتابیسی واقعی را در حافظه تشکیل داده، کلیه آزمون‌های واحد خود را اجرا کرده و پس از پایان کار، اثری از دیتابیس و تغییر داده‌ها و مشکلات بازگردانی اطلاعات به حالت اول وجود نخواهد داشت.


ملاحظات:
الف) مرتب سازی SQLite حساس به حروف کوچک و بزرگ است.
برای برگشت به عادت متداولی که وجود دارد می‌شود به صورت زیر عمل کرد:
select f1 from tbl1 order by f1 COLLATE NOCASE
یک COLLATE NOCASE اضافه شده است.

ب) رعایت نکات مرتبط با سیستم‌های 64 بیتی
در مورد سیستم‌های 64 بیتی و دات نت قبلا مطلبی را نوشته بودم : {+}. این مطلب دقیقا اینجا کاربرد پیدا می‌کند، از این لحاظ که SQLite یک بانک اطلاعاتی Native است. اگر برنامه‌ی دات نت شما برای حالت Any CPU تهیه شده است، در سیستم‌های 32 بیتی نیاز است تا DLL مرتبط SQLite را توزیع کنید و در سیستم‌های 64 بیتی DLL مرتبط 64 بیتی آن نیاز خواهد بود. در غیراینصورت برنامه‌ی شما در بدو امر کرش کرده و اجرا نخواهد شد.

مشکلات:
الف) کلید خارجی بی خاصیت!
SQLite از کلید خارجی پشتیبانی می‌کند اما آن‌را اعمال نمی‌کند! برای اینکه کلید خارجی را اعمال کنید باید خودتان تریگر بنویسید تا این‌کار را انجام دهد.

ب) پشتیبانی در حد صفر از مباحث همزمانی و تردینگ.
اگر برنامه شما مالتی ترد است، در بد مخمصه‌ای گرفتار شده‌اید. مدام با پیغام database is locked مواجه خواهید شد. (چه انتظاری داشتید؟ یک dll کمتر از 2 مگابایت که قرار نیست کار غول‌های دیتابیسی را انجام دهد)
بنابراین اصلا تصورش را هم نکنید که از این دیتابیس به عنوان بانک اطلاعاتی یک سایت (و محیط‌های چند کاربره) بتوان استفاده کرد و کاربران دچار مشکل نشوند.

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

د) امنیت
روی بانک‌های اطلاعاتی اکسس حداقل می‌توان یک کلمه‌ی عبور را قرار داد (که در کسری از ثانیه قابل شکستن است!). در SQLite استاندارد هیچ خبری از این مباحث نبوده و امنیت را باید خودتان تامین کنید. (البته یک نسخه‌ی تجاری هم از این بانک اطلاعاتی با پشتیبانی از رمزنگاری اطلاعات موجود است : +)

ه) مرتب سازی فارسی
هر چند SQLite هیچ مشکلی در ثبت اطلاعات یونیکد و خصوصا متون فارسی ندارد، اما با مرتب سازی کلمات یونیکد مشکل داشته و بر اساس کد اسکی آن‌ها عمل می‌کند. هر چند امکان تعریف Collation سفارشی در آن ممکن است : + (البته ممکن بودن با موجود بودن متفاوت است)


نتیجه گیری:
- SQLite برای دیتابیسی تا حدود یک گیگ که فقط یک نفر قرار باشد از آن استفاده کند انتخاب بسیار مناسبی است (برای مثال فایرفاکس از آن برای ذخیره سازی تنظیمات خودش استفاده می‌کند).

مطالب
آشنایی با الگوی MVP

پروژه‌های زیادی را می‌توان یافت که اگر سورس کدهای آن‌ها را بررسی کنیم، یک اسپاگتی کد تمام عیار را در آن‌ها می‌توان مشاهده نمود. منطق برنامه، قسمت دسترسی به داده‌ها، کار با رابط کاربر، غیره و غیره همگی درون کدهای یک یا چند فرم خلاصه شده‌اند و آنچنان به هم گره خورده‌اند که هر گونه تغییر یا اعمال درخواست‌های جدید کاربران، سبب از کار افتادن قسمت دیگری از برنامه می‌شود.
همچنین از کدهای حاصل در یک پروژه، در پروژه‌‌های دیگر نیز نمی‌توان استفاده کرد (به دلیل همین در هم تنیده بودن قسمت‌های مختلف). حداقل نتیجه یک پروژه برای برنامه نویس، باید یک یا چند کلاس باشد که بتوان از آن به عنوان ابزار تسریع انجام پروژه‌های دیگر استفاده کرد. اما در یک اسپاگتی کد، باید مدتی طولانی را صرف کرد تا بتوان یک متد را از لابلای قسمت‌های مرتبط و گره خورده با رابط کاربر استخراج و در پروژه‌ای دیگر استفاده نمود. برای نمونه آیا می‌توان این کدها را از یک برنامه ویندوزی استخراج کرد و آن‌ها را در یک برنامه تحت وب استفاده نمود؟

یکی از الگوهایی که شیوه‌ی صحیح این جدا سازی را ترویج می‌کند، الگوی MVP یا Model-View-Presenter می‌باشد. خلاصه‌ی این الگو به صورت زیر است:


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

View :
من می‌دانم که چگونه باید اطلاعاتی را به کاربر به شکلی بصری ارائه داد.
من می‌دانم که چگونه باید اعمالی مانند data binding و امثال آن را انجام داد.
من نمی‌دانم که چگونه باید منطق پردازشی موارد ذکر شده را فراهم آورم.

Presenter :
من می‌دانم که چگونه باید درخواست‌های رسیده کاربر به View را دریافت کرده و آن‌ها را به Model‌ انتقال دهم.
من می‌دانم که چگونه باید اطلاعات را به Model ارسال کرده و سپس نتیجه‌ی پردازش آن‌ها را جهت نمایش در اختیار View قرار دهم.
من نمی‌دانم که چگونه باید اطلاعاتی را ترسیم کرد (مشکل View است نه من) و نمی‌دانم که چگونه باید پردازشی را بر روی اطلاعات انجام دهم. (مشکل Model است و اصلا ربطی به اینجانب ندارد!)


یک مثال ساده از پیاده سازی این روش
برنامه‌ای وبی را بنویسید که پس از دریافت شعاع یک دایره از کاربر، مساحت ‌آن‌را محاسبه کرده و نمایش دهد.
یک تکست باکس در صفحه قرار خواهیم داد (txtRadius) و یک دکمه جهت دریافت درخواست کاربر برای نمایش نتیجه حاصل در یک برچسب به نام lblResult

الف) پیاده سازی به روش متداول (اسپاگتی کد)

protected void btnGetData_Click(object sender, EventArgs e)
{
lblResult.Text = (Math.PI * double.Parse(txtRadius.Text) * double.Parse(txtRadius.Text)).ToString();
}
بله! کار می‌کنه!
اما این مشکلات را هم دارد:
- منطق برنامه (روش محاسبه مساحت دایره) با رابط کاربر گره خورده.
- کدهای برنامه در پروژه‌ی دیگری قابل استفاده نیست. (شما متد یا کلاسی را این‌جا با قابلیت استفاده مجدد می‌توانید پیدا می‌کنید؟ آیا یکی از اهداف برنامه نویسی شیءگرا تولید کدهایی با قابلیت استفاده مجدد نبود؟)
- چگونه باید برای آن آزمون واحد نوشت؟

ب) بهبود کد و جدا سازی لایه‌ها از یکدیگر

در روش MVP متداول است که به ازای هر یک از اجزاء ابتدا یک interface نوشته شود و سپس این اینترفیس‌ها پیاده سازی گردد.

پیاده سازی منطق برنامه:

1- ایجاد Model :
یک فایل جدید را به نام CModel.cs به پروژه اضافه کرده و کد زیر را به آن خواهیم افزود:

using System;

namespace MVPTest
{
public interface ICircleModel
{
double GetArea(double radius);
}

public class CModel : ICircleModel
{
public double GetArea(double radius)
{
return Math.PI * radius * radius;
}
}
}
همانطور که ملاحظه می‌کنید اکنون منطق برنامه از موارد زیر اطلاعی ندارد:
- خبری از textbox و برچسب و غیره نیست. اصلا نمی‌داند که رابط کاربری وجود دارد یا نه.
- خبری از رخ‌دادهای برنامه و پاسخ دادن به آن‌ها نیست.
- از این کد می‌توان مستقیما و بدون هیچ تغییری در برنامه‌های دیگر هم استفاده کرد.
- اگر باگی در این قسمت وجود دارد، تنها این کلاس است که باید تغییر کند و بلافاصله کل برنامه از این بهبود حاصل شده می‌تواند بدون هیچگونه تغییری و یا به هم ریختگی استفاده کند.
- نوشتن آزمون واحد برای این کلاس که هیچگونه وابستگی به UI ندارد ساده است.


2- ایجاد View :
فایل دیگری را به نام CView.cs را به همراه اینترفیس زیر به پروژه اضافه می‌کنیم:

namespace MVPTest
{
public interface IView
{
string RadiusText { get; set; }
string ResultText { get; set; }
}
}

کار View دریافت ابتدایی مقادیر از کاربر توسط RadiusText و نمایش نهایی نتیجه توسط ResultText است البته با یک اما.
View نمی‌داند که چگونه باید این پردازش صورت گیرد. حتی نمی‌داند که چگونه باید این مقادیر را به Model جهت پردازش برساند یا چگونه آن‌ها را دریافت کند (به همین جهت از اینترفیس برای تعریف آن استفاده شده).

3- ایجاد Presenter :
در ادامه فایل جدیدی را به نام CPresenter.cs‌ با محتویات زیر به پروژه خواهیم افزود:

namespace MVPTest
{
public class CPresenter
{
IView _view;

public CPresenter(IView view)
{
_view = view;
}

public void CalculateCircleArea()
{
CModel model = new CModel();
_view.ResultText = model.GetArea(double.Parse(_view.RadiusText)).ToString();
}
}
}

کار این کلاس برقراری ارتباط با Model است.
می‌داند که چگونه اطلاعات را به Model ارسال کند (از طریق _view.RadiusText) و می‌داند که چگونه نتیجه‌ی پردازش را در اختیار View قرار دهد. (با انتساب آن به _view.ResultText)
نمی‌داند که چگونه باید این پردازش صورت گیرد (کار مدل است نه او). نمی‌داند که نتیجه‌ی نهایی را چگونه نمایش دهد (کار View است نه او).
روش معرفی View به این کلاس به constructor dependency injection معروف است.

اکنون کد وب فرم ما که در قسمت (الف) معرفی شده به صورت زیر تغییر می‌کند:

using System;

namespace MVPTest
{
public partial class _Default : System.Web.UI.Page, IView
{
protected void Page_Load(object sender, EventArgs e)
{
}

public string RadiusText
{
get { return txtRadius.Text; }
set { txtRadius.Text = value; }
}
public string ResultText
{
get { return lblResult.Text; }
set { lblResult.Text = value; }
}

protected void btnGetData_Click(object sender, EventArgs e)
{
CPresenter presenter = new CPresenter(this);
presenter.CalculateCircleArea();
}
}
}

در این‌جا یک وهله از Presenter برای برقراری ارتباط با Model ایجاد می‌شود. همچنین کلاس وب فرم ما اینترفیس View را نیز پیاده سازی خواهد کرد.

مطالب
بررسی تغییرات Blazor 8x - قسمت پنجم - امکان تعریف جزیره‌های تعاملی Blazor Server
در Blazor 8x می‌توان صفحات SSR ای را به همراه Blazor server islands و یا Blazor WASM islands داشت؛ یعنی یک کامپوننت Blazor Server که داخل یک صفحه‌ی معمولی SSR قرار گرفته و با سرور، ارتباط SiganlR برقرار می‌کند و یا یک کامپوننت Blazor WASM که در قسمتی از صفحه‌ی SSR درج شده و درون مرورگر کاربر اجرا می‌شود. به هر کدام از این‌ها یک «جزیره‌ی تعاملی» گفته می‌شود (interactive island). در این قسمت، نکات مرتبط با جزایر تعاملی Blazor Server را بررسی می‌کنیم.


بررسی یک مثال: تهیه یک برنامه‌ی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آن‌ها

به لطف وجود SSR در Blazor 8x، می‌توان HTML نهایی کامپوننت‌ها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه می‌شود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحه‌ای که یک لیست محصولات فرضی را نمایش می‌دهد : بر اساس SSR
- صفحه‌ای که جزئیات یک محصول را نمایش می‌دهد: بر اساس SSR
- دکمه‌ای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands

یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمت‌های برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمت‌های قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. می‌خواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیره‌ی تعاملی Blazor server واقع شده‌ی در قسمتی از یک صفحه‌ی استاتیک SSR، مدیریت کنیم.


مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

namespace BlazorDemoApp.Models;

public record Product
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Description { get; set; }
    public decimal Price { get; set; }

    public List<int> Related { get; set; } = new();
}
در اینجا، هدف تعریف لیستی از محصولات فرضی است؛ به همراه خاصیتی که Id محصولات مشابه را نگهداری می‌کند (خاصیت Related).


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده می‌کنید:
using BlazorDemoApp.Models;

namespace BlazorDemoApp.Services;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    IList<Product> GetRelatedProducts(int productId);
}

public class ProductStore : IProductStore
{
    private static readonly List<Product> ProductsDataSource =
        new()
        {
            new Product
            {
                Id = 1, Title = "Smart speaker", Price = 22m,
                Description =
                    "This smart speaker delivers excellent sound quality and comes with built-in voice control, offering an impressive music listening experience.",
                Related = new List<int> { 2, 3 },
            },
            new Product
            {
                Id = 2, Title = "Regular speaker", Price = 89m,
                Description =
                    "Enjoy room-filling sound with this regular speaker. With its slick design, it perfectly fits into any room in your house.",
                Related = new List<int> { 1, 3 },
            },
            new Product
            {
                Id = 3, Title = "Speaker cable", Price = 12m,
                Description =
                    "This high-quality speaker cable ensures a reliable and clear audio connection for your sound system.",
            },
        };

    public IList<Product> GetAllProducts() => ProductsDataSource;

    public Product GetProduct(int id) => ProductsDataSource.Single(p => p.Id == id);

    public IList<Product> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return ProductsDataSource.Where(p => product.Related.Contains(p.Id)).ToList();
    }
}
هدف از این سرویس، ارائه‌ی لیست تمام محصولات، دریافت اطلاعات یک محصول و همچنین یافتن لیست محصولات مشابه یک محصول خاص است.
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایل‌های razor برنامه‌ی جاری قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل صفحه‌ی نمایش لیست محصولات

قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:


کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار می‌گیرند، در ادامه مشاهده می‌کنید:

@page "/Products"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

<h3>Products</h3>

@if (_products == null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var item in _products)
    {
        <a href="/ProductDetails/@item.Id">
            <div>
                <div>
                    <h5>@item.Title</h5>
                </div>
                <div>
                    <h5>@item.Price.ToString("c")</h5>
                </div>
            </div>
        </a>
    }
}

@code {
    private IList<Product>? _products;

    protected override Task OnInitializedAsync() => GetProductsAsync();

    private async Task GetProductsAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _products = Store.GetAllProducts();
    }

}
توضیحات:
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شده‌است.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفته‌است.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شده‌است.
- چون این صفحه، یک صفحه‌ی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینک‌های نمایش داده شده‌ی در اینجا، به صفحه‌ی ProductDetails اشاره می‌کنند که در آن، جزئیات محصول انتخابی نمایش داده می‌شوند.


تکمیل صفحه‌ی نمایش جزئیات یک محصول


در صفحه‌ی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت می‌گیرد:

@page "/ProductDetails/{ProductId}"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

@if (_product == null)
{
    <p>Loading...</p>
}
else
{
    <div>
        <div>
            <h5>
                @_product.Title (@_product.Price.ToString("C"))
            </h5>
            <p>
                @_product.Description
            </p>
        </div>
        @if (_product.Related.Count > 0)
        {
            <div>
                <RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
            </div>
        }
    </div>
    <NavLink href="/Products">Back</NavLink>
}

@code {
    private Product? _product;

    [Parameter]
    public string? ProductId { get; set; }

    protected override Task OnInitializedAsync() => GetProductAsync();

    private async Task GetProductAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _product = Store.GetProduct(Convert.ToInt32(ProductId));
    }

}
توضیحات:
- باتوجه به نحوه‌ی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت می‌شود.
- سپس این ProductId را در روال رخ‌دادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار می‌گیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شده‌است.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کرده‌ایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمه‌ای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت می‌کند:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
بنابراین در ادامه کامپوننت RelatedProducts فوق را تکمیل می‌کنیم.


تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت می‌گیرد:
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services
@inject IProductStore Store

<button @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div>
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div>
                        <h5>@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate InteractiveServer mode
        _relatedProducts = Store.GetRelatedProducts(ProductId);
    }

}

تعاملی کردن کامپوننت نمایش لیست محصولات مشابه

مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی related products کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننت‌ها و صفحات فقط یکبار رندر می‌شوند و نه بیشتر. بله می‌توان بر روی آن‌ها ده‌ها دکمه، نوارهای لغزان، دراپ‌داون و غیره را قرار داد، اما ... نمی‌توان هیچگونه تعاملی را با آن‌ها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچ‌جائی اجرا نمی‌شود. در این حالت است که می‌توان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوه‌ی رندر کامپوننت RelatedProducts را به صورت یک جزیره‌ی تعاملی Blazor server درآورد تا رویداد منتسب به دکمه‌ی related products موجود در آن، پردازش شود. بنابراین به صفحه‌ی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر می‌دهیم:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@InteractiveServer"/>
اکنون اگر برنامه را مجددا اجرا کرده و بر روی دکمه‌ی نمایش محصولات مشابه قرار گرفته در ذیل جزئیات یک محصول کلیک کنیم، بدون مشکل کار می‌کند:


نحوه‌ی پردازش پشت صحنه‌ی این نوع صفحات هم جالب است. برای اینکار به برگه‌ی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحه‌ی نمایش جزئیات محصول را طی می‌کنیم:


- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شده‌ی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت می‌شوند:
<script src="_framework/blazor.web.js"></script>

به علاوه در این حالت ای‌جکسی fetch، کار دریافت مجدد فایل‌های استاتیک مرتبط یک صفحه، مانند فایل‌های js.، css.، تصاویر و غیره، مجددا انجام نمی‌شود که این مورد خود مزیتی است نسبت به حالت متداول برنامه‌های ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام می‌شود.
به این قابلیت، enhanced navigation هم گفته می‌شود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال می‌کنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی می‌شود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایده‌ای، یک زمانی در برنامه‌های ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).

- همچنین به محض نمایش صفحه‌ی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وب‌سوکت هم برقرار شده که مرتبط با جزیره‌ی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.

- یک disconnect را هم در اینجا مشاهده می‌کنید. اگر به یک صفحه‌ی تعاملی مراجعه کنیم، همانطور که مشخص است، یک اتصال SignalR برقرار می‌شود (که به آن در اینجا circuit هم می‌گویند). اما اگر از این صفحه به سمت یک صفحه‌ی SSR حرکت کنیم، پس از نمایش آن صفحه، اتصال SignalR قبلی که دیگر نیازی به آن نیست، بسته خواهد شد تا منابع سمت سرور، رها شوند.


در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال می‌شود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریم‌ورک، مدیریت می‌شوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کرده‌ایم که سبب برقراری اتصال یک وب‌سوکت شده‌است. سپس با کلیک بر روی دکمه‌ی back، به صفحه‌ی SSR مشاهده‌ی لیست محصولات برگشته‌ایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شده‌است.


نحوه‌ی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x

به صورت پیش‌فرض زمانیکه از حالت رندر InteractiveServer استفاده می‌کنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمت‌های ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال می‌شوند. در این حالت کاربر، تجربه‌ی کاربری روان‌تری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمت‌هایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@(new InteractiveServerRenderMode(false))"/>
در این حالت اگر برنامه را اجرا کنید، در حین نمایش صفحه‌ی اصلی در برگیرنده‌ی از نوع SSR، فقط جای این کامپوننت در صفحه مشخص می‌شود و پس از برقراری اتصال با سرور از طریق اتصال SignalR، شاهد UI کامپوننت RelatedProducts خواهیم بود، که نسبت به قبل، وقفه‌ای را سبب خواهد شد.

نحوه‌ی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده می‌کنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شده‌است:
@using static Microsoft.AspNetCore.Components.Web.RenderMode

public static class RenderMode
  {
    public static InteractiveServerRenderMode InteractiveServer { get; } = new InteractiveServerRenderMode();

    public static InteractiveWebAssemblyRenderMode InteractiveWebAssembly { get; } = new InteractiveWebAssemblyRenderMode();

    public static InteractiveAutoRenderMode InteractiveAuto { get; } = new InteractiveAutoRenderMode();
  }


public class InteractiveServerRenderMode : IComponentRenderMode
  {
    public InteractiveServerRenderMode()
      : this(true)
    {
    }

    public InteractiveServerRenderMode(bool prerender) => this.Prerender = prerender;

    public bool Prerender { get; }
  }


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip  
مطالب
آشنایی با قابلیت FileStream اس کیوال سرور 2008 - قسمت اول

مطلبی چندی قبل در مورد "ذخیره سازی فایل‌ها در دیتابیس یا استفاده از فایل سیستم متداول؟" منتشر گردید، جهت برشمردن فواید ذخیره سازی فایل‌ها در دیتابیس (+). اما معایب این نوع ذخیره سازی بررسی نشدند:

الف) اختصاص یافتن قسمتی از بافر SQL Server به این امر.
ب) با توجه به قرار گرفتن داده‌های BLOB‌ در دیتابیس ، transaction log قابل توجهی تولید خواهد شد. (+)
ج) بیش از 2GB را نمی‌توان در فیلدهایی از نوع varbinary(max) ذخیره کرد.
د) به روز رسانی BLOB ها سبب ایجاد fragmentation می‌شود.

مایکروسافت برای رفع این مشکلات در SQL Server 2008 قابلیت جدیدی را ارائه داده است به نام FileStream که در طی مقالاتی به بررسی آن خواهیم پرداخت.

FILESTREAM موتور دیتابیس اس کیوال سرور را با سیستم فایل NTFS یکپارچه می‌کند؛ به این صورت که داده‌های BLOB از نوع varbinary(max) را به صورت فایل بر روی سیستم ذخیره خواهد کرد. سپس با استفاده از دستورات T-SQL می‌توان این فایل‌ها را ثبت، حذف، به روز رسانی، جستجو و بک آپ گیری کرد. این قابلیت نیز از فیلدهای varbinary(max) استفاده می‌کند؛ اما اکنون ویژگی و برچسب FILESTREAM به این نوع فیلدها الصاق خواهد شد. FILESTREAM data باید در FILESTREAM filegroups ذخیره شوند. FILESTREAM filegroups در حقیقت همان پوشه‌های فایل سیستم می‌باشند. به آن‌ها data containers نیز گفته می‌شوند که مرزی هستند بین ذخیره سازی داده‌ها در فایل سیستم و در دیتابیس.

مزایای سیستم FileStream چیست؟
الف) سیستم transaction مختص به خود را داشته، به همین جهت سبب رشد غیر منطقی حجم فایل transaction log دیتابیس اصلی نمی‌شوند.
ب) هنگام به روز رسانی فیلدهایی از این دست، صرفا ایجاد یا حذف یک فایل مد نظر است؛ بنابراین fragmentation ایجاد شده در این حالت بسیار کمتر از روش استفاده از فیلدهایی از نوع varbinary(max) می‌باشد.
ج) استفاده از NT system cache جهت کش کردن اطلاعات که سبب بالا بردن بازدهی بانک اطلاعاتی خواهد شد.
د) از buffer pool اس کیوال سرور در این حالت استفاده نشده (مطابق قسمت ج) و این حافظه جهت امور روزمره‌ی اس کیوال سرور کاملا مهیا خواهد بود.
ه) محدودیت 2GB فیلدهایی از نوع varbinary(max) با توجه به ذخیره سازی این نوع BLOBs در فایل سیستم، دیگر وجود نخواهد داشت.

چه زمانی بهتر است از FileStream استفاده شود؟
الف) فایل‌هایی که ذخیره می‌شوند به طور متوسط بیش از یک مگابایت حجم داشته باشند. (برای کمتر از این مقدار varbinary(max) BLOBs کارآیی بهتری را ارائه می‌دهند). هر چند این مرز یک مگابایت مطابق اطلاعات books online است اما تجربیات کاری نشان می‌دهند که این سقف را باید 256 کیلوبایت درنظر گرفت.
ب) قابلیت خواندن سریع اطلاعات فایل‌ها مد نظر باشد (بررسی کارآیی مطابق تصویر زیر از MSDN). سیستم NTFS نسبت به SQL Server‌ در خواندن فایل‌های حجیم سریعتر عمل می‌کند.
ج) اگر از یک معماری middle tier در برنامه‌های خود در حال استفاده‌اید.
د) زمانیکه نیاز باشد تا اطلاعات relational و non-relational در یک تراکنش مورد استفاده قرار گیرند.



نکاتی را که باید هنگام ذخیره سازی اطلاعات در FileStream در نظر داشت
الف) هنگامی که یک جدول حاوی فیلدی از نوع FileStream می‌باشد، باید دارای فیلد ID منحصربفرد نیز باشد.
ب) data containers ایی که پیش از این در مورد آن‌ها صحبت شد، نباید تو در تو باشند.
ج) FILESTREAM filegroups بر روی درایوهای فشرده شده نیز می‌توانند قرار داشته باشند.

FileStream از دیدگاه امنیت
امنیت داده‌های FileStream در اس کیوال سرور دقیقا همانند امنیت سایر اطلاعات ذخیره شده در دیتابیس است (دسترسی در حد جدول و یا فیلد). اگر کاربری دسترسی به فیلد FileStream در یک جدول داشته باشد، می‌تواند آن‌ فایل را گشوده و استفاده کند. رمزنگاری بر روی این ستون‌ها پشتیبانی نمی‌شود. تنها اکانتی که اس کیوال سرور تحت آن در حال اجرا است دسترسی به FILESTREAM container دارد. همچنین توصیه شده است که به هیچ اکانت دیگری این دسترسی داده نشود. زمانیکه یک دیتابیس آغاز و مشغول به کار می‌شود، اس کیوال سرور دسترسی به FILESTREAM data container را محدود خواهد کرد و دسترسی به این اطلاعات تنها از طریق دستورات T-SQL و یا OpenSqlFilestream API میسر خواهد بود. بدیهی است زمانیکه اس کیوال سرور متوقف شود، این اطلاعات بدون هیچگونه محدودیتی قابل دسترسی بوده و تنها محدودیت‌های سیستمی به آن‌ها اعمال خواهند شد (که این مورد باید مد نظر باشد).

نگهداری FileStream
FileStream به صورت فیلدهای varbinary(max) یکپارچه با دیتابیس ذخیره می‌شود؛ بنابراین نحوه‌ی تهیه پشتیبان از آن‌ها همانند روش‌های متداول است بدون هیچگونه تغییری (و این اطلاعات در بک آپ دیتابیس لحاظ می‌شوند). اگر نیاز بود هنگام تهیه پشتیبان از این نوع داده‌ها بک آپ گرفته نشود، می‌توان از partial backup با پارامترهای مربوطه استفاده کرد.


ادامه دارد ...

مطالب
اجرای برنامه‌های ASP.NET به کمک وب سرور Apache توسط Mono در Ubuntu
در قسمت قبل نحوه‌ی اجرای برنامه‌های ASP.NET را به کمک وب سرور آزمایشی Mono بررسی کردیم. بدیهی است این وب سرور برای هاست کردن سایت‌ها و خدمات دهی عمومی، مناسب نیست و صرفا جهت کارهای برنامه نویسی طراحی شده است. در ادامه، ابتدا وب سرور معروف آپاچی را نصب کرده و سپس سعی خواهیم کرد تا همان دو برنامه ASP.NET Web forms و ASP.NET MVC قسمت قبل را بر روی آن اجرا کنیم (دو برنامه خالی ASP.NET کامپایل شده که از ویندوز به لینوکس کپی شده بودند).


نصب وب سرور آپاچی
به اینترنت متصل شده، ترمینال Ubuntu را گشوده (با میانبر ctrl+alt+t) و سپس فرمان‌های ذیل را صادر کنید:
 sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install apache2
به این ترتیب Apache web server بر روی سیستم نصب خواهد شد.


نصب ماژول‌های ASP.NET مخصوص آپاچی
سپس نیاز است ASP.NET runtime for Apache را نصب کنیم:
 sudo apt-get install libapache2-mod-mono mono-apache-server4
mod_mono درون پروسه آپاچی اجرا شده و تمام درخواست‌های رسیده به یک وب سایت ASP.NET را به mod-mono-server که کار هاست سایت را انجام می‌دهد، انتقال خواهد داد.
اگر علاقمند به مشاهده تنظیمات آن بودید باید به مسیر etc/apache2/mods-enabled مراجعه کرده و فایل mod_mono.conf را بررسی کنید (اختیاری). برای مثال، در آن حالت اجرا، بر روی ASP.NET 4 تنظیم شده‌است.

تنظیمات آپاچی برای کار با ASP.NET
نیاز است فایل تنظیمات پیش فرض وب سرور آپاچی را جهت معرفی ASP.NET به آن، اندکی ویرایش کنیم:
 sudo gedit /etc/apache2/sites-available/default
در ادیتور باز شده، ذیل قسمت ServerAdmin، موارد زیر را اضافه کنید:
 MonoAutoApplication disabled
AddHandler mono .aspx .ascx .asax .ashx .config .cs .asmx .axd
MonoApplications "/:/var/www"
MonoServerPath default "/usr/bin/mod-mono-server4"
در ادامه به المان Directory /var/www سطرهای زیر را اضافه کنید:
 SetHandler mono
DirectoryIndex index.aspx index.html default.aspx Default.aspx
المان جدید ذیل را در انتهای فایل اضافه کنید تا یک سری از پوشه‌های خاص ASP.NET همانند محیط‌های ویندوزی در آپاچی نیز محافظت شوند:
 <DirectoryMatch "/([bB]in|[Aa]pp_[Cc]ode|[Aa]pp_[Dd]ata|[Aa]pp_[Gg]lobal[Rr]esources|[Aa]pp_[Ll]ocal[Rr]esources)/">
Order deny,allow
Deny from all
</DirectoryMatch>
فایل را ذخیره کرده و ادیتور را ببندید. اکنون وب سرور آپاچی را اجرا و ری استارت کنید:
 sudo service apache2 restart
همچنین سطح دسترسی مسیر var/www که حاوی برنامه‌های ما خواهد بود نیز باید تغییر کند:
 # r,w,exec for user + group
sudo chgrp -R www-data /var/www
sudo chown -R www-data /var/www
sudo chown -R www-data:www-data /var/www
sudo chmod -R 755 /var/www
اکنون اگر آدرس http://127.0.0.1 را در مرورگر وارد کنید، باید پیام «It Works» را مشاهده کنید و اگر خواستید محتوای آن‌را ویرایش کنید، دستور ذیل را صادر نمائید:
 gedit /var/www/index.html

اجرای مثال‌ها

با تنظیمات فوق، برنامه‌های کپی شده در مسیر var/www به کمک مونو و آپاچی اجرا خواهند شد.
دسترسی‌های فعلی کاربر وارد شده به لینوکس اجازه کپی فایل‌ها را به مسیر var/www نمی‌دهد. همچنین می‌خواهیم این کارها را توسط File browser آن انجام دهیم و نه خط فرمان. برای این منظور دستور ذیل را اجرا کنید تا File browser آن با دسترسی مدیریتی اجرا شود:
 sudo nautilus
الان می‌توان بدون مشکل در همان File browser گرافیکی آن، پوشه مثال وب فرم‌های خود را به درون پوشه var/www کپی کرد.
پس از کپی دو برنامه Web forms و MVC ابتدای بحث، نیاز است مجددا فایل تنظیمات آپاچی را ویرایش کنیم:
 sudo gedit /etc/apache2/sites-available/default
اکنون تنظیم برنامه وب فرم را به نحو ذیل به این فایل اضافه کنید:
 AddMonoApplications default "/webforms_test:/var/www/WebFormsApp/"
<Location /webforms_test>
SetHandler mono
</Location>
در تنظیم webforms_test:/var/www/WebFormsApp/، قسمت webforms_test نامی است که در مرورگر وارد خواهد شد و قسمت بعد از : مسیر فایل‌های برنامه و ریشه سایت است.
و تنظیم برنامه MVC به صورت زیر می‌باشد:
 AddMonoApplications default "/mvc_test:/var/www/Mvc4ProjectApp/"
<Location /mvc_test>
SetHandler mono
</Location>
بعد از هر بار تغییر فایل تنظیمات آپاچی باید یکبار وب سرور را توسط دستور sudo service apache2 restart ری استارت کنیم.
اکنون برنامه وب فرم‌ها در مسیر http://127.0.0.1/webforms_test و برنامه MVC در مسیر http://127.0.0.1/mvc_test قابل دسترسی است.


خلاصه بحث
پس از نصب وب سرور آپاچی و ماژول مونوی مخصوص آن، فایل etc/apache2/sites-available/default را به نحو ذیل ویرایش کنید و مسیرهای برنامه‌های خود را در آن تعریف نمائید:

<VirtualHost *:80>
ServerAdmin webmaster@localhost
        MonoAutoApplication disabled
        AddHandler mono .aspx .ascx .asax .ashx .config .cs .asmx .axd
        MonoApplications "/:/var/www"
        MonoServerPath default "/usr/bin/mod-mono-server4"

DocumentRoot "/var/www"
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /var/www/>
                SetHandler mono
                DirectoryIndex index.aspx index.html default.aspx Default.aspx
Options Indexes Includes FollowSymLinks MultiViews
AllowOverride None
Order allow,deny
allow from all                
</Directory>

ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>

        <DirectoryMatch "/([bB]in|[Aa]pp_[Cc]ode|[Aa]pp_[Dd]ata|[Aa]pp_[Gg]lobal[Rr]esources|[Aa]pp_[Ll]ocal[Rr]esources)/">
          Order deny,allow
          Deny from all
        </DirectoryMatch>

        AddMonoApplications default "/webforms_test:/var/www/WebFormsApp/"
        <Location /webforms_test> 
         SetHandler mono
        </Location>

       AddMonoApplications default "/mvc_test:/var/www/Mvc4ProjectApp/"
        <Location /mvc_test> 
         SetHandler mono
        </Location>

ErrorLog ${APACHE_LOG_DIR}/error.log

# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn

CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>


نظرات مطالب
کار با کوکی‌ها در ASP.NET Core

یک نکته‌ی تکمیلی: در تنظیمات کوکی‌های سایت جدید، SameSite به Strict تنظیم شده بود:

options.Cookie.SameSite = SameSiteMode.Strict;

این موضوع سبب شده بود که اگر کاربری در سایت لاگین کرده بود و ایمیلی را دریافت می‌کرد، پس از کلیک بر روی ایمیل دریافتی و ورود به سایت، اعتبارسنجی نشده تلقی می‌شد و برای مثال امکان ارسال پاسخی را نداشت؛ چون حالت Strict به این معنا است که کوکی‌های سایت (منجمله کوکی‌ اعتبارسنجی کاربر)، فقط به درخواست‌های رسیده‌ی از طریق خود سایت، به صورت خودکار توسط مرورگرها ارسال می‌شوند و نه در حالتیکه یک redirect از سایت دیگری وجود داشته باشد. برای تعدیل این تنظیم می‌توان از حالت Lax استفاده کرد:

options.Cookie.SameSite = SameSiteMode.Lax;

در اصل، حالت Lax با حالت Strict، تفاوت آنچنانی ندارد و هنوز هم کوکی‌ها را فقط به درخواست‌های رسیده‌ی از طریق دومین جاری ارسال می‌کند. اما در این حالت اگر redirect ای نیز به دومین برنامه، از نوع GET وجود داشته باشد، آن‌را امن حساب کرده و کوکی‌های برنامه را در اختیار آن درخواست قرار می‌دهد.

پ.ن.

برای دریافت کوکی‌های جدید از نوع Lax، باید یکبار از برنامه خارج شوید و مجددا به آن وارد شوید تا کوکی‌های جدید را دریافت کنید.

مطالب
هدایت درخواست فایل‌های استاتیک در ASP.NET MVC به یک کنترلر
فرض کنید یک پوشه Export در ریشه سایت دارید که حاوی تعدادی فایل PDF عمومی است.
سؤال: آیا می‌شود دسترسی به فایل‌های قرار گرفته در این پوشه عمومی را کنترل کرد؟ به نحوی که فقط کاربران عضو سایت پس از اعتبارسنجی بتوانند آن‌ها را دریافت کنند؟
پاسخ: شاید عنوان کنید که می‌توان از تگ location در فایل web.config برای اینکار استفاده کرد:
<location path="Export">
    <system.web>
      <authorization>
        <deny users="?" />
      </authorization>
    </system.web>
  </location>
این تنظیمات هیچ اثری بر روی فایل‌های استاتیک PDF ندارند؛ چون در IIS 6 از موتور ASP.NET رد نخواهند شد. مگر اینکه این نوع پسوند‌ها به aspnet_isapi.dll انتساب داده شوند. در IIS 7 به بعد این وضع بهبود یافته است. اگر تنظیم runAllManagedModulesForAllRequests در وب کانفیگ برنامه به true تنظیم شده باشد و برنامه در حالت Integrated pipeline بجای Classic mode اجرا شود، امکان مدیریت فایل‌های استاتیک نیز در برنامه‌های ASP.NET وجود دارد .

سؤال: آیا راه حلی وجود دارد که بتوان فایل‌های استاتیک را صرفنظر از نوع، نگارش و حالت اجرای IIS توسط موتور ASP.NET مدیریت کرد؟
پاسخ: بلی. در ASP.NET MVC با تنظیم یک سطر ذیل، اینکار انجام می‌شود:
public static void RegisterRoutes(RouteCollection routes)
{
   // ...
   routes.RouteExistingFiles = true;
   // ...
}
توضیحات:
RouteCollection در ASP.NET MVC به کمک امکانات MapPathBasedVirtualPathProvider خود، ابتدا درخواست رسیده را بررسی می‌کند. اگر این درخواست به یک فایل عمومی اشاره کند، کل سیستم مسیریابی را غیرفعال می‌کند. اما با تنظیم RouteExistingFiles دیگر این بررسی صورت نخواهد گرفت (به عبارتی در بالا بردن سرعت نمایشی سایت نیز تاثیر گذار خواهد بود؛ چون یکی از بررسی‌ها را حذف می‌کند).


ایجاد کنترلری به نام پوشه‌ای که قرار است محافظت شود

نام پوشه قرار گرفته در ریشه سایت، Export است. بنابراین برای هدایت درخواست‌های رسیده به آن (پس از تنظیم فوق)، نیاز است یک کنترلر جدید را به نام Export نیز ایجاد کنیم:
using System.IO;
using System.Web.Mvc;

namespace Mvc4RouteExistingFiles.Controllers
{
    public class ExportController : Controller
    {
        public ActionResult Index(string id)
        {
            if (string.IsNullOrWhiteSpace(id))
            {
                return Redirect("/");
            }

            var fileName= Path.GetFileName(id);
            var path = Server.MapPath("~/export/"+ fileName);
            return File(path, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
        }
    }
}
البته بدیهی است در اینجا می‌توان فیلتر Authorize را به کل کنترلر اعمال کرد، یا هر تنظیم دیگری که نیاز است.
برای اینکه بررسی کنیم، آیا واقعا فایل‌های استاتیک قرار گرفته در پوشه Export به این کنترلر هدایت می‌شود یا خیر، یک breakpoint را بر روی سطر اول اکشن متد Index قرار می‌دهیم. برنامه را اجرا کنید ... کار نخواهد کرد، زیرا مسیر یک فایل فرضی به صورت ذیل:
 http://localhost/export/test.pdf
به اکشن متد Index کنترلر Export، نگاشت نخواهد شد (index در این مسیر ذکر نشده است).
برای حل این مشکل فقط کافی است مسیر یابی متناظری را تعریف کنیم:
using System.Web.Mvc;
using System.Web.Routing;

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

            routes.RouteExistingFiles = true;

            routes.MapRoute(
                name: "ExportRoute",
                url: "Export/{id}",
                defaults: new { controller = "Export", action = "Index", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در اینجا ExportRoute را مشاهده می‌کنید که به آدرس‌هایی به فرم Export/id پاسخ می‌دهد. در این حالت به صورت خودکار با توجه به تنظیمات انجام شده، اکشن متدی که انتخاب می‌شود همان Index خواهد بود و نیازی به ذکر صریح آن نخواهد بود.
اینبار اگر برنامه را اجرا کنیم، breakpoint ما کار خواهد کرد:



تنظیمات ثانویه پس از فعال سازی RouteExistingFiles

در این حالت با فعال سازی مسیریابی فایل‌های موجود، دیگر هیچ فایل استاتیکی به صورت معمول در اختیار کاربران قرار نخواهد گرفت و اگر همانند توضیحات قبل برای آن‌ها کنترلر جداگانه‌ای را تهیه نکنیم، عملا سایت از کار خواهد افتاد.
برای رفع این مشکل، در ابتدای متد RegisterRoutes فوق، تنظیمات ذیل را اضافه کنید تا پوشه‌های content، scripts و همچنین یک سری فایل با پسوند مشخص، همانند سابق و مستقیما توسط سرور ارائه شوند؛ در غیراینصورت کاربر پیغام 404 را پس از درخواست آن‌ها، دریافت خواهد کرد:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("content/{*pathInfo}");
routes.IgnoreRoute("scripts/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.IgnoreRoute("{resource}.ico");
routes.IgnoreRoute("{resource}.png");
routes.IgnoreRoute("{resource}.jpg");
routes.IgnoreRoute("{resource}.gif");
routes.IgnoreRoute("{resource}.txt");