مطالب
تهیه گزارشات Crosstab به کمک LINQ

در گزارشات Crosstab، ردیف‌های یک گزارش، تبدیل به ستون‌های آن می‌شوند؛ به همین جهت به آن‌ها Pivot tables هم می‌گویند.
برای مثال فرض کنید که قصد دارید گزارش تعداد ساعت کارکرد را به ازای هر پروژه در طول چند ماه تعیین کنید. گزارش متداول از این نوع اطلاعات، یک لیست بلند بالای بی‌مفهوم است. این گزارش تشکیل شده از صدها رکورد به ازای کارکنان مختلف در پروژه‌های مختلف و ... هیچ ارزش آماری خاصی ندارد. یک گزارش بدوی است. زمانیکه این گزارش را تبدیل به حالت crosstab می‌کنیم، اولین ستون فقط یک شماره پروژه خواهد بود و ستون‌های بعدی، مثلا نام ماه‌ها و مقادیر آن‌ها هم جمع کارکرد افراد بر روی یک پروژه مشخص.

مثال اول) تهیه گزارش Crosstab جمع هزینه‌های واحدهای مختلف به تفکیک ماه

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

using System;

namespace Pivot.Sample1
{
public class Expense
{
public DateTime Date { set; get; }
public string Department { set; get; }
public decimal Expenses { set; get; }
}
}

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

using System;
using System.Collections.Generic;

namespace Pivot.Sample1
{
public class ExpenseDataSource
{
public static IList<Expense> ExpensesDataSource()
{
return new List<Expense>
{
new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },

new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },

new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
};
}
}
}

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


که ... خروجی مطلوبی نیست. در اینجا ما فقط 9 رکورد داریم؛ اما در عمل به ازای هر روز، یک رکورد می‌تواند وجود داشته باشد و این لیست طولانی، هیچ ارزش آماری خاصی ندارد. می‌خواهیم سرستون‌های گزارش ما مطابق جدول زیر باشند:


یعنی اگر سه ماه را در نظر بگیریم با هر تعداد رکورد، فقط سه ردیف به ازای هر ماه باید حاصل شود و ستون‌های دیگر هم نام بخش‌ها یا واحدهای موجود باشند.
برای رسیدن به این خروجی Crosstab، می‌توان کوئری LINQ زیر را به کمک امکانات گروه بندی اطلاعات آن تهیه کرد:

using System.Collections;
using System.Linq;

namespace Pivot.Sample1
{
public class PivotTable
{
public static IList ExpensesCrossTab()
{
return ExpenseDataSource
.ExpensesDataSource()
.GroupBy(t =>
new
{
Year = t.Date.Year,
Month = t.Date.Month
})
.Select(myGroup =>
new
{
//Year = myGroup.Key.Year,
Month = myGroup.Key.Month,
ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
})
.ToList();
}
}
}

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


اگر علاقمند باشید که مثال فوق را در برنامه‌ی LINQPad آزمایش کنید، این فایل را دریافت نموده و در آن برنامه باز نمائید.


مثال دوم) تهیه لیست Crosstab حضور و غیاب افراد در طول یک هفته

کلاس StudentStat را جهت ثبت اطلاعات حضور یک دانشجو، می‌توان به شکل زیر تعریف کرد:

using System;

namespace Pivot.Sample2
{
public class StudentStat
{
public int Id { set; get; }
public string Name { set; get; }
public DateTime Date { set; get; }
public bool IsPresent { set; get; }
}
}

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

using System;
using System.Collections.Generic;

namespace Pivot.Sample2
{
public class StudentsStatDataSource
{
public static IList<StudentStat> CreateMonthlyReportDataSource()
{
var result = new List<StudentStat>();
var rnd = new Random();

for (int day = 1; day < 6; day++)
{
for (int student = 1; student < 6; student++)
{
result.Add(new StudentStat
{
Id = student,
Date = new DateTime(2011, 11, day),
IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
Name = "student " + student
});
}
}

return result;
}
}
}

