مطالب
Globalization در ASP.NET MVC - قسمت پنجم
در قسمت قبل راجع به مدل پیش‌فرض پرووایدر منابع در ASP.NET بحث نسبتا مفصلی شد. در این قسمت تولید یک پرووایدر سفارشی برای استفاده از دیتابیس به جای فایل‌های resx. به عنوان منبع نگهداری داده‌ها بحث می‌شود.
قبلا هم اشاره شده بود که در پروژه‌های بزرگ ذخیره تمام ورودی‌های منابع درون فایل‌های resx. بازدهی مناسبی نخواهد داشت. هم‌چنین به مرور زمان و با افزایش تعداد این فایل‌ها، کار مدیریت آن‌ها بسیار دشوار و طاقت‌فرسا خواهد شد. درضمن به‌دلیل رفتار سیستم کشینگ این منابع در ASP.NET، که محتویات کل یک فایل را بلافاصله پس از اولین درخواست یکی از ورودی‌های آن در حافظه سرور کش می‌کند، در صورت وجود تعداد زیادی فایل منبع و با ورودی‌های بسیار، با گذشت زمان بازدهی کلی سایت به شدت تحت تاثیر قرار خواهد گرفت.
بنابراین استفاده از یک منبع مثل دیتابیس برای چنین شرایطی و نیز کنترل مدیریت دسترسی به ورودی‌های آن به صورت سفارشی، می‌تواند به بازدهی بهتر برنامه کمک زیادی کند. درضمن فرایند به‌روزرسانی مقادیر این ورودی‌ها در صورت استفاده از یک دیتابیس می‌تواند ساده‌تر از حالت استفاده از فایل‌های resx. انجام شود.
 
تولید یک پرووایدر منابع دیتابیسی - بخش اول
در بخش اول این مطلب با نحوه پیاده‌سازی کلاس‌های اصلی و اولیه موردنیاز آشنا خواهیم شد. مفاهیم پیشرفته‌تر (مثل کش‌کردن ورودی‌ها و عملیات fallback) و نیز ساختار مناسب جدول یا جداول موردنیاز در دیتابیس و نحوه ذخیره ورودی‌ها برای انواع منابع در دیتابیس در مطلب بعدی آورده می‌شود.
با توجه به توضیحاتی که در قسمت قبل داده شد، می‌توان از طرح اولیه‌ای به صورت زیر برای سفارشی‌سازی یک پرووایدر منابع دیتابیسی استفاده کرد:


اگر مطالب قسمت قبل را خوب مطالعه کرده باشید، پیاده سازی اولیه طرح بالا نباید کار سختی باشد. در ادامه یک نمونه از پیاده‌سازی‌های ممکن نشان داده شده است.
برای آغاز کار ابتدا یک پروژه ClassLibrary جدید مثلا با نام DbResourceProvider ایجاد کنید و ریفرنسی از اسمبلی System.Web به این پروژه اضافه کنید. سپس کلاس‌هایی که در ادامه شرح داده شده‌اند را به آن اضافه کنید.

کلاس DbResourceProviderFactory
همه چیز از یک ResourceProviderFactory شروع می‌شود. نسخه سفارشی نشان داده شده در زیر برای منابع محلی و کلی از کلاس‌های پرووایدر سفارشی استفاده می‌کند که در ادامه آورده شده‌اند.
using System.Web.Compilation;
namespace DbResourceProvider
{
  public class DbResourceProviderFactory : ResourceProviderFactory
  {
    #region Overrides of ResourceProviderFactory
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return new GlobalDbResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return new LocalDbResourceProvider(virtualPath);
    }
    #endregion
  }
}
درباره اعضای کلاس ResourceProviderFactory در قسمت قبل توضیحاتی داده شد. در نمونه سفارشی بالا دو متد این کلاس برای برگرداندن پرووایدرهای سفارشی منابع محلی و کلی بازنویسی شده‌اند. سعی شده است تا نمونه‌های سفارشی در اینجا رفتاری همانند نمونه‌های پیش‌فرض در ASP.NET داشته باشند، بنابراین برای پرووایدر منابع کلی (GlobalDbResourceProvider) نام منبع درخواستی (className) و برای پرووایدر منابع محلی (LocalDbResourceProvider) مسیر مجازی درخواستی (virtualPath) به عنوان پارامتر کانستراکتور ارسال می‌شود.
 
نکته: برای استفاده از این کلاس به جای کلاس پیش‌فرض ASP.NET باید یکسری تنظیمات در فایل کانفیگ برنامه مقصد اعمال کرد که در ادامه آورده شده است.

کلاس BaseDbResourceProvider
برای پیاده‌سازی راحت‌تر کلاس‌های موردنظر، بخش‌های مشترک بین دو پرووایدر محلی و کلی در یک کلاس پایه به صورت زیر قرار داده شده است. این طرح دقیقا مشابه نمونه پیش‌فرض ASP.NET است.
using System.Globalization;
using System.Resources;
using System.Web.Compilation;
namespace DbResourceProvider
{
  public abstract class BaseDbResourceProvider : IResourceProvider
  {
    private DbResourceManager _resourceManager;
    protected abstract DbResourceManager CreateResourceManager();
    private void EnsureResourceManager()
    {
      if (_resourceManager != null) return;
      _resourceManager = CreateResourceManager();
    }
    #region Implementation of IResourceProvider
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      EnsureResourceManager();
      if (_resourceManager == null) return null;
      if (culture == null) culture = CultureInfo.CurrentUICulture;
      return _resourceManager.GetObject(resourceKey, culture);
    }
    public virtual IResourceReader ResourceReader { get { return null; } }
    #endregion
  }
}
کلاس بالا چون یک کلاس صرفا پایه است بنابراین به صورت abstract تعریف شده است. در این کلاس، از نمونه سفارشی DbResourceManager برای بازیابی داده‌ها از دیتابیس استفاده شده است که در ادامه شرح داده شده است.
در اینجا، از متد CreateResourceManager برای تولید نمونه مناسب از کلاس DbResourceManager استفاده می‌شود. این متد به صورت abstract و protected تعریف شده است بنابراین پیاده‌سازی آن باید در کلاس‌های مشتق شده که در ادامه آورده شده‌اند انجام شود.
در متد EnsureResourceManager کار بررسی نال نبودن resouceManager_ انجام می‌شود تا درصورت نال بودن آن، بلافاصله نمونه‌ای تولید شود.

نکته: ازآنجاکه نقطه آغازین فرایند یعنی تولید نمونه‌ای از کلاس DbResourceProviderFactory توسط خود ASP.NET انجام خواهد شد، بنابراین مدیریت تمام نمونه‌های ساخته شده از کلاس‌هایی که در این مطلب شرح داده می‌شوند درنهایت عملا برعهده ASP.NET است. در ASP.NET درطول عمر یک برنامه تنها یک نمونه از کلاس Factory تولید خواهد شد، و متدهای موجود در آن در حالت عادی تنها یکبار به ازای هر منبع درخواستی (کلی یا محلی) فراخوانی می‌شوند. درنتیجه به ازای هر منبع درخواستی (کلی یا محلی) هر یک از کلاس‌های پرووایدر منابع تنها یک‌بار نمونه‌سازی خواهد شد. بنابراین بررسی نال نبودن این متغیر و تولید نمونه‌ای جدید تنها در صورت نال بودن آن، کاری منطقی است. این نمونه بعدا توسط ASP.NET به ازای هر منبع یا صفحه درخواستی کش می‌شود تا در درخواست‌های بعدی تنها از این نسخه کش‌شده استفاده شود.

