مطالب
فارسی نویسی و iTextSharp

شرح یک سری سعی و خطا!
سعی اول:
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var chunk = new Chunk("آزمایش");
pdfDoc.Add(chunk);
}
}
}
}

نتیجه:



بله! هیچی!

مشکل از کجاست؟
در iTextSharp بر اساس نوع فونت انتخابی و encoding مرتبط،‌ نحوه‌ی رندر سازی حروف مشخص می‌شود:



همانطور که ملاحظه می‌کنید، فونت پایه متنی که قرار است اضافه شود، null است.

سعی دوم:
اینبار فونت را تنظیم می‌کنیم:
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

var chunk = new Chunk("آزمایش",tahomaFont);
pdfDoc.Add(chunk);
}
}
}
}

توضیحات:
متد BaseFont.CreateFont می‌تواند مسیری از فونت مورد نظر را دریافت کند. این حالت خصوصا برای برنامه‌های وب که ممکن است فونت مورد نظر آن‌ها در سرور نصب نشده باشد، بسیار مفید است و لزومی ندارد که الزاما فونت مورد استفاده در پوشه fonts‌ ویندوز نصب شده باشد.
نکات مهم دیگر بکار گرفته شده در این متد، استفاده از BaseFont.IDENTITY_H و BaseFont.EMBEDDED است. به این صورت encoding متن، جهت نوشتن متون غیر Ansi تنظیم می‌شود و در این حالت حتما باید فونت را در فایل، مدفون (embed) نمود. از این لحاظ که عموما این نوع فونت‌ها در سیستم‌های کاربران نصب نیستند.

نتیجه:



بد نیست! حداقل حروف نمایش داده شدند؛ اما نیاز است تا چرخانده یا معکوس شوند. برای انجام خودکار آن حداقل دو کار را می‌توان انجام داد.

الف) استفاده از ColumnText و اعمال تنظیمات راست به چپ آن
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

ColumnText ct = new ColumnText(pdfWriter.DirectContent);
ct.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
ct.SetSimpleColumn(100, 100, 500, 800, 24, Element.ALIGN_RIGHT);

var chunk = new Chunk("آزمایش", tahomaFont);

ct.AddElement(chunk);
ct.Go();
}
}
}
}

توضیحات:
در اینجا یک ColumnTex جدید تعریف و سپس خصوصیات این ستون تنظیم شده، به همراه RunDirection آن که اصل قضیه است. سپس chunk تعریف شده را به این ستون اضافه کرده‌ایم.

نتیجه:



بله! کار کرد!

ب) استفاده از PdfTable و اعمال تنظیمات راست به چپ آن
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
table.ExtendLastRow = true;

PdfPCell pdfCell = new PdfPCell(new Phrase("آزمایش", tahomaFont));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

table.AddCell(pdfCell);
pdfDoc.Add(table);
}
}
}
}

در حین استفاده از PdfTable هم لازم است تا RunDirection مربوط به خود جدول و همچنین هر سلول اضافه شده به آن به RTL تنظیم شوند.

این نکات در هر جایی که با این کتابخانه سر و کار داریم باید اعمال شوند. برای مثال:

افزودن Header به صفحات Pdf :
افزودن header در نگارش‌های جدید iTextSharp شامل نکته استفاده از کلاس PdfPageEventHelper به شرح زیر است (و مثال‌هایی را که در وب پیدا خواهید کرد، هیچکدام با آخرین نگارش موجود iTextSharp کار نمی‌کنند):
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace iTextSharpTests
{
public class PageEvents : PdfPageEventHelper
{
Font _font;
public PageEvents()
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
_font = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);
}

public override void OnStartPage(PdfWriter writer, Document document)
{
base.OnStartPage(writer, document);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

PdfPCell pdfCell = new PdfPCell(new Phrase("سر صفحه در صفحه: " + writer.PageNumber, _font));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
pdfCell.HorizontalAlignment = Element.ALIGN_CENTER;

table.AddCell(pdfCell);
document.Add(table);
}
}

class Program
{
static void Main(string[] args)
{
using (var pdfDoc = new Document(PageSize.A4))
{
var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream("Test.pdf", FileMode.Create));
pdfWriter.PageEvent = new PageEvents();
pdfDoc.Open();

var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);

PdfPTable table = new PdfPTable(numColumns: 1);
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
table.ExtendLastRow = true;

PdfPCell pdfCell = new PdfPCell(new Phrase("آزمایش", tahomaFont));
pdfCell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;

table.AddCell(pdfCell);
pdfDoc.Add(table);

pdfDoc.NewPage();
}
}
}
}

نتیجه:



تنها نکته‌ای که اینجا اضافه شده، تعریف کلاس PageEvents است که از کلاس PdfPageEventHelper مشتق شده است. در این کلاس می‌توان یک سری متد کلاس پایه را تحریف کرد و header و footer و غیره را اضافه نمود. سپس جهت اضافه کردن آن، pdfWriter.PageEvent باید مقدار دهی شود.
در اینجا هم اگر نوع فونت، encoding و PdfTable به همراه RunDirection آن اضافه نمی‌شد، یا چیزی در header صفحه قابل مشاهده نبود یا متن مورد نظر معکوس نمایش داده می‌شد.

نظرات مطالب
مقدار دهی اولیه‌ی بانک اطلاعاتی توسط Entity framework Core
یک نکته‌ی تکمیلی:
در EF Core به صورت پیش فرض Backing Fields در setter مرتبط به Property‌ها اجرا نمی‌شوند ولی در صورتی که بخواهیم این حالت پیش فرض را تغییر دهیم به عنوان مثلا فرض کنید فیلد آدرس رو اعتباری سنجی کنید کافی است تغییرات زیر را برای Property موردنظر اعمال کنیم:
modelBuilder.Entity<Blog>()
    .Property(b => b.Url)
    .HasField("_validatedUrl")
    .UsePropertyAccessMode(PropertyAccessMode.Field);
مثال:
public class Blog
{
    private string _validatedUrl;

    public int BlogId { get; set; }

    public string GetUrl()
    {
        return _validatedUrl; 
    }

    public void SetUrl(string url)
    {
        using (var client = new HttpClient())
        {
            var response = client.GetAsync(url).Result;
            response.EnsureSuccessStatusCode();
        }

        _validatedUrl = url;
    }
}
در صورتی که بخواهیم این حالت پیش فرض را برای تمامی Property‌ها تغییر دهیم کافی است به ابتدای متد OnModelCreating دستور زیر را اضافه نماییم:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Property);

