مطالب
شروع به کار با EF Core 1.0 - قسمت 11 - بررسی رابطه‌ی Self Referencing
پیشنیازها
- بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)
- مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
- آشنایی با SQL Server Common Table Expressions - CTE
- بدست آوردن برگهای یک درخت توسط Recursive CTE


در پیشنیازهای بحث، روش تعریف روابط خود ارجاع دهنده و یا Self Referencing را تا EF 6.x می‌توانید مطالعه کنید. در این قسمت قصد داریم معادل این روش‌ها را در EF Core بررسی کنیم.


روش تعریف روابط خود ارجاع دهنده توسط Fluent API در EF Core

اگر همان مدل کامنت‌های چندسطحی یک بلاگ را درنظر بگیریم:
    public class BlogComment
    {
        public int Id { get; set; }

        public string Body { get; set; }

        public DateTime Date1 { get; set; }


        public virtual BlogComment Reply { get; set; }
        public int? ReplyId { get; set; }

        public virtual ICollection<BlogComment> Children { get; set; }
    }
اینبار تنظیمات Fluent API معادل EF Core آن به صورت ذیل خواهد بود:
    public class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BlogComment>(entity =>
            {
                entity.HasIndex(e => e.ReplyId);

                entity.HasOne(d => d.Reply)
                    .WithMany(p => p.Children)
                    .HasForeignKey(d => d.ReplyId);
            });
        }

        public virtual DbSet<BlogComment> BlogComments { get; set; }
    }
هدف از مدل‌های خود ارجاع دهنده، مدلسازی اطلاعات چند سطحی است؛ مانند منوهای چندسطحی یک سایت، کامنت‌های چند سطحی یک مطلب، سلسله مراتب کارمندان یک شرکت و امثال آن.
نکته‌ها‌ی مهم مدلسازی این نوع روابط، موارد ذیل هستند:
الف) وجود خاصیتی از جنس کلاس اصلی در همان کلاس
   public virtual BlogComment Reply { get; set; }
ب) که در حقیقت مشخص می‌کند، والد رکورد جاری کدام رکورد قبلی است:
   public int? ReplyId { get; set; }
برای اینکه چنین رابطه‌ای تشکیل شود، باید این فیلد، مقدارش را از کلید اصلی جدول تامین کند (تشکیل یک کلید خارجی که به کلید اصلی جدول اشاره می‌کند).
علت نال پذیر بودن این خاصیت، نیاز به ثبت ریشه‌ای است که خودش والد است و فرزند رکوردی پیشین، نیست.


ج) همچنین برای سهولت دریافت فرزندان یک ریشه، مجموعه‌ای از جنس همان کلاس نیز تشکیل می‌شود.
 public virtual ICollection<BlogComment> Children { get; set; }


روش تعریف روابط خود ارجاع دهنده توسط Data Annotations در EF Core

در ادامه معادل تنظیمات فوق را توسط ویژگی‌ها ملاحظه می‌کنید که در اینجا توسط ویژگی‌های InverseProperty و ForeignKey، کار تشکیل روابط صورت گرفته‌است:
    public class BlogComment
    {
        public int Id { get; set; }
        public string Body { get; set; }
        public DateTime Date1 { get; set; }

        [ForeignKey("ReplyId")]
        [InverseProperty("Children")]
        public virtual BlogComment Reply { get; set; }
        public int? ReplyId { get; set; }

        [InverseProperty("Reply")]
        public virtual ICollection<BlogComment> Children { get; set; }
    }
البته قسمت تشکیل ایندکس بر روی ReplyId را فقط توسط Fluent API می‌توان انجام داد:
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<BlogComment>(entity =>
            {
                entity.HasIndex(e => e.ReplyId);
            });
        }


ثبت اطلاعات و کوئری گرفتن از روابط خود ارجاع دهنده در EF Core

در اینجا نحوه‌ی ثبت دو سری نظر و زیر نظر را مشاهده می‌کنید:
var comment1 = new BlogComment { Body = "نظر من این است که" };
var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 };
var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 };

context.BlogComments.Add(comment121);

var comment2 = new BlogComment { Body = "نظر من این بود که" };
var comment22 = new BlogComment { Body = "پاسخی به نظر قبلی", Reply = comment2 };
var comment221 = new BlogComment { Body = "پاسخی به پاسخ به نظر من اول", Reply = comment22 };
context.BlogComments.Add(comment221);

context.SaveChanges();


نظرات اصلی، با ReplyId مساوی نال قابل تشخیص هستند. سایر نظرات، توسط همین ReplyId به یکی از Idهای موجود، متصل شده‌اند.

در تصویر فوق و با توجه به اطلاعات ثبت شده، فرض کنید می‌خواهیم ریشه‌ی با id مساوی 1 و تمام زیر ریشه‌های آن‌را بیابیم. انجام یک چنین کاری نه در EF Core و نه در EF 6.x، پشتیبانی نمی‌شود. بدیهی است در اینجا هنوز روش‌های Include و ThenInclue هم جواب می‌دهند؛ اما چون Lazy loading فعال نیست، عملا نمی‌توان تمام زیر ریشه‌ها را یافت و همچنین به چندین و چند رفت و برگشت به ازای هر زیر ریشه خواهیم رسید که اصلا بهینه نیست.
برای اینکار نیاز است مستقیما کوئری نویسی کرد که در مطلب «شروع به کار با EF Core 1.0 - قسمت 10 - استفاده از امکانات بومی بانک‌های اطلاعاتی» زیر ساخت آن‌را بررسی کردیم:
var id = 1;
var childIds = new List<int>();
 
var connection = context.Database.GetDbConnection();
connection.Open();
var command = connection.CreateCommand();
command.CommandText =
    @"WITH Hierachy(ChildId, ParentId) AS (
                SELECT Id, ReplyId
                    FROM BlogComments
                UNION ALL
                SELECT h.ChildId, bc.ReplyId
                    FROM BlogComments bc
                    INNER JOIN Hierachy h ON bc.Id = h.ParentId
            )
 
            SELECT h.ChildId
                FROM Hierachy h
                WHERE h.ParentId = @Id";
command.Parameters.Add(new SqlParameter("id", id));
 
var reader = command.ExecuteReader();
while (reader.Read())
{
    var childId = (int)reader[0];
    childIds.Add(childId);
}
reader.Dispose();

 
var list = context.BlogComments
                .Where(blogComment => blogComment.Id == id ||
                                      childIds.Contains(blogComment.Id))
                .ToList();
