مطالب
نکات کار با استثناءها در دات نت
استثناء چیست؟
واژه‌ی استثناء یا exception کوتاه شده‌ی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ می‌دهد و در نتیجه، جریان عادی برنامه را مختل می‌کند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی درباره‌ی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته می‌شود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر می‌کند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامه‌ی شما باشد و باید این لیست را درون برنامه‌ی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامه‌ی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامه‌تان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی می‌توانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناء‌ها بستگی به حالت‌های مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه می‌تواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
 استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts()
{
    using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml")))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}
همانطور که عنوان شد در حالت اول انتظار داریم که فایلی بر روی دیسک موجود باشد. در نتیجه نیازی نیست هیچ استثناءایی را مدیریت کنیم (زیرا در واقع اگر فایل موجود نباشد هیچ روشی برای ایجاد آن نداریم).
در مثال دوم می‌دانیم که ممکن است فایل از قبل موجود نباشد. بنابراین می‌توانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts()
{
    var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml");
    if (!File.Exists(fullPath))
        return new Product[0];
         
    using (var stream = File.Read(fullPath))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}

چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر می‌گردانند ایجاد کرد.
اجازه دهید دوباره از مثال‌های فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
همانطور که از نام آن پیداست این متد باید همیشه لیستی از محصولات را برگرداند. اگر می‌توانید اینکار را با استفاده از catch کردن یک استثنا انجام دهید در غیر اینصورت نباید درون متد اینکار را انجام داد.
IEnumerable<Product> GetCachedProducts()
در متد فوق می‌توانستیم از FileNotFoundException برای فایل موردنظر استفاده کنیم؛ اما مطمئن بودیم که فایل در ابتدا وجود ندارد.
در واقع استثنا‌ها حالت‌هایی هستند که غیرقابل پیش‌بینی هستند. این حالت‌ها می‌توانند یک خطای منطقی از طرف برنامه‌نویس و یا چیزی خارج کنترل برنامه‌نویس باشند (مانند خطاهای سیستم‌عامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمی‌توان مدیریت کرد.

اگر می‌خواهید استثناء‌ها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.


چه استثناءهایی باید مدیریت شوند و کدام‌ها خیر؟ 
مدیریت صحیح استثناء‌ها می‌تواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ می‌دهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری می‌کند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناء‌هایی را catch کنید که بتوانید برای آن راه‌حلی ارائه دهید.
به عنوان مثال اگر در لایه‌ی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، می‌توانیم آن را catch کرده و درون یک استثناء عمومی‌تر قرار دهیم:
public class UserRepository : IUserRepository
{
    public IList<User> Search(string value)
    {
        try
        {
              return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value));
        }
        catch (SqlException err)
        {
             var msg = String.Format("Ohh no!  Failed to search after users with '{0}' as search string", value);
             throw new DataSourceException(msg, err);
        }
    }
}
همانطور که در کد فوق مشاهده می‌کنید به محض صدور استثنای SqlException آن را درون قسمت catch به صورت یک استثنای عمومی‌تر همراه با افزودن یک سری اطلاعات جدید صادر می‌کنیم. اما همانطور که عنوان شد کار لاگ کردن استثناءها را بهتر است در لایه‌های بالاتر انجام دهیم.
اگر مطمئن نیستید که تمام استثناء‌ها توسط شما مدیریت شده‌اند، می‌توانید در حالت‌های زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: می‌توانید Aplication_Error را پیاده‌سازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException 
WCF: پیاده‌سازی اینترفیس IErrorHandler 
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI


چه زمان‌هایی باید یک استثناء صادر شود؟ 
صادر کردن یک استثناء به تنهایی کار ساده‌ایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه داده‌هایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.

اعتبارسنجی آرگومان‌ها
ساده‌ترین مثال، آرگومان‌های مورد انتظار یک متد است:
public void PrintName(string name)
{
     Console.WriteLine(name);
}
در حالت فوق انتظار داریم مقداری برای پارامتر name تعیین شود. متد فوق با آرگومان null نیز به خوبی کار خواهد کرد؛ یعنی مقدار خروجی یک خط خالی خواهد بود. از لحاظ کدنویسی متد فوق به خوبی کار خود را انجام می‌دهد اما خروجی مورد انتظار کاربر نمایش داده نمی‌شود. در این حالت نمی‌توانیم تشخیص دهیم مشکل از کجا ناشی می‌شود.
مشکل فوق را می‌توانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
     
     Console.WriteLine(name);
}
خوب، name باید دارای طول ثابت و همچنین ممکن است حاوی عدد و حروف باشد:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
    if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long");
    if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics");
     
     Console.WriteLine(name);
}
برای حالت فوق و همچنین جلوگیری از تکرار کدهای داخل متد PrintName می‌توانید یک متد Validator برای کلاسی با نام Person ایجاد کنید.
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحث‌برانگیز متدی با امضای زیر است:
public User GetUser(int id)
{
}
کاملاً مشخص است که متدی همانند متد فوق زمانیکه کاربری را پیدا نکند، مقدار null را برمی‌گرداند. اما این روش درستی است؟ خیر؛ زیرا همانطور که از نام این متد پیداست باید یک کاربر به عنوان خروجی برگردانده شود.
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId);
if (user == null)
    throw new InvalidOperationException("Failed to find user: " + userId);
