مطالب
کوئری نویسی در EF Core - قسمت هفتم - کار با رشته‌‌ها
هدف از این سری مثال‌ها، آشنایی با متدها و توابعی است که در حین کار با خواص رشته‌ای در LINQ to Entities، مجاز به استفاده‌ی از آن‌ها هستیم و همچنین اگر تابعی در EF-Core هنوز تعریف نشده بود، راه حل چیست.


مثال 1: نام تمام کاربران را با قالب 'Surname, Firstname'  نمایش دهید.

var members = context.Members
                                    .Select(member => new { Name = member.Surname + ", " + member.FirstName })
                                    .ToList();
متد Select می‌تواند به همراه اعمال محاسباتی ساده‌ای نیز باشد که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.
با این خروجی:



مثال 2: تمام امکاناتی را که با Tennis شروع می‌شوند، لیست کنید.
این گزارش به همراه تمام ستون‌های جدول است.

var facilities = context.Facilities
                                        .Where(facility => facility.Name.StartsWith("Tennis"))
                                        .ToList();
متدهای استانداردی مانند StartsWith، EndsWith و Contains را می‌توان بر روی خواص رشته‌ای بکار برد.
با این خروجی:



مثال 3: تمام امکاناتی را که با tennis شروع می‌شوند، لیست کنید. این جستجو باید غیرحساس به بزرگی و کوچکی حروف باشد.
این گزارش به همراه تمام ستون‌های جدول است.

نیازی به انجام مجزای این تمرین نیست؛ چون پاسخ آن همان پاسخ مثال 2 است. Collation پیش‌فرض در SQL Server، غیرحساس به بزرگی و کوچکی حروف است. بنابراین چه tennis را جستجو کنیم و یا TeNnis را، تفاوتی نمی‌کند.


مثال 4: شماره تلفن‌های دارای پرانتز را لیست کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

روش اول: در اینجا دوبار از متد Contains استفاده شده‌است:
var members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => member.Telephone.Contains("(")
                                                    && member.Telephone.Contains(")"))
                                    .ToList();
با این خروجی:


روش دوم: اگر می‌خواهیم کنترل بیشتری را بر روی خروجی نهایی LIKE تولیدی داشته باشیم، می‌توان از متد سفارشی استاندارد EF.Functions.Like استفاده کرد که از حروف wild cards نیز پشتیبانی می‌کند:
members = context.Members
                                    .Select(member => new { member.MemId, member.Telephone })
                                    .Where(member => EF.Functions.Like(member.Telephone, "%[()]%"))
                                    .ToList();
با این خروجی:



مثال 5: کد پستی‌ها 5 رقمی هستند. گزارشی را تهیه کنید که در آن اگر کدپستی کمتر از 5 رقم بود، ابتدای آن با صفر شروع شود.
هدف اصلی از این مثال، اعمال متد PadLeft(5, '0') به خاصیت member.ZipCode است.

روش اول: EF-Core فعلا قابلیت ترجمه‌ی PadLeft(5, '0') را به معادل SQL آن‌را ندارد. به همین جهت مجبور هستیم ابتدا ZipCode‌ها را به صورت رشته‌ای بازگشت دهیم که در اینجا استفاده‌ی از Convert.ToString مجاز است.
با این خروجی:
SELECT   CONVERT (NVARCHAR (MAX), [m].[ZipCode]) AS [Zip]
FROM     [Members] AS [m]
ORDER BY CONVERT (NVARCHAR (MAX), [m].[ZipCode]);
 سپس می‌توان بر روی لیست آماده‌ی موجود در حافظه، از LINQ to Objects استفاده کرد و در این حالت دسترسی کاملی به تمام امکانات زبان #C وجود دارد:
var members = context.Members
                                    .Select(member => new { ZipCode = Convert.ToString(member.ZipCode) })
                                    .OrderBy(m => m.ZipCode)
                                    .ToList();
// Now using LINQ to Objects
members = members.Select(member => new { ZipCode = member.ZipCode.PadLeft(5, '0') })
                                                    .OrderBy(m => m.ZipCode)
                                                    .ToList();

روش دوم: SQL Server به همراه تابع استانداردی به نام Replicate است که از آن می‌توان برای شبیه سازی PadLeft، بدون متوسل شدن به LINQ to Objects، استفاده کرد. اما چون این تابع هنوز به EF-Core معرفی نشده‌است، نیاز است خودمان اینکار را انجام دهیم. در این روش، از متد SqlDbFunctionsExtensions.SqlReplicate استفاده می‌شود. روش تعریف این نوع متدها را در مطلب «امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core» پیشتر بررسی کرده‌ایم که برای مثال در اینجا چنین شکلی را پیدا می‌کند:
namespace EFCorePgExercises.Utils
{
    public static class SqlDbFunctionsExtensions
    {
        public static string SqlReplicate(string expression, int count)
            => throw new InvalidOperationException($"{nameof(SqlReplicate)} method cannot be called from the client side.");