مطالب
تبدیل pdf به تصویر توسط GhostScript

در  مطلبی که در همین سایت اشاره شد با استفاده از Adobe Acrobat می‌توان فایل‌های pdf  را به تصویر تبدیل کرد اما چون نیاز بود تا در وب از آن استفاده کنیم و گاهی اوقات امکان نصب Adobe Acrobat Sdk در سرور وجود ندارد می‌توان از روش زیر نیز استفاده کرد.

ابتدا فایل gsdll32.dll را در پوشه bin  پروژه کپی کنید ( این فایل به همراه مثال ارائه شده وجود دارد).

سپس برای متدهای موردنیاز موجود در Api که بصورت Unmanaged می‌باشند یکسری wrapper ایجاد میکنیم. این متدها شامل :

  1. gsapi_new_instance که برای ایجاد یک نمونه جدید از api بکار می‌رود.
  2.  gsapi_init_with_args که برای مقداردهی نمونه ایجاد شده بوسیله آرگومان‌ها بکار می‌رود .
  3. gsapi_delete_instance  و  gsapi_exit برای آزادسازی منابع ایجاد شده. 

در زیر چند آرگومان مهم که باید به api ارسال شوند نیز آمده است:

 -sDEVICE  فرمت تصویر خروجی 
 dFirstPage-  صفحه آغازین برای تبدیل 
dLastPage -  صفحه پایانی برای تبدیل 
dDEVICEWIDTHPOINTS-  اندازه width فایل pdf 
dDEVICEHEIGHTPOINTS- اندازه height فایل pdf
 dDEVICEXRESOLUTION-  resolutionX
-dDEVICEYRESOLUTION  resolutionY
sOutputFile- مسیر فایل(های) خروجی 
  مسیر فایل ورودی 
نکته اول : برای حالتی که قصد دارید بیش از یک صفحه از فایل pdf را به تصویر تبدیل کنید، کافیست در هنگام مقداردهی به پارامتر –sOutputFile درهرکجای آن علامت %d را قرار دهید تا بطور خودکار شمارنده ای برای نام فایل‌ها در نظر گرفته شود. بطور مثال img%d باعث می‌شود که تصاویر تولید شده بصورت img1 و img2 و img3 و غیره ایجاد شوند.
 
نکته دوم :
هنگامی که خواستم از این api درون وب استفاده کنم و از آنجا که سیستم عامل windows server 2008 x64 روی سرور نصب بود موقع دریافت خروجی با خطای زیر مواجه می‌شدم:

BadImageFormatException: An attempt was made to load a program with an incorrect format. Exception from HRESULT: 0x8007000B

برای حل این مشکل IIS  را باز میکنیم و بر روی ApplicationPool ای که برای وب سایت خودمان درنظر گرفتیم کلیک راست کرده و گزینه Advanced Setting را انتخاب میکنیم. با باز شدن این دیالوگ گزینه Enable 32-bit Application را به true تنظیم میکنیم. 

پروژه کامل این مثال 

مطالب
امکان تعریف ساده‌تر کلاس‌های Immutable در C# 9.0 با معرفی نوع جدید record
در مطلب معرفی خواص init-only، با روش معرفی خواص immutable آشنا شدیم. نوع جدیدی که به C# 9.0 به نام record اضافه شده‌است، قسمتی از آن بر اساس همان خواص init-only کار می‌کند. به همین جهت مطالعه‌ی آن مطلب، پیش از ادامه‌ی بحث جاری، ضروری است.


چرا در C# 9.0 تا این اندازه بر روی سادگی ایجاد اشیاء Immutable تمرکز شده‌است؟

به شیءای Immutable گفته می‌شود که پس از وهله سازی ابتدایی آن، وضعیت آن دیگر قابل تغییر نباشد. همچنین به کلاسی Immutable گفته می‌شود که تمام وهله‌های ساخته شده‌ی از آن نیز Immutable باشند. نمونه‌ی یک چنین شیءای را از نگارش 1 دات نت در حال استفاده هستیم: رشته‌ها. رشته‌ها در دات نت غیرقابل تغییر هستند و هرگونه تغییری بر روی آن‌ها، سبب ایجاد یک رشته‌ی جدید (یک شیء جدید) می‌شود. نوع جدید record نیز به همین صورت عمل می‌کند.

مزایای وجود Immutability:

- اشیاء Immutable یا غیرقابل تغییر، thread-safe هستند که در نتیجه، برنامه نویسی همزمان و موازی را بسیار ساده می‌کنند؛ چون چندین thread می‌توانند با شیءای کار کنند که دسترسی به آن، تنها read-only است.
- اشیاء Immutable از اثرات جانبی، مانند تغییرات آن‌ها در متدهای مختلف در امان هستند. می‌توانید آن‌ها را به هر متدی ارسال کنید و مطمئن باشید که پس از پایان کار، این شیء تغییری نکرده‌است.
- کار با اشیاء Immutable، امکان بهینه سازی حافظه را میسر می‌کنند. برای مثال NET runtime.، هش رشته‌های تعریف شده‌ی در برنامه را در پشت صحنه نگهداری می‌کند تا مطمئن شود که تخصیص حافظه‌ی اضافی، برای رشته‌های تکراری صورت نمی‌گیرد. نمونه‌ی دیگر آن نمایش حرف "a" در یک ادیتور یا نمایشگر است. زمانیکه یک شیء Immutable حاوی اطلاعات حرف "a"، ایجاد شود، به سادگی می‌توان این تک وهله را جهت نمایش هزاران حرف "a" مورد استفاده‌ی مجدد قرار داد، بدون اینکه نگران مصرف حافظه‌ی بالای برنامه باشیم.
- کار با اشیاء Immutable به باگ‌های کمتری ختم می‌شود؛ چون همواره امکان تغییر حالت درونی یک شیء، توسط قسمت‌های مختلف برنامه، می‌تواند به باگ‌های ناخواسته‌ای منتهی شوند.
- Hash list‌ها که در جهت بهبود کارآیی برنامه‌ها بسیار مورد استفاده قرار می‌گیرند، بر اساس کلیدهایی Immutable قابل تشکیل هستند.


