نظرات مطالب
C# 6 - The nameof Operator
یک نکته‌ی تکمیلی: تکامل اپراتور nameof در C# 12.0

همانطور که در این مطلب مشاهده کردید، اپراتور nameof، روشی بسیار مفید جهت دسترسی به نام متغیرها، نوع‌ها و یا اعضای یک کلاس است. در C# 12، این ویژگی اندکی بهبود یافته‌است و امکان دسترسی به اطلاعات اعضای یک کلاس را هم دارد:
public class NameofClass
{
    public string SomeProperty { get; set; }

    // Now legal with C# 12
    // would show "Length" on the console
    public const string NameOfSomePropertyLength = nameof(SomeProperty.Length); 
    
    public static int StaticField;
    public const string NameOfStaticFieldMinValue  = nameof(StaticField.MinValue);

    [Description($"String {nameof(SomeProperty.Length)}")]
    public int StringLength(string s)
    {
        return s.Length;
    }
}
در این مثال، اگر سعی کنیم مقدار NameOfSomePropertyLength را در کنسول نمایش دهیم، عبارت Length ظاهر خواهد شد. تا پیش از C# 12 برای دسترسی به یک چنین قابلیتی نیاز به نمونه سازی و تولید شیءای از کلاس NameofClass فوق وجود داشت تا بتوان اپراتور nameof را به خواص آن اعمال کرد. این محدودیت در C# 12 برطرف شده‌است.

همچنین همانطور که مشاهده می‌کنید، امکان دسترسی به اطلاعات فیلدهای استاتیک و یا بکارگیری این قابلیت در Attributes هم میسر شده‌است.
مطالب دوره‌ها
کار با AutoMapper زمانیکه نوع منبع داده مورد استفاده مشخص نیست
در سناریوهای متداول نگاشت اشیاء، مشخص است که نوع ViewModel برنامه چیست و معادل Model آن کدام است. اما حالت‌هایی مانند کار با anonymous objects و یا data reader و data table و امثال آن نیز وجود دارند که در این حالت‌ها، نوع منبع داده‌ی مورد استفاده، شیء مشخصی نیست که بتوان آن‌را در قسمت CreateMap مشخص کرد. برای مدیریت یک چنین حالت‌هایی، متد DynamicMap طراحی شده‌است.

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

فرض کنید یک DataTable را با ساختار و داده‌های ذیل در اختیار داریم:
var dataTable = new DataTable("SalaryList");
dataTable.Columns.Add("User", typeof (string));
dataTable.Columns.Add("Month", typeof (int));
dataTable.Columns.Add("Salary", typeof (decimal));
 
var rnd = new Random();
for (var i = 0; i < 200; i++)
  dataTable.Rows.Add("User " + i, rnd.Next(1, 12), rnd.Next(400, 2000));
نوع این DataTable کاملا پویا است و می‌تواند هربار در قسمت‌های مختلف برنامه تعریف متفاوتی داشته باشد.
در ادامه معادل کلاس ساختار ستون‌های این DataTable را به صورت ذیل تهیه می‌کنیم.
public class SalaryList
{
  public string User { set; get; }
  public int Month { set; get; }
  public decimal Salary { set; get; }
}
اکنون می‌خواهیم اطلاعات DataTable را به لیستی جنریک از SalaryList نگاشت کنیم. برای اینکار تنها کافی است از متد DaynamicMap استفاده نمائیم:
var salaryList = AutoMapper.Mapper.DynamicMap<IDataReader, List<SalaryList>>(dataTable.CreateDataReader());
منبع داده را از نوع IDataReader بر اساس متد CreateDataReader مشخص کرده‌ایم. به این ترتیب AutoMapper قادر خواهد بود تا اطلاعات این DataTable را به صورت خودکار پیمایش کند. سپس مقصد را نیز لیست جنریکی از کلاس SalaryList تعیین کرده‌ایم. مابقی کار را متد DynamicMap انجام می‌دهد.
کار با AutoMapper نسبت به راه حل‌های Reflection متداول بسیار سریعتر است. زیرا AutoMapper از مباحث Fast reflection به صورت توکار استفاده می‌کند.