زمانیکه کدهای فوق اجرا می‌شوند، تنها دو کوئری ذیل به بانک اطلاعاتی ارسال خواهند شد:
exec sp_executesql N'WITH Hierachy(ChildId, ParentId) AS (
                            SELECT Id, ReplyId
                                FROM BlogComments
                            UNION ALL
                            SELECT h.ChildId, bc.ReplyId
                                FROM BlogComments bc
                                INNER JOIN Hierachy h ON bc.Id = h.ParentId
                        )

                        SELECT h.ChildId
                            FROM Hierachy h
                            WHERE h.ParentId = @Id',N'@id int',@id=1
که لیست Idهای تمام زیر ریشه‌های مربوط به id مساوی یک را بر می‌گرداند:


و پس از آن:
 exec sp_executesql N'SELECT [blogComment].[Id], [blogComment].[Body], [blogComment].[Date1], [blogComment].[ReplyId]
FROM [BlogComments] AS [blogComment]
WHERE [blogComment].[Id] IN (@__id_0, 3, 5)',N'@__id_0 int',@__id_0=1
اکنون که Idهای مساوی 3 و 5 را یافتیم، با استفاده از متد Contains آن‌ها را تبدیل به where in کرده و لیست نهایی گزارش را تهیه می‌کنیم:


یافتن Idهای زیر ریشه‌ها توسط روش CTE (پیشنیازهای ابتدای بحث) و سپس کوئری گرفتن بر روی این Idها، بهینه‌ترین روشی‌است که در EF می‌توان جهت کار با مدل‌های خود ارجاع دهنده بکار گرفت.
مطالب
نحوه کار Expression و ایجاد یک DynamicFilter
ساختار Expression‌ها شبیه به ساختار یک درخت است. به عنوان مثال زمانیکه شما یک فیلتر ساده را مانند دستور زیر اجرا میکنید:
Expression<Func<string, bool>> f = s => s.Length < 5;
Expression ایجاد شده از فیلتر شما به صورت زیر میباشد:

منبع : کتاب C# 8 in a Nutshell 

ParameterCollection به پارامترهای استفاده شده در فیلتر اشاره دارد که در فیلتر بالا فقط s استفاده شده‌است و از نوع string است.

BinaryExpression شامل سه قسمت مهم Left , Right و NodeType میباشد. برای فیلتر بالا، مقدار پراپرتی Left برابر s.Length می‌باشد و پراپرتی Right شامل مقدار 5 و مقدار NodeType هم برابر LessThan میباشد. یعنی فیلتر بالا به یک درخت تبدیل شده که نود اصلی آن LessThan است و دو مقدار Left و Right را باهم مقایسه میکند. اما اگر یک شرط دیگر را به فیلتر بالا اعمال کنیم، ساختار Expression کمی تغییر میکند. برای مثال: 

Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;

Expression ایجاد شده برای این فیلتر شامل همان ساختار قبلی است؛ اما با این تغییر که هر کدام از پراپرتی‌های Right و Left، خود یک BinaryExpression شده‌اند و مقدار NodeType اصلی از LessThan به AndAlso تغییر پیدا کرده‌است. Expression ایجاد شده از فیلتر بالا ( filter.Body ) به این صورت است که پراپرتی Left آن برابر است با یک BinaryExpression که مقدار NodeType آن برابر است با GreaterThan و پراپرتی Left آن شامل s.Length میباشد و پراپرتی Right آن برابر 5 میباشد. همچنین پراپرتی Right مربوط به filter.Body برابر یک ExpressionBinary است که مقدار NodeType آن برابر است با LessThan و پراپرتی Left آن برابر s.Length است و پراپرتی Right آن برابر 45 میباشد.

filter.Body شبیه به تصویر زیر میباشد :

اگر بخواهیم خودمان یک Expression tree را ایجاد کنیم، باید از پایین‌ترین نود آن شروع کنیم. یعنی ابتدا باید پراپرتی Left و Right را ایجاد کنیم و سپس این دو پراپرتی را با هم مقایسه کنیم (NodeType). در کد زیر Expression مربوط به فیلتر بالا را نوشته‌ایم:

ParameterExpression parameterExpression = Expression.Parameter(typeof(string));
MemberExpression memberExpression = Expression.Property(parameterExpression, "Length");

ConstantExpression greaterThanConstantExpression = Expression.Constant(5);
BinaryExpression greaterThanComparison = Expression.GreaterThan(memberExpression, greaterThanConstantExpression);
var greaterThan = Expression.Lambda<Func<string, bool>>(greaterThanComparison, parameterExpression);

ConstantExpression lessThanConstantExpression = Expression.Constant(45);
BinaryExpression lessThanComparsion = Expression.LessThan(memberExpression, lessThanConstantExpression);
var lessThan = Expression.Lambda<Func<string, bool>>(lessThanComparsion, parameterExpression);

var param = Expression.Parameter(typeof(string), "x");
var body = Expression.AndAlso(
            Expression.Invoke(greaterThan, param),
            Expression.Invoke(lessThan, param)
        );
Expression<Func<string, bool>> filter = Expression.Lambda<Func<string, bool>>(body, param);

ParameterExpression : نوع پارامتری را که میخواهیم روی آن شرط را روی آن اعمال کنیم، مشخص کرده‌ایم.

MemberExpression  : پراپرتی Length را معرفی کرده‌ایم که قرار است شرطی بر روی این پراپرتی اعمال شود.

ConstantExpression   : مقدار ثابتی که پراپرتی MemeberExpression قرار است با آن مقایسه شود.

BinaryExpression   : نود تایپ را مشخص کرده‌ایم که برابر است با GreaterThan.

سپس Expression مربوط به هرکدام را در greaterThan و lessThan ایجاد کرده‌ایم و این دو را باهم And کرده و در متغییر body قرار داده‌ایم و در نهایت filter را با دستور Expression.Lambda ایجاد کرده‌ایم که برابر است با :

Expression<Func<string, bool>> filter = s => s.Length > 5 && s.Length < 45;


ساخت یک داینامیک فیلتر

در ادامه میخواهیم یک داینامیک فیلتر را ایجاد کنیم که به طور مثال برنامه نویس از سمت فرانت‌اند بتواند فیلتر‌های ساده‌ای را اعمال کند. برای این کار یک کلاس برای فیلتر ایجاد میکنیم :

public class DynamicModel
{
    public string Name { get; set; }
    public string Comparison { get; set; }
    public object Data { get; set; }
}

پراپرتی Data مقداری است که باید با آن مقایسه انجام شود.

Comparison نوع عملیات را مشخص میکند مانند : Equal, LessThan, GreaterThan و... .

پراپرتی Name نام پراپرتی است که باید شرط روی آن اعمال شود.

کلاس ثابت ها:

public static class ComparisonConstant
{
    public const string LessThan = "LesThan";
    public const string LessThanEqual = "LesThanEqual";
    public const string GreaterThan = "GreaterThan";
    public const string GreaterThanEqual = "GreaterThanEqual";
    public const string Equal = "Equal";
    public const string NotEqual = "NotEqual";
}