روش تعریف نوع‌های جدید record

کلاس ساده‌ی زیر را در نظر بگیرید:
public class User
{
   public string Name { set; get; }
}
برای تبدیل آن به یک نوع جدید record فقط کافی است واژه‌ی کلیدی class آن‌را با record جایگزین کنیم (به آن nominal record هم می‌گویند):
public record User
{
   public string Name { set; get; }
}
نحوه‌ی کار با آن و وهله سازی آن نیز دقیقا مانند کلاس‌ها است:
var user = new User();
user.Name = "User 1";
و ... در اینجا امکان انتساب مقداری به خاصیت Name وجود دارد؛ یعنی این خاصیت به صورت پیش‌فرض Immutable نیست.

روش تعریف دومی نیز در اینجا میسر است (به آن positional record هم می‌گویند):
public record User(string Name);
با این‌کار، به صورت خودکار یک record جدید تشکیل می‌شود که به همراه خاصیت Name است؛ چیزی شبیه به record قبلی که تعریف کردیم (به همین جهت نیاز است نام آن‌را شروع شده‌ی با حروف بزرگ درنظر بگیریم). با این تفاوت که این record، اینبار دارای سازنده است و همچنین خاصیت Name آن از نوع init-only است. در این حالت است که کل record به صورت immutable معرفی می‌شود؛ وگرنه روش تعریف یک خاصیت معمولی که از نوع init-only نیست (مانند مثال اول)، سبب بروز Immutability نخواهد شد.

برای کار با رکورد دومی که تعریف کردیم باید سازند‌ه‌ی این record را مقدار دهی کرد:
var user = new User("User 1");
// Error: Init-only property or indexer 'User.Name' can only be assigned
// in an object initializer, or on 'this' or 'base' in an instance constructor
// or an 'init' accessor. [CS9Features]csharp(CS8852)
user.Name = "User 1";
و همانطور که ملاحظه می‌کنید، چون خاصیت Name از نوع init-only است و در سازنده‌ی record تعریف شده مقدار دهی شده‌است، دیگر نمی‌توان آن‌را مقدار دهی مجدد کرد. همچنین در اینجا امکان استفاده‌ی از object initializers مانند new User { Name = "User 1" } نیز وجود ندارد؛ چون به همراه یک سازنده‌ی به صورت خودکار تولید شده‌است که خاصیتی init-only را مقدار دهی کرده‌است.


نوع جدید record چه اطلاعاتی را به صورت خودکار تولید می‌کند؟

روش دوم تعریف recordها اگر در نظر بگیریم:
public record User(string Name);
و در این حالت برنامه را کامپایل کنیم، به کدهای زیر که حاصل از دی‌کامپایل است، می‌رسیم:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using CS9Features;

public class User : IEquatable<User>
{
 protected virtual Type EqualityContract
 {
  [System.Runtime.CompilerServices.NullableContext(1)]
  [CompilerGenerated]
  get
  {
   return typeof(User);
  }
 }

 public string Name
 {
  get;
  set/*init*/;
 }

 public User(string Name)
 {
  this.Name = Name;
  base..ctor();
 }

 public override string ToString()
 {
  StringBuilder stringBuilder = new StringBuilder();
  stringBuilder.Append("User");
  stringBuilder.Append(" { ");
  if (PrintMembers(stringBuilder))
  {
   stringBuilder.Append(" ");
  }
  stringBuilder.Append("}");
  return stringBuilder.ToString();
 }

 protected virtual bool PrintMembers(StringBuilder builder)
 {
  builder.Append("Name");
  builder.Append(" = ");
  builder.Append((object?)Name);
  return true;
 }

 [System.Runtime.CompilerServices.NullableContext(2)]
 public static bool operator !=(User? r1, User? r2)
 {
  return !(r1 == r2);
 }

 [System.Runtime.CompilerServices.NullableContext(2)]
 public static bool operator ==(User? r1, User? r2)
 {
  return (object)r1 == r2 || (r1?.Equals(r2) ?? false);
 }

 public override int GetHashCode()
 {
  return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name);
 }

 public override bool Equals(object? obj)
 {
  return Equals(obj as User);
 }

 public virtual bool Equals(User? other)
 {
  return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name);
 }

 public virtual User <Clone>$()
 {
  return new User(this);
 }

 protected User(User original)
 {
  Name = original.Name;
 }

 public void Deconstruct(out string Name)
 {
  Name = this.Name;
 }
}
این خروجی به صورت خودکار تولید شده‌ی توسط کامپایلر، چنین نکاتی را به همراه دارد:
- record‌ها هنوز هم در اصل همان class‌های استاندارد #C هستند (یعنی در اصل reference type هستند).
- این کلاس به همراه یک سازنده و یک خاصیت init-only است (بر اساس تعاریف ما).
- متد ToString آن بازنویسی شده‌است تا اگر آن‌را بر روی شیء حاصل، فراخوانی کردیم، به صورت خودکار نمایش زیبایی را از محتوای آن ارائه دهد.
- این کلاس از نوع  <IEquatable<User است که امکان مقایسه‌ی اشیاء record را به سادگی میسر می‌کند. برای این منظور متدهای GetHashCode و Equals آن به صورت خودکار بازنویسی و تکمیل شده‌اند (یعنی مقایسه‌ی آن شبیه به value-type است).
- این کلاس امکان clone کردن اطلاعات جاری را مهیا می‌کند.
- همچنین به همراه یک متد Deconstruct هم هست که جهت انتساب خواص تعریف شده‌ی در آن، به یک tuple مفید است.

بنابراین یک رکورد به همراه قابلیت‌هایی است که سال‌ها در زبان #C وجود داشته‌اند و شاید ما به سادگی حاضر به تشکیل و تکمیل آن‌ها نمی‌شدیم؛ اما اکنون کامپایلر زحمت کدنویسی خودکار آن‌ها را متقبل می‌شود!


ساخت یک وهله‌ی جدید از یک record با clone کردن آن