خروجی این گزارش هم در این حالت ساده با 5 دانشجو و فقط 5 روز، 25 رکورد خواهد بود:


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


برای رسیدن به این خروجی Crosstab، مثلا می‌توان از کوئری LINQ زیر کمک گرفت که بر اساس شماره دانشجویی اطلاعات را گروه بندی کرده است:

using System.Collections;
using System.Linq;

namespace Pivot.Sample2
{
public class PivotTable
{
public static IList StudentsStatCrossTab()
{
return StudentsStatDataSource
.CreateWeeklyReportDataSource()
.GroupBy(x =>
new
{
x.Id
})
.Select(myGroup =>
new
{
myGroup.Key.Id,
Name = myGroup.First().Name,
Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
})
.ToList();
}
}
}

و این کوئری خروجی زیر را تولید می‌کند که از هر لحاظ نسبت به لیست قبلی مفهوم‌تر است:


فایل LINQPad این مثال را می‌توانید از اینجا دریافت کنید.

مطالب
تهیه گزارشات Crosstab به کمک LINQ - قسمت دوم

اگر به قسمت اول «تهیه گزارشات Crosstab به کمک LINQ» دقت کرده باشید، یک مشکل کوچک دارد و آن هم لزوم مشخص سازی دقیق ستون‌هایی است که می‌خواهیم در گزارش ظاهر شوند. مثلا دقیقا مشخص کنیم که نام واحد چیست یا دقیقا روز را مشخص کنیم. این مورد برای گزارش‌های کوچک مشکلی ندارد؛ ولی اگر همان مثال دوم را در نظر گرفته و بازه را کمی بیشتر کنیم، مثلا یک ماه، آن وقت باید حداقل 30 بار بنویسیم Day1IsPresent تا ... Day30IsPresent و یا اگر بازه‌ی گزارشگیری به اختیار کاربر باشد آن وقت چه باید کرد؟ مثلا یکبار 7 روز پایان ماه را انتخاب کند، یکبار 14 روز را، شاید یک بار هم مثلا 90 روز را مد نظر داشته باشد (تعداد ستون‌ها متغیر باشد یا به عبارتی Dynamic Crosstab نیاز است ایجاد شود).
برای حل این مساله، می‌توان از متد الحاقی زیر از سایت extensionmethod.net کمک گرفت:

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

namespace PivotExtensions
{
public static class Ext
{
public static Dictionary<TKey1, Dictionary<TKey2, TValue>>
Pivot<TSource, TKey1, TKey2, TValue>
(
this IEnumerable<TSource> source,
Func<TSource, TKey1> key1Selector,
Func<TSource, TKey2> key2Selector,
Func<IEnumerable<TSource>, TValue> aggregate
)
{
return source.GroupBy(key1Selector)
.Select(
key1Group => new
{
Key = key1Group.Key,
Value = key1Group.GroupBy(key2Selector)
.Select(
key2Group => new
{
K = key2Group.Key,
V = aggregate(key2Group)
})
.ToDictionary(e => e.K, o => o.V)
})
.ToDictionary(e => e.Key, o => o.Value);
}
}
}

در این متد:
key1Selector مشخص کننده ستون‌های ثابت و مشخص سمت راست یا چپ (بر اساس جهت صفحه) گزارش است. در سیستم‌های مختلف این ستون‌ها نام‌هایی مانند keyColumn ، leftColumn و Row Heading ممکن است داشته باشند.
key2Selector ستون‌های پویای گزارش را تشکیل می‌دهد. در سایر سیستم‌ها این پارامتر، pivotNameColumn ،VariableColumn ، topField و یا Column Heading هم نامیده می‌شود.
Aggregate در اینجا مشخص می‌کند که مقادیر ستون‌های پویای یاد شده چگونه باید محاسبه شوند.

با توجه به این متد، برای نمونه جهت حل مثال اول قسمت قبل خواهیم داشت:

