مطالب
C# 7 - Generalized Async Return Types
از زمان ارائه‌ی C# 5 و معرفی الگوهای async/await، تنها نوع‌های خروجی پشتیبانی شده، <Task، Task<T و void (در موارد خاص) بودند. مشکل همراه با این روش، اجبار به وهله سازی رسمی یک Task است؛ حتی اگر نوع خروجی کاملا مشخص باشد.
برای نمونه در متد ذیل، میزان حجم مصرفی در یک پوشه بازگشت داده می‌شود:
public async Task<long> GetDirectorySize(string path, string searchPattern)
{
    if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any())
        return 0;
    else
        return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern,
        SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length)));
}
اگر پوشه‌ای خالی باشد، حجم آن صفر است و در این حالت نیازی به ایجاد یک ترد مخصوص آن نیست. اما با توجه به اینکه خروجی متد، <Task<long است، هنوز هم باید این Task وهله سازی شود. برای نمونه اگر به کدهای IL آن دقت کنیم، return 0 آن به صورت ذیل ترجمه می‌شود:
 AsyncTaskMethodBuilder<long>.Create()

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


امکان تعریف خروجی‌های سفارشی متدهای async در C# 7.0

در C# 7 می‌توان خروجی‌های سفارشی را جهت متدهای async تعریف کرد و پیشنیاز اصلی آن پیاده سازی متد GetAwater است. برای مثال <System.Threading.Tasks.ValueTask<T یک چنین نوع سفارشی را ارائه می‌دهد. در این حالت، متد ابتدای بحث را می‌توان به صورت ذیل بازنویسی کرد:
public async ValueTask<long> GetDirectorySize(string path, string searchPattern)
{
    if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any())
        return 0;
    else
        return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern,
        SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length)));
}
اگر دقت کنید بجز تغییر نوع خروجی متد، تغییر دیگری نیاز نبوده‌است.
همانطور که از نام  ValueTask نیز مشخص است، یک struct است؛ برخلاف Task و تخصیص حافظه‌ی آن بر روی stack بجای heap صورت می‌گیرد. به این ترتیب با کاهش فشار بر روی GC، در حلقه‌هایی که خروجی value type دارند، با اندازه گیری‌های انجام شده، کارآیی تا 50 درصد هم می‌تواند بهبود یابد.

برای کامپایل قطعه کد فوق و تامین نوع جدید ValueTask، نیاز به نصب بسته‌ی نیوگت ذیل نیز می‌باشد:
 PM> install-package System.Threading.Tasks.Extensions
مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در حال حاضر امکان خاصی برای ایجاد ایندکس منحصر به فرد در EF First Code وجود ندارد, برای این کار راه‌های زیادی وجود دارد مانند پست قبلی آقای نصیری, در این آموزش از Data Annotation و یا همان Attribute  هایی که بالای Property‌های مدل‌ها قرار می‌دهیم, مانند کد زیر : 
public class User
    {
        public int Id { get; set; }

        [Unique]
        public string Email { get; set; }

        [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)]
        public string Username { get; set; }

        [Unique(UniqueIndexOrder.DESC)]
        public string PersonalCode{ get; set; }

        public string Password { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

همانطور که در کد بالا می‌بینید با استفاده از Attribute Unique ایندکس منحصر به فرد آن در دیتابیس ساخته خواهد شد.
ابتدا یک کلاس برای Attribute Unique به صورت زیر ایحاد کنید : 
using System;

namespace SampleUniqueIndex
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class UniqueAttribute : Attribute
    {
        public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) {
            Order = order;
        }
        public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC)
        {
            IndexName = indexName;
            Order = order;
        }
        public string IndexName { get; private set; }
        public UniqueIndexOrder Order { get; set; }
    }

    public enum UniqueIndexOrder
    {
        ASC,
        DESC
    }
}
در کد بالا یک Enum برای مرتب سازی ایندکس به دو صورت صعودی و نزولی قرار دارد, همانند کد ابتدای آموزش که مشاهده می‌کنید امکان تعریف این Attribute به سه صورت امکان دارد که به صورت زیر می‌باشد:
1. ایجاد Attribute بدون هیچ پارامتری که در این صورت نام ایندکس با استفاده از نام جدول و آن فیلد ساخته خواهد شد :  IX_Unique_TableName_FieldName و مرتب ساری آن به صورت صعودی می‌باشد.
2.نامی برای ایندکس انتخاب کنید تا با آن نام در دیتابیس ذخبره شود, در این حالت مرتب سازی آن هم به صورت صعودی می‌باشد.
3. در حالت سوم شما ضمن وارد کردن نام ایندکس مرتب سازی آن را نیز وارد می‌کنید.
بعد از کلاس Attribute حالا نوبت به کلاس اصلی میرسد که به صورت زیر می‌باشد:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;

namespace SampleUniqueIndex
{
    public static class DbContextExtention
    {
        private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
                foreach (var uniqueProp in GetUniqueProperties(context, entityType, table))
                {
                    var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ?
                        "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName :
                        uniqueProp.IndexName;

                    if (!currentIndexes.Contains(indexName))
                    {
                        query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); ";
                    }
                    else
                    {
                        currentIndexes.Remove(indexName);
                    }
                }
                foreach (var index in currentIndexes)
                {
                    query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; ";
                }
            }

            if (query.Length > 0)
                context.Database.ExecuteSqlCommand(query);
        }

        private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName)
        {
            var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '"
                      + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'";
            var result = context.Database.SqlQuery<string>(sql).ToList();
            return result;
        }
        private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context)
        {
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context))
            {
                var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true);
                if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0)
                    yield return prop;
            }
        }
        private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo)
        {
            var indexedProperties = new List<UniqueProperty>();
            var properties = entity.GetProperties(PublicInstance);
            var tableName = tableInfo.TableName;
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue;

                UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true);
                NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true);
                if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0)
                {
                    var fieldName = GetFieldName(context, entity, prop.Name);
                    if (fieldName != null)
                    {
                        indexedProperties.Add(new UniqueProperty
                        {
                            TableName = tableName,
                            IndexName = uniqueAttributes[0].IndexName,
                            FieldName = fieldName,
                            Order = uniqueAttributes[0].Order.ToString()
                        });
                    }
                }
            }
            return indexedProperties;
        }
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }

        internal class UniqueProperty
        {
            public string TableName { get; set; }
            public string FieldName { get; set; }
            public string IndexName { get; set; }
            public string Order { get; set; }
        }
        internal class TableInfo
        {
            public string EntityName { get; set; }
            public string TableName { get; set; }
            public string TableSchema { get; set; }
        }
    }
}
در کد بالا با استفاده از Extension Method برای کلاس DbContext یک متد با نام ExecuteUniqueIndexes  ایجاد می‌کنیم تا برای ایجاد ایندکس‌ها در دیتابیس از آن استفاده کنیم.
روند اجرای کلاس بالا به صورت زیر می‌باشد:
در ابتدای متد ()ExecuteUniqueIndexes  :
 public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            ...
        }
با استفاده از متد ()GetTables ما تمام جداول ساخته توسط دیتایس توسط DbContext را گرفنه:
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
با استفاده از این طریق چنانچه کاربر نام دیگری برای هر جدول در نظر بگیرد مشکلی ایجاد نمی‌شود و همینطور Schema جدول نیز گرفته می‌شود, سه مشخصه نام مدل و نام جدول و Schema جدول در کلاس TableInfo قرار داده می‌شود و در انتها تمام جداول در یک Collection قرار داده میشوند و به عنوان خروجی متد استفاده می‌شوند.
بعد از آنکه نام جداول متناظر با نام مدل آنها را در اختیار داریم نوبت به گرفتن تمام DbSet‌ها در DbContext می‌باشد که با استفاده از متد ()GetDbSets :
public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
            ....
        }