// actual logic here
به این چنین کدهایی معمولاً The null cancer گفته می‌شود (سرطان نال!) زیرا اجازه داده‌ایم متد، خروجی null را بازگشت دهد. به جای کد فوق می‌توانیم از این روش استفاده کنیم:
public User GetUser(int id)
{
    if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?");
    
    var user = db.Execute<User>("WHERE Id = ?", id);
    if (user == null)
        throw new EntityNotFoundException("Failed to find user with id " + id);
        
    return user;
}
نکته‌ایی که باید به آن توجه کنید این است که در هنگام صدور یک استثناء اطلاعات کافی را نیز به آن پاس دهید. به عنوان مثال در EntityNotFoundException مثال فوق پاس دادن "Failed to find user with id " + id کار دیباگ را برای مصرف کننده، راحتر خواهد کرد.


خطاهای متداول حین کار با استثناءها  


  • صدور مجدد استثناء و از بین بردن stacktrace

کد زیر را در نظر بگیرید:

try
{
    FutileAttemptToResist();
}
catch (BorgException err)
{
     _myDearLog.Error("I'm in da cube! Ohh no!", err);
    throw err;
}
مشکل کد فوق قسمت throw err است. این خط کد، محتویات stacktrace را از بین برده و استثناء را مجدداً برای شما ایجاد خواهد کرد. در این حالت هرگز نمی‌توانیم تشخیص دهیم که منبع خطا از کجا آمده است. در این حالت پیشنهاد می‌شود که تنها از throw استفاده شود. در این حالت استثناء اصلی مجدداً صادر گردیده و مانع حذف شدن محتویات stacktrace خواهد شد(+).
  • اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید

یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:

try
{
    GreaseTinMan();
}
catch (InvalidOperationException err)
{
    throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود
}
  • ارائه ندادن context information

در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:

try
{
   socket.Connect("somethingawful.com", 80);
}
catch (SocketException err)
{
    throw new InvalidOperationException("Socket failed", err);  
}
هنگامی که کد فوق با خطا مواجه شود نمی‌توان تنها با متن Socket failed تشخیص داد که مشکل از چه چیزی است. بنابراین پیشنهاد می‌شود اطلاعات کامل و در صورت امکان به صورت دقیق را به استثناء ارسال کنید. به عنوان مثال در کد زیر سعی شده است تا حد امکان context information کاملی برای استثناء ارائه شود:
void IncreaseStatusForUser(int userId, int newStatus)
{
    try
    {
         var user  = _repository.Get(userId);
         if (user == null)
             throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus));
    
         user.Status = newStatus;
         _repository.Save(user);
    }
   catch (DataSourceException err)
   {
       var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus);
        throw new UpdateException(errMsg, err);
   }

نحوه‌ی طراحی استثناءها 
برای ایجاد یک استثناء سفارشی می‌توانید از کلاس Exception ارث‌بری کنید و چهار سازنده‌ی آن را اضافه کنید:
public NewException()
public NewException(string description )
public NewException(string description, Exception inner)
protected or private NewException(SerializationInfo info, StreamingContext context)
سازنده اول به عنوان default constructor شناخته می‌شود. اما پیشنهاد می‌شود که از آن استفاده نکنید، زیرا یک استثناء بدون context information از ارزش کمی برخوردار خواهد بود.
سازنده‌ی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کرده‌اید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
حالا فرض کنید اگر پیام به صورت زیر باشد چقدر باعث خوانایی و عیب‌یابی ساده‌تر خطا خواهد شد:
“The key ‘abrakadabra’ was not present in the dictionary.”
در نتیجه تا حد امکان سعی کنید که context information شما کاملتر باشد.
سازنده‌ی سوم شبیه به سازنده‌ی قبلی عمل می‌کند با این تفاوت که توسط پارامتر دوم می‌توانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازنده‌ی سوم زمانی مورد استفاده قرار می‌گیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیره‌ی استثناءها درون فایل و...)

خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception
{
    public SampleException(string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
    }
 
    public SampleException(string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
    }
 
    public SampleException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
}

اجباری کردن ارائه‌ی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازنده‌ها را اینگونه تعریف کنیم:
public class HttpException : Exception
{
    System.Net.HttpStatusCode _statusCode;
     
    public HttpException(System.Net.HttpStatusCode statusCode, string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
        _statusCode = statusCode;
    }
 