var list = ExpenseDataSource.ExpensesDataSource();
var pivotList = list.Pivot(
x =>
new
{
x.Date.Year,
x.Date.Month
},
x1 => x1.Department,
x2 => x2.Sum(x => x.Expenses));

با خروجی


فایل LINQPad آن از اینجا قابل دریافت است.


و برای حل مثال دوم قسمت قبل می‌توان نوشت:

var list2 = StudentsStatDataSource.CreateWeeklyReportDataSource();
var lst = list2.Pivot(
x =>
new
{
x.Id,
x.Name
},
x1 => "Day " + x1.Date.Day,
x2 => x2.First().IsPresent);

با خروجی


فایل LINQPad آن از اینجا قابل دریافت است.

نظرات مطالب
ایجاد گزارشات Crosstab در PdfReport
دو نوع crosstab داریم. در حالت معمول مانند ExpensesCrosstab که ستون‌های خروجی مشخص هستند می‌تونید از همان متد column.PropertyName استفاده کنید و به همراه سایر تنظیمات دیگر ستون. در حالت پیشرفته crosstab پویا که تعداد ستون‌های خروجی مشخص نیستند و هربار می‌تواند متغیر باشد مثل گزارش ساعات کاری، یک روش این است که نام خاصیت‌ها را کمی واضح‌تر انتخاب کرد. فارسی هم مجاز است:
 list.Pivot(x =>
                new
                {
                  نام‌ستون = 

مطالب
استفاده از Data Annotations جهت تعریف خواص ستون‌ها در PdfReport
در مطلب «تولید پویای ستون‌ها در PdfReport» عنوان شد که ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. همچنین به کمک متد MainTableAdHocColumnsConventions می‌توان بر اساس نوع‌های داده‌ای، بر روی نحوه نمایش ستون‌ها تاثیر گذاشت. برای مثال هرجایی DateTime مشاهده شد، به صورت خودکار تبدیل به تاریخ شمسی شود.
روش دیگری که این روزها در اکثر فریم‌های دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیت‌های تعریف شده کلاس‌ها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده می‌کند (در تولید پویای کد، یا اعتبار سنجی‌های سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدل‌های اینگونه فریم ورک‌ها، اکثر ویژگی‌ها و Data Annotations متداول را نیز می‌توان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آن‌ها خواهیم پرداخت.

آشنایی با مدل‌های بکار رفته در مثال جاری:
using System.ComponentModel;

namespace PdfReportSamples.Models
{
    public enum JobTitle
    {
        [Description("Grunt")]
        Grunt,

        [Description("Programmer")]
        Programmer,

        [Description("Analyst Programmer")]
        AnalystProgrammer,

        [Description("Project Manager")]
        ProjectManager,

        [Description("Chief Information Officer")]
        ChiefInformationOfficer,
    }
}
در اینجا یک enum، جهت تعیین سمت شغلی تعریف شده است. برای اینکه بتوان خروجی مطلوبی را در گزارشات شاهد بود، می‌توان از ویژگی Description، جهت تعیین مقدار نمایشی آن‌ها نیز استفاده کرد و این تعاریف در PdfReport خوانده و اعمال می‌شوند.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using PdfReportSamples.Models;
using PdfRpt.Aggregates.Numbers;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.DataAnnotations;

namespace PdfReportSamples.DataAnnotations
{
    public class Person
    {
        [IsVisible(false)]
        public int Id { get; set; }

        [DisplayName("User name")]
        //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically.
        [ColumnItemsTemplate(typeof(TextBlockField))]
        public string Name { get; set; }

        [DisplayName("Job title")]
        public JobTitle JobTitle { set; get; }

        [DisplayName("Date of birth")]
        [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime DateOfBirth { get; set; }

        [DisplayName("Date of death")]
        [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")]
        public DateTime? DateOfDeath { get; set; }

        [DisplayFormat(DataFormatString = "{0:n0}")]
        [CustomAggregateFunction(typeof(Sum))]
        public int Salary { get; set; }

        [IsCalculatedField(true)]
        [DisplayName("Calculated Field")]
        [DisplayFormat(DataFormatString = "{0:n0}")]
        [AggregateFunction(AggregateFunction.Sum)]
        public string CalculatedField { get; set; }

        [CalculatedFieldFormula("CalculatedField")]
        public static Func<IList<CellData>, object> CalculatedFieldFormula =
                                                    list =>
                                                    {
                                                        if (list == null) return string.Empty;
                                                        var salary = (int)list.GetValueOf<Person>(x => x.Salary);
                                                        return salary * 0.8;
                                                    };//Note: It's a static field, not a property.
    }
}
مدل فوق جهت مقدار دهی اطلاعات یک شخص تعریف شده است.
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسب‌های سرستون‌ها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالب‌های ستون‌های سفارشی و یا حتی قالب‌های پیش فرض دیگری که متنی نیستند، وجود دارد، می‌توانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاس‌های پیش فرض قالب‌های ستون‌ها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا می‌توان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونه‌ای از استفاده از آن‌را برای تعیین نحوه نمایش تاریخ، ملاحظه می‌کنید. توسط این ویژگی حتی می‌توان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آن‌ها نیز پیش بینی شده است که در قسمت‌های بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه می‌کنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمی‌توان توسط ویژگی‌ها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)


در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;

namespace PdfReportSamples.DataAnnotations
{
    public static class PersonnelDataSource
    {
        public static IList<Person> CreatePersonnelList()
        {
            return new List<Person>
            {
                new Person
                {
                    Id = 1,
                    Name = "Edward",
                    DateOfBirth = new DateTime(1900, 1, 1),
                    DateOfDeath = new DateTime(1990, 10, 15),
                    JobTitle = JobTitle.ChiefInformationOfficer,
                    Salary = 5000
                },
                new Person
                {
                    Id = 2,
                    Name = "Margaret", 
                    DateOfBirth = new DateTime(1950, 2, 9), 
                    DateOfDeath = null,
                    JobTitle = JobTitle.AnalystProgrammer,
                    Salary = 4000
                },
                new Person
                {
                    Id = 3,
                    Name = "Grant", 
                    DateOfBirth = new DateTime(1975, 6, 13), 
                    DateOfDeath = null,
                    JobTitle = JobTitle.Programmer,
                    Salary = 3500
                }
            };
        }
    }
}
در پایان، نحوه استفاده از منبع داده فوق جهت تامین یک گزارش، به نحو زیر می‌باشد:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.DataAnnotations
{
    public class DataAnnotationsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("new rpt.");
                     defaultHeader.RunDirection(PdfRunDirection.LeftToRight);
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.ClassicTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.FitToContent);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.StronglyTypedList(PersonnelDataSource.CreatePersonnelList());
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("Total");
                 summary.PageSummarySettings("Page Summary");
                 summary.PreviousPageSummarySettings("Pervious Page Summary");
             })
             .MainTableAdHocColumnsConventions(adHocColumns =>
             {
                 adHocColumns.ShowRowNumberColumn(true);
                 adHocColumns.RowNumberColumnCaption("#");
             })
             .Export(export =>
             {
                 export.ToExcel();
                 export.ToXml();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\DataAnnotationsSampleRpt.pdf"));
        }
    }
}
همانطور که مشخص است، از ذکر متد MainTableColumns به علت استفاده از DataAnnotations صرفنظر شده و PdfReport این تعاریف را بر اساس ویژگی‌های خواص کلاس شخص دریافت می‌کند. تنها از متد MainTableAdHocColumnsConventions جهت مشخص سازی اینکه نیاز به نمایش ستون ردیف می‌باشد، استفاده کرده‌ایم.


نظرات مطالب
نمایش تاریخ بر حسب تعداد روزهای گذشته
سلام؛ در قسمتی از سایت، بخش مطالب این ماه قرار داره. شما برای به دست آوردن مطالب این ماه، چطور تاریخ رو محاسبه می‌کنید؟ من خودم به این روش رسیدم:
public class Post
{
       public int Id { get; set; }
       public string Title { get; set; }
       public DateTime dt { get; set; }
}
static void Main(string[] args)
{
       List<Post> ListOfPost = new List<Post>();
       DateTime dt = DateTime.Now;
       PersianCalendar pc = new PersianCalendar();
       int day = pc.GetDayOfMonth(dt);
       int month = pc.GetMonth(dt);
       int year = pc.GetYear(dt);
       int DaysInMonth = pc.GetDaysInMonth(year, month);
       DateTime FirstDayOfCurrentMonth = dt.AddDays(-day).Date;
       DateTime LastDayOfCurrentMonth = 
       FirstDayOfCurrentMonth.AddDays(DaysInMonth);
       var query = ListOfPost
                   .Where(x => x.dt.Date > FirstDayOfCurrentMonth.Date)
                   .Where(x => x.dt.Date <= LastDayOfCurrentMonth.Date)
                   .ToList();
}
این روش بهینه هست ؟
نظرات اشتراک‌ها
اهمیت محل فراخوانی OrderBy و Distinct
ممنون از مطالبتون.
من برنامه ای شبیه زیر نوشته ام:
 class MainClass
    {
        class Student
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }
        static void Main()
        {
            List<Student> s = new List<Student>() { 
            new Student(){Name="A",Age=12},
            new Student(){Name="B",Age=10},
            new Student(){Name="A",Age=5},
            new Student(){Name="A",Age=6},
            }.ToList();
            var model = s.Where(x => x.Name.Contains("A")).Distinct().OrderBy(x => x.Age).ToList();
            Console.Read();
        }
    }