در متد GetObject نیز کار استخراج ورودی منابع انجام می‌شود. ابتدا با استفاده از متد EnsureResourceManager از وجود نمونه‌ای از کلاس DbResourceManager اطمینان حاصل می‌شود. سپس درصورتی‌که مقدار این کلاس همچنان نال باشد مقدار نال برگشت داده می‌شود. این حالت وقتی پیش می‌آید که نتوان با استفاده از داده‌های موجود نمونه‌ای مناسب از کلاس DbResourceManager تولید کرد.
سپس مقدار کالچر ورودی بررسی می‌شود و درصورتی‌که نال باشد مقدار کالچر UI ثرد جاری که در CultureInfo.CurrentUICulture قرار دارد برای آن درنظر گرفته می‌شود. درنهایت با فراخوانی متد GetObject از DbResourceManager تولیدی برای کلید و کالچر مربوطه کار استخراج ورودی درخواستی پایان می‌پذیرد.
پراپرتی ResourceReader در این کلاس به صورت virtual تعریف شده است تا بتوان پیاده‌سازی مناسب آن را در هر یک از کلاس‌های مشتق‌شده اعمال کرد. فعلا برای این کلاس پایه مقدار نال برگشت داده می‌شود.

کلاس GlobalDbResourceProvider
برای پرووایدر منابع کلی از این کلاس استفاده می‌شود. نحوه پیاده‌سازی آن نیز دقیقا همانند طرح نمونه پیش‌فرض ASP.NET است.
using System;
using System.Resources;
namespace DbResourceProvider
{
  public class GlobalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _classKey;
    public GlobalDbResourceProvider(string classKey)
    {
      _classKey = classKey;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_classKey);
    }
    public override IResourceReader ResourceReader
    {
      get { throw new NotSupportedException(); }
    }
    #endregion
  }
}
GlobalDbResourceProvider از کلاس پایه‌ای که در بالا شرح داده شد مشتق شده است. بنابراین تنها بخش‌های موردنیاز یعنی متد CreateResourceManager و پراپرتی ResourceReader در این کلاس پیاده‌سازی شده است.
در اینجا نمونه مخصوص کلاس ResourceManager (همان DbResourceManager) با توجه به نام فایل مربوط به منبع کلی تولید می‌شود. نام فایل در اینجا همان چیزی است که در دیتابیس برای نام منبع مربوطه ذخیره می‌شود. ساختار آن بعدا بحث می‌شود.
همان‌طور که می‌بینید برای پراپرتی ResourceReader خطای عدم پشتیبانی صادر می‌شود. دلیل آن در قسمت قبل و نیز به‌صورت کمی دقیق‌تر در ادامه آورده شده است.

کلاس LocalDbResourceProvider
برای منابع محلی نیز از طرحی مشابه نمونه پیش‌فرض ASP.NET که در قسمت قبل نشان داده شد، استفاده شده است.
using System.Resources;
namespace DbResourceProvider
{
  public class LocalDbResourceProvider : BaseDbResourceProvider
  {
    private readonly string _virtualPath;
    public LocalDbResourceProvider(string virtualPath)
    {
      _virtualPath = virtualPath;
    }
    #region Implementation of BaseDbResourceProvider
    protected override DbResourceManager CreateResourceManager()
    {
      return new DbResourceManager(_virtualPath);
    }
    public override IResourceReader ResourceReader
    {
      get { return new DbResourceReader(_virtualPath); }
    }
    #endregion
  }
}
این کلاس نیز از کلاس پایه‌ای BaseDbResourceProvider مشتق شده و پیاده‌سازی‌های مخصوص منابع محلی برای متد CreateResourceManager و پراپرتی ResourceReader در آن انجام شده است.
در متد CreateResourceManager کار تولید نمونه‌ای از DbResourceManager با استفاده از مسیر مجازی صفحه درخواستی انجام می‌شود. این فرایند شبیه به پیاده‌سازی پیش‌فرض ASP.NET است. در واقع در پیاده‌سازی جاری، نام منابع محلی همنام با مسیر مجازی متناظر آن‌ها در دیتابیس ذخیره می‌شود. درباره ساختار جدول دیتابیس بعدا بحث می‌شود.
در این کلاس کار بازخوانی کلیدهای موجود برای پراپرتی‌های موجود در یک صفحه از طریق نمونه‌ای از کلاس DbResourceReader انجام شده است. شرح این کلاس در ادامه آمده است. 

نکته: همانطور که در قسمت قبل هم اشاره کوتاهی شده بود، از خاصیت ResourceReader در پرووایدر منابع برای تعیین تمام پراپرتی‌های موجود در منبع استفاده می‌شود تا کار جستجوی کلیدهای موردنیاز در عبارات بومی‌سازی ضمنی برای رندر صفحه وب راحت‌تر انجام شود. بنابراین از این پراپرتی تنها در پرووایدر منابع محلی استفاده می‌شود. ازآنجاکه در عبارات بومی‌سازی ضمنی تنها قسمت اول نام کلید ورودی منبع آورده می‌شود، بنابراین قسمت دوم (و یا قسمت‌های بعدی) کلید موردنظر که همان نام پراپرتی کنترل متناظر است از جستجو میان ورودی‌های یافته شده توسط این پراپرتی بدست می‌آید تا ASP.NET بداند که برای رندر صفحه چه پراپرتی‌هایی نیاز به رجوع به پرووایدر منبع محلی مربوطه دارد (برای آشنایی بیشتر با عبارت بومی‌سازی ضمنی رجوع شود به قسمت قبل).

نکته: دقت کنید که پس از اولین درخواست، خروجی حاصل از enumerator این ResourceReader کش می‌شود تا در درخواست‌های بعدی از آن استفاده شود. بنابراین در حالت عادی، به ازای هر صفحه تنها یکبار این پراپرتی فراخوانده می‌شود. درباره این enumerator در ادامه بحث شده است.

کلاس DbResourceManager
کار اصلی مدیریت و بازیابی ورودی‌های منابع از دیتابیس از طریق کلاس DbResourceManager انجام می‌شود. نمونه‌ای بسیار ساده و اولیه از این کلاس را در زیر مشاهده می‌کنید:
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceManager
  {
    private readonly string _resourceName;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
    }
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      var data = new ResourceData();
      return data.GetResource(_resourceName, resourceKey, culture.Name).Value;
    }
  }
}
کار استخراج ورودی‌های منابع با استفاده از نام منبع درخواستی در این کلاس مدیریت خواهد شد. این کلاس با استفاده نام منیع درخواستی به عنوان پارامتر کانستراکتور ساخته می‌شود. با استفاده از متد GetObject که نام کلید ورودی موردنظر و کالچر مربوطه را به عنوان پارامتر ورودی دریافت می‌کند فرایند استخراج انجام می‌شود.
برای کپسوله‌سازی عملیات از کلاس جداگانه‌ای (ResourceData) برای تبادل با دیتابیس استفاده شده است. شرح بیشتر درباره این کلاس و نیز پیاده سازی کامل‌تر کلاس DbResourceManager به همراه مدیریت کش ورودی‌های منابع و نیز عملیات fallback در مطلب بعدی آورده می‌شود.