اگر به کدهای حاصل از دی‌کامپایل فوق دقت کنید، یک قسمت جدید clone هم با syntax خاصی در آن ظاهر شده‌است:
public virtual User <Clone>$()
{
  return new User(this);
}
زمانیکه یک شیء Immutable است، دیگر نمی‌توان مقادیر خواص آن‌را در ادامه تغییر داد. اما اگر نیاز به اینکار وجود داشت، باید چکار کنیم؟ در C# 9.0 برای ایجاد وهله‌ی جدید معادلی از یک record، واژه‌ی کلیدی جدیدی را به نام with، اضافه کرده‌اند. برای نمونه اگر record زیر را در نظر بگیریم که دارای دو خاصیت نام و سن است:
public record User(string Name, int Age);
وهله سازی متداول آن به صورت زیر خواهد بود:
var user1 = new User("User 1", 21);
اما اگر خواستیم خاصیت سن آن‌را تغییر دهیم، می‌توان با استفاده از واژه‌ی کلیدی with، به صورت زیر عمل کرد:
var user2 = user1 with { Age = 31 };
کاری که در اصل در اینجا انجام می‌شود، ابتدا clone کردن شیء user1 است (یعنی دقیقا یک وهله‌ی جدید از user1 را با تمام اطلاعات قبلی آن در اختیار ما قرار می‌دهد که این وهله، ارجاعی را به شیء قبلی ندارد و از آن منقطع است). بنابراین نام user2، دقیقا همان "User 1" است که پیشتر تنظیم کردیم؛ با این تفاوت که اینبار مقدار سن آن متفاوت است. با استفاده از cloning، هنوز شیء user1 که immutable است، دست نخورده باقی مانده‌است و توسط with می‌توان خواص آن‌را تغییر داد و حاصل کار، یک شیء کاملا جدید است که مکان آن در حافظه، با مکان شیء user1 در حافظه، یکی نیست.


مقایسه‌ی نوع‌های record

در کدهای حاصل از دی‌کامپایل فوق، قسمت عمده‌ای از آن به تکمیل اینترفیس <IEquatable<User پرداخته شده بود. به همین جهت اکنون دو رکورد با مقادیر خواص یکسانی را ایجاد می‌کنیم:
var user1 = new User("User 1", 21);
var user2 = new User("User 1", 21);
سپس یکبار آن‌ها را از طریق عملگر == و بار دیگر به کمک متد Equals، مقایسه می‌کنیم:
Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2));
Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
خروجی هر دو حالت، True است:
user1.Equals(user2) -> True
user1 == user2 -> True
این مورد، یکی از مهم‌ترین تفاوت‌های recordها با classها هستند.
- زمانیکه عملگر == را بر روی شیء user1 و user2 اعمال می‌کنیم، اگر User، از نوع کلاس معمولی باشد، حاصل آن false خواهد بود؛ چون این دو، به یک مکان از حافظه اشاره نمی‌کنند، حتی با اینکه مقادیر خواص هر دو شیء یکی است.
- اما اگر به قطعه کد دی‌کامپایل شده دقت کنید، در یک رکورد که هر چند در اصل یک کلاس است، حتی عملگر == نیز بازنویسی شده‌است تا در پشت صحنه همان متد Equals را فراخوانی کند و این متد با توجه به پیاده سازی اینترفیس <IEquatable<User، اینبار دقیقا مقادیر خواص رکورد را یک به یک مقایسه کرده و نتیجه‌ی حاصل را باز می‌گرداند:
public virtual bool Equals(User? other)
{
   return (object)other != null &&
 EqualityContract == other!.EqualityContract &&
 EqualityComparer<string>.Default.Equals(Name, other!.Name) && 
EqualityComparer<int>.Default.Equals(Age, other!.Age);
}
این متدی است که به صورت خودکار توسط کامپایلر جهت مقایسه‌ی مقادیر خواص رکورد جدید تعریف شده، تشکیل شده‌است. به عبارتی recordها از لحاظ مقایسه، شبیه به value objects عمل می‌کنند؛ هرچند در اصل یک کلاس هستند.

یک نکته: بازنویسی عملگر == در SDK نگارش rc2 فعلی رخ‌داده‌است و در نگارش‌های قبلی preview، اینگونه نبود.


امکان ارث‌بری در recordها

دو رکورد زیر را در نظر بگیرید که اولی به همراه Name است و نمونه‌ی مشتق شده‌ی از آن، خاصیت init-only سن را نیز به همراه دارد:
    public record User
    {
        public string Name { get; init; }

        public User(string name)
        {
            Name = name;
        }
    }

    public record UserWithAge : User
    {
        public int Age { get; init; }

        public UserWithAge(string name, int age) : base(name)
        {
            Age = age;
        }
    }
در اینجا روش دیگر تعریف recordها را ملاحظه می‌کنید که شبیه به کلاس‌ها است و خواص آن init-only هستند. در این حالت اگر مقایسه‌ی زیر را انجام دهیم:
var user1 = new User("User 1");
var user2 = new UserWithAge("User 1", 21);

Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2));
Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
به خروجی زیر خواهیم رسید:
user1.Equals(user2) -> False
user1 == user2 -> False
علت آن را هم پیشتر بررسی کردیم. تساوی رکوردها بر اساس مقایسه‌ی مقدار تک تک خواص آن‌ها صورت می‌گیرد و چون user1 به همراه سن نیست، مقایسه‌ی این دو، false را بر می‌گرداند.

امکان تعریف ارث‌بری رکوردها به صورت زیر نیز وجود دارد و الزاما نیازی به روش تعریف کلاس مانند آن‌ها، مانند مثال فوق نیست:
public abstract record Food(int Calories);
public record Milk(int C, double FatPercentage) : Food(C);


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

در مثال قبلی اگر یک ToString را بر روی اشیاء تشکیل شده فراخوانی کنیم:
Console.WriteLine(user1.ToString());
Console.WriteLine(user2.ToString());
به این خروجی‌ها می‌رسیم:
User { Name = User 1 }
UserWithAge { Name = User 1, Age = 21 }
که حاصل بازنویسی خودکار متد ToString در پشت صحنه است.


امکان استفاده‌ی از Deconstruct در رکوردها

دو روش برای تعریف رکوردها وجود دارند؛ یکی شبیه به تعریف کلاس‌ها است و دیگری تعریف یک سطری، که positional record نیز نامیده می‌شود:
public record Person(string Name, int Age);
 فقط در حالت تعریف یک سطری positional record فوق است که خروجی خودکار نهایی تولیدی، به همراه public void Deconstruct نیز خواهد بود:
