نظرات مطالب
تبدیل یک قالب HTML معمولی به قالب React
نکته بسیارمهم : در حین انتقال لینک فایل‌های css و js به صفحه index.html در دایرکتوری public حتما قبل از مسیرها عبارت %PUBLIC_URL%/ یا از  /  را در ابتدای آدرس ذکر کنید.
نمونه:
 <link rel="stylesheet" href="dist/css/adminlte.min.css" />

به
 <link rel="stylesheet" href="%PUBLIC_URL%/dist/css/adminlte.min.css" />
یا
 <link rel="stylesheet" href="/dist/css/adminlte.min.css" />

در صورت عدم اصلاح، در زمان مسیریابی ، زمانی که شاخه جدیدی یا پارامتر جدید به مسیر اضافه شود، آدرس دهی فایل‌ها از کار خواهد افتاد.
به عنوان مثال در چنین آدرس صفحه بدون مشکل در دسترس است:
http://localhost:3000/domain

زمانی که به ادرس زیر تغییر کند مسیر فایل‌های css و js دیگر به درستی تشخیص داده نخواهد شد.
http://localhost:3000/domain/1
این مشکل زمانی که صفحه رفرش شود، خود را نشان میدهد.
مطالب
حذف تگ‌های زاید دریافتی از متون MS-Word

یکی از مشکلاتی که من همیشه با کاربران عادی دارم بحث انتقال مطالب از Word مایکروسافت به ادیتورهای WYSWING تحت وب است. برای مثال شما سایت پویایی را درست کرده‌اید که کاربران می‌توانند مطالب آنرا ویرایش یا کم و زیاد کنند.
اگر مطلب از ابتدا در این نوع ادیتورها تایپ و آماده شود هیچ مشکلی وجود نخواهد داشت چون خروجی اکثر آنها استاندارد است، اما متاسفانه خروجی وب word بسیار مشکل‌زا است (copy/paste معمولی مطالب آن در یک ادیتور تحت وب) و خصوصا برای نمایش تایپ فارسی در وب اصلا مناسب نیست. یعنی هیچ الزامی وجود ندارد که اندازه فونت‌ها در متن نهایی نمایش داده شده در وب یکسان باشند یا خطوط در هم فرو نروند و یا عدم تناسب اندازه قلم متن صفحه با قلم استفاده شده در CSS‌ سایت (که شکل ناهماهنگ و غیرحرفه‌ای را حاصل خواهد کرد) و امثال آن. اینجاست که کار شما زیر سؤال می‌رود! "این برنامه درست کار نمیکنه! متن من به‌هم ریخته شده و امثال این"
این کاربر عادی عموما یک تایپیست است یا یک منشی که به او گفته شده است شما از امروز موظفید مطالبی را در این سایت قرار دهید. بنابراین این کاربر حتما از word استفاده خواهد کرد (برای پیش نویس مطالب). همچنین عموما هم مرورگر "سازمانی" مورد استفاده، هنوز که هنوز است همان IE6 است (در اکثر شرکت‌ها و خصوصا ادارات) و مهم نیست که الان آخرین نگارش IE یا فایرفاکس و تمام هیاهوهای مربوطه به کجا ختم شده‌اند. حتما باید سایت با IE6 هم سازگار باشد. بنابراین از برنامه IE tester غافل نشوید.
و دست آخر شما هم نمی‌توانید به کاربر عادی ثابت کنید که این خروجی وب word اصلا استاندارد نیست (حتما کار شما است که مشکل دارد نه شرکت معظم مایکروسافت!). یا اینکه به آنها بگوئید اصلا مجاز نیستید در وب همانند یک فایل word از چندین نوع قلم مختلف فارسی غیراستاندارد استفاده کنید چون ممکن است کاربری این نوع قلم مورد استفاده شما را نداشته باشد و نمایش نهایی به هم ریخته‌تر از آنی خواهد بود که شما فکرش را می‌کنید! یا اینکه با استفاده از این روش حجم نهایی صفحه حداقل 50 کیلو بایت بیشتر خواهد شد (بدلیل حجم بالای تگ‌های زاید word) و نباید کاربران دایال آپ را فراموش کرد.
مدتی در اینباره جستجو کردم و نتیجه حاصل این بود که تمامی روش‌ها به یک مورد ختم می‌شود: حذف تگ‌های غیراستاندارد word هنگام دریافت مطلب و پیش از ذخیره سازی آن در دیتابیس
یک سری از ادیتورهای متنی تحت وب مانند FCK editor این قابلیت را به صورت خودکار اضافه کرده‌اند و حتی اگر کاربر متنی را از word در آنها Paste کند پیغامی را در همین رابطه دریافت خواهد کرد (شکل زیر) و البته کاربر می‌تواند گزینه لغو یا خیر را نیز انتخاب کند و دوباره همان وضعیت قبل تکرار خواهد شد. (یا حتی دکمه مخصوص کپی از word را هم به نوار ابزار خود اضافه کرده‌اند)



برای این منظور تابع زیر تهیه شده‌است که من همواره از آن استفاده می‌کنم و تا به امروز مشکل پاسخ پس دادن به کاربران عادی را به این صورت حل کرده‌ام!
این تابع تمامی تگ‌های اضافی و غیراستاندارد word متن دریافتی از یک ادیتور WYSWING را حذف می‌کند و به این صورت متن نهایی نمایش داده شده در سایت، تابع CSS مورد استفاده در سایت خواهد شد و نه حجم بالایی از تگ‌های غیراستاندارد word. (ممکن است کاربر در ابتدا کمی جا بخورد ولی مهم نیست! سایت باید استاندارد نمایشی خودش را از CSS آن دریافت کند و نه از تگ‌های word)

using System.Text.RegularExpressions;
/// <summary>
/// Removes all FONT and SPAN tags, and all Class and Style attributes.
/// Designed to get rid of non-standard Microsoft Word HTML tags.
/// </summary>
public static string CleanMSWordHtml(string html)
{
try
{
// start by completely removing all unwanted tags
html = Regex.Replace(html, @"<[/]?(font|span|xml|del|ins|[ovwxp]:\w )[^>]*?>", "", RegexOptions.IgnoreCase);
// then run another pass over the html (twice), removing unwanted attributes
html = Regex.Replace(html, @"<([^>]*)(?:class|lang|style|size|face|[ovwxp]:\w )=(?:'[^']*'|""[^""]*""|[^\s>] )([^>]*)>", "<$1$2>", RegexOptions.IgnoreCase);
html = Regex.Replace(html, @"<([^>]*)(?:class|lang|style|size|face|[ovwxp]:\w )=(?:'[^']*'|""[^""]*""|[^\s>] )([^>]*)>", "<$1$2>", RegexOptions.IgnoreCase);
return RemoveHTMLComments(html);
}
catch
{
return html;
}
}

public static string RemoveHTMLComments(string html)
{
try
{
Regex _Regex = new Regex("((<!-- )((?!<!-- ).)*( -->))(\r\n)*", RegexOptions.Singleline);
return _Regex.Replace(html, string.Empty);
}
catch
{
return html;
}
}

متد RemoveHTMLComments را عمدا جدا قرار دادم تا مشخص‌تر باشد. پس از تمیزکاری اولیه، ممکن است دسته‌گل‌های تیم مایکروسافت به صورت کامنت باقی بمانند که باید آنها را هم تمیز کرد! :)

