مطالب
React 16x - قسمت 29 - احراز هویت و اعتبارسنجی کاربران - بخش 4 - محافظت از مسیرها
در قسمت قبل، دکمه‌ی new movie را برای کاربران وارد نشده‌ی به سیستم، از صفحه‌ی نمایش لیست فیلم‌ها، مخفی کردیم. اما ... اگر آدرس http://localhost:3000/movies/new مستقیما در مرورگر وارد شود، هنوز هم برای عموم کاربران قابل دسترسی است.


روش محافظت از مسیریابی‌های تعریف شده‌ی در برنامه

شبیه به روشی را که در قسمت قبل، برای انتقال شیء user، به مسیریابی کامپوننت Movies استفاده کردیم:
<Route
     path="/movies"
     render={props => <Movies {...props} user={this.state.currentUser} />}
/>
در اینجا نیز می‌توان برای محافظت از یک مسیریابی، استفاده کرد. به همین جهت به app.js مراجعه کرده و مسیریابی فعلی کامپوننت MovieForm را:
<Route path="/movies/:id" component={MovieForm} />
به صورت زیر تغییر می‌دهیم:
<Route
  path="/movies/:id"
  render={props => {
    if (!this.state.currentUser) {
      return <Redirect to="/login" />;
    }
    return <MovieForm {...props} />;
  }}
/>
اینبار نیز بجای ویژگی component، از ویژگی render استفاده می‌کنیم تا بتوان در اینجا به صورت پویا، کدنویسی کرد. ابتدا بررسی می‌کنیم که آیا کاربر جاری تنظیم شده‌است؟ اگر خیر، او را به صفحه‌ی لاگین هدایت می‌کنیم؛ در غیراینصورت، همان کامپوننت MovieForm را به همراه تمام props مرتبط با آن، بازگشت می‌دهیم.

اکنون اگر این تغییرات را ذخیره کرده و در حالت Logout، مسیر http://localhost:3000/movies/new را مستقیما درخواست دهیم، به صفحه‌ی لاگین هدایت خواهیم شد.


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

هرچند روشی که تا اینجا برای محافظت از مسیریابی‌ها معرفی شد، بدون مشکل کار می‌کند، اما اگر قرار باشد برای تمام مسیریابی‌های اینگونه، استفاده شود، به تکرار بیش از اندازه‌ی کدهای یکسانی خواهیم رسید. به همین جهت می‌توان این منطق را تبدیل به یک کامپوننت با قابلیت استفاده‌ی مجدد کرد؛ تا دیگر نیازی به تکرار این if/else‌ها نباشد. برای این منظور، فایل جدید src\components\common\protectedRoute.jsx را ایجاد می‌کنیم. کامپوننت جدید protectedRoute را هم در پوشه‌ی common قرار داده‌ایم؛ چون وابستگی به دومین این برنامه نداشته و می‌تواند در سایر برنامه نیز مورد استفاده قرار گیرد. سپس با استفاده از میانبرهای imrc و sfc، یک کامپوننت تابعی بدون حالت را به نام ProtectedRoute ایجاد کرده و در آن، همان کامپوننت اصلی Route را بازگشت می‌دهیم. بنابراین هر زمانیکه از ProtectedRoute استفاده شود، خروجی آن، همان کامپوننت استاندارد Route خواهد بود که اینبار قرار است از وضعیت کاربر جاری وارد شده‌ی به سیستم، مطلع باشد. به همین جهت در اولین قدم، همان قطعه کد Route فوق را که به همراه if/else نوشتیم، از فایل app.js کپی کرده و به اینجا، داخل متد رندر کامپوننت، منتقل می‌کنیم. سپس شروع می‌کنیم به متغیر کردن عباراتی که در آن به صورت صریح و ثابت، مقدار دهی شده‌اند تا به یک کامپوننت با قابلیت استفاده‌ی مجدد برسیم:
import React from "react";
import { Route, Redirect } from "react-router-dom";
import * as auth from "../../services/authService";

const ProtectedRoute = ({ path, component: Component, render, ...rest }) => {
  return (
    <Route
      {...rest}
      render={props => {
        if (!auth.getCurrentUser())
          return (
            <Redirect
              to={{
                pathname: "/login",
                state: { from: props.location }
              }}
            />
          );
        return Component ? <Component {...props} /> : render(props);
      }}
    />
  );
};

export default ProtectedRoute;
- در ابتدا بجای ذکر props بعنوان پارامتر این کامپوننت، از طریق Object Destructuring، خواصی را که قرار است به صورت props دریافت کنیم، مشخص کرده‌ایم. مزیت اینکار، مشخص شدن اینترفیس این کامپوننت به نحو واضحی است. برای مثال بجای ذکر مقدار ویژگی path، به صورت یک رشته‌ی ثابت، آن‌را از طریق یک متغیر دریافت می‌کنیم.
- در این کامپوننت نیاز است اطلاعات کاربر جاری وارد شده‌ی به سیستم در دسترس باشد. یا می‌توان آن‌را به عنوان یکی از خواص props دریافت کرد و یا همانند این مثال، امکان دریافت مستقیم آن از  authService نیز وجود دارد.
- در ادامه اگر CurrentUser مقدار دهی نشده باشد، کامپوننت Redirect را که کاربر را به صفحه‌ی لاگین هدایت می‌کند، بازگشت می‌دهیم. در غیراینصورت نیاز است یک کامپوننت را بجای برای مثال MovieForm، بازگشت دهیم. علت استفاده‌ی از component: Component این است که React انتظار دارد، کامپوننت‌ها با نام بزرگ شروع شوند. به همین جهت خاصیت component را از props دریافت کرده و آن‌را به Component تغییر نام می‌دهیم.
- زمانیکه از کامپوننت Route استاندارد استفاده می‌شود، یا از ویژگی component آن استفاده می‌شود و یا از ویژگی render آن که یک تابع است، تا بتوان داخل آن، کدهای پویایی را درج کرد. به همین جهت ممکن است که مقدار متغیر کامپوننت دریافت شده، نال باشد. بنابراین در اینجا بررسی می‌شود که آیا Component، مقدار دهی شده‌است یا خیر؟ اگر بله، همان کامپوننت را به همراه props آن بازگشت می‌دهیم. در غیراینصورت، متد render مقدار دهی شده را به همراه props ارسالی به آن، بازگشت خواهیم داد.
- علت وجود پارامتر rest نیز این است که این کامپوننت علاوه بر ویژگی‌هایی که تاکنون پیش بینی کرده‌ایم، ممکن است در آینده ویژگی‌های دیگری را نیز نیاز داشته باشد. به همین جهت مابقی آن‌ها را توسط {rest...}، به صورت خودکار در اینجا درج می‌کنیم. برای نمونه در اینجا ذکر path={path} را مشاهده نمی‌کنید؛ چون توسط همان {rest...} به صورت خودکار تامین می‌شود.

اکنون به app.js بازگشته و کدهای قبلی را با این کامپوننت جدید ProtectedRoute، جایگزین می‌کنیم:
import ProtectedRoute from "./components/common/protectedRoute";
// ...

<ProtectedRoute path="/movies/:id" component={MovieForm} />
اینبار نحوه‌ی تعریف ProtectedRoute، همانند نحوه‌ی تعریف کامپوننت Route استاندارد است؛ با این تفاوت که این کامپوننت در پشت صحنه، از وضعیت کاربر جاری سیستم مطلع است و بر اساس آن واکنش نشان می‌دهد.


مدیریت بازگشت کاربران، پس از لاگین به سیستم

