مطالب
مروری بر طراحی Schema less بانک اطلاعاتی SisoDb
اس کیوال سرور، از سال 2005 به بعد، به صورت توکار امکان تعریف و ذخیره سازی اطلاعات schema less و یا schema free را به کمک فیلدهایی از نوع XML ارائه داده است؛ به همراه یکپارچگی آن با زبان XQuery برای تهیه کوئری‌های سریع سمت سرور. در فیلدهای XML می‌توان اطلاعات انواع و اقسام اشیاء را بدون اینکه نیازی به تعریف تک تک فیلدهای مورد نیاز، در بانک اطلاعاتی وجود داشته باشد، ذخیره کرد. یک نمونه از کاربرد چنین امکانی، نوشتن برنامه‌های «فرم ساز» است. برنامه‌هایی که کاربران آن می‌توانند فیلد اضافه و کم کرده و نهایتا اطلاعات را ذخیره و از آن‌ها کوئری بگیرند.
خوب، این فیلد کمتر بحث شده XML، فقط در اس کیوال سرور و نگارش‌های اخیر آن وجود دارد. اگر نیاز به کار با بانک‌های اطلاعاتی سبک‌تری وجود داشت چطور؟ یک راه حل عمومی برای این مساله مراجعه به روش‌های NoSQL است. یعنی بطور کلی بانک‌های اطلاعاتی رابطه‌ای کنار گذاشته شده و به یک سکوی کاری دیگر سوئیچ کرد. در این بین، SisoDb راه حل میانه‌ای را ارائه داده است. با کمک SisoDb می‌توان اطلاعات را به صورت schema less و بدون نیاز به تعریف فیلدهای متناظر آن‌ها، در انواع و اقسام بانک‌های اطلاعاتی SQL Server با فرمت JSON ذخیره و بازیابی کرد. این انواع و اقسام، شامل SQL Server CE نیز می‌شود.


دریافت و نصب SisoDb

دریافت و نصب SisoDb بسیار ساده است. به کمک package manager و امکانات NuGet، کلمه Sisodb را جستجو کنید. در بین مداخل ظاهر شده، پروایدر مورد علاقه خود را انتخاب و نصب نمائید. برای مثال اگر قصد دارید با SQL Server CE کار کنید، SisoDb.SqlCe4 را انتخاب و یا اگر SQL Server 2008 مدنظر شما است، SisoDb.Sql2008 را انتخاب و نصب نمائید.


ثبت و بازیابی اطلاعات به کمک SisoDb

کار با SisoDb بسیار روان است. نیازی به تعاریف نگاشت‌ها و ORM خاصی نیست. یک مثال مقدماتی آن‌را در ادامه ملاحظه می‌کنید:
using SisoDb.Sql2008;

namespace SisoDbTests
{
    public class Customer 
    {
        public int Id { get; set; } 
        public int CustomerNo { get; set; }
        public string Name { get; set; } 
    }

    class Program
    {
        static void Main(string[] args)
        {
            /*var cnInfo = new SqlCe4ConnectionInfo(@"Data source=sisodb2013.sdf;");
            var db = new SqlCe4DbFactory().CreateDatabase(cnInfo);
            db.EnsureNewDatabase();*/

            var cnInfo = new Sql2008ConnectionInfo(@"Data Source=(local);Initial Catalog=sisodb2013;Integrated Security = true");            
            var db = new Sql2008DbFactory().CreateDatabase(cnInfo);            
            db.EnsureNewDatabase();

            var customer = new Customer 
            {
                CustomerNo = 20,
                Name = "Vahid" 
            };
            db.UseOnceTo().Insert(customer);

            using (var session = db.BeginSession()) 
            {
                var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault();

                var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault();
            }
        }
    }
}
در این مثال، ابتدا اتصال به بانک اطلاعاتی برقرار شده و سپس بانک اطلاعاتی جدید تهیه می‌شود. سپس یک وهله از شیء مشتری ایجاد و ذخیره می‌گردد. در ادامه دو کوئری بر روی بانک اطلاعاتی انجام شده است.


ساختار داخلی SisoDb

SisoDb به ازای هر کلاس، حداقل 9 جدول را ایجاد می‌کند. در ادامه نحوه ذخیره سازی شیء مشتری ایجاد شده و مقادیر خواص آن‌را نیز مشاهده می‌نمائید:


ذخیره سازی جداگانه خواص عددی

ذخیره سازی جداگانه خواص رشته‌ای

ذخیره سازی کلی شیء مشتری با فرمت JSON به صورت یک رشته


همانطور که ملاحظه می‌کنید، یک جدول کلی SisoDbIdentities ایجاد شده است که اطلاعات نام اشیاء را در خود نگهداری می‌کند. سپس اطلاعات خواص اشیاء یکبار به صورت JSON ذخیره می‌شوند؛ با تمام اطلاعات تو در توی ذخیره شده در آن‌ها و همچنین یکبار هم هر خاصیت را به صورت یک رکورد جداگانه، بر اساس نوع کلی آن‌ها، در جداول رشته‌ای، عددی و امثال آن ذخیره می‌کند.
شاید بپرسید که چرا به همان فیلد رشته‌ای JSON اکتفاء نشده است؟ از این جهت که پردازشگر سمت بانک اطلاعاتی آن همانند فیلدهای XML در SQL Server و نگارش‌های مختلف آن وجود ندارد (برای مثال به کمک زبان T-SQL می‌توان از زبان XQuery در خود بانک اطلاعاتی، بدون نیاز به واکشی کل اطلاعات در سمت کلاینت، به صورت یکپارچه استفاده کرد). به همین جهت برای کوئری گرفتن و یا تهیه ایندکس، نیاز است این موارد جداگانه ذخیره شوند.
به این ترتیب زمانیکه کوئری تهیه می‌شود، برای مثال:
 var info = session.Query<Customer>().Where(c => c.CustomerNo == 20).FirstOrDefault();
به کوئری زیر ترجمه می‌گردد:
SELECT DISTINCT TOP(1) (s.[StructureId]),
                           s.[Json]
                    FROM   [CustomerStructure] s
                           LEFT JOIN [CustomerIntegers] mem0
                                ON  mem0.[StructureId] = s.[StructureId]
                                AND mem0.[MemberPath] = 'CustomerNo'
                    WHERE  (mem0.[Value] = 20);
و یا کوئری ذیل:
var info2 = session.Query<Customer>().Where(c => c.CustomerNo == 20 && c.Name=="Vahid").FirstOrDefault();
معادل زیر را خواهد داشت:
SELECT DISTINCT TOP(1) (s.[StructureId]),
                           s.[Json]
                    FROM   [CustomerStructure] s
                           LEFT JOIN [CustomerIntegers] mem0
                                ON  mem0.[StructureId] = s.[StructureId]
                                AND mem0.[MemberPath] = 'CustomerNo'
                           LEFT JOIN [CustomerStrings] mem1
                                ON  mem1.[StructureId] = s.[StructureId]
                                AND mem1.[MemberPath] = 'Name'
                    WHERE  ((mem0.[Value] = 20) AND (mem1.[Value] = 'Vahid'));
در هر دو حالت از جداول کمکی تعریف شده برای تهیه کوئری استفاده کرده و نهایتا فیلد JSON اصلی را برای نگاشت نهایی به اشیاء تعریف شده در برنامه بازگشت می‌دهد.

در کل این هم یک روش تفکر و طراحی Schema less است که با بسیاری از بانک‌های اطلاعاتی موجود سازگاری دارد.
برای مشاهده اطلاعات بیشتری در مورد جزئیات این روش می‌توان به Wiki آن مراجعه کرد.
نظرات مطالب
امکان انجام محاسبات سمت کلاینت در EF Core
یک نکته‌ی تکمیلی: کدامیک از متدهای کار با رشته‌ها در سمت کلاینت پردازش می‌شوند و کدام‌ها در سمت سرور؟
در مثال‌های زیر، هر جائیکه قسمت where عبارت SQL حذف شده‌، یعنی Client-Side Evaluation رخ داده‌است:
- با اضافه شدن نوع مقایسه، محاسبه‌ی سمت کلاینت رخ می‌دهد:
var test1 = context.Blogs
.Where(blog => String.Compare(blog.Url, "A", StringComparison.Ordinal) > 0)
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
- بدون مشخص سازی نوع مقایسه، محاسبه‌ی سمت سرور را خواهیم داشت:
var test2 = context.Blogs
.Where(blog => String.Compare(blog.Url, "B") > 0)
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
// WHERE [blog].[Url] > N'B'
- در مورد متد Equals هم دقیقا به همین صورت است. با اضافه شدن نوع مقایسه، محاسبه‌ی سمت کلاینت رخ می‌دهد: 
var test3 = context.Blogs
.Where(blog => blog.Url.Equals("C", StringComparison.OrdinalIgnoreCase))
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
- بدون مشخص سازی نوع مقایسه، محاسبه‌ی سمت سرور را خواهیم داشت: 
var test3_1 = context.Blogs
.Where(blog => blog.Url.Equals("C_1"))
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
// WHERE [blog].[Url] = N'C_1'
- StartsWith و EndsWith در سمت سرور محاسبه می‌شوند:
var test4 = context.Blogs
.Where(blog => blog.Url.StartsWith("D"))
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
// WHERE [blog].[Url] LIKE N'D' + N'%' AND (LEFT([blog].[Url], LEN(N'D')) = N'D')
- اما اگر از عبارات SQL آن‌ها راضی نیستید، از متد EF.Functions.Like استفاده کنید:
var test5 = context.Blogs
.Where(blog => EF.Functions.Like(blog.Url, "S_i%"))
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
// WHERE [blog].[Url] LIKE N'S_i%'
- متدهای ToUpper و ToLower سمت سرور محاسبه می‌شوند (هرچند اگر از SQL Server استفاده می‌کنید، Collation پیش‌فرض آن غیرحساس به حروف کوچک و بزرگ است و در اکثر مواقع نیازی به یک چنین متدهایی نیست):
var test6 = context.Blogs
.Where(blog => blog.Url.ToUpper() == "E")
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
// WHERE UPPER([blog].[Url]) = N'E'
- اما حالت‌های Invariant دار آن‌ها خیر و در سمت کلاینت محاسبه می‌شوند:
var test7 = context.Blogs
.Where(blog => blog.Url.ToUpperInvariant() == "F")
.ToList();
// SELECT [blog].[BlogId], [blog].[Url]
// FROM [Blogs] AS [blog]
مطالب دوره‌ها
نصب و راه اندازی مقدماتی Full Text Search
با استفاده از امکانات ابتدایی T-SQL مانند like می‌توان جستجوهایی را برای یافتن موارد مشابه با عبارتی خاص انجام داد، اما این جستجوها بسیار هزینه‌بر و کند هستند. در SQL Server برای مدیریت جستجوهای سریع و پیشرفته بر روی متون، افزونه‌های توکاری مانند Full text search، Semantic search، Term extraction و Term lookup تدارک دیده شده‌اند. Semantic search از نگارش 2012 آن افزوده شده‌است و مابقی در نگارش‌های پیشین آن نیز وجود داشته‌اند.