در این متد چنانچه Property دارای Attribute NotMapped باشد در لیست خروجی متد قرار داده نمی‌شود. 
سپس داخل چرخه DbSet‌ها نوبت به گرفتن ایندکس‌های موجود با استفاده از متد ()GetCurrentUniqueIndexes برای این مدل می‌باشد تا از ایجاد دوباره آن جلوگیری شود و البته اگر ایندکس هایی را در مدل تعربف نکرده باشید از دیتابیس حذف شوند.
        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            ...
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
            }
        }
بعد از آن نوبت به گرفتن Property‌های دارای Attribute Unique می‌باشد که این کار نیز با استفاده از متد ()GetUniqueProperties انجام خواهد شد.
در متد ()GetUniqueProperties چند شرط بررسی خواهد شد از جمله اینکه Property از نوع Value Type باشد و نه یک کلاس سپس Attribute NotMapped را نداشته باشد و بعد از آن می‌بایست نام متناظر با آن Property را در دیتابیس به دست بیاریم برای این کار از متد ()GetFieldName استفاده می‌کنیم:
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }
برای این کار با استفاده از MetadataWorkspace در DbContext دو لیست SSpace و OSpace استفاده می‌کنیم که در ادامه در مورد این گونه لیست ها بیشتر توضیح می‌دهیم , سپس با استفاده از Member‌های این دو لیست و ایندکس‌های متناظر در این دو لیست نام متناظر با Property را در دیتابیس پیدا خواهیم کرد, البته یک نکته مهم هست چنانچه برای فیلد‌های دیتابیس OrderColumn قرار داده باشید دو لیست Member‌ها از نظر ایندکس متناظر متفاوت خواهند شد پس در نتیجه ایندکس به اشتباه برروی یک فیلد دیگر اعمال خواهد شد.
لیست‌ها در MetadataWorkspace:
1. CSpace : این لیست شامل آبجکت‌های Conceptual از مدل‌های شما می‌باشد تا برای Mapping دیتابیس با مدل‌های شما مانند مبدلی این بین عمل کند.
2. OSpace : این لیست شامل آبجکت‌های مدل‌های شما می‌باشد.
3. SSpace : این لیست نیز شامل آبجکت‌های مربوط به دیتابیس از مدل‌های شما می‌باشد
4. CSSpace : این لیست شامل تنظیمات Mapping بین دو لیست SSpace و CSpace می‌باشد.
5. OCSpace : این لیست شامل تنظیمات Mapping بین دو لیست OSpace و CSpace می‌باشد.
روند Mapping مدل‌های شما از OSpace شروع شده و به SSpace ختم میشود که سه لیست دیگز شامل تنظیماتی برای این کار می‌باشند.
و حالا در متد اصلی ()ExecuteUniqueIndexes ما کوئری مورد نیاز برای ساخت ایندکس‌ها را ساخته ایم.

حال برای استفاده از متد()ExecuteUniqueIndexes می‌بایست در متد Seed آن را صدا بزنیم تا کار ساخت ایندکس‌ها شروع شود، مانند کد زیر:
protected override void Seed(myDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
            context.ExecuteUniqueIndexes();
        }
چند نکته برای ایجاد ایندکس منحصر به فرد وجود دارد که در زیر به آنها اشاره می‌کنیم:
1. فیلد‌های متنی باید حداکثر تا 350 کاراکتر باشند تا ایندکس اعمال شود.
2. همانطور که بالاتر اشاره شد برای فیلد‌های دیتابیس OrderColumn اعمال نکنید که علت آن در بالا توضیح داده شد

دانلود فایل پروژه:
Sample_UniqueIndex.zip
مطالب دوره‌ها
شناسه ها و استفاده از Let
#F هم مانند سایر زبان‌های برنامه نویسی از یک سری Data Type به همراه عملگر و Converter پشتیبانی می‌کند که در ابتدا لازم است یک نگاه کلی به این موارد بیندازیم. به دلیل آشنایی اکثر دوستان به این موارد و به دلیل اینکه تکرار مکررات نشود از توضیح در این موارد خودداری خواهم کرد.(در صورت مبهم بودن می‌توانید از قسمت پرسش و پاسخ استفاد نمایید)
Basic Literal


جدول بالا کاملا واضح است و برنامه نویسان دات نت نظیر #C با انواع داده ای بالا آشنایی دارند. فقط در مورد گزینه آخر unit در فصل‌های بعدی توضیح خواهم داد.

Arithmetic Operators
(عملگر‌های محاسباتی)

Simple String (کار با نوع داده رشته ای)


بعد از بررسی موارد بالا حالا به معرفی شناسه‌ها می‌پردازم. شناسه‌ها در #F راهی هستند برای اینکه شما به مقادیر نام اختصاص دهید. برای اختصاص نام به مقادیر کافیست از کلمه کلیدی let به همراه یک نام  و علامت = و یک عبارت استفاده کنید. چیزی شبیه به تعریف متغیر در سایر زبان‌ها نظیر #C. دلیل اینکه در #F به جای واژه متغیر از شناسه استفاده می‌شود این است که شما می‌توانید به یک شناسه تابعی را نیز اختصاص دهید و مقدار شناسه‌ها دیگر قابل تغییر نیست. در #F کلمه متغیر یک واژه نادرست است چون زمانی که شما یه یک متغیر مقدار اختصاص می‌دهید، مقدار  اون متغیر دیگه قایل تغییر نیست. برای همین اکثر برنامه نویسان #F به جای استفاده از واژه متغیر از واژه مقدار یا شناسه استفاه می‌کنند. برای همین از واژه متغیر برای نام گذاری استفاده نمی‌شود. (البته در #F در بعضی مواقع ما شناسه‌ها رو دوباره تعریف می‌کنیم که چیزی شبیه به استفاده از متغیر هاست ولی با اندکی تفاوت. در این فصل تمرکز ما بر روی شناسه هایی است که مقدارشان تغییر نمی‌کند ولی در فصل برنامه نویسی دستوری به تفصیل در این باره توضیح داده شده است)
let x = 42
در بالا یک شناسه به نام x تعریف شد که مقدار 42 را دریافت کرد. در #F یک شناسه می‌تواند دارای یک مقدار معین باشد یا به یک تابع اشاره کند. این بدین معنی است #F معنی حقیقی برای تابع و پارامتر‌های آن ندارد و همه چیز رو به عنوان مقدار در نظر می‌گیرد.
let myAdd = fun x y -> x + y
کد بالا تعریف یک شناسه به نام myAdd است که به تابعی اشاره می‌کنه که دو پارامتر ورودی دارد و در بدنه آن مقدار پارامتر‌ها با هم جمع می‌شوند.(تعریف توابع به صورت مفصل بحث خواهد شد.) نکته جالب این است که تابع تعریف شده نام ندارد و #F دقیقا با توابع همون رفتاری رو داره که با شناسه‌ها دارد.
let raisePowerTwo x = x ** 2.0
در کد بالا شناسه ای تعریف شده است با نام raisePowerTwo که یک پارامتر ورودی داره به نام x و در بدنه آن (هرچیزی که بعد از = قرار گیرد) مقدار x رو به توان دو می‌کنه.

نام گذاری شناسه ها