مثال دوم: تبدیل لیستی از اشیاء anonymous به لیستی جنریک

در اینجا قصد داریم یک شیء anonymous را به شیء معادل SalaryList آن نگاشت کنیم. این‌کار را نیز می‌توان توسط متد DynamicMap انجام داد:
var anonymousObject = new
{
  User = "User 1",
  Month = 1,
  Salary = 100000
};
var salary = Mapper.DynamicMap<SalaryList>(anonymousObject);
و یا نمونه‌ی دیگر آن تبدیل یک لیست anonymous به معادل جنریک آن است که به نحو ذیل قابل انجام است:
var anonymousList = new[]
{
  new
  {
   User = "User 1",
   Month = 1,
   Salary = 100000
  },
  new
  {
   User = "User 2",
   Month = 1,
   Salary = 300000
  }
};
var salaryList = anonymousList.Select(item => Mapper.DynamicMap<SalaryList>(item)).ToList();
این نکته در مورد حاصل کوئری‌های LINQ یا IQueryable‌ها نیز صادق است.


مثال سوم: نگاشت پویا به یک اینترفیس

فرض کنید یک چنین اینترفیسی، در برنامه تعریف شده‌است و همچنین دارای هیچ نوع پیاده سازی هم در برنامه نیست:
public interface ICustomerService
{
  string Code { get; set; }
  string Name { get; set; }
}
اکنون قصد داریم یک شیء anonymous را به آن نگاشت کنیم:
var anonymousObject = new
{
  Code = "111",
  Name = "Test 1"
};
var result = Mapper.DynamicMap<ICustomerService>(anonymousObject);
در این حالت خاص، AutoMapper با استفاده از یک Dynamic Proxy به نام LinFu (که با اسمبلی آن Merge شده‌است)، پیاده سازی پویایی را از اینترفیس مشخص شده تهیه کرده و سپس کار نگاشت را انجام می‌دهد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: 
AM_Sample05.zip
نظرات مطالب
NHibernate 3 Beginners Guide
این کتاب تاکید زیادی روی Fluent NHibernate دارد و تقریبا تمام مثال‌های آن با Fluent NHibernate پیاده سازی شده. همچنین ConfOrm رو هم توضیح داده. فوق العاده کتاب پرمحتوایی است! مخصوصا فصل unit test و فصل خطاهای متداول با NH آن عالی است.
نظرات مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
ممنون از راهنماییتون . 
یک سوال دیگه هم برام پیش اومد: اگه نحوه ارتباط کلاس‌ها به صورت زیر باشه:
 public class Kala
    {
        [Key]
        public int Kala_id { get; set; }
        
        [DisplayName("نام کالا")]
        public string Name { get; set; }

        [DisplayName("قیمت خرید")]
        public double Fee_Kharid { get; set; }
     
        public virtual Brand Brand { get; set; }
        
        public ICollection<Anbar_Kala> Anbar_Kalas { get; set; }
    }

 public class Anbar_Kala
    {
        [ForeignKey("Anbar_Id")]
        public virtual Anbar Anbar { get; set; }
        public int Anbar_Id { get; set; }

        [ForeignKey("Kala_Id")]
        public virtual Kala Kala{ get; set; }
        public int Kala_Id { get; set; }

        [DisplayName("تعداد")]
        public int Tedad { get; set; } //تعداد کالاها در هر انبار
    }

public class KalaViewModel 
    {
        public int Kala_Id { get; set; }
        public string  Name { get; set; }
        public double Fee_Kharid { get; set; }
        public string Brand_Name { get; set; }
        public int Tedad { get; set; }
    }

//controller
        var kala = _Kala_Service.GetAllKalas();
        var tedad= _Anbar_Kala_Service.GetAllAnbar_Kalas();

        var kalaviewmodel = EntityMapper.Map<List<KalaViewModel>>(kala, tedad);