مطالب
تغییر فضای نام کلاس poco استفاده شده در wcf و از کار افتادن برنامه‌ی مشتری بدون دریافت پیام خطا
چند وقت پیش در پروژه‌ای یک سرویس WCF داشتم که اطلاعاتی را در قالب یک کلاس poco برگشت می‌داد. اخیرا بعد از اصلاحاتی در پروژه متوجه شدم که سرویس کار نمی‌کند. هیچ خطایی هم وجود نداشت. شروع به دیباگ کردم و متوجه شدم که سرویس برنامه اطلاعات را برگشت می‌دهد، اما برنامه‌ی مشتری تعداد اطلاعات دریافتی را صفر اعلام میکند و هیچ خطایی هم گزارش نمی‌شود.
چون اطلاعات در قالب باینری در قسمتی از کلاس poco برگشت می‌شد، ابتدا حدسم حجم فایل بود.
اطلاعات کلاس  poco:
public class OutgoingJob
    {
        public int Id;
        public string JobId;
        public string Subject;
        public string Reciver;
        public byte[][] Attachments;
    }
 تنظیمات سایز ارسال و دریافت رو به حداکثر رسوندم. هیچ فایده ای نداشت. برنامه‌ی مشتری به راحتی به سرویس وصل می‌شد و با سایر متدهایی که خروجی‌های تایپ‌های اصلی مثل bool و string را برمی گرداند کار می‌کرد. فقط با متدی که لیست poco داشت، تعداد لیست اطلاعات دریافتی 0 اعلام می‌شد.
متد WCF برای برگشت اطلاعات و لاگ کردن وقایع:
        public List<OutgoingJob> GetJobsList(int Count)
        {
            LogEvent("GetFaxsList Start...");

            List<OutgoingJob> OutgoingJobs = new List<OutgoingJob>();
           
           // business for fill list

            LogEvent("return job Count = " + OutgoingJobs.Count);
            return OutgoingJobs;
        }
 [TestMethod]
        public void TestMethod1()
        {
            jobService ser = new jobService();
            var listjob = ser.GetJobsList(5);
            Assert.AreNotEqual(0, listjob.Count);
        }
لاگ‌های متد WCF، تعداد را 1 اعلام می‌کند، اما تست، نتیجه را صفر برمی‌گرداند.
بعد از کلی کلنجار با تنظیمات binding و serviceBehaviors  متوجه شدم که اشکال کار یه نکته‌ی کوچک خیلی خیلی ساده است. 
من هیچ تغییری در کلاس‌ها نداده بودم، اما برای مدیریت بهتر پروژه، فضای نام کلاس‌ها را تغییر داده بودم و مسبب همه‌ی مشکلات و وقت کشی‌ها همین بود.
راه حل هم که ساده ست. هنگامی که فضای نام کلاس‌های برگشتی را تغییر می‌دهید، حتما باید update service reference  را در برنامه‌ی مشتری اجرا کنید تا اطلاعات سرویس بروز شود.

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


مدل لغو اعمال

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


یک مثال استفاده از CancellationToken

کدهای زیر، یک فایل حجیم را از مکانی به مکانی دیگر کپی می‌کنند. برای این منظور از متد CopyToAsync که در دات نت 4.5 اضافه شده‌است، استفاده کرده‌ایم؛ زیرا از مکانیزم لغو عملیات پشتیبانی می‌کند.
using System;
using System.IO;
using System.Threading;

namespace Async08
{
    class Program
    {
        static void Main(string[] args)
        {
            var source = @"c:\dir\file.bin";
            var target = @"d:\dir\file.bin";
            using (var inStream = File.OpenRead(source))
            {
                using (var outStream = File.OpenWrite(target))
                {
                    using (var cts = new CancellationTokenSource())
                    {
                        var task = inStream.CopyToAsync(outStream, bufferSize: 4059, cancellationToken: cts.Token);
                        Console.WriteLine("Press 'c' to cancel.");
                        var key = Console.ReadKey().KeyChar;
                        if (key == 'c')
                        {
                            Console.WriteLine("Cancelling");
                            cts.Cancel();
                        }
                        Console.WriteLine("Wating...");
                        task.ContinueWith(t => { }).Wait();
                        Console.WriteLine("Status: {0}", task.Status);
                    }
                }
            }
        }
    }
}
کار با تعریف CancellationTokenSource شروع می‌شود. چون از نوع IDisposable است، نیاز است توسط عبارت using، جهت پاکسازی منابع آن، محصور گردد. سپس در اینجا اگر کاربر کلید c را فشار دهد، متد لغو توکن تعریف شده فراخوانی خواهد شد. این توکن نیز به عنوان آرگومان به متد CopyToAsync ارسال شده‌است.
علت استفاده از ContinueWith در اینجا این است که اگر یک task لغو شود، فراخوانی متد Wait بر روی آن سبب بروز استثناء می‌گردد. به همین جهت توسط ContinueWith یک Task خالی ایجاد شده و سپس بر روی آن Wait فراخوانی گردیده‌است.
همچنین باید دقت داشت که سازنده‌ی CancellationTokenSource امکان دریافت زمان timeout عملیات را نیز دارد. به علاوه متد CancelAfter نیز برای آن طراحی شده‌است. نمونه‌ی دیگری از تنظیم timeout را در قسمت قبل با معرفی متد Task.Delay و استفاده از آن با Task.WhenAny مشاهده کردید.


لغو ظاهری وظایفی که لغو پذیر نیستند

فرض کنید متدی به نام GetBitmapAsync با پارامتر cancellationToken طراحی نشده‌است. در این حالت کاربر قصد دارد با کلیک بر روی دکمه‌ی لغو، عملیات را خاتمه دهد. یک روش حل این مساله، استفاده از متد ذیل است:
    public static class CancellationTokenExtensions
    {
        public static async Task UntilCompletionOrCancellation(Task asyncOp, CancellationToken ct)
        {
            var tcs = new TaskCompletionSource<bool>();
            using (ct.Register(() => tcs.TrySetResult(true)))
            {
                await Task.WhenAny(asyncOp, tcs.Task);
            }
        }
    }
در اینجا از روش Task.WhenAny استفاده شده‌است که در آن دو task ترکیب شده‌اند. Task اول همان وظیفه‌ای اصلی است و task دوم، از یک TaskCompletionSource حاصل شده‌است. اگر کاربر دستور لغو را صادر کند، callback ثبت شده توسط این توکن، اجرا خواهد شد. بنابراین در اینجا TrySetResult به true تنظیم شده و یکی از دو Task معرفی شده در WhenAny خاتمه می‌یابد.
این مورد هر چند task اول را واقعا لغو نمی‌کند، اما سبب خواهد شد تا کدهای پس از await UntilCompletionOrCancellation اجرا شوند.


طراحی متدهای غیرهمزمان لغو پذیر

کلاس زیر را در نظر بگیرید:
    public class CancellationTokenTest
    {
        public static void Run()
        {
            var cts = new CancellationTokenSource();
            Task.Run(async () => await test(), cts.Token);
            Console.ReadLine();
            cts.Cancel();
            Console.WriteLine("Cancel...");
            Console.ReadLine();
        }

        private static async Task test()
        {
            while (true)
            {
                await Task.Delay(1000);
                Console.WriteLine("Test...");
            }
        }
    }
در اینجا cancellationToken متد Task.Run تنظیم شده‌است. همچنین پس از فراخوانی آن، اگر کاربر کلیدی را فشار دهد، متد Cancel این توکن فراخوانی خواهد شد. اما .... خروجی برنامه به صورت زیر است:
Test...
Test...
Test...
 