بررسی‌های مقدماتی

ابتدای کار نیاز است بررسی کنیم آیا افزونه‌ی Full Text Search، به همراه SQL Server نصب شده‌است یا خیر. برای این منظور کوئری ذیل را اجرا کنید:
 select SERVERPROPERTY('IsFullTextInstalled');
اگر خروجی این کوئری عدد 1 بود، یعنی FTS نصب شده‌است؛ اگر خیر، مجددا برنامه‌ی نصاب SQL Server را اجرا کرده و زمانیکه به قسمت feature selection رسیدید، گزینه‌ی ذیل را باید انتخاب کنید:
 instance features -> database engine services -> Full Text



راه اندازی سرویس Full Text Search

پیش از ادامه‌ی بحث، به کنسول سرویس‌های ویندوز مراجعه کرده و مطمئن شوید که سرویس SQL Full-text Filter Daemon Launcher MSSQLSERVER نیز در حال اجرا است. در غیراینصورت با خطای ذیل مواجه خواهید شد:
 SQL Server encountered error 0x80070422 while communicating with full-text filter daemon host (FDHost) process.
اگر این سرویس در حال اجرا است و باز هم خطای فوق ظاهر شد، مجددا به کنسول سرویس‌های ویندوز مراجعه کرد، در برگه‌ی  خواص سرویس SQL Full-text Filter Daemon Launcher MSSQLSERVER، گزینه‌ی logon را یافته و آن‌را به local system account تغییر دهید. سپس سرویس را ری استارت کنید. پس از آن نیاز است دستور ذیل را نیز اجرا کنید:
 sp_fulltext_service 'restart_all_fdhosts'



چه نوع داده‌هایی را می‌توان توسط FTS ایندکس کرد؟

با استفاده از امکانات FTS می‌توان کلیه ستون‌هایی را که دارای نوع‌های ذیل باشند، ایندکس کرد:
 char, nchar, varchar, nvarchar, text, ntext, image, xml, varbinary(max)
البته نوع باینری را که ملاحظه می‌کنید مانند image و varbinary max، نیاز به یک ستون اضافی، برای ذخیره سازی پسوند فایل‌های ذخیره شده در آن‌ها مانند docx، pdf ، xlsx و امثال آن نیز دارند. برای مثال ابتدا یک فایل word را در ستونی از نوع varbinary max ذخیره می‌کنید و سپس نیاز است در همانجا در ستونی دیگر، پسوند این فایل را نیز قید نمائید.
همچنین FTS برای پردازش این فایل‌های باینری و ایندکس کردن اطلاعات آن‌ها، نیاز به افزونه‌هایی به نام IFilters دارد. کار این فیلترها استخراج متن بدون فرمت، از فایل‌های باینری مرتبط و ارائه‌ی آن‌ها به موتور FTS می‌باشد.


نصب فیلترهای مخصوص FTS آفیس

اگر علاقمند هستید که بدانید در حال حاضر چه تعداد فیلترهای FTS بر روی سیستم شما نصب شده‌است، کوئری ذیل را اجرا نمائید:
 exec sys.sp_help_fulltext_system_components 'filter';
برای نمونه اگر آفیس بر روی سیستم شما نصب باشد، در حاصل کوئری فوق، فیلتری مانند offfilt.dll را نیز مشاهده خواهید کرد که به پسوندهایی مانند doc، ppt، xls و امثال آن انتساب داده شده‌است.
فیلترهای آفیس را جداگانه نیز می‌توانید دریافت و نصب کنید (بدون نیاز به نصب کامل آفیس بر روی سرور):
این فیلترها تا نگارش 2013 آفیس را نیز پشتیبانی می‌کنند و آگر آپدیت ویندوز نیز روشن باشد، سرویس پک 2 آن را نیز دریافت خواهید کرد.

پس از اینکه فیلترها را نصب کردید، باید آن‌ها را در وهله‌ی جاری SQL Server ثبت کرد:
 exec sys.sp_fulltext_service 'load_os_resources', 1;
EXEC sp_fulltext_service 'update_languages';
EXEC sp_fulltext_service 'restart_all_fdhosts';
اکنون اگر مجددا کوئری sys.sp_help_fulltext_system_components یاد شده را اجرا کنید. خروجی آن حدودا 50 سطر خواهد بود؛ این اطلاعات را از کوئری ذیل نیز می‌توان بدست آورد:
 select * from sys.fulltext_document_types;
اگر پس از نصب و همچنین ثبت و معرفی فیلترهای آفیس 2010 به بعد، هنوز تعداد 50 ردیف را ملاحظه می‌کنید (اکنون باید بیشتر از 160 مورد باشند)، نیاز است یکبار وهله‌ی جاری SQL Server را ری استارت کنید. برای اینکار در management studio بر روی وهله‌ی جاری، کلیک راست کرده و گزینه‌ی Restart را انتخاب کنید.

فیلترهای فوق علاوه بر اینکه امکان FTS را بر روی کلیه فایل‌های مجموعه آفیس میسر می‌کنند، امکان جستجو FTS را بر روی خواص ویژه اضافی آن‌ها، مانند نام نویسنده، واژه‌های کلیدی، تاریخ ایجاد و امثال آن نیز به همراه دارند.


FTS چگونه کار می‌کند؟

زبان‌های پشتیبانی شده توسط FTS را توسط کوئری ذیل می‌توانید مشاهده کنید:
 select lcid, name from sys.fulltext_languages order by name;
کار FTS با word-breakers و stemmers شروع می‌شود. این‌ها کار آنالیز متن را بر اساس زبانی مشخص انجام می‌دهند. اگر زبان مدنظر توسط FTS پشتیبانی نمی‌شود، می‌توان از زبان انگلیسی و یا همچنین Neutral نیز برای آنالیز آن استفاده کرد. زبان Neutral جزو خروجی کوئری فوق با شماره آی دی صفر است.
word-breakers تک تک کلمات را (که به آن‌ها token نیز گفته می‌شود) تشخیص داده و سپس FTS آن‌ها را با فرمتی فشرده شده، درون ایندکس‌های مخصوص خود ذخیره می‌کند.کار stemmers تولید حالات inflectional (صرفی) یک کلمه بر اساس دستور زبانی مشخص است.
اهمیت آنالیز inflectional، در اینجا است که برای مثال اگر در متنی واژه‌ی jumps وجود داشت و کاربر در حین جستجو، jumped را وارد کرد، FTS بر اساس دستور زبان مورد استفاده، پیشتر، حالات مختلف صرفی jump را ذخیره کرده‌است و امکان انجام یک چنین کوئری پیشرفته‌ای را پیدا می‌کند.


نصب و فعال سازی Semantic Language Database

کار TFS تنها به خرد کردن واژه‌ها و آنالیز صرفی آن‌ها خلاصه نمی‌شود. در مرحله‌ی بعد، انجام Statistical semantic search میسر می‌شود. در اینجا SQL Server بر اساس آمار واژه‌های کلیدی استخراج شده، توانایی یافتن متونی مشابه و یا مرتبط را پیدا می‌کند. Semantic Search جزو تازه‌های SQL Server 2012 است.

برای اینکار نیاز است بانک اطلاعاتی Semantic language statistics نیز نصب شود. برای اطمینان از نصب بودن آن، کوئری ذیل را اجرا کنید:
 select * from sys.fulltext_semantic_language_statistics_database;
اگر حاصل آن خالی بود، نیاز است مستقلا نصب شود. این بانک اطلاعاتی ویژه را در یکی از دو مسیر ذیل
 x64\Setup\SemanticLanguageDatabase.msi
x86\Setup\SemanticLanguageDatabase.msi
در DVD یا فایل ISO نصب SQL Server 2012 می‌توانید پیدا کنید. فایل نصاب msi آن‌را اجرا کنید، دو فایل mdf و ldf را در مسیری که مشخص می‌کنید، کپی می‌کند.
پس از آن نیاز است این بانک اطلاعاتی را Attach و همچنین ثبت کرد:
CREATE DATABASE semanticsdb
    ON ( FILENAME = 'D:\SQL_Data\SemanticLanguageDatabase\semanticsdb.mdf' )
    LOG ON ( FILENAME = 'D:\SQL_Data\SemanticLanguageDatabase\semanticsdb_log.ldf' )
    FOR ATTACH
GO

EXEC sp_fulltext_semantic_register_language_statistics_db @dbname = N'semanticsdb'
GO
زمانیکه این بانک اطلاعاتی کپی می‌شود، دسترسی Write کاربر وارد شده به سیستم را در برگه‌ی Security فایل‌های mdf و ldf آن ندارد. به همین جهت ممکن است در حین Attach، پیام عدم دسترسی را دریافت کنید که با مراجعه به خواص فایل‌ها و تنظیم دسترسی Write کاربر جاری، مشکل برطرف می‌شود.
پس از مراحل فوق، اگر مجددا کوئری یاد شده بر روی sys.fulltext_semantic_language_statistics_database را اجرا کنید، یک سطر خروجی خواهد داشت.
مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
در ادامه‌ی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آن‌را بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایه‌ای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعه‌ی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایل‌های کانفیگ جدید، تزریق وابستگی‌ها و سرویس‌ها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهنده‌ها و ... در ASP.NET Core 1.0 استفاده خواهد شد.


EF Core چیست؟

EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانک‌های اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر می‌کند. توسط آن قسمت عمده‌ی کدهای مستقیم کار با بانک‌های اطلاعاتی حذف شده و تبدیل به کدهای دات نتی می‌شوند. مزیت این لایه‌ی Abstraction اضافی (لایه‌ای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامه‌است؛ بدون نیاز به تغییری در سایر قسمت‌های برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر می‌کنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر می‌رسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئری‌ها، رفتار بسیاری از بانک‌های اطلاعاتی با آن‌ها همانند رویه‌های ذخیره شده است که به عدم تولید Query plan‌های مجزایی به ازای هر کوئری رسیده منجر می‌شود که در نهایت سبب بالا رفتن سرعت اجرای کوئری‌ها و مصرف حافظه‌ی کمتری در سمت سرور بانک اطلاعاتی می‌گردد.


تفاوت EF Core با نگارش‌های دیگر Entity framework در چیست؟

سورس باز بودن
EF از نگارش‌های آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارش‌های پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.

چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفاده‌ی از آن در انواع و اقسام برنامه‌های دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامه‌های مختلف به شرح زیر است:
 • All .NET application (Console, ASP.NET 4, WinForms, WPF)
 • Mac and Linux applications (Mono)
 • UWP (Universal Windows Platform)
 • ASP.NET Core applications
 • Can use EF Core in Windows phone and Windows store app

افزایش تعداد بانک‌های اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانک‌های اطلاعاتی رابطه‌‌ای بود و همچنین مایکروسافت صرفا نگارش‌های مختلف SQL Server را به صورت رسمی پشتیبانی می‌کرد و برای سایر بانک‌های اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانک‌های اطلاعاتی رابطه‌ای، پشتیبانی از بانک‌های اطلاعاتی NoSQL هم اضافه شده‌است؛ به همراه پروایدر ویژه‌‌ای به نام In Memory جهت انجام ساده‌تر Unit testing. کاری که با نگارش‌های پیشین EF به سادگی و از روز اول پشتیبانی نمی‌شد.

حذف و یا عدم پیاده سازی تعدادی از قابلیت‌های EF 6.x
اگر موارد فوق جزو مهم‌ترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگی‌ها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامه‌های UWP نیاز دارد تمام نوع‌ها را همانند زبان‌های C و ++C، در زمان کامپایل بداند)، یک سری از قابلیت‌های EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمی‌شوند. لیست کامل و به روز شده‌ی این موارد را در اینجا می‌توانید مطالعه کنید.
بنابراین امکان انتقال برنامه‌های EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.


برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0

برای نصب تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بسته‌ی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
البته در این قسمت صرفا از بسته‌ی اول که جهت اتصال به SQL Server است استفاده می‌کنیم. بسته‌های دیگر را در قسمت‌های بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
        "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"
    }
}
این مداخلی که توسط نیوگت اضافه شده‌اند، نیاز به اصلاح دارند؛ به این صورت:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        },
        "Microsoft.EntityFrameworkCore.SqlServer.Design": {
            "version": "1.0.0",
            "type": "build"
        }
    },

    "tools": {
       // same as before
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }   
   }
}
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعه‌ی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آن‌را ذخیره کنید؛ تا کار به روز رسانی و نصب بسته‌ها، به صورت خودکار و همچنین صحیحی انجام شود.


فعال سازی صفحات مخصوص توسعه دهنده‌های EF Core 1.0

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بسته‌ی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحه‌ی نمایش خطاهای بانک اطلاعاتی را میسر می‌کند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{
    "dependencies": {
       // same as before
        "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0"
    }
}
سپس می‌توان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   if (env.IsDevelopment())
   {
      app.UseDatabaseErrorPage();
   }
با فعال سازی این صفحه، اگر در حین توسعه‌ی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهده‌ی یک صفحه‌ی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیه‌هایی از طرف تیم EF مشاهده خواهید کرد.


تعریف اولین Context برنامه و مشخص سازی رشته‌ی اتصالی آن


در این تصویر، زیر ساخت نگاشت‌های EF Core را مشاهده می‌کنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرنده‌ی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل این‌ها را مشاهده می‌کنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که این‌ها نیز به فیلدها و ستون‌های آن جدول در سمت بانک اطلاعاتی نگاشت می‌شوند.
بنابراین برای شروع کار، پوشه‌ای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه می‌کنیم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آن‌را به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق می‌شود، صورت خواهد گرفت:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق می‌شود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار می‌دهیم.
سازنده‌ی این کلاس نیز به نحو خاصی تعریف شده‌است. اگر به سورس‌های EF Core مراجعه کنیم، کلاس پایه‌ی DbContext دارای دو سازنده‌ی با و بدون پارامتر است:
protected DbContext()
   : this((DbContextOptions) new DbContextOptions<DbContext>())
{
}

public DbContext([NotNull] DbContextOptions options)
{
  // …
}
اگر از سازنده‌ی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازنده‌ی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"... connection string ...");
        }
    }
}
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشته‌ی اتصالی به آن چیست.
اما چون در یک برنامه‌ی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام می‌شود و در آنجا به سادگی می‌توان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشته‌ی اتصالی را دریافت کرد، مرسوم است از سازنده‌ی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعه‌ی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» به فایل appsettings.json مراجعه کرده و تنظیمات رشته‌ی اتصالی برنامه را به صورت ذیل در آن مشخص می‌کنیم:
{
    "ConnectionStrings": {
        "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true"
    }
}
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهله‌ی پیش فرض SQL Server اشاره شده‌است؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده می‌شود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیده‌است.

پس از تعریف رشته‌ی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازنده‌ی با پارامتر آن استفاده می‌کنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویس‌های ASP.NET Core معرفی می‌کنیم:
    public class Startup
    {
        public IConfigurationRoot Configuration { set; get; }

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]);
            });
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» نحوه‌ی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفته‌است، نیز ذکر شده‌است.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی می‌کنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.


یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد

با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگی‌ها نیز در آن وجود دارد. یعنی بجای روش فوق، می‌توان IConfigurationRoot را به سازنده‌ی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        private readonly IConfigurationRoot _configuration;

        public ApplicationDbContext(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        //{
        //}

        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]);
        }
    }
}
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعه‌ی سرویس‌های برنامه معرفی شده‌است، از آن در تمام کلاس‌های برنامه که تحت نظر سیستم تزریق وابستگی‌های توکار ASP.NET Core هستند، می‌توان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا می‌کند و ساده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>();


یک نکته: امکان تزریق ApplicationDbContext به تمام کلاس‌های برنامه وجود دارد

همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامه‌ی ASP.NET Core نیز وجود دارد:
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Core1RtmEmptyTest.Controllers
{
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
}
در اینجا نحوه‌ی تزریق DB Context برنامه را به یک کنترلر مشاهده می‌کنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگی‌ها نام ندارد و هنوز این کنترلر دقیقا وابسته‌است به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.

در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:


علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشده‌است و همچنین ساختار جداول را به آن منتقل نکرده‌ایم که این موارد را در قسمت‌های بعدی مرور خواهیم کرد.
مطالب
شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها
یکی از مهم‌ترین قسمت‌های مدل سازی موجودیت‌ها، تعیین نوع‌های صحیح ستون‌ها و همچنین تعیین اندازه‌ی مناسبی برای آن‌ها است؛ به همراه تعیین اجباری بودن یا نبودن مقدار دهی آن‌ها.

تعیین اجباری بودن یا نبودن ستون‌ها در EF Core

به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته می‌شود؛ مانند:
 string, int?, byte[]
و هر ستونی که نوع CLR آن نال پذیر نباشد، مقدار دهی آن در EF Core اجباری است؛ مانند:
 int, decimal, bool, DateTime
همچنین باید دقت داشت که حتی اگر در تنظیمات نگاشت‌های برنامه به صورت اختیاری تعریف شوند، باز هم EF Core آن‌ها را اجباری درنظر می‌گیرد.

برای لغو اختیاری بودن یک خاصیت نال پذیر می‌توان از ویژگی Required استفاده کرد:
 [Required]
public string Url { get; set; }
نوع string نال پذیر است. برای لغو این وضعیت می‌توان از ویژگی Required استفاده کرد که در سمت بانک اطلاعاتی نیز به not null ترجمه می‌شود.
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Blog>()
              .Property(b => b.Url)
              .IsRequired();
}
با توجه به این توضیحات، نیازی نیست در بالای یک خاصیت از نوع int، ویژگی Required را ذکر کرد. چون int نال پذیر نیست، مقدار دهی آن اجباری است.


کار با رشته‌ها در EF Core

ذکر یک خاصیت رشته‌ای به این صورت:
public string FirstName { get; set; }
به معنای نال پذیر بودن این ستون است (چون Required تعریف نشده‌است) و همچنین نوع و طول آن در SQL Server به nvarchar max تنظیم می‌شود. این تنظیم طول هرچند در مورد SQL Server صادق است، اما ممکن است در SQL Server CE به nvarchar 4000 تفسیر شود (و این مشکل را به همراه داشته باشد که چرا نمی‌توان متون طولانی را در آن ثبت کرد). به عبارتی عدم ذکر دقیق طول یک خاصیت رشته‌ای، در پروایدرهای مختلف، ممکن است معانی مختلفی را به همراه داشته باشد. بنابراین نیاز است طول خواص رشته‌ای حتما ذکر شوند تا در تمام بانک‌های اطلاعاتی با دقت کامل و بدون حدس و گمان تنظیم گردند.
  [StringLength(450)]
  public string FirstName { get; set; }

  [MaxLength(450)]
  public string LastName { get; set; }

  [MaxLength]
  public string Address { get; set; }
برای تعیین طول دقیق یک فیلد رشته‌ای، می‌توان از ویژگی‌های StringLength و یا MaxLength با ذکر اندازه‌ای استفاده کرد.
برای تعیین صریح یک فیلد رشته‌ای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانک‌های اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشته‌ای:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasMaxLength(450);
و برای تعیین صریح nvarchar max بودن آن فیلد:
modelBuilder.Entity<Person>()
   .Property(x => x.Address)
   .HasColumnType("nvarchar(max)");
حالت پیش فرض EF Core، کار با رشته‌های یونیکد است. یعنی تمام فیلدهای فوق به nvarchar تفسیر می‌شوند و این n ایی که در ابتدا ذکر شده‌است به معنای یونیکد بودن آن است. اگر می‌خواهید این پیش‌فرض تعیین نوع یونیکد را تغییر دهید، می‌توان از ویژگی Column استفاده کرد:
   [Column(TypeName = "varchar")]
  [MaxLength]
  public string Address { get; set; }
البته اگر اطلاعاتی را که با آن کار می‌کنید چندزبانی و یونیکد هستند، بهتر است این مورد را تغییر ندهید.

نکته‌ای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
 Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)',
or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
در اینجا عنوان می‌کند که اگر مقصود شما varchar max است، ویژگی MaxLength را حذف کرده و تنها بنویسید:
   [Column(TypeName = "varchar(max)")]