سوالی که داشتم اینه چرا با وجود متد Distinct دستورات فوق، سه رکورد را بر می‌گرداند؟
نظرات مطالب
EF Code First #1
سلام مهندس نصیری، چرا این کد توی EF5 خطای کلید خارجی میده؟
کدش از کتاب Code First که معرفی کردین استفاده کردم اما کد خودتون خطا نداره

using System;
using System.Collections.Generic;
namespace ChapterOneProject
{
public class Patient
    {
        public Patient()
        {
            Visits = new List<Visit>();
        }

        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime BirthDate { get; set; }
        //[ForeignKey("AnimalTypeId")]
        
        public AnimalType AnimalType { get; set; }
        //public int AnimalTypeId { get; set; }

        public DateTime FirstVisit { get; set; }
        public List<Visit> Visits { get; set; }
    }

public class Visit
    {
        [Key]
        public int Id { get; set; }
        public DateTime Date { get; set; }
        public String ReasonForVisit { get; set; }
        public String Outcome { get; set; }
        public Decimal Weight { get; set; }

        //[ForeignKey("PatientId")]
        //public virtual Patient Patient { get; set; }
        public int PatientId { get; set; }
    }

public class AnimalType
    {
        public int Id { get; set; }
        public string TypeName { get; set; }
    }
}

