پاسخ به بازخورد‌های پروژه‌ها
نیاز به sql server
خیر. بانک اطلاعاتی آن یک فایل XML ساده‌است.
مطالب
چقدر سی‌شارپ را می‌شناسیم؟!
هر چند که #C به عنوان یک زبان ساده برای درک و یادگیری شناخته میشود، گاهی رفتاری غیرمنتظره را حتی برای توسعه دهنده‌های با تجربه خواهد داشت. در این نوشته مروری بر بعضی از این رفتارها و توضیح دلایل پشت آن خواهیم کرد.

Value 

اگر مقدار null مدیریت نشود، میتواند باعث ایجاد نتایج نامطلوب، یا باعث از کار افتادن برنامه شود. شئ null به خودی خود مخرب نیست؛ اما اگر بخواهیم به یکی از متدها یا خاصیت‌های آن دسترسی داشته باشیم، با استثنای معروف NullReferenceException روبرو می‌شویم. برای در امان ماندن، باید همیشه اطمینان داشته باشیم که پیش از استفاده از امکانات شئ، ارجاع آن null نباشد. در قطعه کد زیر برخی از رفتارهای null value آورده شده:
// Behavior 1 
object obj = null;
bool objValueEqual = obj.Equals(null);

// Behavior 2 
object obj = null;
Type objType = obj.GetType();

// Behavior 3
string str = (string)null;
bool strType = str is string;

// Behavior 4
int num = 5;
Nullable<int> nullableNum = 5;
bool typeEqual = num.GetType() == nullableNum.GetType();