ساخت اکستنشن متد:

public static IQueryable<TModel> DynamicFilter<TModel>(this IQueryable<TModel> iqueryable, IEnumerable<DynamicModel> dynamicModel)
{
    return iqueryable.Where(Filter<TModel>(dynamicModel));
}  
public static Expression<Func<TModel, bool>> Filter<TModel>(IEnumerable<DynamicModel> dynamicModel)
{
    Expression<Func<TModel, bool>> result = a => true;
    foreach (var item in dynamicModel)
    {
        ParameterExpression parameterExpression = Expression.Parameter(typeof(TModel));
        MemberExpression memberExpression = Expression.Property(parameterExpression, item.Name);
        ConstantExpression constantExpression = Expression.Constant(item.Data);
        BinaryExpression comparison = GetBinaryExpression(item.Comparison, memberExpression, constantExpression);
        var expression = Expression.Lambda<Func<TModel, bool>>(comparison, parameterExpression);
        var param = Expression.Parameter(typeof(TModel), "x");
        var body = Expression.AndAlso(
                    Expression.Invoke(result, param),
                    Expression.Invoke(expression, param)
                );
        result = Expression.Lambda<Func<TModel, bool>>(body, param);
    }
    return result;
}

ورودی این مدل، لیستی از DynamicModel میباشد که به ازای هر کدام از آیتم‌ها، یک BinaryExpression ایجاد میکند و آن را با result تعریف شده And میکند. یعنی تمامی آیتم‌های ارسال شده باهم And میشوند.

متد GetBinaryExpression بر اساس مقدار فیلد Comparison که از سمت فرانت ارسال میشود، کار میکند:

private static BinaryExpression GetBinaryExpression(string comparison, MemberExpression memberExpression, ConstantExpression constantExpression)
{
    switch (comparison)
    {
        case ComparisonConstant.Equal:
            return Expression.Equal(memberExpression, constantExpression);
        case ComparisonConstant.LessThan:
            return Expression.LessThan(memberExpression, constantExpression);
        case ComparisonConstant.GreaterThan:
            return Expression.GreaterThan(memberExpression, constantExpression);
        case ComparisonConstant.NotEqual:
            return Expression.NotEqual(memberExpression, constantExpression);
        case ComparisonConstant.GreaterThanEqual:
            return Expression.GreaterThanOrEqual(memberExpression, constantExpression);
        case ComparisonConstant.LessThanEqual:
            return Expression.LessThanOrEqual(memberExpression, constantExpression);
        default:
            return null;
    }
}

 کلاس Category را در نظر بگیرید که شامل دو پراپرتی Title و Id میباشد و میخواهیم از این داینامیک فیلتر، برای فیلتر کردن دیتاها استفاده کنیم از سمت فرانت‌اند. اگر از سمت فرانت‌اند چنین دیتایی ارسال شود:

[
   {
      "Name":"Title",
      "Comparison":"Equal",
      "Data":"Hi"
   },
   {
      "Name":"Id",
      "Comparison":"LesThanEqual",
      "Data": 100
   }
]

تمامی رکوردهایی که مقدار پراپرتی Title آنها برابر Hi باشد و Id آن کوچکتر مساوی 100 باشد، از دیتابیس خوانده میشود.

var categories = _dbContext.Categories
                         .DynamicFilter(filter)//filter => IEnumerable<DynamicModel>
                         .ToList();

گیت هاب داینامیک فیلتر 

مطالب
Roslyn #5
بررسی Semantic Models

همانطور که از قسمت قبل به‌خاطر دارید، برای دسترسی به اطلاعات semantics، نیاز به یک context مناسب که همان Compilation API است، می‌باشد. این context دارای اطلاعاتی مانند دسترسی به تمام نوع‌های تعریف شده‌ی توسط کاربر و متادیتاهای ارجاعی، مانند کلاس‌های پایه‌ی دات نت فریم‌ورک است. بنابراین پس از ایجاد وهله‌ای از Compilation API، کار با فراخوانی متد GetSemanticModel آن ادامه می‌یابد. در ادامه با مثال‌هایی، کاربرد این متد را بررسی خواهیم کرد.


ساختار جدید Optional

خروجی‌های تعدادی از متدهای Roslyn با ساختار جدیدی به نام Optional ارائه می‌شوند:
    public struct Optional<T>
    {
        public bool HasValue { get; }
        public T Value { get; }
    }
این ساختار که بسیار شبیه است به ساختار قدیمی <Nullable<T، منحصر به Value types نیست و Reference types را نیز شامل می‌شود و بیانگر این است که آیا یک Reference type، واقعا مقدار دهی شده‌است یا خیر؟


دریافت مقادیر ثابت Literals

فرض کنید می‌خواهیم مقدار ثابت ; int x = 42 را دریافت کنیم. برای اینکار ابتدا باید syntax tree آن تشکیل شود و سپس نیاز به یک سری حلقه و if و else و همچنین بررسی نال بودن بسیاری از موارد است تا به نود مقدار ثابت 42 برسیم. سپس متد GetConstantValue مربوط به GetSemanticModel را بر روی آن فراخوانی می‌کنیم تا به مقدار واقعی آن که ممکن است در اثر محاسبات جاری تغییر کرده باشد، برسیم.
اما روش بهتر و توصیه شده، استفاده از CSharpSyntaxWalker است که در انتهای قسمت سوم معرفی شد:
class ConsoleWriteLineWalker : CSharpSyntaxWalker
{
    public ConsoleWriteLineWalker()
    {
        Arguments = new List<ExpressionSyntax>();
    }
 
    public List<ExpressionSyntax> Arguments { get; }
 
    public override void VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        var member = node.Expression as MemberAccessExpressionSyntax;
        var type = member?.Expression as IdentifierNameSyntax;
        if (type != null && type.Identifier.Text == "Console" && member.Name.Identifier.Text == "WriteLine")
        {
            if (node.ArgumentList.Arguments.Count == 1)
            {
                var arg = node.ArgumentList.Arguments.Single().Expression;
                Arguments.Add(arg);
                return;
            }
        }
 