پس از خروج از برنامه، اگر سعی در ویرایش یکی از فیلم‌های موجود کنیم، به صفحه‌ی لاگین هدایت خواهیم شد. پس از لاگین موفق، مجددا به ریشه‌ی سایت بازگشت داده می‌شویم و نه به صفحه‌ای که پیش از لاگین، مدنظر کاربر بوده‌است. برای رفع این مشکل نیاز است بتوان به آدرس قبلی درخواستی، دسترسی یافت و این مورد توسط سیستم مسیریابی، به کامپوننت‌ها به صورت خودکار تزریق می‌شود. برای مثال اگر در کامپوننت ProtectedRoute، مقدار شیء props دریافتی را لاگ کنیم:
  return (
    <Route
      {...rest}
      render={props => {
        console.log(props);
و سپس بر روی لینک به مشاهده‌ی جزئیات و ویرایش یک فیلم کلیک کنیم، تصویر زیر حاصل می‌شود:


همانطور که مشخص است، شیء location دریافتی از props، به همراه اطلاعات آدرسی است که پیش از هدایت خودکار به صفحه‌ی لاگین، درخواست کرده بودیم. به همین جهت یک چنین تنظیمی، در تعاریف کامپوننت ProtectedRoute درنظر گرفته شده‌اند:
<Redirect
              to={{
                pathname: "/login",
                state: { from: props.location }
              }}
            />
در کامپوننت Redirect، مقدار to می‌تواند یک رشته و یا یک شیء باشد. اگر حالت انتساب یک شیء را انتخاب کردیم، خاصیت pathname آن مانند قبل است و مکان نهایی Redirect را مشخص می‌کند. اما کار خاصیت state آن، ارسال اطلاعاتی اضافی است به کامپوننتی که قرار است کار Redirect به آن صورت گیرد. برای مثال در تنظیم فوق، شیء ای که دارای خاصیت from و با مقدار props.location است، به صورت خودکار به کامپوننت مقصد ارسال می‌شود.
اکنون که این شیء، به کامپوننت لاگین، پس از Redirect خودکار ارسال می‌شود، نیاز است به src\components\loginForm.jsx مراجعه کرده و تغییرات زیر را اعمال کنیم:
  doSubmit = async () => {
    try {
      const { data } = this.state;
      await auth.login(data.username, data.password);

      const { state } = this.props.location;
      window.location = state ? state.from.pathname : "/";
    } catch (ex) {
      //...
در اینجا خاصیت state، از شیء location تزریق شده‌ی به props این کامپوننت، استخراج می‌شود. سپس با مقدار دهی window.location به from.pathname آن، کار هدایت کاربر را پس از لاگین موفق، به آدرس قبلی مدنظر او، انجام می‌دهیم.

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


عدم نمایش مجدد صفحه‌ی لاگین، به کاربران وارد شده‌ی به سیستم

آخرین تغییری را که در اینجا اعمال خواهیم کرد، رفع مشکل امکان مشاهده‌ی مجدد صفحه‌ی لاگین، با وارد کردن مستقیم آدرس آن در مرورگر، پس از ورود موفقیت آمیز به سیستم است. برای این منظور، ابتدای متد رندر کامپوننت فرم لاگین را به صورت زیر تغییر می‌دهیم تا اگر کاربر، پیشتر به سیستم وارد شده بود، به صورت خودکار به ریشه‌ی سایت هدایت شده و مجددا فرم لاگین برای او رندر نشود:
import { Redirect } from "react-router-dom";
//...


  render() {
    if (auth.getCurrentUser()) return <Redirect to="/" />;


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-29-backend.zip و sample-29-frontend.zip
مطالب دوره‌ها
عیب یابی و دیباگ برنامه‌های SignalR
1) برنامه SignalR در IE کار نمی‌کند.
پردازشگر json، در نگارش‌های اخیر IE به آن اضافه شده است. برای رفع این مشکل در نگارش‌های قدیمی، نیاز است از اسکریپت کمکی http://nuget.org/List/Packages/json2 استفاده نمائید. همچنین مرورگر IE را نیز باید وادار ساخت تا بر اساس آخرین موتور پردازشی خود کار کند:
 <meta http-equiv="X-UA-Compatible" content="IE=edge" />

2) هنگام فراخوانی مسیر signalr/hubs پیغام 404 (یافت نشد) دریافت می‌شود.
برای رفع این مشکل ابتدا اطمینان حاصل کنید که تنظیمات مسیریابی تعریف شده در فایل global.asax.cs موجود هستند.
در ادامه اطمینان حاصل نمائید مسیر اسکریپت‌های signalr/hubs به درستی تعریف شده‌اند:
 <script type="text/javascript" src="/signalr/hubs"></script>
برای مثال در برنامه‌های MVC و وب فرم‌ها تعریف صحیح باید به شکل زیر باشد:
 MVC:
<script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script>

Web forms:
<script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
همچنین وجود تنظیمات ذیل را در فایل وب کانفیگ برنامه نیز بررسی کنید:
<configuration>
   <system.webServer>
        <modules runAllManagedModulesForAllRequests="true">
        </modules>
    </system.webServer>
</configuration>

3) متدهای سمت کلاینت من فراخوانی نمی‌شوند.
بهترین راه برای مشاهده ریز جرئیات خطاها، ذکر سطر ذیل در کدهای سمت کلاینت جاوا اسکریپتی برنامه است:
 $.connection.hub.logging = true;
و سپس مراجعه به کنسول جاوا اسکریپت مرورگر برای بررسی خطاهای لاگ شده.

4) خطای «Connection must be started before data can be sent» را دریافت می‌کنم.
همانطور که در قسمت قبل عنوان شد، کلیه فراخوانی‌های SignalR از نوع غیرهمزمان هستند. بنابراین باید با استفاده از callback و زمان فراخوانی آن‌ها که عموما پس از برقراری اتصال رخ می‌دهد، نسبت به انجام امور دلخواه اقدام کرد.
               var connection = $.hubConnection('http://localhost:8081/');
               proxy = connection.createProxy('collectionhub')
               connection.start()
                    .done(function () {
                        proxy.invoke('subscribe', 'Product');
                        $('#messages').append('<li>invoked subscribe</li>');
                    })
                    .fail(function () { alert("Could not Connect!"); });
همانطور که در این مثال مشاهده می‌کنید، سطر proxy.invoke در یک callback فراخوانی شده است و نه بلافاصله در سطری پس از connection.start. هر زمان که اتصال به نحو موفقیت آمیزی برقرار شد، آنگاه متد subscribe در سمت سرور فراخوانی می‌گردد.
در حالت استفاده بدون پروکسی نیز چنین callbackهایی قابل تعریف هستند:
$.connection.hub.start()
                .done(function() {
                    myHub.server.SomeFunction(SomeParam) //e.g. a login or init
                         .done(connectionReady); 
                })
                .fail(function() {
                    alert("Could not Connect!");
                 });

5) بعد از 10 اتصال به IIS، برنامه متوقف می‌شود.
این مورد، محدودیت ذاتی IIS 7 نصب شده بر روی ویندوز 7 است. بهتر است از یک IIS کامل موجود در ویندوزهای سرور استفاده کنید. در این سرورها عدد پیش فرض تنظیم شده 5000 اتصال است که در صورت نیاز با استفاده از دستور زیر قابل تغییر است:
 appcmd.exe set config /section:system.webserver/serverRuntime /appConcurrentRequestLimit:100000
به علاوه ASP.NET نیز محدودیت 5000 اتصال به ازای هر CPU را دارد. برای تغییر آن باید به مسیر ذیل مراجعه
  %windir%\Microsoft.NET\Framework\v4.0.30319\aspnet.config
و سپس مقدار maxConcurrentRequestsPerCPU را تنظیم کرد:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <runtime>
        <legacyUnhandledExceptionPolicy enabled="false" />
        <legacyImpersonationPolicy enabled="true"/>
        <alwaysFlowImpersonationPolicy enabled="false"/>
        <SymbolReadingPolicy enabled="1" />
        <shadowCopyVerifyByTimestamp enabled="true"/>
    </runtime>
    <startup useLegacyV2RuntimeActivationPolicy="true" />
    <system.web>
        <applicationPool maxConcurrentRequestsPerCPU="20000" />
    </system.web>
</configuration>
به علاوه ASP.NET پس از رد شدن از حد maxConcurrentRequestsPerCPU، درخواست‌ها را در صف قرار می‌دهد. این مورد نیز قابل تنظیم است. ابتدا به مسیر ذیل مراجعه کرده
 %windir%\Microsoft.NET\Framework\v4.0.30319\Config\machine.config
و سپس در صورت نیاز و لزوم، مقدار requestQueueLimit را تغییر دهید:
 <processModel autoConfig="false" requestQueueLimit="250000" />
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش سوم
تغییر الگوریتم پیش فرض هش کردن کلمه‌های عبور ASP.NET Identity

کلمه‌های عبور کاربران فعلی سیستم با الگوریتمی متفاوت از الگوریتم مورد استفاده Identity هش شده‌اند. برای اینکه کاربرانی که قبلا ثبت نام کرده بودند بتوانند با کلمه‌های عبور خود وارد سایت شوند، باید الگوریتم هش کردن Identity را با الگوریتم فعلی مورد استفاده Iris جایگزین کرد.

برای تغییر روش هش کردن کلمات عبور در Identity باید اینترفیس IPasswordHasher را پیاده سازی کنید:
    public class IrisPasswordHasher : IPasswordHasher
    {
        public string HashPassword(string password)
        {
            return Utilities.Security.Encryption.EncryptingPassword(password);
        }

        public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            return Utilities.Security.Encryption.VerifyPassword(providedPassword, hashedPassword) ?
                                                                PasswordVerificationResult.Success :
                                                                PasswordVerificationResult.Failed;
        }
    }

  سپس باید وارد کلاس ApplicationUserManager شده و در سازنده‌ی آن اینترفیس IPasswordHasher را به عنوان وابستگی تعریف کنید:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
            IUnitOfWork uow,
            IIdentity identity,
            IApplicationRoleManager roleManager,
            IDataProtectionProvider dataProtectionProvider,
            IIdentityMessageService smsService,
            IIdentityMessageService emailService, IPasswordHasher passwordHasher)
            : base(store)
        {
            _store = store;
            _uow = uow;
            _identity = identity;
            _users = _uow.Set<ApplicationUser>();
            _roleManager = roleManager;
            _dataProtectionProvider = dataProtectionProvider;
            this.SmsService = smsService;
            this.EmailService = emailService;
            PasswordHasher = passwordHasher;
            createApplicationUserManager();
        }