        private static readonly MethodInfo _sqlReplicateMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlReplicate),
                new[] { typeof(string), typeof(int) }
            );


        public static void AddCustomSqlFunctions(this ModelBuilder modelBuilder)
        {
            modelBuilder.HasDbFunction(_sqlReplicateMethodInfo)
                .HasTranslation(args =>
                {
                    return SqlFunctionExpression.Create("REPLICATE",
                        args,
                        _sqlReplicateMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
پس از آن فقط کافی است متد AddCustomSqlFunctions را به Context برنامه معرفی کنیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
         // ...

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
         // ...
            modelBuilder.AddCustomSqlFunctions();
         // ...
        }
    }
}
اکنون می‌توان از تابع SqlDbFunctionsExtensions.SqlReplicate جهت شبیه سازی PadLeft به صورت زیر استفاده کرد:
var newMembers = context.Members
                                        .Select(member => new
                                        {
                                            ZipCode =
                                                SqlDbFunctionsExtensions.SqlReplicate(
                                                    "0", 5 - Convert.ToString(member.ZipCode).Length)
                                                + member.ZipCode
                                        })
                        .OrderBy(m => m.ZipCode)
                        .ToList();
با این خروجی:



مثال 6: اولین حرف نام خانوادگی کاربران در کل ردیف‌های جدول چندبار تکرار شده‌است؟
این گزارش باید به همراه ستون‌های letter,  count باشد.

var members = context.Members
                                    .Select(member => new { Letter = member.Surname.Substring(0, 1) })
                                    .GroupBy(m => m.Letter)
                                    .Select(g => new
                                    {
                                        Letter = g.Key,
                                        Count = g.Count()
                                    })
                                    .OrderBy(r => r.Letter)
                                    .ToList();
هدف از این مثال بیان مجاز بودن استفاده‌ی از متد Substring بر روی خواص رشته‌ای است که EF-Core امکان ترجمه‌ی آن‌ها را به کدهای SQL دارد.
با این خروجی:



مثال 7: حروف '-','(',')', ' ' را از شماره تلفن‌ها حذف کنید.
این گزارش باید به همراه ستون‌های memid, telephone باشد.

بانک اطلاعاتی PostgreSQL به همراه تابع استاندارد regexp_replace است و می‌توان از آن برای حل یک چنین مسایلی استفاده کرد:
select memid, regexp_replace(telephone, '[^0-9]', '', 'g') as telephone
from members
order by memid;
اما SQL Server هنوز هم به همراه یک چنین تابعی نیست. بنابراین از روش زیر نیز می‌توان مثال جاری را حل کرد:
var members = context.Members
                                .Select(member => new
                                {
                                    member.MemId,
                                    Telephone = member.Telephone.Replace("-", "")
                                                        .Replace("(", "")
                                                        .Replace(")", "")
                                                        .Replace(" ", "")
                                })
                                .OrderBy(r => r.MemId)
                                .ToList();
با این خروجی:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
Tuple در دات نت 4

نوع جدیدی در دات نت 4 به نام Tuple اضافه شده است که در این مطلب به بررسی آن خواهیم پرداخت.
در ریاضیات، Tuple به معنای لیست مرتبی از اعضاء با تعداد مشخص است. Tuple در زبان‌های برنامه نویسی Dynamic مانند اف شارپ، Perl ، LISP و بسیاری موارد دیگر مطلب جدیدی نیست. در زبان‌های dynamic برنامه نویس‌ها می‌توانند متغیرها را بدون معرفی نوع آن‌ها تعریف کنند. اما در زبان‌های Static مانند سی شارپ، برنامه نویس‌ها موظفند نوع متغیرها را پیش از کامپایل آن‌ها معرفی کنند که هر چند کار کد نویسی را اندکی بیشتر می‌کند اما به این صورت شاهد خطاهای کمتری نیز خواهیم بود (البته سی شارپ 4 این مورد را با معرفی واژه‌ی کلیدی dynamic تغییر داده است).
برای مثال در اف شارپ داریم:
let data = (“John Doe”, 42)

که سبب ایجاد یک tuple که المان اول آن یک رشته و المان دوم آن یک عدد صحیح است می‌شود. اگر data را بخواهیم نمایش دهیم خروجی آن به صورت زیر خواهد بود:
printf “%A” data
// Output: (“John Doe”,42)

در دات نت 4 فضای نام جدیدی به نام System.Tuple معرفی شده است که در حقیقت ارائه دهنده‌ی نوعی جنریک می‌باشد که توانایی در برگیری انواع مختلفی را دارا است :
public class Tuple<T1>
up to:
public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>

همانند آرایه‌ها، اندازه‌ی Tuples نیز پس از تعریف قابل تغییر نیستند (immutable). اما تفاوت مهم آن با یک آرایه در این است که اعضای آن می‌توانند نوع‌های کاملا متفاوتی داشته باشند. همچنین تفاوت مهم آن با یک ArrayList یا آرایه‌ای از نوع Object، مشخص بودن نوع هر یک از اعضاء آن است که type safety بیشتری را به همراه خواهد داشت و کامپایلر می‌تواند در حین کامپایل دقیقا مشخص نماید که اطلاعات دریافتی از نوع صحیحی هستند یا خیر.

یک مثال کامل از Tuples را در کلاس زیر ملاحظه خواهید نمود:

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

namespace TupleTest
{
class TupleCS4
{
#region Methods (4)

// Public Methods (4)

public static Tuple<string, string> GetFNameLName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new NullReferenceException("name is empty.");

var nameParts = name.Split(',');

if (nameParts.Length != 2)
throw new FormatException("name must contain ','");

return Tuple.Create(nameParts[0], nameParts[1]);
}

public static void PrintSelectedTuple()
{
var list = new List<Tuple<string, int>>
{
new Tuple<string, int>("A", 1),
new Tuple<string, int>("B", 2),
new Tuple<string, int>("C", 3)
};

var item = list.Where(x => x.Item2 == 2).SingleOrDefault();
if (item != null)
Console.WriteLine("Selected Item1: {0}, Item2: {1}",
item.Item1, item.Item2);
}

public static void PrintTuples()
{
var tuple1 = new Tuple<int>(12);
Console.WriteLine("tuple1 contains: item1:{0}", tuple1.Item1);

var tuple2 = Tuple.Create("Item1", 12);
Console.WriteLine("tuple2 contains: item1:{0}, item2:{1}",
tuple2.Item1, tuple2.Item2);

var tuple3 = Tuple.Create(new DateTime(2010, 5, 6), "Item2", 20);
Console.WriteLine("tuple3 contains: item1:{0}, item2:{1}, item3:{2}",
tuple3.Item1, tuple3.Item2, tuple3.Item3);
}

public static void Tuple8()
{
var tup =
new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>
(1, 2, 3, 4, 5, 6, 7, new Tuple<int, int>(8, 9));

Console.WriteLine("tup.Rest Item1: {0}, Item2: {1}",
tup.Rest.Item1,tup.Rest.Item2);
}

#endregion Methods
}
}

using System;

namespace TupleTest
{
class Program
{
static void Main()
{
var data = TupleCS4.GetFNameLName("Vahid, Nasiri");
Console.WriteLine("Data Item1:{0} & Item2:{1}",
data.Item1, data.Item2);

TupleCS4.PrintTuples();

TupleCS4.PrintSelectedTuple();

TupleCS4.Tuple8();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

توضیحات :
- روش‌های متفاوت ایجاد Tuples را در متد PrintTuples می‌توانید ملاحظه نمائید. همچنین نحوه‌ی دسترسی به مقادیر هر کدام از اعضاء نیز مشخص شده است.
- کاربرد مهم Tuples در متد GetFNameLName نمایش داده شده است؛ زمانیکه نیاز است تا چندین خروجی از یک تابع داشته باشیم. به این صورت دیگر نیازی به تعریف آرگومان‌هایی به همراه واژه کلیدی out نخواهد بود یا دیگر نیازی نیست تا یک شیء جدید را ایجاد کرده و خروجی را به آن نسبت دهیم. به همان سادگی زبان‌های dynamic در اینجا نیز می‌توان یک tuple را ایجاد و استفاده کرد.
- بدیهی است از Tuples در یک لیست جنریک و یا حالات دیگر نیز می‌توان استفاده کرد. مثالی از این دست را در متد PrintSelectedTuple ملاحظه خواهید نمود. ابتدا یک لیست جنریک از Tuple ایی با دو عضو تشکیل شده است. سپس با استفاده از امکانات LINQ ، عضوی که آیتم دوم آن مساوی 2 است یافت شده و سپس المان‌های آن نمایش داده می‌شود.
- نکته‌ی دیگری را که حین کار با Tuples می‌توان در نظر داشت این است که اعضای آن حداکثر شامل 8 عضو می‌توانند باشند که عضو آخر باید یک Tuple تعریف گردد و بدیهی است این Tuple‌ نیز می‌تواند شامل 8 عضو دیگر باشد و الی آخر که نمونه‌ای از آن را در متد Tuple8 می‌توان مشاهده کرد.

مطالب
نکاتی در مورد استفاده از توابع تجمعی در Entity framework
استفاده از Aggregate functions یا توابع تجمعی چه در زمان SQL نویسی مستقیم و یا در حالت استفاده از LINQ to Entities نیاز به ملاحظات خاصی دارد که عدم رعایت آن‌ها سبب کرش برنامه در زمان موعد خواهد شد. در ادامه تعدادی از این موارد را مرور خواهیم کرد.

ابتدا مدل‌های برنامه را در نظر بگیرید که از یک صورتحساب، به همراه ریز قیمت‌های آیتم‌های مرتبط با آن تشکیل شده است:
    public class Bill
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<Transaction> Transactions { set; get; }
    }

    public class Transaction
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }
        public int Amount { set; get; }

        [ForeignKey("BillId")]
        public virtual Bill Bill { set; get; }
        public int BillId { set; get; }
    }
در ادامه این کلاس‌ها را در معرض دید EF Code first قرار می‌دهیم:
    public class MyContext : DbContext
    {
        public DbSet<Bill> Bills { get; set; }
        public DbSet<Transaction> Transactions { get; set; }
    }
همچنین تعدادی رکورد اولیه را نیز جهت انجام آزمایشات به بانک اطلاعاتی متناظر، اضافه خواهیم کرد:
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var bill1 = new Bill { Name = "bill-1" };
            context.Bills.Add(bill1);

            for (int i = 0; i < 11; i++)
            {
                context.Transactions.Add(new Transaction
                {
                    AddDate = DateTime.Now.AddDays(-i),
                    Amount = 1000000000 + i,
                    Bill = bill1
                });
            }
            base.Seed(context);
        }
    }
در اینجا به عمد از ارقام بزرگ استفاده شده است تا نمایانگر عملکرد یک سیستم واقعی در طول زمان باشد.


اولین مثال: یک جمع ساده

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var context = new MyContext())
            {
                var sum = context.Transactions.Sum(x => x.Amount);
                Console.WriteLine(sum);
            }
        }
    }