        base.VisitInvocationExpression(node);
    }
}
اگر به کدهای ادامه‌ی بحث دقت کنید، قصد داریم مقادیر ثابت آرگومان‌های Console.WriteLine را استخراج کنیم. به همین جهت در این SyntaxWalker، نوع Console و متد WriteLine آن مورد بررسی قرار گرفته‌اند. اگر این نود دارای یک تک آرگومان بود، آین آرگومان استخراج شده و به لیست آرگومان‌های خروجی این کلاس اضافه می‌شود.
در ادامه نحوه‌ی استفاده‌ی از این SyntaxWalker را ملاحظه می‌کنید. در اینجا ابتدا سورس کدی حاوی یک سری Console.WriteLine که دارای تک آرگومان‌های ثابتی هستند، تبدیل به syntax tree می‌شود. سپس از روی آن CSharpCompilation تولید می‌گردد تا بتوان به اطلاعات semantics دسترسی یافت:
static void getConstantValue()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        Console.WriteLine(3.14);
                        Console.WriteLine(""qux"");
                        Console.WriteLine('c');
                        Console.WriteLine(null);
                        Console.WriteLine(x * 2 + 1);
                    }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
 
    // Analyze the constant argument (if any).
    foreach (var arg in walker.Arguments)
    {
        var val = model.GetConstantValue(arg);
        if (val.HasValue)
        {
            Console.WriteLine(arg + " has constant value " + (val.Value ?? "null") + " of type " + (val.Value?.GetType() ?? typeof(object)));
        }
        else
        {
            Console.WriteLine(arg + " has no constant value");
        }
    }
}
در ادامه با استفاده از CSharpCompilation و متد GetSemanticModel آن به SemanticModel جاری دسترسی خواهیم یافت. اکنون SyntaxWalker را وارد به حرکت بر روی ریشه‌ی syntax tree سورس کد آنالیز شده می‌کنیم. به این ترتیب لیست آرگومان‌های متدهای Console.WriteLine بدست می‌آیند. سپس با فراخوانی متد model.GetConstantValue بر روی هر آرگومان دریافتی، مقادیر آن‌ها با فرمت <Optional<T استخراج می‌شوند.
خروجی نمایش داده شده‌ی توسط برنامه به صورت ذیل است:
 3.14 has constant value 3.14 of type System.Double
"qux" has constant value qux of type System.String
'c' has constant value c of type System.Char
null has constant value null of type System.Object
x * 2 + 1 has no constant value


درک مفهوم Symbols

اینترفیس ISymbol در Roslyn، ریشه‌ی تمام Symbolهای مختلف مدل سازی شده‌ی در آن است که تعدادی از آن‌ها را در تصویر ذیل مشاهده می‌کنید:


API کار با Symbols بسیار شبیه به API کار با Reflection است با این تفاوت که در زمان آنالیز کدها رخ می‌دهد و نه در زمان اجرای برنامه. همچنین در Symbols API امکان دسترسی به اطلاعاتی مانند locals, labels و امثال آن نیز وجود دارد که با استفاده از Reflection زمان اجرای برنامه قابل دسترسی نیستند. برای مثال فضاهای نام در Reflection صرفا به صورت رشته‌ای، با دات جدا شده از نوع‌های آنالیز شده‌ی توسط آن است؛ اما در اینجا مطابق تصویر فوق، یک اینترفیس مجزای خاص خود را دارد. جهت سهولت کار کردن با Symbols، الگوی Visitor با معرفی کلاس پایه‌ی SymbolVisitor نیز پیش بینی شده‌است.
static void workingWithSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        // #insideBar
                    }
                }
 
                class Qux
                {
                    protected int Baz { get; set; }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse enclosing symbol hierarchy.
    var cursor = code.IndexOf("#insideBar");
    var barSymbol = model.GetEnclosingSymbol(cursor);
    for (var symbol = barSymbol; symbol != null; symbol = symbol.ContainingSymbol)
    {
        Console.WriteLine(symbol);
    }
 
    // Analyze accessibility of Baz inside Bar.
    var bazProp = ((CompilationUnitSyntax)root)
        .Members.OfType<ClassDeclarationSyntax>()
        .Single(m => m.Identifier.Text == "Qux")
        .Members.OfType<PropertyDeclarationSyntax>()
        .Single();
    var bazSymbol = model.GetDeclaredSymbol(bazProp);
    var canAccess = model.IsAccessible(cursor, bazSymbol);
}
یکی از کاربردهای مهم Symbols API دریافت اطلاعات Symbols نقطه‌ای خاص از کدها می‌باشد. برای مثال در محل اشاره‌گر ادیتور، چه Symbols ایی تعریف شده‌اند و از آن‌ها در مباحث ساخت افزونه‌های آنالیز کدها زیاد استفاده می‌شود. نمونه‌ای از آن‌را در قطعه کد فوق ملاحظه می‌کنید. در اینجا با استفاده از متد GetEnclosingSymbol، سعی در یافتن Symbols قرار گرفته‌ی در ناحیه‌ی insideBar# کدهای فوق داریم؛ با خروجی ذیل که نام demo.exe آن از نام CSharpCompilation آن گرفته شده‌است:
 Foo.Bar(int)
Foo
<global namespace>
Demo.exe
Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null


همچنین در ادامه‌ی کد، توسط متد IsAccessible قصد داریم بررسی کنیم آیا Symbol قرار گرفته در محل کرسر، دسترسی به خاصیت protected کلاس Qux را دارد یا خیر؟ که پاسخ آن خیر است.


آشنایی با Binding symbols

یکی از مراحل کامپایل کد، binding نام دارد و در این مرحله است که اطلاعات Symbolic هر نود از Syntax tree دریافت می‌شود. برای مثال در اینجا مشخص می‌شود که این x، آیا یک متغیر محلی است، یا یک فیلد و یا یک خاصیت؟
مثال ذیل بسیار شبیه است به مثال getConstantValue ابتدای بحث، با این تفاوت که در حلقه‌ی آخر کار از متد GetSymbolInfo استفاده شده‌است:
static void bindingSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    private int y;
 
                    void Bar(int x)
                    {
                        Console.WriteLine(x);
                        Console.WriteLine(y);
 
                        int z = 42;
                        Console.WriteLine(z);
 
                        Console.WriteLine(a);
                    }
                }";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
    // Bind the arguments.
    foreach (var arg in walker.Arguments)
    {
        var symbol = model.GetSymbolInfo(arg);
        if (symbol.Symbol != null)
        {
            Console.WriteLine(arg + " is bound to " + symbol.Symbol + " of type " + symbol.Symbol.Kind);
        }
        else
        {
            Console.WriteLine(arg + " could not be bound");
        }
    }
}
با این خروجی:
 x is bound to int of type Parameter
y is bound to Foo.y of type Field
z is bound to z of type Local
a could not be bound
در مثال فوق، با استفاده از Syntax Walker طراحی شده در ابتدای بحث که کار استخراج آرگومان‌های متدهای Console.WriteLine را انجام می‌دهد، قصد داریم بررسی کنیم، هر آرگومان به چه Symbol ایی بایند شده‌است و نوعش چیست؟ برای مثال Console.WriteLine اول که از پارامتر x استفاده می‌کند، نوع x مورد استفاده‌اش چیست؟ آیا فیلد است، متغیر محلی است یا یک پارامتر؟ این اطلاعات را با استفاده از متد model.GetSymbolInfo می‌توان استخراج کرد.
اشتراک‌ها
دوره پیاده سازی minimal API با دات نت 7