کلاس DbResourceReader
این کلاس که درواقع پیاده‌سازی اینترفیس IResourceReader است برای یافتن تمام کلیدهای تعریف شده برای یک منبع به‌کار می‌رود، پیاده‌سازی آن نیز به صورت زیر است:
using System.Collections;
using System.Resources;
using System.Security;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceReader : IResourceReader
  {
    private readonly string _resourceName;
    private readonly string _culture;
    public DbResourceReader(string resourceName, string culture = "")
    {
      _resourceName = resourceName;
      _culture = culture;
    }
    #region Implementation of IResourceReader
    public void Close() { }
    public IDictionaryEnumerator GetEnumerator()
    {
      return new DbResourceEnumerator(new ResourceData().GetResources(_resourceName, _culture));
    }
    #endregion
    #region Implementation of IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
    #endregion
    #region Implementation of IDisposable
    public void Dispose()
    {
      Close();
    }
    #endregion
  }
}
این کلاس تنها با استفاده از نام منبع و عنوان کالچر موردنظر کار بازخوانی ورودی‌های موجود را انجام می‌دهد.
تنها نکته مهم در کد بالا متد GetEnumerator است که نمونه‌ای از اینترفیس IDictionaryEnumerator را برمی‌گرداند. در اینجا از کلاس DbResourceEnumerator که برای کار با دیتابیس طراحی شده، استفاده شده است. همانطور که قبلا هم اشاره شده بود، هر یک از اعضای این enumerator از نوع DictionaryEntry هستند که یک struct است. این کلاس در ادامه شرح داده شده است.
متد Close برای بستن و از بین بردن منابعی است که در تهیه enumerator موردبحث نقش داشته‌اند. مثل منایع شبکه‌ای یا فایلی که باید قبل از اتمام کار با این کلاس به صورت کامل بسته شوند. هرچند در نمونه جاری چنین موردی وجود ندارد و بنابراین این متد بلااستفاده است.
در کلاس فوق نیز برای دریافت اطلاعات از ResourceData استفاده شده است که بعدا به همراه ساختار مناسب جدول دیتابیس شرح داده می‌شود.
 
نکته: دقت کنید که در پیاده‌سازی نشان داده شده برای کلاس LocalDbResourceProvider برای یافتن ورودی‌های موجود از مقدار پیش‌فرض (یعنی رشته خالی) برای کالچر استفاده شده است تا از ورودی‌های پیش‌فرض که در حالت عادی باید شامل تمام موارد تعریف شده موجود هستند استفاده شود (قبلا هم شرح داده شد که منبع اصلی و پیش‌فرض یعنی همانی که برای زبان پیش‌فرض برنامه درنظر گرفته می‌شود و بدون نام کالچر مربوطه است، باید شامل حداکثر ورودی‌های تعریف شده باشد. منابع مربوطه به سایر کالچرها می‌توانند همه این ورودی‌های تعریف‌شده در منبع اصلی و یا قسمتی از آن را شامل شوند. عملیات fallback تضمین می‌دهد که درنهایت نزدیک‌ترین گزینه متناظر با درخواست جاری را برگشت دهد).
 
کلاس DbResourceEnumerator
کلاس دیگری که در اینجا استفاده شده است، DbResourceEnumerator است. این کلاس در واقع پیاده سازی اینترفیس IDictionaryEnumerator است. محتوای این کلاس در زیر آورده شده است:
using System.Collections;
using System.Collections.Generic;
using DbResourceProvider.Models;
namespace DbResourceProvider
{
  public sealed class DbResourceEnumerator : IDictionaryEnumerator
  {
    private readonly List<Resource> _resources;
    private int _dataPosition;
    public DbResourceEnumerator(List<Resource> resources)
    {
      _resources = resources;
      Reset();
    }
    public DictionaryEntry Entry
    {
      get
      {
        var resource = _resources[_dataPosition];
        return new DictionaryEntry(resource.Key, resource.Value);
      }
    }
    public object Key { get { return Entry.Key; } }
    public object Value { get { return Entry.Value; } }
    public object Current { get { return Entry; } }
    public bool MoveNext()
    {
      if (_dataPosition >= _resources.Count - 1) return false;
      ++_dataPosition;
      return true;
    }
    public void Reset()
    {
      _dataPosition = -1;
    }
  }
}
تفاوت این اینترفیس با اینترفیس IEnumerable در سه عضو اضافی است که برای استفاده در سیستم مدیریت منابع ASP.NET نیاز است. همان‌طور که در کد بالا مشاهده می‌کنید این سه عضو عبارتند از پراپرتی‌های Entry و Key و Value. پراپرتی Entry که ورودی جاری در enumerator را مشخص می‌کند از نوع DictionaryEntry است. پراپرتی‌های Key و Value هم که از نوع object تعریف شده‌اند برای کلید و مقدار ورودی جاری استفاده می‌شوند.
این کلاس لیستی از Resource به عنوان پارامتر کانستراکتور برای تولید enumerator دریافت می‌کند. کلاس Resource مدل تولیدی از ساختار جدول دیتابیس برای ذخیره ورودی‌های منابع است که در مطلب بعدی شرح داده می‌شود. بقیه قسمت‌های کد فوق هم پیاده‌سازی معمولی یک enumerator است.

نکته: به جای تعریف کلاس جداگانه‌ای برای enumerator اینترفیس IResourceProvider می‌توان از enumerator کلاس‌هایی که IDictionary را پیاده‌سازی کرده‌اند نیز استفاده کرد، مانند کلاس <Dictionary<object,object یا ListDictionary.
 
تنظیمات فایل کانفیگ
برای اجبار کردن ASP.NET به استفاده از Factory موردنظر باید تنظیمات زیر را در فایل web.config اعمال کرد:
<system.web>
    ...
    <globalization resourceProviderFactoryType=" نام کامل اسمبلی مربوطه ,نام پرووایدر فکتوری به همراه فضای نام آن " />
    ...
</system.web>
روش نشان داده شده در بالا حالت کلی تعریف و تنظیم یک نوع داده در فایل کانفیگ را نشان می‌دهد. درباره نام کامل اسمبلی در اینجا شرح داده شده است.
مثلا برای پیاده‌سازی نشان داده شده در این مطلب خواهیم داشت:
<globalization resourceProviderFactoryType="DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />

در مطلب بعدی درباره ساختار مناسب جدول یا جداول دیتابیس برای ذخیره ورودهای منابع و نیز پیاده‌سازی کامل‌تر کلاس‌های مورداستفاده بحث خواهد شد.

منابع:
مطالب
تزریق وابستگی‌ها در پروفایل‌های AutoMapper در برنامه‌های ASP.NET Core
Profileهای AutoMapper، قابلیت تزریق وابستگی‌ها را در سازنده‌ی خود ندارند؛ به همین جهت در این مطلب، دو راه حل را جهت رفع این محدودیت بررسی می‌کنیم.


مثال: نیاز به نگاشت کلمه‌ی عبور، به کلمه‌ی عبور هش شده

فرض کنید موجودیت کاربری که قرار است در بانک اطلاعاتی ذخیره شود، چنین ساختاری را دارد:
namespace AutoMapperInjection.Entities
{
    public class User
    {
        public int Id { set; get; }

        public string HashedPassword { set; get; }
    }
}
در اینجا کلمه‌ی عبور، به صورت معمولی ذخیره نمی‌شود و معادل هش شده‌ی آن ذخیره خواهد شد. اما اطلاعاتی را که از کاربر دریافت می‌کنیم:
namespace AutoMapperInjection.Models
{
    public class UserDto
    {
        public string Password { set; get; }
    }
}
حاوی کلمه‌ی عبور معمولی است که باید در حین نگاشت UserDto به User، هش شود. این اطلاعات نیز به صورت زیر توسط اکشن متد RegisterUser دریافت می‌شوند که توسط متد mapper.Map، قرار است به یک نمونه از شیء User تبدیل شود:
namespace AutoMapperInjection.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        private readonly IMapper _mapper;

        public HomeController(IMapper mapper)
        {
            _mapper = mapper ?? throw new NullReferenceException(nameof(mapper));
        }

        [HttpPost("[action]")]
        public IActionResult RegisterUser(UserDto model)
        {
            var user = _mapper.Map<User>(model);

            // TODO: Save user

            return Ok();
        }
    }
}
کار این هش شدن نیز برای مثال توسط سرویس زیر انجام می‌شود:
using System;
using System.Security.Cryptography;
using System.Text;