ساده‌ترین نیازی را که در اینجا می‌توان مدنظر داشت، جمع کل تراکنش‌‌های سیستم است. به نظر شما خروجی کوئری فوق چیست؟
خروجی SQL کوئری فوق به نحو زیر است:
SELECT 
         [GroupBy1].[A1] AS [C1]
         FROM ( SELECT 
                    SUM([Extent1].[Amount]) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
                    )  AS [GroupBy1]
و خروجی واقعی آن استثنای زیر می‌باشد:
 Arithmetic overflow error converting expression to data type int.
بله. محاسبه ممکن نیست؛ چون جمع حاصل از بازه اعداد صحیح خارج شده است.

راه حل:
نیاز است جمع را بر روی Int64 بجای Int32 انجام دهیم:
var sum2 = context.Transactions.Sum(x => (Int64)x.Amount);

SELECT 
      [GroupBy1].[A1] AS [C1]
         FROM ( SELECT 
                    SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
               )  AS [GroupBy1]                  


مثال دوم: سیستم باید بتواند با نبود رکوردها نیز صحیح کار کند
برای نمونه کوئری زیر را بر روی بازه‌ا‌ی که سیستم عملکرد نداشته است، در نظر بگیرید:
var date = DateTime.Now.AddDays(10);
var sum3 = context.Transactions
                  .Where(x => x.AddDate > date)  
                  .Sum(x => (Int64)x.Amount);               
یک چنین خروجی SQL ایی دارد:
SELECT 
     [GroupBy1].[A1] AS [C1]
        FROM ( SELECT 
                    SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                    FROM [dbo].[Transactions] AS [Extent1]
                    WHERE [Extent1].[AddDate] > @p__linq__0
              )  AS [GroupBy1]
اما در سمت کدهای ما با خطای زیر متوقف می‌شود:
The cast to value type 'Int64' failed because the materialized value is null.
Either the result type's generic parameter or the query must use a nullable type.
راه حل: استفاده از نوع‌های nullable در اینجا ضروری است:
var date = DateTime.Now.AddDays(10);
var sum3 = context.Transactions
                  .Where(x => x.AddDate > date)
                  .Sum(x => (Int64?)x.Amount) ?? 0;
به این ترتیب، خروجی صفر را بدون مشکل، دریافت خواهیم کرد.

مثال سوم: حالت‌های خاص استفاده از خواص راهبری
کوئری زیر را در نظر بگیرید:
 var sum4 = context.Bills.First().Transactions.Sum(x => (Int64?)x.Amount) ?? 0;
در اینجا قصد داریم جمع تراکنش‌های صورتحساب اول را بدست بیاوریم که از طریق استفاده از خاصیت راهبری Transactions کلاس Bill، به نحو فوق میسر شده است. به نظر شما خروجی SQL آن به چه صورتی است؟
SELECT 
     [Extent1].[Id] AS [Id], 
     [Extent1].[AddDate] AS [AddDate], 
     [Extent1].[Amount] AS [Amount], 
     [Extent1].[BillId] AS [BillId]
   FROM [dbo].[Transactions] AS [Extent1]
   WHERE [Extent1].[BillId] = @EntityKeyValue1
بله! در اینجا خبری از Sum نیست. ابتدا کل اطلاعات دریافت شده و سپس جمع و منهای نهایی در سمت کلاینت بر روی آن‌ها انجام می‌شود؛ که بسیار ناکارآمد است. (قرار است این مورد ویژه، در نگارش‌های بعدی بهبود یابد)
راه حل کنونی:
var entry = context.Bills.First();
var sum5 = context.Entry(entry).Collection(x => x.Transactions).Query().Sum(x => (Int64?)x.Amount) ?? 0;
در اینجا باید از روش خاصی که مشاهده می‌کنید جهت کار با خواص راهبری استفاده کرد و نکته اصلی آن استفاده از متد Query است. حاصل کوئری LINQ فوق اینبار SQL مطلوب زیر است که سمت سرور عملیات جمع را انجام می‌دهد و نه سمت کلاینت:
SELECT 
    [GroupBy1].[A1] AS [C1]
     FROM ( SELECT 
               SUM( CAST( [Extent1].[Amount] AS bigint)) AS [A1]
                   FROM [dbo].[Transactions] AS [Extent1]
                    WHERE [Extent1].[BillId] = @EntityKeyValue1
            )  AS [GroupBy1]                  


نکاتی که در اینجا ذکر شدند در مورد تمام توابع تجمعی مانند Sum، Count، Max و Min و غیره صادق هستند و باید به آن‌ها نیز دقت داشت.
مطالب
تعریف نوع جنریک به صورت متغیر

در تهیه مثال Auto Mapping به کمک امکانات توکار NH 3.2 به این مورد نیاز پیدا کردم:
بتوان نوع متد جنریک را به صورت متغیر تعریف کرد و این نوع در زمان کامپایل برنامه مشخص نباشد. مثلا چیزی شبیه به این مثال:

using System;

namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
}

class Program
{
static void Main(string[] args)
{
var type = typeof(Nullable<int>);
TestGenerics.Print<type>(1);
}
}
}

این نوع فراخوانی متد Print در دات نت به صورت پیش فرض غیرمجاز است و نوع جنریک را نمی‌توان به صورت متغیر معرفی کرد.
که البته این هم راه حل دارد و به کمک Reflection قابل حل است:

using System;

namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}
}

class Program
{
static void Main(string[] args)
{
var nullableIntType = typeof(Nullable<>).MakeGenericType(typeof(int));
var method = typeof(TestGenerics).GetMethod("Print");
var genericMethod = method.MakeGenericMethod(new[] { nullableIntType });
genericMethod.Invoke(null, new object[] { 1 });
}
}
}

دو متد MakeGenericType و MakeGenericMethod برای ساخت پویای نوع‌های جنریک و همچنین ارسال آن‌ها به متدهای جنریک در دات نت وجود دارند که مثالی از نحوه استفاده از آن‌ها را در بالا ملاحظه می‌کنید.

مثال دوم:
اگر کلاس TestGenerics نسخه غیرجنریک متد Print را هم داشت، ‌چطور؟ مثلا:

class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}

public static void Print(object data)
{
Console.WriteLine("Print");
}
}

اینبار اگر برنامه فوق را اجرا کنیم، پیغام Ambiguous match found را حین فراخوانی GetMoethod دریافت خواهیم کرد؛ چون دو متد با یک نام در کلاس یاد شده وجود دارند. برای حل این مشکل باید به نحو زیر عمل کرد:

using System;
using System.Linq;

namespace GenericsSample
{
class TestGenerics
{
public static void Print<T>(T data)
{
Console.WriteLine("Print<T>");
}

public static void Print(object data)
{
Console.WriteLine("Print");
}
}

class Program
{
static void Main(string[] args)
{
var nullableIntType = typeof(Nullable<>).MakeGenericType(typeof(int));
var method = typeof(TestGenerics).GetMethods()
.First(x => x.Name == "Print" && (x.GetParameters()[0]).ParameterType.IsGenericParameter);
var genericMethod = method.MakeGenericMethod(new[] { nullableIntType });
genericMethod.Invoke(null, new object[] { 1 });
}
}
}

GetMethods تمام متدها را بازگشت داده و سپس بر اساس متادیتای متدها، ‌می‌توان تشخیص داد که کدام یک جنریک است.

مطالب
استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First
یکی از روش‌های تهیه‌ی برنامه‌های چند مستاجری، ایجاد بانک‌های اطلاعاتی مستقلی به ازای هر مشتری است؛ یا نمونه‌ی دیگر آن، برنامه‌هایی هستند که اطلاعات هر سال را در یک بانک اطلاعاتی جداگانه نگه‌داری می‌کنند. در ادامه قصد داریم، نحوه‌ی کار با این بانک‌های اطلاعاتی را به صورت همزمان، توسط EF Code first و در حالت استفاده از الگوی واحد کار و تزریق وابستگی‌ها، به همراه فعال سازی خودکار مباحث migrations و به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، بررسی کنیم.