protected override void Configure()
        {
        Mapper.CreateMap<Kala, KalaViewModel>()
       .ForMember(des => des.Brand_Name, op => op.MapFrom(src => src.Brand.Brand_Name));
              
       Mapper.CreateMap<Anbar_Kala, KalaViewModel>(); // این نگاشت باید به چه صورتی باشد؟
      .ForMember(des =>des.Kala_Id, op=>op.Ignore();     
        }
تو شرایط فوق که نحوه ارتباط کلاس‌ها به صورت عکس حالت قبله، اگر به صورت بالا نگاشت صورت بگیره، قبل از نگاشت پراپرتی‌ها نال نیستند ولی بعد از نگاشت باز هم پارامترهای مربوط به کلاس کالا که در  Modelview قرار دارند Null میشوند.
نظرات مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
دوست عزیز آوردن پراپرتی Tedad در کلاس KalaViewModel اشتباهه و همچنین map کردن Anbar_kala با آن.
کار زیر را میتونی انجام بدی:
public class Anbar_KalaViewModel
    {
        public Anbar Anbar { get; set; }
        public Kala Kala{ get; set; }
        public int Tedad { get; set; }
    }
//Class Configure
Mapper.CreateMap<Anbar_Kala,Anbar_KalaViewModel>();


پروژه‌ها
شمسی ساز تاریخ اکسپلورر ویندوز
چقدر خوب می‌شد اگر تاریخ و ساعت کنار صفحه ویندوز، شمسی بود


یا اگر لیست فایل‌ها را بررسی می‌کردیم، ستون تاریخ آن‌ها نیز قابل درک بود


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


- با استفاده از برنامه سورس باز ExplorerPCal.exe می‌توانید به این قابلیت‌ها دسترسی پیدا کنید.
- این برنامه برای اجرا نیاز به دات نت فریم ورک 4 دارد. بنابراین بر روی ویندوزهای XP SP3 به بعد قابل اجرا است. ضمنا برنامه ExplorerPCal.exe با هر دو نگارش 32 بیتی و 64 بیتی ویندوز سازگار است.
- برای اجرای آن تنها کافی است فایل «ExplorerPCal.exe» را اجرا کنید. همچنین بهتر است برنامه را در درایو C کپی نکنید.
- برای حذف آن از سیستم، نیاز است پوشه‌ی مربوطه را حذف نمائید. در این حال اگر ویندوز پیام در حال استفاده بودن فایلی را می‌دهد، ویندوز را یکبار ری‌استارت کنید و پس از آن فایل‌های باقیمانده را بدون مشکل می‌توانید حذف نمائید.
- برای ویندوز 10 به این مطلب مراجعه کنید.
مطالب
خواندن اطلاعات از فایل اکسل با استفاده از LinqToExcel
در این مقاله مروری سریع و کاربردی خواهیم داشت بر توانایی‌های مقدماتی LinqToExcel
در ابتدا می‌بایست LinqToExcel را از طریق NuGet به پروژه افزود.
PM> Install-Package LinqToExcel
و یا از طریق solution Explorer گزینه Manage NuGet Packages 

اکنون فایل اکسل ذیل را در نظر بگیرید.

روش خواندن اطلاعات از فایل اکسل فوق تحت فرامین Linq و با مشخص کردن نام sheet مورد نظر  توسط شئ ExcelQueryFactory  بصورت زیر است.

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            var persons = from a in excel.Worksheet(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a["Name"]+" "+a["Family"]);
            }


در صورتیکه بخواهیم انتقال اطلاعات فایل اکسل به جداول بانک اطلاعاتی مانند Sql Server بطور مثال با روش EF Entity Framework را انجام دهیم کلاس زیر با نام person را فرض نمایید.

 public class Person
        {
            public string Name { get; set; }
            public string Family { get; set; }
        }
باید بدانید که بصورت پیشفرض سطر اول از فایل اکسل به عنوان نام ستون انتخاب می‌شود و می‌بایست جهت نگاشت با نام property‌های کلاس ما دقیقاً همنام باشد.

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
            var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            var persons = from a in excel.Worksheet<Person>(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a.Name+" "+a.Family);
            }
  اگر فایل اکسل ما ستون‌های بیشتری داشته باشد تنها ستونهای همنام با propertyهای کلاس ما به کلاس نگاشت پیدا می‌کند و سایر ستونها نادیده گرفته می‌شود.