    public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
        _statusCode = statusCode;
    }
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
     
    public System.Net.HttpStatusCode StatusCode { get; private set; }
 
}
همچنین بهتر است پراپرتی Message را برای نمایش پیام مناسب بازنویسی کنید:
public override string Message
{
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
}
مورد دیگری که باید در کد فوق مد نظر داشت این است که status code قابلیت سریالایز شدن را ندارد. بنابراین باید متد GetObjectData را برای سریالایز کردن بازنویسی کنیم:
public class HttpException : Exception
{
    // [...]
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        // this is new
        StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode");
    }
 
    public HttpStatusCode StatusCode { get; private set; }
 
    public override string Message
    {
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
    }
 
    // this is new
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue("HttpStatusCode", (int) StatusCode);
    }
}
در اینحالت فیلدهای اضافی در طول فرآیند Serialization به خوبی سریالایز خواهند شد.

در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را می‌توان ارائه داد، این مورد در یافتن راه‌حل خیلی کمک خواهد کرد.


طراحی پیام‌های مناسب 
پیام‌های exception مختص به توسعه‌دهندگان است نه کاربران نهایی.
نوشتن این نوع پیام‌ها برای برنامه‌نویس کار خسته‌کننده‌ایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType");
throw new Exception("Unecpected workingDirectory");
این نوع پیام‌ها حتی اگر از لحاظ نوشتاری مشکلی نداشته باشند یافتن راه‌حل را خیلی سخت خواهند کرد. اگر در زمان برنامه‌نویسی با این نوع خطاها روبرو شوید ممکن است با استفاده از debugger ورودی نامعتبر را پیدا کنید. اما در یک برنامه و خارج از محیط برنامه‌نویسی، یافتن علت بروز خطا خیلی سخت خواهد بود.
توسعه‌دهندگانی که exception message را در اولویت قرار می‌دهند، معتقد هستند که از لحاظ تجربه‌ی کاربری پیام‌ها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیش‌تر عنوان گردید این نوع پیام‌ها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایه‌های زیرین. همچنین پیام‌هایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامه‌نویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیام‌ها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیام‌ها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوه‌ی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیام‌های استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیام‌ها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناء‌هایی که توسط Base Class Library و دیگر کتابخانه‌های thirt-party صادر می‌شوند چیست؟ اینها انگلیسی هستند.

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

خب اگر پیام‌ها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام می‌تواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیام‌های زیر مناسب هستند:
"Unecpected workingDirectory"
یا:
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory.

"The invalid value was: "fllobdedy"."
یافتن مشکل در پیام اول خیلی سخت خواهد بود زیرا فاقد اطلاعات کافی برای یافتن مشکل است. اما پیام دوم مشکل را به صورت کامل توضیح داده است. در حالت اول شما قطعاً نیاز خواهید داشت تا از دیباگر برای یافتن مشکل استفاده کنید. اما در حالت دوم پیام به خوبی شما را برای یافتن راه‌حل راهنمایی می‌کند.
همیشه برای نوشتن پیام‌های مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوه‌ی رفع مشکل را توضیح دهید
نظرات مطالب
معرفی ASP.NET Identity
کلاس کاربر:
public class AppUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }

    public virtual UserProfile Profile { get; set; }
}

کلاس پروفایل کاربر:
public class UserProfile
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public DateTime? Birthday { get; set; }

    public byte[] Avatar { get; set; }
}

کلاس کانتکست دیتابیس:
public class SampleDbContext : IdentityDbContext
{
    public SampleDbContext() : base("DefaultConnection") { }

    static SampleDbContext()
    {
        Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SampleDbContext>());
    }

    public DbSet<UserProfile> UserProfiles { get; set; }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    ...
}


این کلاس‌ها می‌تونن تو لایه دیگری مثل Domain Models تعریف بشن.
نظرات مطالب
رسم نمودار توسط Kendo Chart
با سلام و تشکر
من مشکل مرتب سازی فیلد‌های از نوع عددی (int) در Kendo Chart دارم. همانند شکل زیر


کد‌های نوشته شده به شکل زیر هستند (Year از جنس عدد هست)

 $(document).ready(function () {
        //چارت 4
        $.ajax({
            type: 'POST',
            url: '@Url.Action("TierStatistics4", "Dashboard", new { area = "Tier" })', 
            dataType: 'json',
            data: { CostCenterId: $("#CostCenterId").val(), QueryIndex: $("#QueryIndex").val() },
            success: function (data) {
                dataSource = new kendo.data.DataSource({
                    data: data,
                    group: {
                        field: "Series"
                    },
                    sort:
                        {
                            field: "Year",
                            dir: "desc"
                        }
                });
                $("#chart4").kendoChart({
                    title: {
                        text: "آمار تعداد/هزینه(ریال) تایر مصرفی شعب به تفکیک مراکز هزینه و ماه‌های سال",
                        font: "irsans",
                    },
                    dataSource: dataSource,
                    series: [{
                        type: "column",
                        field: "Value",
                        categoryField: "Year",
                        name: "#=  group.value  #",
                    }],
                    categoryAxis: {

                        font: "irsans",
                        labels: {
                            font: "irsans"
                        }
                    },
                    valueAxis:
                        {
                            labels: {
                                font: "irsans",
                                visible: true,
                            }
                        },
                    seriesDefaults: {

                        style: "smooth",
                        labels: {
                            position: "Bottom",
                            visible: false,
                            fromat: "{0:n0}",
                            font: "irsans",
                            font: "irsans",
                            format: "{0:n0}",
                        }
                    },
                    legend: {
                        position: "top",
                        labels: {
                            font: "irsans"
                        }
                    },
                    tooltip: {
                        visible: true,
                        format: "{0:n0}",
                        template: "#= series.name #: #= value #",
                        font: "irsans",
                    }
                })
            },
            error: function (ex) {
                alert('خطا در بازیابی اطلاعات' + ex);
            }
        });
        return false;
    });