مشخص سازی رشته‌های متفاوت اتصالی

فرض کنید برنامه‌ی جاری شما قرار است از دو بانک اطلاعاتی مشخص استفاده کند که تعاریف رشته‌های اتصالی آن‌ها در وب کانفیگ به صورت ذیل مشخص شده‌اند:
  <connectionStrings>
    <clear />
    <add name="Sample07Context" connectionString="Data Source=(local);Initial Catalog=TestDbIoC;Integrated Security = true" providerName="System.Data.SqlClient" />
    <add name="Database2012" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" />
  </connectionStrings>
البته، ذکر این مورد کاملا اختیاری است و می‌توان رشته‌های اتصالی را به صورت پویا نیز در زمان اجرا مشخص و مقدار دهی کرد.


تغییر Context برنامه جهت پذیرش رشته‌های اتصالی پویای قابل تغییر در زمان اجرا

اکنون که قرار است کاربران در حین ورود به برنامه، بانک اطلاعاتی مدنظر خود را انتخاب کنند و یا سیستم قرار است به ازای کاربری خاص، رشته‌ی اتصالی خاص او را به Context ارسال کند، نیاز است Context برنامه را به صورت ذیل تغییر دهیم:
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DomainClasses;
 
namespace EF_Sample07.DataLayer.Context
{
    public class Sample07Context : DbContext, IUnitOfWork
    {
        public DbSet<Category> Categories { set; get; }
        public DbSet<Product> Products { set; get; }
 
        /// <summary>
        /// It looks for a connection string named Sample07Context in the web.config file.
        /// </summary>
        public Sample07Context()
            : base("Sample07Context")
        {
        }
 
        /// <summary>
        /// To change the connection string at runtime. See the SmObjectFactory class for more info.
        /// </summary>
        public Sample07Context(string connectionString)
            : base(connectionString)
        {
            //Note: defaultConnectionFactory in the web.config file should be set.
        }
 
 
        public void SetConnectionString(string connectionString)
        {
            this.Database.Connection.ConnectionString = connectionString;
        }
    }
}
در اینجا دو متد سازنده را مشاهده می‌کنید. سازنده‌ی پیش فرض، از رشته‌ای اتصالی با نامی مساوی Sample07Context استفاده می‌کند و سازنده‌ی دوم، امکان پذیرش یک رشته‌ی اتصالی پویا را دارد. مقدار پارامتر ورودی آن می‌تواند نام رشته‌ی اتصالی و یا حتی مقدار کامل رشته‌ی اتصالی باشد. حالت پذیرش نام رشته‌ی اتصالی زمانی مفید است که همانند مثال ابتدای بحث، این نام‌ها را پیشتر در فایل کانفیگ برنامه ثبت کرده باشید و حالت پذیرش مقدار کامل رشته‌ی اتصالی، جهت مقدار دهی پویای آن بدون نیاز به ثبت اطلاعاتی در فایل کانفیگ برنامه مفید است.

یک متد دیگر هم در اینجا در انتهای کلاس به نام SetConnectionString تعریف شده‌است. از این متد در حین ورود کاربر به سایت می‌توان استفاده کرد. برای مثال حداقل دو نوع طراحی را می‌توان درنظر گرفت:
الف) کاربر با برنامه‌ای کار می‌کند که به ازای سال‌های مختلف، بانک‌های اطلاعاتی مختلفی دارد و در ابتدای ورود، یک drop down انتخاب سال کاری برای او درنظر گرفته شده‌است (علاوه بر سایر ورودی‌های استانداردی مانند نام کاربری و کلمه‌ی عبور). در این حالت بهتر است متد SetConnectionString نام رشته‌ی اتصالی را بر اساس سال انتخابی، در حین لاگین دریافت کند که اطلاعات آن در فایل کانفیگ سایت پیشتر مشخص شده‌است.
ب) کاربر یا مشتری پس از ورود به سایت، نیاز است صرفا از بانک اطلاعاتی خاص خودش استفاده کند. بنابراین اطلاعات تعریف کاربران و مشتری‌ها در یک بانک اطلاعاتی مجزا قرار دارند و پس از لاگین، نیاز است رشته‌ی اتصالی او به صورت پویا از بانک اطلاعاتی خوانده شده و سپس توسط متد SetConnectionString تنظیم گردد.


مدیریت سشن‌های رشته‌ی اتصالی جاری

پس از اینکه کاربر، در حین ورود مشخص کرد که از چه بانک اطلاعاتی قرار است استفاده کند و یا اینکه برنامه بر اساس اطلاعات ثبت شده‌ی او تصمیم‌گیری کرد که باید از کدام رشته‌ی اتصالی استفاده کند، نگهداری این رشته‌ی اتصالی نیاز به سشن دارد تا به ازای هر کاربر متصل به سایت منحصربفرد باشد. در مورد مدیریت سشن‌ها در برنامه‌های وب، از نکات مطرح شده‌ی در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» استفاده خواهیم کرد:
using System;
using System.Threading;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;
using StructureMap.Web;
using StructureMap.Web.Pipeline;
 
namespace EF_Sample07.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        public static void HttpContextDisposeAndClearAll()
        {
            HttpContextLifecycle.DisposeAndClearAll();
        }
 
        private static Container defaultContainer()
        {
            return new Container(ioc =>
            {
                // session manager setup
                ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>();
                ioc.For<HttpSessionStateBase>()
                   .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session));
 
                ioc.For<IUnitOfWork>()
                   .HybridHttpOrThreadLocalScoped()
                   .Use<Sample07Context>()
                    // Remove these 2 lines if you want to use a connection string named Sample07Context, defined in the web.config file.
                   .Ctor<string>("connectionString")
                   .Is(ctx => getCurrentConnectionString(ctx));
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.Policies.SetAllProperties(properties =>
                {
                    properties.OfType<IUnitOfWork>();
                    properties.OfType<ICategoryService>();
                    properties.OfType<IProductService>();
                    properties.OfType<ISessionProvider>();
                });
            });
        }
 
        private static string getCurrentConnectionString(IContext ctx)
        {
            if (HttpContext.Current != null)
            {
                // this is a web application
                var sessionProvider = ctx.GetInstance<ISessionProvider>();
                var connectionString = sessionProvider.Get<string>("CurrentConnectionString");
                if (string.IsNullOrWhiteSpace(connectionString))
                {
                    // It's a default connectionString.
                    connectionString = "Database2012";
                    // this session value should be set during the login phase
                    sessionProvider.Store("CurrentConnectionStringName", connectionString);
                }
 
                return connectionString;
            }
            else
            {
                // this is a desktop application, so you can store this value in a global static variable.
                return "Database2012";
            }
        }
    }
}
در اینجا نحوه‌ی پویا سازی تامین رشته‌ی اتصالی را مشاهده می‌کنید. در مورد اینترفیس ISessionProvider و کلاس پایه HttpSessionStateBase پیشتر در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» بحث شد.
نکته‌ی مهم این تنظیمات، قسمت مقدار دهی سازنده‌ی کلاس Context برنامه به صورت پویا توسط IoC Container جاری است. در اینجا هر زمانیکه قرار است وهله‌ای از Sample07Context ساخته شود، از سازنده‌ی دوم آن که دارای پارامتری به نام connectionString است، استفاده خواهد شد. همچنین مقدار آن به صورت پویا از متد getCurrentConnectionString که در انتهای کلاس تعریف شده‌است، دریافت می‌گردد.
در این متد ابتدا مقدار HttpContext.Current بررسی شده‌است. این مقدار اگر نال باشد، یعنی برنامه‌ی جاری یک برنامه‌ی دسکتاپ است و مدیریت رشته‌ی اتصالی جاری آن‌را توسط یک خاصیت Static یا Singleton تعریف شده‌ی در برنامه نیز می‌توان تامین کرد. از این جهت که در هر زمان، تنها یک کاربر در App Domain جاری برنامه‌ی دسکتاپ می‌تواند وجود داشته باشد و Singleton یا Static تعریف شدن اطلاعات رشته‌ی اتصالی، مشکلی را ایجاد نمی‌کند. اما در برنامه‌های وب، چندین کاربر در یک App Domain به سیستم وارد می‌شوند. به همین جهت است که مشاهده می‌کنید در اینجا از تامین کننده‌ی سشن، برای نگهداری اطلاعات رشته‌ی اتصالی جاری کمک گرفته شده‌است.