.NET 7 minimal API from scratch | FULL COURSE | clean architecture, repository pattern, CQRS MediatR

In this course I want to provide you a project structure and code organization to get you started with real .NET 7 minimal API projects. It's a full course on this topic where I start from creating and explaining the project structure, setting up different layers using EF Core, repository pattern, CQRS and MediatR. The biggest part of the video is however around the .NET 7 minimal API, taking you from the initial setup, explaining route handlers, implementing all CRUD operations and so on. Last but not least, this course walks you through the process of refactoring the .NET 7 minimal API so that it becomes readable, maintainable and scalable. At the end, you'll have a full project structure organized according to modern architectural patterns that you can take as a template for your own projects.

Contents
1. Intro: 00:00
2. Structuring the solution: 01:00
3. Coding the domain layer: 05:25
4. Coding the data access layer: 08:22
5. Creating repositories: 11:17
6. Adding migrations and database update: 22:30
7. CQRS with MediatR: 29:07
8. Route and rout handlers: 52:06
9. Dependency injection: 55:52
10. Implementing GET by ID : 57:40
11. Implementing POST route: 01:00:26
12. Implementing GET all route: 01:03:41
13. Implement PUT and DELETE: 01:04:57
14. Testing with Postman: 01:09:01
15. Is there a problem? 01:12:41
16. Refactoring service registrations: 01:15:49
17. Refactoring route registrations: 01:20:01
18. Automatic registration of endpoints: 01:26:28
19. Introducing route groups:  01:31:43
20. Extract lambdas into instance methods: 01:34:31
21: Model validation with endpoint filters: 01:45:58
22. Global exception handling: 01:55:10
23. Conclusions: 01:59:49 

دوره پیاده سازی minimal API با دات نت 7
مطالب
توسعه سیستم مدیریت محتوای 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 یا همان گزارش خطا هم سیستمی شبیه به امتیاز دهی ستاره‌ای در نظر گرفته ایم. برای اینکه مقاله خیلی طولانی شد، بهتر است در مقاله‌ای جدا کار را ادامه داد.
نتیجه این قسمت

مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
در ادامه مطالب قبل
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#

قبل از شروع توضیحات متد‌های کلاس Shape در ادامه پست‌های قبل در ^ و ^ ابتدا به تشریح یک تصویر می‌پردازیم.

نحوه ترسیم شی

خوب همانگونه که در تصویر بالا مشاده می‌نمایید، برای رسم یک شی چهار حالت متفاوت ممکن است پیش بیاید. (دقت کنید که ربع اول محور مختصات روی بوم گرافیکی قرار گرفته است، در واقع گوشه بالا و سمت چپ بوم گرافیکی نقطه (0 و 0) محور مختصات است و عرض بوم گرافیکی محور X‌ها و ارتفاع بوم گرافیکی محور Y‌ها را نشان می‌دهد)
  1. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y < EndPoint.Y خواهد بود. (StartPoint نقطه ای است که ابتدا ماوس شروع به ترسیم می‌کند، و EndPoint زمانی است که ماوس رها شده و پایان ترسیم را مشخص می‌کند.)
  2. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  3. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  4. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.

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

using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Helpers
    /// </summary>
    public static class Helpers
    {
        /// <summary>
        /// Draws the preview.
        /// </summary>
        /// <param name="g">The g.</param>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundBrush">The background brush.</param>
        /// <param name="shapeType">Type of the shape.</param>
        public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Brush backgroundBrush, ShapeType shapeType)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }

            switch (shapeType)
            {
                case ShapeType.Ellipse:
                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Rectangle:
                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Circle:
                    float raduis = Math.Max(width, height);

                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, raduis, raduis);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, raduis, raduis);
                    break;
                case ShapeType.Square:
                    float side = Math.Max(width, height);

                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, side, side);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, side, side);
                    break;
                case ShapeType.Line:
                    g.DrawLine(new Pen(foreColor, thickness), startPoint, endPoint);
                    break;
                case ShapeType.Diamond:
                    var points = new PointF[4];
                    points[0] = new PointF(x + width / 2, y);
                    points[1] = new PointF(x + width, y + height / 2);
                    points[2] = new PointF(x + width / 2, y + height);
                    points[3] = new PointF(x, y + height / 2);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, points);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), points);
                    break;
                case ShapeType.Triangle:
                    var tPoints = new PointF[3];
                    tPoints[0] = new PointF(x + width / 2, y);
                    tPoints[1] = new PointF(x + width, y + height);
                    tPoints[2] = new PointF(x, y + height);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, tPoints);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), tPoints);
                    break;
            }
            if (shapeType != ShapeType.Line)
            {
                g.DrawString(String.Format("({0},{1})", x, y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x - 20, y - 25);
                g.DrawString(String.Format("({0},{1})", x + width, y + height), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x + width - 20, y + height + 5);
            }
            else
            {
                g.DrawString(String.Format("({0},{1})", startPoint.X, startPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), startPoint.X - 20, startPoint.Y - 25);
                g.DrawString(String.Format("({0},{1})", endPoint.X, endPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), endPoint.X - 20, endPoint.Y + 5);
            }

        }
    }
}
متد های این کلاس:
  • DrawPreview : این متد پیش نمایشی برای شی در زمان ترسیم، جابجایی و تغییر اندازه آماده می‌کند، پارامترهای آن عبارتند از : بوم گرافیکی، نقطه شروع، نقطه پایان و رنگ قلم ترسیم پیش نمایش شی، ضخامت خط، آیا شی توپر باشد؟، الگوی پر کردن پس زمینه شی ، و نوع شی ترسیمی می‌باشد.