نکته‌ای در مورد ایندکس‌ها: در قسمت قبل عنوان شد که می‌توان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشته‌ها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را می‌توان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافته‌است (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستون‌های رشته‌ای، یونیکد و nvarchar درنظر گرفته می‌شود، حداکثر طول امنی را که می‌توان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشته‌ای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.


کار با اعداد در EF Core

کلاس نمونه‌ای را با ساختار ذیل درنظر بگیرید:
    public class Person 
    {
        public int Id { set; get; }

        public DateTime? DateAdded { set; get; }

        public DateTime? DateUpdated { set; get; }

        [StringLength(450)]
        public string FirstName { get; set; }

        [MaxLength(450)]
        public string LastName { get; set; }

        //[Column(TypeName = "varchar")]
        [MaxLength]
        public string Address { get; set; }


        //bit
        public bool IsActive { get; set; }

        //tiny Int
        public byte Age { get; set; }

        //small Int
        public short Short { get; set; }

        //int
        public int Int32 { get; set; }

        //Big int
        public long Long { get; set; }
    }
پس از اعمال مهاجرت‌ها و به روز رسانی ساختار بانک اطلاعاتی، به ساختار ذیل خواهیم رسید:


همانطور که ملاحظه می‌کنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شده‌اند.


نکته‌ای در مورد اعداد اعشاری: توصیه شده‌است در تعاریف موجودیت‌های خود بهتر است از نوع‌های float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسه‌ی دقیق آن‌ها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»


کار با تاریخ در EF Core

اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شده‌است:
 CREATE TABLE [dbo].[Persons](
 [DateAdded] [datetime2](7) NULL,
 [DateUpdated] [datetime2](7) NULL,
اگر در داده‌های خود نیازی به زمان ندارید، می‌توان این نوع پیش فرض را با ویژگی Column که پیشتر بحث شد، به date تغییر داد.
اطلاعات بیشتر: «کنترل نوع‌های داده با استفاده از EF در SQL Server»

به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آن‌را نال پذیر تعریف کنید.


کار با مباحث همزمانی در EF Core

EF Core به صورت پیش فرض، فرض می‌کند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافته‌است و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره می‌کند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
 [ConcurrencyCheck]
public string Name { set; get; }
و یا متد IsConcurrencyToken حالت Fluent API استفاده نمائید:
modelBuilder.Entity<Person>()
    .Property(p => p.Name)
    .IsConcurrencyToken();
در این حالت کوئری به روز رسانی، علاوه بر فیلد Id رکورد، حاوی فیلد Name نیز خواهد بود (در حین تشکیل شرط یافتن رکورد) و اگر در بین فاصله‌ی یافتن شخص و به روز رسانی نام او، شخص دیگری این‌کار را انجام داده باشد، این به روز رسانی موفقیت آمیز نبوده و استثنایی صادر می‌شود.

اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، می‌توان از روش ویژه‌ای به نام Timestamp/row version استفاده کرد:
 [Timestamp]
 public byte[] Timestamp { get; set; }
با معادل Fluent API ذیل:
modelBuilder.Entity<Blog>()
   .Property(p => p.Timestamp)
   .ValueGeneratedOnAddOrUpdate()
   .IsConcurrencyToken();
در مورد ValueGeneratedOnAddOrUpdate در قسمت قبل بحث کردیم. فیلد TimeStamp نیز جزو فیلدهای ویژه‌ای است که SQL Server به صورت خودکار قادر است آن‌را مقدار دهی کند و زمانیکه ValueGeneratedOnAddOrUpdate قید می‌شود، یعنی این فیلد همواره با فراخوانی متد SaveChanges، به صورت خودکار مقدار دهی خواهد شد (و نیازی نیست تا توسط برنامه مقدار دهی شود).
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آن‌را تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر می‌شود.
مطالب
Globalization در ASP.NET MVC
اگر بازار هدف یک محصول شامل چندین کشور، منطقه یا زبان مختلف باشد، طراحی و پیاده سازی آن برای پشتیبانی از ویژگی‌های چندزبانه یک فاکتور مهم به حساب می‌آید. یکی از بهترین روشهای پیاده سازی این ویژگی در دات نت استفاده از فایلهای Resource است. درواقع هدف اصلی استفاده از فایلهای Resource نیز Globalization است. Globalization برابر است با Internationalization + Localization که به اختصار به آن g11n میگویند. در تعریف، Internationalization (یا به اختصار i18n) به فرایند طراحی یک محصول برای پشتیبانی از فرهنگ(culture)‌ها و زبانهای مختلف و Localization (یا L10n) یا بومی‌سازی به شخصی‌سازی یک برنامه برای یک فرهنگ یا زبان خاص گفته میشود. (اطلاعات بیشتر در اینجا).
استفاده از این فایلها محدود به پیاده سازی ویژگی چندزبانه نیست. شما میتوانید از این فایلها برای نگهداری تمام رشته‌های موردنیاز خود استفاده کنید. نکته دیگری که باید بدان اشاره کرد این است که تقرببا تمامی منابع مورد استفاده در یک محصول را میتوان درون این فایلها ذخیره کرد. این منابع در حالت کلی شامل موارد زیر است:
- انواع رشته‌های مورد استفاده در برنامه چون لیبل‌ها و پیغام‌ها و یا مسیرها (مثلا نشانی تصاویر یا نام کنترلرها و اکشنها) و یا حتی برخی تنظیمات ویژه برنامه (که نمیخواهیم براحتی قابل نمایش یا تغییر باشد و یا اینکه بخواهیم با تغییر زبان تغییر کنند مثل direction و امثال آن)
- تصاویر و آیکونها و یا فایلهای صوتی و انواع دیگر فایل ها
- و ...
 نحوه بهره برداری از فایلهای Resource در دات نت، پیاده سازی نسبتا آسانی را در اختیار برنامه نویس قرار میدهد. برای استفاده از این فایلها نیز روشهای متنوعی وجود دارد که در مطلب جاری به چگونگی استفاده از آنها در پروژه‌های ASP.NET MVC پرداخته میشود.

Globalization در دات نت
فرمت نام یک culture دات نت (که در کلاس CultureInfo پیاده شده است) بر اساس استاندارد RFC 4646 (^ و ^) است. (در اینجا اطلاعاتی راجع به RFC یا Request for Comments آورده شده است). در این استاندارد نام یک فرهنگ (کالچر) ترکیبی از نام زبان به همراه نام کشور یا منطقه مربوطه است. نام زبان برپایه استاندارد ISO 639 که یک عبارت دوحرفی با حروف کوچک برای معرفی زبان است مثل fa برای فارسی و en برای انگلیسی و نام کشور یا منطقه نیز برپایه استاندارد ISO 3166 که یه عبارت دوحرفی با حروف بزرگ برای معرفی یک کشور یا یک منطقه است مثل IR برای ایران یا US برای آمریکاست. برای نمونه میتوان به fa-IR برای زبان فارسی کشور ایران و یا en-US برای زبان انگلیسی آمریکایی اشاره کرد. البته در این روش نامگذاری یکی دو مورد استثنا هم وجود دارد (اطلاعات کامل کلیه زبانها: National Language Support (NLS) API Reference). یک فرهنگ خنثی (Neutral Culture) نیز تنها با استفاده از دو حرف نام زبان و بدون نام کشور یا منطقه معرفی میشود. مثل fa برای فارسی یا de برای آلمانی. در این بخش نیز دو استثنا وجود دارد (^).
در دات نت دو نوع culture وجود دارد: Culture و UICulture. هر دوی این مقادیر در هر Thread مقداری منحصربه فرد دارند. مقدار Culture بر روی توابع وابسته به فرهنگ (مثل فرمت رشته‌های تاریخ و اعداد و پول) تاثیر میگذارد. اما مقدار UICulture تعیین میکند که سیستم مدیریت منابع دات نت (Resource Manager) از کدام فایل Resource برای بارگذاری داده‌ها استفاده کند. درواقع در دات نت با استفاده از پراپرتی‌های موجود در کلاس استاتیک Thread برای ثرد جاری (که عبارتند از CurrentCulture و CurrentUICulture) برای فرمت کردن و یا انتخاب Resource مناسب تصمیم گیری میشود. برای تعیین کالچر جاری به صورت دستی میتوان بصورت زیر عمل کرد:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
دراینجا باید اشاره کنم که کار انتخاب Resource مناسب با توجه به کالچر ثرد جاری توسط ResourceProviderFactory پیشفرض دات نت انجام میشود. در مطالب بعدی به نحوه تعریف یک پرووایدر شخصی سازی شده هم خواهم پرداخت.

پشتیبانی از زبانهای مختلف در MVC
برای استفاده از ویژگی چندزبانه در MVC دو روش کلی وجود دارد.
1. استفاده از فایلهای Resource برای تمامی رشته‌های موجود
2. استفاده از View‌های مختلف برای هر زبان
البته روش سومی هم که از ترکیب این دو روش استفاده میکند نیز وجود دارد. انتخاب روش مناسب کمی به سلیقه‌ها و عادات برنامه نویسی بستگی دارد. اگر فکر میکنید که استفاده از ویوهای مختلف به دلیل جداسازی مفاهیم درگیر در کالچرها (مثل جانمایی اجزای مختلف ویوها یا بحث Direction) باعث مدیریت بهتر و کاهش هزینه‌های پشتیبانی میشود بهتر است از روش دوم یا ترکیبی از این دو روش استفاده کنید. خودم به شخصه سعی میکنم از روش اول استفاده کنم. چون معتقدم استفاده از ویوهای مختلف باعث افزایش بیش از اندازه حجم کار میشود. اما در برخی موارد استفاده از روش دوم یا ترکیبی از دو روش میتواند بهتر باشد.

تولید فایلهای Resource
بهترین مکان برای نگهداری فایلهای Resource در یک پروژه جداگانه است. در پروژه‌های از نوع وب‌سایت پوشه‌هایی با نام App_GlobalResources یا App_LocalResources وجود دارد که میتوان از آنها برای نگهداری و مدیریت این نوع فایلها استفاده کرد. اما همانطور که در اینجا توضیح داده شده است این روش مناسب نیست. بنابراین ابتدا یک پروژه مخصوص نگهداری فایلهای Resource ایجاد کنید و سپس اقدام به تهیه این فایلها نمایید. سعی کنید که عنوان این پروژه به صورت زیر باشد. برای کسب اطلاعات بیشتر درباره نحوه نامگذاری اشیای مختلف در دات نت به این مطلب رجوع کنید.
<SolutionName>.Resources
برای افزودن فایلهای Resource به این پروژه ابتدا برای انتخاب زبان پیش فرض محصول خود تصمیم بگیرید. پیشنهاد میکنم که از زبان انگلیسی (en-US) برای اینکار استفاده کنید. ابتدا یک فایل Resource (با پسوند resx.) مثلا با نام Texts.resx به این پروژه اضافه کنید. با افزودن این فایل به پروژه، ویژوال استودیو به صورت خودکار یک فایل cs. حاوی کلاس متناظر با این فایل را به پروژه اضافه میکند. این کار توسط ابزار توکاری به نام ResXFileCodeGenerator انجام میشود. اگر به پراپرتی‌های این فایل resx. رجوع کنید میتوانید این عنوان را در پراپرتی Custom Tool ببینید. البته ابزار دیگری برای تولید این کلاسها نیز وجود دارد. این ابزارهای توکار برای سطوح دسترسی مخنلف استفاده میشوند. ابزار پیش فرض در ویژوال استودیو یعنی همان ResXFileCodeGenerator، این کلاسها را با دسترسی internal تولید میکند که مناسب کار ما نیست. ابزار دیگری که برای اینکار درون ویژوال استودیو وجود دارد PublicResXFileCodeGenerator است و همانطور که از نامش پیداست از سطح دسترسی public استفاده میکند. برای تغییر این ابزار کافی است تا عنوان آن را دقیقا در پراپرتی Custom Tool تایپ کنید.

نکته: درباره پراپرتی مهم Build Action این فایلها در مطالب بعدی بیشتر بحث میشود.
برای تعیین سطح دسترسی Resource موردنظر به روشی دیگر، میتوانید فایل Resource را باز کرده و Access Modifier آن را به Public تغییر دهید.

سپس برای پشتیبانی از زبانی دیگر، یک فایل دیگر Resource به پروژه اضافه کنید. نام این فایل باید همنام فایل اصلی به همراه نام کالچر موردنظر باشد. مثلا برای زبان فارسی عنوان فایل باید Texts.fa-IR.resx یا به صورت ساده‌تر برای کالچر خنثی (بدون نام کشور) Texts.fa.resx باشد. دقت کنید اگر نام فایل را در همان پنجره افزودن فایل وارد کنید ویژوال استودیو این همنامی را به صورت هوشمند تشخیص داده و تغییراتی را در پراپرتی‌های پیش فرض فایل Resource ایجاد میکند.
نکته: این هوشمندی مرتبه نسبتا بالایی دارد. بدین صورت که تنها درصورتیکه عبارت بعد از نام فایل اصلی Resource (رشته بعد از نقطه مثلا fa در اینجا) متعلق به یک کالچر معتبر باشد این تغییرات اعمال خواهد شد.
مهمترین این تغییرات این است که ابزاری را برای پراپرتی Custom Tool این فایلها انتخاب نمیکند! اگر به پراپرتی فایل Texts.fa.resx مراجعه کنید این مورد کاملا مشخص است. در نتیجه دیگر فایل cs. حاوی کلاسی جداگانه برای این فایل ساخته نمیشود. همچنین اگر فایل Resource جدید را باز کنید میبنید که برای Access Modifier آن گزینه No Code Generation انتخاب شده است.
در ادامه شروع به افزودن عناوین موردنظر در این دو فایل کنید. در اولی (بدون نام زبان) رشته‌های مربوط به زبان انگلیسی و در دومی رشته‌های مربوط به زبان فارسی را وارد کنید. سپس در هرجایی که یک لیبل یا یک رشته برای نمایش وجود دارد از این کلیدهای Resource استفاده کنید مثل:
<SolutionName>.Resources.Texts.Save
<SolutionName>.Resources.Texts.Cancel

استفاده از Resource در ویومدل ها
دو خاصیت معروفی که در ویومدلها استفاده میشوند عبارتند از: DisplayName و Required. پشتیبانی از کلیدهای Resource به صورت توکار در خاصیت Required وجود دارد. برای استفاده از آنها باید به صورت زیر عمل کرد:
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
در کد بالا باید از نام فایل Resource اصلی (فایل اول که بدون نام کالچر بوده و به عنوان منبع پیشفرض به همراه یک فایل cs. حاوی کلاس مربوطه نیز هست) برای معرفی ErrorMessageResourceType استفاده کرد. چون ابزار توکار ویژوال استودیو از نام این فایل برای تولید کلاس مربوطه استفاده میکند.
متاسفانه خاصیت DisplayName که در فضای نام System.ComponentModel (در فایل System.dll) قرار دارد قابلیت استفاده از کلیدهای Resource را به صورت توکار ندارد. در دات نت 4 خاصیت دیگری در فضای نام System.ComponentModel.DataAnnotations به نام Display (در فایل System.ComponentModel.DataAnnotations.dll) وجود دارد که این امکان را به صورت توکار دارد. اما قابلیت استفاده از این خاصیت تنها در MVC 3 وجود دارد. برای نسخه‌های قدیمیتر MVC امکان استفاده از این خاصیت حتی اگر نسخه فریمورک هدف 4 باشد وجود ندارد، چون هسته این نسخه‌های قدیمی امکان استفاده از ویژگی‌های جدید فریمورک با نسخه بالاتر را ندارد. برای رفع این مشکل میتوان کلاس خاصیت DisplayName را برای استفاده از خاصیت Display به صورت زیر توسعه داد:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  {
    private readonly DisplayAttribute _display;
    public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
    {
      _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName };
    }
    public override string DisplayName
    {
      get
      {
        try
        {
          return _display.GetName();
        }
        catch (Exception)
        {
          return _display.Name;
        }
      }
    }
  }
در این کلاس با ترکیب دو خاصیت نامبرده امکان استفاده از کلیدهای Resource فراهم شده است. در پیاده سازی این کلاس فرض شده است که نسخه فریمورک هدف حداقل برابر 4 است. اگر از نسخه‌های پایین‌تر استفاده میکنید در پیاده سازی این کلاس باید کاملا به صورت دستی کلید موردنظر را از Resource معرفی شده بدست آورید. مثلا به صورت زیر:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
{
    private readonly PropertyInfo nameProperty;
    public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null)
        : base(displayNameKey)
    {
        if (resourceType != null)
            nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
    }
    public override string DisplayName
    {
        get
        {
            if (nameProperty == null) base.DisplayName;
            return (string)nameProperty.GetValue(nameProperty.DeclaringType, null);
        }
    }
}
برای استفاده از این خاصیت جدید میتوان به صورت زیر عمل کرد:
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
البته بیشتر خواص متداول در ویومدلها از ویژگی موردبحث پشتیبانی میکنند.
نکته: به کار گیری این روش ممکن است در پروژه‌های بزرگ کمی گیج کننده و دردسرساز بوده و باعث پیچیدگی بی‌مورد کد و نیز افزایش بیش از حد حجم کدنویسی شود. در مقاله آقای فیل هک (Model Metadata and Validation Localization using Conventions) روش بهتر و تمیزتری برای مدیریت پیامهای این خاصیت‌ها آورده شده است.