Cancel...
Test...
Test...
Test...
Test...
بله. وظیفه‌ی شروع شده، لغو شده‌است اما متد test آن هنوز مشغول به کار است.
روش اول حل این مشکل، معرفی پارامتر CancellationToken به متد test و سپس بررسی مداوم خاصیت IsCancellationRequested آن می‌باشد:
public class CancellationTokenTest
    {
        public static void Run()
        {
            var cts = new CancellationTokenSource();
            Task.Run(async () => await test(cts.Token), cts.Token);
            Console.ReadLine();
            cts.Cancel();
            Console.WriteLine("Cancel...");
            Console.ReadLine();
        }

        private static async Task test(CancellationToken ct)
        {
            while (true)
            {
                await Task.Delay(1000, ct);
                Console.WriteLine("Test...");

                if (ct.IsCancellationRequested)
                {
                    break;
                }
            }
            Console.WriteLine("Test cancelled");
        }
    }
در اینجا اگر متد cts.Cancel فراخوانی شود، مقدار خاصیت ct.IsCancellationRequested مساوی true شده و حلقه خاتمه می‌یابد.
روش دوم لغو عملیات، استفاده از متد Register است. هر زمان که توکن لغو شود، callback آن فراخوانی خواهد شد:
        private static async Task test2(CancellationToken ct)
        {
            bool isRunning = true;

            ct.Register(() =>
            {
                isRunning = false;
                Console.WriteLine("Query cancelled");
            });

            while (isRunning)
            {
                await Task.Delay(1000, ct);
                Console.WriteLine("Test...");
            }
            Console.WriteLine("Test cancelled");
        }
این روش خصوصا برای حالت‌هایی مفید است که در آن‌ها از متدهایی استفاده می‌شود که خودشان امکان لغو شدن را نیز دارند. به این ترتیب دیگر نیازی نیست مدام بررسی کرد که آیا مقدار IsCancellationRequested مساوی true شده‌است یا خیر. هر زمان که callback ثبت شده در متد Register فراخوانی شد، یعنی عملیات باید خاتمه یابد.
مطالب
یافتن Contextهای Dispose نشده در Entity framework
این دو متد را در نظر بگیرید:
        private static void disposedContext()
        {
            using (var context = new MyContext())
            {
                Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
            }
        }

        private static void nonDisposedContext()
        {
            var context = new MyContext();
            Debug.WriteLine("Posts count: " + context.BlogPosts.Count());
        }
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشده‌است.

سؤال: در یک برنامه‌ی بزرگ چطور می‌توان لیست Contextهای Dispose نشده را یافت؟

در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی می‌توان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;

namespace EFNonDisposedContext.Core
{
    public class DatabaseInterceptor : IDbConnectionInterceptor
    {
        public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Closed);
        }

        public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Disposed);
        }

        public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext)
        {
            Connections.AddOrUpdate(connection, ConnectionStatus.Opened);
        }
      
        // the rest of the IDbConnectionInterceptor methods ...

    }
}
همانطور که ملاحظه می‌کنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection می‌توان دسترسی یافت.

مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شده‌است؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» می‌توانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی می‌توان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
    public enum ConnectionStatus
    {
        None,
        Opened,
        Closed,
        Disposed
    }

    public class ConnectionInfo
    {
        public string ConnectionId { set; get; }
        public string StackTrace { set; get; }
        public ConnectionStatus Status { set; get; }

        public override string ToString()
        {
            return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace);
        }
    }
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه می‌کنیم.
StackTrace توسط نکته‌ی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کرده‌اند؟ » تهیه می‌شود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص می‌گردد.

در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آن‌ها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشده‌است، مشخص می‌کنند.


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

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

دو کلاس متفاوت برای استخراج متن از فایل‌های PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy

using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
using iTextSharp.text.pdf.parser;

namespace TestReaders
{
    class Program
    {
        private static void readPdf1()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
               var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }

        static void Main(string[] args)
        {
            readPdf1();
        }
    }
}
مثال فوق، متن موجود در تمام صفحات یک فایل PDF را در فایل‌های txt جداگانه‌ای ثبت می‌کند. برای نمونه اگر از PDF پیشنیاز یاد شده استفاده کنیم، خروجی آن به نحو زیر خواهد بود:
 Test
ld Wor llo He
Hello People
علت آن نیز پیشتر بررسی گردید. متن، در این فایل ویژه در مختصات خاصی ترسیم شده است. حاصل از دیدگاه خواننده نهایی بسیار خوانا است؛ اما خروجی hello world متنی جالبی از آن استخراج نمی‌شود. SimpleTextExtractionStrategy دقیقا بر اساس همان عملگر‌های Tj و همچنین منابع صفحه، عبارات را یافته و سر هم می‌کند.


ب) LocationTextExtractionStrategy

همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }
کلاس LocationTextExtractionStrategy هوشمند‌تر عمل کرده و بر اساس عملگرهای هندسی یک فایل PDF، سعی می‌کند جملات و حروف را کنار هم قرار دهد و در نهایت خروجی متنی بهتری را تولید کند. برای نمونه اینبار خروجی متنی حاصل به صورت زیر خواهد بود:
 Test
Hello World
Hello People
این خروجی با آنچه که در صفحه نمایش داده می‌شود تطابق دارد.


استخراج متون فارسی از فایل‌های PDF توسط iTextSharp

روش‌های فوق با PDFهای فارسی هم کار می‌کنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
        static void writePdf2()
        {
            using (var document = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create));
                document.Open();

                FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");
                var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H);

                ColumnText.ShowTextAligned(
                            canvas: writer.DirectContent,
                            alignment: Element.ALIGN_CENTER,
                            phrase: new Phrase("تست می‌شود", tahoma),
                            x: 100,
                            y: 100,
                            rotation: 0,
                            runDirection: PdfWriter.RUN_DIRECTION_RTL,
                            arabicOptions: 0);                
            }

            Process.Start("test.pdf");
        }
از متد فوق، برای تولید یک فایل PDF که متنی فارسی را نمایش می‌دهد استفاده خواهیم کرد. اگر متد readPdf2 را که به همراه LocationTextExtractionStrategy تعریف شده است، بر روی فایل حاصل فراخوانی کنیم، خروجی آن به صورت زیر خواهد بود:
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
برای تبدیل آن به یونیکد خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());                
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اکنون خروجی ثبت شده در فایل متنی حاصل به صورت زیر است:
 ﺩﻮﺷﻲﻣ ﺖﺴﺗ
دقیقا به همان نحوی است که iTextSharp و اکثر تولید کننده‌های PDF فارسی از آن استفاده می‌کنند و اصطلاحا چرخاندن حروف یا تولید Glyph mirrors صورت می‌گیرد. روش‌های زیادی برای چرخاندن حروف وجود دارند. در ادامه از روشی استفاده خواهیم کرد که خود ویندوز در کارهای داخلی‌اش از آن استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;

namespace TestReaders
{
    [SuppressUnmanagedCodeSecurity]
    class GdiMethods
    {
        [DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr hgdiobj);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags);