در صورتیکه نام ستونهای فایل اکسل(سطر اول) با نام property‌های کلاس یکسان نباشد جهت نگاشت آنها در کلاس می‌توان از متد AddMapping استفاده نمود.
 

 string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
            var excel = new ExcelQueryFactory(pathToExcelFile);
            string sheetName = "Sheet1";
            excel.AddMapping("Name","نام");
            excel.AddMapping("Family", "نام خانوادگی");
            var persons = from a in excel.Worksheet<Person>(sheetName) select a;
            foreach (var a in persons)
            {
                MessageBox.Show(a.Name+" "+a.Family);
            }

در کدهای بالا در صورتی که sheetName قید نشود بصورت پیشفرض Sheet1 از فایل اکسل  انتخاب می‌شود.

var persons = from a in excel.Worksheet<Person>() select a;
همچنین می‌توان از اندیس جهت مشخص نمودن Sheet مورد نظر استفاده نمود که اندیس‌ها از صفر شروع می‌شوند.

var persons = from a in excel.Worksheet<Person>(0) select a;
توسط متد GetWorksheetNames می توان نام sheet‌ها را بدست آورد.

public IEnumerable<string> getWorkSheets()
{
string pathToExcelFile = @"C:\Users\MASOUD\Desktop\ExcelFile.xlsx";
    
    var excel = new ExcelQueryFactory(pathToExcelFile);

    return excel.GetWorksheetNames();
}
و توسط متد GetColumnNames   می توان نام ستونها را بدست آورد.  

var SheetColumnNames = excel.GetColumnNames(sheetName);
همانطور که می‌بینید با روش توضیح داده شده در این مقاله به راحتی از فرامین Linq مانند where می‌توان در انتخاب اطلاعات از فایل اکسل استفاده نمود و سپس نتیجه را به جداول مورد نظر انتقال داد.
مطالب
شروع به کار با DNTFrameworkCore - قسمت 2 - طراحی موجودیت‌های سیستم
در قسمت قبل، امکانات این زیرساخت را ملاحظه کردیم. در این مطلب و مطالب آینده، روش طراحی بخش‌های مختلف یکسری سیستم فرضی را با استفاده از امکانات مذکور و با جزئیات بیشتر، بررسی خواهیم کرد.
به منظور اعمال خودکار یکسری مفاهیم توسط زیرساخت، نیاز است موجودیت‌های شما قراردادهای مورد نظر را پیاده سازی کرده باشند یا اینکه از موجودیت‌های پایه که آن قراردادها را پیاده سازی کرده‌اند، به عنوان میانبر، از آنها ارث بری کنید. برای دسترسی به این موجودیت‌های پایه و یکسری واسط که به عنوان قراردادهایی در بخش‌های مختلف این زیرساخت استفاده می‌شوند، نیاز است تا ابتدا بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0

مثال اول: یک موجودیت ساده بدون نیاز به مباحث ردیابی تغییرات

public class MeasurementUnit : Entity<int>, IAggregateRoot
{
   public const int MaxTitleLength = 50;
   public const int MaxSymbolLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Symbol { get; set; }
    public byte[] RowVersion { get; set; }
}

‎کلاس جنریک Entity، در برگیرنده یکسری اعضای مشترک بین سایر موجودیت‌های سیستم از جمله Id و TrackingState (به منظور سناریوهای Master-Detail)، می‌باشد. 

‎نکته: در این زیرساخت برای پیاده سازی CrudService برای یک موجودیت خاص، نیاز است تا واسط IAggregateRoot را نیز پیاده سازی کرده باشد. برای پیاده سازی واسط مذکور نیاز است تا خصوصیت RowVersion را به منظور مدیریت Optimistic مباحث همزمانی، به کلاس بالا اضافه کنیم. این موضوع برای موجودیت‌های وابسته به یک Aggregate ضروری نیست، چرا که آنها با AggregateRoot ذخیره خواهند شد و تراکنش جدایی برای ثبت، ویرایش و یا حذف آنها وجود ندارد.

مثال دوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت و آخرین ویرایش