کلید این سشن نیز در این مثال مساوی CurrentConnectionStringName تعریف شده‌است. بنابراین در حین لاگین موفقیت آمیز کاربر، دو مرحله‌ی زیر باید طی شوند:
 sessionProvider.Store("CurrentConnectionString", "Sample07Context");
uow.SetConnectionString(WebConfigurationManager.ConnectionStrings[_sessionProvider.Get<string>("CurrentConnectionString")].ConnectionString);
ابتدا باید سشن CurrentConnectionStringName به بانک اطلاعاتی انتخابی کاربر تنظیم شود. برای نمونه در این مثال خاص، از نام رشته‌ی اتصالی مشخص شده‌ی در وب کانفیگ برنامه (مثال ابتدای بحث) به نام Sample07Context استفاده شده‌است.
سپس از متد SetConnectionString برای خواندن مقدار نام مشخص شده در سشن CurrentConnectionStringName کمک گرفته‌ایم. هرچند سازنده‌ی کلاس Context برنامه، هر دو حالت استفاده از نام رشته‌ی اتصالی و یا مقدار کامل رشته‌ی اتصالی را پشتیبانی می‌کند، اما خاصیت this.Database.Connection.ConnectionString تنها رشته‌ی کامل اتصالی را می‌پذیرد (بکار رفته در متد SetConnectionString).

تا اینجا کار پویا سازی انتخاب و استفاده از رشته‌ی اتصالی برنامه به پایان می‌رسد. هر زمانیکه قرار است Context برنامه توسط IoC Container نمونه سازی شود، به متد getCurrentConnectionString رجوع کرده و مقدار رشته‌ی اتصالی را از سشن تنظیم شده‌‌ای به نام CurrentConnectionStringName دریافت می‌کند. سپس از مقدار آن جهت مقدار دهی سازنده‌ی دوم کلاس Context استفاده خواهد کرد.


مدیریت migrations خودکار برنامه در حالت استفاده از چندین بانک اطلاعاتی

یکی از مشکلات کار با برنامه‌های چند دیتابیسی، به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، پس از تغییری در ساختار مدل‌های برنامه است. از این جهت که اگر تمام بانک‌های اطلاعاتی به روز نشوند، کوئری‌های جدید برنامه که از خواص و فیلدهای جدید استفاده می‌کنند، دیگر کار نخواهند کرد. پویا سازی اعمال این تغییرات را می‌توان به صورت ذیل انجام داد:
using System;
using System.Data.Entity;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.IoCConfig;
 
namespace EF_Sample07.WebFormsAppSample
{
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            initDatabases();
        }
 
        private static void initDatabases()
        {
            // defined in web.config
            string[] connectionStringNames =
            {
                "Sample07Context",
                "Database2012"
            };
 
            foreach (var connectionStringName in connectionStringNames)
            {
                Database.SetInitializer(
                    new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>(connectionStringName));
 
                using (var ctx = new Sample07Context(connectionStringName))
                {
                    ctx.Database.Initialize(force: true);
                }
            }
        }
 
        void Application_EndRequest(object sender, EventArgs e)
        {
            SmObjectFactory.HttpContextDisposeAndClearAll();
        } 
    }
}
نکته‌ی مهمی که در اینجا بکار گرفته شده‌است، مشخص سازی صریح سازنده‌ی شیء MigrateDatabaseToLatestVersion است. به صورت معمول در اکثر برنامه‌های تک دیتابیسی، نیازی به مشخص سازی پارامتر سازنده‌ی این کلاس نیست و در این حالت از سازنده‌ی بدون پارامتر کلاس Context برنامه استفاده خواهد شد. اما اگر سازنده‌ی آن‌را مشخص کنیم، به صورت خودکار از متد سازنده‌ای در کلاس Context استفاده می‌کند که پارامتر رشته‌ی اتصالی را به صورت پویا می‌پذیرد.
در این مثال خاص، متد initDatabases در حین آغاز برنامه فراخوانی شده‌است. منظور این است که اینکار در طول عمر برنامه تنها کافی است یکبار انجام شود و پس از آن است که EF Code first می‌تواند از رشته‌های اتصالی متفاوتی که به آن ارسال می‌شود، بدون مشکل استفاده کند. زیرا اطلاعات نگاشت کلاس‌های مدل برنامه به جداول بانک اطلاعاتی به این ترتیب است که کش می‌شوند و یا بر اساس کلاس Configuration به صورت خودکار به بانک اطلاعاتی اعمال می‌گردند.


کدهای کامل این مثال را که در حقیقت نمونه‌ی بهبود یافته‌ی مطلب «EF Code First #12» است، از اینجا می‌توانید دریافت کنید:
UoW-Sample
مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت دوم
در قسمت قبل با نحوه اجرای پرس و جو آشنا شدید و همچنین به بررسی متدهای Find و Single و First و تفاوت‌های آنها پرداختیم. در این قسمت با خصوصیت Local و متد Load آشنا خواهیم شد. همانطور که در قسمت قبل دیدید، مقادیر اولیه‌ای برای Database و جداولمان مشخص کردیم. برای جدول Customer این داده‌ها را داشتیم:
 FamilyName  ID
 Nasiri Vahid یک مقدار Guid
 Akbari Mohsen یک مقدار Guid 
 Jamshidi Mohsen یک مقدار Guid 
ID توسط Database تولید می‌شوند به همین دلیل از ذکر مقداری مشخص خودداری شده است.
به کد زیر دقت کنید:
private static void Query7()
{
    using (var context = new StoreDbContext())
    {
        // Add
        context.Customers.Add(new Customer { Name = "Ali", Family = "Jamshidi" });

        // change
        var customer1 = context.Customers.Single(c => c.Family == "Jamshidi");
        customer1.Name = "Mohammad";

        // Remove
        var customer2 = context.Customers.Single(c => c.Family == "Akbari");
        context.Customers.Remove(customer2);

        var customers = context.Customers.Where(c => c.Name != "Vahid");

        foreach (var cust in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", cust.Name, cust.Family)
        }
    }
}
همانطور که مشاهده می‌کنید عمل اضافه، تغییر و حذف روی Customer انجام شده ولی هنوز هیچ تغییری در Database ذخیره نشده است. آخرین پرس و جو چه نتیجه ای را دربر خواهد داشت؟
 Customer Name: Mohammad, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari
بله، فقط تغییر یک موجودیت در نظر گرفته شده است ولی اضافه و حذف نه! 
نتیجه مهمی که حاصل می‌شود این است که در پرس و جوهایی که روی Database اجرا می‌شوند سه مورد را باید در نظر داشت:
  • داده هایی که اخیرا به DbContext اضافه شده‌اند ولی هنوز در Database ذخیره نشده‌اند، درنظر گرفته نخواهند شد.
  • داده هایی که در DbContext حذف شده‌اند ولی در Database  هستند، در نتیجه پرس و جو خواهند بود.
  • داده هایی که قبلا از database توسط پرس و جوی دیگری گرفته شده و تغییر کرده‌اند، آن تغییرات در نتیجه پرس و جو موثر خواهند بود.