ضمنا ViewModel هم به صورت زیر می‌باشد
 public class DashoardViewModel : System.Object
    {
        public DashoardThirdViewModel()
        {
        }
        public string Series { get; set; }
        // **********
        public int Year { get; set; }
        //**********
        public long Value { get; set; }
        // **********


    }

ممنون از راهنمائی تون.
مطالب
Entity Framework و InnerException
در Entity Framework  بیشتر استثناها تودرتو هستند و ما باید تمام استثناها رو بررسی کنیم تا به پیغام اصلی خطا برسیم. با استفاده از تکه کد زیر به راحتی می‌تونیم استثناها رو پیمایش کنیم و متن خطا را مشخص کنیم.
                catch (Exception ex)
                {
                    StringBuilder errorMsg = new StringBuilder();
                    for (Exception current = ex; current != null; current = current.InnerException)
                    {
                        if (errorMsg.Length > 0)
                            errorMsg.Append("\n");

                        errorMsg.Append(current.Message.Replace("See the inner exception for details.", string.Empty));
                    }
                    // log
                    errorMsg.ToString();
                }
برای استفاده در قسمت‌های مختلف برنامه یک متد الحاقی مانند زیر تعریف می‌کنیم: 
        public static string ExceptionToString(this Exception ex)
        {
            StringBuilder errorMsg = new StringBuilder();
            for (Exception current = ex; current != null; current = current.InnerException)
            {
                if (errorMsg.Length > 0)
                    errorMsg.Append("\n");
                errorMsg.Append(current.Message.
                    Replace("See the inner exception for details.", string.Empty));
            }
            return errorMsg.ToString();
        }
catch (Exception ex)
                {
                    // log
                    ex.ExceptionToString();
                }
مطالب
یک دست سازی ی و ک در برنامه‌های Entity framework 6
تا قبل از EF 6 برای طراحی یک سیستم عمومی تغییر مقادیر ثبت شده در بانک اطلاعاتی، می‌شد با استفاده از امکانات توکار Tracking آن، مقادیر تغییر کرده را یافت و برای مثال ی و ک آن‌ها را پیش از درج در بانک اطلاعاتی، یک دست کرد. در EF 6 با معرفی یک سری interceptor می‌توان به مراحل پیش و پس از اجرای کوئری‌ها دسترسی پیدا کرد. عمده‌ترین کاربرد آن، لاگ کردن SQLهای تولیدی و نوشتن برنامه‌هایی شبیه به EF Profiler است. اما ... استفاده‌ی دیگری را نیز می‌توان از IDbCommandInterceptor جدید آن تدارک دید: دستکاری SQL تولیدی توسط آن پیش از اعمال به بانک اطلاعاتی.

طراحی یک Interceptor برای یک دست سازی ی و ک

در اینجا کدهای کلاس YeKeInterceptor را ملاحظه می‌کنید. در متدهایی که به کلمه‌ی Executing ختم می‌شوند، می‌توان به دستورات SQL تولید شده توسط EF، پیش از اعمال بر روی بانک اطلاعاتی دسترسی داشت:
    public class YeKeInterceptor : IDbCommandInterceptor
    {
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            command.ApplyCorrectYeKe();
        }
    }
DbCommand، حاوی تمام اطلاعاتی است که به آن نیاز داریم؛ شامل CommandText یا همان SQL تولید شده و همچنین command.Parameters برای دسترسی به مقادیر پارامترهای کوئری. نکته‌ی مهم تمام این موارد، قابل ویرایش بودن آن‌ها است.
    public static class YeKe
    {
        public const char ArabicYeChar = (char)1610;
        public const char PersianYeChar = (char)1740;

        public const char ArabicKeChar = (char)1603;
        public const char PersianKeChar = (char)1705;

        public static string ApplyCorrectYeKe(this object data)
        {
            return data == null ? null : ApplyCorrectYeKe(data.ToString());
        }

        public static string ApplyCorrectYeKe(this string data)
        {
            return string.IsNullOrWhiteSpace(data) ?
                        string.Empty :
                        data.Replace(ArabicYeChar, PersianYeChar).Replace(ArabicKeChar, PersianKeChar).Trim();
        }

        public static void ApplyCorrectYeKe(this DbCommand command)
        {
            command.CommandText = command.CommandText.ApplyCorrectYeKe();

            foreach (DbParameter parameter in command.Parameters)
            {
                switch (parameter.DbType)
                {
                    case DbType.AnsiString:
                    case DbType.AnsiStringFixedLength:
                    case DbType.String:
                    case DbType.StringFixedLength:
                    case DbType.Xml:
                        parameter.Value =   parameter.Value is DBNull ? parameter.Value : parameter.Value.ApplyCorrectYeKe();
                        break;
                }
            }
        }
    }