public class Blog : TrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک TrackableEntity علاوه بر خصوصیات Id و TrackingState، یکسری خصوصیت دیگر از جمله زمان ثبت، زمان آخرین ویرایش، شناسه کاربر ثبت کننده، شناسه آخرین کاربر ویرایش کننده، اطلاعات مرورگرهای آنها و ... را نیز دارا می‌باشد. این خصوصیات به صورت خودکار توسط زیرساخت مقداردهی خواهند شد.


مثال سوم: یک موجودیت به همراه مباحث ردیابی تغییرات ثبت، آخرین ویرایش و حذف نرم

public class Blog : FullTrackableEntity<long>, IAggregateRoot
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
}

کلاس جنریک FullTrackableEntity علاوه بر خصوصیات ذکر شده در مثال دوم، یکسری خصوصیت دیگر از جمله IsDeleted، شناسه کاربر حذف کننده، زمان حذف و ... را نیز دارا می‌باشد. همچنین مباحث فیلتر خودکار رکوردهای حذف شده، به صورت خودکار توسط زیرساخت انجام می‌گیرد که امکان غیرفعال کردن آن در شرایط مورد نیاز نیز وجود دارد.

مثال چهارم: یک موجودیت با پشتیبانی از چند مستاجری

public class Blog : Entity<long>, IAggregateRoot, ITenantEntity
{
    public const int MaxTitleLength = 50;
    public const int MaxUrlLength = 50;
    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Url { get; set; }
    public byte[] RowVersion { get; set; }
    public long TenantId { get; set; }
}

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

مثال پنجم: یک موجودیت به همراه تعدادی موجودیت جزئی (سناریوهای Master-Detail)

public class Invoice : TrackableEntity<long>, IAggregateRoot
{
    public InvoiceStatus Status { get; set; }
    public decimal TotalNet { get; set; }
    public decimal Total { get; set; }
    public decimal PayableTotal { get; set; }
    public decimal Debit { get; set; }
    public decimal Credit { get; set; }
    public decimal Gratuity { get; set; }
    public byte[] RowVersion { get; set; }

    public ICollection<InvoiceItem> Items { get; set; }
}

public class InvoiceItem : TrackableEntity
{
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Price { get; set; }
    public decimal UnitPriceDiscount { get; set; }

    public long ItemId { get; set; }
    public Item Item { get; set; }
    public long InvoiceId { get; set; }
    public Invoice Invoice { get; set; }
}

همانطور که مشخص می‌باشد، موجودیت وابسته یا همان Detail، نیاز به پیاده سازی IAggregateRoot را نخواهد داشت. همانطور که اشاره شد، تراکنش مجزایی برای این موجودیت‌ها نخواهیم داشت و درون تراکنش AggregateRoot، عملیات CRUD آنها انجام خواهد شد و برای انجام عملیات ویرایش، به همراه Root متناظر با خود، واکشی خواهند شد. این موضوع یکی از نقاط قوت زیرساخت محسوب می‌شود که در مقالات آینده و در قسمت طراحی سرویس‌های متناظر با موجودیت‌های سیستم، با جزئیات بیشتری بررسی خواهد شد.

مثال ششم: یک موجودیت با امکان شماره گذاری خودکار

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }
}

همانطور که در مطلب «طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید» ملاحظه کردید، نیاز است تا موجودیت مورد نظر، پیاده ساز واسط INumberedEntity نیز باشد. این واسط دارای خصوصیت رشته‌ای Number می‌باشد و همچنین زیرساخت به صورت خودکار در زمان ثبت، این خصوصیت را برای موجودیت‌هایی از این نوع، با رعایت مباحث همزمانی مقداردهی می‌کند.

مثال هفتم: یک موجودیت با امکان ذخیره سازی اطلاعات اضافی در قالب فیلد JSON

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity, IExtendableEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024;

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Number { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public byte[] RowVersion { get; set; }

    public string ExtensionJson { get; set; }
}

با پیاده سازی واسط IExtendableEntity، یکسری متد الحاقی برروی اشیاء موجودیت مورد نظر فعال خواهند شد که امکان مقداردهی یا خواندن این اطلاعات اضافی را خواهید داشت. به عنوان مثال:

var task = new Task();
task.SetExtensionValue("Name","Value");
var value = task.ReadExtensionValue("Name");
//or any complex object as string json

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


مثال هشتم: طراحی یک نوع شمارشی (Enum)

public class OrderStatus : Enumeration
{
    public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
    public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
    public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
    public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
    public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
    public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());

    protected OrderStatus()
    {
    }

    public OrderStatus(int id, string name)
        : base(id, name)
    {
    }
}

برای سناریوهایی که صرفا قصد انتخاب یک یا چند (حالت enum flags) مورد از بین یک لیست مشخص و سپس ذخیره سازی آنها را دارید، استفاده از نوع داده enum کفایت می‌کند؛ ولی اگر قصد استفاده از آنها برای flow control را دارید، در این صورت به طراحی شکننده‌ای خواهید رسید که پر شده است از if/else هایی که مقادیر مختلف enum مورد نظر را بررسی می‌کنند. با استفاده از کلاس Enumeration امکان مدل کردن انوع شمارشی که مرتبط هستند با منطق تجاری سیستم را با راه حل شیء گرا خواهید داشت. در این صورت رفتارهای متناظر با هریک از فیلدهای یک نوع شمارشی می‌تواند به عنوان رفتاری در دل خود کپسوله شده باشد و اینبار داده و رفتار کنار هم خواهند بود. 

نکته: برای مطالعه بیشتر می‌توانید به مطالب ^ و ^ مراجعه کنید.

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

نیاز به حذف نرم بدون نگهداری اطلاعات ردیابی تغییرات

public interface ISoftDeleteEntity
{
    bool IsDeleted { get; set; }
}

.با پیاده سازی واسط بالا این امکان را خواهید داشت که صرفا از مکانیزم حذف نرم استفاده کنید؛ بدون نیاز به نگهداری سایر اطلاعات

نیاز به مقداردهی خودکار زمان ثبت یک موجودیت خاص 

این امر با پیاده سازی واسط زیر امکان پذیر خواهد بود.

public interface IHasCreationDateTime
{
    DateTimeOffset CreationDateTime { get; set; }
}

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

مطالب دوره‌ها
تبدیلگر تاریخ شمسی برای AutoMapper
فرض کنید مدل معادل با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class User
{
    public int Id { set; get; }
    public string Name { set; get; }
    public DateTime RegistrationDate { set; get; }
}
و ViewModel ایی که قرار است به کاربر نمایش داده شود این ساختار را دارد:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public string RegistrationDate { set; get; }
}
در اینجا می‌خواهیم حین تبدیل User به UserViewModel، تاریخ میلادی به صورت خودکار، تبدیل به یک رشته‌ی شمسی شود. برای مدیریت یک چنین سناریوهایی توسط AutoMapper، امکان نوشتن تبدیلگرهای سفارشی نیز پیش بینی شده‌است.


تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص AutoMapper

در ذیل یک تبدیلگر سفارشی مخصوص AutoMapper را با پیاده سازی اینترفیس ITypeConverter آن ملاحظه می‌کنید:
public class DateTimeToPersianDateTimeConverter : ITypeConverter<DateTime, string>
{
    private readonly string _separator;
    private readonly bool _includeHourMinute;
 
    public DateTimeToPersianDateTimeConverter(string separator = "/", bool includeHourMinute = true)
    {
        _separator = separator;
        _includeHourMinute = includeHourMinute;
    }
 
    public string Convert(ResolutionContext context)
    {
        var objDateTime = context.SourceValue;
        return objDateTime == null ? string.Empty : toShamsiDateTime((DateTime)context.SourceValue);
    }
 