پس پرس و جو‌های LINQ ابتدا روی database انجام می‌شوند و id‌های بازگشت داده شده با id‌های موجود در DbContext مطابقت داده می‌شوند یا در DbContext وجود دارند که در این صورت آن موجودیت بازگشت داده می‌شود یا وجود ندارند که در این صورت موجودیتی که از Database خوانده شده، بازگشت داده می‌شوند.
برای درک بیشتر کد زیر را در نظر بگیرید:
private static void Query7_1()
{
    using (var context = new StoreDbContext())
    {
        // Add
        context.Customers.Add(new Customer { Name = "Ali", Family = "Jamshidi" });

        // change
        var customer1 = context.Customers.Single(c => c.Family == "Jamshidi");
        customer1.Name = "Vahid";

        // Remove
        var customer2 = context.Customers.Single(c => c.Family == "Akbari");
        context.Customers.Remove(customer2);

        var customers = context.Customers.Where(c => c.Name != "Vahid");

        foreach (var cust in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", cust.Name, cust.Family)
        }
    }
}
این کد همان کد قبلی است اما نام customer1 در DbContext (که Mohsen بوده در Database) به Vahid تغییر کرده و پرس و جو روی نام هایی زده شده است که Vahid نباشند خروجی به صورت زیر خواهد بود:
 Customer Name: Vahid, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari
 Vahid در خروجی آمده در صورتیکه در شرط صدق نمی‌کند چراکه پرس و جو روی Database زده شده، جاییکه نام این مشتری Mohsen بوده اما موجودیتی بازگشت داده شده که دارای همان Id هست اما در DbContext دستخوش تغییر شده است.

Local: همانطور که قبلا اشاره شد خصوصیتی از DbSet می‌باشد که شامل تمام داده هایی هست که:
  • اخیرا از database پرس و جو شده است (می‌تواند تغییر کرده یا نکرده باشد)
  • اخیرا به Context اضافه شده است (توسط متد Add)
دقت شود که Local شامل داده‌هایی که از database خوانده شده و از Context، حذف (Remove) شده‌اند، نمی‌باشد.
نوع این خصوصیت ObservableCollection می‌باشد که می‌توان از آن برای Binding در پروژه‌های ویندوزی استفاده کرد.
به کد زیر دقت کنید:
private static void Query8()
{
    using (var context = new StoreDbContext())
    {
        // Add
        context.Customers.Add(new Customer { Name = "Ali", Family = "Jamshidi" });

        // change
        var customer1 = context.Customers.Single(c => c.Family == "Jamshidi");
        customer1.Name = "Mohammad";

        // Remove
        var customer2 = context.Customers.Single(c => c.Family == "Akbari");
        context.Customers.Remove(customer2);

        var customers = context.Customers.Local;

        foreach (var cust in customers)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", cust.Name, cust.Family);
        }
    }
}
کد بالا شبیه به کد قبلی می‌باشد با این تفاوت که در انتها foreach روی Local زده شده است. خروجی به صورت زیر خواهد بود:
Customer Name: Ali, Customer Family: Jamshidi
Customer Name: Mohammad, Customer Family: Jamshidi
همانطور که ملاحظه می‌کنید Local شامل Ali Jamshidi که اخیرا اضافه شده (ولی در Database ذخیره نشده) و Mohammad Jamshidi که از Database خوانده شده و تغییر کرده، می‌باشد اما شامل Mohsen Akbari که از Database خوانده شده اما در Context حذف شده است، نمی‌باشد.
می‌توان روی Local نیز پرس و جوی اجرا کرد. در این صورت از پروایدر LINQ To Object استفاده خواهد شد و درنتیجه دست بازتر هست و تمام امکانات این پروایدر می‌توان استفاده کرد.

Load: یکی دیگر از مواردی که باعث اجرای پرس و جو می‌شود متد Load می‌باشد که یک Extension Method می‌باشد. این متد در حقیقت یک پیمایش روی پرس و جو انجام می‌دهد و باعث بارگذاری داده‌ها در Context می‌شود. مانند استفاده از ToList البته بدون ساختن List که سربار ایجاد می‌کند.
private static void Query9()
{
    using (var context = new StoreDbContext())
    {
        var customers = context.Customers.Where(c => c.Name == "Mohsen");
        customers.Load();

        foreach (var cust in context.Customers.Local)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", cust.Name, cust.Family);
        }
    }
    // Output:
    // Customer Name: Mohsen, Customer Family: Akbari
    // Customer Name: Mohsen, Customer Family: Jamshidi
}

مطالب
ویژگی Batching در EF Core
در EF 6.x به ازای هر عبارت insert/update/delete یکبار رفت و برگشت به بانک اطلاعاتی صورت می‌گیرد. به همین جهت کارآیی تعداد بالای ثبت، به روز رسانی و حذف رکوردها توسط آن پایین است. برای رفع این مشکل ویژگی Batching به EF Core اضافه شده‌است که توسط آن اینبار دسته‌ای از عبارات را به صورت یکجا و در طی یک رفت و برگشت، به سمت بانک اطلاعاتی ارسال می‌کند. به این ترتیب کارآیی و سرعت insert/update/delete آن به شدت افزایش خواهد یافت.


نحوه‌ی فعالسازی Batching در EF Core

Batching به صورت پیش فرض در EF Core بدون نیاز به هیچگونه تنظیم اضافه‌تری فعال است. اما اگر خواستید برای مثال، حالت پیش فرض EF 6.x را توسط آن شبیه سازی کنید، می‌توانید مقدار MaxBatchSize را به عدد 1 تنظیم نمائید (تا غیرفعال شود):
optionsBuilder.UseSqlServer(
   @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;",
   options => options.MaxBatchSize(1)
);

مقدار پیش فرض MaxBatchSize را در کلاس SqlServerModificationCommandBatch می‌توانید مشاهده کنید:
public class SqlServerModificationCommandBatch : AffectedCountModificationCommandBatch
    {
        private const int DefaultNetworkPacketSizeBytes = 4096;
        private const int MaxScriptLength = 65536 * DefaultNetworkPacketSizeBytes / 2;
        private const int MaxParameterCount = 2100;
        private const int MaxRowCount = 1000;
در اینجا MaxRowCount همان MaxBatchSize پیش فرض است که به عدد 1000 تنظیم شده‌است. بنابراین اگر تنظیم options => options.MaxBatchSize(1) را ذکر نکنید، به معنای ارسال 1000 تایی دستورات insert/update/delete در طی یک درخواست به سمت سرور است.


آیا محدودیتی هم در مورد عملیات Batching وجود دارد؟

SQL Server به ازای هر batch تنها 2100 پارامتر را پشتیبانی می‌کند. در این حالت EF Core به صورت خودکار یک چنین کوئری‌های حجیمی را به چند Batch جهت تنظیم این محدودیت تقسیم خواهد کرد و در نهایت برنامه به مشکلی بر نمی‌خورد.


یک آزمایش: Batching پیش فرض به چه صورتی کار می‌کند و چه اثری را دارد؟

کدهای کامل این آزمایش را از اینجا می‌توانید دریافت کنید: Batching.zip
در اینجا کلاس Blog را به همراه Context متناظر با آن مشاهده می‌کنید:
    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;"/*,
                options => options.MaxBatchSize(2)*/
                );
            optionsBuilder.EnableSensitiveDataLogging();
        }
    }
در ابتدا MaxBatchSize را تنظیم نخواهیم کرد. یعنی از همان عدد 1000 پیش فرض استفاده می‌شود. تنظیم EnableSensitiveDataLogging نیز سبب می‌شود تا لاگ نهایی تهیه شده جهت نمایش، پرمحتواتر شود.
در این حالت اگر به روز رسانی‌ها (2 مورد) و ثبت‌های ذیل (6 مورد) را انجام دهیم:
            using (var db = new BloggingContext())
            {
                db.GetService<ILoggerFactory>().AddProvider(new MyLoggerProvider());

                // Modify some existing blogs
                var existing = db.Blogs.ToArray();
                existing[0].Url = "http://sample.com/blogs/dogs";
                existing[1].Url = "http://sample.com/blogs/cats";

                // Insert some new blogs
                db.Blogs.Add(new Blog { Name = "The Horse Blog", Url = "http://sample.com/blogs/horses" });
                db.Blogs.Add(new Blog { Name = "The Snake Blog", Url = "http://sample.com/blogs/snakes" });
                db.Blogs.Add(new Blog { Name = "The Fish Blog", Url = "http://sample.com/blogs/fish" });
                db.Blogs.Add(new Blog { Name = "The Koala Blog", Url = "http://sample.com/blogs/koalas" });
                db.Blogs.Add(new Blog { Name = "The Parrot Blog", Url = "http://sample.com/blogs/parrots" });
                db.Blogs.Add(new Blog { Name = "The Kangaroo Blog", Url = "http://sample.com/blogs/kangaroos" });

                db.SaveChanges();
            }