        [DllImport("GDI32.dll")]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
    }

    [StructLayout(LayoutKind.Sequential)]
    struct GcpResults
    {
        public uint lStructSize;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string lpOutString;
        public IntPtr lpOrder;
        public IntPtr lpDx;
        public IntPtr lpCaretPos;
        public IntPtr lpClass;
        public IntPtr lpGlyphs;
        public uint nGlyphs;
        public int nMaxFit;
    }

    public class UnicodeCharacterPlacement
    {
        const int GcpReorder = 0x0002;
        GCHandle _caretPosHandle;
        GCHandle _classHandle;
        GCHandle _dxHandle;
        GCHandle _glyphsHandle;
        GCHandle _orderHandle;

        public Font Font { set; get; }

        public string Apply(string lines)
        {
            if (string.IsNullOrWhiteSpace(lines))
                return string.Empty;

            return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2);
        }

        public IEnumerable<string> Apply(IEnumerable<string> lines)
        {
            if (Font == null)
                throw new ArgumentNullException("Font is null.");

            if (!hasUnicodeText(lines))
                return lines;

            var graphics = Graphics.FromHwnd(IntPtr.Zero);
            var hdc = graphics.GetHdc();
            try
            {
                var font = (Font)Font.Clone();
                var hFont = font.ToHfont();
                var fontObject = GdiMethods.SelectObject(hdc, hFont);
                try
                {
                    var results = new List<string>();
                    foreach (var line in lines)
                        results.Add(modifyCharactersPlacement(line, hdc));
                    return results;
                }
                finally
                {
                    GdiMethods.DeleteObject(fontObject);
                    GdiMethods.DeleteObject(hFont);
                    font.Dispose();
                }
            }
            finally
            {
                graphics.ReleaseHdc(hdc);
                graphics.Dispose();
            }
        }

        void freeResources()
        {
            _orderHandle.Free();
            _dxHandle.Free();
            _caretPosHandle.Free();
            _classHandle.Free();
            _glyphsHandle.Free();
        }

        static bool hasUnicodeText(IEnumerable<string> lines)
        {
            return lines.Any(line => line.Any(chr => chr >= '\u00FF'));
        }

        void initializeResources(int textLength)
        {
            _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned);
            _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned);
        }

        string modifyCharactersPlacement(string text, IntPtr hdc)
        {
            var textLength = text.Length;
            initializeResources(textLength);
            try
            {
                var gcpResult = new GcpResults
                {
                    lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)),
                    lpOutString = new String('\0', textLength),
                    lpOrder = _orderHandle.AddrOfPinnedObject(),
                    lpDx = _dxHandle.AddrOfPinnedObject(),
                    lpCaretPos = _caretPosHandle.AddrOfPinnedObject(),
                    lpClass = _classHandle.AddrOfPinnedObject(),
                    lpGlyphs = _glyphsHandle.AddrOfPinnedObject(),
                    nGlyphs = (uint)textLength,
                    nMaxFit = 0
                };
                var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder);
                return result != 0 ? gcpResult.lpOutString : text;
            }
            finally
            {
                freeResources();
            }
        }
    }
}
از کلاس فوق در هر برنامه‌ای که راست به چپ را به نحو صحیحی پشتیبانی نمی‌کند، می‌توان استفاده کرد؛ خصوصا برنامه‌های گرافیکی.
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                text = new UnicodeCharacterPlacement
                {
                    Font = new System.Drawing.Font("Tahoma", 12)
                }.Apply(text);
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اگر خروجی متد اصلاح شده فوق را بررسی کنیم، دقیقا به «تست می‌شود» خواهیم رسید.

سؤال: آیا این روش با تمام PDFهای فارسی کار می‌کند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer می‌تواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام می‌دهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
مطالب
نگاشت اشیاء در AutoMapper توسط Attribute ها #1
نگاشت اشیاء امری مفید و لذت بخش است. ولی بخاطر تنظیمات خاص آن و افزایش کدها، همیشه کمی دردسر ساز بوده است. استفاده از کلاس Profile راه کار مناسبی است؛ اما در این حالت کلاس مقصد (ViewModel) از تنظیمات نگاشت‌ها بی اطلاع می‌ماند و فقط حاوی داده خواهد بود. برای ادغام کلاس و تنظیمات نگاشت در اینجا راهکاری ارائه گردید که در ادامه و با الگو گیری از همین ایده، اقدام به ارائه‌ی روشی جدید می‌کنم که با استفاده از Attribute‌ها تنظیمات نگاشت اشیاء را در AutoMapper انجام می‌دهد.
در نهایت می‌خواهیم نگاشت‌ها را اینچنین تنظیم کنیم:
 [MapFrom(typeof (Student), ignoreAllNonExistingProperty: true, alsoCopyMetadata: true)]
 public class AdminStudentViewModel
 {
     // [IgnoreMap]
     public int Id { set; get; }

     [MapForMember("Name")]
     public string FirstName { set; get; }

     [MapForMember("Family")]
     public string LastName { set; get; }

     public string Email { set; get; }

     [MapForMember("RegisterDateTime")]
     public string RegisterDateTimePersian { set; get; }

     [UseValueResolver(typeof (BookCountValueResolver))]
     public int BookCounts { set; get; }

     [UseValueResolver(typeof (BookPriceValueResolver))]
     public decimal BookPrice { set; get; }
 };
  این سبک تنظیم کردن نگاشت‌های اشیاء به نظر بهتر از روش‌های دیگر است؛ چون کلاس‌های ویوومدل را معنادار کرده و همچنین برای برنامه نویسان EF و ASP.NET MVC استفاده‌ی از ویژگی‌ها، یک شیوه‌ی کاری معمول به حساب می‌آید. 
به تعریف و توضیح صفت‌های (ویژگی‌ها یا Attributes) مورد نیاز می‌پردازم:

صفت MapFromAttribute

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class MapFromAttribute : Attribute
{
    public Type SourceType { get; private set; }
    public bool IgnoreAllNonExistingProperty { get; private set; }
    public bool AlsoCopyMetadata { get; private set; }    //Go to: https://www.dntips.ir/courses/topic/16/cb36bc2e-4263-431e-86a5-236322cb5576

    public MapFromAttribute(Type sourceType, bool ignoreAllNonExistingProperty = false,
        bool alsoCopyMetadata = false)
    {
        SourceType = sourceType;
        IgnoreAllNonExistingProperty = ignoreAllNonExistingProperty;
        AlsoCopyMetadata = alsoCopyMetadata;
    }
};
این صفت روی کلاس‌ها می‌نشیند و توسط آرگومان sourceType آن، نوع مبدأ را برای automapper مشخص می‌کند. در واقع همه چیز از اینجا شروع می‌شود. همچنین آرگومان ignoreAllNonExistingProperty مشخص می‌کند کلیه‌ی صفاتی که در مقصد هستند ولی معادل اسمی در مبدأ ندارند، بصورت خودکار رد (Ignore) شده و از آن‌ها صرف نظر شود تا از شکست متد AutoMapper.Mapper.AssertConfigurationIsValid جلوگیری کند (پرداخته شده در اینجا). آرگومان alsoCopyMetadata پیاده سازی نمی‌شود؛ ولی می‌تواند پرچمی باشد تا اجازه دهد  Data Annotations از مدل‌های ef به ViewModel انتقال یابند.


صفت IgnoreMapAttribute
[AttributeUsage(AttributeTargets.Property)]
public class IgnoreMapAttribute : Attribute {};
از این صفت برای رد کردن خصیصه‌‌ای در نگاشت‌ها استفاده می‌کنیم. لازم به ذکر است که صفتی مشابه در Automapper.IgnoreAttribute وجود دارد که می‌تواند به جای این صفت مورد استفاده قرار گیرد. «نگارنده جهت همخوانی با سایر صفات، اقدام به استفاده‌ی از این صفت می‌کند»