namespace AutoMapperInjection.Services
{
    public interface IPasswordHasherService
    {
        string GetSha256Hash(string input);
    }

    public class PasswordHasherService : IPasswordHasherService
    {
        public string GetSha256Hash(string input)
        {
            using var hashAlgorithm = new SHA256CryptoServiceProvider();
            var byteValue = Encoding.UTF8.GetBytes(input);
            var byteHash = hashAlgorithm.ComputeHash(byteValue);
            return Convert.ToBase64String(byteHash);
        }
    }
}
به همین جهت در حین تعریف نگاشت‌های AutoMaper، نیاز خواهیم داشت تا بتوانیم از این سرویس استفاده کنیم.


روش اول: IValueResolver‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

توسط یک IValueResolver سفارشی، می‌توانیم مشخص کنیم که برای مثال اطلاعات یک خاصیت خاص، چگونه باید از منبع دریافتی تامین شود:
        public class HashedPasswordResolver : IValueResolver<UserDto, User, string>
        {
            private readonly IPasswordHasherService _hasher;

            public HashedPasswordResolver(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public string Resolve(UserDto source, User destination, string destMember, ResolutionContext context)
            {
                return _hasher.GetSha256Hash(source.Password);
            }
        }
همانطور که مشاهده می‌کنید، در اینجا می‌توان سرویس مدنظر را به سازنده‌ی این کلاس تزریق کرد و سپس از آن جهت تامین مقدار هش شده‌ی کلمه‌ی عبور استفاده کرد. IValueResolverها تنها تامین کننده‌ی مقدار یک خاصیت، در حین نگاشت هستند.

پس از آن، روش استفاده‌ی از این تامین کننده‌ی مقدار سفارشی، به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .ForMember(user => user.HashedPassword, exp => exp.MapFrom<HashedPasswordResolver>());
        }
    }
با این تعاریف، هر زمانیکه قرار است کار var user = _mapper.Map<User>(model) انجام شود، مقدار خاصیت HashedPassword، از طریق HashedPasswordResolver تامین می‌شود که در اینجا کار تزریق وابستگی‌های آن نیز به صورت خودکار توسط AutoMapper مدیریت می‌شود. البته بدیهی است که سرویس IPasswordHasherService باید به نحو زیر پیشتر به سیستم معرفی شده باشد:
namespace AutoMapperInjection
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IPasswordHasherService, PasswordHasherService>();
            services.AddAutoMapper(typeof(UserDtoMappingsProfile).Assembly);
            services.AddControllers();
        }


روش دوم: IMappingAction‌ها قابلیت تزریق وابستگی را در سازنده‌ی خود دارند

می‌توان پیش و یا پس از عملیات نگاشت، منطقی را توسط یک IMappingAction سفارشی بر روی آن اجرا کرد که در اینجا نیز امکان تزریق وابستگی در سازنده‌ی IMappingActionهای پیاده سازی شده، وجود دارد:
        public class UserDtoMappingsAction : IMappingAction<UserDto, User>
        {
            private readonly IPasswordHasherService _hasher;

            public UserDtoMappingsAction(IPasswordHasherService hasher)
            {
                _hasher = hasher ?? throw new ArgumentNullException(nameof(hasher));
            }

            public void Process(UserDto source, User destination, ResolutionContext context)
            {
                destination.HashedPassword = _hasher.GetSha256Hash(source.Password);
            }
        }
در اینجا می‌خواهیم مقدار یک یا چندین خاصیت از مقصد نگاشت را (شیء User در اینجا) پس از نگاشت ابتدایی از طریق مقدار دریافتی از کاربر، با منطق خاصی تغییر دهیم. برای مثال کلمه‌ی عبور ساده‌ی دریافتی را هش کنیم و به خاصیت خاصی نسبت دهیم (و یا حتی مقدار خواص دیگری را نیز پس از نگاشت، تغییر دهیم).

در این حالت روش استفاده‌ی از کلاس UserDtoMappingsAction به صورت زیر است:
    public class UserDtoMappingsProfile : Profile
    {
        public UserDtoMappingsProfile()
        {
            // Map from User (entity) to UserDto, and back
            this.CreateMap<User, UserDto>()
                .ReverseMap()
                .AfterMap<UserDtoMappingsAction>();
        }
    }


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: AutoMapperInjection.zip
مطالب
استفاده از Razor در فایل‌های JavaScript و CSS
یکی از مشکلات سینتکس Razor سمت سرور، این است که در فایل‌های JavaScript و CSS سمت کاربر نمی‌توانیم از آن استفاده کنیم، به عنوان مثال فرض کنید در یک فایل JavaScript نیاز به مشخص سازی آدرس یک اکشن متد دارید؛ مثلاً انجام یک عملیات ای‌جکسی. در این حالت به عنوان یک Best Practice بهتر است از Url.Action استفاده کنید. اما همانطور که عنوان شد، این امکان یعنی استفاده از سینتکس Razor در فایل‌های JS و CSS مهیا نیست.
ساده‌ترین راه‌حل، تولید ویوهای سمت سرور JavaScript ایی است. برای اینکار تنها کاری که باید انجام دهیم، تغییر مقدار Content-Type صفحه به مقدار موردنظر می‌باشد؛ مثلاً text/javascript برای فایل‌های JS و text/css برای فایل‌های CSS. به عنوان مثال برای فایل‌های CSS به این صورت عمل خواهیم کرد:
public ActionResult Style()
{
            Response.ContentType = "text/css";
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
برای ویوی آن نیز خواهیم داشت:
@model ExternalJavaScript.Models.Style
@{
    Layout = null;
}
body {
    color : @Model.Color;
    background-color : @Model.Background;
}
در نهایت ویوی فوق را به عنوان فایل CSS در فایل Layout استفاده خواهیم کرد:
<link rel="stylesheet" href="@Url.Action("Style","Home")" />
برای حالت فوق می‌توانیم یک اکشن فیلتر به صورت زیر تهیه کنیم:
public class ContentType : ActionFilterAttribute
    {
        private string _contentType;
        public ContentType(string ct)
        {
            this._contentType = ct;
        }

        public override void OnActionExecuted(ActionExecutedContext context) { /* nada */ }
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            context.HttpContext.Response.ContentType = this._contentType;
        }
    }
و برای استفاده از آن خواهیم داشت:
[ContentType("text/css")]
public ActionResult Style()
 {
     var model = new Style
      {
                Color = "red",
                Background = "blue"
      };
     return View(model);
}

برای فایل‌های JS نیز می‌توانیم از یک View به عنوان محل قرارگیری کدهای جاوا اسکریپت استفاده کنیم:
public class JavaScriptSettingsController : Controller
{
        public ActionResult Index()
        {
            return PartialView();
        }
}
در این حالت در داخل فایل Index.cshtml کدهای جاوا اسکریپت را همراه با سینتکس Razor می‌توانیم بنویسیم:
$(function(){
    $.post('@Url.Action("GetData", "Home")', function (data) {
        $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
    });
});
سپس در داخل فایل Layout.cshtml_ می‌توانیم به ویوی فوق ارجاعی داشته باشیم:
<script src="/JavaScriptSettings"></script>
این روش به خوبی برای ویوهای JS و CSS کار خواهد کرد؛ اما از آنجائیکه ویوی ما توسط ویژوال استودیو به عنوان یک فایل JS و یا CSS معتبر شناخته نمی‌شود، Intellisense برای آن مهیا نیست. برای فعال سازی Intellisense و همچنین معتبر شناخته شدن ویوی فوق، بهترین راه‌حل قرار دادن کدهای JS درون بلاک script است (برای فایل‌های CSS نیز همینطور):
<script>
    $(function () {
        $.post('@Url.Action("Index", "Home")', function (data) {
            $('.notificationList').html(data);
            if ($(data).filter("li").length != 0) {
                $('#notificationCounter').html($(data).filter("li").length);
            }
        });
    });
