نظرات مطالب
فارسی نویسی و iTextSharp
- به تصویر آخر و کدهای آن دقت کنید. کلمه آزمایش از سمت راست شروع شده.
- DirectionR2L کاربردی ندارد در اینجا. PdfWriter.RUN_DIRECTION_RTL باید باشد.
مطالب
ایجاد ایندکس منحصربفرد در EF Code first
در EF Code first برای ایجاد UNIQUE INDEX ویژگی یا تنظیمات Fluent API خاصی درنظر گرفته نشده است و می‌توان از همان روش‌های متداول اجرای مستقیم کوئری SQL بر روی بانک اطلاعاتی جهت ایجاد UNIQUE INDEX‌ها کمک گرفت:
public static void CreateUniqueIndex(this DbContext context, string tableName, string fieldName)
{
      context.Database.ExecuteSqlCommand("CREATE UNIQUE INDEX [IX_Unique_" + tableName 
+ "_" + fieldName + "] ON [" + tableName + "]([" + fieldName + "] ASC);");
}
و این کل کاری است که باید در متد Seed کلاس مشتق شده از DbMigrationsConfiguration انجام شود. مثلا:
public class MyDbMigrationsConfiguration : DbMigrationsConfiguration<MyContext>
{
        public BlogDbMigrationsConfiguration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            CreateUniqueIndex(context, "table1", "field1");
            base.Seed(context);
        }
}
روش فوق کار می‌کند اما آنچنان مناسب نیست؛ چون به صورت strongly typed تعریف نشده است و اگر نام جداول یا فیلدها را بعدها تغییر دادیم، به مشکل برخواهیم خورد و کامپایلر خطایی را صادر نخواهد کرد؛ چون table1 و field1 در اینجا به صورت رشته تعریف شده‌اند.
برای حل این مشکل و تبدیل کدهای فوق به کدهایی Refactoring friendly، نیاز است نام جدول به صورت خودکار از DbContext دریافت شود. همچنین نام خاصیت یا فیلد نیز به صورت strongly typed قابل تعریف باشد.
کدهای کامل نمونه بهبود یافته را در ذیل مشاهده می‌کنید:
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;

namespace General
{
    public static class ContextExtensions
    {
        public static string GetTableName<T>(this DbContext context) where T : class
        {
            ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
            return objectContext.GetTableName<T>();
        }

        public static string GetTableName<T>(this ObjectContext context) where T : class
        {
            var sql = context.CreateObjectSet<T>().ToTraceString();
            var regex = new Regex("FROM (?<table>.*) AS");
            var match = regex.Match(sql);
            string table = match.Groups["table"].Value;
            return table
                    .Replace("`", string.Empty)
                    .Replace("[", string.Empty)
                    .Replace("]", string.Empty)
                    .Replace("dbo.", string.Empty)
                    .Trim();
        }

        private static bool hasUniqueIndex(this DbContext context, string tableName, string indexName)
        {
            var sql = "SELECT count(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '" 
                      + tableName + "' and CONSTRAINT_NAME = '" + indexName + "'";
            var result = context.Database.SqlQuery<int>(sql).FirstOrDefault();
            return result > 0;
        }

        private static void createUniqueIndex(this DbContext context, string tableName, string fieldName)
        {
            var indexName = "IX_Unique_" + tableName + "_" + fieldName;
            if (hasUniqueIndex(context, tableName, indexName))
                return;

            var sql = "ALTER TABLE [" + tableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + fieldName + "])";
            context.Database.ExecuteSqlCommand(sql);
        }

        public static void CreateUniqueIndex<TEntity>(this DbContext context, Expression<Func<TEntity, object>> fieldName) where TEntity : class
        {
            var field = ((MemberExpression)fieldName.Body).Member.Name;
            createUniqueIndex(context, context.GetTableName<TEntity>(), field);
        }
    }
}

توضیحات
متد GetTableName ، به کمک SQL تولیدی حین تعریف جدول متناظر با کلاس جاری، نام جدول را با استفاده از عبارات باقاعده جدا کرده و باز می‌گرداند. به این ترتیب به دقیق‌ترین نامی که واقعا جهت تولید جدول مورد استفاده قرار گرفته است خواهیم رسید.
در مرحله بعد آن، همان متد createUniqueIndex ابتدای بحث را ملاحظه می‌کنید. در اینجا جهت حفظ سازگاری بین SQL Server کامل و SQL CE از UNIQUE CONSTRAINT استفاده شده است که همان کار ایجاد ایندکس منحصربفرد را نیز انجام می‌دهد. به علاوه مزیت دیگر آن امکان دسترسی به تعاریف قید اضافه شده توسط view ایی به نام INFORMATION_SCHEMA.TABLE_CONSTRAINTS است که در نگارش‌های مختلف SQL Server به یک نحو تعریف گردیده و قابل دسترسی است. از این view در متد hasUniqueIndex جهت بررسی تکراری نبودن UNIQUE CONSTRAINT در حال تعریف، استفاده می‌شود. اگر این قید پیشتر تعریف شده باشد، دیگر سعی در تعریف مجدد آن نخواهد شد.
متد CreateUniqueIndex تعریف شده در انتهای کلاس فوق، امکان دریافت نام خاصیتی از TEntity را به صورت strongly typed میسر می‌سازد.
اینبار برای تعریف یک قید و یا ایندکس منحصربفرد بر روی خاصیتی مشخص در متد Seed، تنها کافی است بنویسیم:
context.CreateUniqueIndex<User>(x=>x.Name);
در اینجا دیگر از رشته‌ها خبری نبوده و حاصل نهایی Refactoring friendly است. به علاوه نام جدول را نیز به صورت خودکار از context استخراج می‌کند.
 