صفت MapForMemberAttribute
[AttributeUsage(AttributeTargets.Property)]
public class MapForMemberAttribute : Attribute
{
    public string MemberToMap { get; private set; }
    public MapForMemberAttribute(string memberToMap)
    {
        MemberToMap = memberToMap;
    }
};
اگر نام خصیصه‌ها در مبدأ و مقصد یکی نباشند، از این صفت برای همگام سازی این دو استفاده می‌کنیم.


صفت UseValueResolverAttribute
[AttributeUsage(AttributeTargets.Property)]
public class UseValueResolverAttribute : Attribute
{
    public IValueResolver ValueResolver { get; private set; }
    public UseValueResolverAttribute(Type valueResolver)
    {
        ValueResolver = valueResolver.GetConstructors()[0].Invoke(new object[] {}) as IValueResolver;
    }
};
استفاده از ValueResolver‌ها در اینجا ذکر شده است. از این صفت برای تنظیم این مقدار برای یک خصیصه استفاده می‌شود. برای مثال فیلد FullName را در مقصد درنظر بگیرد که از دو فیلد Name و Family در مبدأ تشکیل می‌شود.

تا اینجا صفات پیش نیاز کار فراهم شدند. حال باید این صفت‌ها را به نگاشت متناسبی در automapper تبدیل کنیم.
دریافت کدها
ادامه دارد...
نظرات مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
این روش برای نوشتن دلیل شکست عملیات در response body، با بازنویسی متد HandleChallengeAsync، آزمایش شده:
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;

namespace OpenAPISwaggerDoc.Web.Authentication;

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private string _failReason;

    public BasicAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey("Authorization"))
        {
            _failReason = "Missing Authorization header";
            return Task.FromResult(AuthenticateResult.Fail(_failReason));
        }

        try
        {
            var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
            var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
            var username = credentials[0];
            var password = credentials[1];

            if (string.Equals(username, "DNT", StringComparison.Ordinal) &&
                string.Equals(password, "123", StringComparison.Ordinal))
            {
                var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
                var identity = new ClaimsIdentity(claims, Scheme.Name);
                var principal = new ClaimsPrincipal(identity);
                var ticket = new AuthenticationTicket(principal, Scheme.Name);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }

            _failReason = "Invalid username or password";
            return Task.FromResult(AuthenticateResult.Fail(_failReason));
        }
        catch
        {
            _failReason = "Invalid Authorization header";
            return Task.FromResult(AuthenticateResult.Fail(_failReason));
        }
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        await base.HandleChallengeAsync(properties);
        if (Response.StatusCode == StatusCodes.Status401Unauthorized &&
            !string.IsNullOrWhiteSpace(_failReason))
        {
            Response.Headers.Add("WWW-Authenticate", _failReason);
            Response.ContentType = "application/json";
            await WriteProblemDetailsAsync(_failReason);
        }
    }

    private Task WriteProblemDetailsAsync(string detail)
    {
        var problemDetails = new ProblemDetails { Detail = detail, Status = Context.Response.StatusCode };
        var result = new ObjectResult(problemDetails)
                     {
                         ContentTypes = new MediaTypeCollection(),
                         StatusCode = problemDetails.Status,
                         DeclaredType = problemDetails.GetType(),
                     };
        var executor = Context.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
        var routeData = Context.GetRouteData() ?? new RouteData();
        var actionContext = new ActionContext(Context, routeData, new ActionDescriptor());
        return executor.ExecuteAsync(actionContext, result);
    }
}
مطالب
بهبود کارآیی استفاده از JSON در دات نت 6 با معرفی Source generators آن
دات نت 6 به همراه source generator‌های توکاری است که می‌توانند کار serialization و deserialization نوع JSON را با کارآیی بسیار بیشتری انجام دهند؛ با آزمایش‌هایی که این بهبود را در حد 40 درصد سریعتر نسبت به حالت متداول آن نمایش می‌دهند و ... این مساله بسیار مهم است. از این جهت که این روزها، JSON را در همه‌جا مشاهده می‌کنیم؛ در Web APIها، در تنظیمات برنامه‌ها، در ارسال پیام‌ها بین برنامه‌ها و غیره. بنابراین هرگونه بهبودی در زمینه‌ی کارآیی serialization و deserialization آن، تاثیر بسیار قابل ملاحظه‌ای را بر روی کارآیی کلی یک برنامه بجا خواهد گذاشت.


System.Text.Json source generator چیست؟

پا‌یه‌ی تمام اعمال serialization و deserialization در دات نت، استفاده از Reflection است که در زمینه‌ی ارائه‌ی برنامه‌هایی با کارآیی بالا و با مصرف حافظه‌ی پایین، بهینه عمل نمی‌کند. راه‌حل جایگزین استفاده از Reflection که در زمان اجرای برنامه رخ می‌دهد، به همراه دات نت 5 ارائه شد و source generators نام دارد. Source generators امکان تولید فایل‌های #C را در زمان کامپایل برنامه میسر می‌کنند که نسبت به راه‌حل Reflection که در زمان اجرای برنامه فعال می‌شود، کارآیی بسیار بیشتری را ارائه می‌کنند. برای مثال به همراه دات نت 6، علاوه بر روش پیش‌فرض مبتنی بر Reflection ارائه شده‌ی توسط System.Text.Json، راه حل جدید امکان استفاده‌ی از source generators توکار آن نیز پیش بینی شده‌است. کار اصلی آن، انجام تمام مراحلی است که پیشتر توسط Reflection در زمان اجرای برنامه صورت می‌گرفت، اینبار در زمان کامپایل برنامه و ارائه‌ی آن به صورت از پیش آماده شده و مهیا.
مزایای این روش شامل موارد زیر است:
- بالا رفتن سرعت برنامه
- کاهش زمان آغاز اولیه‌ی برنامه
- کاهش میزان حافظه‌ی مورد نیاز برنامه
- عدم نیاز به استفاده‌ی از System.Reflection و System.Reflection.Emit
- ارائه‌ی Trim-compatible serialization که سبب کاهش اندازه‌ی نهایی برنامه می‌شود. برای مثال در برنامه‌های Blazor می‌توان با فعالسازی Trimming، کدهای استفاده نشده را از فایل‌های بایناری نهایی حذف کرد. استفاده از source generators، با این روش سازگاری کاملی دارد.



مثالی از نحوه‌ی کار با JSON در دات نت 6، توسط source generators آن