public void Deconstruct(out string Name, out int Age)
{
  Name = this.Name;
  Age = this.Age;
}
در این حالت می‌توان از tuples نیز برای کار با آن استفاده کرد:
var (name, age) = new Person("User 1", 21);
واژه‌ی «positional» نیز دقیقا به همین قابلیت اشاره می‌کند که بر اساس موقعیت خواص تعریف شده‌ی در رکورد، امکان Deconstruct آن‌ها به متغیرهای یک tuple وجود دارد. حالت تعریف کلاس مانند رکوردها، nominal نام دارد.


امکان استفاده‌ی از نوع‌های record در ASP.NET Core 5x

سیستم model binding در ASP.NET Core 5x، از نوع‌های record نیز پشتیبانی می‌کند؛ یک مثال:
 public record Person([Required] string Name, [Range(0, 150)] int Age);

 public class PersonController
 {
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
    // ...
   }
 }


پرسش و پاسخ

آیا نوع‌های record به صورت value type معرفی می‌شوند؟
پاسخ: خیر. رکوردها در اصل reference type هستند؛ اما از لحاظ مقایسه، شبیه به value types عمل می‌کنند.

آیا می‌توان در یک کلاس، خاصیتی از نوع رکورد را تعریف کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.

آیا می‌توان در رکوردها، از struct و یا کلاس‌ها جهت تعریف خواص استفاده کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.

آیا می‌توان از واژه‌ی کلیدی with با کلاس‌ها و یا structها استفاده کرد؟
پاسخ: خیر. این واژه‌ی کلیدی در C# 9.0 مختص به رکوردها است.

آیا رکوردها به صورت پیش‌فرض Immutable هستند؟
پاسخ: اگر آن‌ها را به صورت positional records تعریف کنید، بله. چون در این حالت خواص تشکیل شده‌ی توسط آن‌ها از نوع init-only هستند. در غیراینصورت، می‌توان خواص غیر init-only را نیز به تعریف رکوردها اضافه کرد.
نظرات مطالب
کاهش تعداد بار تعریف using ها در C# 10.0 و NET 6.0.
یک نکته‌ی تکمیلی: ترفندی برای معرفی Stubs توسط ویژگی global using statements در unit tests

برای نمونه متد زیر را درنظر بگیرید:
public static string CurrentInvariantMonthName()
    {
        var month = DateTimeOffset.UtcNow.Month;
        return CultureInfo
            .InvariantCulture
            .DateTimeFormat
            .GetMonthName(month);
    }
در کل نوشتن آزمون واحد برای متدهایی که با زمان و خواصی مانند UtcNow و یا Now، کار می‌کنند، مشکل است. در آزمون‌های واحد نیاز داریم تا یک خروجی مشخص را با مقداری از پیش معلوم، مقایسه کنیم تا اطمینان حاصل شود که عملیات صورت گرفته، صحیح است. اما UtcNow هر بار متغیر است.
برای حل این مشکل، با استفاده از ویژگی global using statements و compiler directives، می‌توان دو مفهوم متفاوت را برای زمان ارائه داد:
#if MOCK_TIME
global using DateTimeOffset = StubDateTimeOffset; 
#else
global using DateTimeOffset = System.DateTimeOffset;
#endif

public static class StubDateTimeOffset
{
    private static System.DateTimeOffset? value;
    
    public static System.DateTimeOffset UtcNow 
        => value ?? System.DateTimeOffset.UtcNow ;
    
    public static void Set(System.DateTimeOffset dateTimeOffset) {
        value = dateTimeOffset;
    }

    public static void Reset() => value = null;
}
در اینجا یک فایل خالی cs. ایجاد شده و قطعه کد فوق در آن درج می‌شود. در این حالت اگر توسط تنظیمات کامپایلر در فایل csproj برنامه، MOCK_TIME فعال شود، مفهوم DateTimeOffset، دیگر همان System.DateTimeOffset استاندارد نخواهد بود؛ بلکه از یک کلاس بدلی به نام StubDateTimeOffset تامین می‌شود. برای فعالسازی MOCK_TIME هم می‌توان به صورت زیر عمل کرد که این تعریف، در فایل csproj پروژه‌ی آزمایشی قرار می‌گیرد:
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
   <DefineConstants>MOCK_TIME</DefineConstants>
</PropertyGroup>
پس از این تغییر، اینبار کلاس StubDateTimeOffset، تامین کننده‌ی DateTimeOffset در آزمون‌های واحد خواهد بود و در این آزمون‌ها می‌توان با مقدار دهی DateTimeOffset به صورت زیر، هربار آزمایشات را بر اساس یک UtcNow «مشخص» انجام داد:
// set time
StubDateTimeOffset.Set(new(new(2022, 7, 1)));
مطالب
کار با وب سرویس جاوایی تشخیص ایمیل‌های موقتی در دات نت
یکی از وب سرویس‌های سایت name api، امکان تشخیص موقتی بودن ایمیل مورد استفاده‌ی جهت ثبت نام در یک سایت را فراهم می‌کند. آدرس WSDL آن نیز در اینجا قرار دارد. اگر مطابق معمول استفاده از سرویس‌های وب در دات نت، بر روی ارجاعات پروژه کلیک راست کرده و گزینه‌ی Add service refrence را انتخاب کنیم و سپس آدرس WSDL یاد شده را به آن معرفی کنیم، بدون مشکل ساختار این وب سرویس دریافت و برای استفاده‌ی از آن به یک چنین کدی خواهیم رسید:
var client = new SoapDisposableEmailAddressDetectorClient();
 
var context = new soapContext
{
    //todo: get your API key here: http://www.nameapi.org/en/register/
    apiKey = "test"
};
var result = client.isDisposable(context, "DaDiDoo@mailinator.com"); 
 
if (result.disposable.ToString() == "YES")
{
    Console.WriteLine("YES! It's Disposable!");
}
متد isDisposable ارائه شده‌ی توسط این وب سرویس، دو پارامتر context که در آن باید API Key خود را مشخص کرد و همچنین آدرس ایمیل مورد بررسی را دریافت می‌کند. اگر به همین ترتیب این پروژه را اجرا کنید، با خطای Bad request از طرف سرور متوقف خواهید شد:
 Additional information: The remote server returned an unexpected response: (400) Bad Request.