// Behavior 5
Type inType = typeof(int);
Type nullableIntType = typeof(Nullable<int>);
bool typeEqual = inType == nullableIntType;
  • در رفتار اول هرچند که متد Equals از شی null در دسترس است و با مقدار null مقایسه شده اما در زمان اجرا پیغام خطای NullReferenceException را خواهیم داشت. 
  • در رفتار دوم هم پیغام خطا را خواهیم داشت. شئ با مقدار null، در زمان اجرا هیچ نوعی را برنمیگرداند. 
  • در رفتار سوم هر چند که مقدار null صریحا به رشته تبدیل شده و برای چاپ متغیر str پیام خطایی را نخواهیم داشت، اما متغیر strType در خروجی، false خواهد بود. همانطور که در رفتار دوم گفته شد، شیء با مقدار null هیچ نوعی را برنمیگرداند. 
  • خروجی رفتار چهارم true خواهد بود. به این صورت که هر دو از نوع System.int32 خواهند بود.
  • در رفتار پنجم اگر از نوع‌ها، خروجی جداگانه بگیریم، خواهیم دیدکه نوع int از System.int32 و <Nullable<int از نوع System.Nullable`1[System.Int32] میباشند، در نتیجه خروجی false است. اشیای nullable بعد از اینکه مقداری مشخص را دریافت کردند، به صورت یک شیء غیر nullable رفتار خواهند کرد.

مدیریت مقادیر null در سربارگذاری متدها   

        static void Main(string[] args)
        {
            Console.WriteLine(Method(null));
            Console.ReadLine();
        }
        private static string Method(object obj)
        {
            return "Object parameter";
        }
        private static string Method(string str)
        {
            return "String parameter";
        }
در قطعه کد بالا، فراخوانی متد سربارگذاری شده با مقدار ورودی null، باعث اجرای متدی میشود که پارامتر ورودی آن از نوع رشته است. تا زمانیکه یکی از پارامترها بتواند به دیگری تبدیل شود، برنامه بدون خطا کامپایل خواهد شد. اما اگر هیچ تبدیل نوعی بین پارامترها وجود نداشته باشد، کد کامپایل نخواهد شد. بین متدهای سربارگذاری شده، متدی که نوع پارامتر آن مشخص‌تر است، فراخوانی میشود. برای اینکه متد خاصی را مجبور به اجرا کنیم، باید مقدار null را پیش از ارسال، به نوع پارامتر آن متد تبدیل کنید.(object)null

رفتارهای ()Math.Round

var rounded = Math.Round(1.5); // 2
var rounded = Math.Round(2.5); // 2

var rounded = Math.Round(2.5, MidpointRounding.ToEven); // 2
var rounded = Math.Round(2.5, MidpointRounding.AwayFromZero); // 3

var value = 1.4f;
var rounded = Math.Round(value + 0.1f); // 1
متد Round از کلاس Math، ورودی را که عددی اعشاری است، گرد میکند. اگر مقدار اعشار کمتر از ۰.۵ باشد، به سمت پایین و اگر بیشتر از ۰.۵ باشد، به سمت بالا گرد میشود. اما اگر ورودی دقیقا مقدار اعشاری ۰.۵ را داشته باشد چطور؟ متد Round به صورت پیش‌فرض ورودی  را به نزدیکترین عدد زوج گرد میکند، به این دلیل خط‌های ۱ و ۲ از قطعه کد بالا، خروجی یکسان ۲ را خواهند داشت. این متد آرگومان دومی هم دارد که دو حالت MidpointRounding.ToEven و MidpointRounding.AwayFromZero را می‌توان برای آن مشخص کرد. ToEven همان رفتار پیش‌فرض متد است که ورودی را به نزدیکترین عدد زوج گرد میکند و از حالت AwayFromZero میشود برای گرد کردن ورودی به عدد بزرگتر استفاده کرد (خط ۵). 
در خط ۸ یک حالت خاص دیگر نیز داریم. انتظار میرود که خروجی، به نزدیکترین عدد زوج گرد شود و نتیجه ۲ باشد؛ مثل خط ۱، اما خروجی ۱ خواهد بود. وقتی ورودی‌ها را از نوع float در نظر بگیریم، مقدار 0.1f کمی کمتر از ۰.۱ خواهد بود و نتیجه محاسبه کمی کمتر از ۱.۵. برای پرهیز از این مسئله بهتر است ورودی متد Round را از نوع decimal در نظر بگیریم.
 

مقدار دهی اولیه کلاسها 

پیشنهاد میشود برای جلوگیری از وقوع استثناءها از مقدار دهی اولیه کلاسها در سازنده کلاس، بخصوص اگر سازنده استاتیک داشته باشیم، پرهیز کنیم. ترتیب مقدار دهی اولیه زمانیکه از یک کلاس یه وهله ساخته میشود، به قرار زیر است:
  • فیلدهای استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
  • سازنده استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
  • فیلدهایی از کلاس که در نمونه ساخته شده در دسترس قرار میگیرند.
  • سازنده کلاس که در زمان ایجاد یک نمونه از کلاس در دسترس قرار میگیرد.
در قطعه کد زیر اگر نمونه‌ای از کلاس FailingClass ساخته شود، انتظار میرود که خطای InvalidOperationException صادر شود؛ اما برنامه با خطای TypeInitializationException متوقف میشود. در واقع در زمان اجرا به صورت خودکار خطای TypeInitializationException، خطای InvalidOperationException را پوشش میدهد. اگر بجای  InvalidOperationException یک دستور ساده WriteLine داشته باشیم، سازنده کلاس FailingClass مجال کامل شدن را خواهد داشت. اما با خطایی که داخل سازنده صادر کرده‌ایم، سازنده کلاس بدون اینکه به طور کامل به پایان برسد، متوقف خواهد شد. 
    public static class Config
    {
        public static bool ThrowException { get; set; } = true;
    }

    public class FailingClass
    {
        static FailingClass()
        {
            if (Config.ThrowException)
            {
                throw new InvalidOperationException();
            }
        }
    }
حال که میدانیم خطای اصلی که در این مواقع صادر میشود چیست، شاید بخواهیم به روش زیر آن را مدیریت کنیم.
try
{
   var failedInstance = new FailingClass();
}
catch (TypeInitializationException) { }

Config.ThrowException = false;
var instance = new FailingClass();
اگر قطعه کد بالا را بدون بخش try  اجرا کنیم، برنامه ابتدا صدور خطا را false میکند و بدون مشکل از کلاس نمونه‌ای ساخته میشود. اما اگر بخش try را داشته باشیم، هر چند که خطا در بخش try گرفته میشود و تنظیم صدور خطا false است، باز هم در خط آخر و در زمان ایجاد یک نمونه از کلاس، پیام خطای TypeInitializationException خواهیم داشت. علت آن است که سازنده استاتیک کلاس فقط یک بار فراخوانی میشود و اگر در این فراخوانی خطایی رخ دهد، این خطا در اثر ایجاد سایر نمونه‌ها و یا استفاده مستقیم از کلاس، مجددا صادر خواهد شد. در نتیجه این کلاس تا زمانیکه پردازش آن در جریان است، غیرقابل استفاده خواهد بود. یک مثال دیگر از ترتیب فراخوانی‌ها را بررسی میکنیم.
public class BaseClass
{
    {
        public BaseClass()
        {
            VirtualMethod(1);
        }
        public virtual int VirtualMethod(int dividend)
        {
            return dividend / 1;
        }
    }

    public class DerivedClass : BaseClass
    {
        int divisor;
        public DerivedClass()
        {
            divisor = 1;
        }
        public override int VirtualMethod(int dividend)
        {
            return base.VirtualMethod(dividend / divisor);
        }
    }
در قطعه کد بالا هر چند که همه چیز درست به نظر میرسد، اما اگر از کلاس DerivedClass نمونه‌ای ساخته شود، با پیام خطای DivideByZeroException مواجه میشویم. علت این مشکل ترتیب مقدار دهی اولیه در کلاسهای فرزند است. ابتدا فیلدهای کلاس فرزند مقدار دهی میشوند و بعد فیلدهای کلاس پایه، بعد سازنده کلاس پایه فراخوانی میشود و پس از آن سازنده کلاس فرزند. ترتیب فراخوانی‌ها به همین جا محدود نمیشود. 
در مثال بالا متد VirtualMethod که در سازنده کلاس پایه فراخوانی شده، پیش از این که کد داخل خود را اجرا کند، متد VirtualMethod را در کلاس فرزند، فراخوانی میکند و کلاس فرزند مجالی را برای مقدار دهی متغیر divisor، در سازنده خود نخواهد داشت. در نتیجه مقدار این متغیر در متد VirtualMethod صفر خواهد ماند و باعث صدور استثناء میشود. برای پرهیز از چنین مشکلاتی بهتر است فیلدهای یک کلاس به صورت مستقیم مقدار دهی اولیه بشوند. مقدار دهی اولیه و یا فراخوانی متدهای virtual در سازنده کلاس‌ها میتواند باعث بروز رفتارهای پیش بینی نشده‌ای شوند.

چند ریختی 

 چند ریختی قابلیتی است برای کلاسهای متفاوت تا بتوانند یک اینترفیس مشابه را به صورت‌های مختلفی پیاده‌سازی کنند. اما قطعه کد زیر قاعده چند ریختی را نقض میکند. 
 class Program
    {
        static void Main(string[] args)
        {
            var instance = new DerivedClass();
            var result = instance.Method();
            result = ((BaseClass)instance).Method();
            Console.WriteLine(instance + " -> " + result); // Derived Class ...  -> Method in BaseClass
            Console.ReadLine();

        }
    }

    public class BaseClass
    {
        public virtual string Method()
        {
            return "Method in BaseClass";
        }
    }

    public class DerivedClass : BaseClass
    {
        public override string ToString()
        {
            return "Derived Class ... ";
        }

        public new string Method()
        {
            return "Method in DerivedClass";
        }
    }
در خروجی کنسول هرچند که Instance همچنان وهله‌ای از DerivedClass است اما به دلیل تبدیل در خط ۷، Method کلاس DerivedClass به وسیله کلاس پایه پنهان شده و Method کلاس پایه فراخوانی میشود. در قطعه کد زیر حالت مشابه‌ای را که در بالا داشتیم، برای interface‌ها دیده میشود.
class Program
    {
        static void Main(string[] args)
        {
            var instance = new DerivedClass();
            var result = instance.Method(); // -> Method in DerivedClass
            result = ((IInterface)instance).Method(); // -> Method belonging to IInterface
            Console.WriteLine(result);
            Console.ReadLine();
        }
    }

    public interface IInterface
    {
        string Method();
    }

    public class DerivedClass : IInterface
    {
        public string Method()
        {
            return "Method in DerivedClass";
        }
        string IInterface.Method()
        {
            return "Method belonging to IInterface";
        }
}
هرچند که به نظر میرسد دلیلی برای استفاده از روشهای گفته شده وجود ندارد، اما اگر بخواهیم بیش از یک پیاده‌سازی را برای یک متد در یک کلاس داشته باشیم، میتواند مورد توجه قرار گیرد. بخصوص اگر نیاز باشد که پیاده‌سازی دوم خودش به طور مستقلی در کلاسی دیگر استفاده شود.

Iterators 

Iterator‌ها (تکرار شونده‌ها) ساختارهایی هستند که برای حرکت در عناصر یک collection استفاده میشوند. عموما از دستور foreach استفاده و نوع جنریک <IEnumerable<T را نمایندگی میکنند. هر چند که استفاده از آنها ساده است، اما اگر کارکرد داخلی iteratorها را درک نکنیم ممکن است به دام استفاده نادرست از آنها گرفتار شویم. در قطعه کد زیر کلاس Test صدا زده میشود و مقادیر یک تا پنج به صورت یک IEnumerable از داخل بلوک using بازگشت داده میشود. 
private IEnumerable<int> GetEnumerable(StringBuilder log)
{
     using (var test = new Test(log))
      {
          return Enumerable.Range(1, 5);
      }
}

فرض کنیم کلاس Test اینترفیس IDisposable را پیاده‌سازی کرده و در سازنده و متد Dispose خود پیامهایی را به log اضافه کند. در مثالهای واقعی، کلاس Testمیتواند اتصالی به پایگاه داده باشد و رکوردهای خوانده شده، بازگشت داده شوند. توسط حلقه زیر مقدار خروجی تابع را چاپ میکنیم.
var log = new StringBuilder();
            
foreach (var number in GetEnumerable(log))
{
     log.AppendLine($"{number}");
}
انتظار میرود که خروجی به این صورت باشد که ابتدا رشته Created (از سازنده کلاس Test) چاپ شود بعد اعداد یک تا پنج و در نهایت رشته Disposed (از متد Dispose کلاس Test). به عبارتی در ابتدای کار، بلوک using، سازنده کلاس را فراخوانی کند و بعد از اینکه بلوک به پایان کارش رسید متد Dispose کلاس فراخوانی شود. اما در واقع خروجی به صورت زیر خواهد بود. 
Created
Disposed
1
2
3
4
5
این تفاوت در دنیای واقعی مهم است؛ به اینصورت که مثلا اتصال به پایگاه داده قبل از اینکه داده‌ها خوانده شوند، بسته میشود و قطعه کد به درستی عمل نخواهد کرد. تنها راه حل، پیمایش در collection داخل using و بازگشت هر مقدار به صورت مجزا است، که در زیر آمده است.
 using (var test = new Test(log))
 {
     foreach (var i in Enumerable.Range(1,5))
     {
         yield return i;
     }
 }
فقط در این صورت است که کلاس Test بعد از اتمام کار حلقه و در زمان درست به پایان میرسد. توسط کلمه کلیدی yield و برای متدی که خروجی قابل پیمایش داشته باشد میتوان چندین مقدار را بازگشت داد. ترتیب اجرای دستورات در قطعه کد بالا به این صورت است که ابتدا نمونه‌ای از کلاس Test ایجاد میشود و سازنده کلاس فراخوانی میشود، سپس حلقه foreach به تعداد مشخص شده در Range مقادیر بازگشتی را در خروجی تابع قرار میدهد. وقتی که کار حلقه تمام شد، بلوک using دستورات را ادامه خواهد داد که برابر با خاتمه دادن به تمام نمونه‌ها و منابع استفاده شده در بلوک است؛ یعنی فراخوانی متد Dispose. با استفاده از این روش خروجی به شکل زیر خواهد بود. 
Created
1
2
3
4
5
Disposed

مطالب
تهیه یک DynamicXml برای خواندن اطلاعات فایل Xml با استفاده از انقیاد پویا در سی‌شارپ

در فریمورک NET. ابزارهای مختلفی برای کار با داده‌های XML در نظر گرفته شده‌است که بعد از نسخه 3.5 آن، انتخاب اول LINQ to XML می باشد. در این مطلب قصد داریم API ای را برای خواندن اطلاعات فایل‌های XML با استفاده از LINQ to XML و انقیاد پویا در سی‌شارپ (Dynamic Binding) تهیه کنیم.


راه حل اول: استفاده از ExpandoObject

public static class ExpandoXml
{
    public static dynamic AsExpando(this XDocument document)
    {
        return CreateExpando(document.Root);
    }

    private static dynamic CreateExpando(XElement element)
    {
        var result = new ExpandoObject() as IDictionary<string, object>;
        if (element.Elements().Any(e => e.HasElements))
        {
            var list = new List<ExpandoObject>();
            result.Add(element.Name.ToString(), list);
            foreach (var childElement in element.Elements())
            {
                list.Add(CreateExpando(childElement));
            }
        }
        else
        {
            foreach (var leafElement in element.Elements())
            {
                result.Add(leafElement.Name.ToString(), leafElement.Value);
            }
        }
        return result;
    }
}

در تکه کد بالا از طریق متد CreateExpando به صورت بازگشتی ابتدا بررسی می‌شود که آیا عنصر جاری دارای عناصری می‌باشد و همچنین آیا آنها دارای فرزند می‌باشند یا خیر؛ در صورت برقراری شرط، نتیجه‌ی اجرای متد CreateExpando بر روی تک تک عناصر فرزند را درون لیستی از ExpandoObject قرار داده و سپس آن لیست نیز به عنوان Value عنصر جاری در نظر گرفته می‌شود. در صورت عدم برقراری شرط مذکور، مقادیر مربوط به عناصر فرزند را در قالب یک ExpandoObject به عنوان خروجی بازگشت خواهد داد.


راه حل دوم: استفاده از DynamicObject

برای این منظور کلاس زیر را در نظر بگیرید:
    public class DynamicXml : DynamicObject, IEnumerable
    {
        private readonly dynamic _xml;
        public DynamicXml(string fileName)
        {
            _xml = XDocument.Load(fileName);
        }

        public DynamicXml(dynamic xml)
        {
            _xml = xml;
        }

        public IEnumerator GetEnumerator()
        {
            foreach (var item in _xml.Elements())
            {
                yield return new DynamicXml(item);
            }
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var xml = _xml.Element(binder.Name);
            if (xml != null)
            {
                result = new DynamicXml(xml);
                return true;
            }

            var attribute = _xml.Attribute(binder.Name);
            if (attribute != null)
            {
                result = new DynamicXml(attribute);
                return true;
            }

            result = null;
            return false;
        }

        public static implicit operator string(DynamicXml xml)
        {
            return xml._xml.Value;
        }
    }

کلاس DynamicXml از طریق سازنده اول، نام فایل را دریافت کرده و از طریق LINQ to XML با استفاده از متد Load کلاس XDocument، فایل مورد نظر بارگذاری شده و درون فیلدی به نام xml_ از نوع dynamic نگه داشته می‌شود. کار بعدی، بازنویسی متد TryGetMember می‌باشد. در بدنه بازنویسی شده این متد ابتدا بررسی می‌شود که آیا با نام خصوصت درخواست شده عنصری در داده XML وجود دارد یا خیر؛ در صورت موجود بودن، پارامتر result با یک  وهله جدید از DynamicXml مقدار دهی می‌شود که عنصر یافت شده از طریق سازنده دوم، به عنوان داده xml برای مقدار دهی فیلد xml_ به عنوان آرگومان ارسال می‌شود. در صورت عدم وجود عنصر مذکور، بدنبال خصوصیتی با آن نام بوده و در صورت یافت شدن، باز به عنوان یک وهله DynamicXml برای مقدار دهی result استفاده می‌شود.

در ادامه برای نسبت دادن یک وهله از DynamicXml به یک متغیر string و دستیابی به مقدار یک عنصر که از طریق خصوصیت، درخواست می‌شود نیاز است تا اپراتور ضمنی string را نیز برای کلاس بالا نظر بگیریم. همچنین برای ایجاد امکان پیمایش برروی عناصر فرزند از طریق foreach، لازم است واسط IEnumerable را نیز پیاده سازی کرده باشیم.


طریقه استفاده

    class Program
    {
        static void Main(string[] args)
        {
            var doc1 = XDocument.Load("Employees.xml");
            
            foreach (var element in doc1.Element("Employees").Elements("Employee"))
            {
                Console.WriteLine(element.Element("FirstName").Value);
            }

            dynamic doc2 = XDocument.Load("Employees.xml").AsExpando();
            foreach (var employee in doc2.Employees)
            {
                Console.WriteLine(employee.FirstName);
            }

            dynamic doc3 = new DynamicXml("Employees.xml");
            foreach (var employee in doc3.Employees)
            {
                Console.WriteLine(employee.FirstName);
                Console.WriteLine(employee.Id);
            }
        }
    }
و فایل Employees.xml
<?xml version="1.0" encoding="utf-8" ?>
<Employees>
    <Employee Id="1">
        <FirstName>
            Employee1
        </FirstName>
    </Employee>
     <Employee Id="2">
        <FirstName>
            Employee2
        </FirstName>
    </Employee>
     <Employee Id="3">
        <FirstName>
            Employee3
        </FirstName>
    </Employee>
     <Employee Id="4">
        <FirstName>
            Employee4
        </FirstName>
    </Employee>
</Employees>

مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت دوم - کوئری گرفتن از جدول مجازی FTS
پس از آشنایی با نحوه‌ی ایجاد و به روز رسانی جدول مجازی FTS، اکنون قصد داریم با روش‌های کوئری گرفتن از آن آشنا شویم. برای این منظور در ابتدا نیاز است تعدادی رکورد را در آن ثبت کنیم:
        private static void seedDb(ApplicationDbContext context)
        {
            if (!context.Chapters.Any())
            {
                var user1 = context.Users.Add(new User { Name = "Test User" });
                context.Chapters.Add(new Chapter
                {
                    Title = "Learn SQlite FTS5",
                    Text = "This tutorial teaches you how to perform full-text search in SQLite using FTS5",
                    User = user1.Entity
                });
                context.Chapters.Add(new Chapter
                {
                    Title = "Advanced SQlite Full-text Search",
                    Text = "Show you some advanced techniques in SQLite full-text searching",
                    User = user1.Entity
                });
                context.Chapters.Add(new Chapter
                {
                    Title = "SQLite Tutorial",
                    Text = "Help you learn SQLite quickly and effectively",
                    User = user1.Entity
                });
                context.Chapters.Add(new Chapter
                {
                    Title = "Handle markup in text",
                    Text = "<p>Isn't this <font face=\"Comic Sans\">funny</font>?",
                    User = user1.Entity
                });

                context.Chapters.Add(new Chapter
                {
                    Title = "آزمایش متن فارسی",
                    Text = "برای نمونه تهیه شده‌است",
                    User = user1.Entity
                });

                context.Chapters.Add(new Chapter
                {
                    Title = "Exclude test 1",
                    Text = "in the years 2018-2019 something happened.",
                    User = user1.Entity
                });
                context.Chapters.Add(new Chapter
                {
                    Title = "Exclude test 2",
                    Text = "It was 2018 and then it was 2019",
                    User = user1.Entity
                });

                context.SaveChanges();
            }
        }
در اینجا به صورت متداولی، اطلاعات در جدول اصلی Chapters ثبت می‌شوند و چون SaveChanges را در قسمت قبل جهت به روز رسانی خودکار جدول مجازی Chapters_FTS بازنویسی کردیم، فراخوانی آن، سبب تولید ایندکس‌های Full Text هم می‌شود.

ثبت اطلاعات فوق، چنین رکوردهایی را در جدول Chapters به وجود می‌آورد که شامل اطلاعات یونیکد، HTML ای و غیره است:



اجرای اولین کوئری بر روی جدول مجازی Chapters_FTS به صورت مستقیم

کوئری‌های Full-text در SQLite، چنین شکل کلی را دارند و توسط تابع match انجام می‌شوند:
select * from Chapters_FTS where Chapters_FTS match "fts5"
که یک چنین خروجی را نیز به همراه دارد:


همانطور که مشاهده می‌کنید در اینجا تنها دو ستونی که ایندکس شده‌اند، در خروجی نهایی ظاهر می‌شوند؛ اما این جدول به همراه ستون‌های مخفی توکار دیگری نیز هست:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5"
در این کوئری اینبار ستون‌های مخفی rank و همچنین rowid را نیز می‌توانید مشاهده کنید:


- Rowid با توجه به تعریفی که در قسمت قبل انجام دادیم:
CREATE VIRTUAL TABLE "Chapters_FTS"
USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
به همان primary-key جدول اصلی chapters اشاره می‌کند. بنابراین اگر نیاز باشد تا این خروجی حاصل از کوئری بر روی جدول مجازی Chapters_FTS را به جدول اصلی chapters متصل کرد، می‌توان از مقدار rowid بازگشتی استفاده نمود.

- تمام جداول مجازی FTS، به همراه ستون مخفی rank نیز هستند که میزان نزدیک بودن خروجی حاصل را به کوئری درخواستی مشخص می‌کنند. این عدد توسط تابعی به نام bm25 تهیه می‌شود. اگر کوئری FTS به همراه قسمت where نباشد، مقدار rank همواره نال خواهد بود. اما اگر قسمت where به همراه match قید شود، مقدار rank، مقدار از پیش محاسبه شده‌ی تابع توکار bm25 است. به همین جهت کار با این مقدار از پیش محاسبه شده، سریعتر از فراخوانی مستقیم متد bm25 است. برای مثال دو کوئری زیر اساسا یکی هستند؛ اما دومی سریعتر است:
select * from Chapters_FTS where Chapters_FTS match "fts5" ORDER BY bm25(fts);
select * from Chapters_FTS where Chapters_FTS match "fts5" ORDER BY rank;

یک نکته: کوئری FTS فوق بر روی هر دو ستون title و text اجرا می‌شود (و یا هر ستون موجود دیگری که پیشتر ایندکس شده باشد).


اجرای اولین کوئری بر روی جدول مجازی Chapters_FTS توسط EF Core

پس از آشنایی مقدماتی با کوئری نویسی FTS در SQLite، بر انجام یک چنین کوئری در EF Core می‌توان به صورت زیر عمل کرد:
- ابتدا باید یک موجودیت بدون کلید را مطابق ستون‌های مخفی و ایندکس شده‌ی بازگشتی تهیه کنیم:
namespace EFCoreSQLiteFTS.Entities
{
    public class ChapterFTS
    {
        public int RowId { get; set; }
        public decimal? Rank { get; set; }

        public string Title { get; set; }
        public string Text { get; set; }
    } 
}
همانطور که مشاهده می‌کنید، rank به صورت نال پذیر تعریف شده‌است؛ چون اگر قسمت where ذکر نشود، مقداری نخواهد داشت.
- سپس نیاز است این موجودیت بدون کلید را به EF معرفی کنیم:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        //...

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<ChapterFTS>().HasNoKey().ToView(null);
        }

        //...
    }
}
در اینجا ChapterFTS تهیه شده، با متد HasNoKey علامتگذاری می‌شود تا آن‌را بتوان بدون مشکل در کوئری‌های EF استفاده کرد. همچنین فراخوانی ToView(null) سبب می‌شود تا EF Core جدولی را در حین Migration از روی این موجودیت ایجاد نکند و آن‌را به همین حال رها کند.

- و در آخر روش کوئری گرفتن از جدول مجازی FTS در EF Core به صورت زیر می‌باشد که توسط متد FromSqlRaw به صورت پارامتری (مقاوم در برابر حملات تزریق اس‌کیوال)، قابل انجام است:
const string ftsSql = "SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH {0}";
foreach (var chapter in context.Set<ChapterFTS>().FromSqlRaw(ftsSql, "fts5"))
{
  Console.WriteLine($"Title: {chapter.Title}");
  Console.WriteLine($"Text: {chapter.Text}");
}


بررسی قابلیت‌های ویژه‌ی کوئری‌های FTS در SQLite

اکنون که با روش کلی کوئری گرفتن از جدول مجازی FTS آشنا شدیم، نکات ویژه‌ی آن‌را بررسی می‌کنیم و در اینجا بیشتر پارامتر ذکر شده‌ی پس از عملگر match تغییر خواهد کرد و مابقی قسمت‌های آن ثابت و مانند قبل هستند.

بجای عملگر match می‌توان از = نیز استفاده کرد

دو کوئری زیر دقیقا به یک معنا هستند:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5";
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS = "fts5";
و هر دو همانطور که عنوان شد بر روی تمام ستون‌های ایندکس شده‌ی موجود اجرا می‌شوند و اگر نیاز است نتایج را بر اساس میزان نزدیکی آن‌ها به کوئری انجام شده مرتب کرد، می‌توان یک ORDER by rank را نیز به انتهای آن‌ها افزود:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "fts5" ORDER by rank;


جستجوهایی به همراه واژه‌هایی در کنار هم

از دیدگاه FTS، دو کوئری زیر که در قسمت match آن‌ها، واژه‌ها با فاصله در کنار هم قرار گرفته‌اند، یکی هستند:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn SQLite" ORDER by rank;
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn + SQLite" ORDER by rank;
و هر دو خروجی زیر را تولید می‌کنند:


علت اینجا است که یک full-text search بر اساس ایندکس شدن واژه‌ها تولید می‌شود و هر کدام از این واژه‌ها به یک توکن نگاشت خواهند شد. به همین جهت است که در اینجا تفاوتی بین + و فاصله در عبارت جستجو شده وجود ندارد. در این حالت اگر در یکی از ستون‌های ایندکس شده، واژه‌ی learn و یا واژه‌ی SQLite بکار رفته باشد، در خروجی نهایی لیست خواهد شد.


امکان جستجو بر اساس پیشوندها

می‌توان با استفاده از *، تمام توکن‌های ایندکس شده و شروع شده‌ی با واژه‌ی مشخصی را جستجو کرد:
 SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search*" ORDER by rank;
برای مثال در اینجا رکوردهایی که دارای واژه‌هایی مانند search، searching و غیره هستند، بازگشت داده می‌شوند:



امکان استفاده از عملگرهای بولی NOT، AND و OR

اگر learn text را جستجو کنیم:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn text" ORDER by rank;


رکوردی با ID مساوی 1 بازگشت داده می‌شود. اما اگر نیاز باشد رکوردی بازگشت داده شود که حاوی learn باشد، اما text خیر، می‌توان از عملگر NOT استفاده کرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "learn NOT text" ORDER by rank;


که اینبار رکوردی با ID مساوی 3 را بازگشت داده‌است.

نکته‌ی مهم: عملگرهای بولی FTS مانند AND، OR، NOT و غیره باید با حروف بزرگ قید شوند.

در ادامه مثال دیگری از ترکیب عملگرهای بولی را مشاهده می‌کنید:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search AND sqlite OR help" ORDER by rank;


که تقدم و تاخر این عملگرها را می‌توان توسط پرانتزها به صورت صریحی نیز مشخص کرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "search AND (sqlite OR help)" ORDER by rank;



امکان ذکر صریح ستون‌های مدنظر در کوئری

همانطور که عنوان شد، حالت پیش‌فرض جستجوهای تمام متنی، جستجوی واژه‌ی مدنظر در تمام ستون‌های ایندکس شده‌است؛ اما شاید این مورد مدنظر شما نباشد. به همین منظور می‌توان ابتدا نام ستون مدنظر را ذکر کرد و پس از آن یک : را قرار داد تا فقط جستجو بر روی آن ستون خاص صورت گیرد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "text:some AND title:sqlite" ORDER by rank;


امکان ترکیب نام ستون‌ها به صورت {col2 col1 col3} نیز وجود دارد.

نکته‌ی مهم! در جستجوهای FTS در SQLite، ذکر - به معنای قید صریح نام یک ستون خاص است (و یا لیست ستون‌هایی به صورت {col2 col1 col3}-) که قرار نیست چیزی با آن(ها) انطباق داده شود (- شبیه به عملگر NOT عمل می‌کند؛ اینبار در مورد ستون‌ها) و این مورد عموما تازه‌کاران را به اشتباه می‌اندازد. برای مثال در ابتدای بحث، دو رکورد را که دارای text ای مساوی عبارات زیر هستند، ثبت کردیم:
"in the years 2018-2019 something happened"
"It was 2018 and then it was 2019"
اکنون فرض کنید می‌خواهیم 2018-2019 را جستجو کنیم:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "2018-2019" ORDER by rank;
خروجی آن خطای زیر است و عنوان می‌کند که ستون 2019 تعریف نشده‌است؛ چون پس از -، به دنبال نام یک ستون ایندکس شده می‌گردد:
Execution finished with errors.
Result: no such column: 2019
برای رفع این مشکل می‌توان - را حذف کرد:


و یا می‌توان عبارت جستجو شده را بین "" قرار داد:

SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH '"2018-2019"' ORDER by rank;


و یا حتی می‌توان '"2018 2019"' را نیز جستجو کرد که نتیجه‌ی مشابهی را ارائه می‌دهد.


امکان جستجوی بر روی عبارات یونیکد

FTS5 و آخرین نگارش SQLite، به همراه tokenizer مخصوص یونیکد نیز هست و با اینگونه جستجوهای تمام متنی، مشکلی ندارد:
SELECT rowid, title, text, rank FROM Chapters_FTS WHERE Chapters_FTS MATCH "آزمایش"
ORDER by rank;



توابع کمکی FTS در SQLite برای متمایز سازی عبارات یافت شده‌ی در متن

فرض کنید می‌خواهیم واژه‌ی fts5 را جستجو کرده و همچنین در خروجی نهایی، هرجائیکه fts5 قرار دارد، آن‌را به صورت bold نمایش دهیم. برای اینکار، تابع توکار highlight قابل استفاده‌است. اما اگر در این بین خواستیم فقط قسمت کوتاهی از متن مورد نظر را که به جستجوی ما نزدیک است نمایش دهیم، می‌توان از متد توکار snippet استفاده کرد:
SELECT rowid, highlight(Chapters_FTS, title, '<b>', '</b>') as title,
snippet(Chapters_FTS, text, '<b>', '</b>', '...', 64) as text, rank FROM Chapters_FTS
WHERE Chapters_FTS MATCH "fts5" ORDER BY rank


نکته‌ی مهم: چون بر اساس نکات قسمت قبل، متنی که به Chapters_FTS  ارسال می‌شود، نرمال سازی شده‌است، متدهای فوق کارآیی خودشان را از دست می‌دهند. برای مثال اگر در کوئری فوق، واژه‌ی funny را که به یک رکورد HTML ای اشاره می‌کند، جستجو کنیم، خروجی زیر را دریافت خواهیم کرد:


خروجی نهایی، چون به جدول اصلی chapters متصل است، اصل متن را بازگشت می‌دهد، اما چون اطلاعاتی را که به Chapters_FTS  ارسال کرده‌ایم، فاقد تگ‌های HTML هستند، تا خروجی دقیقی حاصل شود، متدهای highlight و snippet دیگر قادر به علامتگذاری خروجی نهایی نبوده و اینکار را باید خودمان به صورت دستی در سمت کلاینت انجام دهیم.
مطالب
آشنایی با مفاهیم نوع داده Enum و توسعه آن - قسمت یکم
نوع داده شمارشی یا Enum، جهت تعاریف مقادیر ثابت و قابل شمارش در برنامه، بسیار کاربرد دارد. مقادیری که در این نوع داده تعریف می‌شوند بطور خودکار از عدد 0 شماره گذاری می‌شوند و به ترتیب یکی به آن‌ها اضافه می‌شود. برای مثال حالت زیر را در نظر بگیرید:
    public enum Grade
    {
        Failing,        // = 0
        BelowAverage,   // = 1
        Average,        // = 2
        VeryGood,       // = 3
        Excellent       // = 4
    }
  • در این حالت متد ()ToString نوع داده Enum عنوان مقادیر ثابت را بر می‌گرداند.
  • جهت برگشت مقدار عددی و شماره مقادیر ثابت‌های تعریف شده از متد ()ToString با فرمت D (شماره مقدار را بصورت Decimal نشان می‌دهد) و فرمت X جهت نمایش بصورت هگزا می‌توان استفاده کرد.
  • روش عرف برای نمایش مقدار عددی استفاده از تبدیل نوع صریح به int است.
به منظور درک بهتر موضوع، از یک برنامه کنسول استفاده می‌کنیم تا این نوع داده شمارشی را در آن استفاده کنیم.
static void Main(string[] args)
{
    Grade grade = Grade.Average;
    Console.WriteLine(grade.ToString());    // Print Avarage
    Console.WriteLine(grade.ToString("D")); // Print 2
    Console.WriteLine(grade.ToString("X")); // Print 00000002
    Console.WriteLine((int) grade);         //Print 2
    Console.ReadKey();
}
تغییر شماره (اندیس) مقادیر ثابت تعریف شده:
جهت تغییر شماره مقادیر کافیست بصورت زیر عمل کنیم:
public enum Grade
{
    Failing = 5,
    BelowAverage = 10,
    Average = BelowAverage + 5, // = 15
    VeryGood = 18,
    Excellent = 20
}
همانطور که در بالا می‌بینید برای مقدار Average بصورت ترکیبی عمل شده است.
بصورت پیش فرض کامپایلر سی شارپ از Int32 جهت نگهداری اعضای یک Enum استفاده می‌کند. هر چند غیر معقول به نظر می‌رسد اما شما می‌توانید این نوع را به byte - sbyte - short - ushort - uint - long تغییر دهید.
public enum Grade : byte
{
    Failing = 5,
    BelowAverage = 10,
    Average = BelowAverage + 5, // = 15
    VeryGood = 18,
    Excellent = 20
}
بدیهی است در این حالت خروجی دستور زیر 0F خواهد بود:
Console.WriteLine(grade.ToString("X")); // Print 0F
همچنین به خروجی دستورات زیر در حالت فوق توجه کنید:
Console.WriteLine("Underlying type: {0}", Enum.GetUnderlyingType(grade.GetType())); // Print System.Byte

Console.WriteLine("Type Code      : {0}", grade.GetTypeCode()); // Print Byte
و البته این:
Console.WriteLine("Value : {0}", (int)grade); // Print 15
در قسمت دوم این مطلب با استفاده از فضای نام System.Reflection و Extension Method‌ها و Custom Attribute کمی مقادیر Enum را توسعه خواهیم داد.
نظرات اشتراک‌ها
مقایسه کارآیی ORMهای مطرح دات نت
ADO.NET شبیه به زبان اسمبلی دسترسی به اطلاعات در دات نت هست. این پایین‌ترین سطحی است که برای کار با بانک‌های اطلاعاتی در دنیای دات نت وجود دارد. تمام ORMهای موجود لایه‌ای هستند بر روی این سطح.
مطالب
بازنویسی سطح دوم کش برای Entity framework 6
چندی قبل مطلبی را در مورد پیاده سازی سطح دوم کش در EF در این سایت مطالعه کردید. اساس آن مقاله‌ای بود که نحوه‌ی کش کردن اطلاعات حاصل از LINQ to Objects را بیان کرده بود (^). این مقاله پایه‌ی بسیاری از سیستم‌های کش مشابه نیز شده‌است (^ و ^ و ...).
مشکل مهم این روش عدم سازگاری کامل آن با EF است. برای مثال در آن تفاوتی بین (Include(x=>x.Tags و (Include(x=>x.Users وجود ندارد. به همین جهت در این نوع موارد، قادر به تولید کلید منحصربفردی جهت کش کردن اطلاعات یک کوئری مشخص نیست. در اینجا یک کوئری LINQ، به معادل رشته‌ای آن تبدیل می‌شود و سپس Hash آن محاسبه می‌گردد. این هش، کلید ذخیره سازی اطلاعات حاصل از کوئری، در سیستم کش خواهد بود. زمانیکه دو کوئری Include دار متفاوت EF، هش‌های یکسانی را تولید کنند، عملا این سیستم کش، کارآیی خودش را از دست می‌دهد. برای رفع این مشکل پروژه‌ی دیگری به نام EF cache ارائه شده‌است. این پروژه بسیار عالی طراحی شده و می‌تواند جهت ایده دادن به تیم EF نیز بکار رود. اما در آن فرض بر این است که شما می‌خواهید کل سیستم را در یک کش قرار دهید. وارد مکانیزم DBCommand و DataReader می‌شود و در آن‌جا کار کش کردن تمام کوئری‌ها را انجام می‌دهد؛ مگر آنکه به آن اعلام کنید از کوئری‌های خاصی صرفنظر کند.
با توجه به این مشکلات، روش بهتری برای تولید هش یک کوئری LINQ to Entities بر اساس کوئری واقعی SQL تولید شده توسط EF، پیش از ارسال آن به بانک اطلاعاتی به صورت زیر وجود دارد:
        private static ObjectQuery TryGetObjectQuery<T>(IQueryable<T> source)
        {
            var dbQuery = source as DbQuery<T>;

            if (dbQuery != null)
            {
                const BindingFlags privateFieldFlags = 
                    BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;

                var internalQuery =
                    source.GetType().GetProperty("InternalQuery", privateFieldFlags)
                        .GetValue(source);

                return
                    (ObjectQuery)internalQuery.GetType().GetProperty("ObjectQuery", privateFieldFlags)
                        .GetValue(internalQuery);
            }

            return null;
        }
این متد یک کوئری LINQ مخصوص EF را دریافت می‌کند و با کمک Reflection، اطلاعات درونی آن که شامل ObjectQuery اصلی است را استخراج می‌کند. سپس فراخوانی متد objectQuery.ToTraceString بر روی حاصل آن، سبب تولید SQL معادل کوئری LINQ اصلی می‌گردد. همچنین objectQuery امکان دسترسی به پارامترهای تنظیم شده‌ی کوئری را نیز میسر می‌کند. به این ترتیب می‌توان به معادل رشته‌ای منطقی‌تری از یک کوئری LINQ رسید که قابلیت تشخیص JOINها و متد Include نیز به صورت خودکار در آن لحاظ شده‌است.

این اطلاعات، پایه‌ی تهیه‌ی کتابخانه‌ی جدیدی به نام EFSecondLevelCache گردید. برای نصب آن کافی است دستور ذیل را در کنسول پاورشل نیوگت صادر کنید:
 PM> Install-Package EFSecondLevelCache
سپس برای کش کردن کوئری معمولی مانند:
 var products = context.Products.Include(x => x.Tags).FirstOrDefault();
می‌توان از متد جدید Cacheable آن به نحو ذیل استفاده کرد (این روش بسیار تمیزتر است از روش مقاله‌ی قبلی و امکان استفاده‌ی از انواع و اقسام متدهای EF را به صورت متداولی میسر می‌کند):
 var products = context.Products.Include(x => x.Tags).Cacheable().FirstOrDefault(); // Async methods are supported too.

پس از آن نیاز است کدهای کلاس Context خود را نیز به نحو ذیل ویرایش کنید (به روز رسانی شده‌ی آن در اینجا):
namespace EFSecondLevelCache.TestDataLayer.DataLayer
{
    public class SampleContext : DbContext
    {
        // public DbSet<Product> Products { get; set; }
 
        public SampleContext()
            : base("connectionString1")
        {
        }
 
        public override int SaveChanges()
        {
            return SaveAllChanges(invalidateCacheDependencies: true);
        }
 
        public int SaveAllChanges(bool invalidateCacheDependencies = true)
        {
            var changedEntityNames = getChangedEntityNames();
            var result = base.SaveChanges();
            if (invalidateCacheDependencies)
            {
               new EFCacheServiceProvider().InvalidateCacheDependencies(changedEntityNames);
            }
            return result;
        }
 
        private string[] getChangedEntityNames()
        {
            return this.ChangeTracker.Entries()
                .Where(x => x.State == EntityState.Added ||
                            x.State == EntityState.Modified ||
                            x.State == EntityState.Deleted)
                .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName)
                .Distinct()
                .ToArray();
        }
    }
}
متد InvalidateCacheDependencies سبب می‌شود تا اگر تغییری در بانک اطلاعاتی رخ‌داد، به صورت خودکار کش‌های کوئری‌های مرتبط غیر معتبر شوند و برنامه اطلاعات قدیمی را از کش نخواند.


کدهای کامل این پروژه را از مخزن کد ذیل می‌توانید دریافت کنید:
EFSecondLevelCache



پ.ن.
این کتابخانه هم اکنون در سایت جاری در حال استفاده است.
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه
خطا به دلیل این هست که هنوز بانک اطلاعاتی ایجاد نشده و نمی‌تواند بانک اطلاعاتی رو پیداکنه. آیا راهی هست این بخش بعد از ایجاد بانک اطلاعاتی ثبت شود؟
مطالب
کوئری هایی با قابلیت استفاده ی مجدد
با توجه به اصل Dry تا می‌توان باید از نوشتن کدهای تکراری خودداری کرد و کد‌ها را تا جایی که ممکن است به قسمت هایی با قابلیت استفاده‌ی مجدد تبدیل کرد. حین کار کردن با ORM‌های معروف مثل NHibernate و EntityFramework زمان زیادی نوشتن کوئری‌ها جهت واکشی داده‌ها از دیتابیس صرف می‌شود. اگر بتوان کوئری هایی با قابلیت استفاده‌ی مجدد نوشت علاوه بر کاهش زمان توسعه قابلیت هایی قدرتمندی مانند زنجیر کردن کوئری‌ها به دنبال هم به دست می‌آید. 
با یک مثال نحوه‌ی نوشتن و مزایای کوئری با قابلیت استفاده‌ی مجدد را بررسی می‌کنیم : 
برای مثال دو جدول شهر‌ها و دانش آموزان را درنظر بگیرید:
namespace ReUsableQueries.Model
{
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    [ForeignKey("BornInCityId")]
        public virtual City BornInCity { get; set; }
        public int BornInCityId { get; set; }
    }

    public class City
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Student> Students { get; set; }
    }
}
در ادامه این کلاس‌ها را در معرض دید EF Code first قرار داده:
using System.Data.Entity;
using ReUsableQueries.Model;

namespace ReUsableQueries.DAL
{
    public class MyContext : DbContext
    {
        public DbSet<City> Cities { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}

و همچنین تعدادی رکورد آغازین را نیز به جداول مرتبط اضافه می‌کنیم:
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }
        protected override void Seed(MyContext context)
        {
            var city1 = new City { Name = "city-1" };
            var city2 = new City { Name = "city-2" };
            context.Cities.Add(city1);
            context.Cities.Add(city2);
            var student1 = new Student() {Name = "Shaahin",LastName = "Kiassat",Age=22,BornInCity = city1};
            var student2 = new Student() { Name = "Mehdi", LastName = "Farzad", Age = 31, BornInCity = city1 };
            var student3 = new Student() { Name = "James", LastName = "Hetfield", Age = 49, BornInCity = city2 };
            context.Students.Add(student1);
            context.Students.Add(student2);
            context.Students.Add(student3);
            base.Seed(context);
        }
    }
فرض کنید قرار است یک کوئری نوشته شود که در جدول دانش آموزان بر اساس نام ، نام خانوادگی و سن جستجو کند : 
         var context = new MyContext();
          var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where(
              x => x.Age == age);
احتمالا هنوز کسانی هستند که فکر می‌کنند کوئری‌های LINQ همان لحظه که تعریف می‌شوند اجرا می‌شوند اما اینگونه نیست . در واقع این کوئری فقط یک Expression از رکورد‌های جستجو شده است و تا زمانی که متد ToList یا ToArray روی آن اجرا نشود هیچ داده ای برگردانده نمی‌شود.
در یک برنامه‌ی واقعی داده‌های باید به صورت صفحه بندی شده و مرتب شده برگردانده شود پس کوئری به این صورت خواهد بود : 
        var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where(
              x => x.Age == age).OrderBy(x=>x.LastName).Skip(skip).Take(take);
ممکن است بخواهیم در متد دیگری در لیست دانش آموزان بر اساس نام ، نام خانوادگی ، سن و شهر جستجو کنیم و سپس خروجی را اینبار بر اساس سن مرتب کرده و صفحه بندی نکنیم:
          var query = context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where
              (
                  x => x.Age == age).Where(x => x.BornInCityId == 1).OrderBy(x => x.Age);
همانطور که می‌بینید قسمت هایی از این کوئری با کوئری هایی که قبلا نوشتیم یکی است ، همچنین حتی ممکن است در قسمت دیگری از برنامه نتیجه‌ی همین کوئری را به صورت صفحه بندی شده لازم داشته باشیم.
اکنون نوشتن این کوئری‌ها میان کد های Business Logic باعث شده هیچ استفاده‌ی مجددی نتوانیم از این کوئری‌ها داشته باشیم. حال بررسی می‌کنیم که چگونه می‌توان کوئری هایی با قابلیت استفاده‌ی مجدد نوشت : 
namespace ReUsableQueries.Quries
{
    public static class StudentQueryExtension
    {
        public static IQueryable<Student> FindStudentsByName(this IQueryable<Student> students,string name)
        {
            return students.Where(x => x.Name.Contains(name));
        }
        public static IQueryable<Student> FindStudentsByLastName(this IQueryable<Student> students, string lastName)
        {
            return students.Where(x => x.LastName.Contains(lastName));
        }
        public static IQueryable<Student> SkipAndTake(this IQueryable<Student> students, int skip , int take)
        {
            return students.Skip(skip).Take(take);
        }
        public static IQueryable<Student> OrderByAge(this IQueryable<Student> students)
        {
            return students.OrderBy(x=>x.Age);
        }
    }
}
همان طور که مشاهده می‌کنید به کمک متد‌های الحاقی برای شیء IQueryable<Student> چند کوئری نوشته ایم . اکنون در محل استفاده از کوئری‌ها می‌توان این کوئری‌ها را به راحتی به هم زنجیر کرد. همچنین اگر روزی قرار شد منطق یکی از کوئری‌ها عوض شود با عوض کردن آن در یک قسمت برنامه همه جا اعمال می‌شود.  نحوه‌ی استفاده از این متدهای الحاقی به این صورت خواهد بود : 
     var query = context.Students.FindStudentsByName(name).FindStudentsByLastName(lastName).SkipAndTake(skip,take);          
فرض کنید قرار است یک سیستم جستجوی پیشرفته به برنامه اضافه شود که بر اساس شرط‌های مختلف باید یک شرط در کوئری اعمال شود یا نشود ، به کمک این طراحی جدید به راحتی می‌توان بر اساس شرط‌های مختلف یک کوئری را اعمال کرد یا نکرد : 
        var query = context.Students.AsQueryable();
          if (searchByName)
          {
              query= query.FindStudentsByName(name);
          }
          if (orderByAge)
          {
              query = query.OrderByAge();
          }
          if (paging)
          {
             query =  query.SkipAndTake(skip, take);
          }
          return query.ToList();
همچنین این کوئری‌ها وابسته به ORM خاصی نیستند البته این نکته هم مد نظر است که LINQ Provider بعضی ORM‌ها ممکن است بعضی کوئری‌ها را پشتیبانی نکند.