فرض کنید قصد داریم اعمال serialization و deserialization از نوع JSON را بر روی نمونه‌های کلاس زیر انجام دهیم:
namespace Test
{
    internal class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
اولین کاری که در این زمینه باید انجام شود، ایجاد یک کلاس خالی، با نامی دلخواه، اما مشتق شده‌ی از JsonSerializerContext است. در این حالت اخطارهایی را در IDE خود مبتنی بر نیاز به پیاده سازی تعدادی از متدهای این کلاس پایه دریافت می‌کنیم. اما ... ما قصد نداریم این متدها را پیاده سازی کنیم؛ Source generator قرار است اینکار را انجام دهد. به همین جهت این کلاس را partial تعریف کرده (تا source generator بتواند آن‌را در فایلی دیگر تکمیل کند) و همچنین آن‌را مزین به ویژگی JsonSerializable از نوع کلاسی که می‌خواهیم آن‌را serialize کنیم، خواهیم کرد تا سبب فعال شدن source generator بر روی این کلاس شویم:
using System.Text.Json.Serialization;

namespace Test
{
    [JsonSerializable(typeof(Person))]
    internal partial class MyJsonContext : JsonSerializerContext
    {
    }
}
و ... همین! کدهای این کلاس partial توسط source generator در زمان کامپایل برنامه به صورت خودکار تولید و تکمیل می‌شوند.
پس از آن فقط کافی است MyJsonContext را به عنوان پارامتر متدهای جدید Serialize و یا Deserialize، به صورت زیر ارسال کنیم تا از آن استفاده شود:
Person person = new() { FirstName = "Jane", LastName = "Doe" };
byte[] utf8Json = JsonSerializer.SerializeToUtf8Bytes(person, MyJsonContext.Default.Person);
person = JsonSerializer.Deserialize(utf8Json, MyJsonContext.Default.Person);

متدهای جدید این API مبتنی بر source generators را در ادامه ملاحظه می‌کنید:
namespace System.Text.Json
{
    public static class JsonSerializer
    {
        public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(ReadOnlySpan<char> json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(string json, Type returnType, JsonSerializerContext context) => ...;
        public static object? Deserialize(ref Utf8JsonReader reader, Type returnType, JsonSerializerContext context) => ...;
        public static ValueTask<object?> DeserializeAsync(Stream utf8Json, Type returnType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static ValueTask<TValue?> DeserializeAsync<TValue>(Stream utf8Json, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static TValue? Deserialize<TValue>(ReadOnlySpan<byte> utf8Json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(string json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static TValue? Deserialize<TValue>(ref Utf8JsonReader reader, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static string Serialize(object? value, Type inputType, JsonSerializerContext context) => ...;
        public static void Serialize(Utf8JsonWriter writer, object? value, Type inputType, JsonSerializerContext context) { }
        public static Task SerializeAsync(Stream utf8Json, object? value, Type inputType, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task SerializeAsync<TValue>(Stream utf8Json, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static byte[] SerializeToUtf8Bytes(object? value, Type inputType, JsonSerializerContext context) => ...;
        public static byte[] SerializeToUtf8Bytes<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
        public static void Serialize<TValue>(Utf8JsonWriter writer, TValue value, JsonTypeInfo<TValue> jsonTypeInfo) { }
        public static string Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => ...;
    }
}


روش معرفی تنظیمات Serializer به Source generator

برای معرفی تنظیمات serialization و deserialization، برای مثال تهیه‌ی خروجی‌های CamelCase، می‌توان از ویژگی JsonSourceGenerationOptions به صورت زیر استفاده کرد:
using System.Text.Json.Serialization;

namespace Test
{
    [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
    [JsonSerializable(typeof(Person))]
    internal partial class MyJsonContext : JsonSerializerContext
    {
    }
}
در این حالت مابقی کدها مانند قبل باقی خواهند ماند:
string json = JsonSerializer.Serialize(person, MyJsonContext.Default.Person);
Person person = JsonSerializer.Deserialize(json, MyJsonContext.Default.Person);


روش استفاده از JSON Source generators در برنامه‌های ASP.NET Core

در این نوع برنامه‌ها، JsonSerializerContext‌ها را می‌توان توسط متد AddContext به صورت زیر به تنظیمات JSON برنامه معرفی کرد:
services.AddControllers().AddJsonOptions(options => options.AddContext<MyJsonContext>());


روش استفاده از JSON Source generators در برنامه‌های Blazor

البته در اینجا بیشتر منظور امکان استفاده‌ی از آن‌ها توسط HttpClient است که به صورت زیر توسط متد GetFromJsonAsync واقع در فضای نام System.Net.Http.Json، میسر شده‌است:
[JsonSerializable(typeof(WeatherForecast[]))]
internal partial class MyJsonContext : JsonSerializerContext { }

@code {
    private WeatherForecast[] forecasts;

    private static JsonSerializerOptions Options = new(JsonSerializerDefaults.Web);
    private static MyJsonContext Context = new MyJsonContext(Options);

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync("sample-data/weather.json", Context.WeatherForecastArray);
    }
}
لیست کامل‌تر این API جدید به صورت زیر است:
namespace System.Net.Http.Json
{
    public static partial class HttpClientJsonExtensions
    {
        public static Task<object?> GetFromJsonAsync(this HttpClient client, string? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<object?> GetFromJsonAsync(this HttpClient client, System.Uri? requestUri, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, string? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(this HttpClient client, System.Uri? requestUri, TValue value, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
    }
    public static partial class HttpContentJsonExtensions
    {
        public static Task<object?> ReadFromJsonAsync(this HttpContent content, Type type, JsonSerializerContext context, CancellationToken cancellationToken = default(CancellationToken)) => ...;
        public static Task<T?> ReadFromJsonAsync<T>(this HttpContent content, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default(CancellationToken)) => ...;
    }
}
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت اول
قصد داریم طی یک سری مقالات به توسعه یک سیستم مدیریت محتوا بپردازیم. مسلما فاصله‌ی زمانی بین انتشار مقالات این سری، کمی زیاد خواهد بود. ولی سعی خواهیم کرد تا قدم به قدم و با تحلیل و توضیح کافی هر بخش به این هدف برسیم.
همکاران این قسمت:
پیشنیاز‌ها:
در زیر بخش هایی که برای این سیستم تا به امروز در نظر گرفتیم (مسلما با ارائه ایده‌ها و بازخورد‌های دوستان این امکانات دستخوش تغییر خواهند شد) ، به شرح زیر میباشد:
  • انجمن
  • ارتباط دوستی
  • سیستم ترفیع رتبه
  • Themeable
  • سیستم Following
  • صفحات داینامیک
  • سیستم پیام رسانی
  • امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی مختلف
  • پیغام خصوصی
  • وبلاگ
  • نظرسنجی ها
  • مدیریت کاربران با دسترسی‌ها داینامیک
  • اخبار
  • آگهی ها
در این قسمت مدل‌های مربوط به بخش وبلاگ را بررسی میکنیم. ابتدا قصد داشتیم تا امکان نگارش یک پست توسط چندین کاربر را به طور همزمان، در سیستم قرار دهیم و ایده کار هم که توسط یکی دوستان (محمد شریفی) پیشنهاد شد به صورت زیر بود:
ابتدا کاربری به عنوان ایجاد کننده اصلی پست، بخش‌ها مختلفی را برای یک پست در نظر بگیرد و هر قسمت را به یک همکار نسبت دهد و با اتمام تمام بخش‌ها و اعلام آن توسط همکاران، کاربر اصلی ایجاد کننده پست بتواند بخش‌ها را باهم ادغام کند و برای آماده انتشار نهایی باشد.
ولی خب به نظر بنده الان سیستم‌های ارتباطی قدرتمندتری هم هست که همکاران در مورد نگارش یک پست به صورت مشارکتی بپردازند و صرفا در مرحله‌ی انتشار، این کاربران به عنوان کاربران همکار به پست منتشر شده نسبت داده شوند تا در زیر پست مشخصات کامل آنها قابل مشاهد باشد (راه حلی که پیاده سازی خواهیم کرد).
مدل‌های در نظر گرفته شده برای سیستم وبلاگ به صورت زیر می‌باشد:

مخزن برچسب ها
/// <summary>
    /// Represents the lable 
    /// </summary>
    public class Tag
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="Tag"/>
        /// </summary>
        public Tag()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets Tag's identifier
        /// </summary>
        public virtual Guid  Id { get; set; }
        /// <summary>
        /// sets or gets Tag's name
        /// </summary>
        public virtual string Name { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets Tag's posts
        /// </summary>
        public virtual ICollection<BlogPost> BlogPosts { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی مخزن برچسب‌های سیستم ما خواهد بود. جدول حاصل از این مدل با جداول سایر بخش‌ها که نیاز به داشتن برچسب خواهند داشت، ارتباط چند به چند خواهد داشت. برای این منظور همانطور که مشخص است در قسمت NavigationProperties یک لیست از BlogPost معرفی شده است و در ادامه خواهیم دید که لیستی از کلاس Tag هم در کلاس BlogPost معرفی خواهد شد.

مدل پیش نویس ها
  /// <summary>
    /// Represents the Post's Draft
    /// </summary>
    public  class BlogDraft
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="BlogDraft"/>
        /// </summary>
        public  BlogDraft()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of post's draft
        /// </summary>
        public virtual  Guid Id { get; set; }
        /// <summary>
        /// gets or sets body of post's draft
        /// </summary>
        public virtual  string Body { get; set; }
        /// <summary>
        /// gets or set title of post's draft
        /// </summary>
        public virtual  string Title { get; set; }
        /// <summary>
        /// gets or sets tags of post's draft that seperated using ','
        /// </summary>
        public virtual  string TagNames { get; set; }
        /// <summary>
        /// gets or sets value indicating whether this draft is ready to publish
        /// </summary>
        public virtual  bool IsReadyForPublish { get; set; }
        /// <summary>
        /// ges ro sets DateTime that this draft added
        /// </summary>
        public virtual  DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets date that this draft publish as ready
        /// </summary>
        public virtual DateTime? ReadyForPublishOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Id of user that he is owner of this draft
        /// </summary>
        public virtual  long OwnerId { get; set; }
        /// <summary>
        /// gets or sets user that he is owner of this draft
        /// </summary>
        public virtual  User Owner { get; set; }
        #endregion
    }
کلاس فوق برای ذخیره سازی پست‌های پیشنویس کاربران در نظر گرفته شده است. شاید بهتر بود این پیش نویس‌ها هم در همان مدل پستی که در ادامه مشاهده می‌کنید ادغام شوند. ولی این یک تصمیم شخصی برای این کار بوده و برای سیستی بزرگی که امکان دارد حذف و درج پیشنویس‌ها زیاد باشد و فرض بر اینکه long هم برای آی دی جواب گو نخواهد بود و نیاز است از Guid ای که به صورت متوالی افزایش میابد به منظور جلوگیری از Fragmentation، استفاده کرد. بقیه فیلد‌ها هم مشخص هستند و نیازی به توضیح اضافی نخواهد بود. مسلما هر کاربر می‌تواند چندین پیشنویس داشته باشد. لذا ارتباط یک به چند مابین کاربر و پیشنویس پست، خواهیم داشت.
مدل امتیاز دهی کاربران
/// <summary>
    /// Section of Rating
    /// </summary>
    public enum RatingSection
    {
        News,
        Announcement,
        ForumTopic,
        BlogComment,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        ...
    }


    /// <summary>
    /// Represents Rating Record regard by section type for Rating System
    /// </summary>
    public class UserRating
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="UserRating"/>
        /// </summary>
        public UserRating()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of Rating Record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets value of rate
        /// </summary>
        public virtual double RatingValue { get; set; }
        /// <summary>
        /// gets or sets Section's Id 
        /// </summary>
        public virtual long SectionId { get; set; }
        /// <summary>
        /// gets or sets Section 
        /// </summary>
        public virtual RatingSection Section { get; set; }
        #endregion

        #region Navigation Properties
        /// <summary>
        /// gets or sets user that rate one section
        /// </summary>
        public virtual User Rater { get; set; }
        /// <summary>
        /// gets or sets Rater Id that rate one section
        /// </summary>
        public virtual long RaterId { get; set; }

        #endregion
    }
نوع داده‌ی شمارشی RatingSection مشخص کننده‌ی بخش‌های مختلف سیستم می‌باشد که نیاز به امتیاز دهی دارند. چون سیستم ما یک سیستم بسته می‌باشد و برای امتیاز دهی نیاز به احراز هویت خواهد بود، لذا باید از امتیاز دادن بیش از یک بار برای هر بخش جلوگیری کنیم. برای این کار می‌توان بین تمام بخش‌های موجود ارتباط‌های چند به چند در نظر گرفت. برای مثال یک ارتباط چند به چند بین کاربر و نظرات که مشخص کننده‌ی این است که یک کاربر می‌تواند به چند نظر امتیاز دهد و هر نظر هم میتواند چندین امتیاز دهنده داشته باشد ولی تعداد جداول بالا خواهد رفت و کار آن هم زیاد خواهد شد. برای این منظور میتوان یک جدول به مانند UserRating در نظر گرفت که صرفا با جدول کاربر ما یک ارتباط یک به چند دارد و با استفاده از خصوصیت RatingSection موجود در این کلاس وخصوصیت SectionId و همچنین در نظر گرفتن یک کلید منحصر به فرد بر روی خصوصیت‌های RatingSection ، RaterId و SectionId، از امتیاز دادن چند باره‌ی کاربر به یک بخش جلوگیری کرد.
/// <summary>
    /// Represent the rating as ComplexType
    /// </summary>
    [ComplexType]
    public class Rating
    {
        /// <summary>
        /// sets or gets total of rating
        /// </summary>
        public virtual double? TotalRating { get; set; }
        /// <summary>
        /// sets or gets rater's count
        /// </summary>
        public virtual long? RatersCount { get; set; }
        /// <summary>
        /// sets or gets average of rating
        /// </summary>
        public virtual double? AverageRating { get; set; }
    }
کلاس بالا یک ComplexType می‌باشد که در نهایت به هیچ جدولی مپ نخواهد شد و صرفا به منظور کپسوله کردن یکسری فیلد مورد استفاده قرار گرفته است و از این کلاس می‌توان در هر بخش که لازم است امتیاز دهی داشته باشیم، به عنوان یک خصوصیت، استفاده کرد.

کلاس پایه‌ی محتوا
/// <summary>
    /// Represents a base class for every content in system
    /// </summary>
    public abstract class BaseContent
    {
        #region Properties

        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of publishing content
        /// </summary>
        public virtual DateTime PublishedOn { get; set; }
        /// <summary>
        /// gets or sets Last Update's Date
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets meta title for seo
        /// </summary>
        public virtual string MetaTitle { get; set; }
        /// <summary>
        /// gets or sets meta keywords for seo
        /// </summary>
        public virtual string MetaKeywords { get; set; }
        /// <summary>
        /// gets or sets meta description of the content
        /// </summary>
        public virtual string MetaDescription { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual string FocusKeyword { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content use CanonicalUrl
        /// </summary>
        public virtual bool UseCanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets CanonicalUrl That the Post Point to it
        /// </summary>
        public virtual string CanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Follow for Seo
        /// </summary>
        public virtual bool UseNoFollow { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Index for Seo
        /// </summary>
        public virtual bool UseNoIndex { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content in sitemap
        /// </summary>
        public virtual bool IsInSitemap { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed for anonymouses 
        /// </summary>
        public virtual bool AllowCommentForAnonymous { get; set; }
        /// <summary>
        /// gets or sets  viewed count by rss
        /// </summary>
        public virtual long ViewCountByRss { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.Approved).Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.UnApproved).Count()
        /// We use this property for performance optimization (no SQL command executed)</remarks></summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets value  indicating whether the content is logical deleted or hidden
        /// </summary>
        public virtual bool IsDeleted { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content show with rssFeed
        /// </summary>
        public virtual bool ShowWithRss { get; set; }
        /// <summary>
        /// gets or sets value indicating maximum days count that users can send comment
        /// </summary>
        public virtual int DaysCountForSupportComment { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets icon name with size 200*200 px for snippet 
        /// </summary>
        public virtual string SocialSnippetIconName { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public virtual string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public virtual string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets body of content's comment
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Author { get; set; }

        /// <summary>
        /// gets or sets Id of user that create this record
        /// </summary>
        public virtual long AuthorId { get; set; }
        /// <summary>
        /// get or set  the tags integrated with content
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        #endregion

    }

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

نکته‌ای که وجود دارد فیلد‌های ApprovedCommentsCount  UnApprovedCommentsCount و TagNames می‌باشند که هنگام درج نظر جدید باید تعداد نظرات ذخیره شده را  ویرایش کنیم و هنگام ویرایش خود پست یا خبر با ... و یا حتی ویرایش خود تگ یا حذف آن تگ باید TagNames که لیست برچسب‌های محتوا را به صورت جدا شده با (,) از هم دیگر می‌باشد، ویرایش کنیم (جای بحث دارد).

مشخص است که هر یک از مطالب منتشر شده در بخش‌های وبلاگ، اخبار، نظرسنجی و آگهی‌ها، یک کابر ایجاد کننده (Author نامیده‌ایم) خواهد داشت و هر کاربر هم می‌تواند چندین مطلب را ایجاد کند. لذا رابطه‌ی یک به چند بین تمام این بخش‌ها مذکور و کاربر ایجاد خواهد شد.

مدل  LinkBack

 /// <summary>
    /// Represents link for implemention linkback
    /// </summary>
    public class LinkBack
    {

        #region Ctor
        /// <summary>
        /// create one instance of <see cref="LinkBack"/>
        /// </summary>
        public LinkBack()
        {
            CreatedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets link's Id
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets text for show Link
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets link's address 
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or set value indicating whether this link is internal o external
        /// </summary>
        public virtual LinkBackType Type { get; set; }
        /// <summary>
        /// gets or sets date that this record is added
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Post that associated
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets id of Post that associated
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }


 /// <summary>
    /// represents Type of ReferrerLinks
    /// </summary>
    public enum LinkBackType
    {
        /// <summary>
        /// Internal link
        /// </summary>
        Internal,
        /// <summary>
        /// External Link
        /// </summary>
        External
    }

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

مدل پست ها

  /// <summary> 
    /// Represents a blog post
    /// </summary>
    public  class BlogPost : BaseContent
    {
        #region Ctor
        /// <summary>
        /// Create one Instance of <see cref="BlogPost"/>
        /// </summary>
        public BlogPost()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

       #region Properties
        /// <summary>
        /// gets or sets Status of LinkBack Notifications
        /// </summary>
        public virtual LinkBackStatus LinkBackStatus { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set  blog post's Reviews
        /// </summary>
        public virtual ICollection<BlogComment> Comments { get; set; }
        /// <summary>
        /// get or set collection of links that reference to this blog post
        /// </summary>
        public virtual ICollection<LinkBack> LinkBacks { get; set; }
        /// <summary>
        /// get or set Collection of Users that Contribute on this post
        /// </summary>
        public virtual ICollection<User> Contributors  { get; set; }
        
        #endregion
    }


  /// <summary>
    /// Represents Status for ReferrerLinks
    /// </summary>
    public enum  LinkBackStatus
    {
        [Display(Name ="غیرفعال")]
        Disable,
        [Display(Name = "فعال")]
        Enable,
        [Display(Name = "لینک‌ها داخلی")]
        JustInternal,
        [Display(Name = "لینک‌ها خارجی")]
        JustExternal
    }
 کلاس بالا نشان دهنده‌ی مدل پست‌های ما در سیستم می‌باشد که بیشتر خصوصیات خود را از کلاس پایه‌ی BaseContent به ارث برده و لازم به تکرار آنها نیست. به علاوه یکسری خصوصیات دیگر، از جلمه‌ی آنها یک لیست از کلاس LinkBack، لیستی از کاربران مشارکت کننده به منظور نمایش مشخصات آنها در زیر پست و لیستی از کلاس BlogComment که نشان دهنده‌ی نظرات پست می‌باشد، هم خواهد داشت. خصوصیتی از نوع داده‌ی LinkBackStatus هم صرفا به منظور تنظیمات مدیریتی در نظر گرفته شده است. 

کلاس پایه نظرات
  /// <summary>
    /// Represents a base class for every comment in system 
    /// </summary>

    public abstract class BaseComment
    {
        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets displayName of this comment's Creator if  he/she is Anonymous
        /// </summary>
        public virtual string CreatorDisplayName { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// gets or sets siteUrl of Creator if he/she is Anonymous
        /// </summary>
        public virtual string SiteUrl { get; set; }
        /// <summary>
        /// gets or sets Email of Creator if he/she is anonymous
        /// </summary>
        public virtual string Email { get; set; }
        /// <summary>
        /// gets or sets status of comment
        /// </summary>
        public virtual CommentStatus Status { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }

        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long? CreatorId { get; set; }
        #endregion
    }

public enum CommentStatus
    {
        /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
        [Display(Name = "تأیید شده")]
        Approved = 0,
        [Display(Name = "در انتظار بررسی")]
        Pending = 1,
        [Display(Name = "جفنگ")]
        Spam = 2,
        [Display(Name = "زباله دان")]
        Trash = -1
    }
به مانند BaseContent که برای کپسوله کردن خصوصیات تکراری و نه برای اعمال ارث بری TPH یا TPT، کلاس BaseComment را در نظر گرفته‌ایم. نکته‌ی مهم این است که هر نظری یک درج کننده دارد. ولی اگر فیلد AllowCommentForAnonymous مربوط به مطلب True باشد، این امکان وجود خواهد داشت تا کاربران احراز هویت نشده نیز نظر ارسال کنند. لذا CreatorId در کلاس BaseComment به صورت Nullable در نظر گرفته شده است و یک سری خصوصیت‌ها از قبیل SiteUrl ، CreatorDisplayName ، Email برای کاربران احراز هویت نشده در نظر گرفته شده است. نوع شمارشی CommentStatus نیز برای اعمال مدیریتی در نظر گرفته شده است.
مدل نظرات پست ها
/// <summary>
    /// Represents a blog post's comment
    /// </summary>
    public class BlogComment : BaseComment
    {
        #region Ctor
        /// <summary>
        /// Create One Instance for <see cref="BlogComment"/>
        /// </summary>
        public BlogComment()
        {
            Rating = new Rating();
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets BlogComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual BlogComment Reply { get; set; }
        /// <summary>
        /// get or set collection of blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<BlogComment> Children { get; set; }
        /// <summary>
        /// gets or sets post that this comment sent to it
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets post'Id that this comment sent to it
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی نظرات پست‌های وبلاگ می‌باشند که بیشتر خصوصیت‌های خود را از کلاس پایه‌ی BaseComment به ارث برده است و همچنین ساختار سلسله مراتبی آن نیز  قابل مشاهده است. 
برای سیستم Report یا همان گزارش خطا هم سیستمی شبیه به امتیاز دهی ستاره‌ای در نظر گرفته ایم. برای اینکه مقاله خیلی طولانی شد، بهتر است در مقاله‌ای جدا کار را ادامه داد.
نتیجه این قسمت