اگر به خروجی این وب سرویس در فیدلر مراجعه کنیم، چنین شکلی را خواهد داشت:
 <html><head><title>Bad Request</title></head><body><h1>Bad Request</h1><p>No api-key provided!</p></body></html>
عنوان کرده‌است که api-key را، در درخواست وب خود ذکر نکرده‌ایم.
اگر همین وب سرویس را توسط امکانات سایت http://wsdlbrowser.com بررسی کنید، بدون مشکل کار می‌کند. اما تفاوت در کجاست؟
خروجی ارسالی به سرور، توسط سایت http://wsdlbrowser.com به این شکل است:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/">
  <SOAP-ENV:Body>
    <ns1:isDisposable>
      <context>
        <apiKey>test</apiKey>       
      </context>
      <emailAddress>sdsdg@site.com</emailAddress>
    </ns1:isDisposable>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
و نمونه‌ی تولید شده‌ی توسط WCF (امکان Add service reference در حقیقت یک WCF Client را ایجاد می‌کند) به صورت زیر می‌باشد:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
     <isDisposable xmlns="http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/">
          <context xmlns=""><apiKey>test</apiKey></context>
          <emailAddress xmlns="">DaDiDoo@mailinator.com</emailAddress>
      </isDisposable>
  </s:Body>
</s:Envelope>
از لحاظ اصول XML، خروجی تولیدی توسط WCF هیچ ایرادی ندارد. از این جهت که نام فضای نام مرتبط با http://schemas.xmlsoap.org/soap/envelope/ را به s تنظیم کرده‌است و سپس با استفاده از این نام، Envelope را تشکیل داده‌است. اما ... این وب سرور جاوایی دقیقا با نام SOAP-ENV کار می‌کند و فضای نام ns1 بعدی آن. کاری هم به اصول XML ندارد که باید بر اساس نام xmlns ذکر شده، کار Parse ورودی دریافتی صورت گیرد و نه بر اساس یک رشته‌ی ثابت از پیش تعیین شده. بنابراین باید راهی را پیدا کنیم تا بتوان این s را تبدیل به SOAP-ENV کرد.

برای این منظور به سه کلاس ذیل خواهیم رسید:
public class EndpointBehavior : IEndpointBehavior
{
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    { }
 
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    { }
 
    public void Validate(ServiceEndpoint endpoint)
    { }
 
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new ClientMessageInspector());
    }
}
 
public class ClientMessageInspector : IClientMessageInspector
{
    public void AfterReceiveReply(ref Message reply, object correlationState)
    { }
 
    public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel)
    {
        request = new MyCustomMessage(request);
        return request;
    }
}
 
/// <summary>
/// To customize WCF envelope and namespace prefix
/// </summary>
public class MyCustomMessage : Message
{
    private readonly Message _message;
 
    public MyCustomMessage(Message message)
    {
        _message = message;
    }
 
    public override MessageHeaders Headers
    {
        get { return _message.Headers; }
    }
 
    public override MessageProperties Properties
    {
        get { return _message.Properties; }
    }
 
    public override MessageVersion Version
    {
        get { return _message.Version; }
    }
 
    protected override void OnWriteStartBody(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("Body", "http://schemas.xmlsoap.org/soap/envelope/");
    }
 
    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
        _message.WriteBodyContents(writer);
    }
 
    protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer)
    {
        writer.WriteStartElement("SOAP-ENV", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
        writer.WriteAttributeString("xmlns", "ns1", null, value: "http://disposableemailaddressdetector.email.services.v4_0.soap.server.nameapi.org/");
    }
}
که پس از تعریف client به نحو ذیل معرفی می‌شوند:
 var client = new SoapDisposableEmailAddressDetectorClient();
client.Endpoint.Behaviors.Add(new EndpointBehavior());
توسط EndpointBehavior سفارشی، می‌توان به متد OnWriteStartEnvelope دسترسی یافت و سپس s آن‌را با SOAP-ENV درخواستی این وب سرویس جایگزین کرد. اکنون اگر برنامه را اجرا کنید، بدون مشکل کار خواهد کرد و دیگر پیام یافت نشدن API-Key را صادر نمی‌کند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب دوره‌ها
جلوگیری از deadlock در برنامه‌های async
توضیح مطلب جاری نیاز به یک مثال دارد. به همین جهت یک برنامه‌ی WinForms یا WPF را آغاز کنید (تفاوتی نمی‌کند). سپس یک دکمه و یک برچسب را در صفحه قرار دهید. در ادامه کدهای فرم را به نحو ذیل تغییر دهید.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json.Linq;

namespace Async13
{
    public static class JsonExt
    {
        public static async Task<JObject> GetJsonAsync(this Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri);
                return JObject.Parse(jsonString);
            }
        }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnGo_Click(object sender, EventArgs e)
        {
            var url =
                "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo";
            txtResult.Text = new Uri(url).GetJsonAsync().Result.ToString();
        }
    }
}
این کدها برای کامپایل نیاز به نصب بسته‌ی
 PM> Install-Package Newtonsoft.Json
و همچنین افزودن ارجاعی به اسمبلی استاندارد System.Net.Http نیز دارند.
در اینجا قصد داریم اطلاعات JSON دریافتی را در یک TextBox نمایش دهیم. کاری که انجام شده، فراخوانی متد async ایی است به نام GetJsonAsync و سپس استفاده از خاصیت Result این Task برای صبر کردن تا پایان عملیات.
اگر برنامه را اجرا کنید و بر روی دکمه‌ی دریافت اطلاعات کلیک نمائید، برنامه قفل خواهد کرد. چرا؟
البته تفاوتی هم نمی‌کند که این یک برنامه‌ی دسکتاپ است یا یک برنامه‌ی وب. در هر دو حالت یک deadlock کامل را مشاهده خواهید کرد.


علت بروز deadlock در کدهای async چیست؟

همواره نتیجه‌ی await، در context فراخوان آن بازگشت داده می‌شود. اگر برنامه‌ی دسکتاپ است، این context همان ترد اصلی UI برنامه می‌باشد و اگر برنامه‌ی وب است، این context، زمینه‌ی درخواست در حال پردازش می‌باشد.
خاصیت Result و یا استفاده از متد Wait یک Task، به صورت همزمان عمل می‌کنند و نه غیرهمزمان. متد GetJsonAsync یک Task ناتمام را که فراخوان آن باید جهت پایان‌اش صبر کند، بازگشت می‌دهد. سپس در همینجا کد فراخوان، تردجاری را توسط فراخوانی خاصیت Result قفل می‌کند. متد GetJsonAsync منتظر خواهد ایستاد تا این ترد آزاد شده و بتواند به کارش که بازگردان نتیجه‌ی عملیات به context جاری است، ادامه دهد.
به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجه‌ی کار یک deadlock است.