کد کانتکست
public class VetContext : DbContext
    {
        public DbSet<Patient> Patients { get; set; }
        public DbSet<Visit> Visits { get; set; }
        //public DbSet<AnimalType> AnimalTypes { get; set; }
    }
و در تابع Main برنامه Console این نوشته شده اما خطا میده و ثبت نمی‌شه
var dog = new AnimalType { TypeName = "Dog" };
            var visit = new List<Visit>
                            {
                                new Visit
                                    {
                                        Date = new DateTime(2011, 9, 1),
                                        Outcome = "Test",
                                        ReasonForVisit = "Test",
                                        Weight = 32,
                                    }
                            };
            var patient = new Patient
                              {
                                  Name = "Sampson",
                                  BirthDate = new DateTime(2008, 1, 28),
                                  AnimalType = dog,
                                  Visits = visit,
                              };
            using (var context = new VetContext())
            {
                context.Patients.Add(patient);
                context.SaveChanges();
            }

کد‌های دیگه تست کردم مشکلی نداشت اما این مورد ؟
با profiler چک کردم خطای عدم توانایی در تبدیل نوع datetime2 به datetime میده
نظرات مطالب
PersianDateTime جایگزینی برای System.DateTime
کلاس PersianDateTime سازنده‌های متعددی دارد. از سازنده‌ای که نوع DateTime را مستقیما دریافت می‌کند، استفاده کنید:
new PersianDateTime(t.ResponseDate.Value).ToString(PersianDateTimeFormat.DateShortTime)
یعنی با وجود این سازنده، نیازی به ToString و سپس DateTime.Parse آن نیست:
    /// <summary>
    /// Initializes a new instance of the PersianDateTime class to a specified dateTime.
    /// </summary>
    /// <param name="dateTime">A date and time in the Gregorian calendar.</param>
    public PersianDateTime(DateTime dateTime)
    {
        _dateTime = dateTime;
    }