پشتیبانی از ویژگی چند زبانه
مرحله بعدی برای چندزبانه کردن پروژه‌های MVC تغییراتی است که برای مدیریت Culture جاری برنامه باید پیاده شوند. برای اینکار باید خاصیت CurrentUICulture در ثرد جاری کنترل و مدیریت شود. یکی از مکانهایی که برای نگهداری زبان جاری استفاده میشود کوکی است. معمولا برای اینکار از کوکی‌های دارای تاریخ انقضای طولانی استفاده میشود. میتوان از تنظیمات موجود در فایل کانفیگ برای ذخیره زبان پیش فرض سیستم نیز استفاه کرد.
روشی که معمولا برای مدیریت زبان جاری میتوان از آن استفاده کرد پیاده سازی یک کلاس پایه برای تمام کنترلرها است. کد زیر راه حل نهایی را نشان میدهد:
public class BaseController : Controller
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    protected override void ExecuteCore()
    {
      var cookie = HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.ExecuteCore();
    }
  }
راه حل دیگر استفاده از یک ActionFilter است که نحوه پیاده سازی یک نمونه از آن در زیر آورده شده است:
public class LocalizationActionFilterAttribute : ActionFilterAttribute
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        filterContext.HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.OnActionExecuting(filterContext);
    }
  }
نکته مهم: تعیین زبان جاری (یعنی همان مقداردهی پراپرتی CurrentCulture ثرد جاری) در یک اکشن فیلتر بدرستی عمل نمیکند. برای بررسی بیشتر این مسئله ابتدا به تصویر زیر که ترتیب رخ‌دادن رویدادهای مهم در ASP.NET MVC را نشان میدهد دقت کنید:

همانطور که در تصویر فوق مشاهده میکنید رویداد OnActionExecuting که در یک اکشن فیلتر به کار میرود بعد از عملیات مدل بایندینگ رخ میدهد. بنابراین قبل از تعیین کالچر جاری، عملیات validation و یافتن متن خطاها از فایلهای Resource انجام میشود که منجر به انتخاب کلیدهای مربوط به کالچر پیشفرض سرور (و نه آنچه که کاربر تنظیم کرده) خواهد شد. بنابراین استفاده از یک اکشن فیلتر برای تعیین کالچر جاری مناسب نیست. راه حل مناسب استفاده از همان کنترلر پایه است، زیرا متد ExecuteCore قبل از تمامی این عملیات صدا زده میشود. بنابرابن همیشه کالچر تنظیم شده توسط کاربر به عنوان مقدار جاری آن در ثرد ثبت میشود.

امکان تعیین/تغییر زبان توسط کاربر
برای تعیین یا تغییر زبان جاری سیستم نیز روشهای گوناگونی وجود دارد. استفاده از زبان تنظیم شده در مرورگر کاربر، استفاده از عنوان زبان در آدرس صفحات درخواستی و یا تعیین زبان توسط کاربر در تنظیمات برنامه/سایت و ذخیره آن در کوکی یا دیتابیس و مواردی از این دست روشهایی است که معمولا برای تعیین زبان جاری از آن استفاده میشود. در کدهای نمونه ای که در بخشهای قبل آورده شده است فرض شده است که زبان جاری سیستم درون یک کوکی ذخیره میشود بنابراین برای استفاده از این روش میتوان از قطعه کدی مشابه زیر (مثلا در فایل Layout.cshtml_) برای تعیین و تغییر زبان استفاه کرد:
<select id="langs" onchange="languageChanged()">
  <option value="fa-IR">فارسی</option>
  <option value="en-US">انگلیسی</option>
</select>
<script type="text/javascript">
  function languageChanged() {
    setCookie("MyLanguageCookieName", $('#langs').val(), 365);
    window.location.reload();
  }
  document.ready = function () {
    $('#langs').val(getCookie("MyLanguageCookieName"));
  };
  function setCookie(name, value, exdays, path) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ;
    document.cookie = name + "=" + newValue;
  }
  function getCookie(name) {
    var i, x, y, cookies = document.cookie.split(";");
    for (i = 0; i < cookies.length; i++) {
      x = cookies[i].substr(0, cookies[i].indexOf("="));
      y = cookies[i].substr(cookies[i].indexOf("=") + 1);
      x = x.replace(/^\s+|\s+$/g, "");
      if (x == name) {
        return unescape(y);
      }
    }
  }