روش‌های جلوگیری از deadlock در کدهای async؟

الف) در مورد متد ConfigureAwait در قسمت‌های قبل بحث شد و به عنوان یک best practice مطرح است:
    public static class JsonExt
    {
        public static async Task<JObject> GetJsonAsync(this Uri uri)
        {
            using (var client = new HttpClient())
            {
                var jsonString = await client.GetStringAsync(uri).ConfigureAwait(continueOnCapturedContext: false);
                return JObject.Parse(jsonString);
            }
        }
    }
با استفاده از ConfigureAwait false سبب خواهیم شد تا نتیجه‌ی عملیات به context جاری بازگشت داده نشود و نتیجه بر روی thread pool thread ادامه یابد. با اعمال این تغییر، کدهای متد btnGo_Click بدون مشکل اجرا خواهند شد.

ب) راه حل دوم، عدم استفاده از خواص و متدهای همزمان با متدهای غیر همزمان است:
        private async void btnGo_Click(object sender, EventArgs e)
        {
            var url =
                "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo";
            var data = await new Uri(url).GetJsonAsync();
            txtResult.Text = data.ToString();
        }
ابتدا امضای متد رویدادگردان را اندکی تغییر داده و واژه‌ی کلیدی async را به آن اضافه می‌کنیم. سپس از await برای صبر کردن تا پایان عملیات متد GetJsonAsync استفاده خواهیم کرد. صبر کردنی که در اینجا انجام شده، یک asynchronous waits است؛ برخلاف روش همزمان استفاده از خاصیت Result یا متد Wait.


خلاصه‌ی بحث
Await را با متدهای همزمان Wait یا خاصیت Result بلاک نکنید. در غیراینصورت در ترد اجرا کننده‌ی دستورات، یک deadlock رخ‌خواهد داد؛ زیرا نتیجه‌ی await باید به context جاری بازگشت داده شود اما این context توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شده‌است.
نظرات مطالب
ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax
برای این View که از ویژگی multiple مربوط به HTML5 استفاده می‌کند و با نام کنترل files
@using (Html.BeginForm("Multiple", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })) { 
    <input type="file" name="files" multiple />
    <button class="btn btn-default">Upload</button>   
}
و یا این View که سه کنترل هم نام (با نام files) ارسال فایل را تعریف کرده‌است:
@using (Html.BeginForm("Multiple", "Home", FormMethod.Post, new { enctype = "multipart/form-data" })) {  
    <input type="file" name="files" /><br />
    <input type="file" name="files" /><br />
    <input type="file" name="files" /><br />
    <button class="btn btn-default">Upload</button>   
}
هر دو یک اکشن متد سمت سرور را خواهند داشت (اگر نام کنترل‌های سمت کلاینت یکسان باشند، در سمت سرور، یک آرایه را تشکیل می‌دهند):
[HttpPost]
public ActionResult Multiple(IEnumerable<HttpPostedFileBase> files)
{
    foreach (var file in files)
    {
        if (file != null && file.ContentLength > 0)
        {
            file.SaveAs(Path.Combine(Server.MapPath("/uploads"), Guid.NewGuid() + Path.GetExtension(file.FileName)));
        }
    }
    return View();
}
نظرات مطالب
ایجاد نقشه سایت (Site Map) داینامیک
نیاز به کمی اصلاح دارد. جنریک هندلرها برای معرفی به سیستم مسیریابی باید از طریق یک IRouteHandler معرفی شوند:
// نحوه‌ی معرفی جنریک هندلر به سیستم مسیریابی
public class RouteHandler : System.Web.Routing.IRouteHandler
{
    public IHttpHandler GetHttpHandler(System.Web.Routing.RequestContext requestContext)
    {
        return new Sitemap();
    }
}

// Global.asax file
void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.Add(new Route("sitemap.xml", new RouteHandler()));
}
خلاصه آن به صورت یک متد الحاقی
مطالب
طراحی ValidationAttribute دلخواه و هماهنگ سازی آن با ASP.NET MVC
در سری پست‌های آقای مهندس یوسف نژاد با عنوان Globalization در ASP.NET MVC روشی برای پیاده سازی کار با Resource‌ها در ASP.NET با استفاده از دیتابیس شرح داده شده است. یکی از کمبودهایی که در این روش وجود داشت عدم استفاده از این نوع Resourceها از طریق Attributeها در ASP.NET MVC بود. برای استفاده از این روش در یک پروژه به این مشکل برخورد کردم و پس از تحقیق و بررسی چند پست سرانجام در این پست  پاسخ خود را پیدا کرده و با ترکیب این روش با روش آقای یوسف نژاد موفق به پیاده سازی Attribute دلخواه شدم.
در این پست و با استفاده از سری پست‌های آقای مهندس یوسف نژاد  در این زمینه، یک Attribute جهت هماهنگ سازی با سیستم اعتبار سنجی ASP.NET MVC در سمت سرور و سمت کلاینت (با استفاده از jQuery Validation) بررسی خواهد شد.

قبل از شروع مطالعه سری پست‌های MVC و Entity Framework الزامی است.

برای انجام این کار ابتدا مدل زیر را در برنامه خود ایجاد می‌کنیم.

using System;

public class SampleModel
{
public DateTime StartDate { get; set; }
public string Data { get; set; }
public int Id { get; set; }
با استفاده از این مدل در برنامه در زمان ثبت داده‌ها هیچ گونه خطایی صادر نمی‌شود. برای اینکه بتوان از سیستم خطای پیش فرض ASP.NET MVC کمک گرفت می‌توان مدل را به صورت زیر تغییر داد.
using System;
using System.ComponentModel.DataAnnotations;

public class SampleModel
{
    [Required(ErrorMessage = "Start date is required")]
    public DateTime StartDate { get; set; }

    [Required(ErrorMessage = "Data is required")]
    public string Data { get; set; }