برای نام گذاری شناسه‌ها نام انتخابی یا باید با Underscore شروع شود یا با حروف. بعد از آن می‌تونید از اعداد هم استفاده کنید.(نظیر سایر زبان‌های برنامه نویسی)
#F از unicode هم پشتیبانی می‌کنه یعنی می‌تونید متغیری به صورت زیر رو تعریف کنید.
let مسعود = ""
اگر احساس می‌کنید که قوانین نام گذاری در #F کمی محدود کننده است می‌تونید از علامت '' ''  استفاده کنید و در بین این علامت  هر کاراکتری که می‌خواهید رو قرار دهید و #F اونو به عنوان نام شناسه قبول خواهد کرد. برای نمونه
let ``more? `` = true
یا
let ``class`` = "style"
حتی امکان استفاده از کلمات کلیدی هم نظیر class به این روش وجود دارد.

محدوده تعریف شناسه ها
به دلیل اینکه در #F از {} به عنوان شروع و اتمام محدوده استفاده نمی‌شود دونستن و شناختن محدوده توابع بسیار مهم و ضروری است. چون اگر از شناسه ای که در یک محدوده در دسترس نباشد استفاده کنید با خطای کامپایلر متوقف خواهید شد.
همون بحث متغیر‌های محلی و سراسری (در سایر زبان ها) در این جا نیز صادق است یعنی در #F شناسه‌های سراسری و محلی خواهیم داشت. تمام شناسه ها، چه اون هایی که در توابع استفاده می‌شوند و چه اونهایی که به مقادیر اشاره می‌کنند محدودشون از نقطه ای که تعریف می‌شوند تا جایی که اتمام استفاده از اونهاست تعریف شده است. برای مثال اگر یک شناسه رو در بالای فایل تعریف کنید که یک مقدار دارد تا پایان SourceFile قابل استفاده است.( به دلیل نبود مفهوم کلاس از واژه sourceFile استفاده کردم). هم چنین شناسه هایی که در توابع تعریف می‌شوند فقط در همون توابع قابل استفاده هستند.
حالا سوال این است که با نبودن {} چگونه محدوده خود توابع مشخص میشود؟
در #F با استفاده از فضای خای یا space محدوده شناسه‌ها و توابع رو مشخص می‌کنیم.  برای روشن شدن مطلب به مثال زیر دقت کنید.
let test a b =
    let dif = b - a
    let mid = dif / 2
    mid + a

printfn "(test 5 11) = %i" (test 5 11)
printfn "(test 11 5) = %i" (test 11 5)
ابتدا اختلاف بین دو ورودی محاسبه می‌شود و در یک شناسه به نام dif قرار می‌گیرد. برای اینکه مشخص شود که این شناسه خود عضو یک تابع دیگر به نام test است از 4 فضای خالی استفاده شده است. در خط بعدی شناسه mid مقدار شناسه dif رو بر 2 تقسیم می‌کند. در انتها نیز مقدار mid با مقدار a جمع می‌شود و حاصل برگشت داده می‌شود.(انتهای بدنه تابع)
نکته مهم: به جای استفاده از فضای خالی(space) نمی‌تونید از TAB استفاده کنید.

LIGHTWEIGHT SYNTAX یا VERBOSE SYNTAX


در #F دو نوع سبک کد نویسی وجود دارد. یکی lightweight و دیگری Verbose. البته اکثر برنامه نویسان از سبک lightweight که به صورت پیش فرض در #F تعبیه شده است استفاده می‌کنند ولی آشنایی با سبک verbose نیز به عنوان برنامه نویس #F ضروری است. ما نیز به تبعیت از سایرین از سبک lightweight استفاده خواهیم کرد ولی یک فصل به عنوان مطالب تکمیلی اختصاص دادم که تفاوت این دو سبک را در طی چندین مثال بیان میکند.
همان طور که قبلا بیان شد #F بر اساس زبان OCaml پیاده سازی شده است. زبان OCaml مانند #F، یک زبان LIGHTWEIGHT SYNTAX نیست. LIGHTWEIGHT SYNTAX  بدین معنی است محدوده شناسه‌ها بر اساس فضای خالی بین اون‌ها مشخص می‌شود نه با ;. (البته استفاده از ; به صورت اختیاری است)
بازنویسی مثال بالا
let halfWay a b =
let dif = b - a in
let mid = dif / 2 in
mid + a
برای اینکه کامپایلر #F متوجه شود که قصد کدنویسی به سبک lightweight رو نداریم، باید در ابتدای هر فایل از دستور زیر استفاده کنیم.
#light "off"
مطالب
آموزش فایرباگ - #8 - Script Panel
تب Script در FireBug مخصوص دیباگ کردن کدهای JavaScript است. امکاناتی که در این قسمت گنجانده شده بسیار کاربردی بوده و همچنین در بیشتر قسمت‌ها با ابزارهای خطایابی دیگر مشابه است. برای مثال اگر با Visual Studio کار کرده باشید، با نحوه‌ی ایجاد Break Point ، قسمت‌های Watch,Stack و ... آشنا خواهید بود.

این پنل هم مشابه پنل‌های دیگر فایرباگ دارای یک بخش با عنوان Options Menu هست که با راست کلیک کردن بروی عناون یا کلیک بروی مثلث کنار عنوان پنل قابل دسترسی است. تنظیماتی که در اینجا قابل تعیین است عبارتند از:


  • Enabled/Disabled : برای فعال/غیرفعال کردن پنل است. فعال بودن این قسمت ممکن است موجب کاهش سرعت بارگزاری صفحات شود.
  • Track Throw/Catch : در صورت فعال بودن این گزینه، در صورت رویدادن خطا در بلاک try/catch ، دیباگر در آن نقطه از برنامه متوقف شده و کنترل برنامه به شما داده می‌شود. (البته بنده موفق بررسی کامل این قابلیت نشدم. ظاهرا ورژن‌های آخری باگ دارند. مثل غیرفعال شدن کامل همین پنل، "Debugger not activated"! اگر کسی موفقیتی در کار با این مورد داشت، سپاسگذار میشوم اطلاع بدهد.)
  • Show Break Notifications : در صورت فعال بودن این گزینه، هنگام توقف کدی در صفحه، اطلاعاتی در مورد علت توقف آن در بالای پنل ارائه خواهد شد.
توجه داشته باشید که ممکن است در ورژن‌های مختلف، تعداد این گزینه‌ها بیشتر یا کمتر باشد.


 Panel Toolbar

نواری است که ابزارهای پنل بروی آن قرار دارند و به ترتیب عبارنتد از:
  • Break On Next : این دکمه که مشابه آن در اکثر پنل‌ها وجود دارد، هنگام اجرای یک دستور JavaScript آن را متوقف کرده و شما می‌توانید به بررسی آن بپردازید.
  • Script Type Menu : با انتخاب یکی از چهار گزینه موجود می‌توانید نتیجه اسکریپت‌های اضافه شده به صفحه که در قسمت Script Location Menu نمایش داده شده اند را فیلتر کنید. (متاسفانه این گزینه هم مشابه گزینه Track Throw/Catch برای بنده، ورژن 1.11.4 نتیجه ای نداشته است.)
  • Script Location Menu : در این قسمت اسکریپت هایی که در صفحه وجود دارند نمایش داده می‌شوند. مشابه پنل HTML می‌توانید هنگام بازبودن این لیست، با تایپ کردن نتایج را فیلتر کنید.
    همچنین با راست کلیک کردن بروی این قسمت می‌توانید آیتم یا فایل انتخاب شده را در ادیتور مورد نظر باز کنید، در پنل DOM بررسی کنید، فایل را در یک تب جدید باز کنید، آدرس فایل را در حافظه موقت کپی کنید.
  • Execution Control Buttons : این دکمه‌ها زمانی که دیباگر به یک Break Point برسد یا به دلیل فعال بودن دکمه‌های Break On ... متوقف شود، فعال می‌شوند و با کمک آن‌ها می‌توانید عملیات خطایابی را ادامه دهید.
    در جدول زیر این دکمه‌ها و توضیحات تکمیلی هریک را مشاهده می‌کنید:

     نوعدکمه
    Shortcut
    توضیحات
    اجرای مجدد

    Shift + F18
    Stack فعلی را مجدد اجرا می‌کند.
    (Stack: لیستی از توابع به ترتیبی که با فراخوانی آنها تابع جاری فراخونی شده است. در مقاله بعدی توضیحات بیشتری ارائه خواهد شد.)
    ادامه

    F8
    اجرای برنامه را (تا Break Point بعدی) ادامه می‌دهد.
    وارد شدن

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

    F10
    اجرای برنامه را در سطح (Scope) فعلی ادامه می‌دهد.
    مشابه قبلی وارد تابع نمی‌شود.
    خارج شدن

    Shift + F11
    کنترل به تابعی که تابع جاری را فراخوانی کرده است بازمیگردد.
    روشی سودمند زمانی که هنگام خطایابی وارد کدهای jQuery یا مثل آن می‌شوید :)