برای اینکه کلاس IrisPasswordHasher را به عنوان نمونه درخواستی IPasswordHasher معرفی کنیم، باید در تنظیمات StructureMap کد زیر را نیز اضافه کنید:
x.For<IPasswordHasher>().Use<IrisPasswordHasher>();

پیاده سازی اکشن متد ثبت نام کاربر با استفاده از Identity

در کنترلر UserController، اکشن متد Register را به شکل زیر بازنویسی کنید:
        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public virtual async Task<ActionResult> Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    CreatedDate = DateAndTime.GetDateTime(),
                    Email = model.Email,
                    IP = Request.ServerVariables["REMOTE_ADDR"],
                    IsBaned = false,
                    UserName = model.UserName,
                    UserMetaData = new UserMetaData(),
                    LastLoginDate = DateAndTime.GetDateTime()
                };

                var result = await _userManager.CreateAsync(user, model.Password);

                if (result.Succeeded)
                {
                    var addToRoleResult = await _userManager.AddToRoleAsync(user.Id, "user");
                    if (addToRoleResult.Succeeded)
                    {
                        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id);
                        var callbackUrl = Url.Action("ConfirmEmail", "User",
                            new { userId = user.Id, code }, protocol: Request.Url.Scheme);

                        _emailService.SendAccountConfirmationEmail(user.Email, callbackUrl);

                        return Json(new { result = "success" });
                    }

                    addErrors(addToRoleResult);
                }

                addErrors(result);
            }

            return PartialView(MVC.User.Views._Register, model);
        }
نکته: در اینجا برای ارسال لینک فعال سازی حساب کاربری، از کلاس EmailService خود سیستم IRIS استفاده شده است؛ نه EmailService مربوط به ASP.NET Identity. همچنین در ادامه نیز از EmailService مربوط به خود سیستم Iris استفاده شده است.

برای این کار متد زیر را به کلاس EmailService  اضافه کنید: 
        public SendingMailResult SendAccountConfirmationEmail(string email, string link)
        {
            var model = new ConfirmEmailModel()
            {
                ActivationLink = link
            };

            var htmlText = _viewConvertor.RenderRazorViewToString(MVC.EmailTemplates.Views._ConfirmEmail, model);

            var result = Send(new MailDocument
            {
                Body = htmlText,
                Subject = "تایید حساب کاربری",
                ToEmail = email
            });

            return result;
        }
همچنین قالب ایمیل تایید حساب کاربری را در مسیر Views/EmailTemplates/_ConfirmEmail.cshtml با محتویات زیر ایجاد کنید:
@model Iris.Model.EmailModel.ConfirmEmailModel

<div style="direction: rtl; -ms-word-wrap: break-word; word-wrap: break-word;">
    <p>با سلام</p>
    <p>برای فعال سازی حساب کاربری خود لطفا بر روی لینک زیر کلیک کنید:</p>
    <p>@Model.ActivationLink</p>
    <div style=" color: #808080;">
        <p>با تشکر</p>
        <p>@Model.SiteTitle</p>
        <p>@Model.SiteDescription</p>
        <p><span style="direction: ltr !important; unicode-bidi: embed;">@Html.ConvertToPersianDateTime(DateTime.Now, "s,H")</span></p>
    </div>
</div>

اصلاح پیام موفقیت آمیز بودن ثبت نام  کاربر جدید


سیستم IRIS از ارسال ایمیل تایید حساب کاربری استفاده نمی‌کند و به محض اینکه عملیات ثبت نام تکمیل می‌شد، صفحه رفرش می‌شود. اما در سیستم Identity یک ایمیل حاوی لینک فعال سازی حساب کاربری به او ارسال می‌شود.
برای اصلاح پیغام پس از ثبت نام، باید به فایل myscript.js درون پوشه‌ی Scripts مراجعه کرده و رویداد onSuccess شیء RegisterUser را به شکل زیراصلاح کنید: 
RegisterUser.Form.onSuccess = function (data) {
    if (data.result == "success") {
        var message = '<div id="alert"><button type="button" data-dismiss="alert">×</button>ایمیلی حاوی لینک فعال سازی، به ایمیل شما ارسال شد؛ لطفا به ایمیل خود مراجعه کرده و بر روی لینک فعال سازی کلیک کنید.</div>';
        $('#registerResult').html(message);
    }
    else {
        $('#logOnModal').html(data);
    }
};
برای تایید ایمیل کاربری که ثبت نام کرده است نیز اکشن متد زیر را به کلاس UserController اضافه کنید:
        [AllowAnonymous]
        public virtual async Task<ActionResult> ConfirmEmail(int? userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var result = await _userManager.ConfirmEmailAsync(userId.Value, code);
            return View(result.Succeeded ? "ConfirmEmail" : "Error");
        }
این اکشن متد نیز احتیاج به View دارد؛ پس view متناظر آن را با محتویات زیر اضافه کنید:
@{
    ViewBag.Title = "حساب کاربری شما تایید شد";
}
<h2>@ViewBag.Title.</h2>
<div>
    <p>
        با تشکر از شما، حساب کاربری شما تایید شد.
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

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

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async virtual Task<ActionResult> LogOn(LogOnModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                if (Request.IsAjaxRequest())
                    return PartialView(MVC.User.Views._LogOn, model);
                return View(model);
            }


            const string emailRegPattern =
                @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";

            string ip = Request.ServerVariables["REMOTE_ADDR"];

            SignInStatus result = SignInStatus.Failure;

            if (Regex.IsMatch(model.Identity, emailRegPattern))
            {

                var user = await _userManager.FindByEmailAsync(model.Identity);

                if (user != null)
                {
                    result = await _signInManager.PasswordSignInAsync
                   (user.UserName,
                   model.Password, model.RememberMe, shouldLockout: true);
                }
            }
            else
            {
                result = await _signInManager.PasswordSignInAsync(model.Identity, model.Password, model.RememberMe, shouldLockout: true);
            }


            switch (result)
            {
                case SignInStatus.Success:
                    if (Request.IsAjaxRequest())
                        return JavaScript(IsValidReturnUrl(returnUrl)
                            ? string.Format("window.location ='{0}';", returnUrl)
                            : "window.location.reload();");
                    return redirectToLocal(returnUrl);

                case SignInStatus.LockedOut:
                    ModelState.AddModelError("",
                        string.Format("حساب شما قفل شد، لطفا بعد از {0} دقیقه دوباره امتحان کنید.",
                            _userManager.DefaultAccountLockoutTimeSpan.Minutes));
                    break;
                case SignInStatus.Failure:
                    ModelState.AddModelError("", "نام کاربری یا کلمه عبور اشتباه است.");
                    break;
                default:
                    ModelState.AddModelError("", "در ورود شما خطایی رخ داده است.");
                    break;
            }


            if (Request.IsAjaxRequest())
                return PartialView(MVC.User.Views._LogOn, model);
            return View(model);
        }