در اینجا پیاده سازی متد الحاقی ApplyCorrectYeKe را که در کلاس YeKeInterceptor مورد استفاده قرار گرفت، ملاحظه می‌کنید.
در آن، CommandText و همچنین parameter.Valueها در صورت رشته‌ای بودن، اصلاح می‌شوند.
سربار این روش نسبت به روش‌های پیشین استفاده از Reflection کمتر است. همچنین اشیاء پیچیده و تو در تو را نیز بهتر پشتیبانی می‌کند؛ چون در مرحله Executing، کار پردازش این اشیاء پایان یافته و SQL خام نهایی آن در اختیار ما است.


نحوه‌ی استفاده از YeKeInterceptor

در آغاز برنامه (برای مثال متد Application_Start فایل Global.asax.cs برنامه‌های MVC )، سطر زیر را فراخوانی کنید:
 DbInterception.Add(new YeKeInterceptor());

یک مثال کامل برای دریافت
Sample32.cs
مطالب
فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز سمت کلاینت جهت فعال سازی جستجو در jqGrid

در سمت کلاینت، در حین تعریف ستون‌ها، ابتدا باید توسط مقدار دهی خاصیت search، ستون‌های مشارکت کننده‌ی در حین جستجو را مشخص کرد:
                colModel: [
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 200,
                        search: true, stype: 'text', searchoptions: { sopt: searchOptions }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 200,
                        search: true, stype: 'select', edittype: 'select', searchOperators: true,
                        searchoptions:
                        {
                            sopt: searchOptions, dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        }
                    }
                ],
- برای نمونه در اینجا search: true، جهت دو ستون نام محصول و نام تولید کننده، تنظیم شده‌اند.
- stype، روش مقایسه‌ی مقادیر را مشخص می‌کند. مقدار پیش فرض آن text است و مقادیری مانند int، integer، float، number، numeric، date و datetime را می‌پذیرد.
- searchoptions برای تنظیم جزئیات نحو‌ه‌ی جستجوی بر روی فیلدها بکار می‌رود. توسط sopt می‌توان آرایه‌ای با مقادیر ذیل را مقدار دهی کرد:
 var searchOptions = ['eq', 'ne', 'lt', 'le', 'gt', 'ge', 'bw', 'bn', 'in', 'ni', 'ew', 'en', 'cn', 'nc'];
eq به معنای مساوی است، ne، مساوی نیست، lt کمتر و به همین ترتیب.
البته باید دقت داشت که آرایه فوق کاملترین حالت ممکن است و ضرورتی ندارد تمام حالات را برای یک فیلد تعریف کرد. چون برای مثال جستجو cn یا contains برای مقادیر رشته‌ای معنا دارد و نه سایر حالات.
- searchoptions گزینه‌های دیگری را نیز می‌تواند شامل شود. برای مثال در حین نمایش جستجوی داخل ردیفی یا صفحه‌ی دیالوگ مخصوص آن، قصد داریم فیلد نام تولید کننده را توسط یک drop down نمایش دهیم و نه یک text box پیش فرض. برای این منظور dataUrl مقدار دهی شده‌است.
SuppliersSelect آن به اکشن متد ذیل اشاره می‌کند که لیست تولید کنندگان را با فرمت لیستی از SelectListItemها به یک partial view تولید کننده‌ی دراپ داون ارسال می‌کند:
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
و محتوای فایل _SelectPartial نیز به صورت ذیل است:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در این حالت با نمایش صفحه‌ی جستجو و انتخاب فیلد نام تولید کننده، به صورت خودکار یک dorp down پویا نمایش داده خواهد شد.

- اگر دقت کرده باشید، نام فیلد تولید کننده با Supplier.Id مقدار دهی شده‌است. علت اینجا است که در زمان استفاده از drop down، مقدار Id آیتم انتخابی، به سرور ارسال می‌شود. به همین جهت کار کردن پویا با Supplier.Id ساده‌تر خواهد بود.




- برای نمایش دیالوگ و یا نوار ابزار توکار جستجو، می‌توان دکمه‌هایی را به فوتر گرید اضافه کرد:


        $(document).ready(function () {
            $('#list').jqGrid({
              // ... مانند قبل و توضیحات فوق
            })
            .jqGrid('navGrid', '#pager',
                { add: false, edit: false, del: false },
                {},  // default settings for edit
                {},  // default settings for add
                {},  // delete instead that del:false we need this
                {
                    // search options
                    multipleSearch: true,
                    closeOnEscape: true,
                    closeAfterSearch: true,
                    ignoreCase: true
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "نوار ابزار جستجو", title: "Search Toolbar", buttonicon: 'ui-icon-search',
                onClickButton: function () {
                    toolbarSearching();
                }
            });
        });
        

        function toolbarSearching() {
            $('#list').filterToolbar({
                groupOp: 'OR',
                defaultSearch: "cn",
                autosearch: true,
                searchOnEnter: true,
                searchOperators: true, // فعال سازی منوی اپراتورها
                stringResult : true // وجود این سطر سبب می‌شود تا اپراتورها به سرور ارسال شوند
            });
        };

        function singleSearching() {
            $('#list').searchGrid({
                closeAfterSearch: true
            });
        };

        function advancedSearching() {
            $('#list').searchGrid({
                multipleSearch: true,
                closeAfterSearch: true
            });
        };