    private string toShamsiDateTime(DateTime info)
    {
        var year = info.Year;
        var month = info.Month;
        var day = info.Day;
        var persianCalendar = new PersianCalendar();
        var pYear = persianCalendar.GetYear(new DateTime(year, month, day, new GregorianCalendar()));
        var pMonth = persianCalendar.GetMonth(new DateTime(year, month, day, new GregorianCalendar()));
        var pDay = persianCalendar.GetDayOfMonth(new DateTime(year, month, day, new GregorianCalendar()));
        return _includeHourMinute ?
            string.Format("{0}{1}{2}{1}{3} {4}:{5}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture), info.Hour.ToString("00"), info.Minute.ToString("00"))
            : string.Format("{0}{1}{2}{1}{3}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture));
    } 
}
ITypeConverter دو پارامتر جنریک را قبول می‌کند. پارامتر اول نوع ورودی و پارامتر دوم، نوع خروجی مورد انتظار است. در اینجا باید خروجی متد Convert را بر اساس آرگومان دوم ITypeConverter مشخص کرد. توسط ResolutionContext می‌توان به برای مثال context.SourceValue که معادل DateTime دریافتی است، دسترسی یافت. سپس این DateTime را بر اساس متد toShamsiDateTime تبدیل کرده و بازگشت می‌دهیم.


ثبت و معرفی تبدیلگرهای سفارشی AutoMapper

پس از تعریف یک تبدیلگر سفارشی AutoMapper، اکنون نیاز است آن‌را به AutoMapper معرفی کنیم:
public class TestProfile1 : Profile
{
    protected override void Configure()
    {
        // این تنظیم سراسری هست و به تمام خواص زمانی اعمال می‌شود
        this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); 
        this.CreateMap<User, UserViewModel>();
     }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
جهت مدیریت بهتر نگاشت‌های AutoMapper ابتدا یک کلاس Profile را آغاز خواهیم کرد و سپس توسط متدهای CreateMap، کار معرفی نگاشت‌ها را آغاز می‌کنیم.
همانطور که مشاهده می‌کنید در اینجا دو نگاشت تعریف شده‌اند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوه‌ی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفته‌است.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایل‌ها اعمال می‌شوند.


بررسی خروجی تبدیلگر سفارشی تاریخ

اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز می‌شود:
Mapper.Initialize(cfg => // In Application_Start()
{
     cfg.AddProfile<TestProfile1>();
});
سپس نحوه‌ی استفاده از متد Mapper.Map همانند قبل خواهد بود:
var dbUser1 = new User
{
    Id = 1,
    Name = "Test",
    RegistrationDate = DateTime.Now.AddDays(-10)
};
 
var uiUser = new UserViewModel();

Mapper.Map(source: dbUser1, destination: uiUser);
در اینجا در حین کار تبدیل و نگاشت dbUser به uiUser، زمانیکه AutoMapper به هر خاصیت DateTime ایی می‌رسد، مقدار آن‌را با توجه به تبدیلگر سفارشی تاریخی که به آن معرفی کردیم، تبدیل به معادل رشته‌ای شمسی می‌کند.


نوشتن تبدیلگرهای غیر سراسری

همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال می‌شود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>()
             .ForMember(userViewModel => userViewModel.RegistrationDate,
                        opt => opt.ResolveUsing(src =>
                        {
                             var dt = src.RegistrationDate;
                             return dt.ToShortDateString();
                        }));
اینبار در همان کلاس پروفایل ابتدای بحث، نگاشت User به ViewModel آن با کمک متد ForMember، سفارشی سازی شده‌است. در اینجا عنوان شده‌است که اگر به خاصیت ویژه‌ی RegistrationDate رسیدی، مقدار آن‌را با توجه به فرمولی که مشخص شده، محاسبه کرده و بازگشت بده. این تنظیم خصوصی است و به کل برنامه اعمال نمی‌شود.


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

اگر می‌خواهید تنظیمات TestProfile1 به کل برنامه اعمال نشود، نیاز است یک MappingEngine جدید و مجزای از MappingEngine سراسری AutoMapper را ایجاد کرد:
var configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
configurationStore.AddProfile<TestProfile1>();
var mapper = new MappingEngine(configurationStore);
mapper.Map(source: dbUser1, destination: uiUser);
به صورت پیش فرض و در پشت صحنه، متد Mapper.Map از یک MappingEngine سراسری استفاده می‌کند. اما می‌توان در یک برنامه چندین MappingEngine مجزا داشت که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample02.zip