در ادامه پست‌های قبل ادامه کد کلاس Shape را تشریح می‌کنیم.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Net;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape (Base Class)
    /// </summary>
    public abstract partial class Shape
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        protected Shape(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            CalulateLocationAndSize(startPoint, endPoint);
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            IsFill = isFill;
            BackgroundColor = backgroundColor;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> class.
        /// </summary>
        protected Shape() { }

#endregion Constructors 
        
#region Methods (10) 

// Public Methods (9) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public virtual void Draw(Graphics g)
        {
            if (!IsSelected) return;
            float diff = Thickness + 4;
            Color myColor = Color.DarkSeaGreen;
            g.DrawString(String.Format("({0},{1})", StartPoint.X, StartPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), StartPoint.X - 20, StartPoint.Y - 25);
            g.DrawString(String.Format("({0},{1})", EndPoint.X, EndPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), EndPoint.X - 20, EndPoint.Y + 5);
            if (ShapeType != ShapeType.Line)
            {
                g.DrawRectangle(new Pen(myColor), X, Y, Width, Height);

                //  1 2 3
                //  8   4 
                //  7 6 5   
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, StartPoint.Y - diff / 2);
                var point3 = new PointF(EndPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point4 = new PointF(EndPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);
                var point5 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point6 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, EndPoint.Y - diff / 2);
                var point7 = new PointF(StartPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point8 = new PointF(StartPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);


                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point3.X, point3.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point4.X, point4.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point5.X, point5.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point6.X, point6.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point7.X, point7.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point8.X, point8.Y, diff, diff);
            }
            else
            {
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
            }
        }

        /// <summary>
        /// Points the in sahpe.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="tolerance">The tolerance.</param>
        /// <returns>
        ///   <c>true</c> if [has point in sahpe] [the specified point]; otherwise, <c>false</c>.
        /// </returns>
        public virtual bool HasPointInSahpe(PointF point, byte tolerance = 5)
        {
            return point.X > (StartPoint.X - tolerance) && point.X < (EndPoint.X + tolerance) && point.Y > (StartPoint.Y - tolerance) && point.Y < (EndPoint.Y + tolerance);
        }

        /// <summary>
        /// Moves the specified location.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns></returns>
        public virtual PointF Move(Point location)
        {
            StartPoint = new PointF(location.X, location.Y);
            EndPoint = new PointF(location.X + Width, location.Y + Height);
            return StartPoint;
        }

        /// <summary>
        /// Moves the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual PointF Move(int dx, int dy)
        {
            StartPoint = new PointF(StartPoint.X + dx, StartPoint.Y + dy);
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return StartPoint;
        }

        /// <summary>
        /// Resizes the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual SizeF Resize(int dx, int dy)
        {
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return new SizeF(Width, Height);
        }

        /// <summary>
        /// Resizes the specified start point.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="currentPoint">The current point.</param>
        public virtual void Resize(PointF startPoint, PointF currentPoint)
        {
            var dx = (int)(currentPoint.X - startPoint.X);
            var dy = (int)(currentPoint.Y - startPoint.Y);
            if (startPoint.X >= X - 5 && startPoint.X <= X + 5)
            {
                StartPoint = new PointF(currentPoint.X, StartPoint.Y);
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.X >= EndPoint.X - 5 && startPoint.X <= EndPoint.X + 5)
            {
                Width += dx;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.Y >= Y - 5 && startPoint.Y <= Y + 5)
            {
                Y = currentPoint.Y;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
            else if (startPoint.Y >= EndPoint.Y - 5 && startPoint.Y <= EndPoint.Y + 5)
            {
                Height += dy;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
        }

        /// <summary>
        /// Sets the background brush as hatch.
        /// </summary>
        /// <param name="hatchStyle">The hatch style.</param>
        public virtual void SetBackgroundBrushAsHatch(HatchStyle hatchStyle)
        {
            var brush = new HatchBrush(hatchStyle, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as linear gradient.
        /// </summary>
        public virtual void SetBackgroundBrushAsLinearGradient()
        {
            var brush = new LinearGradientBrush(StartPoint, EndPoint, ForeColor, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as solid.
        /// </summary>
        public virtual void SetBackgroundBrushAsSolid()
        {
            var brush = new SolidBrush(BackgroundColor);
            BackgroundBrush = brush;
        }
// Private Methods (1) 

        /// <summary>
        /// Calulates the size of the location and.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        private void CalulateLocationAndSize(PointF startPoint, PointF endPoint)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            EndPoint = new PointF(X + width, Y + height);
        }

#endregion Methods 
    }
}

حال به تشریح سازنده کلاس می‌پردازیم:
  • Shape: پارامترهای این سازنده به ترتیب عبارتند از نقطه شروع، نقطه پایان، عمق شی، رنگ قلم، ضخامت خط، آیا شی توپر باشد؟، و رنگ پر کردن شی، در این سازنده ابتدا توسط متدی به نام CalulateLocationAndSize(startPoint, endPoint); b نقاط ابتدا و انتهای شی مورد نظر تنظیم می‌شود، در متد مذکور بررسی می‌شود در صورتی که نقاط شروع و پایان یکی از حالت‌های 1 ، 2، 3، 4 از تصویر ابتدا پست باشد همگی تبدیل به حالت 1 خواهد شد.

سپس به تشریح متدهای کلاس Shape می‌پردازیم:

  • Draw: این متد دارای یک پارامتر ورودی است که بوم گرافیکی مورد نظر می‌باشد، در واقع شی مورد نظر خود را بروی این بوم گرافیکی ترسیم می‌کند. در کلاس پایه کار این متد زیاد پیچیده نیست، در صورتی که شی در حالت انتخاب باشد (IsSelected = true) بروی شی مورد نظر 8 مربع کوچک ترسیم می‌شود و اگر شی مورد نظر خط باشد دو مربع کوچک در طرفین خط رسم می‌شود که نشان دهنده انتخاب شدن شی مورد نظر است. این متد به صورت virtual تعریف شده است یعنی کلاس هایی که از Shape ارث میبرند می‌توانند این متد را برای خود از نو بازنویسی کرده (override کنند) و تغییر رفتار دهند.
  • HasPointInSahpe : این متد نیز به صورت virtual تعریف شده است دارای خروجی بولین می‌باشد. پارامتر‌های این متد عبارتند از یک نقطه و یک عدد که نشان دهنده تلرانش نقطه بر حسب پیکسل می‌باشد. کار این متد این است که یک نقطه را گرفته و بررسی می‌کند که آیا نقطه مورد نظر با تلرانس وارد شده آیا در داخل شی واقع شده است یا خیر (مثلا وجود نقطه در مستطیل یا وجود نقطه در دایره فرمول‌های متفاوتی دارند که در اینجا پیش فرض برای تمامی اشیا حالت مستطیل در نظر گرفته شده که می‌توانید آنها را بازنویسی (override) کنید).
  • Move: این متد به عنوان پارامتر یک نقطه را گرفته و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را تغییر می‌دهد.
  • Move: این متد نیز برای جابجایی شی به کار می‌رود، این متد دارای پارامترهای جابجابی در راستای محور Xها , جابجایی در راستای محور Yها؛ و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را با توجه به پارامترهای ورودی تغییر می‌دهد. 
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، این متد دارای پارامترهای تغییر اندازه در راستای محور Xها , تغییر اندازه در راستای محور Yها می‌باشد و نقطه پایان شی مورد نظر را تغییر می‌دهد اما نقطه شروع تغییری نمی‌کند.
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، در زمان تغییر اندازه شی با ماوس ابتدا یک نقطه شروع وجود دارد که ماوس در آن نقطه کلیک شده و شروع به درگ کردن شی جهت تغییر اندازه می‌کند (پارامتر اول این متد نقطه شروع درگ کردن جهت تغییر اندازه را مشخص می‌کند startPoint)، سپس در یک نقطه ای درگ کردن تمام می‌شود در این نقطه باید شی تغییر اندازه پیدا کرده و ترسیم شود ( پارامتر دوم این متد نقطه مذکور می‌باشد currentLocation). سپس با توجه با این دو نقطه بررسی می‌شود که تغییر اندازه در کدام جهت صورت گرفته است و اعداد جهت تغییرات نقاط شروع و پایان شی مورد نظر محاسبه می‌شوند. (مثلا تغییر اندازه در مستطیل از ضلع بالا به طرفین، یا از ضلع سمت راست به طرفین و ....). البته برای مربع و دایره باید کاری کنیم که طول و عرض تغییر اندازه یکسان باشد.
  • CalulateLocationAndSize: این متد که در سازنده کلاس استفاده شده در واقع دو نقطه شروع و پایان را گرفته و با توجه به تصویر ابتدای پست حالت‌های 1 و 2 و3  و 4 را به حالت 1 تبدیل کرده و StartPoint و EndPoint را اصلاح می‌کند.
  • SetBackgroundBrushAsHatch: این متد یک الگوی Brush گرفته و با توجه به رنگ پس زمینه شی خصوصیت BackgroundBrush را مقداردهی می‌کند.
  • SetBackgroundBrushAsLinearGradient: این متد با توجه به خصوصیت ForeColor و BackgroundColor یک Gradiant Brush ساخته و آن را به خصوصیت
    BackgroundBrush نسبت می‌کند. 
  • SetBackgroundBrushAsSolid: یک الگوی پر کردن توپر برای شی مورد نظر با توجه به خصوصیت BackgroundColor شی ایجاد کرده و آن را به خصوصیت BackgroundBrush شی نسبت می‌دهد.

تذکر: متد‌های Move، Resize و HasPointInShape به صورت virtual تعریف شده تا کلاس‌های مشتق شده در صورت نیاز خود کد رفتار مورد نظر خود را override کرده یا از همین رفتار استفاده نمایند.

خوشحال می‌شم در صورتی که در Refactoring کد نوشته شده با من همکاری کنید.

در پست‌های آینده به بررسی و پیاده سازی دیگر کلاس‌ها خواهیم پرداخت.

نظرات مطالب
نمایش ساختارهای درختی توسط jqGrid
- مدل توضیح داده شده در اینجا Adjacency model است و شباهت زیادی به «مدل‌های خود ارجاع دهنده» دارد. طراحی خودتان را بر اساس مطلب یاد شده انجام دهید و یا نگاشت نهایی اطلاعات خودتان را تبدیل کنید به این حالت. مهم نیست ساختار اصلی بانک اطلاعاتی شما به چه صورتی است. همینقدر که خروجی کوئری آن Adjacency model باشد (شبیه به ساختار کلاس BlogComment مطلب فوق)، با توضیحات فوق سازگار خواهد بود. اگر مستقیما SQL می‌نویسید، در مطلب «SQL Antipattern #2» کوئری‌های آن موجود است. اگر با LINQ و EF کار می‌کنید، توضیحات مطلب «مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first» را پیگیری کنید.
 
+ امکان اتصال دو جدول با کلید خارجی نیز در اینجا وجود دارد:
public ActionResult GetComments(JqGridRequest request, int? nodeid, int? parentid, int? n_level)
در امضای متد فوق که در بحث مطرح شده، node id تعیین کننده‌ی واکشی از parent id است. اگر node id نال بود، یعنی نمایش بار اول لیست (نمایش لیست استان‌ها):
if (nodeid == null)
{
     productsQuery = productsQuery.Where(x => x.ParentId == null);
}
 اگر نال نبود (درخواست واکشی اطلاعات استان بر اساس node id آن)، یعنی روی یک نود کلیک شده‌است. در اینجا فیلد parent id می‌تواند به عنوان کلید خارجی که به جدولی دیگر اشاره می‌کند نیز تفسیر و جایگزین شود:
else
{
   // آی دی یک گره می‌تواند کلید خارجی یک جدول دیگر باشد
   productsQuery = productsQuery.Where(x => x.ParentId == nodeid.Value);
}
برای این متد نهایتا مهم نیست که productsQuery به چه نحوی تهیه می‌شود. مهم نیست که از چند جدول مختلف حاصل می‌شود. فقط مقادیر نهایی آن مهم است.
مطالب
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
برای بررسی صحت کدملی باید کمی با ساختار این کد آشنا شویم. کد ملی 6-761161-007 را در نظر بگیرید. همانطور که مشاهده می‌کنید این کد به 3 قسمت تقسیم شده است. قسمت اول که یک عدد 3 رقمی است نشان دهنده محل تولد فرد است. 6 رقم وسط شماره شناسایی فرد و رقم آخر، رقم کنترل است. در حقیقت ساختار 9 رقم اول طبق الگوریتمی قابل بررسی است که خروجی آن الگوریتم همان رقم آخر است. اگر خروجی الگوریتم با رقم کنترل برابر باشد می‌توان گفت کدملی وارد شده معتبر است و در غیر اینصورت کدملی معتبر نخواهد بود.
برای این منظور دو متدالحاقی زیر به پروژه DNT.Extensions اضافه شده است که بدنه آنها به شرح زیر است:
        /// <summary>
        /// Validate IR National Code
        /// </summary>
        /// <param name="nationalcode">National Code</param>
        /// <returns></returns>
        public static bool IsValidNationalCode(this string nationalcode)
        {
            int last;
            return nationalcode.IsValidNationalCode(out last);
        }

        /// <summary>
        /// Validate IR National Code
        /// </summary>
        /// <param name="nationalcode">National Code</param>
        /// <param name="lastNumber">Last Number Of National Code</param>
        /// <returns></returns>
        public static bool IsValidNationalCode(this string nationalcode, out int lastNumber)
        {
            lastNumber = -1;
            if (!nationalcode.IsItNumber()) return false; 
            var array = nationalcode.ToCharArray();
            if (array.Length != 10) return false;
            var j = 10;
            var sum = 0;
            for (var i = 0; i < array.Length - 1; i++)
            {
                sum += Int32.Parse(array[i].ToString(CultureInfo.InvariantCulture)) * j;
                j--;
            }
            var div = sum / 11;
            var r = div * 11;
            var diff = Math.Abs(sum - r);

            if (diff <= 2)
            {
                lastNumber = diff;
                return diff == Int32.Parse(array[9].ToString(CultureInfo.InvariantCulture));
            }
            var temp = Math.Abs(diff - 11);
            lastNumber = temp;
            return temp == Int32.Parse(array[9].ToString(CultureInfo.InvariantCulture));
        }
طریقه استفاده از این متدها نیز به شرح ذیل است:
bool b = "0077611616".IsValidNationalCode();
پی نوشت:
لطفاً بخش نظرات خوانندگان را نیز دنبال کنید.
مطالب
ارسال مستقیم یک فایل PDF به چاپگر
برنامه رایگان Adobe reader یک سری خط فرمان دارد که توسط آن‌ها می‌توان فایل‌های PDF را مستقیما به چاپگر ارسال کرد. در ادامه قطعه کدی را ملاحظه خواهید کرد که انجام اینکار را کپسوله می‌کند:
using System;
using System.Diagnostics;
using System.IO;
using System.Management;
using Microsoft.Win32;

namespace PdfFilePrinter
{
    /// <summary>
    /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
    /// dialog box, then terminating the Reader.
    /// </summary>
    public class AcroPrint
    {
        /// <summary>
        /// The Adobe Reader or Adobe Acrobat path such as 'C:\Program Files\Adobe\Adobe Reader X\AcroRd32.exe'.
        /// If it's not specified, the InstalledAdobeReaderPath property value will be used.
        /// </summary>
        public string AdobeReaderPath { set; get; }

        /// <summary>
        /// Returns the default printer name.
        /// </summary>
        public string DefaultPrinterName
        {
            get
            {
                var query = new ObjectQuery("SELECT * FROM Win32_Printer");
                using (var searcher = new ManagementObjectSearcher(query))
                {
                    foreach (var mo in searcher.Get())
                    {
                        if (((bool?)mo["Default"]) ?? false)
                            return mo["Name"] as string;
                    }
                }
                return string.Empty;
            }
        }

        /// <summary>
        /// The name and path of the PDF file to print.
        /// </summary>
        public string PdfFilePath { set; get; }

        /// <summary>
        /// Name of the printer such as '\\PrintServer\HP LaserJet'.
        /// If it's not specified, the DefaultPrinterName property value will be used.
        /// </summary>
        public string PrinterName { set; get; }

        /// <summary>
        /// Returns the HKEY_CLASSES_ROOT\Software\Adobe\Acrobat\Exe value.
        /// If AcroRd32.exe does not exist, returns string.Empty
        /// </summary>
        public string InstalledAdobeReaderPath
        {
            get
            {
                var acroRd32Exe = Registry.ClassesRoot.OpenSubKey(@"Software\Adobe\Acrobat\Exe", writable: false);
                if (acroRd32Exe == null)
                    return string.Empty;

                var exePath = acroRd32Exe.GetValue(string.Empty) as string;
                if (string.IsNullOrEmpty(exePath))
                    return string.Empty;

                exePath = exePath.Trim(new[] { '"' });
                return File.Exists(exePath) ? exePath : string.Empty;
            }
        }

        /// <summary>
        /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
        /// dialog box, then terminating the Reader.
        /// </summary>
        /// <param name="timeout">The amount of time, in milliseconds, to wait for the associated process to exit. The maximum is the largest possible value of a 32-bit integer, which represents infinity to the operating system.</param>
        public void PrintPdfFile(int timeout = Int32.MaxValue)
        {
            if (!File.Exists(PdfFilePath))
                throw new ArgumentException(PdfFilePath + " does not exist.");

            var args = string.Format("/N /T \"{0}\" \"{1}\"", PdfFilePath, getPrinterName());
            var process = startAdobeProcess(args);
            if (!process.WaitForExit(timeout))
                process.Kill();
        }

        private Process startAdobeProcess(string arguments = "")
        {
            var startInfo = new ProcessStartInfo
                {
                    FileName = this.getExePath(),
                    Arguments = arguments,
                    CreateNoWindow = true,
                    ErrorDialog = false,
                    UseShellExecute = false,
                    Verb = "print"
                };

            return Process.Start(startInfo);
        }

        private string getPrinterName()
        {
            var printer = PrinterName;
            if (string.IsNullOrEmpty(printer))
                printer = DefaultPrinterName;

            if (string.IsNullOrEmpty(printer))
                throw new ArgumentException("Please set the PrinterName.");

            return printer;
        }

        private string getExePath()
        {
            var exePath = AdobeReaderPath;
            if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath))
                exePath = InstalledAdobeReaderPath;

            if (string.IsNullOrEmpty(exePath))
                throw new ArgumentException("Please set the full path of the AcroRd32.exe or Acrobat.exe.");

            return exePath;
        }
    }
}

توضیحات:
استفاده ابتدایی از کلاس فوق به نحو زیر است:
            new AcroPrint 
            {
                PdfFilePath = @"D:\path\test.pdf"
            }.PrintPdfFile();
به این ترتیب فایل PDF ذکر شده به چاپگر پیش فرض سیستم ارسال می‌شود.

ملاحظات:
- کدهای فوق نیاز به ارجاعی به اسمبلی استاندارد System.Management.dll نیز دارند.
- اگر علاقمند بودید که چاپگر خاصی را معرفی کنید (برای مثال یک چاپگر تعریف شده در شبکه)، می‌توانید خاصیت PrinterName را مقدار دهی نمائید.
- محل نصب Adobe reader از رجیستری ویندوز استخراج می‌شود. اما اگر محل نصب برنامه استاندارد نبود، نیاز است خاصیت AdobeReaderPath مقدار دهی گردد.
- تحت هر شرایطی برنامه Adobe reader ظاهر خواهد شد؛ حتی اگر در حین آغاز پروسه سعی در مخفی کردن پنجره آن نمائید. اینکار به عمد جهت مسایل امنیتی در این برنامه درنظر گرفته شده است تا کاربر بداند که پروسه چاپ آغاز شده است.
اشتراک‌ها
دوره مقدماتی 9 ساعته زبان Go

A 9-Hour Go Course for Beginners

⭐️ Contents ⭐️
(0:00:00) Intro
(0:03:17) Ch 1. Why write Go?
(0:27:39) Ch 2. Variables
(0:51:11) Ch 3. Functions
(1:16:58) Ch 4. Structs
(1:34:36) Ch 5. Interfaces
(2:00:26) Ch 6. Errors
(2:22:01) Ch 7. Loops
(2:48:21) Ch 8. Slices
(3:39:54) Ch 9. Maps
(4:06:19) Ch 10. Advanced functions
(4:31:03) Ch 11. Pointers
(4:48:02) Ch 12. Local development
(5:31:43) Ch 13. Channels & concurrency
(6:07:38) Ch 14. Mutexes
(6:30:56) Ch 15. Generics
(6:38:38) Ch 16. Quiz
(6:43:13) P1. RSS aggregator project
(6:53:43) P2. Chi router
(7:11:37) P3. Postgres database
(7:39:10) P4. Authentication w/ API keys
(8:18:28) P5. Many to many relationships
(8:39:13) P6. Aggregation worker
(9:05:28) P7. Viewing blog posts 

دوره مقدماتی 9 ساعته زبان Go