</script> 
متدهای setCookie و getCookie جاوا اسکریپتی در کد بالا از اینجا گرفته شده اند البته پس از کمی تغییر.
نکته: مطلب Cookieها بحثی نسبتا مفصل است که در جای خودش باید به صورت کامل آورده شود. اما در اینجا تنها به همین نکته اشاره کنم که عدم توجه به پراپرتی path کوکی‌ها در این مورد خاص برای خود من بسیار گیج‌کننده و دردسرساز بود. 
به عنوان راهی دیگر میتوان به جای روش ساده استفاده از کوکی، تنظیماتی در اختیار کاربر قرار داد تا بتواند زبان تنظیم شده را درون یک فایل یا دیتابیس ذخیره کرد البته با درنظر گرفتن مسائل مربوط به کش کردن این تنظیمات.
راه حل بعدی میتواند استفاده از تنظیمات مرورگر کاربر برای دریافت زبان جاری تنظیم شده است. مرورگرها تنظیمات مربوط به زبان را در قسمت Accept-Languages در HTTP Header درخواست ارسالی به سمت سرور قرار میدهند. بصورت زیر:
GET https://www.dntips.ir HTTP/1.1
...
Accept-Language: fa-IR,en-US;q=0.5
...
این هم تصویر مربوط به Fiddler آن:

نکته: پارامتر q در عبارت مشخص شده در تصویر فوق relative quality factor نام دارد و به نوعی مشخص کننده اولویت زبان مربوطه است. مقدار آن بین 0 و 1 است و مقدار پیش فرض آن 1 است. هرچه مقدار این پارامتر بیشتر باشد زبان مربوطه اولویت بالاتری دارد. مثلا عبارت زیر را درنظر بگیرید:
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
در این حالت اولویت زبان fa-IR برابر 1 و fa برابر 0.8 (fa;q=0.8) است. اولویت دیگر زبانهای تنظیم شده نیز همانطور که نشان داده شده است در مراتب بعدی قرار دارند. در تنظیم نمایش داده شده برای تغییر این تنظیمات در IE میتوان همانند تصویر زیر اقدام کرد:

در تصویر بالا زبان فارسی اولویت بالاتری نسبت به انگلیسی دارد. برای اینکه سیستم g11n دات نت به صورت خودکار از این مقادیر جهت زبان ثرد جاری استفاده کند میتوان از تنظیم زیر در فایل کانفیگ استفاده کرد:
<system.web>
    <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization>
</system.web>
در سمت سرور نیز برای دریافت این مقادیر تنظیم شده در مرورگر کاربر میتوان از کدهای زیر استفاه کرد. مثلا در یک اکشن فیلتر:
var langs = filterContext.HttpContext.Request.UserLanguages;
پراپرتی UserLanguages از کلاس Request حاوی آرایه‌ای از استرینگ است. این آرایه درواقع از Split کردن مقدار Accept-Languages با کاراکتر ',' بدست می‌آید. بنابراین اعضای این آرایه رشته‌ای از نام زبان به همراه پارامتر q مربوطه خواهند بود (مثل "fa;q=0.8").
راه دیگر مدیریت زبانها استفاده از عنوان زبان در مسیر درخواستی صفحات است. مثلا آدرسی شبیه به www.MySite.com/fa/Employees نشان میدهد کاربر درخواست نسخه فارسی از صفحه Employees را دارد. نحوه استفاده از این عناوین و نیز موقعیت فیزیکی این عناوین در مسیر صفحات درخواستی کاملا به سلیقه برنامه نویس و یا کارفرما بستگی دارد. روش کلی بهره برداری از این روش در تمام موارد تقریبا یکسان است.
برای پیاده سازی این روش ابتدا باید یک route جدید در فایل Global.asax.cs اضافه کرد:
routes.MapRoute(
    "Localization", // Route name
    "{lang}/{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
دقت کنید که این route باید قبل از تمام routeهای دیگر ثبت شود. سپس باید کلاس پایه کنترلر را به صورت زیر پیاده سازی کرد:
public class BaseController : Controller
{
  protected override void ExecuteCore()
  {
    var lang = RouteData.Values["lang"];
    if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString()))
    {
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString());
    }
    base.ExecuteCore();
  }
}
این کار را در یک اکشن فیلتر هم میتوان انجام داد اما با توجه به توضیحاتی که در قسمت قبل داده شد استفاده از اکشن فیلتر برای تعیین زبان جاری کار مناسبی نیست.
نکته: به دلیل آوردن عنوان زبان در مسیر درخواستها باید کتترل دقیقتری بر کلیه مسیرهای موجود داشت!

استفاده از ویوهای جداگانه برای زبانهای مختلف
برای اینکار ابتدا ساختار مناسبی را برای نگهداری از ویوهای مختلف خود درنظر بگیرید. مثلا میتوانید همانند نامگذاری فایلهای Resource از نام زبان یا کالچر به عنوان بخشی از نام فایلهای ویو استفاده کنید و تمام ویوها را در یک مسیر ذخیره کنید. همانند تصویر زیر:

البته اینکار ممکن است به مدیریت این فایلها را کمی مشکل کند چون به مرور زمان تعداد فایلهای ویو در یک فولدر زیاد خواهد شد. روش دیگری که برای نگهداری این ویوها میتوان به کار برد استفاده از فولدرهای جداگانه با عناوین زبانهای موردنظر است. مانند تصویر زیر:

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

استفاه از هرکدام از این روشها کاملا به سلیقه و راحتی مدیریت فایلها برای برنامه نویس بستگی دارد. درهر صورت پس از انتخاب یکی از این روشها باید اپلیکشن خود را طوری تنظیم کنیم که با توجه به زبان جاری سیستم، ویوی مربوطه را جهت نمایش انتخاب کند.
مثلا برای روش اول نامگذاری ویوها میتوان از روش دستکاری متد OnActionExecuted در کلاس پایه کنترلر استفاده کرد:
public class BaseController : Controller
{
  protected override void OnActionExecuted(ActionExecutedContext context)
  {
    var view = context.Result as ViewResultBase;
    if (view == null) return; // not a view
    var viewName = view.ViewName;
    view.ViewName = GetGlobalizationViewName(viewName, context);
    base.OnActionExecuted(context);
  }
  private static string GetGlobalizationViewName(string viewName, ControllerContext context)
  {
    var cultureName = Thread.CurrentThread.CurrentUICulture.Name;
    if (cultureName == "en-US") return viewName; // default culture
    if (string.IsNullOrEmpty(viewName))
      return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa"
    int i;
    if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml
      return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml"
    return viewName + "." + cultureName; // "Index" ==> "Index.fa"
  }
}
همانطور که قبلا نیز شرح داده شد، چون متد ExecuteCore قبل از OnActionExecuted صدا زده میشود بنابراین از تنظیم درست مقدار کالچر در ثرد جاری اطمینان داریم.
روش دیگری که برای مدیریت انتخاب ویوهای مناسب استفاده از یک ویوانجین شخصی سازی شده است. مثلا برای روش سوم نامگذاری ویوها میتوان از کد زیر استفاده کرد:
public sealed class RazorGlobalizationViewEngine : RazorViewEngine
  {
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
      return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath));
    }
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
      return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath);
    }
    private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath)
    {
      //var controllerName = controllerContext.RouteData.GetRequiredString("controller");
      var request = controllerContext.HttpContext.Request;
      var lang = request.Cookies["MyLanguageCookie"];
      if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US")
      {
        var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value));
        if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath;
      }
      return viewPath;
    }
و برای ثبت این ViewEngine در فایل Global.asax.cs خواهیم داشت:
protected void Application_Start()
{
  ViewEngines.Engines.Clear();
  ViewEngines.Engines.Add(new RazorGlobalizationViewEngine());
}

محتوای یک فایل Resource
ساختار یک فایل resx. به صورت XML استاندارد است. در زیر محتوای یک نمونه فایل Resource با پسوند resx. را مشاهده میکنید:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema ...
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   ...
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="RightToLeft" xml:space="preserve">
    <value>false</value>
    <comment>RightToleft is false in English!</comment>
  </data>
</root>
در قسمت ابتدایی تمام فایلهای resx. که توسط ویژوال استودیو تولید میشود کامنتی طولانی وجود دارد که به صورت خلاصه به شرح محتوا و ساختار یک فایل Resource میپردازد. در ادامه تگ نسبتا طولانی xsd:schema قرار دارد. از این قسمت برای معرفی ساختار داده ای فایلهای XML استفاده میشود. برای آشنایی بیشتر با XSD (یا XML Schema) به اینجا مراجعه کنید. به صورت خلاصه میتوان گفت که XSD برای تعیین ساختار داده‌ها یا تعیین نوع داده ای اطلاعات موجود در یک فایل XML به کار میرود. درواقع تگهای XSD به نوعی فایل XML ما را Strongly Typed میکند. با توجه به اطلاعات این قسمت، فایلهای resx. شامل 4 نوع گره اصلی هستند که عبارتند از: metadata و assembly و data و resheader. در تعریف هر یک از گره‌ها در این قسمت مشخصاتی چون نام زیر گره‌های قابل تعریف در هر گره و نام و نوع خاصیتهای هر یک معرفی شده است.
بخش موردنظر ما در این مطلب قسمت انتهایی این فایلهاست (تگهای resheader و data). همانطور در بالا مشاهده میکنید تگهای reheader شامل تنظیمات مربوط به فایل resx. با ساختاری ساده به صورت name/value است. یکی از این تنظیمات resmimetype فایل resource را معرفی میکند که درواقع مشخص کننده نوع محتوای (Content Type) فایل XML است(^). برای فایلهای resx این مقدار برابر text/microsoft-resx است. تنظیم بعدی نسخه مربوط به فایل resx (یا Microsoft ResX Schema) را نشان میدهد. در حال حاضر نسخه جاری (در VS 2010) برابر 2.0 است. تنظیم بعدی مربوط به کلاسهای reader و writer تعریف شده برای استفاده از این فایلهاست. به نوع این کلاسهای خواننده و نویسنده فایلهای resx. و مکان فیزیکی و فضای نام آنها دقت کنید که در مطالب بعدی از آنها برای ویرایش و بروزرسانی فایلهای resource در زمان اجرا استفاده خواهیم کرد.
در پایان نیز تگهای data که برای نگهداری داده‌ها از آنها استفاده میشود. هر گره data شامل یک خاصیت نام (name) و یک زیرگره مقدار (value) است. البته امکان تعیین یک کامنت در زیرگره comment نیز وجود دارد که اختیاری است. هر گره data مینواند شامل خاصیت type و یا mimetype نیز باشد. خاصیت type مشخص کننده نوعی است که تبدیل text/value را با استفاده از ساختار TypeConverter پشتیبانی میکند. البته اگر در نوع مشخص شده این پشتیبانی وجود نداشته باشد، داده موردنظر پس از سریالایز شدن با فرمت مشخص شده در خاصیت mimetype ذخیره میشود. این mimetype اطلاعات موردنیاز را برای کلاس خواننده این فایلها (ResXResourceReader به صورت پیشفرض) جهت چگونگی بازیابی آبجکت موردنظر فراهم میکند. مشخص کردن این دو خاصیت برای انواع رشته ای نیاز نیست. انواع mimetype قابل استفاده عبارتند از:
- application/x-microsoft.net.object.binary.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Binary.BinaryFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود (راجع به انکدینگ base64 ^ و ^).
- application/x-microsoft.net.object.soap.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Soap.SoapFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
- application/x-microsoft.net.object.bytearray.base64: آبجکت ابتدا باید با استفاده از یک System.ComponentModel.TypeConverter به آرایه ای از بایت سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
نکته: امکان جاسازی کردن (embed) فایلهای resx. در یک اسمبلی یا کامپایل مستقیم آن به یک سَتِلایت اسمبلی (ترجمه مناسبی برای satellite assembly پیدا نکردم، چیزی شبیه به اسمبلی قمری یا وابسته و از این قبیل ...) وجود ندارد. ابتدا باید این فایلهای resx. به فایلهای resources. تبدیل شوند. اینکار با استفاده از ابزار Resource File Generator (نام فایل اجرایی آن resgen.exe است) انجام میشود (^ و ^). سپس میتوان با استفاده از Assembly Linker ستلایت اسمبلی مربوطه را تولید کرد (^). کل این عملیات در ویژوال استودیو با استفاده از ابزار msbuild به صورت خودکار انجام میشود!