بعد از Script Location Menu قسمتی وجود دارد که وضعیت فعلی Stack را نشان می‌دهد و با کلیک بروی هرکدام، به کد آن قسمت هدایت می‌شوید. ( البته اگر فایرباگ شما در این قسمت باگ نداشته باشد! )

Context Menu
تنها قابلیت جدیدی که در این منو وجود دارد، Run To This Line است. هنگامی که پنل در حالت دیباگ است، با راست کلیک کردن بروی خط مورد نظر و انتخاب گزینه‌ی Run to This Line می‌توانید خطوط میانی را رد کرده و به خط مورد نظر بروید. البته خطوطی که رد می‌شوند ، اجرا می‌شوند.
این کار معادل این است که درخط مورد نظر یک Break Point اضافه کنید و دکمه‌ی F8 را بزنید.



 Breakpoints
توسط Break Point‌ها می‌توانید خطوطی از برنامه را برای خطایابی مشخص کنید. زمانی که دیباگر به نقطه‌ی مورد نظر برسد متوقف شده و می‌توانید به بررسی برنامه بپردازید. می‌توانید با بردن موس بروی متغییرها مقدار آن هارا بررسی کنید یا با کمک قسمت‌های Watch و Stack در پنل جانبی اطلاعات بیشتری در مورد اجرای برنامه در نقطه‌ی جاری بدست آورید.(در مورد پنل‌های جانبی در قسمت بعدی توضیح خواهم داد.) همچنین با کمک دکمه هایی که در جدول فوق توضیح داده شده اند روند اجرای برنامه را خط به خط ادامه دهید.
برای ایجاد یک Break Point، بروی شماره‌ی خط مورد نظر کلیک کنید. نقطه ای قرمز رنگ نمایش داده خواهد شد. البته دقت کنید که همه‌ی خطوط برنامه اجرا نمی‌شوند و نمی‌توانید در آن خطوط از این امکان بهره ببرید. شماره‌ی خطوط فعال با رنگ سبز مشخص شده اند.

امکان مفید دیگری که همراه با این قابلیت ارائه شده است (که در محیط‌های دیگر هم وجود دارد)، عبارت شرطی است. به این صورت که ضمن قرار دادن Break Point در یک نقطه از برنامه، می‌توانید یک شرط هم برای توقف برنامه تعیین کنید.
فرض کنید یک حلقه دارید که 300 بار تکرار می‌شود و مثلا در اجرای 250ام آن مشکلی وجود دارد. در این حالت می‌توانید از این قابلیت استفاده کنید و شرط توقف را i == 250 قرار بدهید.
برای تعیین یک شرط برای یک Break Point، بروی خط مورد نظر راست کلیک کنید و شرط را در قسمت مشخص شده وارد کنید.


امکان مفید دیگری که وجود دارد، Break Point خودکار است. اگر از مقالات قبلی دکمه‌ی Break On All Errors در پنل Console و Break On Mutate در پنل HTML را بخاطر داشته باشید می‌دانید که در هر یک هنگام اجرای یک رخداد مورد نظر، دیباگر در خطی که موجب آن رخداد شده است متوقف شده و می‌توانید کنترل برنامه را بدست بگیرید. در این حالت نیازی به ایجاد Break Point نیست و FireBug بصورت خودکار این کار را انجام می‌دهد.


 Search
این بخش در همه پنل‌ها وجود دارد با این تفاوت که در پنل Script و CSS دو گزینه‌ی Multiple Files و Use Regular Expression وجود دارد که به ترتیب امکان جستجو در فایل‌های js و css اضافه شده به صفحه هستند. قابلیت دیگری هم که فقط در پنل Script وجود دارد، پرش به یک خط در برنامه است به این صورت که با وارد کردن # و یک عدد به عنوان شماره‌ی خط، همان خط نمایش داده می‌شود.