</script>
اما با اجرای برنامه، در کنسول مرورگر بلافاصله خطای Uncaught SyntaxError: Unexpected token < را دریافت خواهید کرد. در این حالت به روشی نیاز داریم که در زمان اجرا بلاک script را حذف نمائید. بنابراین از یک اکشن فیلتر سفارشی برای اینکار استفاده خواهیم کرد. کار این اکشن فیلتر، تغییر مقدار Content-Type و همچنین حذف بلاک مورد نظر می‌باشد:
public class ExternalFileAttribute : ActionFilterAttribute
    {
        private readonly string _contentType;
        private readonly string _tag;
        public ExternalFileAttribute(string ct, string tag)
        {
            this._contentType = ct;
            _tag = tag;
        }

        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            var response = filterContext.HttpContext.Response;
            response.Filter = new StripEnclosingTagsFilter(response.Filter, _tag);
            response.ContentType = _contentType;
        }

        private class StripEnclosingTagsFilter : MemoryStream
        {
            private static Regex _leadingOpeningScriptTag;
            private static Regex _trailingClosingScriptTag;

            //private static string Tag;

            private readonly StringBuilder _output;
            private readonly Stream _responseStream;

            /*static StripEnclosingTagsFilter()
            {
                LeadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", Tag), RegexOptions.Compiled);
                TrailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", Tag), RegexOptions.Compiled);
            }*/

            public StripEnclosingTagsFilter(Stream responseStream, string tag)
            {
                _leadingOpeningScriptTag = new Regex(string.Format(@"^\s*<{0}[^>]*>", tag), RegexOptions.Compiled);
                _trailingClosingScriptTag = new Regex(string.Format(@"</{0}>\s*$", tag), RegexOptions.Compiled);

                _responseStream = responseStream;
                _output = new StringBuilder();
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                string response = GetStringResponse(buffer, offset, count);
                _output.Append(response);
            }

            public override void Flush()
            {
                string response = _output.ToString();

                if (_leadingOpeningScriptTag.IsMatch(response) && _trailingClosingScriptTag.IsMatch(response))
                {
                    response = _leadingOpeningScriptTag.Replace(response, string.Empty);
                    response = _trailingClosingScriptTag.Replace(response, string.Empty);
                }

                WriteStringResponse(response);
                _output.Clear();
            }

            private static string GetStringResponse(byte[] buffer, int offset, int count)
            {
                byte[] responseData = new byte[count];
                Buffer.BlockCopy(buffer, offset, responseData, 0, count);

                return Encoding.Default.GetString(responseData);
            }

            private void WriteStringResponse(string response)
            {
                byte[] outdata = Encoding.Default.GetBytes(response);
                _responseStream.Write(outdata, 0, outdata.GetLength(0));
            }
        }
    }
 در نهایت می‌توانیم اکشن‌متد موردنظرمان را با فیلتر سفارشی مزین کنیم:
[ExternalFile("text/javascript", "script")]
public ActionResult Index()
{
      return PartialView();
}
برای تولید ویوهای CSS نیز کافی است مقادیر فیلتر را تغییر دهیم:
[ExternalFile("text/css", "style")]
public ActionResult Style()
{
            var model = new Style
            {
                Color = "red",
                Background = "blue"
            };
            return View(model);
}
مطالب
حذف محدودیت‌های فایل‌های PDF توسط iTextSharp
پیشنیاز
«رمزنگاری فایل‌های PDF با استفاده از کلید عمومی توسط iTextSharp»

در مطلب فوق در مورد رمزنگاری اطلاعات فایل‌های PDF به کمک iTextSharp بحث شد. در مطلب جاری به نحوه رفع این محدودیت‌ها خواهیم پرداخت.

الف) رمزگشایی با استفاده از کلمه عبور
using System.IO;
using iTextSharp.text.pdf;

namespace PdfDecryptor.Core
{
    public class PasswordDecryptor
    {
        public string ReadPassword { set; get; }
        public string PdfPath { set; get; }
        public string OutputPdf { set; get; }

        public void DecryptPdf()
        {
            PdfReader.unethicalreading = true;

            PdfReader reader;
            if (string.IsNullOrWhiteSpace(ReadPassword))
                reader = new PdfReader(PdfPath);
            else
                reader = new PdfReader(PdfPath, System.Text.Encoding.UTF8.GetBytes(ReadPassword));

            using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create)))
            {
                stamper.Close();
            }
        }
    }
}
کلاس فوق دوکاربرد را می‌تواند به همراه داشته باشد:
- اگر PDF ایی صرفا دارای محدودیت چاپ بوده و این قابلیت ویژه آن غیرفعال شده است، فقط کافی است مسیر فایل PDF موجود (PdfPath) و مسیر فایل جدیدی که قرار است تولید شود (OutputPdf) ذکر گردد. خروجی فایلی خواهد بود که هیچگونه محدودیتی ندارد. این مساله هم صرفا توسط PdfReader.unethicalreading میسر شده است. به عبارتی ذکر و تنظیم edit password در فایل‌های PDF فاقد امنیت است. همین اندازه که PdfReader می‌تواند فایلی را بخواند، امکان تهیه یک کپی بدون محدودیت از آن توسط PdfStamper وجود خواهد داشت.
در مورد ReadPassword در پیشنیاز ذکر شده، توضیحات کافی به همراه تصویر وجود دارد؛ حالت خاصی که کاربران برای مشاهده محتویات فایل نیاز خواهند داشت تا کلمه‌ی عبور مرتبط را وارد نمایند. در اینجا ذکر ReadPassword الزامی  است. خروجی نهایی کلاس فوق رفع کامل این محدودیت است.


ب) رمزگشایی توسط کلید عمومی
using System.IO;
using iTextSharp.text.pdf;

namespace PdfDecryptor.Core
{
    public class Decryptor
    {
        public string PfxPath { set; get; }
        public string PfxPassword { set; get; }
        public string InputPdf { set; get; }
        public string OutputPdf { set; get; }

        public void DecryptPdf()
        {
            var certs = new PfxReader().ReadCertificate(PfxPath, PfxPassword);
            var reader = new PdfReader(InputPdf, certs.X509Certificates[0], certs.PrivateKey);
            using (var stamper = new PdfStamper(reader, new FileStream(OutputPdf, FileMode.Create)))
            {
                stamper.Close();
            }
        }
    }
}
در اینجا کدهای کامل رمزگشایی فایل PDF ایی که توسط فایل‌های مخصوص PFX رمزنگاری شده است را مشاهده می‌کنید. کلاس PfxReader آن در پیشنیاز بحث موجود است.
در این حالت مسیر فایل PFX به همراه کلمه عبور آن (PfxPassword) باید مشخص شود. خروجی فایلی است بدون محدودیت خاصی.


پ.ن.
این مثال را به صورت یک فایل اجرایی از اینجا می‌توانید دریافت کنید.
مطالب
ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF
طی این  مقاله، نحوه‌ی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامه‌ی هدف ما که از این API استفاده می‌کند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings)
{
  // example of saving 
  _settings.General.SiteName = "دات نت تیپس";
  _settings.Seo.HomeMetaTitle = ".Net Tips";
  _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection";
  _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";
  _settings.Save();
}

همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنام‌های General و Seo است که شامل  تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایه‌ی SettingBase ارث بری کرده‌اند. دو دلیل برای انجام این کار وجود دارد:
  1. تنظیمات به صورت گروه بندی شده در کنار  هم قرار گرفته‌اند و یافتن تنظیمات برای زمانی که نیاز به دسترسی  به آنها داریم، راحت‌تر و ساده‌تر خواهد بود. 
  2. به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.

اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟ 

شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و می‌توان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامه‌ی وب شما ری استارت خواهد شد (چه زمان‌هایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس می‌باشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده می‌کنم.

مراحل ساخت Setting API مورد نظر به شرح زیر است:
  1. ساخت یک Asp.net Web Application 
  2. ساخت مدل Setting و افزودن آن به کانتکست Entity Framework 
  3. ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
  4. ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کرده‌اند.
  5. ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات 

یک برنامه‌ی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحله‌ی اول ما به پایان رسید؛ چرا که ویژوال استودیو کار‌های مورد نیاز ما را انجام خواهد داد.
 لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models
{
    public interface IUnitOfWork
    {
        DbSet<Setting> Settings { get; set; }
        int SaveChanges();
    }
} 

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork
    {
        public DbSet<Setting> Settings { get; set; }
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }


namespace DynamicSettingAPI.Models
{
    public class Setting
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Value { get; set; }
    }
}
مدل تنظیمات ما خیلی ساده است و دارای سه پراپرتی به نام‌های Name ، Type ، Value هست که به ترتیب برای دریافت مقدار تنظیمات، نام کلاسی که از کلاس SettingBase ارث برده و نام تنظیمی که لازم داریم ذخیره کنیم، در نظر گرفته شده‌اند. 
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Setting>()
                    .HasKey(x => new { x.Name, x.Type });

            modelBuilder.Entity<Setting>()
                        .Property(x => x.Value)
                        .IsOptional();

            base.OnModelCreating(modelBuilder);
        }
ساختاری به شکل زیر مد نظر ماست:

  کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service
{
    public abstract class SettingsBase
    {
        //1
        private readonly string _name;
        private readonly PropertyInfo[] _properties;

        protected SettingsBase()
        {
            //2
            var type = GetType();
            _name = type.Name;
            _properties = type.GetProperties();
        }

        public virtual void Load(IUnitOfWork unitOfWork)
        {
            //3 get setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                //get the setting from setting list
                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    //4 set 
                    propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
                }
            }
        }
        public virtual void Save(IUnitOfWork unitOfWork)
        {
            //5 get all setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                var propertyValue = propertyInfo.GetValue(this, null);
                var value = (propertyValue == null) ? null : propertyValue.ToString();

                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    // 6 update existing value
                    setting.Value = value;
                }
                else
                {
                    // 7 create new setting
                    var newSetting = new Setting()
                    {
                        Name = propertyInfo.Name,
                        Type = _name,
                        Value = value,
                    };
                    unitOfWork.Settings.Add(newSetting);
                }
            }
        }
    }
}
این کلاس قرار است توسط کلاس‌های تنظیمات ما به ارث برده شود و در واقع کارهای مربوط به رفلکشن را در این کلاس کپسوله کرده‌ایم. همانطور که مشخص است ما دو فیلد را به نام‌های name_ و properties_ به صورت فقط خواندنی در نظر گرفته ایم که نام کلاس مورد نظر ما که از این کلاس به ارث خواهد برد، به همراه پراپرتی‌های آن، در این ظرف‌ها قرار خواهند گرفت.
متد Load وظیفه‌ی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند. 
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفه‌ی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد. 
این متد دیتا‌های موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.

  کلاس‌های تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
  public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
        public bool RegisterUsersEnabled { get; set; }
    }

 public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
    }
نیازی به توضیح ندارد.
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم: 
public interface ISettings
{
    GeneralSettings General { get; }
    SeoSettings Seo { get; }
    void Save();
}

public class Settings : ISettings
{
    // 1
    private readonly Lazy<GeneralSettings> _generalSettings;
    // 2
    public GeneralSettings General { get { return _generalSettings.Value; } }

    private readonly Lazy<SeoSettings> _seoSettings;
    public SeoSettings Seo { get { return _seoSettings.Value; } }

    private readonly IUnitOfWork _unitOfWork;
    public Settings(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        // 3
        _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
        _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
    }

    public void Save()
    {
        // only save changes to settings that have been loaded
        if (_generalSettings.IsValueCreated)
            _generalSettings.Value.Save(_unitOfWork);

        if (_seoSettings.IsValueCreated)
            _seoSettings.Value.Save(_unitOfWork);

        _unitOfWork.SaveChanges();
    }
    // 4
    private T CreateSettings<T>() where T : SettingsBase, new()
    {
        var settings = new T();
        settings.Load(_unitOfWork);
        return settings;
    }
}
این اینترفیس مشخص می‌کند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفه‌ی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.

 حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازنده‌ای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
    // ARGUMENT CHECKING SKIPPED FOR BREVITY
    _unitOfWork = unitOfWork;
    _cache = cache;
    _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
    _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}

private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
    // this is where you would implement loading from ICache
    throw new NotImplementedException();
}
در آخر هم به شکل زیر میتوان (به عنوان دمو فقط ) از این API استفاده کرد.
   public ActionResult Index()
        {
            using (var uow = new ApplicationDbContext())
            {
                var _settings = new Settings(uow);
                _settings.General.SiteName = "دات نت تیپس";
                _settings.General.AdminEmail = "admin@gmail.com";
                _settings.General.RegisterUsersEnabled = true;
                _settings.Seo.HomeMetaTitle = ".Net Tips";
                _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection";
                _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";

                var settings2 = new Settings(uow);
                var output = string.Format("SiteName: {0} HomeMetaDescription: {1}  MetaKeywords:  {2}  MetaTitle:  {3}  RegisterEnable:  {4}",
                    settings2.General.SiteName,
                    settings2.Seo.HomeMetaDescription,
                    settings2.Seo.MetaKeywords,
                    settings2.Seo.HomeMetaTitle,
                    settings2.General.RegisterUsersEnabled.ToString()
                    );
                return Content(output);
            }

        }

خروجی :

نکته: در پروژه ای که جدیدا در سایت ارائه داده‌ام و در حال تکمیل آن هستم، از بهبود یافته‌ی این مقاله استفاده می‌شود. حتی برای اسلاید شو‌های سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژه‌ی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفته‌تر از این مقاله، همچین امکانی را دارد.
مطالب
آشنایی با Refactoring - قسمت 9

این قسمت از آشنایی با Refactoring به کاهش cyclomatic complexity اختصاص دارد و خلاصه آن این است: «استفاده از if های تو در تو بیش از سه سطح، مذموم است» به این علت که پیچیدگی کدهای نوشته شده را بالا برده و نگهداری آن‌ها را مشکل می‌کند. برای مثال به شبه کد زیر دقت کنید:

if
if
if
if
do something
endif
endif
endif
endif


که حاصل آن شبیه به نوک یک پیکان (Arrow head) شده است. یک مثال بهتر:


namespace Refactoring.Day9.RemoveArrowhead.Before
{
public class Role
{
public string RoleName { set; get; }
public string UserName { set; get; }
}
}

using System;
using System.Collections.Generic;
using System.Linq;