در اینجا نکات ذیل قابل توجه هستند:
- با استفاده از متد jqGrid و پارامتر navGrid، در ناحیه‌ی pager گرید، تنظیمات جستجو را فعال کرده‌ایم.
multipleSearch به معنای امکان جستجوی همزمان بر روی بیش از یک فیلد است. closeOnEscape سبب بسته شدن صفحه‌ی دیالوگ جستجو با فشردن دکمه‌ی ESC می‌شود. اگر closeAfterSearch به true تنظیم نشود، صفحه‌ی دیالوگ جستجو پس از جستجو، در صفحه باقی مانده و بسته نخواهد شد.
- این دکمه‌ی جستجو، جزو موارد توکار jqGrid است. اگر قصد داشته باشیم یک دکمه‌ی سفارشی دیگر را نیز اضافه کنیم، مجددا از متد jqGrid با پارامتر navButtonAdd در ناحیه‌ی pager استفاده خواهیم کرد. کلیک بر روی آن سبب اجرای متد toolbarSearching می‌شود.

در اینجا حداقل سه نوع جستجو را می‌توان فعال کرد:
- filterToolbar که سبب نمایش نوار ابزار جستجو، دقیقا بالای ستون‌های جدول می‌شود.
- searchGrid که صفحه‌ی دیالوگ مستقلی را جهت جستجو به صورت پویا تولید می‌کند. اگر خاصیت multipleSearch آن به true تنظیم نشود، این جستجو هربار تنها بر روی یک فیلد قابل انجام خواهد بود و برعکس.
- در حالت جستجوی نوار ابزاری، اگر خواص searchOperators و stringResult به true تنظیم شوند، مانند تصویر ذیل، به ازای هر ستون می‌توان از عملگرهای مختلفی استفاده کرد. در غیراینصورت جستجوی انجام شده بر اساس groupOp و defaultSearch پیش فرض انجام می‌شود. یعنی And یا Or تمام موارد تنها در حالت مثلا contains یا تساوی و امثال آن و نه حالت پیشرفته‌ی انتخاب عملگرها توسط کاربر.


یک نکته
اگر می‌خواهید صفحه‌ی جستجو در وسط صفحه ظاهر شود، می‌توانید از تنظیمات CSS ذیل استفاده کنید:
/* align center search popup in jqgrid */
.ui-jqdialog {
    display: none;
    width: 300px;
    position: absolute;
    padding: .2em;
    font-size: 11px;
    overflow: visible;
    left: 30% !important;
    top: 40% !important;
}


پردازش سمت سرور جستجوی پویای jqGrid

کدهای سمت سرور، با کدهای استفاده از dynamic LINQ مایکروسافت یکی است. با این تفاوت که اینبار قسمت where این کوئری نیز پویا می‌باشد. پیشتر قسمت order by را پویا پردازش کرده بودیم. برای ساخت where پویا که در dynamic LINQ به خوبی پشتیبانی می‌شود، باید ابتدا ساختار اطلاعات ارسالی به سرور را آنالیز کنیم:
 //single field search
//_search=true&nd=1403935889318&rows=10&page=1&sidx=Id&sord=asc&searchField=Id&searchString=4444&searchOper=eq&filters=

//multi-field search
//_search=true&nd=1403935941367&rows=10&page=1&sidx=Id&sord=asc&filters=%7B%22groupOp%22%3A%22AND%22%2C%22rules%22%3A%5B%7B%22field%22%3A%22Id%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%2244%22%7D%2C%7B%22field%22%3A%22SupplierID%22%2C%22op%22%3A%22eq%22%2C%22data%22%3A%221%22%7D%5D%7D&searchField=&searchString=&searchOper=
// filters -> {"groupOp":"AND","rules":[{"field":"All","op":"cn","data":"fffff"},{"field":"Price","op":"bn","data":"ffff"}]}

//toolbar search
//_search=true&nd=1403935593036&rows=10&page=1&sidx=Id&sord=asc&Id=2&Name=333&SupplierID=1&CategoryID=1&Price=44
در اینجا ساختار ارسالی به سرور را در سه حالت مختلف جستجوی پویای jqGrid، ملاحظه می‌کنید:
- در تمام این حالات پارامتر _search مساوی true است (تفاوت آن با درخواست اطلاعات معمولی).
- در حالت جستجوی نوار ابزاری، اگر گزینه‌های searchOperators و stringResult به true تنظیم نشوند، حالت toolbar search فوق را شاهد خواهیم بود. در غیراینصورت به حالت multi-field search سوئیچ می‌شود.
- در حالت جستجوی تک فیلدی توسط صفحه دیالوگ جستجوی jqGrid، فیلد در حال جستجو توسط searchField و مقدار آن توسط searchString به سرور ارسال شده‌اند. مابقی پارامترها نال هستند.
- در حالت جستجوی چند فیلدی توسط صفحه دیالوگ جستجوی jqGrid، اینبار filters مقدار دهی شده‌است و سایر پارامترها نال هستند. مقدار filters ارسالی، در حقیقت یک شیء JSON است با ساختار کلی ذیل:
        { "groupOp": "AND",
              "groups" : [ 
                { "groupOp": "OR",
                    "rules": [
                        { "field": "name", "op": "eq", "data": "England" }, 
                        { "field": "id", "op": "le", "data": "5"}
                     ]
                } 
              ],
              "rules": [
                { "field": "name", "op": "eq", "data": "Romania" }, 
                { "field": "id", "op": "le", "data": "1"}
              ]
        }