یک چنین خروجی SQL ایی تولید می‌شود:
Executed DbCommand (41ms) [Parameters=[@p1='57', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='58', @p2='http://sample.com/blogs/cats' (Size = 4000), @p4='The Horse Blog' (Size = 4000), @p5='http://sample.com/blogs/horses' (Size = 4000), @p6='The Snake Blog' (Size = 4000), @p7='http://sample.com/blogs/snakes' (Size = 4000), @p8='The Fish Blog' (Size = 4000), @p9='http://sample.com/blogs/fish' (Size = 4000), @p10='The Koala Blog' (Size = 4000), @p11='http://sample.com/blogs/koalas' (Size = 4000), @p12='The Parrot Blog' (Size = 4000), @p13='http://sample.com/blogs/parrots' (Size = 4000), @p14='The Kangaroo Blog' (Size = 4000), @p15='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Blogs] SET [Url] = @p0
WHERE [BlogId] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Blogs] SET [Url] = @p2
WHERE [BlogId] = @p3;
SELECT @@ROWCOUNT;

DECLARE @inserted2 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p4, @p5, 0),
(@p6, @p7, 1),
(@p8, @p9, 2),
(@p10, @p11, 3),
(@p12, @p13, 4),
(@p14, @p15, 5)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted2;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted2 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];
در این دستورات موارد ذیل قابل توجه هستند:
- فقط یکبار Executed DbCommand مشاهده می‌شود.
- کل دستورات update و insert در طی یک درخواست و یک تراکنش به سمت بانک اطلاعاتی ارسال شده‌اند.
- ثبت دسته‌ای توسط merge using انجام شده‌است.
- در آخر نیز طبق معمول کار EF، شماره Idهای رکوردهای ثبت شده به سمت کلاینت بازگشت داده می‌شود.