در قسمت بعدی پنل جانبی که شامل سه بخش Watch، Stack و Breakpoints است را بررسی خواهیم کرد.
بازخوردهای پروژه‌ها
مشکل در ایجاد CustomHeader در نسخه جدید
من با توجه به نسخه 1.4 برای ایجاد CustomHeader کد زیر را نوشته بودم
 public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
    {
        var httpCookie = HttpContext.Current.Request.Cookies["FinancialMarine"];
          if (httpCookie != null)
          {
              int requestId = int.Parse(httpCookie.Value);
              var ctx = new clearanceEntities();

              var item = (from d in ctx.CLEARANCE_ITEMS
                          where d.REQUEST_ID == requestId
                          select d).FirstOrDefault();
              if (item != null)
              {
                 
                 
                


                  string imagepath = HttpRuntime.AppDomainAppPath + "tir.JPG";
                  string imagepath2 = HttpRuntime.AppDomainAppPath + "tir2.JPG";

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

                  PdfPTable table = new PdfPTable(numColumns: 3);

                  table.WidthPercentage = 100;
                  table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  table.ExtendLastRow = false;

                  Image img = Image.GetInstance(imagepath);
                  Image img2 = Image.GetInstance(imagepath2);

                  PdfPCell c = new PdfPCell(img);
                  c.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  c.Border = 0;


                  table.AddCell(c);

                  c = new PdfPCell(img2);
                  c.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  c.Border = 0;
                  c.Padding = 5;
                  table.AddCell(c);

                  ////////////////////////////////////////////////////////////////////////
                  PdfPTable table2 = new PdfPTable(numColumns: 2);
                  table2.WidthPercentage = 100;

                  table2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  table2.ExtendLastRow = false;

                  PdfPCell cell2 = new PdfPCell(new Phrase("تاریخ:", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  cell2 = new PdfPCell(new Phrase(" ", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  cell2 = new PdfPCell(new Phrase("شماره: ", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  cell2 = new PdfPCell(new Phrase("TST/F/91-4641 ", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  cell2 = new PdfPCell(new Phrase("پیوست: ", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  cell2 = new PdfPCell(new Phrase("مدارک ضمیمه ", tahomaFont));
                  cell2.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell2.Border = 0;
                  table2.AddCell(cell2);

                  var cell5 = new PdfPCell(table2);
                  cell5.Colspan = 3;
                  cell5.Border = 0;
                  table.AddCell(cell5);
                  ////////////////////////////////////////////////////////////////////////////////////
                  PdfPCell cell = new PdfPCell(new Phrase("شرکت", tahomaFont));
                  cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell.Padding = 10;
                  cell.Border = 0;
                  cell.Colspan = 3;
                  table.AddCell(cell);
                  cell = new PdfPCell(new Phrase("اداره محترم بازرگانی / تدارکات کالا", tahomaFont));
                  cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell.Padding = 10;
                  cell.Border = 0;
                  cell.Colspan = 3;
                  table.AddCell(cell);

                  cell = new PdfPCell(new Phrase("با سلام", tahomaFont));
                  cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell.Padding = 10;
                  cell.Border = 0;
                  cell.Colspan = 3;
                  table.AddCell(cell);

                  cell = new PdfPCell(new Phrase("با احترام به شرح زیر یک فقره صورتحساب مربوط به ترخیص محمولات آن شرکت جهت اطلاع و صدور دستور مقتضی بحضورتان ایفاد می‌گردد.", tahomaFont));
                  cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell.Padding = 10;
                  cell.Border = 0;
                  cell.Colspan = 3;
                  table.AddCell(cell);


                  ////////////////////////////////////////////////////////////////
                  
                  PdfPTable table3 = new PdfPTable(numColumns: 6);
                  table3.WidthPercentage = 100;

                  table3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  table3.ExtendLastRow = false;

                  PdfPCell cell3 = new PdfPCell(new Phrase("کشتی/کامیون", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(item.CLEARANCE_REQUEST.SHIP_NAME + " " + item.CLEARANCE_REQUEST.TRAVEL_NO, tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(" تعداد و نوع بسته بندی", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(item.QUANTITY.ToString(CultureInfo.InvariantCulture) + " " + item.PACKING_TYPES.PACKING_NAME, tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(" شماره پروانه", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(item.CLEARANCE_REQUEST.PERMIT_NO, tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase("شماره بارنامه ", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);


                  cell3 = new PdfPCell(new Phrase(item.CLEARANCE_REQUEST.WAYBILL_NO, tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(" وزن(کیلوگرم)", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(item.TARE_WEIGHT.ToString(CultureInfo.InvariantCulture), tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(" شماره درخواست", tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell3 = new PdfPCell(new Phrase(item.CLEARANCE_REQUEST.REQUEST_NO, tahomaFont));
                  cell3.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
                  cell3.Border = 0;
                  table3.AddCell(cell3);

                  cell = new PdfPCell(table3);
                  cell.Colspan = 3;
                  cell.Border = 0;

                  table.AddCell(cell);
                  return table;
              }
          }
        return null;
        
    }
ولی حال که از نسخه 1.5 استفاده می‌کنم دیگه نمی‌تونم مقدار Return تابع RenderingReportHeader را برابر tableدر کد بالا قرار دهم.

ممنون





مطالب
API Versioning
فرض کنید امروز یک API را برای استفاده عموم ارائه میدهید. آیا با یک breaking change در منابع شما که باعث تغییر در داده‌های ورودی یا خروجی API شود، باید استفاده کنندگان این API در سیستمی که از آن استفاده کرده‌اند، تغییراتی را اعمال کنند یا خیر؟ جواب خیر می‌باشد؛ اصلی‌ترین استفاده از API Versioning دقیقا برای این منظور است که بدون نگرانی از توسعه‌های بعدی، از ورژن‌های قدیمی API بتوانیم استفاده کنیم. 
در این مقاله با روش‌های مختلف ورژن بندی API آشنا خواهید شد.
سه روش اصلی زیر را میتوان برای این منظور در نظر گرفت:
  1.  URI-based versioning 
  2.  Header-based versioning 
  3.  Media type-based versioning 

پیاده سازی URI-based versioning
حداقل به 3 طریق میتوان این مکانیسم را پیاده کرد:
راه حل اول: اگر از Attribute Routing استفاده نمی‌کنید، با تغییر جزئی در تعاریف مسیریابی خود میتوانید به نتیجه مورد نظر برسید. برای ادامه کار دو ویوومدل و دو کنترلر را که هر کدام مربوط به ورژن خاصی از API ما هستند، پیاده سازی میکنیم:
public class ItemViewModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
}
public class ItemV2ViewModel : ItemViewModel
{
    public double Price { get; set; }
}
ItemViewModel مربوط به ورژن 1 میباشد.
 public class ItemsController : ApiController   
 {
        [ResponseType(typeof(ItemViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
 public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
ItemsController مربوط به ورژن 1 میباشد.
اگر قرار باشد از مسیرهای متمرکز استفاده کنیم، کافی است که تغییرات زیر را اعمال کنیم:
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new
{
    controller = "ItemsV2",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute("Items", "api/items/{id}", new
{
    controller = "Items",
    id = RouteParameter.Optional
});
config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);
در کد بالا به صراحت برای تک تک کنترلرها مسیریابی متناسب با ورژنی که آن را پشتیبانی میکند، معرفی کرده‌ایم.
نکته: این تنظیمات خاص باید قبل از تنظیمات پیش فرض قرار گیرند.

روش بالا به خوبی کار خواهد کرد ولی آنچنان مطلوب نمیباشد. به همین دلیل یک مسیریابی عمومی و کلی را در نظر میگیریم که شامل ورژن مورد نظر، در قالب یک پارامتر میباشد. در روش جایگزین باید به شکل زیر عمل کنید:
config.Routes.MapHttpRoute(
"defaultVersioned",
"api/v{version}/{controller}/{id}",
new { id = RouteParameter.Optional }, new { version = @"\d+" });

config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new { id = RouteParameter.Optional }
);

با این تنظیمات فعلا به مسیریابی ورژن بندی شده‌ای دست نیافته‌ایم. زیرا فعلا به هیچ طریق به Web API اشاره نکرده‌ایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.

IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده می‌باشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:

 به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.

public class VersionFinder
    {
        private static bool NeedsUriVersioning(HttpRequestMessage request, out string version)
        {
            var routeData = request.GetRouteData();
            if (routeData != null)
            {
                object versionFromRoute;
                if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute))
                {
                    version = versionFromRoute as string;
                    if (!string.IsNullOrWhiteSpace(version))
                    {
                        return true;
                    }
                }
            }
            version = null;
            return false;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
        public static int GetVersionFromRequest(HttpRequestMessage request)
        {
            string version;

            return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0;
        }
    }

کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری  مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت می‌گیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار می‌گیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.

 public class VersionAwareControllerSelector : DefaultHttpControllerSelector
    {
        public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { }
        public override string GetControllerName(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);
            var version = VersionFinder.GetVersionFromRequest(request);
                
        return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName;
        }
        private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName,
        int version)
        {
            var versionControllerName = $"{baseControllerName}v{version}";
            HttpControllerDescriptor descriptor;
            if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor))
            {
                return versionControllerName;
            }

            throw new HttpResponseException(request.CreateErrorResponse(
            HttpStatusCode.NotFound,
                $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}"));
        }
    }

متد  GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از  IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند. 

config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));

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

راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالب‌های مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.

[RoutePrefix("api/v1/Items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country =        "Japan" };
            return Ok(viewModel);
        }
    }


 [RoutePrefix("api/V2/Items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [Route("{id:int}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

اگر توجه کرده باشید در مثال ما، نام‌های کنترلر‌ها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نام‌های مختلف برای یک کنترلر در ورژن‌های مختلف، آن را با یک نام یکسان درون namespace‌های مختلف احاطه کنیم یا حتی آنها را درون Class Library‌های جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید. 

راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر می‌باشد:

کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:

public class VersionConstraint : IHttpRouteConstraint
{
    public VersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }
    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
    IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

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

متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کرده‌ایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.

 به روش استفاده از این Constraint توجه کنید:

namespace UriBasedVersioning.Namespace.Controllers.V1
{
    using Models.V1;

RoutePrefix("api/{version:VersionConstraint(v1)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
}
namespace UriBasedVersioning.Namespace.Controllers.V2
{
    using Models.V2;


RoutePrefix("api/{version:VersionConstraint(v2)}/items")]
public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [Route("{id}")]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }
}

با توجه به کد بالا می‌توان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Route‌ها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از API‌های بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:

var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));
config.MapHttpAttributeRoutes(constraintsResolver);

مرحله بعدی کار، پیاده سازی IHttpControllerSelector می‌باشد:

  public class NamespaceControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
        public NamespaceControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string,
                HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var version = GetVersion(routeData);
            if (version == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                version, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string,
                HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName =
                    controllerType.Name.Remove(controllerType.Name.Length -
                                               DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}",
                    segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                        controllerType.Name,
                        controllerType);
                }
            }
            return dictionary;
        }
        private static T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
        private static string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            var dataTokenValue = subroute?.Route.DataTokens.First().Value;
            var controllerName =
                ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }
        private static string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version");
        }
    }

سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکته‌ی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing می‌باشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامتر‌های controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلر‌های موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.

کارکرد متد InitializeControllerDictionary :

همانطور که میدانید به صورت پیش‌فرض Web API توجهی به فضای نام مرتبط با کنترلر‌ها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:

public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()

لذا در کلاس پیاده سازی شده، خصوصیتی به نام ‎‎_controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا می‌باشد، تعریف کرده‌ایم. متد InitializeControllerDictionary کار آماده سازی داده‌های مورد نیاز خصوصیت ‎‎_controllers می‌باشد. به این صورت که تمام کنترلر‌های موجود در برنامه را واکشی کرده و این بار کلید‌های مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کرده‌ایم.

حال تنظیمات زیر را اعمال کنید:

 public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var constraintsResolver = new DefaultInlineConstraintResolver();
            constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof
            (VersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config));

        }
    }

این بار برنامه را اجرا کرده و API‌های مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.

نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم  فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.

پیاده سازی Header-based versioning  

در این روش کافی است هدری را برای دریافت ورژن API درخواستی مشخص کرده باشید. برای مثال هدری به نام api-version، که استفاده کننده از این طریق، ورژن درخواستی خود را اعلام میکند. همانطور که قبلا اشاره کردیم، با استفاده از Constraint‌ها دست شما خیلی باز خواهد بود. برای مثال این بار به جای واکشی پارامتر version از RouteData، کد همان قسمت را برای واکشی آن از هدر درخواستی تغییر خواهیم داد:
public class HeaderVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionHeaderName = "api-version";

        public HeaderVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
            IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Contains(VersionHeaderName)) return false;

            var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault();

            return VersionToInt(version) == AllowedVersion;
        }
        private static int VersionToInt(string versionString)
        {
            int version;
            if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version))
                return 0;
            return version;
        }
    }
این بار در متد Match، به دنبال هدر خاصی به نام api-version می‌گردیم و مقدار آن را با AllowdVersion مقایسه میکنیم تا مشخص کنیم که آیا این Route نوشته شده برای ادامه کار واکشی کنترلر مورد نظر استفاده شود یا خیر. در ادامه یک RouteFactoryAttribute شخصی سازی شده را به منظور معرفی این محدودیت تعریف شده، در نظر میگیریم:
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute
    {
        public HeaderVersionedRouteAttribute(string template) : base(template)
        {
            Order = -1;
        }

        public int Version { get; set; }

        public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary
        {
            {"", new HeaderVersionConstraint(Version)}
        };
    }

با استفاده از خصوصیت Constraints، توانستیم محدودیت پیاده سازی شده خود را به سیستم مسیریابی معرفی کنیم. توجه داشته باشید که این بار هیچ پارامتری در Uri تحت عنوان version را نداریم و از این جهت به صراحت کلیدی برای آن مشخص نکرده ایم (‎‎‎‎‏‏‎‎{"", new HeaderVersionConstraint(Version)} ‎‎ ).
کنترلر‌های ما به شکل زیر خواهند بود:
    [RoutePrefix("api/items")]
    public class ItemsController : ApiController
    {
        [ResponseType(typeof(ItemViewModel))]
        [HeaderVersionedRoute("{id}", Version = 1)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" };
            return Ok(viewModel);
        }
    }
  [RoutePrefix("api/items")]
    public class ItemsV2Controller : ApiController
    {
        [ResponseType(typeof(ItemV2ViewModel))]
        [HeaderVersionedRoute("{id}", Version = 2)]
        public IHttpActionResult Get(int id)
        {
            var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 };
            return Ok(viewModel);
        }
    }

این بار از VersionedRoute برای اعمال مسیر یابی Attribute-based استفاده کرده ایم و به صراحت ورژن هر کدام را با استفاده از خصوصیت Version آن مشخص کرده‌ایم. نتیجه:


پیاده سازی Media type-based versioning
قرار است ورژن API مورد نظر را که استفاده کننده درخواست میدهد، از دل درخواست‌هایی به فرم زیر واکشی کنیم:
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v1+json
GET /api/Items HTTP 1.1
Accept: application/vnd.mediatype.versioning-v2+json
در این روش دست ما کمی بازتر است چرا که میتوانیم بر اساس فرمت درخواستی نیز تصمیماتی را برای ارائه ورژن خاصی از API  داشته باشیم. روال کار شبیه به روش قبلی با پیاده سازی یک Constraint و یک RouteFactoryAttribute انجام خواهد گرفت.
 public class MediaTypeVersionConstraint : IHttpRouteConstraint
    {
        private const string VersionMediaType = "vnd.mediatype.versioning";

        public MediaTypeVersionConstraint(int allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public int AllowedVersion { get; }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values,
            HttpRouteDirection routeDirection)
        {
            if (!request.Headers.Accept.Any()) return false;

            var acceptHeaderVersion =
                request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType));

            //Accept: application/vnd.mediatype.versioning-v1+json
            if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") ||
                !acceptHeaderVersion.MediaType.Contains("+"))
                return false;

            var version = acceptHeaderVersion.MediaType.Between("-v", "+");
            return VersionToInt(version)==AllowedVersion;
        }
}
در متد Match کلاس بالا به دنبال مدیا تایپ مشخصی هستیم که با هدر Accept برای ما ارسال میشود. برای آشنایی با فرمت ارسال Media Type‌ها میتوانید به اینجا مراجعه کنید. کاری که در اینجا انجام شده‌است، یافتن ورژن ارسالی توسط استفاده کننده می‌باشد که آن را با فرمت خاصی که نشان داده شد و مابین (v-) و (+) قرار داده، ارسال میشود. ادامه کار به مانند روش Header-based میباشد و از مطرح کردن آن در مقاله خودداری میکنیم.
نتیجه به شکل زیر خواهد بود:


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