که می‌توان چنین ساختاری را برای آن متصور شد:
    public class SearchFilter
    {
        public string groupOp { set; get; }
        public List<SearchGroup> groups { set; get; }
        public List<SearchRule> rules { set; get; }
    }

    public class SearchRule
    {
        public string field { set; get; }
        public string op { set; get; }
        public string data { set; get; }

        public override string ToString()
        {
            return string.Format("'{0}' {1} '{2}'", field, op, data);
        }
    }

    public class SearchGroup
    {
        public string groupOp { set; get; }
        public List<SearchRule> rules { set; get; }
    }
در اینجا AND و Or کلی مشخص می‌شود، به همراه فیلدهای ارسالی به سرور، عملگرهای اعمالی بر روی آن‌ها و مقادیر مرتبط.
اگر این موارد را کنار هم قرار دهیم، به متدی عمومی ApplyFilter با امضای ذیل خواهیم رسید.
   public IQueryable<T> ApplyFilter<T>(IQueryable<T> query, bool _search, string searchField, string searchString,
string searchOper, string filters, NameValueCollection form)
کدهای کامل آن‌را به علت طولانی بودن پردازش سه حالت ذکر شده‌ی فوق، از پروژه‌ی پیوست می‌توانید دریافت کنید.
پس از آن، تغییراتی که در کدهای متد GetProducts باید اعمال شوند به صورت ذیل است:
        [HttpPost]
        public ActionResult GetProducts(string sidx, string sord, int page, int rows,
                                        bool _search, string searchField, string searchString,
                                        string searchOper, string filters)
        {
            var list = ProductDataSource.LatestProducts;            

            var pageIndex = page - 1;
            var pageSize = rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();

            productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
                                                           searchOper, filters, this.Request.Form);
            var productsList = productsQuery.OrderBy(sidx + " " + sord)
                                            .Skip(pageIndex * pageSize)
                                            .Take(pageSize)
                                            .ToList();

            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = page,
                Records = totalRecords,
                Rows = (productsList.Select(product => new JqGridRowData
                {
                    Id = product.Id,
                    RowCells = new List<string>
                               {
                                     product.Id.ToString(CultureInfo.InvariantCulture),
                                     product.Name,
                                     product.Supplier.CompanyName,
                                     product.Category.Name,
                                     product.Price.ToString(CultureInfo.InvariantCulture)
                                }
                })).ToArray()
            };
            return Json(productsData, JsonRequestBehavior.AllowGet);
        }
- ابتدا چند پارامتر اضافه‌تر به امضای متد اضافه شده‌اند، تا فیلدهای جستجو را نیز دریافت کنند.
- نوع متد به HttpPost تغییر کرده‌است. این مورد برای ارسال اطلاعات حجیم جستجوها به سرور ضروری است و بهتر است از حالت Get استفاده نشود.
این حالت در سمت کلاینت نیز باید تنظیم شود:
 $('#list').jqGrid({
//url access method type
mtype: 'POST',
- سایر سطرها مانند قبل است؛ فقط یک سطر ذیل جهت اعمال where پویا به عبارت LINQ ساخته شده، اضافه شده‌است:
 productsQuery = new JqGridSearch().ApplyFilter(productsQuery, _search, searchField, searchString,
  searchOper, filters, this.Request.Form);

برای مطالعه بیشتر
جستجوی تک فیلدی
جستجوی نوار ابزاری
جستجوی چند فیلدی


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid03.zip
 
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها
ساده شدن امکان تعریف ایندکس‌ها با Attributes از EF-Core 5x

ویژگی جدید Index که در اسمبلی Microsoft.EntityFrameworkCore.Abstractions واقع شده‌است، امکان تعریف انواع و اقسام ایندکس‌ها را میسر می‌کند. این ویژگی باید به خود کلاس اعمال شود و نه تک تک خواص. چند مثال:
الف) تعریف ایندکس بر روی خاصیت Url یک کلاس
[Index(nameof(Url))]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}
که امکان تعریف نام سفارشی آن نیز میسر است:
[Index(nameof(Url), Name = "Index_Url")]

ب) ایندکس‌های ترکیبی
[Index(nameof(FirstName), nameof(LastName))]
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