مطالب
بررسی مقدمات کتابخانه‌ی JSON.NET
چرا JSON.NET؟
JSON.NET یک کتابخانه‌ی سورس باز کار با اشیاء JSON در دات نت است. تاریخچه‌ی آن به 8 سال قبل بر می‌گردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شده‌است. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شده‌است و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانه‌ها و فریم ورک‌های دات نتی می‌باشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینت‌های کار با API خود استفاده می‌کنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی می‌کند.

برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
 PM> install-package Newtonsoft.Json

معماری JSON.NET

کتابخانه‌ی JSON.NET از سه قسمت عمده تشکیل شده‌است:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema


الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن می‌باشد که این تنظیمات را به شکل ویژگی‌های خواص نیز می‌توان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شده‌است.
یک مثال:
 var roles = new List<string>
{
   "Admin",
   "User"
};
string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
در اینجا نحوه‌ی استفاده از JSON.NET را جهت تبدیل یک شیء دات نتی، به معادل JSON آن مشاهده می‌کنید. اعمال تنظیم Formatting.Indented سبب خواهد شد تا خروجی آن دارای Indentation باشد. برای نمونه اگر در برنامه‌ی خود قصد دارید فرمت JSON تو در تویی را به نحو زیبا و خوانایی نمایش دهید یا چاپ کنید، همین تنظیم ساده کافی خواهد بود.
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده می‌کنید:
 var jsonString = JsonConvert.SerializeObject(new
{
   Id =1,
   Name = "Test"
}, Formatting.Indented);
به صورت پیش فرض تنها خواص عمومی کلاس‌ها توسط JSON.NET تبدیل خواهند شد.


تنظیمات پیشرفته‌تر JSON.NET

مزیت مهم JSON.NET بر سایر کتابخانه‌ها‌ی موجود مشابه، قابلیت‌های سفارشی سازی قابل توجه آن است. در مثال ذیل نحوه‌ی معرفی JsonSerializerSettings را مشاهده می‌نمائید:
var jsonData = JsonConvert.SerializeObject(new
{
   Id = 1,
   Name = "Test",
   DateTime = DateTime.Now
}, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   Converters =
   {
      new JavaScriptDateTimeConverter()
   }
});
در اینجا با استفاده از تنظیم JavaScriptDateTimeConverter، می‌توان خروجی DateTime استانداردی را به مصرف کنندگان جاوا اسکریپتی سمت کاربر ارائه داد؛ با خروجی ذیل:
 {
  "Id": 1,
  "Name": "Test",
  "DateTime": new Date(1409821985245)
}


نوشتن خروجی JSON در یک استریم