مطالب
درخت‌ها و گراف‌ها قسمت سوم
همانطور که در قسمت قبلی گفتیم، در این قسمت قرار است به پیاده سازی درخت جست و جوی دو دویی مرتب شده بپردازیم. در مطلب قبلی اشاره کردیم که ما متدهای افزودن، جستجو و حذف را قرار است به درخت اضافه کنیم و برای هر یک از این متدها توضیحاتی را ارائه خواهیم کرد. به این نکته دقت داشته باشید درختی که قصد پیاده سازی آن را داریم یک درخت متوازن نیست و ممکن است در بعضی شرایط کارآیی مطلوبی نداشته باشد.
همانند مثال‌ها و پیاده سازی‌های قبلی، دو کلاس داریم که یکی برای ساختار گره است <BinaryTreeNode<T و دیگری برای ساختار درخت اصلی <BinaryTree<T.
کلاس BinaryTreeNode که در پایین نوشته شده‌است بعدا داخل کلاس BinaryTree قرار خواهد گرفت:
internal class BinaryTreeNode<T> :
    IComparable<BinaryTreeNode<T>> where T : IComparable<T>
{
    // مقدار گره
    internal T value;
 
    // شامل گره پدر
    internal BinaryTreeNode<T> parent;
 
    // شامل گره سمت چپ
    internal BinaryTreeNode<T> leftChild;
 
    // شامل گره سمت راست
    internal BinaryTreeNode<T> rightChild;
 
    /// <summary>سازنده</summary>
    /// <param name="value">مقدار گره ریشه</param>
    public BinaryTreeNode(T value)
    {
        if (value == null)
        {
            // از آن جا که نال قابل مقایسه نیست اجازه افزودن را از آن سلب می‌کنیم
            throw new ArgumentNullException(
                "Cannot insert null value!");
        }
 
        this.value = value;
        this.parent = null;
        this.leftChild = null;
        this.rightChild = null;
    }
 
    public override string ToString()
    {
        return this.value.ToString();
    }
 
    public override int GetHashCode()
    {
        return this.value.GetHashCode();
    }
 
    public override bool Equals(object obj)
    {
        BinaryTreeNode<T> other = (BinaryTreeNode<T>)obj;
        return this.CompareTo(other) == 0;
    }
 
    public int CompareTo(BinaryTreeNode<T> other)
    {
        return this.value.CompareTo(other.value);
    }
}
تکلیف کدهای اولیه که کامنت دارند روشن است و قبلا چندین بار بررسی کردیم ولی کدها و متدهای جدیدتری نیز نوشته شده‌اند که آن‌ها را بررسی می‌کنیم:
ما در مورد این درخت می‌گوییم که همه چیز آن مرتب شده است و گره‌ها به ترتیب چیده شده اند و اینکار تنها با مقایسه کردن گره‌های درخت امکان پذیر است. این مقایسه برای برنامه نویسان از طریق یک ذخیره در یک ساختمان داده خاص یا اینکه آن را به یک نوع Type قابل مقایسه ارسال کنند امکان پذیر است. در سی شارپ نوع قابل مقایسه با کلمه‌های کلیدی زیر امکان پذیر است:
T : IComparable<T>
در اینجا T می‌تواند هر نوع داده‌ای مانند Byte و int و ... باشد؛ ولی علامت : این محدودیت را اعمال می‌کند که کلاس باید از اینترفیس IComparable ارث بری کرده باشد. این اینترفیس برای پیاده‌سازی تنها شامل تعریف یک متد است به نام (CompareTo(T obj که عمل مقایسه داخل آن انجام می‌گردد و در صورت بزرگ بودن شیء جاری از آرگومان داده شده، نتیجه‌ی برگردانده شده، مقداری مثبت، در حالت برابر بودن، مقدار 0 و کوچکتر بودن مقدارمنفی خواهد بود. شکل تعریف این اینترفیس تقریبا چنین چیزی باید باشد:
public interface IComparable<T>
{
    int CompareTo(T other);
}
نوشتن عبارت بالا در جلوی کلاس، به ما این اطمینان را می‌بخشد که که نوع یا کلاسی که به آن پاس می‌شود، یک نوع قابل مقایسه است و از طرف دیگر چون می‌خواهیم گره‌هایمان نوعی قابل مقایسه باشند <IComparable<T را هم برای آن ارث بری می‌کنیم.
همچنین چند متد دیگر را نیز override کرده‌ایم که اصلی‌ترین آن‌ها GetHashCode و Equal است. موقعی که متد CompareTo مقدار 0 بر می‌گرداند مقدار برگشتی Equals هم باید True باشد.
... و یک نکته مفید برای خاطرسپاری اینکه موقعیکه دو شیء با یکدیگر برابر باشند، کد هش تولید شده آن‌ها نیز با هم برابر هستند. به عبارتی اشیاء یکسان کد هش یکسانی دارند. این رفتار سبب می‌شود که که بتوانید مشکلات زیادی را که در رابطه با مقایسه کردن پیش می‌آید، حل نمایید. 

پیاده سازی کلاس اصلی BinarySearchTree
مهمترین نکته در کلاس زیر این مورد است که ما اصرار داشتیم، T باید از اینترفیس IComparable مشتق شده باشد. بر این حسب ما می‌توانیم با نوع داده‌هایی چون int یا string کار کنیم، چون قابل مقایسه هستند ولی نمی‌توانیم با  []int یا streamreader کار کنیم چرا که قابل مقایسه نیستند.
public class BinarySearchTree<T>    where T : IComparable<T>
{
    /// کلاسی که بالا تعریف کردیم
    internal class BinaryTreeNode<T> :
        IComparable<BinaryTreeNode<T>> where T : IComparable<T>
    {
        // …
    }
 
    /// <summary>
    /// ریشه درخت
    /// </summary>
    private BinaryTreeNode<T> root;
 
    /// <summary>
    /// سازنده کلاس
    /// </summary>
    public BinarySearchTree()
    {
        this.root = null;
    }
 
//پیاده سازی متدها مربوط به افزودن و حذف و جست و جو
}
در کد بالا ما کلاس اطلاعات گره را به کلاس اضافه می‌کنیم و یه سازنده و یک سری خصوصیت رابه آن اضافه کرده ایم.در این مرحله گام به گام هر یک از سه متد افزودن ، جست و جو و حذف را بررسی می‌کنیم و جزئیات آن را توضیح می‌دهیم.

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

پیاده سازی الگوریتم بالا در کلاس اصلی:
public void Insert(T value)
{
    this.root = Insert(value, null, root);
}
 
/// <summary>
/// متدی برای افزودن عنصر به درخت
/// </summary>
/// <param name="value">مقدار جدید</param>
/// <param name="parentNode">والد گره جدید</param>
/// <param name="node">گره فعلی که همان ریشه است</param>
/// <returns>گره افزوده شده</returns>
private BinaryTreeNode<T> Insert(T value,
        BinaryTreeNode<T> parentNode, BinaryTreeNode<T> node)
{
    if (node == null)
    {
        node = new BinaryTreeNode<T>(value);
        node.parent = parentNode;
    }
    else
    {
        int compareTo = value.CompareTo(node.value);
        if (compareTo < 0)
        {
            node.leftChild =
                Insert(value, node, node.leftChild);
        }
        else if (compareTo > 0)
        {
            node.rightChild =
                Insert(value, node, node.rightChild);
        }
    }
 
    return node;
}
متد درج سه آرگومان دارد، یکی مقدار گره جدید است؛ دوم گره والد که با هر بار صدا زدن تابع بازگشتی، گره والد تغییر خواهد کرد و به گره‌های پایین‌تر خواهد رسید و سوم گره فعلی که با هر بار پاس شدن به تابع بازگشتی، گره ریشه‌ی آن زیر درخت است.
در مقاله قبلی اگر به یاد داشته باشید گفتیم که جستجو چگونه انجام می‌شود و برای نمونه به دنبال یک عنصر هم گشتیم و جستجوی یک عنصر در این درخت بسیار آسان است. ما این کد را بدون تابع بازگشتی و تنها با یک حلقه while پیاده خواهیم کرد. هر چند مشکلی با پیاده سازی آن به صورت بازگشتی وجود ندارد.
الگوریتم از ریشه بدین صورت آغاز می‌گردد و به ترتیب انجام می‌شود:
  • اگر عنصر جدید برابر با گره فعلی باشد، همان گره را بازگشت بده.
  • اگر عنصر جدید کوچکتر از گره فعلی است، گره سمت چپ را بردار و عملیات را از ابتدا آغاز کن (در کد زیر به ابتدای حلقه برو).
  • اگر عنصر جدید بزرگتر از گره فعلی است، گره سمت راست را بردار و عملیات را از ابتدا آغاز  کن.
در انتها اگر الگوریتم، گره را پیدا کند، گره پیدا شده را باز می‌گرداند؛ ولی اگر گره را پیدا نکند، یا درخت خالی باشد، مقدار برگشتی نال خواهد بود.

حذف یک عنصر
حذف کردن در این درخت نسبت به درخت دودودیی معمولی پیچیده‌تر است. اولین گام این عمل، جستجوی گره مدنظر است. وقتی گره‌ایی را مدنظر داشته باشیم، سه بررسی زیر انجام می‌گیرد:
  • اگر گره برگ هست و والد هیچ گره‌ای نیست، به راحتی گره مد نظر را حذف می‌کنیم و ارتباط گره والد با این گره را نال می‌کنیم.
  • اگر گره تنها یک فرزند دارد (هیچ فرقی نمی‌کند چپ یا راست) گره مدنظر حذف و فرزندش را جایگزینش می‌کنیم.
  • اگر گره دو فرزند دارد، کوچکترین گره در زیر درخت سمت راست را پیدا کرده و با گره مدنظر جابجا می‌کنیم. سپس یکی از دو عملیات بالا را روی گره انجام می‌دهیم.
اجازه دهید عملیات بالا را به طور عملی بررسی کنیم. در درخت زیر ما می‌خواهیم گره 11 را حذف کنیم. پس کوچکترین گره سمت راست، یعنی 13 را پیدا می‌کنیم و با گره 11 جابجا می‌کنیم.

بعد از جابجایی، یکی از دو عملیات اول بالا را روی گره 11 اعمال می‌کنیم و در این حالت گره 11 که یک گره برگ است، خیلی راحت حذف و ارتباطش را با والد، با یک نال جایگزین می‌کنیم.

/// عنصر مورد نظر را جست و جوی می‌کند و اگر مخالف نال بود گره برگشتی را به تابع حذف ارسال می‌کند
public void Remove(T value)
{
    BinaryTreeNode<T> nodeToDelete = Find(value);
    if (nodeToDelete != null)
    {
        Remove(nodeToDelete);
    }
}
 
private void Remove(BinaryTreeNode<T> node)
{
    //بررسی می‌کند که آیا دو فرزند دارد یا خیر
    // این خط باید اول همه باشد که مرحله یک و دو بعد از آن اجرا شود
    if (node.leftChild != null && node.rightChild != null)
    {
        BinaryTreeNode<T> replacement = node.rightChild;
        while (replacement.leftChild != null)
        {
            replacement = replacement.leftChild;
        }
        node.value = replacement.value;
        node = replacement;
    }
 
    // مرحله یک و دو اینجا بررسی میشه
    BinaryTreeNode<T> theChild = node.leftChild != null ?
            node.leftChild : node.rightChild;
 
    // اگر حداقل یک فرزند داشته باشد
    if (theChild != null)
    {
        theChild.parent = node.parent;
 
        // بررسی می‌کند گره ریشه است یا خیر
        if (node.parent == null)
        {
            root = theChild;
        }
        else
        {
            // جایگزینی عنصر با زیر درخت فرزندش
            if (node.parent.leftChild == node)
            {
                node.parent.leftChild = theChild;
            }
            else
            {
                node.parent.rightChild = theChild;
            }
        }
    }
    else
    {
        // کنترل وضعیت موقعی که عنصر ریشه است
        if (node.parent == null)
        {
            root = null;
        }
        else
        {
            // اگر گره برگ است آن را حذف کن
            if (node.parent.leftChild == node)
            {
                node.parent.leftChild = null;
            }
            else
            {
                node.parent.rightChild = null;
            }
        }
    }
}

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


دو فرزندی

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


تک فرزندی

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

اگر نتیجه نال نباشد باید این گره حذف و گره فرزند ارتباطش را با والد گره حذفی برقرار کند. در صورتیکه گره حذفی ریشه باشد و والدی نداشته باشد، این نکته باید رعایت شود که گره فرزند بری متغیر root که در سطح کلاس تعریف شده است، نیز قابل شناسایی باشد.

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


بدون فرزند (برگ)

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


پیمایش درخت به روش DFS یا LVR یا In-Order

public void PrintTreeDFS()
{
    PrintTreeDFS(this.root);
    Console.WriteLine();
}
 

private void PrintTreeDFS(BinaryTreeNode<T> node)
{
    if (node != null)
    {
        PrintTreeDFS(node.leftChild);
        Console.Write(node.value + " ");
        PrintTreeDFS(node.rightChild);
    }
}


در مقاله بعدی درخت دودویی متوازن را که پیچیده‌تر از این درخت است و از کارآیی بهتری برخوردار هست، بررسی می‌کنیم.

نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
return File در ASP.NET Core، نیاز به مسیرنسبی فایل‌ها را از wwwroot دارد و نه مسیر مطلق و کامل آن‌ها را.
        private IFileProvider GetFileProvider(VirtualFileResult result)
        {
            if (result.FileProvider != null)
            {
                return result.FileProvider;
            }

            result.FileProvider = _hostingEnvironment.WebRootFileProvider;

            return result.FileProvider;
}
در اینجا تمام مسیرها نسبت به hostingEnvironment.WebRootFileProvider_ سنجیده می‌شوند. یعنی:
return File("~/foo.js","text/javascript")
به معنای دسترسی به فایل wwwroot/foo.js خواهد بود.
اشتراک‌ها
15 تکنیک سودمند CSS
  you have been a frontend web developer for a while, there is a high chance that you have had a moment when you were trying to find out how to code something and realised after a bit of googling, that "there is CSS for that". If you hadn’t, well you are about to. 
15 تکنیک سودمند CSS
نظرات مطالب
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
با سلام

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

 Private Function ValidateNcode(ByVal NC As Int64) As Boolean
        If IsNumeric(NC) = False Or NC.ToString.Length < 10 Then
            Return False
        End If
        Dim vnc As String = NC.ToString
        Dim pos As Integer = 10
        Dim sum As Integer = 0

        For i = 0 To 8
            sum += Integer.Parse(vnc.ToCharArray()(i).ToString()) * pos
            pos = pos - 1
        Next i

        Dim remind As Integer = sum Mod 11
        Dim controlNum As Integer = Integer.Parse(vnc.ToCharArray()(9))

        If remind <= 2 Then
            If controlNum = remind Then
                Return True
            Else
                Return False
            End If
        Else
            If (11 - remind) = controlNum Then
                Return True
            Else
                Return False
            End If
        End If
    End Function
private bool ValidateNcode(Int64 NC)
{
if (IsNumeric(NC) == false | NC.ToString.Length < 10) {
return false;
}
string vnc = NC.ToString;
int pos = 10;
int sum = 0;

for (i = 0; i <= 8; i++) {
sum += int.Parse(vnc.ToCharArray()(i).ToString()) * pos;
pos = pos - 1;
}

int remind = sum % 11;
int controlNum = int.Parse(vnc.ToCharArray()(9));

if (remind <= 2) {
if (controlNum == remind) {
return true;
} else {
return false;
}
} else {
if ((11 - remind) == controlNum) {
return true;
} else {
return false;
}
}