اصلاح اکشن متد خروج کاربر از سایت

        [HttpPost]
        [ValidateAntiForgeryToken]
        [Authorize]
        public virtual ActionResult LogOut()
        {
            _authenticationManager.SignOut();

            if (Request.IsAjaxRequest())
                return Json(new { result = "true" });

            return RedirectToAction(MVC.User.ActionNames.LogOn, MVC.User.Name);
        }

پیاده سازی ریست کردن کلمه‌ی عبور با استفاده از ASP.NET Identity

مکانیزم سیستم IRIS برای ریست کردن کلمه‌ی عبور به هنگام فراموشی آن، ساخت GUID و ذخیره‌ی آن در دیتابیس است. سیستم Identity  با استفاده از یک توکن رمز نگاری شده و بدون استفاده از دیتابیس، این کار را انجام می‌دهد و با استفاده از قابلیت‌های تو کار سیستم Identity، تمهیدات امنیتی بهتری را نسبت به سیستم کنونی در نظر گرفته است.

برای این کار کدهای کنترلر ForgottenPasswordController را به شکل زیر ویرایش کنید:
using System.Threading.Tasks;
using System.Web.Mvc;
using CaptchaMvc.Attributes;
using Iris.Model;
using Iris.Servicelayer.Interfaces;
using Iris.Web.Email;
using Microsoft.AspNet.Identity;

namespace Iris.Web.Controllers
{
    public partial class ForgottenPasswordController : Controller
    {
        private readonly IEmailService _emailService;
        private readonly IApplicationUserManager _userManager;

        public ForgottenPasswordController(IEmailService emailService, IApplicationUserManager applicationUserManager)
        {
            _emailService = emailService;
            _userManager = applicationUserManager;
        }

        [HttpGet]
        public virtual ActionResult Index()
        {
            return PartialView(MVC.ForgottenPassword.Views._Index);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public async virtual Task<ActionResult> Index(ForgottenPasswordModel model)
        {
            if (!ModelState.IsValid)
            {
                return PartialView(MVC.ForgottenPassword.Views._Index, model);
            }

            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.Id)))
            {
                // Don't reveal that the user does not exist or is not confirmed
                return Json(new
                {
                    result = "false",
                    message = "این ایمیل در سیستم ثبت نشده است"
                });
            }

            var code = await _userManager.GeneratePasswordResetTokenAsync(user.Id);

            _emailService.SendResetPasswordConfirmationEmail(user.UserName, user.Email, code);

            return Json(new
            {
                result = "true",
                message = "ایمیلی برای تایید بازنشانی کلمه عبور برای شما ارسال شد.اعتبارایمیل ارسالی 3 ساعت است."
            });
        }

        [AllowAnonymous]
        public virtual ActionResult ResetPassword(string code)
        {
            return code == null ? View("Error") : View();
        }


        [AllowAnonymous]
        public virtual ActionResult ResetPasswordConfirmation()
        {
            return View();
        }

        //
        // POST: /Account/ResetPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return RedirectToAction("Error");
            }
            var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "ForgottenPassword");
            }
            addErrors(result);
            return View();
        }

        private void addErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

    }
}

همچنین برای اکشن متدهای اضافه شده، View‌های زیر را نیز باید اضافه کنید:

- View  با نام  ResetPasswordConfirmation.cshtml را اضافه کنید.
@{
    ViewBag.Title = "کلمه عبور شما تغییر کرد";
}

<hgroup>
    <h1>@ViewBag.Title.</h1>
</hgroup>
<div>
    <p>
        کلمه عبور شما با موفقیت تغییر کرد
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

- View با نام ResetPassword.cshtml
@model Iris.Model.ResetPasswordViewModel
@{
    ViewBag.Title = "ریست کردن کلمه عبور";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("ResetPassword", "ForgottenPassword", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>ریست کردن کلمه عبور</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Code)
    <div>
        @Html.LabelFor(m => m.Email, "ایمیل", new { @class = "control-label" })
        <div>
            @Html.TextBoxFor(m => m.Email)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.Password, "کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.Password)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.ConfirmPassword, "تکرار کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.ConfirmPassword)
        </div>
    </div>
    <div>
        <div>
            <input type="submit" value="تغییر کلمه عبور" />
        </div>
    </div>
}

همچنین این View و Controller متناظر آن، احتیاج به ViewModel زیر دارند که آن را به پروژه‌ی Iris.Models اضافه کنید.
using System.ComponentModel.DataAnnotations;
namespace Iris.Model
{
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "ایمیل")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "کلمه عبور باید حداقل 6 حرف باشد", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "کلمه عبور")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "تکرار کلمه عبور")]
        [Compare("Password", ErrorMessage = "کلمه عبور و تکرارش یکسان نیستند")]
        public string ConfirmPassword { get; set; }

        public string Code { get; set; }
    }
}

حذف سیستم قدیمی احراز هویت

برای حذف کامل سیستم احراز هویت IRIS، وارد فایل Global.asax.cs شده و سپس از متد Application_AuthenticateRequest کدهای زیر را حذف کنید:

var principalService = ObjectFactory.GetInstance<IPrincipalService>();
var formsAuthenticationService = ObjectFactory.GetInstance<IFormsAuthenticationService>();
context.User = principalService.GetCurrent()

فارسی کردن خطاهای ASP.NET Identity

سیستم Identity، پیام‌های خطا‌ها را از فایل Resource موجود در هسته‌ی خود، که به طور پیش فرض، زبان آن انگلیسی است، می‌خواند. برای مثال وقتی ایمیلی تکراری باشد، پیامی به زبان انگلیسی دریافت خواهید کرد و متاسفانه برای تغییر آن، راه سر راست و واضحی وجود ندارد. برای تغییر این پیام‌ها می‌توان از سورس باز بودن Identity استفاده کنید و قسمتی را که پیام‌ها را تولید می‌کند، خودتان با پیام‌های فارسی باز نویسی کنید.