در ادامه MaxBatchSize را به عدد 2 تنظیم می‌کنیم:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
   optionsBuilder.UseSqlServer(
     @"Server=(localdb)\mssqllocaldb;Database=Demo.Batching;Trusted_Connection=True;",
     options => options.MaxBatchSize(2)
    );
    optionsBuilder.EnableSensitiveDataLogging();
}
در این حالت اگر برنامه را اجرا کنیم، یک چنین خروجی قابل مشاهده خواهد بود:
Executed DbCommand (17ms) [Parameters=[@p1='65', @p0='http://sample.com/blogs/dogs' (Size = 4000), @p3='66', @p2='http://sample.com/blogs/cats' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Blogs] SET [Url] = @p0
WHERE [BlogId] = @p1;
SELECT @@ROWCOUNT;

UPDATE [Blogs] SET [Url] = @p2
WHERE [BlogId] = @p3;
SELECT @@ROWCOUNT;

Executed DbCommand (18ms) [Parameters=[@p0='The Horse Blog' (Size = 4000), @p1='http://sample.com/blogs/horses' (Size = 4000), @p2='The Snake Blog' (Size = 4000), @p3='http://sample.com/blogs/snakes' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];

Executed DbCommand (34ms) [Parameters=[@p0='The Fish Blog' (Size = 4000), @p1='http://sample.com/blogs/fish' (Size = 4000), @p2='The Koala Blog' (Size = 4000), @p3='http://sample.com/blogs/koalas' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];

Executed DbCommand (15ms) [Parameters=[@p0='The Parrot Blog' (Size = 4000), @p1='http://sample.com/blogs/parrots' (Size = 4000), @p2='The Kangaroo Blog' (Size = 4000), @p3='http://sample.com/blogs/kangaroos' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([BlogId] int, [_Position] [int]);
MERGE [Blogs] USING (
VALUES (@p0, @p1, 0),
(@p2, @p3, 1)) AS i ([Name], [Url], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([Name], [Url])
VALUES (i.[Name], i.[Url])
OUTPUT INSERTED.[BlogId], i._Position
INTO @inserted0;

SELECT [t].[BlogId] FROM [Blogs] t
INNER JOIN @inserted0 i ON ([t].[BlogId] = [i].[BlogId])
ORDER BY [i].[_Position];
در این دستورات موارد ذیل قابل توجه هستند:
- اینبار تعداد 4 دستور Executed DbCommand مشاهده می‌شود ( برای انجام 2 به روز رسانی و 6 ثبت).
- هر batch بر اساس تنظیم MaxBatchSize به 2 دستور T-SQL محدود شده‌است که البته در انتها در حالت‌های insert، یک select هم برای بازگشت Idها به سمت کلاینت وجود دارد.
بنابراین اینبار بجای یکبار رفت و برگشت حالت قبل (استفاده از مقدار پیش فرض 1000 برای MaxBatchSize)، 4 بار رفت و برگشت به سمت بانک اطلاعاتی صورت گرفته‌است.

زمان کل انجام عملیات در حالت اول 41 میلی ثانیه و در حالت دوم 84 میلی ثانیه است که سرعت آن 51 درصد نسبت به حالت اول کاهش یافته‌است.
مطالب
فلسفه وجودی بخش finally در try catch چیست؟
حتما شما هم متوجه شدید که وقتی رخداد یک استثناء را با استفاده از try و catch کنترل می‌کنیم، هر چیزی که بعد از بسته شدن تگ catch بنویسیم، در هر صورت اجرا می‌شود.

 try {
     int i=0;
     string s = "hello";
     i = Convert.ToInt32(s);
} catch (Exception ex)
{
     Console.WriteLine("Error");
}
Console.WriteLine("I am here!");

پس فلسفه استفاده از بخش finally چیست؟
در قسمت finally منابع تخصیص داده شده در try را آزاد می‌کنیم. کد موجود در این قسمت به هر روی اجرا می‌شود چه استثناء رخ دهد چه ندهد. البته اگر استثناء رخ داده شده در لیست استثناء هایی که برای آنها catch انجام دادیم نباشد، قسمت finally هم عمل نخواهد کرد مگر اینکه از catch به صورت سراسری استفاده کنیم.
اما مهمترین مزیتی که finally ایجاد می‌کند در این است که حتی اگر در قسمت try با استفاده از دستوراتی مثل return یا break یا continue از ادامه کد منصرف شویم و مثلا مقداری برگردانیم، چه خطا رخ دهد یا ندهد کد موجود در finally اجرا می‌شود در حالی که کد نوشته شده بعد از try catch finally فقط در صورتی اجرا می‌شود که به طور منطقی اجرای برنامه به آن نقطه برسد. اجازه بدهید با یک مثال توضیح دهم. اگر کد زیر را اجرا کنیم:
  public static int GetMyInt()
 {
     try {
          for (int i=10;i>=0;i--)
             Console.WriteLine(10/i);
         return 1;
     } catch
     {
         Console.WriteLine("Error!");
     }
     finally {
         Console.WriteLine("ok");
     }
     Console.WriteLine("can you reach here?");  
     return -1;  
 }

برنامه خطای تقسیم بر صفر می‌دهد اما با توجه به کدی که نوشتیم، عدد -1 به خروجی خواهد رفت. در عین حال عبارت ok و can you reach here در خروجی چاپ شده است. اما حال اگر مشکل تقسیم بر صفر را حل کنیم، آیا باز هم عبارت can you reach here در خروجی چاپ خواهد شد؟
 
public static int GetMyInt()
 {
     try {
          for (int i=10;i>=1;i--)
             Console.WriteLine(10/i);
         return 1;
     } catch
     {
         Console.WriteLine("Error!");
     }
     finally {
         Console.WriteLine("ok");
     }
     Console.WriteLine("can you reach here?");  
     return -1;  
 }

مشاهده می‌کنید که مقدار 1 برگردانده می‌شود و عبارت can you reach here در خروجی چاپ نمی‌شود ولی همچنان عبارت ok که در finally ذکر شده در خروجی چاپ می‌شود. یک مثال خوب استفاده از چنین وضعیتی، زمانی است که شما یک ارتباط با بانک اطلاعاتی باز می‌کنید، و نتیجه یک عملیات را با دستور return به کاربر بر می‌گردانید. مسئله این است که در این وضعیت چگونه ارتباط با دیتابیس بسته شده و منابع آزاد می‌گردند؟ اگر در حین عملیات بانک اطلاعاتی، خطایی رخ دهد یا ندهد، و شما دستور آزاد سازی منابع و بستن ارتباط را در داخل قسمت finally نوشته باشید، وقتی دستور return فراخوانی می‌شود، ابتدا منابع آزاد و سپس مقدار به خروجی بر می‌گردد.
public int GetUserId(string nickname)
{
     SqlConnection connection = new SqlConnection(...);
     SqlCommand command = connection.CreateCommand();
     command.CommandText = "select id from users where nickname like @nickname";
     command.Parameters.Add(new SqlParameter("@nickname", nickname));
      try {
         connection.Open();
          return Convert.ToInt32(command.ExecuteScalar());
      } 
      catch(SqlException exception)
      {
      // some exception handling
         return -1;
      } finally {
          if (connection.State == ConnectionState.Open)
         connection.Close();
      }
      // if all things works, you can not reach here
}


مطالب
استفاده از فیلدهای XML در NHibernate

در مورد طراحی یک برنامه "فرم ساز" در مطلب قبلی بحث شد ... حدودا سه سال قبل اینکار را برای شرکتی انجام دادم. یک برنامه درخواست خدمات نوشته شده با ASP.NET که مدیران برنامه می‌توانستند برای آن فرم طراحی کنند؛ فرم درخواست پرینت، درخواست نصب نرم افزار، درخواست وام، درخواست پیک، درخواست آژانس و ... فرم‌هایی که تمامی نداشتند! آن زمان برای حل این مساله از فیلدهای XML استفاده کردم.
فیلدهای XML قابلیت نه چندان جدیدی هستند که از SQL Server 2005 به بعد اضافه شده‌اند. مهم‌ترین مزیت آن‌ها‌ هم امکان ذخیره سازی اطلاعات هر نوع شیء‌ایی به عنوان یک فیلد XML است. یعنی همان زیرساختی که برای ایجاد یک برنامه فرم ساز نیاز است. ذخیره سازی آن هم آداب خاصی را طلب نمی‌کند. به ازای هر فیلد مورد نظر کاربر، یک نود جدید به صورت رشته معمولی باید اضافه شود و نهایتا رشته تولیدی باید ذخیره گردد. از دید ما یک رشته‌ است، از دید SQL Server یک نوع XML واقعی؛ به همراه این مزیت مهم که به سادگی می‌توان با T-SQL/XQuery/XPath از جزئیات اطلاعات این نوع فیلدها کوئری گرفت و سرعت کار هم واقعا بالا است؛ به علاوه بر خلاف مطلب قبلی در مورد dynamic components ، اینبار نیازی نیست تا به ازای هر یک فیلد درخواستی کاربر، واقعا یک فیلد جدید را به جدول خاصی اضافه کرد. داخل این فیلد XML هر نوع ساختار دلخواهی را می‌توان ذخیره کرد. به عبارتی به کمک فیلدهایی از نوع XML می‌توان داخل یک سیستم بانک اطلاعاتی رابطه‌ای، schema-less کار کرد (un-typed XML) و همچنین از این اطلاعات ویژه، کوئری‌های پیچیده هم گرفت.
تا جایی که اطلاع دارم، چند شرکت دیگر هم در ایران دقیقا از همین ایده فیلدهای XML برای ساخت برنامه فرم ساز استفاده کرده‌اند ...؛ البته مطلب جدیدی هم نیست؛ برنامه‌های فرم ساز اوراکل و IBM هم سال‌ها است که از XML برای همین منظور استفاده می‌کنند. مایکروسافت هم به همین دلیل (شاید بتوان گفت مهم‌ترین دلیل وجودی فیلدهای XML در SQL Server)، پشتیبانی توکاری از XML به عمل آورده‌ است.
یا روش دیگری را که برای طراحی سیستم‌های فرم ساز پیشنهاد می‌کنند استفاده از بانک‌های اطلاعاتی مبتنی بر key-value‌ مانند Redis یا RavenDb است؛ یا استفاده از بانک‌های اطلاعاتی schema-less واقعی مانند CouchDb.


خوب ... اکنون سؤال این است که NHibernate برای کار با فیلدهای XML چه تمهیداتی را درنظر گرفته است؟
برای این منظور خاصیتی را که قرار است به یک فیلد از نوع XML نگاشت شود، با نوع XDocument مشخص خواهیم ساخت:
using System.Xml.Linq;

namespace TestModel
{
public class DynamicTable
{
public virtual int Id { get; set; }
public virtual XDocument Document { get; set; }
}
}

سپس باید جهت معرفی این نوع ویژه، به صورت صریح از XDocType استفاده کرد؛ یعنی نکته‌ی اصلی، استفاده از CustomType مرتبط است:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;
using NHibernate.Type;

namespace TestModel
{
public class DynamicTableMapping : IAutoMappingOverride<DynamicTable>
{
public void Override(AutoMapping<DynamicTable> mapping)
{
mapping.Id(x => x.Id);
mapping.Map(x => x.Document).CustomType<XDocType>();
}
}
}

البته لازم به ذکر است که دو نوع NHibernate.Type.XDocType و NHibernate.Type.XmlDocType برای کار با فیلد‌های XML در NHibernate وجود دارند. XDocType برای کار با نوع System.Xml.Linq.XDocument طراحی شده است و XmlDocType مخصوص نگاشت نوع System.Xml.XmlDocument است.

اکنون اگر به کمک کلاس SchemaExport ، اسکریپت تولید جدول متناظر با اطلاعات فوق را ایجاد کنیم به حاصل زیر خواهیم رسید:
   if exists (select * from dbo.sysobjects
where id = object_id(N'[DynamicTable]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
drop table [DynamicTable]

create table [DynamicTable] (
Id INT IDENTITY NOT NULL,
Document XML null,
primary key (Id)
)

یک سری اعمال متداول ذخیره سازی اطلاعات و تهیه کوئری نیز در ادامه ذکر شده‌اند:
//insert
object savedId = 0;
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var obj = new DynamicTable
{
Document = System.Xml.Linq.XDocument.Parse(
@"<Doc><Node1>Text1</Node1><Node2>Text2</Node2></Doc>"
)
};
savedId = session.Save(obj);
tx.Commit();
}
}

//simple query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var entity = session.Get<DynamicTable>(savedId);
if (entity != null)
{
Console.WriteLine(entity.Document.Root.ToString());
}

tx.Commit();
}
}

//advanced query
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var list = session.CreateSQLQuery("select [Document].value('(//Doc/Node1)[1]','nvarchar(255)') from [DynamicTable] where id=:p0")
.SetParameter("p0", savedId)
.List();

if (list != null)
{
Console.WriteLine(list[0]);
}

tx.Commit();
}
}

و در پایان بدیهی است که جهت کار با امکانات پیشرفته‌تر موجود در SQL Server در مورد فیلد‌های XML ( برای نمونه: + و +) باید مثلا رویه ذخیره شده تهیه کرد (یا مستقیما از متد CreateSQLQuery همانند مثال فوق کمک گرفت) و آن‌را در NHibernate مورد استفاده قرار داد. البته به این صورت کار شما محدود به SQL Server خواهد شد و باید در نظر داشت که در کل تعداد کمی بانک اطلاعاتی وجود دارند که نوع‌های XML را به صورت توکار پشتیبانی می‌کنند.