namespace Refactoring.Day9.RemoveArrowhead.Before
{
public class RoleRepository
{
private IList<Role> _rolesList = new List<Role>();

public IEnumerable<Role> Roles { get { return _rolesList; } }

public void AddRole(string username, string roleName)
{
if (!string.IsNullOrWhiteSpace(roleName))
{
if (!string.IsNullOrWhiteSpace(username))
{
if (!IsInRole(username, roleName))
{
_rolesList.Add(new Role
{
UserName=username,
RoleName=roleName
});
}
else
{
throw new InvalidOperationException("User is already in this role.");
}
}
else
{
throw new ArgumentNullException("username");
}
}
else
{
throw new ArgumentNullException("roleName");
}
}

public bool IsInRole(string username, string roleName)
{
return _rolesList.Any(x => x.RoleName == roleName && x.UserName == username);
}
}
}

متد AddRole فوق، نمونه‌ی بارز پیچیدگی بیش از حد حاصل از اعمال if های تو در تو است و ... بسیار متداول. برای حذف این نوک پیکان حاصل از if های تو در تو، از بالاترین سطح شروع کرده و شرط‌ها را برعکس می‌کنیم؛ با این هدف که هر چه سریعتر متد را ترک کرده و خاتمه دهیم:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Refactoring.Day9.RemoveArrowhead.After
{
public class RoleRepository
{
private IList<Role> _rolesList = new List<Role>();

public IEnumerable<Role> Roles { get { return _rolesList; } }

public void AddRole(string username, string roleName)
{
if (string.IsNullOrWhiteSpace(roleName))
throw new ArgumentNullException("roleName");

if (string.IsNullOrWhiteSpace(username))
throw new ArgumentNullException("username");

if (IsInRole(username, roleName))
throw new InvalidOperationException("User is already in this role.");

_rolesList.Add(new Role
{
UserName = username,
RoleName = roleName
});
}

public bool IsInRole(string username, string roleName)
{
return _rolesList.Any(x => x.RoleName == roleName && x.UserName == username);
}
}
}

اکنون پس از اعمال این Refactoring ، متد AddRole بسیار خواناتر شده و هدف اصلی آن که اضافه کردن یک شیء به لیست نقش‌ها است، واضح‌تر به نظر می‌رسد. به علاوه اینبار قسمت‌های مختلف متد AddRole، فقط یک کار را انجام می‌دهند و وابستگی‌های آن‌ها به یکدیگر نیز کاهش یافته است.


نظرات مطالب
برش تصاویر قبل از آپلود (Crop)
پیرو مطلب بالا، در صورتی که تصویر شما ابعادی بزرگتر از اندازه صفحه داشته باشد باعث میشود که عکس به درستی در صفحه جا نشود، به همین دلیل ممکن است تگ img را با تگ Div با مشخصات زیر بپوشانید:
 
  <div style="overflow: auto">
            <img  id="preview" />
        </div>
این کد باعث میشود که عکس به صورت تمام در صفحه نمایش داده شود و قسمت‌های نهان آن با اسکرول جابجا شوند. در اینجا چند مشکل ایجاد می‌شود اول اینکه زیبایی صفحه زیاد نیست، دوم اینکه در حین کراپ چندان خوشاید نیست و سوم اینکه در صفحه موبایل عکس بزرگ و اسکرول چندان کارایی ندارد.

در این بین من با استفاده از فریمورک بوت استراپ کلاس img-responsive را به تگ image می‌دهم تا تصویر در هر صفحه نمایشی متناسب با اندازه آن نمایش داده شود. مشکل زیبایی تصویر در صفحه و نحوه کراپ کردن آن حل میشود ولی مشکل جدیدتر این است که تصویری که کراپ می‌شود آن ناحیه ای نیست که شما قبلا انتخاب کرده بودید؛ دلیل آن هم این است که تصویری که responsive شده است اندازه جدیدی برای خود دارد و برش ناحیه در سمت کلاینت و مختصاتی که به شما داده میشود بر اساس اندازه تغییر یافته است ولی در سمت سرور شما با اندازه واقعی تصویر سر و کار دارید و به همین دلیل مختصات ناحیه برش داده شده اشتباه بوده و قسمت‌های دیگری از تصویر برش میخورند.
برای حل این مشکل ابتدا دو المان مخفی زیر را به صفحه اضافه میکنیم:
<input type="hidden" id="RealW" name="RealW" />
<input type="hidden" id="RealH" name="RealH" />
این دو المان برای نگهداری اندازه تگ img است که ممکن است در هر صفحه نمایشی متفاوت باشد و کراپ کردن بر اساس آن انجام میشود.
سپس کدهای زیر را به فایل js به شکل زیر ویرایش می‌کنیم:
$('#preview').Jcrop({

                    aspectRatio: 2, 
                    bgFade: true, 
                    bgOpacity: .3,
                    onChange: updateInfo,
                    onSelect: updateInfo
                }, function () {
                    // use the Jcrop API to get the real image size

//============== خطوط جدید
                    $("#RealW").val($('#preview').css("width").replace("px", ""));
                    $("#RealH").val($('#preview').css("height").replace("px", ""));
//==============

                    jcrop_api = this;
                });
دو خط جدید اضافه شده به این کد اندازه تگ img را درخواست کرده و داخل المان‌های مخفی جای میدهد تا در هنگام پست شدن صفحه ارسال شوند و از آنجا که اندازه‌های دریافتی به همراه نام واحد  میباشند (که در اینجا px است) آن‌ها را از انتهای رشته حذف کرده ام.
سپس در سمت کلاینت با محاسبه فرمول‌های تناسب این مشکل را رفع میکنیم تا مختصات‌های دریافت شده را به مختصات‌های واقعی تبدیل کنیم:
 public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2,int realW,int realH)
        {

            //convert to full size image
            ImageConverter ic = new ImageConverter();
            Image src = (Image)(ic.ConvertFrom(byteImageIn)); //original size

            if (src == null)
                return null;

// چهار خط زیر فرمول تناسب را اجرا می‌کند
            x1 = src.Width * x1 / realW;
            x2 = src.Width * x2 / realW;

            y1 = src.Height * y1 / realH;
            y2 = src.Height * y2 / realH;

            Bitmap target = new Bitmap(x2 - x1, y2 - y1);
            using (Graphics graphics = Graphics.FromImage(target))
                graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height),
                    new Rectangle(x1,y1,x2-x1,y2-y1), 
                    GraphicsUnit.Pixel);
            src = target;
            using (var ms = new MemoryStream())
            {
                src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
                return ms.ToArray();
            }

        }
مطالب
نحوه ایجاد یک گزارش فاکتور فروش توسط PdfReport
شکل زیر را که شبیه به یک فاکتور فروش است درنظر بگیرید:



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