خروجی متد JsonConvert.SerializeObject یک رشته‌است که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره می‌باشد. اما برای رسیدن به حداکثر کارآیی و سرعت می‌توان از استریم‌ها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json"))
{
    var jsonSerializer = new JsonSerializer
   {
      Formatting = Formatting.Indented
   };
   jsonSerializer.Serialize(stream, new
   {
     Id = 1,
     Name = "Test",
     DateTime = DateTime.Now
   });
}
کلاس JsonSerializer و متد Serialize آن یک استریم را نیز جهت نوشتن خروجی می‌پذیرند. برای مثال response.Output برنامه‌های وب نیز یک استریم است و در اینجا نوشتن مستقیم در استریم بسیار سریعتر است از تبدیل شیء به رشته و سپس ارائه خروجی آن؛ زیرا سربار تهیه رشته JSON از آن حذف می‌گردد و نهایتا GC کار کمتری را باید انجام دهد.


تبدیل JSON رشته‌ای به اشیاء دات نت

اگر رشته‌ی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونه‌ای از شیء User ذیل کنیم:
public class User
{
   public int Id { set; get; }
   public string Name { set; get; }
   public DateTime DateTime { set; get; }
}
خواهیم داشت:
 var user = JsonConvert.DeserializeObject<User>(jsonData);
در اینجا از متد DeserializeObject به همراه مشخص سازی صریح نوع شیء نهایی استفاده شده‌است.
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings
{
   Converters = {  new JavaScriptDateTimeConverter() }
});


مقدار دهی یک نمونه یا وهله‌ی از پیش موجود

متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد می‌کند. اگر قصد دارید صرفا تعدادی از خواص یک وهله‌ی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
 JsonConvert.PopulateObject(jsonData, user);


کاهش حجم JSON تولیدی

زمانیکه از متد JsonConvert.SerializeObject استفاده می‌کنیم، تمام خواص عمومی تبدیل به معادل JSON آن‌ها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص می‌شوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings
{
   NullValueHandling = NullValueHandling.Ignore,
   Formatting = Formatting.Indented
});
مورد دیگری که سبب کاهش حجم خروجی نهایی خواهد شد، تنظیم DefaultValueHandling = DefaultValueHandling.Ignore است. در این حالت کلیه خواصی که دارای مقدار پیش فرض خودشان هستند، در خروجی JSON ظاهر نخواهند شد. مثلا مقدار پیش فرض خاصیت int مساوی صفر است. در این حالت کلیه خواص از نوع int که دارای مقدار صفر می‌باشند، در خروجی قرار نمی‌گیرند.
به علاوه حذف Formatting = Formatting.Indented نیز توصیه می‌گردد. در این حالت فشرده‌ترین خروجی ممکن حاصل خواهد شد.


مدیریت ارث بری توسط JSON.NET

در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند می‌باشد، ملاحظه می‌کنید:
public class Employee
{
    public string Name { set; get; }
}

public class Manager : Employee
{
    public IList<Employee> Reports { set; get; }
}
در اینجا هر مدیر لیست کارمندانی را که به او گزارش می‌دهند نیز به همراه دارد. در ادامه نمونه‌ای از مقدار دهی این اشیاء ذکر شده‌اند:
 var employee = new Employee { Name = "User1" };
var manager1 = new Manager { Name = "User2" };
var manager2 = new Manager { Name = "User3" };
manager1.Reports = new[] { employee, manager2 };
manager2.Reports = new[] { employee };
با فراخوانی
 var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