نحوه یافتن کلیدهای Resource در بین فایلهای مختلف Resx توسط پرووایدر پیش فرض در دات نت
عملیات ابتدا با بررسی خاصیت CurrentUICulture از ثرد جاری آغاز میشود. سپس با استفاده از عنوان استاندارد کالچر جاری، فایل مناسب Resource یافته میشود. در نهایت بهترین گزینه موجود برای کلید درخواستی از منابع موجود انتخاب میشود. مثلا اگر کالچر جاری fa-IR و کلید درخواستی از کلاس Texts باشد ابتدا جستجو برای یافتن فایل Texts.fa-IR.resx آغاز میشود و اگر فایل موردنظر یا کلید درخواستی در این فایل یافته نشد جستجو در فایل Texts.fa.resx ادامه می‌یابد. اگر باز هم یافته نشد درنهایت این عملیات جستجو در فایل resource اصلی خاتمه می‌یابد و مقدار کلید منبع پیش فرض به عنوان نتیجه برگشت داده میشود. یعنی در تمامی حالات سعی میشود تا دقیقترین و بهترین و نزدیکترین نتیجه انتخاب شود. البته درصورتیکه از یک پرووایدر شخصی سازی شده برای کار خود استفاده میکنید باید چنین الگوریتمی را جهت یافتن کلیدهای منابع خود از فایلهای Resource (یا هرمنبع دیگر مثل دیتابیس یا حتی یک وب سرویس) درنظر بگیرید.

Globalization در کلاینت (javascript g11n)
یکی دیگر از موارد استفاده g11n در برنامه نویسی سمت کلاینت است. با وجود استفاده گسترده از جاوا اسکریپت در برنامه نویسی سمت کلاینت در وب اپلیکیشنها، متاسفانه تا همین اواخر عملا ابزار یا کتابخانه مناسبی برای مدیریت g11n در این زمینه وجود نداشته است. یکی از اولین کتابخانه‌های تولید شده در این زمینه کتابخانه jQuery Globalization است که توسط مایکروسافت توسعه داده شده است (برای آشنایی بیشتر با این کتابخانه به ^ و ^ مراجعه کنید). این کتابخانه بعدا تغییر نام داده و اکنون با عنوان Globalize شناخته میشود. Globalize یک کتابخانه کاملا مستقل است که وابستگی به هیچ کتابخانه دیگر ندارد (یعنی برای استفاده از آن نیازی به jQuery نیست). این کتابخانه حاوی کالچرهای بسیاری است که عملیات مختلفی چون فرمت و parse انواع داده‌ها را نیز در سمت کلاینت مدیریت میکند. همچنین با فراهم کردن منابعی حاوی جفتهای key/culture میتوان از مزایایی مشابه مواردی که در این مطلب بحث شد در سمت کلاینت نیز بهره برد. نشانی این کتابخانه در github اینجا است. با اینکه خود این کتابخانه ابزار کاملی است اما در بین کالچرهای موجود در فایلهای آن متاسفانه پشتیبانی کاملی از زبان فارسی نشده است. ابزار دیگری که برای اینکار وجود دارد پلاگین jquery localize است که برای بحث g11n رشته‌ها پیاده‌سازی بهتر و کاملتری دارد.

در مطالب بعدی به مباحث تغییر مقادیر کلیدهای فایلهای resource در هنگام اجرا با استفاده از روش مستقیم تغییر محتوای فایلها و کامپایل دوباره توسط ابزار msbuild و نیز استفاده از یک ResourceProvider شخصی سازی شده به عنوان یک راه حل بهتر برای اینکار میپردازم.
در تهیه این مطلب از منابع زیر استفاده شده است:

بازخوردهای دوره
بررسی کارآیی و ایندکس گذاری بر روی اسناد XML در SQL Server - قسمت دوم
یک نکته‌ی تکمیلی
از SQL Server 2012 SP1 به بعد، ایندکس جدیدی به نام Selective XML Indexes به مجموعه‌ی ایندکس‌های قابل تعریف بر روی یک ستون XML ایی اضافه شده‌است و این مزایا را به همراه دارد:
- برخلاف ایندکس‌های اولیه و ثانویه بحث شده در مطلب جاری، کل محتوای سند را ایندکس نمی‌کنند. به همین جهت حجم کمتری را اشغال کرده و سرعت Insert و Update را کاهش نمی‌دهند.
- با استفاده از Selective XML Indexes تنها XPathهایی را که مشخص می‌کنید، ایندکس خواهند شد. بنابراین بر اساس کوئری‌های موجود، می‌توان ایندکس‌های بهتری را تعریف کرد.

این نوع ایندکس‌ها به صورت پیش فرض فعال نبوده و نیاز است از طریق رویه ذخیره شده سیستمی sp_db_selective_xml_index، فعال شوند.
 sys.sp_db_selective_xml_index @dbname = 'dbname', @selective_xml_index = 'action: on|off|true|false'
و پس از آن برای تعریف یک ایندکس انتخابی خواهیم داشت:
 create selective xml index index_name
on table_name(column_name)
for (<path>)
قسمت path آن برای مثال در عمل، چنین شکلی را می‌تواند داشته باشد:
 for(
 pathColor = '/Item/Product/Color' as SQL nvarchar(20),
 pathSize = '/Item/Product/Size' as SQL int
)  
نظرات مطالب
ساخت یک Form Generator ساده در MVC
امکان کوئری گرفتن پیچیده‌ی از آن مشکلی ندارد؛ ولی باید دوره‌ی XML را یکبار مرور کرده باشید. البته فیلدهای JSON هم به SQL Server 2016 اضافه شده‌اند که بحث دیگری است و می‌تواند مدنظر باشد.
مطالب
تولید اطلاعات تصادفی توسط GenFu
گاها برای تولید اطلاعات تصادفی، خصوصا هنگام نوشتن تست‌ها، زمان زیادی بیهوده تلف شده و حجم زیادی کد اضافه تولید میشود. کتابخانه‌ای بنام GenFu ایجاد شده که وظیفه ایجاد داده‌های تصادفی را بر عهده گرفته‌است. این کتابخانه متن باز (Open Source) بوده و می‌توانید آن را از مخزن گیت‌هاب دریافت نمایید.

در مطلب جاری قصد ایجاد اطلاعات تصادفی برای کلاس زیر را داریم :
public class Person
{
    public int ID { get; set; }

    public string Firstname { get; set; }

    public string Lastname { get; set; }

    public string Email { get; set; }

    public string PhoneNumber { get; set; }

    public override string ToString()
    {
        return $"{ID}: {Firstname} {Lastname} - {Email} - {PhoneNumber}";
    }
}

نصب GenFu

برای نصب کتابخانه GenFu از دستور زیر در Package Manager Console استفاده میکنیم :
Install-Package GenFu

1. ایجاد یک شخص

برای ایجاد شخصی جدید همراه با اطلاعاتی تصادفی به شکل زیر عمل خواهیم کرد :
var person = A.New<Person>();
Console.WriteLine(person);
نتیجه کد فوق به این صورت خواهد بود :
18: Diedra Morgan - Zachary.Garcia@telus.net - (531) 273-9001
اگر دقت کنید متوجه میشوید که GenFu بصورت خودکار داده‌هایی مرتبط با Property هایی که نام گذاری کردید‌، ایجاد کرده‌است.
برای Email، داده‌ای با فرمت صحیح ایمیل و برای PhoneNumber هم شماره تلفنی با فرمت صحیح تولید شده است.

2. ایجاد چند شخص

برای ایجاد لیستی از اشخاص نیز میتوانید از متد ListOf استفاده کرده و تعداد اشخاص مورد نیازتان را به آن ارسال کنید ( پیشفرض 25 ) :
var people = A.ListOf<Person>(5);
people.ForEach(Console.WriteLine);
کد فوق باعث ایجاد 5 شخص با اطلاعات تصادفی متفاوتی خواهد شد:
97: Maria MacKenzie - Alexandra.Johnson@rogers.ca - (670) 787-3053
34: Alexander Scott - Isaiah.Price@gmail.com - (730) 645-4946
66: Kevin Perez - Gabrielle.Alexander@hotmail.com - (230) 758-8233
81: Maria Evans - Vanessa.Bell@rogers.ca - (508) 572-4343
79: Tyler Parker - Alyssa.Taylor@telus.net - (297) 357-7617

تا به اینجای کار GenFu بخوبی جوابگوی نیازهایمان بوده‌است. اما اگر پیشفرض‌ها جوابگو نبود و بخواهیم فرمت داده‌های تولید شده را تغییر دهیم چطور ؟
برای اینکار میتوانیم از متد Configure استفاده کرده و نحوه ایجاد داده را برای Property هایی که مشخص میکنیم، تغییر دهیم.

3. ایجاد چند شخص و مقدارهی یک property با مقدار ثابت

اگر بخواهیم داده‌های ایجاد شده را داخل دیتابیس لحاظ کنیم، نیاز داریم تا ID آن‌ها را برابر 0 قرار دهیم تا دیتابیس مشکلی برای ثبتشان نداشته باشد. برای ایجاد لیستی از اشخاص که ID آن‌ها برابر 0 باشد :
A.Configure<Person>().Fill(x => x.ID, 0);

var people = A.ListOf<Person>(5);
people.ForEach(Console.WriteLine);
نتیجه :
0: Darron Gonzalez - Benjamin.Daeninck@hotmail.com - (405) 418-7783
0: Melanie Garcia - Jennifer.Griffin@microsoft.com - (711) 277-8826
0: James Hughes - Tristan.Ward@live.com - (734) 400-8322
0: Miranda Torres - Ross.Davis@rogers.ca - (495) 479-8147
0: David Hughes - Jillian.Alexander@live.com - (361) 617-6642
در این حالت بدون هیچگونه مشکلی میتوانید داده‌های ایجاد شده را داخل دیتابیس ذخیره نمایید.