ج) ایندکس‌های منحصربفرد
[Index(nameof(Url), IsUnique = true)]
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}
پاسخ به بازخورد‌های پروژه‌ها
مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
سلام، جناب نصیری
طبق گفته شما، از چنین کدی استفاده می‌کنم، که با استفاده از علامت مقدار نهایی ستون باقیمانده، ستون تشخیص را مشخص نمایم.
if (args.CellType == CellType.PreviousPageSummaryCell ||
                      args.CellType == CellType.PageSummaryCell ||
                      args.CellType == CellType.SummaryRowCell)
                    {
                        if (args.TableRowData != null)
                        {
                            var summaryRowRemain = args.TableRowData.FirstOrDefault(x => x.PropertyName == "CaclulatedRemains");
                            if (summaryRowRemain != null && args.Cell.RowData.PropertyName == "CaclulatedDetection")
                            {
                                args.Cell.RowData.FormattedValue = (summaryRowRemain.PropertyValue.ToSafeDouble() > 0)
                                    ? "بستانکار"
                                    : (summaryRowRemain.PropertyValue.ToSafeDouble() < 0 ? "بدهکار" : string.Empty);
                            }
                        }
                    }
واسه سطر اول که جمع کل صفحه باشه مشکل ندارم، برای سطر بعدی که جمع کل هست، مشکل اینه اگه مقدار منفی بوده باشه الان چون Math.Abs استفاده می‌کنیم. مقدار مثبت میشه حتی مقدار PropertyValue و مثلا به جای بدهکار مقدار بستانکار نمایش داده میشه.

دو ستون نیز به این صورت تعریف شده اند:
columns.AddColumn(column =>
                {
                    column.PropertyName<VoucherRowPrintViewModel>(x => x.CaclulatedDetection);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(1);
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Empty);
                    });
                    column.HeaderCell("تشخیص");
                });
                columns.AddColumn(column =>
                {
                    column.PropertyName<VoucherRowPrintViewModel>(x => x.CaclulatedRemains);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Right);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(2);
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", (Math.Abs(Convert.ToInt64(obj.ToSafeString().Replace(",", string.Empty))))));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", (Math.Abs(Convert.ToInt64(obj.ToSafeString().Replace(",", string.Empty))))));
                    });
                    column.HeaderCell("مانده");
                });

نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت سوم - نرمال سازها و اعتبارسنج‌ها
تغییرات مطابق موارد زیر داده شد ولی باز هم همان ارور 
public class CustomNormalizer : ILookupNormalizer
    {
        public string NormalizeName(string key)
        {
            key = Normalize(key);
            key = key.ApplyCorrectYeKe()
                     .RemoveDiacritics()
                     .CleanUnderLines()
                     .RemovePunctuation();
            key = key.Trim().Replace(" ", "");
            return key;
        }
        public string NormalizeEmail(string key)
        {
            key = Normalize(key);
            key = fixGmailDots(key);
            return key;
        }
        public string Normalize(string key)
        {
            if (string.IsNullOrWhiteSpace(key))
            {
                return null;
            }

            key = key.Trim();
            key = key.ToUpperInvariant();
            return key;
        }

        private static string fixGmailDots(string email)
        {
            email = email.ToLowerInvariant().Trim();
            var emailParts = email.Split('@');
            var name = emailParts[0].Replace(".", string.Empty);

            var plusIndex = name.IndexOf("+", StringComparison.OrdinalIgnoreCase);
            if (plusIndex != -1)
            {
                name = name.Substring(0, plusIndex);
            }

            var emailDomain = emailParts[1];
            emailDomain = emailDomain.Replace("googlemail.com", "gmail.com");

            string[] domainsAllowedDots =
            {
                "gmail.com",
                "facebook.com"
            };

            var isFromDomainsAllowedDots = domainsAllowedDots.Any(domain => emailDomain.Equals(domain));
            return !isFromDomainsAllowedDots ? email : string.Format("{0}@{1}", name, emailDomain);
        }
    }

اینترفیس ILookupNormalizer   به شکل زیر است

#region Assembly Microsoft.Extensions.Identity.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// C:\Users\kazemi\.nuget\packages\microsoft.extensions.identity.core\3.0.0-preview-18579-0056\lib\netstandard2.0\Microsoft.Extensions.Identity.Core.dll
#endregion

namespace Microsoft.AspNetCore.Identity
{
    //
    // Summary:
    //     Provides an abstraction for normalizing keys for lookup purposes.
    public interface ILookupNormalizer
    {
        //
        // Summary:
        //     Returns a normalized representation of the specified key.
        //
        // Parameters:
        //   key:
        //     The key to normalize.
        //
        // Returns:
        //     A normalized representation of the specified key.
        string Normalize(string key);
    }
}

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


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

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

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

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

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

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

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

مهاجرت به Visual Studio 2013

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

public class ApplicationDbContext : IdentityDbContext<User>
{
        
}

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

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

public class UserManager : UserManager<User>
{
        
}

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

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

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

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

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

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

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

            return Convert.ToBase64String(bRet);
    }
}



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

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

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

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

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

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

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

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

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

            return Guid.NewGuid();
        }
}


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

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