یک چنین خروجی JSON ایی حاصل می‌شود:
{
  "Reports": [
    {
      "Name": "User1"
    },
    {
      "Reports": [
        {
          "Name": "User1"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
این خروجی JSON جهت تبدیل به نمونه‌ی معادل دات نتی خود، برای مثال جهت رسیدن به manager1 در کدهای فوق، چندین مشکل را به همراه دارد:
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شده‌است. این شیء JSON اگر به نمونه‌ی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهله‌ی اصلی که سبب تولید این خروجی JSON شده‌است.

برای حل این دو مشکل، تغییرات ذیل را می‌توان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   TypeNameHandling = TypeNameHandling.Objects,
   PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
با این خروجی:
{
  "$id": "1",
  "$type": "JsonNetTests.Manager, JsonNetTests",
  "Reports": [
    {
      "$id": "2",
      "$type": "JsonNetTests.Employee, JsonNetTests",
      "Name": "User1"
    },
    {
      "$id": "3",
      "$type": "JsonNetTests.Manager, JsonNetTests",
      "Reports": [
        {
          "$ref": "2"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
- با تنظیم TypeNameHandling = TypeNameHandling.Objects سبب خواهیم شد تا خاصیت اضافه‌ای به نام $type به خروجی JSON اضافه شود. این نوع، در حین فراخوانی متد JsonConvert.DeserializeObject جهت تشخیص صحیح نگاشت اشیاء بکار گرفته خواهد شد و اینبار مشخص است که کدام شیء، کارمند است و کدامیک مدیر.
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه می‌گردد. اینبار اگر به گزارش دهنده‌ها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب می‌شود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.

باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.


ویژگی‌های قابل تنظیم در JSON.NET

علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگی‌ها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آن‌ها است:
public class User
{
   public int Id { set; get; }

   [JsonIgnore]
   public string Name { set; get; }

   public DateTime DateTime { set; get; }
}
JsonIgnore سبب می‌شود تا خاصیتی در خروجی نهایی JSON تولیدی حضور نداشته باشد و از آن صرفنظر شود.

- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، می‌توان تنظیم نمود. همچنین گاهی از اوقات کتابخانه‌های جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روش‌های نامگذاری دات نتی پیروی نمی‌کنند، در طراحی خود استفاده می‌کنند. در اینجا می‌توان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { set; get; }
همچنین در اینجا امکان تنظیم Order نیز وجود دارد. برای مثال مشخص کنیم که خاصیت X در ابتدا قرار گیرد و پس از آن خاصیت Y رندر شود.

- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)]
public class User
{
    public int Id { set; get; }

    [JsonProperty]
    public string Name { set; get; }
 
    public DateTime DateTime { set; get; }
}

- با استفاده از ویژگی JsonConverter می‌توان نحوه‌ی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))]
public DateTime DateTime { set; get; }


تهیه یک JsonConverter سفارشی

با استفاده از JsonConverterها می‌توان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor
{
   public int Red { set; get; }
   public int Green { set; get; }
   public int Blue { set; get; }
}

var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
}, Formatting.Indented);
در اینجا علاقمندیم، در حین عملیات serialization، بجای اینکه مقادیر اجزای رنگ تهیه شده به صورت int نمایش داده شوند، کل رنگ با فرمت hex رندر شوند. برای اینکار نیاز است یک JsonConverter سفارشی را تدارک دید:
    public class HtmlColorConverter : JsonConverter
    {

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(HtmlColor);
        }

        public override object ReadJson(JsonReader reader, Type objectType,
                                        object existingValue, JsonSerializer serializer)
        {
            throw new NotSupportedException();
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var color = value as HtmlColor;
            if (color == null)
                return;

            writer.WriteValue("#" + color.Red.ToString("X2")
                + color.Green.ToString("X2") + color.Blue.ToString("X2"));
        }
    }
کار با ارث بری از کلاس پایه JsonConverter شروع می‌شود. سپس باید تعدادی از متدهای این کلاس پایه را بازنویسی کرد. در متد CanConvert اعلام می‌کنیم که تنها اشیایی از نوع کلاس HtmlColor را قرار است پردازش کنیم. سپس در متد WriteJson منطق سفارشی خود را می‌توان پیاده سازی کرد.
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشده‌است.

در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
},  new JsonSerializerSettings
    {
      Formatting = Formatting.Indented,
      Converters = { new HtmlColorConverter() }
    });