مطالب
نحوه‌ی محاسبه‌ی هش کلمات عبور کاربران در ASP.NET Identity
روش‌های زیادی برای ذخیره سازی کلمات عبور وجود دارند که اغلب آن‌ها نیز نادرست هستند. برای نمونه شاید ذخیره سازی کلمات عبور، به صورت رمزنگاری شده، ایده‌ی خوبی به نظر برسد؛ اما با دسترسی به این کلمات عبور، امکان رمزگشایی آن‌ها، توسط مهاجم وجود داشته و همین مساله می‌تواند امنیت افرادی را که در چندین سایت، از یک کلمه‌ی عبور استفاده می‌کنند، به خطر اندازد.
در این حالت هش کردن کلمات عبور ایده‌ی بهتر است. هش‌ها روش‌هایی یک طرفه هستند که با داشتن نتیجه‌ی نهایی آن‌ها، نمی‌توان به اصل کلمه‌ی عبور مورد استفاده دسترسی پیدا کرد. برای بهبود امنیت هش‌های تولیدی، می‌توان از مفهومی به نام Salt نیز استفاده نمود. Salt در اصل یک رشته‌ی تصادفی است که پیش از هش شدن نهایی کلمه‌ی عبور، به آن اضافه شده و سپس حاصل این جمع، هش خواهد شد. اهمیت این مساله در بالا بردن زمان یافتن کلمه‌ی عبور اصلی از روی هش نهایی است (توسط روش‌هایی مانند brute force یا امتحان کردن بازه‌ی وسیعی از عبارات قابل تصور).
اما واقعیت این است که حتی استفاده از یک Salt نیز نمی‌تواند امنیت بازیابی کلمات عبور هش شده را تضمین کند. برای مثال نرم افزارهایی موجود هستند که با استفاده از پرداش موازی قادرند بیش از 60 میلیارد هش را در یک ثانیه آزمایش کنند و البته این کارآیی، برای کار با هش‌های متداولی مانند MD5 و SHA1 بهینه سازی شده‌است.


روش هش کردن کلمات عبور در ASP.NET Identity