راه اول این است که از این پروژه استفاده کرد و کلاس‌های زیر را به پروژه اضافه کنید:

    public class CustomUserValidator<TUser, TKey> : IIdentityValidator<ApplicationUser>
        where TUser : class, IUser<int>
        where TKey : IEquatable<int>
    {

        public bool AllowOnlyAlphanumericUserNames { get; set; }
        public bool RequireUniqueEmail { get; set; }

        private ApplicationUserManager Manager { get; set; }
        public CustomUserValidator(ApplicationUserManager manager)
        {
            if (manager == null)
                throw new ArgumentNullException("manager");
            AllowOnlyAlphanumericUserNames = true;
            Manager = manager;
        }
        public virtual async Task<IdentityResult> ValidateAsync(ApplicationUser item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var errors = new List<string>();
            await ValidateUserName(item, errors);
            if (RequireUniqueEmail)
                await ValidateEmailAsync(item, errors);
            return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }

        private async Task ValidateUserName(ApplicationUser user, ICollection<string> errors)
        {
            if (string.IsNullOrWhiteSpace(user.UserName))
                errors.Add("نام کاربری نباید خالی باشد");
            else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, "^[A-Za-z0-9@_\\.]+$"))
            {
                errors.Add("برای نام کاربری فقط از کاراکتر‌های مجاز استفاده کنید ");
            }
            else
            {
                var owner = await Manager.FindByNameAsync(user.UserName);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این نام کاربری قبلا ثبت شده است");
            }
        }

        private async Task ValidateEmailAsync(ApplicationUser user, ICollection<string> errors)
        {
            var email = await Manager.GetEmailStore().GetEmailAsync(user).WithCurrentCulture();
            if (string.IsNullOrWhiteSpace(email))
            {
                errors.Add("وارد کردن ایمیل ضروریست");
            }
            else
            {
                try
                {
                    var m = new MailAddress(email);

                }
                catch (FormatException)
                {
                    errors.Add("ایمیل را به شکل صحیح وارد کنید");
                    return;
                }
                var owner = await Manager.FindByEmailAsync(email);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این ایمیل قبلا ثبت شده است");
            }
        }
    }

    public class CustomPasswordValidator : IIdentityValidator<string>
    {
        #region Properties
        public int RequiredLength { get; set; }
        public bool RequireNonLetterOrDigit { get; set; }
        public bool RequireLowercase { get; set; }
        public bool RequireUppercase { get; set; }
        public bool RequireDigit { get; set; }
        #endregion

        #region IIdentityValidator
        public virtual Task<IdentityResult> ValidateAsync(string item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var list = new List<string>();

            if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength)
                list.Add(string.Format("کلمه عبور نباید کمتر از 6 کاراکتر باشد"));

            if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit))
                list.Add("برای امنیت بیشتر از حداقل از یک کارکتر غیر عددی و غیر حرف  برای کلمه عبور استفاده کنید");

            if (RequireDigit && item.All(c => !IsDigit(c)))
                list.Add("برای امنیت بیشتر از اعداد هم در کلمه عبور استفاده کنید");
            if (RequireLowercase && item.All(c => !IsLower(c)))
                list.Add("از حروف کوچک نیز برای کلمه عبور استفاده کنید");
            if (RequireUppercase && item.All(c => !IsUpper(c)))
                list.Add("از حروف بزرک نیز برای کلمه عبور استفاده کنید");
            return Task.FromResult(list.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(string.Join(" ", list)));
        }

        #endregion

        #region PrivateMethods
        public virtual bool IsDigit(char c)
        {
            if (c >= 48)
                return c <= 57;
            return false;
        }

        public virtual bool IsLower(char c)
        {
            if (c >= 97)
                return c <= 122;
            return false;
        }


        public virtual bool IsUpper(char c)
        {
            if (c >= 65)
                return c <= 90;
            return false;
        }

        public virtual bool IsLetterOrDigit(char c)
        {
            if (!IsUpper(c) && !IsLower(c))
                return IsDigit(c);
            return true;
        }
        #endregion

    }

سپس باید کلاس‌های فوق را به Identity معرفی کنید تا از این کلاس‌های سفارشی شده به جای کلاس‌های پیش فرض خودش استفاده کند. برای این کار وارد کلاس ApplicationUserManager شده و درون متد createApplicationUserManager کدهای زیر را اضافه کنید: 
            UserValidator = new CustomUserValidator< ApplicationUser, int>(this)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            PasswordValidator = new CustomPasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
            };
روش دیگر مراجعه به سورس ASP.NET Identity است. با مراجعه به مخزن کد آن، فایل Resources.resx آن را که حاوی متن‌های خطا به زبان انگلیسی است، درون پروژه‌ی خود کپی کنید. همچین کلاس‌های UserValidator و PasswordValidator را نیز درون پروژه کپی کنید تا این کلاس‌ها از فایل Resource موجود در پروژه‌ی خودتان استفاده کنند. در نهایت همانند روش قبلی درون متد createApplicationUserManager کلاس ApplicationUserManager، کلاس‌های UserValidator و PasswordValidator را به Identity معرفی کنید.


ایجاد SecurityStamp برای کاربران فعلی سایت

سیستم Identity برای لحاظ کردن یک سری موارد امنیتی، به ازای هر کاربر، فیلدی را به نام SecurityStamp درون دیتابیس ذخیره می‌کند و برای این که این سیستم عملکرد صحیحی داشته باشد، باید این مقدار را برای کاربران فعلی سایت ایجاد کرد تا کاربران فعلی بتوانند از امکانات Identity نظیر فراموشی کلمه عبور، ورود به سیستم و ... استفاده کنند.
برای این کار Identity، متدی به نام UpdateSecurityStamp را در اختیار قرار می‌دهد تا با استفاده از آن بتوان مقدار فیلد SecurityStamp را به روز رسانی کرد.
معمولا برای انجام این کارها می‌توانید یک کنترلر تعریف کنید و درون اکشن متد آن کلیه‌ی کاربران را واکشی کرده و سپس متد UpdateSecurityStamp را بر روی آن‌ها فراخوانی کنید.
        public virtual async Task<ActionResult> UpdateAllUsersSecurityStamp()
        {
            foreach (var user in await _userManager.GetAllUsersAsync())
            {
                await _userManager.UpdateSecurityStampAsync(user.Id);
            }
            return Content("ok");
        }
البته این روش برای تعداد زیاد کاربران کمی زمان بر است.


انتقال نقش‌های کاربران به جدول جدید و برقراری رابطه بین آن‌ها

در سیستم Iris رابطه‌ی بین کاربران و نقش‌ها یک به چند بود. در سیستم Identity این رابطه چند به چند است و من به عنوان یک حرکت خوب و رو به جلو، رابطه‌ی چند به چند را در سیستم جدید انتخاب کردم. اکنون با استفاده از دستورات زیر به راحتی می‌توان نقش‌های فعلی و رابطه‌ی بین آن‌ها را به جداول جدیدشان منتقل کرد:
        public virtual async Task<ActionResult> CopyRoleToNewTable()
        {
            var dbContext = new IrisDbContext();

            foreach (var role in await dbContext.Roles.ToListAsync())
            {
                await _roleManager.CreateAsync(new CustomRole(role.Name)
                {
                    Description = role.Description
                });
            }

            var users = await dbContext.Users.Include(u => u.Role).ToListAsync();

            foreach (var user in users)
            {
                await _userManager.AddToRoleAsync(user.Id, user.Role.Name);
            }
            return Content("ok");
        }
البته اجرای این کد نیز برای تعداد زیادی کاربر، زمانبر است؛ ولی روشی مطمئن و دقیق است.
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
- مثال را به روز کردم تا خطاهای حاصل را بهتر دریافت کنید.
- کلمه عبور و سایر تنظیمات آن باید لحاظ شوند و گرنه موفق به ثبت اطلاعات نخواهید شد.
- نحوه‌ی استخراج اطلاعات DbEntityValidationException  
مطالب دوره‌ها
متدهای async تقلبی
تا اینجا مشاهده کردیم که اگر یک چنین متد زمانبری را داشته باشیم که در آن عملیاتی طولانی انجام می‌شود،
class MyService
{
  public int CalculateXYZ()
  {
    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;
    return 42;
  }
}
برای نوشتن معادل async آن فقط کافی است که امضای متد را به async Task تغییر دهیم و سپس داخل آن از Task.Run استفاده کنیم:
class MyService
{
  public async Task<int> CalculateXYZAsync()
  {
    return await Task.Run(() =>
    {
      // Tons of work to do in here!
      for (int i = 0; i != 10000000; ++i)
        ;
      return 42;
    });
  }
}
و ... اگر از آن در یک کد UI استفاده کنیم، ترد آن‌را قفل نکرده و برنامه، پاسخگوی سایر درخواست‌های رسیده خواهد بود. اما ... به این روش اصطلاحا Fake Async گفته می‌شود؛ یا Async تقلبی!
کاری که در اینجا انجام شده، استفاده‌ی ناصحیح از Task.Run در حین طراحی یک متد و یک API است. عملیات انجام شده در آن واقعا غیرهمزمان نیست و در زمان انجام آن، باز هم ترد جدید اختصاص داده شده را تا پایان عملیات قفل می‌کند. اینجا است که باید بین CPU-bound operations و IO-bound operations تفاوت قائل شد. اگر Entity Framework 6 و یا کلاس WebClient و امثال آن، متدهایی Async را نیز ارائه داده‌اند، این‌ها به معنای واقعی کلمه، غیرهمزمان هستند و در آن‌ها کوچکترین CPU-bound operation ایی انجام نمی‌شود.
در حلقه‌ای که در مثال فوق در حال پردازش است و یا تمام اعمال انجام شده توسط CPU، از مرزهای سیستم عبور نمی‌کنیم. نه قرار است فایلی را ذخیره کنیم، نه با اینترنت سر و کار داشته باشیم و یا مثلا اطلاعاتی را از وب سرویسی دریافت کنیم و نه هیچگونه IO-bound operation خاصی قرار است صورت گیرد.
زمانیکه برنامه نویسی قرار است با API شما کار کند و به امضای async Task می‌رسد، فرضش بر این است که در این متد واقعا یک کار غیرهمزمان در حال انجام است. بنابراین جهت بالابردن کارآیی برنامه، این نسخه را نسبت به نمونه‌ی غیرهمزمان انتخاب می‌کند.
حال تصور کنید که استفاده کننده از این API یک برنامه‌ی دسکتاپ نیست، بلکه یک برنامه‌ی ASP.NET است. در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازش‌های درخواست‌های ASP.NET در تردهای خاص خود انجام می‌شوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بی‌جهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشده‌ایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش داده‌ایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آن‌ها، مصرف حافظه‌ی بیشتری نیز خواهد داشت.