    public int Id { get; set; }
}
حال ویو این مدل را طراحی می‌کنیم.
@model SampleModel
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<section>
    <header>
        <h3>SampleModel</h3>
    </header>
    @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })

    @using (Html.BeginForm("SaveData", "Sample", FormMethod.Post))
    {
        <p>
            @Html.LabelFor(x => x.StartDate)
            @Html.TextBoxFor(x => x.StartDate)
            @Html.ValidationMessageFor(x => x.StartDate)
        </p>
        <p>
            @Html.LabelFor(x => x.Data)
            @Html.TextBoxFor(x => x.Data)
            @Html.ValidationMessageFor(x => x.Data)
        </p>
        <input type="submit" value="Save"/>
    }
</section>
و بخش کنترلر آن را به صورت زیر پیاده سازی می‌کنیم.
 public class SampleController : Controller
    {
        //
        // GET: /Sample/

        public ActionResult Index()
        {
            return View();
        }

        public ActionResult SaveData(SampleModel item)
        {
            if (ModelState.IsValid)
            {
                //save data
            }
            else
            {
                ModelState.AddModelError("","لطفا خطاهای زیر را برطرف نمایید");
                RedirectToAction("Index", item);
            }
            return View("Index");
        }
    }
حال با اجرای این کد و زدن دکمه Save صفحه مانند شکل پایین خطاها را نمایش خواهد داد.

تا اینجای کار روال عادی همیشگی است. اما برای طراحی Attribute دلخواه جهت اعتبار سنجی (مثلا برای مجبور کردن کاربر به وارد کردن یک فیلد با پیام دلخواه و زبان دلخواه) باید یک کلاس جدید تعریف کرده و از کلاس RequiredAttribute ارث ببرد. در پارامتر ورودی این کلاس جهت کار با Resource‌های ثابت در نظر گرفته شده است اما برای اینکه فیلد دلخواه را از دیتابیس بخواند این روش جوابگو نیست. برای انجام آن باید کلاس RequiredAttribute بازنویسی شود.

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

public class VegaRequiredAttribute : RequiredAttribute, IClientValidatable
    {
#region Fields (2) 

        private readonly string _resourceId;
        private String _resourceString = String.Empty;

#endregion Fields 

#region Constructors (1) 

        public VegaRequiredAttribute(string resourceId)
        {
            _resourceId = resourceId;
            ErrorMessage = _resourceId;
            AllowEmptyStrings = true;
        }

#endregion Constructors 

#region Properties (1) 

        public new String ErrorMessage
        {
            get { return _resourceString; }
            set { _resourceString = GetMessageFromResource(value); }
        }

#endregion Properties 

#region Methods (2) 

// Public Methods (1) 

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {

            yield return new ModelClientValidationRule
            {
                ErrorMessage = GetMessageFromResource(_resourceId),
                ValidationType = "required"
            };
        }
// Private Methods (1) 

        private string GetMessageFromResource(string resourceId)
        {
            var errorMessage = HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;
            return errorMessage ?? ErrorMessage;
        }

#endregion Methods 
    }
در این کلاس دو نکته وجود دارد.
1- ابتدا دستور
HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;  

که عنوان کلید Resource را از سازنده کلاس گرفته (کد اقای یوسف نژاد) رشته معادل آن را از دیتابیس بازیابی میکند.

2- ارث بری از اینترفیس IClientValidatable، در صورتی که از این اینترفیس ارث بری نداشته باشیم. Validator طراحی شده در طرف کلاینت کار نمی‌کند. بلکه کاربر با کلیک بروی دکمه مورد نظر داده‌ها را به سمت سرور ارسال می‌کند. در صورت وجود خطا در پست بک خطا نمایش داده خواهد شد. اما با ارث بری از این اینترفیس و پیاده سازی متد GetClientValidationRules می‌توان تعریف کرد که در طرف کلاینت با استفاده از Unobtrusive jQuery  پیام خطای مورد نظر به کنترل ورودی مورد نظر (مانند تکست باکس) اعمال می‌شود. مثلا در این مثال خصوصیت data-val-required به input هایی که قبلا در مدل ما Reqired تعریف شده اند اعمال می‌شود.

حال در مدل تعریف شده می‌توان به جای Required می‌توان از VegaRequiredAttribute مانند زیر استفاده کرد. (همراه با نام کلید مورد نظر در دیتابیس)

public class SampleModel
{
    [VegaRequired("RequiredMessage")]
    public DateTime StartDate { get; set; }

    [VegaRequired("RequiredMessage")]
    public string Data { get; set; }

    public int Id { get; set; }
}
ورودی Validator مورد نظر نام کلیدی است به زبان دلخواه که عنوان آن RequiredMessage تعریف شده است و مقدار آن در دیتابیس مقداری مانند "تکمیل این فیلد الزامی است" است. با این کار در زمان اجرا با استفاده از این ولیدتور ابتدا کلید مورد نظر با توجه به زبان فعلی از دیتابیس بازیابی شده و در متادیتابی مدل ما قرار می‌گیرد. به جای استفاده از Resource‌ها می‌توان پیام‌های خطای دلخواه را در دیتابیس ذخیره کرد و در مواقع ضروری جهت جلوگیری از تکرار از آنها استفاده نمود.
با اجرای برنامه اینبار خروجی به شکل زیر خواهد بود.

جهت فعال ساری اعتبار سنجی سمت کلاینت ابتدا باید اسکریپت‌های زیر به صفحه اضافه شود.
<script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
سپس در فایل web.config تنظیمات زیر باید اضافه شود
<appSettings>
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
سپس برای اعمال Validator طراحی شده باید توسط کدهای جاوا اسکریپت زیر داده‌های مورد نیاز سمت کلاینت رجیستر شوند.
<script type="text/javascript">
        jQuery.validator.addMethod('required', function (value, element, params) {
            if (value == null | value == "") {
                return false;
            } else {
                return true;
            }

        }, '');

        jQuery.validator.unobtrusive.adapters.add('required', {}, function (options) {
            options.rules['required'] = true;
            options.messages['required'] = options.message;
        });
    </script>
البته برای مثال ما قسمت بالا به صورت پیش فرض رجیستر شده است اما در صورتی که بخواهید یک ولیدتور دلخواه و غیر استاندارد بنویسید روال کار به همین شکل است.
موفق و موید باشید.
منابع ^ و ^ و ^ و ^