ASP.NET Identity 2.x که در حال حاضر آخرین نگارش تکامل یافته‌ی روش‌های امنیتی توصیه شده‌ی توسط مایکروسافت، برای برنامه‌های وب است، از استانداردی به نام RFC 2898 و الگوریتم PKDBF2 برای هش کردن کلمات عبور استفاده می‌کند. مهم‌ترین مزیت این روش خاص، کندتر شدن الگوریتم آن با بالا رفتن تعداد سعی‌های ممکن است؛ برخلاف الگوریتم‌هایی مانند MD5 یا SHA1 که اساسا برای رسیدن به نتیجه، در کمترین زمان ممکن طراحی شده‌اند.
PBKDF2 یا password-based key derivation function جزئی از استاندارد RSA نیز هست (PKCS #5 version 2.0). در این الگوریتم، تعداد بار تکرار، یک Salt و یک کلمه‌ی عبور تصادفی جهت بالا بردن انتروپی (بی‌نظمی) کلمه‌ی عبور اصلی، به آن اضافه می‌شوند. از تعداد بار تکرار برای تکرار الگوریتم هش کردن اطلاعات، به تعداد باری که مشخص شده‌است، استفاده می‌گردد. همین تکرار است که سبب کندشدن محاسبه‌ی هش می‌گردد. عدد معمولی که برای این حالت توصیه شده‌است، 50 هزار است.
این استاندارد در دات نت توسط کلاس Rfc2898DeriveBytes پیاده سازی شده‌است که در ذیل مثالی را در مورد نحوه‌ی استفاده‌ی عمومی از آن، مشاهده می‌کنید:
using System;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
 
namespace IdentityHash
{
    public static class PBKDF2
    {
        public static byte[] GenerateSalt()
        {
            using (var randomNumberGenerator = new RNGCryptoServiceProvider())
            {
                var randomNumber = new byte[32];
                randomNumberGenerator.GetBytes(randomNumber);
                return randomNumber;
            }
        }
 
        public static byte[] HashPassword(byte[] toBeHashed, byte[] salt, int numberOfRounds)
        {
            using (var rfc2898 = new Rfc2898DeriveBytes(toBeHashed, salt, numberOfRounds))
            {
                return rfc2898.GetBytes(32);
 
            }
        }
    }
 
    class Program
    {
        static void Main(string[] args)
        {
            var passwordToHash = "VeryComplexPassword";
            hashPassword(passwordToHash, 50000);
            Console.ReadLine();
        }
 
        private static void hashPassword(string passwordToHash, int numberOfRounds)
        {
            var sw = new Stopwatch();
            sw.Start();
            var hashedPassword = PBKDF2.HashPassword(
                                        Encoding.UTF8.GetBytes(passwordToHash),
                                        PBKDF2.GenerateSalt(),
                                        numberOfRounds);
            sw.Stop();
            Console.WriteLine();
            Console.WriteLine("Password to hash : {0}", passwordToHash);
            Console.WriteLine("Hashed Password : {0}", Convert.ToBase64String(hashedPassword));
            Console.WriteLine("Iterations <{0}> Elapsed Time : {1}ms", numberOfRounds, sw.ElapsedMilliseconds);
        }
    }
}
شیء Rfc2898DeriveBytes برای تشکیل، نیاز به کلمه‌ی عبوری که قرار است هش شود به صورت آرایه‌ای از بایت‌ها، یک Salt و یک عدد اتفاقی دارد. این Salt توسط شیء RNGCryptoServiceProvider ایجاد شده‌است و همچنین نیازی نیست تا به صورت مخفی نگه‌داری شود. آن‌را  می‌توان در فیلدی مجزا، در کنار کلمه‌ی عبور اصلی ذخیره سازی کرد. نتیجه‌ی نهایی، توسط متد rfc2898.GetBytes دریافت می‌گردد. پارامتر 32 آن به معنای 256 بیت بودن اندازه‌ی هش تولیدی است. 32 حداقل مقداری است که بهتر است انتخاب شود.
پیش فرض‌های پیاده سازی Rfc2898DeriveBytes استفاده از الگوریتم SHA1 با 1000 بار تکرار است؛ چیزی که دقیقا در ASP.NET Identity 2.x بکار رفته‌است.


تفاوت‌های الگوریتم‌های هش کردن اطلاعات در نگارش‌های مختلف ASP.NET Identity

اگر به سورس نگارش سوم ASP.NET Identity مراجعه کنیم، یک چنین کامنتی در ابتدای آن قابل مشاهده است:
 /* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 2:
* PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
* (See also: SDL crypto guidelines v5.1, Part III)
* Format: { 0x00, salt, subkey }
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
در نگارش دوم آن از الگوریتم PBKDF2 با هزار بار تکرار و در نگارش سوم با 10 هزار بار تکرار، استفاده شده‌است. در این بین، الگوریتم پیش فرض HMAC-SHA1 نگارش‌های 2 نیز به HMAC-SHA256 در نگارش 3، تغییر کرده‌است.
در یک چنین حالتی بانک اطلاعاتی ASP.NET Identity 2.x شما با نگارش بعدی سازگار نخواهد بود و تمام کلمات عبور آن باید مجددا ریست شده و مطابق فرمت جدید هش شوند. بنابراین امکان انتخاب الگوریتم هش کردن را نیز پیش بینی کرده‌اند.

در نگارش دوم ASP.NET Identity، متد هش کردن یک کلمه‌ی عبور، چنین شکلی را دارد:
public static string HashPassword(string password, int numberOfRounds = 1000)
{
    if (password == null)
        throw new ArgumentNullException("password");
 
    byte[] saltBytes;
    byte[] hashedPasswordBytes;
    using (var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, 16, numberOfRounds))
    {
        saltBytes = rfc2898DeriveBytes.Salt;
        hashedPasswordBytes = rfc2898DeriveBytes.GetBytes(32);
    }
    var outArray = new byte[49];
    Buffer.BlockCopy(saltBytes, 0, outArray, 1, 16);
    Buffer.BlockCopy(hashedPasswordBytes, 0, outArray, 17, 32);
    return Convert.ToBase64String(outArray);
}
تفاوت این روش با مثال ابتدای بحث، مشخص کردن طول salt در متد Rfc2898DeriveBytes است؛ بجای محاسبه‌ی اولیه‌ی آن. در این حالت متد Rfc2898DeriveBytes مقدار salt را به صورت خودکار محاسبه می‌کند. این salt بجای ذخیره شدن در یک فیلد جداگانه، به ابتدای مقدار هش شده اضافه گردیده و به صورت یک رشته‌ی base64 ذخیره می‌شود. در نگارش سوم، از کلاس ویژه‌ی RandomNumberGenerator برای محاسبه‌ی Salt استفاده شده‌است.
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای 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 Core
نکته تکمیلی:
در صورت استفاده از mailkit در محیط net core. میتوان کلاس SMTPClient را به شکل زیر مورد استفاده قرار داد.
 public class DiskSmtpClient : SmtpClient
    {
        public DiskSmtpClient(IOptionsSnapshot<MailKitOptions> mailOptionsSnapshot)
        {
            if (mailOptionsSnapshot.Value.SpecifiedPickupDirectory)
            {
                SpecifiedPickupDirectory = true;
                PickupDirectoryLocation = mailOptionsSnapshot.Value.PickupDirectoryLocation;
            }
            
        }
        public bool SpecifiedPickupDirectory { get; set; }
        public string PickupDirectoryLocation { get; set; }

        public override Task SendAsync(MimeMessage message, CancellationToken cancellationToken = new CancellationToken(),
            ITransferProgress progress = null)
        {
            if (!SpecifiedPickupDirectory)
                return base.SendAsync(message, cancellationToken, progress);
            return SaveToPickupDirectory(message, PickupDirectoryLocation);

        }

    



        private async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory)
        {
            using (var stream = new FileStream($@"{pickupDirectory}\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
            {
                await message.WriteToAsync(stream);
            }
        }

       

        public override Task ConnectAsync(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto,
            CancellationToken cancellationToken = new CancellationToken())
        {
            if (!SpecifiedPickupDirectory)
                return base.ConnectAsync(host, port, options, cancellationToken);
            return Task.CompletedTask;
        }



        public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = new CancellationToken())
        {
            if (!SpecifiedPickupDirectory)
                return base.DisconnectAsync(quit, cancellationToken);

            return Task.CompletedTask;
        }
    }
در کد بالا دو خصوصیت SpecifiedPickupDirectory  و PickupDirectoryLocation به آن اضافه شده اند و با رونویسی از متدهای مورد استفاده به جای ارسال ایمیل در صورت مقداردهی  SpecifiedPickupDirectory  ایمیل در آدرس  PickupDirectoryLocation   ذخیره میگردد. سپس به شکل زیر آن را مورد استفاده قرار میدهیم:
 services.AddTransient<DiskSmtpClient>();
  var email = "mail@dotnettips.info";
            var subject = "subject";
            var message = "message";

            var emailMessage = new MimeMessage();

            emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(TextFormat.Html)
            {
                Text = message
            };

            
                _client.SpecifiedPickupDirectory = true;
                _client.PickupDirectoryLocation = "c:\\mail";

                _client.LocalDomain = "dotnettips.info";
                await _client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                await _client.SendAsync(emailMessage).ConfigureAwait(false);
                await _client.DisconnectAsync(true).ConfigureAwait(false);
در صورتی که قصد ندارید کد اضافه‌تری را نیز اعمال نمایید میتوانید با اضافه کردن تکه کد زیر به فایل startup و محتوای تنظیمات آن به فایل appsettings.json دو خط بالا را حذف نمایید:
services.Configure<MailKitOptions>(options => Configuration.GetSection("MailKitOptions").Bind(options));
  "MailKitOptions": {
    "SpecifiedPickupDirectory": true,
    "PickupDirectoryLocation": "c:\\mail"
  }
در این صورت میتوان تنظیمات جداگانه ای برای حالت انتشار و توسعه نیز در نظر گرفت.
کلاس متناظر MailKitOptions
    public class MailKitOptions
    {
        public  bool SpecifiedPickupDirectory { get; set; }
        public  string PickupDirectoryLocation { get; set; }
    }

مطالب
روش‌های مختلف انجام چند کار به صورت همزمان در C# .NET - قسمت دوم
در قسمت قبل دیدیم که انجام کارهای همزمان، با Objectهایی که به اصطلاح Thread Safe نیستند (مانند DbContext) خروجی چندان جالبی ندارد و برای مثال اگر در یک Service یک DbContext را Inject کنیم (مثلا با Constructor injection) و از آن در متدی استفاده کنیم که آن متد یا با TPL یا RX و ... به صورت چندتایی و همزمان اجرا شود، DbContext به مشکل می‌خورد؛ یعنی نمی‌توان یک وهله از DbContext را بین چند Thread همزمان پردازش موازی، به اشتراک گذاشت.
در کدهای فرضی مثال‌های قسمت قبل، متدی داشتیم با نام DoSomethingWithCustomer که مثلا همان متدی بود که قرار است همزمان اجرا شود. یکی از ساده‌ترین کارهایی که برای رفع این مشکل می‌توان انجام داد، نوشتن چنین کدی است:
public async Task DoSomethingWithCustomer(Customer customer)
{
    using var dbContext = new AppDbContext();

    // ...
}
در این حالت، اگر متد DoSomethingWithCustomer به صورت همزمان اجرا شود، به ازای هر بار اجرا، یک DbContext جدید ساخته میشود؛ پس از یک DbContext مشترک، به صورت همزمان توسط چندین Thread پردازش موازی، استفاده نخواهد شد. ولی مشکل اینجاست که از Dependency Injection برای ساختن DbContext استفاده نشده‌است و ما خودمان، هم new کرده‌ایم و هم Dispose!

این روش ابدا توصیه نمی‌شود؛ برای اینکه Dependency Injection این روزها مسائل خیلی زیادی را مدیریت می‌کند. مثلا DbContext در EF Core وقتی با Dependency Injection ساخته شود، Logging اش هم فعال می‌شود و یا مثلا اگر از متد زیر
services.AddDbContextPool<AppDbContext>();
برای Register کردن DbContext استفاده کرده باشیم، تعدادی DbContext ساخته می‌شود و در پروژه به صورت امن از همان چندتا استفاده می‌شود؛ بجای اینکه Objectها مدام ساخته و از بین برده شوند که این مهم، روی کارآیی تاثیر گذار است.
و یا مثلا برای HttpClient فقط در همین سایت نزدیک به یک دوجین مقاله توضیح داده‌اند که چرا new کردن و Dispose کردن HTTP Client مناسب نیست و بهتر است برای Register کردن HttpClient، از services.AddHttpClient استفاده و از IHttpClientFactory و سایر روشها برای Resolve کردن HttpClient استفاده کنیم که اینها نیز توسط Dependency Injection قابل استفاده هستند. از مسائلی مانند Polly و ... نیز می‌گذریم.
راه حل ایجاد یک Context جدید، تقریبا در تمامی کتابخانه‌های Dependency Injection دیده شده‌است و آن ساختن یک Child Scope است. در ادامه با Microsoft.Extensions.DependencyInjection یک پیاده سازی آن‌را خواهیم داشت؛ ولی مشابه این روش در سایر کتابخانه‌ها همچون Autofac نیز شدنی است.

برای شروع System.IServiceProvider را inject کنید. سپس کد قبلی را به این شکل بنویسید:
(نیاز به ;using Microsoft.Extensions.DependencyInjection در بالای فایل کد است)
public async Task DoSomethingWithCustomer(Customer customer)
{
    using var scope = _serviceProvider.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    var httpClient = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient();
    // ...
}
در این روش فقط نیاز به using نوشتن برای خط اول است؛ یعنی scope که Dispose شود، Objectهایی که به وسیله آن scope ساخته شده‌اند، آزاد خواهند شد. تمام آن چیزهایی را که قبلا با Constructor یا Property injection می‌توانستید بگیرید را اکنون می‌توانید با متد GetRequiredService بگیرید.
همچنین می‌توانید برای داشتن کدی بهتر، یک interface و class را ایجاد کنید و logic مربوطه را در آن سرویس قرار دهید و در آن سرویس با constructor یا property injection از DbContext و HttpClient و سایر سرویس‌ها استفاده کنید و در نهایت آن interface/class را رجیستر کنید و در DoSomethingWithCustomer به کمک child scope، یک object از آن سرویس بسازید و متدش را فراخوانی کنید. برای مثال اگر هدف ساختن Excel تاریخچه خریدهای مشتری است، داریم:
public interface IOrderHistoryService
{
    Task BuildCustomerHistory();
}

public class OrderHistoryService : IOrderHistoryService
{
    private AppDbContext _dbContext;

    public OrderHistoryService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task BuildCustomerHistory()
    {
        // ...
    }
}
سپس اگر جایی لازم شد روی لیستی از مشتری‌ها این متد اجرا شود، از TPL استفاده می‌کنیم و متدی را فراخوانی می‌کنیم که در آن child scope می‌سازیم و scope.GetRequiredService را برای گرفتن IOrderHistoryService استفاده می‌کنیم. در این روش هم مشکلی در استفاده از Object هایی که thread safe نیستند (مانند DbContext) رخ نخواهد داد و از Dependency injection و مزیت‌های بی‌شمار آن نیز بهره برده‌ایم.
مطالب
ارتقاء به Angular 6: بررسی تغییرات Angular CLI
اولین مرحله‌ی ارتقاء به Angular 6، به روز رسانی Angular CLI 1.x به نگارش 6 آن است. این شماره نگارش نیز با شماره نگارش Angular یکی شده‌است و دیگر 1x نیست. CLI 6.0 فقط پروژه‌های Angular 5.x و 6x را پشتیبانی می‌کند و برای نصب آن نیاز به حداقل NodeJS 8.9 و NPM 5.5 را خواهید داشت. بنابراین ابتدا دستورات زیر را صادر کرده و اگر هنوز از نگارش‌های قدیمی این ابزارها استفاده می‌کنید، قبل از هر کاری باید آن‌ها را به روز رسانی کنید:
node -v
npm -v
این نگارش از CLI در پشت صحنه از Webpack 4 استفاده می‌کند که نسبت به نگارش‌های پیشین آن بسیار سریعتر است، tree-shaking بهتری را انجام می‌دهد و در نهایت سبب تولید برنامه‌هایی با حجم کمتر و با سرعت build بیشتری خواهد شد.


فایل پیشین angular-cli.json حذف و فایل جدید angular.json بجای آن معرفی شده‌است

یکی از مهم‌ترین تغییرات CLI 6.0 نسبت به نگارش‌های قبلی آن، پشتیبانی از چندین پروژه است و به همین منظور ساختار فایل تنظیمات آن‌را به طور کامل تغییر داده‌اند و اگر دستور ng new project1 را صادر کنید، دیگر از فایل پیشین angular-cli.json خبری نبوده و بجای آن فایل جدید angular.json قابل مشاهده‌است:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "project1": {
در اینجا ساختار ابتدای فایل angular.json را مشاهده می‌کنید که در آن مفهوم projects (و یا workspace در اینجا) به همراه project1 جدیدی که ایجاد کردیم، به عنوان زیر مجموعه‌ی آن قابل مشاهده‌است.
مزیت مهم این قابلیت، امکان ایجاد libraries است که به صورت توکار از این نگارش پشتیبانی می‌شود و می‌توان اجزایی مانند components ،directives ،pipes و services اشتراکی را در یک یا چندین کتابخانه قرار داد و سپس از آن‌ها در پروژه‌ی اصلی و یا چندین پروژه‌ی متصل استفاده کرد.
نگارش 6 در پشت صحنه، پروژه‌ی موفق ng-packagr را به مجموعه‌ی CLI اضافه کرده‌است و از این پس توسط خود CLI می‌توان کتابخانه‌های استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانه‌های npm حاصل را نیز به همراه دارد؛ مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار، با بسته‌های فعلی npm مخصوص Angular وجود دارند. برای مثال بدون استفاده‌ی از این ابزار، نیاز است مستندات 13 صفحه‌ای ساخت کتابخانه‌های Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0، به صورت خودکار ایجاد و مدیریت می‌شود.

بنابراین اکنون سؤال مهمی که مطرح می‌شود این است: آیا باید فایل angular-cli.json پیشین را به صورت دستی به این فایل جدید به روز رسانی کرد و چگونه؟


به روز رسانی تمام بسته‌های سراسری سیستم

در ادامه پیش از هر کاری نیاز است تمام بسته‌های سراسری npm ایی را که هر از چندگاهی به سیستم خود اضافه کرده‌اید، به روز رسانی کنید. برای مشاهده‌ی لیست موارد تاریخ مصرف گذشته‌ی آن‌ها، دستور زیر را صادر کنید:
 npm outdated -g --depth=0
و برای به روز رسانی یکجای آن‌ها نیاز است دستور زیر اجرا شود:
 npm update -g


به روز رسانی خودکار ساختار فایل angular-cli.json

این به روز رسانی توسط CLI 6.0 به صورت خودکار پشتیبانی می‌شود و شامل این مراحل است:
ابتدا نیاز است بسته‌ی سراسری آن‌را به روز رسانی کرد:
 npm i -g @angular/cli
سپس وارد پوشه‌ی اصلی پروژه‌ی خود شده و این دستور را به صورت محلی نیز وارد کنید:
 npm install --save-dev @angular/cli@latest
با اینکار بسته‌ی CLI محلی پروژه به روز شده و اکنون می‌توانیم از قابلیت جدید آن که ng update نام دارد، استفاده کنیم. برای این منظور دستورات ذیل را به ترتیب اجرا کنید:
ng update @angular/cli
ng update @angular/core
ng update rxjs
دستور اول کار تبدیل خودکار فایل angular-cli.json قدیمی را به ساختار جدید آن انجام میدهد؛ با این لاگ:
DELETE .angular-cli.json
CREATE angular.json (4273 bytes)
UPDATE karma.conf.js (1008 bytes)
UPDATE src/tsconfig.spec.json (322 bytes)
UPDATE package.json (2076 bytes)
UPDATE tslint.json (3217 bytes)
دستور دوم بسته‌های هسته‌ی angular را به روز رسانی می‌کند و دستور سوم کار به روز رسانی کتابخانه‌ی rxjs را انجام می‌دهد.
لیست سایر بسته‌هایی را که می‌توان توسط این دستور به روز رسانی کرد، با اجرا دستور ng update می‌توانید مشاهده کنید. برای مثال اگر از Angular Material نیز استفاده می‌کنید، دستور به روز رسانی آن به صورت زیر است:
 ng update @angular/material


مشکل! دستور ng update کار نمی‌کند!

اگر پروژه‌ی شما صرفا مبتنی بر بسته‌های اصلی Angular باشد، مراحل یاد شده‌ی فوق را با موفقیت به پایان خواهید رساند. اما اگر از کتابخانه‌های ثالثی استفاده کرده باشید، منهای دستور «ng update @angular/cli» که کار تولید فایل جدید angular.json را انجام می‌دهد، مابقی با خطاهایی مانند «incompatible peer dependency» و یا «Invalid range:>=2.3.1 <3.0.0||>=4.0.0» متوقف می‌شوند.
در یک چنین حالتی نیاز است ابتدا وابستگی‌های محلی پروژه را به روز کرد و سپس دستورات ng update را تکرار نمود. برای این منظور ابتدا بسته‌ی npm-check-updates را نصب کنید:
 npm install npm-check-updates -g
کار آن به روز رسانی خودکار بسته‌های npm یک پروژه است. پس از آن دستورات زیر را صادر نمائید:
ncu -u
npm install
دستور اول تمام شماره نگارش‌های بسته‌های موجود در فایل package.json را به صورت خودکار به آخرین نگارش آن‌ها روز رسانی می‌کند و دستور دوم این بسته‌های جدید را دریافت و نصب خواهد کرد.
اکنون تمام وابستگی‌های محلی پروژه‌ی شما به صورت خودکار به آخرین نگارش آن‌ها به روز رسانی شده‌اند و اینبار اگر دستورات ذیل را اجرا کنید، با خطاهای یاد شده مواجه نخواهید شد:
ng update @angular/core
ng update rxjs

البته اگر در این حالت برنامه را کامپایل کنید، کار نخواهد کرد. علت اصلی آن به به‌روز رسانی rxjs به نگارش 6 آن مرتبط می‌شود که در مطلب بعدی پیگیری خواهد شد و این نگارش شامل حذفیات بسیاری است در جهت کاهش حجم آن، یکپارچکی و یک دست شدن syntax آن و همچنین بهبود قابل ملاحظه‌ی کارآیی آن. البته پیشنیاز الزامی آن آشنایی با pipe-able operators است. علت دیگر کامپایل نشدن برنامه هم می‌تواند عدم استفاده از HttpClient نگارش 4.3 به بعد باشد.


خاموش کردن اخطار «TypeScript version mismatch»

اگر نگارش TypeScript نصب شده‌ی در سیستم به صورت سراسری، با نگارش محلی پروژه‌ی شما یکی نباشد، اخطار «TypeScript version mismatch» را دریافت می‌کنید. روش خاموش کردن آن در CLI جدید با اجرای دستور زیر است:
 ng config cli.warnings.typescriptMismatch false
البته نگارش 6 نیاز به TypeScript 2.7.2 ذکر شده‌ی در package.json پروژه‌ی محلی را دارد؛ وگرنه اصلا شروع به کامپایل برنامه نمی‌کند:
{
  "devDependencies": {
    "typescript": "~2.7.2"
  }
}
بهترین راه برای یافتن این شماره، اجرای دستور ng new projet1 در یک پوشه‌ی خالی است و سپس مقایسه‌ی محتوای فایل package.json آن با فایل پروژه‌ی موجود.


 تغییرات ng build در نگارش 6

در نگارش 6، مفهوم پیشین environments به configurations تغییر یافته‌است و اینبار در فایل جدید angular.json تنظیم می‌شوند:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-template-driven-forms-lab": {
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },
و پیشتر اگر برای تنظیم محیط build از سوئیچ env استفاده می‌شد:
 ng build --env staging
اکنون این سوئیچ به configuration تغییر یافته‌است و نام آن از مداخل configurations فایل angular.json، مانند مثال فوق که «production» نام دارد، باید دریافت و تنظیم شود:
 ng build --configuration staging
و یا برای مثال دستور «ng build --env=prod» دیگر اجرا نمی‌شود و env=prod حذف شده‌است و اکنون اجرای  ng build --prod مانند اجرای دستور ng build --configuration=prod است.


تغییرات نام‌های نهایی تولیدی

در CLI 6.0، نام‌های نهایی تولیدی دیگر به همراه bundle  و یا chunk نیستند. برای مثال دستور ng build یک چنین خروجی را تولید می‌کند:
 >ng build --watch

Date: 2018-05-05T09:10:50.158Z
Hash: a43eab94ff01539b8592
Time: 31733ms
chunk {main} main.js, main.js.map (main) 9.38 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 226 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.4 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 15.6 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.05 MB [initial] [rendered]

و هش‌های حالت prod به صورت زیر تولید و به نام فایل اضافه می‌شوند:
>ng build --prod --watch

Date: 2018-05-05T09:17:01.803Z
Hash: f25fd6788a4969c52b70
Time: 73279ms
chunk {0} runtime.6afe30102d8fe7337431.js (runtime) 1.05 kB [entry] [rendered]
chunk {1} styles.34c57ab7888ec1573f9c.css (styles) 0 bytes [initial] [rendered]
chunk {2} polyfills.6c08419970f9e4781b69.js (polyfills) 59.4 kB [initial] [rendered]


ساده شدن افزودن وابستگی‌های ثالث به پروژه‌های CLI

برای نصب یک کتابخانه‌ی ثالث، پیشتر می‌بایستی ابتدا بسته‌ی npm آن جداگانه نصب و سپس فایل config برنامه، جهت معرفی مداخل آن، ویرایش می‌شد. اکنون دستور جدید ng add تمام این مراحل را به صورت خودکار انجام می‌دهد:
 ng add @angular/material
برای نمونه دستور فوق نه تنها فایل‌های Angular Material را دریافت می‌کند، بلکه فایل‌های CSS و ماژول‌های مرتبط با آن‌را نیز import خواهد کرد. البته این مورد از کلید ng-add فایل package.json بسته‌ی در حال نصب دریافت و تنظیم می‌شود و کتابخانه‌های جدید باید خود را بر این اساس وفق دهند.
مطالب
یکسان سازی ی و ک دریافتی حین استفاده از NHibernate


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

در ادامه‌ی بحث آلرژی مزمن به وجود انواع "ی" و "ک" در بانک اطلاعاتی (+ و + و +)، اینبار قصد داریم این اطلاعات را به NHibernate بسط دهیم. شاید یک روش اعمال یک دست سازی "ی" و "ک" این باشد که در کل برنامه هر جایی که قرار است update یا insert ایی صورت گیرد، خواص رشته‌ای را یافته و تغییر دهیم. این روش "کار می‌کنه" ولی ایده آل نیست؛ چون حجم کار تکراری در برنامه زیاد خواهد شد و نگهداری آن هم مشکل می‌شود. همچنین امکان فراموش کردن اعمال آن هم وجود دارد.
در NHibernate یک سری EventListener وجود دارند که کارشان گوش فرا دادن به یک سری رخدادها مانند مثلا update یا insert است. این رخدادها می‌توانند پیش یا پس از هرگونه ثبت یا ویرایشی در برنامه صادر شوند. بنابراین بهترین جایی که جهت اعمال این نوع ممیزی (Auditing) بدون بالا بردن حجم برنامه یا اضافه کردن بیش از حد یک سری کد تکراری در حین کار با NHibernate می‌توان یافت، روال‌های مدیریت کننده‌ی همین EventListener ها هستند.

کلاس YeKeAuditorEventListener نهایی با پیاده سازی IPreInsertEventListener و IPreUpdateEventListenerبه شکل زیر خواهد بود:
using NHibernate.Event;

namespace NHYeKeAuditor
{
public class YeKeAuditorEventListener : IPreInsertEventListener, IPreUpdateEventListener
{
// Represents a pre-insert event, which occurs just prior to performing the
// insert of an entity into the database.
public bool OnPreInsert(PreInsertEvent preInsertEvent)
{
var entity = preInsertEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}

// Represents a pre-update event, which occurs just prior to performing the
// update of an entity in the database.
public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
{
var entity = preUpdateEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}
}
}
در کدهای فوق روال‌های OnPreInsert و OnPreUpdate پیش از ثبت و ویرایش اطلاعات فراخوانی می‌شوند (همواره و بدون نیاز به نگرانی از فراموش شدن فراخوانی کدهای مربوطه). اینجا است که فرصت داریم تا تغییرات مورد نظر خود را جهت یکسان سازی "ی" و "ک" دریافتی اعمال کنیم (کد کلاس CorrectYeKe را در پیوست خواهید یافت).

تا اینجا فقط تعریف YeKeAuditorEventListener انجام شده است. اما NHibernate چگونه از وجود آن مطلع خواهد شد؟
برای تزریق کلاس YeKeAuditorEventListener به تنظیمات برنامه باید به شکل زیر عمل کرد:
using System;
using System.Linq;
using FluentNHibernate.Cfg;
using NHibernate.Cfg;

namespace NHYeKeAuditor
{
public static class MappingsConfiguration
{
public static FluentConfiguration InjectYeKeAuditorEventListener(this FluentConfiguration fc)
{
return fc.ExposeConfiguration(configListeners());
}

private static Action<Configuration> configListeners()
{
return
c =>
{
var listener = new YeKeAuditorEventListener();
c.EventListeners.PreInsertEventListeners =
c.EventListeners.PreInsertEventListeners
.Concat(new[] { listener })
.ToArray();
c.EventListeners.PreUpdateEventListeners =
c.EventListeners.PreUpdateEventListeners
.Concat(new[] { listener })
.ToArray();
};
}
}
}
به این معنا که FluentConfiguration خود را همانند قبل ایجاد کنید. درست در زمان پایان کار تنها کافی است متد InjectYeKeAuditorEventListener فوق بر روی آن اعمال گردد و بس (یعنی پیش از فراخوانی BuildSessionFactory).

کدهای NHYeKeAuditor را از اینجا می‌توانید دریافت کنید.

مطالب
آشنایی با jQuery Live

در نگارش‌های اخیر کتابخانه jQuery (از نگارش 1.3 به بعد) متدی به نام live به آن اضافه شده است که کاربرد آن‌را در ادامه مرور خواهیم کرد.
ابتدا مثال زیر را در نظر بگیرید:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestLive.aspx.cs" Inherits="TestJQueryAjax.TestLive" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="js/jquery.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
$('a.mylink').click(function(e) {
var $a = $(this);
alert($a.attr('id'));
});

$('a#lnkLoad').click(function(e) {
$('div#dynContent').load('live.ashx');
});
});
</script>

</head>
<body>
<form id="form1" runat="server">
<a href='#' id='lnk1' class='mylink'>link1</a>
<br />
<a href='#' id='lnkLoad'>load .ashx</a>
<div id='dynContent'>
</div>
</form>
</body>

</html>


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;

namespace TestJQueryAjax
{
/// <summary>
/// Summary description for $codebehindclassname$
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class live : IHttpHandler
{

public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Write("<a href='#' id='lnk2' class='mylink'>link2</a>");
}

public bool IsReusable
{
get
{
return false;
}
}
}
}
در این مثال ساده با کلیک بر روی لینک‌هایی با کلاس mylink ،‌ یک alert حاوی id آن لینک نمایش داده خواهد شد.
همچنین اگر بر روی لینکی با id مساوی lnkLoad کلیک شود، محتوایی پویا از یک generic handler به نام live.ashx دریافت شده و به div ایی با id مساوی dynContent اضافه می‌گردد.
این محتوای دریافتی از generic handler ما نیز کلاسی مساوی mylink دارد، اما این‌بار هر چقدر بر روی لینک اضافه شده به صفحه کلیک کنیم کار نمی‌کند. چرا؟ چون در هنگام فراخوانی document.ready ، این لینک وجود نداشته و روال رخدادگردانی به آن bind نشده است.
به صورت خلاصه می‌خواهیم روال کلیک بر روی لینک‌هایی با کلاس mylink همیشه کار کند. (چه در مورد عناصری در صفحه که از قبل وجود داشته‌اند و چه عناصری که توسط عملیاتی Ajax ایی بعدا اضافه خواهند شد)
این مشکل با معرفی متد live حل شده است. برای این منظور تنها کافی است کد ما به صورت زیر تغییر کند:

<script type="text/javascript">
$(document).ready(function() {
$('a.mylink').live("click", function() {
var $a = $(this);
alert($a.attr('id'));
});
.
.
.

اکنون jQuery کلیه لینک‌هایی با کلاس مساوی mylink را که از این پس اضافه خواهند شد، به صورت live و زنده تحت نظر قرار می‌دهد و عکس العمل نشان خواهد داد.

نظرات مطالب
عبارت using و نحوه استفاده صحیح از آن
مطلب جاری بیشتر به شبیه سازی try/finally معادل using که توسط کامپایلر به صورت خودکار تولید می‌شود مرتبط است نه try/catch کلی. بحث dispose خودکار اشیاء disposable و اینکه استفاده از using به دلیلی که عنوان شد مناسب نیست. بنابراین بجای using از SafeUsingBlock استفاده کنید (شبیه سازی بهتر کاری است که کامپایلر در پشت صحنه جهت معادل سازی یا پیاده سازی using انجام می‌دهد؛ اما بدون از دست رفتن استثناهای رخ داده). مابقی را هم ELMAH انجام می‌دهد.
اگر از using استفاده کنید و ELMAH، فقط خطاهای مرتبط با مثلا iTextSharp رو در لاگ‌ها خواهید یافت؛ مثلا شیء document آن dispose شده، اما خطا و مشکل اصلی که به کدهای ما مرتبط بوده و نه iTextSharp، این میان گم خواهد شد. اما با استفاده از SafeUsingBlock ، دلیل اصلی نیز لاگ می‌شود.