یک اصل مهم در طراحی کدهای Async
استفاده از Task.Run در پیاده سازی بدنه متدهای غیرهمزمان، یک code smell محسوب می‌شود.


چکار باید کرد؟
اگر در کدهای خود اعمال Async واقعی دارید که IO-bound هستند، از معادل‌های Async طراحی شده برای کار با آن‌ها، مانند متد SaveChangesAsync در EF، متد DownloadStringTaskAsync کلاس WebClient و یا متدهای جدید Async کلاس Stream برای خواندن و نوشتن اطلاعات استفاده کنید. در یک چنین حالتی ارائه متدهای async Task بسیار مفید بوده و در جهت بالابردن مقیاس پذیری سیستم بسیار مؤثر واقع خواهند شد.
اما اگر کدهای شما صرفا قرار است بر روی CPU اجرا شوند و تنها محاسباتی هستند، اجازه دهید مصرف کننده تصمیم بگیرد که آیا لازم است از Task.Run برای فراخوانی متد ارائه شده در کدهای خود استفاده کند یا خیر. اگر برنامه‌ی دسکتاپ است، این فراخوانی مفید بوده و سبب آزاد شدن ترد UI می‌شود. اگر برنامه‌ی وب است، به هیچ عنوان نیازی به Task.Run نبوده و فراخوانی متداول آن با توجه به اینکه درخواست‌های برنامه‌های ASP.NET در تردهای مجزایی اجرا می‌شوند، کفایت می‌کند.

به صورت خلاصه
از Task.Run در پیاده سازی بدنه متدهای API خود استفاده نکنید.
از Task.Run در صورت نیاز (مثلا در برنامه‌های دسکتاپ) در حین فراخوانی و استفاده از متدهای API ارائه شده استفاده نمائید:
 private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.CalculateXYZ());
}
در این مثال از همان نسخه‌ی غیرهمزمان متد محاسباتی استفاده شده‌است و اینبار مصرف کننده است که تصمیم گرفته در حین فراخوانی و استفاده نهایی، برای آزاد سازی ترد UI از await Task.Run استفاده کند (یا خیر).

بنابراین نوشتن یک چنین کدهایی در پیاده سازی یک API غیرهمزمان
await Task.Run(() =>
{
   for (int i = 0; i != 10000000; ++i)
     ;
});
صرفا خود را گول زدن است. کل این عملیات بر روی CPU انجام شده و هیچگاه از مرزهای IO سیستم عبور نمی‌کند.

برای مطالعه بیشتر
Should I expose asynchronous wrappers for synchronous methods
نظرات مطالب
ساخت دیتابیس sqlite با EF6 Code First
سلام مجدد
من از دات نت Core استفاده نمی‌کنم.
من مطابق زیر عمل کردم ولی بازم همان خطا را می‌دهد.
 the underlying provider failed to open
نمیدونم چرا تگ  Connection-string  را نمیشناسه. 
دقیقا در پروژه شما اگر تگ  Connection-string  را برداریم با چنین خطایی روبرو می‌شویم.
من پروژه تستی خودم را اینجا آپلود کردم.
کد XML فایل پروژه
<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301880
  -->
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
  </configSections>
  <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" />
  </appSettings>
  <connectionStrings>
    <add name="constr" connectionString="data source=.\phonebook.sqlite;foreign keys=true" providerName="System.Data.SQLite" />
  </connectionStrings>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v13.0" />
      </parameters>
    </defaultConnectionFactory>
    <!--<providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>-->
    <providers>
      <provider invariantName="System.Data.SQLite" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
      <provider invariantName="System.Data.SQLite.EF6" type="System.Data.SQLite.EF6.SQLiteProviderServices, System.Data.SQLite.EF6" />
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
  <system.data>
    <DbProviderFactories>
      <remove invariant="System.Data.SQLite.EF6" />
      <add name="SQLite Data Provider (Entity Framework 6)" invariant="System.Data.SQLite.EF6" description=".NET Framework Data Provider for SQLite (Entity Framework 6)" type="System.Data.SQLite.EF6.SQLiteProviderFactory, System.Data.SQLite.EF6" />
      <remove invariant="System.Data.SQLite" />
      <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".NET Framework Data Provider for SQLite" type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
  </system.data>
</configuration>
کد xml فایل Package
<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="bootstrap" version="3.0.0" targetFramework="net45" />
  <package id="EntityFramework" version="6.1.3" targetFramework="net45" />
  <package id="jQuery" version="1.10.2" targetFramework="net45" />
  <package id="Microsoft.AspNet.Mvc" version="5.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.Razor" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.AspNet.WebPages" version="3.2.3" targetFramework="net45" />
  <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.0" targetFramework="net45" />
  <package id="Microsoft.Net.Compilers" version="1.0.0" targetFramework="net45" developmentDependency="true" />
  <package id="Microsoft.Web.Infrastructure" version="1.0.0.0" targetFramework="net45" />
  <package id="Modernizr" version="2.6.2" targetFramework="net45" />
  <package id="SQLite.CodeFirst" version="1.3.0.17" targetFramework="net45" />
  <package id="System.Data.SQLite" version="1.0.105.0" targetFramework="net45" />
  <package id="System.Data.SQLite.Core" version="1.0.105.0" targetFramework="net45" />
  <package id="System.Data.SQLite.EF6" version="1.0.105.0" targetFramework="net45" />
  <package id="System.Data.SQLite.Linq" version="1.0.105.0" targetFramework="net45" />
</packages>

مابقی همون کلاس‌ها و کد‌های مثال آزمایشی شماست.
من انتظار دارم در لحظه اول در نبود Database.sqlte آن را ایجاد کند. ( مانند مثال شما )

مطالب
هدایت درخواست فایل‌های استاتیک در 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");
مطالب
به روز رسانی ساده‌تر اجزاء ارتباطات در EF Code first به کمک GraphDiff
دو نوع حالت کلی کارکردن با EF وجود دارند: متصل و منقطع.
در حالت متصل مانند برنامه‌های متداول دسکتاپ، Context مورد استفاده در طول عمر صفحه‌ی جاری زنده نگه داشته می‌شود. در این حالت اگر شیءایی اضافه شود، حذف شود یا تغییر کند، توسط EF ردیابی شده و تنها با فراخوانی متد SaveChanges، تمام این تغییرات به صورت یکجا به بانک اطلاعاتی اعمال می‌شوند.
در حالت غیرمتصل مانند برنامه‌های وب، طول عمر Context در حد طول عمر یک درخواست است. پس از آن از بین خواهد رفت و دیگر فرصت ردیابی تغییرات سمت کاربر را نخواهد یافت. در این حالت به روز رسانی کلیه تغییرات انجام شده در خواص و همچنین ارتباطات اشیاء موجود، کاری مشکل و زمانبر خواهد بود.
برای حل این مشکل، کتابخانه‌ای به نام GraphDiff طراحی شده‌است که صرفا با فراخوانی متد UpdateGraph آن، به صورت خودکار، محاسبات تغییرات صورت گرفته در اشیاء منقطع و اعمال آن‌ها به بانک اطلاعاتی صورت خواهد گرفت. البته ذکر متد SaveChanges پس از آن نباید فراموش شود.


اصطلاحات بکار رفته در GraphDiff

برای کار با GraphDiff نیاز است با یک سری اصطلاح آشنا بود:

Aggregate root
گرافی است از اشیاء به هم وابسته که مرجع تغییرات داده‌ها به شمار می‌رود. برای مثال یک سفارش و آیتم‌های آن‌را درنظر بگیرید. بارگذاری آیتم‌های سفارش، بدون سفارش معنایی ندارند. بنابراین در اینجا سفارش aggregate root است.

AssociatedCollection/AssociatedEntity
حالت‌های Associated به GraphDiff اعلام می‌کنند که اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root نباید به روز رسانی شوند. در این حالت تنها ارجاعات به روز رسانی خواهند شد.
اگر خاصیت راهبری از نوع ICollection است، حالت AssociatedCollection و اگر صرفا یک شیء ساده است، از AssociatedEntity استفاده خواهد شد.

OwnedCollection/OwnedEntity
حالت‌های Owned به GraphDiff اعلام می‌کنند که جزئیات و همچنین ارجاعات اینگونه خواص راهبری تعریف شده، در حین به روز رسانی aggregate root باید به روز رسانی شوند.


دریافت و نصب GraphDiff

برای نصب خودکار کتابخانه‌ی GraphDiff می‌توان از دستور نیوگت ذیل استفاده کرد:
 PM> Install-Package RefactorThis.GraphDiff


بررسی GraphDiff در طی یک مثال

مدل‌های برنامه آزمایشی، از سه کلاس ذیل که روابط many-to-many و one-to-many با یکدیگر دارند، تشکیل شده‌است:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace GraphDiffTests.Models
{
    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        public virtual ICollection<Tag> Tags { set; get; } // many-to-many

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }

        public BlogPost()
        {
            Tags = new List<Tag>();
        }
    }

    public class Tag
    {
        public int Id { set; get; }

        [StringLength(maximumLength: 450), Required]
        public string Name { set; get; }

        public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many

        public Tag()
        {
            BlogPosts = new List<BlogPost>();
        }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; } // one-to-many
    }
}
- یک مطلب می‌تواند چندین برچسب داشته باشد و هر برچسب می‌تواند به چندین مطلب انتساب داده شود.
- هر کاربر می‌تواند چندین مطلب ارسال کند.

در این حالت، Context برنامه چنین شکلی را خواهد یافت:
using System;
using System.Data.Entity;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }
        public DbSet<Tag> Tags { get; set; }


        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
}
به همراه تنظیمات به روز رسانی ساختار بانک اطلاعاتی به صورت خودکار:
using System.Data.Entity.Migrations;
using System.Linq;
using GraphDiffTests.Models;

namespace GraphDiffTests.Config
{
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            if(context.Users.Any())
                return;

            var user1 = new User {Name = "User 1"};
            context.Users.Add(user1);

            var tag1 = new Tag { Name = "Tag1" };
            context.Tags.Add(tag1);

            var post1 = new BlogPost { Title = "Title...1", Content = "Content...1", User = user1};
            context.BlogPosts.Add(post1);

            post1.Tags.Add(tag1);

            base.Seed(context);
        }
    }
}
در متد Seed آن یک سری اطلاعات ابتدایی ثبت شده‌اند؛ یک کاربر، یک برچسب و یک مطلب.




در این تصاویر به Id هر کدام از رکوردها دقت کنید. از آن‌ها در ادامه استفاده خواهیم کرد.
در اینجا نمونه‌ای از نحوه‌ی استفاده از GraphDiff را جهت به روز رسانی یک Aggregate root ملاحظه می‌کنید:
            using (var context = new MyContext())
            {
                var user1 = new User { Id = 1, Name = "User 1_1_1" };
                var post1 = new BlogPost { Id = 1, Title = "Title...1_1", Content = "Body...1_1",
                    User = user1, UserId = user1.Id };
                var tags = new List<Tag>
                {
                    new Tag {Id = 1, Name = "Tag1_1"},
                    new Tag {Id=12, Name = "Tag2_1"},
                    new Tag {Name = "Tag3"},
                    new Tag {Name = "Tag4"},
                };
                tags.ForEach(tag => post1.Tags.Add(tag));

                context.UpdateGraph(post1, map => map
                    .OwnedEntity(p => p.User)
                    .OwnedCollection(p => p.Tags)
                    );

                context.SaveChanges();
            }
پارامتر اول UpdateGraph، گرافی از اشیاء است که قرار است به روز رسانی شوند.
پارامتر دوم آن، همان مباحث Owned و Associated بحث شده در ابتدای مطلب را مشخص می‌کنند. در اینجا چون می‌خواهیم هم برچسب‌ها و هم اطلاعات کاربر مطلب اول به روز شوند، نوع رابطه را Owned تعریف کرده‌ایم.
در حین کار با متد UpdateGraph، ذکر Idهای اشیاء منقطع از Context بسیار مهم هستند. اگر دستورات فوق را اجرا کنیم به خروجی ذیل خواهیم رسید:




- همانطور که مشخص است، چون id کاربر ذکر شده و همچنین این Id در post1 نیز درج گردیده است، صرفا نام او ویرایش گردیده است. اگر یکی از موارد ذکر شده رعایت نشوند، ابتدا کاربر جدیدی ثبت شده و سپس رابطه‌ی مطلب و کاربر به روز رسانی خواهد شد (userId آن به userId آخرین کاربر ثبت شده تنظیم می‌شود).
- در حین ثبت برچسب‌ها، چون Id=1 از پیش در بانک اطلاعاتی موجود بوده، تنها نام آن ویرایش شده‌است. در سایر موارد، برچسب‌های تعریف شده صرفا اضافه شده‌اند (چون Id مشخصی ندارند یا Id=12 در بانک اطلاعاتی وجود خارجی ندارد).
- چون Id مطلب مشخص شده‌است، فیلدهای عنوان و محتوای آن نیز به صورت خودکار ویرایش شده‌اند.

و ... تمام این کارها صرفا با فراخوانی متدهای UpdateGraph و سپس SaveChanges رخ داده‌است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
GraphDiffTests.zip
مطالب
آشنایی با کلیدهای کنترلی و کاربرد آنها
کلید‌ها یا کاراکترهای کنترلی  که در ویکی پدیای فارسی به نویسه‌های کنترلی ترجمه شده اند تنها یک خط تعریف دارند:
یک کاراکتر کنترلی، یک نقطه کدی است که به وسیله علائم نوشتاری قابل نمایش نباشد. مانند  Backspace

تعریف بالا به ما می‌گوید که در یک متن نوشتاری، به غیر از کد حروفی که مشاهده می‌کنیم، کدهای دیگری هم هستن که قابل نمایش نیستند ولی بین متون وجود دارند. شاید شما تعدادی از آن‌ها را بشناسید مثل کدهای 10 و 13 برای خط بعد و اول سطر که به line feed و carriage return معروف هستند. در این نوشتار قصد داریم با تعدادی از آنها آشنا شویم.

قبل از آغاز این نوشتار به شما توصیه می‌کنم یک نگاه اجمالی هم که شده بر نوشتار «داستانی از unicode» داشته باشید تا اطلاعات تکمیلی‌تری از این نوشتار به دست آورید. مبحث کلیدهای کنترلی از زمانی آغاز شد که کدهای اسکی ایجاد شدند و به دو دسته‌ی c0 و c1 تقسیم شدند. خود کدهای اسکی هم بر اساس کدهای تلگراف ایجاد شدند و بسیاری از کلید‌های کنترلی هم از آنجا به استاندارد اسکی پیوستند و برای ارتباط و کنترل دستگاه‌هایی چون چاپگرها و تهیه اطلاعات متا در مورد طلاعاتی که قرار بود در نوار مغناطیسی ذخیره شوند به کار رفتند. به عنوان نمونه کد 10 به عنوان line feed در چاپگر، یک خط  کاغذ را به سمت داخل می‌کشید و کد 13 هم باعث می‌شد چاپگر به ابتدای سطر بازگردد. البته بیشتر این کاراکترها در پردازش متون به خصوص امروزه استفاده نمی‌شوند و فقط یک سری از آن‌ها رایج هستند؛ مثل دو موردی که در بالا و در همین خط به آن‌ها اشاره شد.
دسته‌ی c0 از کد 0 آغاز شده و تا کد 31 ادامه می‌یابد. دو کد بعدی که کدهای Space و DEL هستند در هیچ گروهی قرار نمی‌گیرند. گروه c1 از کدهای 128 آغاز شده و تا 159 ادامه می‌یابند که جدول این گروه‌ها و کلید‌ها کنترلی را می‌توانید مشاهده کنید. برای مثال اولین کلید کنترلی که کد آن 0 است به نام نال است که در قدیم هم برای بستن رشته‌ها در زبان سی از آن استفاده می‌کردیم.
 هر چند به مرور زمان هم تعدادی از همین کلیدهای کنترلی کاربرد خود را از دست دادند و برای آن‌ها شکلک هایی چون خنده، قلب، نت موسیقی و ... را قرار دادند ولی گاهی اوقات برنامه نویس‌ها هنوز در برنامه‌های خود از کد آن‌ها برای کارهایی چون انجام عملیات بیتی استفاده می‌کنند.