4. ایجاد چند شخص و مقداردهی یک property با متد

حالت دیگری که میتوانید نحوه مقداردهی یک Property را تنظیم کنید، استفاده از متد یا delegate است:
var i = 1;

A.Configure<Person>()
    .Fill(c => c.ID, () => i++);

var people = A.ListOf<Person>(5);
people.ForEach(Console.WriteLine);

نتیجه :
1: Paul Long - Carlos.Kelly@telus.net - (202) 573-6278
2: Jesse Iginla - Liberty.Moore@gmail.com - (589) 791-3606
3: Raymundo Price - Ang.Taylor@live.com - (336) 400-1601
4: Elizabeth Getzlaff - Leslie.Campbell@att.com - (662) 582-9010
5: Abigail Bailey - Tristan.Ross@live.com - (225) 661-7023
همانطور که می‌بینید، ID اشخاص بصورت تصاعدی مقداردهی شده است.

5. ایجاد چند شخص و مقداردهی یک property با مقادیر property‌های دیگر

همچنین میتوانید از مقادیر Property‌های دیگر برای مقداردهی یک Property استفاده کنید :
A.Configure<Person>()
    .Fill(c => c.ID, 0)
    .Fill(c => c.Email,
        c => $"{c.Firstname}.{c.Lastname}@gmail.com");

var people = A.ListOf<Person>(5);
people.ForEach(Console.WriteLine);
کد فوق باعث تولید اشخاصی میشود که ایمیل آن‌ها برابر (Firstname).(Lastname) خواهد بود :
0: Patrick Perry - Patrick.Perry@gmail.com - (796) 460-6576
0: Rebecca Main - Rebecca.Main@gmail.com - (757) 472-3332
0: Kimberly Carter - Kimberly.Carter@gmail.com - (436) 484-8273
0: Sara Lewis - Sara.Lewis@gmail.com - (424) 717-7682
0: Lauren Ross - Lauren.Ross@gmail.com - (277) 294-5776

6. استفاده از Extension‌های درون ساخت GenFu برای مقداردهی

GenFu دارای Extension هایی بوده که باعث میشوند اطلاعات یک Property با مقادیر قابل درک و مشخصی پر شوند.
مثال :
A.Configure<Person>()
    .Fill(x => x.Firstname).AsPersonTitle();

var people = A.ListOf<Person>(5);
people.ForEach(Console.WriteLine);  
نتیجه :
64: Miss. Ratzlaff - Bryce.Simmons@att.com - (386) 309-2414
7: Air Marshall Yarobi - Ariana.Russell@att.com - (459) 238-0717
96: Air Marshall Taylor - Luke.Olsen@gmail.com - (775) 401-5281
28: Doctor Cox - Leah.Diaz@att.com - (569) 464-7961
99: Master Phillips - Chloe.Scott@hotmail.com - (578) 221-9021

7. GenFu WireFrame

در نهایت GenFu دارای پکیج جانبی به اسم Wireframes است که شامل HTML Helper هایی است که با استفاده از آن‌ها میتوانید المان‌های HTML مانند P, Image, Table و ... را با مقادیری برای تست بعنوان Placeholder ایجاد کنید. 
برای نصب و مطالعه بیشتر درباره GenFu WireFrames این لینک را ببینید.
مطالب
استفاده از SQLDom برای آنالیز عبارات T-SQL
به همراه بسته Features pack اس کیوال سرور 2012، دو بسته SqlDom.msi نیز وجود دارند (نسخه‌های X86 و X64). این بسته حاوی اسمبلی Microsoft.SqlServer.TransactSql.ScriptDom.dll می‌باشد که نهایتا در آدرس Program Files\Microsoft SQL Server\110\SDK\Assemblies کپی خواهد شد.
به کمک آن می‌توان عبارات پیچیده T-SQL را Parse و آنالیز کرد. البته باید در نظر داشت هرچند این بسته جهت SQL Server 2012 ارائه شده اما این اسمبلی با نگارش‌های 2005 به بعد اس کیوال سرور کاملا سازگار است و اساسا نیازی هم به SQL Server ندارد. در ادامه مروری خواهیم داشت بر نحوه استفاده از آن.


یافتن کوئری‌های * Select در بین انبوهی از اسکریپت‌ها به کمک SQLDom

در مورد مضرات کوئری‌های * select پیشتر مطلبی را در این سایت خوانده‌اید. در ادامه قصد داریم به کمک امکانات اسمبلی Microsoft.SqlServer.TransactSql.ScriptDom.dll، تعدادی عبارت T-SQL را آنالیز کرده و مشخص کنیم که آیا حاوی * select هستند یا خیر. کد کامل آن‌را در ذیل مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace DbCop
{
    // Microsoft® SQL Server® 2012 Transact-SQL ScriptDom 
    // SQL Server 2012 managed parser, Supports SQL Server 2005+
    // SQLDom.msi (redist x86/x64)
    // http://www.microsoft.com/en-us/download/details.aspx?id=29065
    // X86: http://go.microsoft.com/fwlink/?LinkID=239634&clcid=0x409
    // X64: http://go.microsoft.com/fwlink/?LinkID=239635&clcid=0x409
    // Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll

    class Program
    {
        static void Main()
        {
            const string tSql = @"
                -- select * in PROCEDURE
                CREATE PROCEDURE dbo.SelectStarTest
                AS
                SELECT * FROM dbo.tbl1
                go

                -- select * in PROCEDURE with TableVar
                Create PRocedure SelectAll
                AS
                Declare @X table(Id integer)
                Select * from @x
                go

                -- select * in PROCEDURE with ctex
                CREATE PROCEDURE dbo.SelectAllCte
                AS 
                WITH ctex
                AS (
                SELECT * FROM sys.objects
                )
                SELECT * FROM ctex
                go

                -- normal select *
                select * from tbl1; 
                select * from dbo.tbl2;
            ";

            IList<ParseError> errors;
            TSqlScript sqlFragment;
            using (var reader = new StringReader(tSql))
            {
                var parser = new TSql110Parser(initialQuotedIdentifiers: true);
                sqlFragment = (TSqlScript)parser.Parse(reader, out errors);
            }

            if (errors != null && errors.Any())
            {
                var sb = new StringBuilder();
                foreach (var error in errors)
                    sb.AppendLine(error.Message);

                throw new InvalidOperationException(sb.ToString());
            }

            var i = 0;
            foreach (var batch in sqlFragment.Batches)
            {
                Console.WriteLine("Batch: {0}, Statement(s): {1}", ++i, batch.Statements.Count);
                foreach (var statement in batch.Statements)
                {
                    processStatement(statement);
                }
                Console.WriteLine();
            }

            Console.WriteLine("\nPress a key...");
            Console.Read();
        }

        private static void processStatement(TSqlStatement statement)
        {
            var createProcedureStatement = statement as CreateProcedureStatement;
            if (createProcedureStatement != null)
            {
                var statementList = createProcedureStatement.StatementList;
                foreach (var procedureStatement in statementList.Statements)
                {
                    processStatement(procedureStatement);
                }
            }

            var selectStatement = statement as SelectStatement;
            if (selectStatement != null)
            {
                var query = selectStatement.QueryExpression;
                var selectElements = ((QuerySpecification)query).SelectElements;
                foreach (var selectElement in selectElements)
                {
                    var expression = selectElement as SelectStarExpression;
                    if (expression == null) continue;
                    Console.WriteLine(
                        "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}",
                        expression.StartOffset,
                        expression.StartLine,
                        statementToString(selectStatement));
                }
            }
        }

        private static string statementToString(TSqlFragment selectStatement)
        {
            var text = new StringBuilder();
            for (var i = selectStatement.FirstTokenIndex; i <= selectStatement.LastTokenIndex; i++)
            {
                text.Append(selectStatement.ScriptTokenStream[i].Text);
            }
            return text.ToString();
        }
    }
}

توضیحات:
پس از نصب SQLDom.msi، ارجاعی را به اسمبلی زیر اضافه نمائید تا بتوانید کد فوق را کامپایل کنید:
Program Files\Microsoft SQL Server\110\SDK\Assemblies\Microsoft.SqlServer.TransactSql.ScriptDom.dll

کار با ایجاد وهله‌ای از TSql110Parser شروع می‌شود. متد Parse آن، آرگومانی از نوع TextReader را قبول می‌کند. برای مثال با استفاده از StringReader می‌توان محتوای یک متغیر رشته‌ای را به آن ارسال کرد و یا توسط StreamReader یک فایل sql را.
پس از فراخوانی متد Parse، بهتر است بررسی شود که آیا عبارت T-SQL دریافتی معتبر بوده است یا خیر. اینکار را توسط لیستی از ParseError‌های دریافتی می‌توان انجام داد.
خروجی متد Parse، حاوی یک سری Batch آنالیز شده است. هر عبارت Go در اینجا یک Batch را تشکیل می‌دهد. سپس در داخل هر batch به دنبال batch.Statements خواهیم گشت تا بتوان به عبارات T-SQL آن‌ها دسترسی یافت.
در ادامه کار اصلی توسط متد processStatement صورت می‌گیرد. عبارات دریافتی، در حالت کلی از نوع TSqlStatement هستند اما در اصل می‌توانند یکی از مشتقات آن نیز باشند. در اینجا فقط دو مورد CreateProcedureStatement و SelectStatement بررسی شده‌اند (مطابق رشته tSql ابتدای مثال). هر دو عبارت، از کلاس TSqlStatement مشتق شده‌اند.
در متد processStatement عبارات select معمولی و همچنین آن‌هایی که داخل رویه‌های ذخیره شده تعریف شده‌اند، استخراج شده و در نهایت بررسی می‌شوند که آیا از نوع SelectStarExpression هستند یا خیر (همان * select صورت مساله).
خروجی مثال فوق به شرح زیر است:
Batch: 1, Statement(s): 1
`Select *` detected @StartOffset:140, Line:5, T-SQL: SELECT * FROM dbo.tbl1

Batch: 2, Statement(s): 1
`Select *` detected @StartOffset:368, Line:12, T-SQL: Select * from @x

Batch: 3, Statement(s): 1
`Select *` detected @StartOffset:659, Line:22, T-SQL: WITH ctex
                AS (
                SELECT * FROM sys.objects
                )
                SELECT * FROM ctex

Batch: 4, Statement(s): 2
`Select *` detected @StartOffset:753, Line:26, T-SQL: select * from tbl1;
`Select *` detected @StartOffset:791, Line:27, T-SQL: select * from dbo.tbl2;