در ادامه کدهای کامل این مثال را مشاهده می‌کنید. همچنین این کد و کلاس‌های وابسته به آن مانند User و TransparentTemplate به سورس‌های کتابخانه PdfReport نیز اضافه شده‌اند.
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfReportSamples.Templates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.Tax
{
    public class TaxPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new TransparentTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 7; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");

                events.MainTableAdded(args =>
                {
                    var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
                    var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);

                    var others = Math.Round(balance * 1.8 / 100);
                    var tax = Math.Round(balance * 2.2 / 100);
                    var total = balance + tax + others;

                    var taxTable = new PdfPTable(args.Table); // Create a clone of the MainTable's structure                   

                    taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "عوارض";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", others);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "جمع کل";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", total);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "قابل پرداخت";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = total.NumberToText(Language.Persian) + " ریال";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                            cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                        });

                    args.PdfDoc.Add(taxTable);
                });
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\TaxReportSample.pdf"));
        }
    }
}
توضیحات:
تنها تفاوت این مثال با مثال‌های قبلی، کدهای مرتبط با متد events.MainTableAdded می‌باشند.
توسط متد args.LastOverallAggregateValueOf، می‌توان به مقدار نهایی متد تجمعی تعریف شده برای یک ستون خاص دسترسی یافت:
var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);
سپس بر این اساس، امکان محاسبه مالیات و عوارض میسر می‌شود:
var others = Math.Round(balance * 1.8 / 100);
var tax = Math.Round(balance * 2.2 / 100);
var total = balance + tax + others;
در ادامه نیاز داریم تا یک جدول جدید را ذیل جدول اصلی ایجاد کنیم. نکته مهم این جدول جدید، هماهنگی عرض ستون‌های آن با ستون‌های جدول اصلی است. به همین منظور می‌توان از خاصیت args.Table جهت دسترسی به خواص جدول اصلی استفاده کرد و جدول جدیدی را ایجاد نمود:
var taxTable = new PdfPTable(args.Table);
از اینجا به بعد دیگر به عهده خودتان است. می‌توانید از دانش iTextSharp خود استفاده کرده و ردیف‌های این جدول جدید را پر کنید. یا اینکه می‌توانید از متد کمکی توکار AddSimpleRow به نحو زیر استفاده نمائید:
taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });
با توجه به اینکه قصد نداریم در سه ستون اول این جدول جدید، عنصری را نمایش دهیم، آن‌ها را با null مقدار دهی کرده و سپس ستون برچسب و ستون مقدار را اضافه می‌کنیم (آرگومان‌های این متد به صورت params تعریف شده‌اند. بنابراین هر تعداد ستون که نیاز باشد قابل تعریف است).
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت می‌گردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه می‌کنید، می‌توان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);

یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف می‌تواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)

 
مطالب
Microbenchmark
What Is Micro Benchmark? Micro benchmark is a benchmark designed to measure the performance of a very small and specific piece of code. (^)
البته این موضوع امروزه بیشتر در Java مطرحه تا دات نت (^ و ^ و ^) اما مفاهیم اصلی مختص یک زبان یا پلتفرم نیست.
وقتی در مورد آزمایش بار برای مقایسه کارایی کلاس StrigBuilder تحقیق میکردم به مطلب جالبی برخورد کردم. خلاصش این میشه که برای تست بار قسمتهایی از کدتون میتونین زمان موردنیاز برای اجرای اون کد رو بررسی کنین و چون ممکنه انجام این کار چندین بار نیاز بشه بهتره از متد زیر برای اینکار استفاده کنین:
static void Profile(string description, int iterations, Action func) {
    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    // warm up 
    func();

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
سه خط اول متد بالا برای آماده‌سازی حافظه جهت اجرای تست موردنظر است. برای آشنایی بیشتر با نحوه عملکرد Garbage Collector (^ و ^ و ^) خوندن کتاب فوق العاده CLR via C# - 3rd ed رو پیشنهاد میکنم (فصلهای 21 و 22).
سپس یکبار اکشن موردنظر (که حاوی قطعه کد موردنظره) اجرا میشه تا مسائل مربوطه به بارگذاری‌های اولیه در نتیجه تست تاثیر نزاره (warm up).
در نهایت هم آزمایش بار برای تعداد تکرار درخواست شده انجام میشه و زمان اجرای اون در خروجی چاپ میشه.
برای استفاده از متد فوق میشه از کد زیر استفاده کرد:
Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});
و برای استفاده از این متد در آزمایش کارایی کلاس StringBuilder میشه از کدی شبیه به کد زیر استفاده کزد:
var iterations = 10000000;
var testString = ".NET Tips is awesme!";
do
{
  var sb1 = new StringBuilder(testString);
  var sb2 = new StringBuilder(testString) { Capacity = testString.Length * iterations };
  try
  {
    Profiler.Profile("StringBuilder Profiler", iterations, () => sb1.Append(testString));
    Profiler.Profile("StringBuilder Capacity Profiler", iterations, () => sb2.Append(testString));
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
  }
  finally
  {
    Console.WriteLine("----------------------------------------------------------------");
    sb1.Clear();
    sb2.Clear();
  }
} while (Console.ReadKey(true).Key == ConsoleKey.C); // C = continue
البته برای اینکه عملیات مقدار دهی خاصیت Capacity در قسمت warm up متد profile نتایج رو تحت تاثیر قرار نده برای این تست من اون قسمت رو کامنت کردم (اگر این کار رو نکنین زمانهای بدست اومده برای هر دو مورد یکی خواهد بود). اجرای کد بالا نتایج زیر رو تو سیستم من ارائه داد:

می‌بینین که نتایج استفاده از متد موردبحث کمی فرق داره و افزایش کارایی در حالت استفاده از پراپرتی Capacity دیگه حدود 3 برابر نیست و حدود 2 دو برابره. البته زمان بدست اومده برای هر دو مورد نسبت به قبل کاهش داشته که بیشترش میتونه مربوطه به عدم درنظر گرفتن زمان موردنیاز برای ایجاد کلاس StringBuilder در این تست جدید باشه (چون بعید میدونم عملیات پاکسازی حافظه توسط GC تو این تست تاثیر چندانی داشته باشه). درهر حال نتایج این تست بیشتر به واقعیت نزدیکه!
نظرات مطالب
بهبود کارآیی حلقه‌های foreach در دات نت 7
یک نکته‌ی تکمیلی: آشنایی با مفهوم «C# Lowering»


در این مطلب، جهت بررسی درک علت یکسان بودن کارآیی حلقه‌هایی که از دیدگاه ما یکی نیستند، از قابلیت نمایش #Low-level C استفاده شد که نام اصلی آن «C# Lowering» است. Lowering به معنای ترجمه‌ی امکانات سطح بالای یک زبان به امکانات سطح پایین آن است. یعنی حاصل عملیات صورت گرفته نیز باز به همان زبان اولیه است که نمونه‌ی آن، تبدیل یک حلقه‌ی foreach سطح بالا به نمونه‌ی سطح پایینی است که توسط NET Runtime. بهتر درک شده و ساده‌تر اجرا می‌شود.

مزایای Lowering
- بهبود کارآیی برنامه: برای مثال یکی از کارهایی که در این بین عموما انجام می‌شود «Loop unrolling» است. یعنی یک حلقه به چندین حلقه‌ی کوچکتر تقسیم می‌شود تا سربار instructions کنترلی حلقه کاهش پیدا کنند.
- طراحی ساده‌تر زبان: اینکار به تیم طراحی زبان امکان نوشتن کدهای اضافه‌تری را می‌دهد که کار برنامه نویس‌ها را کمتر می‌کند. برای مثال یک record واقعا چیزی نیست بجز یک کلاس پیاده سازی کننده‌ی IEquatable به صورت خودکار و در پشت صحنه.

Lowering چه زمانی رخ می‌دهد؟
Lowering جزئی از عملیات صورت گرفته‌ی در حین کامپایل است. زمانیکه دستور dotnet build را صادر می‌کنیم، ابتدا semantics & syntax analysis صورت می‌گیرد تا اگر برای مثال خطای دستوری وجود دارد، مشخص شود. سپس کدها به CIL یا Common intermediate language تبدیل می‌شوند. در حین این قسمت است که عملیات lowering نیز انجام می‌شود.
اگر علاقمند به مشاهده‌ی این کد #C ثانویه‌ی تولید شده‌ی توسط کامپایلر هستید، می‌توان از ابزار https://sharplab.io نیز استفاده کرد. برای مثال در سمت چپ آن کدهای زیر را قرار دهید:
using System;
using System.Collections.Generic;

var list = new List<int> { 1, 2 };

foreach(var item in list)
    Console.Write(item);
سپس در سمت راست آن، گزینه‌ی #Results C را انتخاب کنید تا بتوانید نمونه‌ی معادل تبدیل شده‌ی توسط کامپایلر را مشاهده نمائید.