استفاده‌های C0
کلید‌های کنترلی این دسته بیشتر برای منظم ساختن متن‌های ساده و همچنین ایجاد ارتباط در پروتکل ارتباطی و دستگاه‌های مختلف به کار می‌رفت؛ ارسال فرمان‌هایی چون آغاز و توقف کار یا انجام عملی خاص توسط هر یک از این کلیدها صورت می‌گرفت. دستگاه‌هایی چون کارت پانچ‌ها، ماشین تایپ و موارد مشابه، از این نوع هستند. با اینکه عمر این دستگاه به سر آمد ولی کلیدهای کنترلی جان سالم به در بردند.

استفاده‌های C1
این دسته در اواخر سال 1970 اضافه شدند و بیشتر برای ارتباط با چاپگر و صفحه‌ی نمایش به کار می‌رفتند؛ مثل پیمایش‌های افقی و عمودی، تعریف ناحیه‌ای برای پر کردن فرم و Line-Break و کلیدهای انتقالی (شیفت) برای پشتیبانی از کلیدهای کنترلی و قابل چاپ بیشتر. 2 تا از کلیدها هم برای استفاده‌ی خصوصی برنامه نویس کنار گذاشته شدند و 4 تا هم رزرو شده برای استفاده‌ی آینده، تا بعدا استانداردسازی شوند.


کلیدهای کنترلی در سی شارپ
بسیاری از ما از علامت \ در کدهایمان برای قرار دادن کلید‌های کنترلی استفاده می‌کنیم مثل r\n\ که ترکیب دو کد CR و LF است.

برای شناسایی یک کلید کنترلی در سی شارپ از متد ایستای Char.IsControl استفاده می‌نماییم. کد زیر در مجموعه‌ی MSDN برای نشان دادن قابلیت این متد نوشته شده است که در طی یک حلقه رنجی از کد پوینت‌ها را بررسی کرده و نتیجه را به صورت شش ستونی در کنسول نمایش می‌دهد. یا کد مشابه دیگر که بر اساس دسیمال نمایش می‌دهد.
using System;

public class ControlChars
{
   public static void Main()
   {
      int charsWritten = 0;

      for (int ctr = 0x00; ctr <= 0xFFFF; ctr++)
      {
         char ch = Convert.ToChar(ctr);
         if (char.IsControl(ch))
         {
            Console.Write(@"\U{0:X4}    ", ctr);
            charsWritten++;
            if (charsWritten % 6 == 0)
               Console.WriteLine();
         }     
      }  
   }
}
// The example displays the following output to the console: 
//       \U0000    \U0001    \U0002    \U0003    \U0004    \U0005 
//       \U0006    \U0007    \U0008    \U0009    \U000A    \U000B 
//       \U000C    \U000D    \U000E    \U000F    \U0010    \U0011 
//       \U0012    \U0013    \U0014    \U0015    \U0016    \U0017 
//       \U0018    \U0019    \U001A    \U001B    \U001C    \U001D 
//       \U001E    \U001F    \U007F    \U0080    \U0081    \U0082 
//       \U0083    \U0084    \U0085    \U0086    \U0087    \U0088 
//       \U0089    \U008A    \U008B    \U008C    \U008D    \U008E 
//       \U008F    \U0090    \U0091    \U0092    \U0093    \U0094 
//       \U0095    \U0096    \U0097    \U0098    \U0099    \U009A 
//       \U009B    \U009C    \U009D    \U009E    \U009F

آیا هنوز برنامه نویس‌ها از کلیدهای کنترلی استفاده می‌کنند؟
این سوال بستگی به برنامه‌ای دارد که شما می‌نویسید. باید گفت هنوز بسیاری از آن‌ها در بسیاری از برنامه‌ها استفاده می‌شوند. مانند بعضی از درایور‌ها برای ارسال اطلاعات به سمت یک قطعه یا دستگاه یا حتی از شما می‌خواهند برنامه‌ای بنویسید که با دستگاه‌های قدیمی ارتباط برقرار کند. برنامه‌هایی که نیاز به کار با رشته‌ها دارند و ...

لیست زیر مشخص می‌کند که کدامیک از کلیدهای کنترلی تا چه اندازه امروزه توسط برنامه نویسان استفاده می‌شوند.
 Null استفاده روزمره‌ای از آن در همه‌ی برنامه‌ها وجود دارد و نیاز به معرفی ندارد.
 Transmission Control
 این کلید‌ها که 10 عدد هستند شامل SOH , ACK , DLE , ENQ , EOT , ETB , ETX , Nak , STX , SYN هستند. کاربردشان در انتقال اطلاعات بود ولی امروزه استفاده از آن‌ها به شدت کم شده است و انتقال داده‌ها با سوکت TCP/IP و HTTP و FTP و دیگر پروتکول‌ها به سرانجام رسید و گاها برای بعضی کاربردهای ویژه استفاده می‌شوند.
 BEL  این مورد واقعا کاربردش را از دست داده است. وظیفه قبلی‌اش ارسال یه هشدار یا یک زنگ اخطار به کاربر بود. مثلا برای اینکه ماشین تایپ به کاربر هشدار بدهد به آخر خط رسیده است، یک کد BELL به سمت آن ارسال می‌کرد.
 Format Effectors
 کدهای این دسته عبارتند از BS , CR , FF , HT , HTJ , HTS , IND , LF , NEL , PLD , PLU , RI , VT , VTS  هستند که احتمالا مهمترین کدهایی هستند که امروزه از آن‌ها استفاده می‌شود. کاربردشان در فرمت بندی یا قالب بندی متون نوشته شده یا همان کلید‌های قابل چاپ می‌باشد. CR و LF که همیشه معرف حضور ما هستند و بودنشان در سیستم یک امر حیاتی است. HT که همان tab است. BS که همان Backspace است. FF و VT هم که امروزه به ندرت استفاده می‌شوند.
Device Control هنوز برای ارتباط با دستگاه‌های مختلف مثل کار با پورتها استفاده می‌شوند. کلیدهای معروف آن DC1 و DC3 هستند که به XON و XOFF هم شناخته می‌شوند. یکی از کاربردهای آن.
 SUB  یک نماد جایگزین که استفاده‌ی خود را از دست داده است. موقعیکه نمادی نامعتبر بود یا خطایی رخ می‌داد، این نماد جایگزین آن می‌شد. امروزه بیشتر از علامت ؟ در متون استفاده می‌شود. در یک صفحه کلید استاندارد این کد توسط فشرده شدن Ctrl+Z ارسال می‌شود.
 CAN , EM  کاربردی امروزه ندارد. CAN برای کنترل خطا به کار می‌رفت و EM در نوارهای مغناطیسی.
 Information Separators
 شامل 4 کلید FS ,GS , RS و US می‌شود که برای جداسازی داده‌ها از یکدیگر به کار می‌روند؛ ولی به‌خاطر جایگزینی آن‌ها با اسنادی مثل XML یا دیتابیس‌ها، استفاده از آن‌ها تا حدودی به پایان رسیده است.
 SP همان کلید space است که نیاز به معرفی ندارد و کارش گویای همه چیز هست.
 DELL  همان کلید Delete است.
 NBSP این کلید همان کاراکتر ;nbsp& است که در کدهای HTML استفاده می‌شود.
 SHY علامت - یا Hyphen است که به شدت استفاده از آن کم شده است.

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