مطالب
بررسی ویجت Kendo UI File Upload
Kendo UI به همراه یک ویجت وب مخصوص ارسال فایل‌ها به سرور نیز هست. این ویجت قابلیت ارسال چندین فایل با هم را به صورت Ajax ایی دارا است و همچنین کاربران می‌توانند فایل‌ها را با کشیدن و رها کردن بر روی آن، به لیست فایل‌های قابل ارسال اضافه کنند.
ارسال فایل Ajax ایی آن توسط HTML5 File API صورت می‌گیرد که در تمام مرورگرهای جدید پشتیبانی خوبی از آن وجود دارد. در مرورگرهای قدیمی‌تر، به صورت خودکار همان حالت متداول ارسال همزمان فایل‌ها را فعال می‌کند (یا همان post back معمولی).

فعال سازی مقدماتی kendoUpload

ابتدایی‌ترین حالت کار با kendoUpload، فعال سازی حالت post back معمولی است؛ به شرح زیر:
<form method="post" action="submit" enctype="multipart/form-data">
  <div>
    <input name="files" id="files" type="file" />    
    <input type="submit" value="Submit" class="k-button" />
  </div>
</form>
<script>
  $(document).ready(function() {
     $("#files").kendoUpload();
  });
</script>
در این حالت صرفا input با نوع file، با ظاهری سازگار با سایر کنترل‌های Kendo UI به نظر می‌رسد و عملیات ارسال فایل، همانند قبل به همراه یک post back است. این روش برای حالتی مفید است که بخواهید یک فایل را به همراه سایر عناصر فرم در طی یک مرحله به سمت سرور ارسال کنید.


فعال سازی حالت ارسال فایل Ajax ایی kendoUpload

برای فعال سازی ارسال Ajax ایی فایل‌ها در Kendo UI نیاز است خاصیت async آن‌را به نحو ذیل مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: { // async configuration
                    saveUrl: "@Url.Action("Save", "Home")", // the url to save a file is '/save'
                    removeUrl: "@Url.Action("Remove", "Home")", // the url to remove a file is '/remove'
                    autoUpload: false, // automatically upload files once selected
                    removeVerb: 'POST'
                },
                multiple: true,
                showFileList: true
            }); 
        });
    </script>
در اینجا دو آدرس ذخیره سازی فایل‌ها و همچنین حذف آن‌ها را مشاهده می‌کنید. امضای این دو اکشن متد در ASP.NET MVC به صورت ذیل هستند:
        [HttpPost]
        public ActionResult Save(IEnumerable<HttpPostedFileBase> files)
        {
            if (files != null)
            {
                // ...
                // Process the files and save them
                // ...
            }

            // Return an empty string to signify success
            return Content("");
        }

        [HttpPost]
        public ContentResult Remove(string[] fileNames)
        {
            if (fileNames != null)
            {
                foreach (var fullName in fileNames)
                {
                    // ...
                    // delete the files
                    // ...
                }
            }

            // Return an empty string to signify success
            return Content("");
        }
در هر دو حالت، لیستی از فایل‌ها توسط kendoUpload به سمت سرور ارسال می‌شوند. در حالت Save، محتوای این فایل‌ها جهت ذخیره سازی بر روی سرور در دسترس خواهد بود. در حالت Remove، صرفا نام این فایل‌ها برای حذف از سرور، توسط کاربر ارسال می‌شوند.
دو دکمه‌ی حذف با کارکردهای متفاوت در ویجت kendoUpload وجود دارند. در ابتدای کار، پیش از ارسال فایل‌ها به سرور:


کلیک بر روی دکمه‌ی حذف در این حالت، صرفا فایلی را از لیست سمت کاربر حذف می‌کند.

پس از ارسال فایل‌ها به سرور:


اما پس از پایان عملیات ارسال، اگر کاربر بر روی دکمه‌ی حذف کلیک کند، توسط آدرس مشخص شده توسط خاصیت removeUrl، نام فایل‌های مورد نظر، برای حذف از سرور ارسال می‌شوند.


چند نکته‌ی تکمیلی
- تنظیم خاصیت autoUpload به true سبب می‌شود تا پس از انتخاب فایل‌ها توسط کاربر، بلافاصله و به صورت خودکار عملیات ارسال فایل‌ها به سرور آغاز شوند. اگر به false تنظیم شود، دکمه‌ی ارسال فایل‌ها در پایین لیست نمایش داده خواهد شد.
- شاید علاقمند باشید تا removeVerb را به DELETE تغییر دهید؛ بجای POST. به همین منظور می‌توان خاصیت removeVerb در اینجا مقدار دهی کرد.
- با تنظیم خاصیت multiple به true، کاربر قادر خواهد شد تا توسط صفحه‌ی دیالوگ انتخاب فایل‌ها، قابلیت انتخاب بیش از یک فایل را داشته باشد.
- showFileList نمایش لیست فایل‌ها را سبب می‌شود.


تعیین پسوند فایل‌‌های صفحه‌ی انتخاب فایل‌ها

هنگامیکه کاربر بر روی دکمه‌ی انتخاب فایل‌ها برای ارسال کلیک می‌کند، در صفحه‌ی دیالوگ باز شده می‌توان پسوندهای پیش فرض مجاز را نیز تعیین کرد.
برای این منظور تنها کافی است ویژگی accept را به input از نوع فایل اضافه کرد. چند مثال در این مورد:
<!-- Content Type with wildcard.  All Images -->
<input type="file" id="demoFile" title="Select file" accept="image/*" />
 
<!-- List of file extensions -->
<input type="file" id="demoFile" title="Select file" accept=".jpg,.png,.gif" />
 
<!-- Any combination of the above -->
<input type="file" id="demoFile" title="Select file" accept="audio/*,application/pdf,.png" />


نمایش متن کشیدن و رها کردن، بومی سازی برچسب‌ها و نمایش راست به چپ

همانطور که در تصاویر فوق ملاحظه می‌کنید، نمایش این ویجت راست به چپ و پیام‌های آن نیز ترجمه شده‌اند.
برای راست به چپ سازی آن مانند قبل تنها کافی است input مرتبط، در یک div با کلاس k-rtl محصور شود:
        <div class="k-rtl k-header">
            <input name="files" id="files" type="file"  />
        </div>
برای بومی سازی پیام‌های آن می‌توان مانند مثال ذیل، خاصیت localization را مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                 //...
                },
                //...
                localization: {
                    select: 'انتخاب فایل‌ها برای ارسال',
                    remove: 'حذف فایل',
                    retry: 'سعی مجدد',
                    headerStatusUploading: 'در حال ارسال فایل‌ها',
                    headerStatusUploaded: 'پایان ارسال',
                    cancel: "لغو",
                    uploadSelectedFiles: "ارسال فایل‌ها",
                    dropFilesHere: "فایل‌ها را برای ارسال، کشیده و در اینجا رها کنید",
                    statusUploading: "در حال ارسال",
                    statusUploaded: "ارسال شد",
                    statusWarning: "اخطار",
                    statusFailed: "خطا در ارسال"
                }
            });
        });
    </script>
به علاوه متن dropFilesHere به صورت پیش فرض نامرئی است. برای نمایش آن نیاز است CSS موجود را بازنویسی کرد تا em مرتبط مرئی شود:
<style type="text/css">
div.k-dropzone {
    border: 1px solid #c5c5c5; /* For Default; Different for each theme */
}

div.k-dropzone em {
    visibility: visible;
}
</style>


تغییر قالب نمایش لیست فایل‌ها

لیست فایل‌ها در ویجت kendoUpload دارای یک قالب پیش فرض است که امکان بازنویسی کامل آن وجود دارد. ابتدا نیاز است یک kendo-template را بر این منظور تدارک دید:
    <script id="fileListTemplate" type="text/x-kendo-template">
        <li class='k-file'>
            <span class='k-progress'></span>
            <span class='k-icon'></span>
            <span class='k-filename' title='#=name#'>#=name# (#=size# bytes)</span>
            <strong class='k-upload-status'></strong>
        </li>
    </script>
و سپس برای استفاده از آن خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                // ...
                },
                // ...
                template: kendo.template($('#fileListTemplate').html()),
                // ...
            });
        });
    </script>
در این قالب، مقدار size هر فایل نیز در کنار نام آن نمایش داده می‌شود.


رخدادهای ارسال فایل‌ها

افزونه‌ی kendoUpload در حالت ارسال Ajax ایی فایل‌ها، رخدادهایی مانند شروع به ارسال، موفقیت، پایان، درصد ارسال فایل‌ها و امثال آن‌را نیز به همراه دارد که لیست کامل آن‌ها را در ذیل مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: { // async configuration
                //...
                },
                //...
                localization: {
                },
                cancel: function () {
                    console.log('Cancel Event.');
                },
                complete: function () {
                    console.log('Complete Event.');
                },
                error: function () {
                    console.log('Error uploading file.');
                },
                progress: function (e) {
                    console.log('Uploading file ' + e.percentComplete);
                },
                remove: function () {
                    console.log('File removed.');
                },
                select: function () {
                    console.log('File selected.');
                },
                success: function () {
                    console.log('Upload successful.');
                },
                upload: function (e) {
                    console.log('Upload started.');
                }
            }); 
        });
    </script>


ارسال متادیتای اضافی به همراه فایل‌های ارسالی

فرض کنید می‌خواهید به همراه فایل‌های ارسالی به سرور، پارامتر codeId را نیز ارسال کنید. برای این منظور باید خاصیت e.data رویداد upload را به نحو ذیل مقدار دهی کرد:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                //...
                },
                //...
                localization: {
                },
                upload: function (e) {
                    console.log('Upload started.');
                    // Sending metadata to the save action
                    e.data = {
                        codeId: "1234567",
                        param2: 12
                        //, ...
                    };
                }
            });
        });
    </script>
سپس در سمت سرور، امضای متد Save بر اساس پارامترهای تعریف شده در سمت کاربر، به نحو ذیل تغییر می‌کند:
   [HttpPost]
  public ActionResult Save(IEnumerable<HttpPostedFileBase> files, string codeId)


فعال سازی ارسال batch

اگر در متد Save سمت سرور یک break point قرار دهید، مشاهده خواهید کرد که به ازای هر فایل موجود در لیست در سمت کاربر، یکبار متد Save فراخوانی می‌شود و عملا متد Save، لیستی از فایل‌ها را در طی یک فراخوانی دریافت نمی‌کند. برای فعال سازی این قابلیت تنها کافی است خاصیت batch را به true تنظیم کنیم:
    <script type="text/javascript">
        $(function () {
            $("#files").kendoUpload({
                name: "files",
                async: {
                    // ....
                    batch: true
                },
            });
        });
    </script>
به این ترتیب دیگر لیست فایل‌ها به صورت مجزا در سمت کاربر نمایش داده نمی‌شود و تمام آن‌ها با یک کاما از هم جدا خواهند شد. همچنین دیگر شاهد نمایش درصد پیشرفت تکی فایل‌ها نیز نخواهیم بود و اینبار درصد پیشرفت کل batch گزارش می‌شود.
در یک چنین حالتی باید دقت داشت که تنظیم maxRequestLength در web.config برنامه الزامی است؛ زیرا به صورت پیش فرض محدودیت 4 مگابایتی ارسال فایل‌ها توسط ASP.NET اعمال می‌شود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <!-- The request length is in kilobytes, execution timeout is in seconds  -->
    <httpRuntime maxRequestLength="10240" executionTimeout="120" />
  </system.web>

  <system.webServer>
    <security>
      <requestFiltering>
        <!-- The content length is in bytes  -->
        <requestLimits maxAllowedContentLength="10485760"/>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>
مطالب
جزئیات برنامه نویسی افزونه فارسی به پارسی

این افزونه با استفاده از ابزار Visual Studio Tools for Office که به VSTO مشهور شده است، تهیه شد. در بسته به روز رسانی سیستم که در ذیل (معرفی افزونه) نیز معرفی شد نگارش sp1 vsto3.0 آن به صورت خودکار نصب خواهد شد.
برای ایجاد این پروژه در VS.Net 2008 ، تنها کافی است یک پروژه جدید Word add-in را آغاز نمائیم. (شکل زیر)





قبل از ادامه بحث، بهتر است در مورد بانک اطلاعاتی مورد استفاده نیز توضیح داده شود. در اینجا از SQLite استفاده شد. (بسیار سبک، کم حجم و سریع است و اساسا یک کاربر نهایی برای تنظیمات آن نیازی نیست اطلاعاتی داشته باشد). بسته به روز رسانی سیستم (در مطلب قبلی)، این مورد را نیز به صورت خودکار نصب خواهد کرد (در GAC باید نصب شود وگرنه افزونه قادر به یافتن آن نخواهد شد).
برای ایجاد این بانک اطلاعاتی، از افزونه SQLite manager برای فایرفاکس استفاده شد. (این افزونه رایگان شما را از هر ابزار جانبی برای مدیریت یک بانک اطلاعاتی SQLite بی‌نیاز می‌کند)
برای مثال فایل ErrorsBank.sqlite برنامه افزونه فارسی به پارسی را توسط افزونه SQLite manager فایرفاکس باز کنید (این فایل را در محل نصب افزونه می‌توانید پیدا کنید). در اینجا می‌توان جداول جدید را ایجاد کرد، کوئری‌های دلخواه را اجرا نمود و یا اطلاعات را مرور کرده، حذف یا ویرایش کرد (شکل زیر).




و خوشبختانه این بانک اطلاعاتی و محصور کننده‌های آن با اطلاعات یونیکد فارسی هیچ مشکلی ندارند و برای کارهایی با وسعت کم و تعداد رکورد پائین یکی از بهترین انتخاب‌ها به‌شمار می‌روند.
نحوه استفاده از SQLite نیز در دات نت بسیار ساده است. اگر با ADO.Net کار کرده باشید، پس از افزودن ارجاعی از اسمبلی System.Data.SQLite.DLL به پروژه و معرفی فضای نام آن به پروژه، تنها کافی است در کدهای قبلی خود برای مثال SqlConnection را به SQLiteConnectionتغییر دهید و امثال آن. یعنی دانش ADO.Net شما در اینجا نیز کاملا قابل استفاده خواهد بود و نیازی نیست مدتی را صرف آشنا شدن با کلاس‌ها و مفاهیم جدید نمائید (البته این تنها زمانی معنا خواهد داشت که به ویزاردها عادت نکرده باشید و کارهای خود را با کد نویسی انجام داده باشید).
تنها یک نکته را باید به‌خاطر داشت و آن هم مربوط است به ساز و کار درونی SQLite . هنگام انجام عملیات update یا insert حتما از transaction استفاده کنید تا سرعت کوئری‌های شما در SQLite به نحو شگفت انگیزی افزایش یابد. مثالی در این مورد را در فایل chm راهنمای SQLite.NET می‌توانید پیدا کنید.

مطلب دیگری که پیش از پرداختن به کد نویسی افزونه باید با آن آشنا شویم، مفهوم smart tags در مجموعه آفیس است که در این پروژه از آن استفاده گردید.
smart tags در مجموعه آفیس برچسب‌هایی هستند که به صورت خودکار توسط یکی از محصولات آفیس مثلا ورد یا اکسل و امثال آن، پس از تشخیص یک کلمه خاص ایجاد می‌شوند و می‌توان اعمالی را به این برچسب ایجاد شده انتساب داد. برای مثال در اینجا امکان جایگزین کردن کلمه فارسی با معادل پارسی در نظر گرفته شد.
ویدیویی در مورد نحوه ایجاد اسمارت تگ‌ها در VS.Net و یا مثالی پیشرفته‌تر در مورد تشخیص دمای فارنهایت در یک متن و ایجاد smart tag مخصوص به آن برای تبدیل به سلسیوس. (از regular expressions جهت یافتن یک الگو در متن استفاده شده است)

در این پروژه، حدود 3800 واژه فارسی به‌ یک smart tag انتساب داده می‌شود (در روال استاندارد ThisAddIn_Startup). سپس در هنگام نمایش آن، معادل پارسی کلمه نیز به منوی باز شده افزوده گشته و در روال رخداد کلیک آن، تعویض کلمه تشخیص داده شده با واژه پیدا شده صورت خواهد گرفت.

در ادامه فرض بر این است که یک پروژه جدید word add-in را در VS.Net ایجاد کرده‌اید و همچنین ارجاعی را به فایل System.Data.SQLite.DLL افزوده‌اید.

using System;
using System.Diagnostics;
using Microsoft.Office.Tools.Word;
using Action = Microsoft.Office.Tools.Word.Action;

private SmartTag _st;
private void init()
{
try
{
//Enable Smart Tags in Word
if (!Application.Options.LabelSmartTags)
{
//ممکن است اسمارت تگ‌ها در ورد غیرفعال باشند. به این صورت می‌شود آنها را فعال کرد
Application.Options.LabelSmartTags = true;
}

_st = new SmartTag(@"www.microsoft.com/Demo#FarsiSmartTag", @"فارسی به پارسی");

//دریافت واژه‌های فارسی از دیتابیس و افزودن خودکار آنها به اسمارت تگ‌ها
if (!DBhelper.AddSmartTagItems(_st, "select distinct farsi from tblFarsiToParsi")) return;

Action stActions = new Action("تبدیل");//تعریف یک اکشن جدید
stActions.Click += stActions_Click;//انتساب روال‌های رخداد گردان
stActions.BeforeCaptionShow += stActions_BeforeCaptionShow;
_st.Actions = new[] { stActions };
VstoSmartTags.Add(_st);//افزودن اسمارت تگ به مجموعه
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

private void ThisAddIn_Startup(object sender, EventArgs e)
{
init();
}

دو روال رخداد گردان زیر نیز جهت تغییر عنوان پیش فرض به واژه یافته شده در لحظه نمایش منو و روال کلیک نیز ایجاد خواهد شد:

static void stActions_BeforeCaptionShow(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
string parsi = DBhelper.FindParsi(e.Text);//معادل پارسی از دیتابیس دریافت می‌شود
clickedAction.Caption = (parsi == string.Empty ? e.Text : parsi);
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

static void stActions_Click(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
e.Range.Text = clickedAction.Caption;//جایگزینی متن موجود با عنوانی که پیشتر پارسی شده است
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

نکته‌ای را که در اینجا باید حتما رعایت کرد بحث exception handling‌ است. خصوصا در روال استاندارد ThisAddIn_Startup . اگر در این روال خطایی مدیریت نشده رخ دهد، word افزودنی شما را به صورت غیرفعال به مجموعه اضافه خواهد کرد و فعال سازی بعدی آن پس از اصلاح کد واقعا مشکل خواهد بود. همانطور که ملاحظه می‌کنید تمامی خطاها در event log‌ ویندوز نوشته می‌شوند.
همچنین باید دقت داشت که اگر متغیری در سطح کلاس تعریف نشود به احتمال زیاد تا دقایقی بعد توسط garbage collector به دیار باقی خواهد شتافت (تعریف st_ در اینجا). اینجاست که شاید ساعت‌ها وقت صرف کنید که چرا روال‌های رخ‌داد گردان دیگر اجرا نمی‌شوند. چرا افزونه دیگر کار نمی‌کند.

همین! کل سورس این add-in منهای بحث دریافت اطلاعات از دیتابیس همین بود! وظیفه‌ی تشخیص کلمات معرفی شده به ms-word به‌عهده‌ی خود آن است و این‌کار را نیز به‌خوبی انجام می‌دهد. در گذشته‌های نچندان دور ایجاد یک افزونه برای word واقعا مشکل بود که با این روش بسیاری از موانع برطرف شده است.

کلاس DBHelper که کار دریافت اطلاعات واژه‌ها را از دیتابیس SQLite انجام می‌دهد به شرح زیر است:

using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Office.Tools.Word;

namespace Farsi2Parsi
{
class DBhelper
{
#region Methods (2)

// Public Methods (2)

public static bool AddSmartTagItems(SmartTag st, string strSQL)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
bool ret = false;
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
while (myReader.Read())
{
if (myReader.GetValue(0) != DBNull.Value)
st.Terms.Add(myReader.GetValue(0).ToString());
}

ret = true;
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 7);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}

public static string FindParsi(string farsi)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
string ret = string.Empty;
string strSQL = "select parsi from tblFarsiToParsi where farsi='" + farsi.Replace("'", "''") + "'";
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
{
myReader.Read(); //اولین مورد کافی است
if (myReader.GetValue(0) != DBNull.Value)
ret = myReader.GetValue(0).ToString();
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 8);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}
#endregion Methods
}
}

همانطور که پیشتر نیز عنوان شد اگر با ADO.net آشنایی داشته باشید، هیچ نکته‌ی خاص جدیدی را در اینجا مشاهده نخواهید کرد و تنها یک سری امور روزمره کاری با ADO.net مطرح شده است، باز کردن کانکشن، اجرای کوئری، دریافت اطلاعات و پاکسازی نهایی. (قسمت finally را با استفاده از عبارت using می‌شود حذف کرد)

هنگام نصب برنامه، مسیر پوشه نصب در رجیستری ویندوز توسط نصاب نوشته خواهد شد. از همین مورد برای ایجاد رشته اتصالی به دیتابیس استفاده گردید.

class ConStr
{
public static string ConnectionString
{
get
{
return Microsoft.Win32.Registry.LocalMachine.OpenSubKey("SOFTWARE\\FarsiToParsi").GetValue("folder") + "\\ErrorsBank.sqlite";
}
}
}

سورس کامل این افزونه را به صورت یک پروژه VS.Net 2008 SP1 از اینجا می‌توانید دریافت کنید.
نصاب برنامه با استفاده از NSIS ایجاد شده که در روزی دیگر درباره‌ی آن توضیح خواهم داد.
اگر قصد داشته باشید از روش‌های متداول استفاده کنید، مشاهده ویدیوی زیر توصیه می‌شود:
http://msdn.microsoft.com/en-us/office/bb851702.aspx

برای توزیع این نوع افزونه‌ها علاوه بر دات نت فریم ورک، به چهار به روز رسانی دیگر نیز نیاز خواهد بود:
به روز رسانی نصاب ویندوز (که احتمالا نصب هست)
WindowsInstaller-KB893803-v2-x86.exe
Microsoft Office System Update: Redistributable Primary Interop Assemblies :
o2007pia.msi
نصب vsto و همچنین sp1 آن
vstor30.exe
vstor30sp1-KB949258-x86.exe

این موارد را من در بسته به روز رسانی سیستم قرار داده‌ام که به صورت خودکار و یکی پس از دیگری اجرا و نصب خواهند شد.
پس از آن با کلیک بر روی فایلی با پسوند vsto که در پوشه build برنامه موجود است، می‌توان افزونه را نصب کرد (click once installation).




سایر اطلاعات در مورد پروژه‌های VSTO را می‌توان از طریق وبلاگ رسمی آنها دنبال کرد:
http://blogs.msdn.com/vsto/

ایده‌های دیگری را هم در همین رابطه می‌توان پیاده سازی کرد. برای مثال درست کردن یک افزونه برای بررسی آئین نگارش فارسی در متون word. دقیقا با همین روش قابل پیاده سازی است و یا ایجاد غلط یاب بهتری نسبت به آن‌چه که هم اکنون برای آفیس 2003 توسط مایکروسافت ارائه شده است (این غلط یاب با صفحه کلید استاندارد تایپ ایران همخوانی ندارد، به همین جهت با استقبال نیز مواجه نشد).


مطالب
بررسی خطاهای متداول عملیات Migration در حین به روز رسانی پروژه‌های EF Code First

1. شاید یکی از آزاردهنده‌ترین مشکلات، برخورد با پیغام‌های خطا، هنگام عملیات migration باشد. یکی از ده‌ها نوع خطا، زمانی رخ می‌دهد که متد seed در حال اجراست. در این حالت هیچ نوع break-point ایی به کمک ما نخواهد آمد.

سوال ایجاست که آیا می‌توان این بخش را دیباگ نمود؟ بهترین راه حل، اجرای آپدیت از طریق متدها(یا اکشن ها) است.

فراخوانی migration بسیار ساده است. باید یک نمونه از کلاس Configuration را ساخته و در جایی از پروژه قرار دهیم و صد البته مطمئن باشیم که migration  فعال است. 

var configuration = new Configuration();
var migrator = new DbMigrator(configuration);
migrator.Update();

اگر بخواهید این تغییرات بر روی دیتابیسی با اسم و رسم انجام شود، از کد زیر بهره بگیرید: 

var configuration = new Configuration();
configuration.TargetDatabase = new DbConnectionInfo(
    "Server=MyServer;Database=MyDatabase;Trusted_Connection=True;", 
    "System.Data.SqlClient");
var migrator = new DbMigrator(configuration);
migrator.Update();
می‌توانید این کد را در ابتدای اکشن index در کنترلر Home قرار دهید و با قرار دادن Break-Point در بخش‌های مختلف متد Seed ، آن را بررسی کنید. 

2. خطای :

Duplicate type name within an assembly.

 معمولا بخاطر وجود break-point این مشکل رخ میدهد. یا break-pointهای درون seed را حذف کنید یا جای آنها را تغییر دهید.  [اینجا]

3. خطای : 

Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.

این مشکل میتواند دلایل مختلفی داشته باشد. کد درون متد seed را داخلtry/catch قرا ر دهید و علت آن را بررسی کنید:

                try
                {
                    var user = new ApplicationUser { UserName = "Admin", Phone = "09120000000", Email = "m@gmail.com" };
                    usermanager.Create(user, "09120000000");                                
                    usermanager.AddToRole(user.Id, "admin");
                }
                catch (DbEntityValidationException dbEx)
                {
                    foreach (var validationErrors in dbEx.EntityValidationErrors)
                    {
                        foreach (var validationError in validationErrors.ValidationErrors)
                        {
                            Trace.TraceInformation("Property: {0} Error: {1}", validationError.PropertyName, validationError.ErrorMessage);
                        }
                    }
                }

فراموش نکنید، seed را به کمک حالتی که در شماره 1 گفته شد اجرا کنید تا بتوانید از BreakPoint استفاده کنید.

4.خطای : 

More than one context type was found in the assembly 'Ex1_CodeFirst'

این خطا وقتی ظاهر میشود که چندین Context برای پروژه جاری داشته باشیم و ویژوال استودیو نتواند Context مورد نظر ما را تشخیص دهد. بهتر است هنگام اجرای migration نام context مورد نظر را بنویسیم (مثلا MyConfiguration نام کانتکست شماست) :

Update-Database -ConfigurationTypeName MyConfiguration
5.خطای : 

There is already an object named 'UserProfile' in the database.

یعنی مدل شما پس از اینکه جدولی با همین نام (در این جا به عنوان مثال UserProfile) از پیش موجود بوده، تغییر کرده است؛ پس migration آپدیت شدنی نیست. ابتدا این دستور را می‌نویسیم (پیش از آنکه تغییراتمان را روی کلاس مربوط به جدول UserProfile اعمال کنیم): 

Add-Migration Initial –IgnoreChanges
اینکار باعث می‌شود یک فایل خالی به نام InitialMigration ایجاد شود. حالا تنظیمات مورد نظر، اعم از تغییر نام یا اضافه کردن ستون خاص را به کلاس UserProfile اعمال کرده و دیتابیس را آپدیت میکنیم :
update-database –verbose

گاهی این مشکل زمانی پیش می‌آید که واقعا تغییری در جدول نامبرده انجام نداده‌ایم. در این حالت روش پله‌ای زیر را به کار می‌بریم: 
        • پاک کردن یا ریست کردن migration (در شماره 9 همین مقاله این کار در چند مرحله توضیح داده شده است. در مرحله سوم حتما از Add-Migration Initial –IgnoreChanges استفاده کنید)  
        • بعد از آپدیت دیتابیس باید فایل زیر دارای محتویات باشد  

محتویات این فایل دقیقا شرایط فعلی جدول شماست.
        • اگر این فایل ایجاد نشده است مراحل را تکرار کنید تا محتویات درون آن را ببینید.
        • حالا تغییری را در یکی از مدل‌های خود انجام دهید. احتمالا با مشکل آپدیت شدن مواجه میشوید و پیغام زیر را دوباره خواهید دید: 
There is already an object named 'UserProfile' in the database
        • جدولی را که پیغام خطا به آن اشاره کرده، در فایل فوق بیابید و محدوده create آن را کامنت کنید تا ساخته نشود. 
        • دیتابیس را آپدیت کنید، احتمالا پیغام خطای فوق برای جدول دیگری نمایش داده می‌شود. آن را هم کامنت کنید و دیتابیس را آپدیت کنید و اگر باز هم خطا بود مکانیزم بالا را تا جایی تکرار کنید که خطایی نبینید .
        • حالا جدولی را که تغییراتی در آن داده بودید، در دیتابیس چک کنید که تغییرات اعمال شده باشد.
        • هر آنچه را در فایل initial کامنت کرده بودید، از کامنت خارج کنید و دیتابیس را آپدیت کنید .
        • برای آزمایش، یک آیتم به یکی از مدل‌ها اضافه کنید و ببینید که migration درست کار می‌کند یا خیر.
6.  خطای : 

Unable to update database to match the current model because there are pending changes 
and automatic migration is disabled. Either write the pending model changes to a code-based 
migration or enable automatic migration. 
Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true to enable automatic migration.
You can use the Add-Migration command to write the pending model changes to a code-based migration.
همانطور که از متن این پیغام پیداست، یعنی شما AutomaticMigration را true  نکرده‌اید و اینکار را باید در فایل  Configuration.cs  در فولدر Migration انجام داد. 

7.خطای : 
Automatic migration was not applied because it would result in data loss.
این خط را در سازنده‌ی کلاس Configuration اضافه میکنیم :

   AutomaticMigrationDataLossAllowed = true;
8.خطای : 

Introducing FOREIGN KEY constraint 'FK_dbo.ProductProductGroups_dbo.ProductGroups_ProductGroupId' on table 
'ProductProductGroups' may cause cycles or multiple cascade paths. 
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.
خطای فوق وقتی رخ میدهد که دو جدول از یک طرف به هم وصل باشند و از طرف دیگر مشخصات ذکر نشده باشد.
9. راه حل برای خطاهای عجیبی که شاید نیاز به صرف زمان بیشتر برای کشف و برطرف کردن داشته باشند : 
بهتر نیست مایگریشن خود را از نو بسازید؟
روش به روز رسانی و بازسازی migration [اینجا ]:
- پاک کردن فولدر Migrations در پروژه
- پاک کردن جدولی به نام MigrationHistory_  در دیتابیس (ممکن است زیر مجموعه جدول‌های system باشد)
- اجرای دستور زیر کنسول پکیچ منیجر ویژوال استودیو :
 Enable-Migrations -EnableAutomaticMigrations -Force
- اجرای دستور زیر :
 Add-Migration Initial


اشتراک‌ها
لیستی از C# Source Generators

C# Source Generators

A list of C# Source Generators (not necessarily awesome), because I haven't found a good list yet.

C# Source Generators is a Roslyn compiler feature introduced in C#9/.NET 5. It lets C# developers inspect user code and generate new C# source files that can be added to a compilation. 

لیستی از C# Source Generators
نظرات مطالب
استفاده از قابلیت پارتیشن بندی در آرشیو جداول بانک‌های اطلاعاتی SQL Server
با سلام.
من چندتا نکته بگم که شاید مساله‌ی شما بنیادی‌تر رفع بشه:
1- شما تاریخ رو به صورت شمسی در دیتابیس ذخیره میکنید. این خودش مسائل زیر را ایجاد میکنه:
الف) شما هیچ کنترلی برای اعتبارسنجی در سطح دیتابیس برای تاریخ تون ندارید (یعنی 31-12-1398 از نظر دیتابیس مشکلی نداره!) از طرف دیگه مقایسه تاریخی شما با هزینه همراه هستش (یعنی شما وقتی میخواید کوئیری بزنید که مثلا داده هایی که در بین دوتا بازه هستن و براتون لود کنه)
ب) شما قابلیت اجرای کوئیری‌ها مثلا یک ماه اخیر و ... رو به سختی در سطح دیتابیس دارید. (البته راه حل داره که الان از این موضوع خارجه)
2- حجم دیتابیس! شما برای این فیلد به 10 کاراکتر نیاز دارید که میشه 10 بایت! در صورتی که به صورت خیلی ساده! فقط نوع ستون رو به int تغییر بدید و این مقدار رو به صورت 13951230 نگه داری کنید، حداقل 60 درصد کاهش فضا در این فیلد دارید! مزایای بیشتری هم داره از جمله مقایسه دوتا عدد خیلی کم هزینه‌تر از مقایسه دوتا رشته هستش و ....
مستقل از مسائل طراحی موجود در طراحی شما (که الان خارج از موضوع هستش) به نظرم شما باید کوئیری که میزنید رو هم بذارید، چون کلید پارتیشن باید توی کوئیری دخیل شده باشه که دیتابیس بتونه براساس اون، پارتیشن یا پارتیشن‌های مورد نظر را فیلتر کنه و مجبور نباشه روی کل دیتابیس بچرخه!
مطالب
تزریق وابستگی‌ها به صورت پویا در فروشگاه‌ساز Nop Commerce
این روش منحصر به Nop نیست و امکان استفاده‌ی از آن بر روی هر سورس دیگری نیز وجود دارد. همچنین اگر در رابطه با NopCommerce اطلاعاتی ندارید، میتوانید از اینجا جهت آشنا شدن با این فروشگاه ساز Asp.net core استفاده کنید.
همانطور که در جریان هستید، برای اینکه بحث DI را در پروژه داشته باشیم، باید به ازای هر سرویس مشخص کنیم که کدام اینترفیس، به کدام کلاس، map شود. به بیان دیگر باید مشخص کرد هر وقت یک شیء از Container درخواست شد، از چه کلاسی باید این شیء ساخته شود؛ در عین‌حال باید LifeTime وجود شیء در حافظه نیز مشخص شود. حال تصور کنید تعداد سرویس‌های شما در حال زیاد شدن است. در این حالت مجبور هستید دائما این سرویس‌ها را ثبت کنید؛ علاوه بر اینکه باید کدهای تکراری را جهت تعریف این سرویس‌ها بنویسید و باید به‌خاطر بسپارید که سرویس جدید را ثبت کنید. در این مقاله تلاش بر این است تا دیگر نیازی به تعریف کردن تک تک سرویس‌ها نباشد؛ به‌طوری که با رعایت دو قانون کلی بتوان سرویس‌ها را به صورت خودکار ثبت کرد.

مراحل پیاده سازی

 یک اینترفیس را به اسم ICustomService ایجاد کردم که  یک Prop به اسم InjectType دارد و مشخص میکند به چه صورتی این سرویس به ServiceCollection تزریق شود. از طرفی با استفاده از Order، الویت اضافه شدن سرویس به ServiceCollection را مشخص میکنیم و در نهایت با ImplementationType مشخص میکنیم سرویسی که اضافه شده، باید به یک اینترفیس Map شود یا خیر؟ اما مهم‌تر از اینکه ویژگی‌های تزریق وابستگی مشخص شود، مشخص میکند چه سرویس‌هایی توسط ما اضافه شده‌اند و از سرویس‌های nop تفکیک می‌شوند.
namespace Nop.Services
{
    public interface ICustomService
    {
        protected InjectType Inject { get;  }
        protected int Order { get; }
        protected ImplementationType implementationType { get; }
    }
    public enum ImplementationType
    {
        WithInterface = 0,
        WithoutInterface = 1
    }
    public enum InjectType
    {
        Scopped=0,
        Transit=1,
        SingleTon=2
    }
}

قانون اول

برای هر سرویسی که ایجاد میکنیم و میخواهیم به DI معرفی کنیم، آن سرویس باید ICustomService را پیاده سازی کرده باشد؛ دقیقا به خاطر دو دلیلی که در بالا به آن‌ها اشاره شد.

قانون دوم

هر کلاسی که Interface مرتبط به سرویس‌ها را پیاده سازی میکند، باید prop InjectType را در سازنده‌ی خودش مقدار دهی کند. بدین شکل متوجه میشویم از چه طریقی باید تزریق انجام شود. تا اینجا یک چارچوب را مشخص کردیم تا سرویس‌ها را بتوانیم تشخیص دهیم\ اما هنوز کار اصلی باقی مانده‌است. برای نمونه میتوان کد زیر را در نظر گرفت :

namespace Nop.Services
{
    public interface IMyCustomService: ICustomService
    {
        int ok();
    }
}
برای پیاده سازی سرویس ایجاد شده، کد زیر را ایجاد میکنیم :
namespace Nop.Services
{
    public class MyCustomService : IMyCustomService
    {
        public InjectType Inject { get;  }
        public int Order { get;  }
        public ImplementationType implementationType { get;  }

        public MyCustomService()
        {
            implementationType = ImplementationType.WithInterface;
            Inject = InjectType.Scopped;
            Order = 1;
        }
        public int ok()
        {
            return 10;
        }
    }
}

تعیین نقطه شروع

باید نقطه شروع به کار Nop را پیدا کنیم. از آنجایی که با معماری Nop جلو میرویم، با کمی بررسی و دیدن کد‌ها، به کلاسی میرسیم به اسم NopStartup در قسمت Nop.Web.Framework. مسیر دقیق آن: Nop.Web.Framework\Infrastructure\NopStartup.cs. حالا این کلاس چیست؟ در واقع هر کلاسی که از سرویس INopStartup ارث بری کرده باشد، اولویت پیدا میکند و قبل از کدهای دیگر اجرا می‌شود. باید کلاس جدیدی را به اسم مثلا CustomDependencyInjection ایجاد کنیم، با این تفاوت که حتما از کلاس NopStartup ارث بری کرده باشد و همچنین حتما باید متدی را به اسم ConfigureServices، بازنویسی کند. حالا داخل متدی که گفتم باید شروع کنیم به کار.

کد زیر در واقع نقطه‌ی اتصال سرویس‌های نوشته شده و اتمام کار تزریق وابستگی است. با توجه به پیاده سازی‌های انجام شده‌ی توسط سرویس‌ها می‌توان با Reflection سرویس‌های نوشته شده را تشخیص داد که در نهایت با  ویژگی‌هایی که در سرویس‌ها پیاده سازی شده موجود است، به ServiceCollection اضافه می‌شوند.

namespace Nop.Web.Framework.Infrastructure
{
    public class CustomDependencyInjection : NopStartup
    {
        private static bool IsSubInterface(Type t1, Type t2)
        {
            if (!t2.IsAssignableFrom(t1))
                return false;

            if (t1.BaseType == null)
                return true;

            return !t2.IsAssignableFrom(t1.BaseType);
        }
        public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
        {
            //-------------Get All Services-------------
            var asm = AppDomain.CurrentDomain
                 .GetAssemblies()
                 .Single(x => x.FullName.Contains("Nop.Services"));
            //-------------find Services that inheriance of ICustomService-------------
            var types = asm.DefinedTypes.Where(x => IsSubInterface(x, typeof(ICustomService)));
            //-----------Get All Custom Service Classess-------
            var allRelatedClassServices = types
                .Where(x => x.IsClass)
                .OrderBy(x=>(Int32)x.GetProperty("Order")
                .GetValue(Activator.CreateInstance(x), null));

            //-----------Get All Custom Service Interfaces-------
            var allRelatedInterfaceServices = types.Where(x => x.IsInterface);
            //-----------Matche Class Services To Related Interface Services-------
            TypeInfo interfaceService=null;
            foreach (var classService in allRelatedClassServices)
            {
                //-----------detect Implementation Type for service-----------
                var implementationValue = (ImplementationType)classService.GetProperty("implementationType")
                   .GetValue(Activator.CreateInstance(classService), null);

                //-----------detect inject type for service-----------
                var InjectValue = (InjectType)classService.GetProperty("Inject")
                   .GetValue(Activator.CreateInstance(classService), null);

                //-----------get related interface for service class-----------
                if (implementationValue == ImplementationType.WithInterface)
                    interfaceService = allRelatedInterfaceServices.Single(x => x.Name == $"I{classService.Name}");

               

                //----------finally Add Custom Service To Service Collection-----------
                switch (InjectValue)
                {
                    case InjectType.Scopped:
                        if(interfaceService!=null)
                            services.AddScoped(interfaceService, classService);
                        else
                            services.AddScoped(classService);
                        break;
                    case InjectType.Transit:
                        if (interfaceService != null)
                            services.AddTransient(interfaceService, classService);
                        else
                            services.AddTransient(classService);
                        break;
                    case InjectType.SingleTon:
                        if (interfaceService != null)
                            services.AddSingleton(interfaceService, classService);
                        else
                            services.AddSingleton(classService);
                        break;
                    default:
                        break;
                }
                interfaceService = null;
            }
        }
       
    }
}
نکته‌ی آخر آن که این داستان‌ها صرفا برای سرویس‌هایی هست که توسط برنامه نویس به پروژه‌ی Nop اضافه می‌شود.

لینک گیت‌هاب  
مطالب
اعتبارسنجی در فرم‌های ASP.NET MVC با Remote Validation

بعد از آمدن نسخه‌ی سوم ASP.NET MVC مکانیسمی به نام Remote Validation به آن اضافه شد که کارش اعتبارسنجی از راه دور بود. فرض کنید نیاز است در یک فرم، قبل از اینکه کل فرم به سمت سرور ارسال شود، مقداری بررسی شده و اعتبارسنجی آن انجام گیرد و این اعتبارسنجی چیزی نیست که بتوان سمت کاربر و بدون فرستاده شدن مقداری به سمت سرور صورت گیرد. نمونه بارز این مسئله صفحه عضویت اکثر سایت‌هایی هست که روزانه داریم با آن‌ها کار می‌کنیم. فیلد نام کاربری توسط شما پر شده و بعد از بیرون آمدن از آن فیلد، سریعا مشخص می‌شود که آیا این نام کاربری قابل استفاده برای شما هست یا خیر. به‌صورت معمول برای انجام این کار باید با جاوا اسکریپت، مدیریتی روی فیلد مربوطه انجام دهیم. مثلا با بیرون آمدن فوکوس از روی فیلد، با Ajax نام کاربری وارد شده را به سمت سرور بفرستیم، چک کنیم و بعد از اینکه جواب برگشت بررسی کنیم که الان آیا این نام کاربری قبلا گرفته شده یا نه.
انجام این کار به‌راحتی با مزین‌کردن خصوصیت (Property) مربوطه موجود در مدل برنامه به Attribute یا ویژگی Remote و داشتن یک Action در Controller مربوطه که کارش بررسی وجود یوزرنیم هست امکان پذیر است. ادامه بحث را با مثال همراه می‌کنم.
به عنوان مثال در سیستمی که قرار هست محصولات ما را ثبت کند، باید بیایم و قبل از اینکه محصول جدید به ثبت برسد این عملیات چک‌کردن را انجام دهیم تا کالای تکراری وارد سیستم نشود. شناسه اصلی که برای هر محصول وجود دارد بارکد هست و ما آن را میخواهیم مورد بررسی قرار دهیم.


مدل برنامه

    public class ProductModel
    {
        public int Id { get; set; }

        [Display(Name = "نام کالا")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        public string Name { get; set; }

        [Display(Name = "قیمت")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [DataType(DataType.Currency)]
        public double Price { get; set; }

        [Display(Name = "بارکد")]
        [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")]
        [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")]
        [Remote("IsProductExist", "Product", HttpMethod = "POST", ErrorMessage = "این بارکد از قبل در سیستم وجود دارد.")]
        public string Barcode { get; set; }
    }

همونطور که می‌بینید خصوصیت Barcode را مزین کردیم به ویژگی Remote. این ویژگی دارای ورودی‌های خاص خودش هست. وارد کردن نام اکشن و کنترلر مربوطه برای انجام این چک‌کردن از مهم‌ترین قسمت‌های اصلی هست. چیزهایی دیگه‌ای هم هست که می‌توانیم آن‌ها را مقداردهی کنیم. مثل HttpMethod، ErrorMessage و یا AdditionFields. HttpMethod که همان طریقه‌ی ارسال درخواست به سرور هست. ErrorMessage هم همان خطایی هست که در زمان رخ‌داد قرار است نشان داده شود. AdditionFields هم خصوصیتی را مشخص می‌کند که ما می‌خوایم به‌همراه فیلد مربوطه به سمت سرور بفرستیم. مثلا می‌تونیم به‌همراه بارکد، نام کالا را هم برای بررسی‌های مورد نیازمان بفرستیم.


کنترلر برنامه

        [HttpPost]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult IsProductExist(string barcode)
        {
            if (barcode == "123456789") return Json(false); // اگر محصول وجود داشت
            return Json(true);
        }

در اینجا به نمایش قسمتی از کنترلر برنامه می‌پردازیم. اکشنی که مربوط می‌شود به چک‌کردن مقدارهای لازم و در پایان آن یک خروجی Json را برمی‌گردانیم که مقدار true یا false دارد. در حقیقت مقدار را به این صورت برمی‌گردانیم که اگر مقدار ورودی در پایگاه داده وجود دارد، false را برمی‌گرداند و اگر وجود نداشت true. همین‌طور آمدیم از کش شدن درخواست‌هایی که با Ajax آمده با ویژگی OutputCache جلوگیری کردیم.

مطالب
EF Code First #15

EF Code first و بانک‌های اطلاعاتی متفاوت

در آخرین قسمت از سری EF Code first بد نیست نحوه استفاده از بانک‌های اطلاعاتی دیگری را بجز SQL Server نیز بررسی کنیم. در اینجا کلاس‌های مدل و کدهای مورد استفاده نیز همانند قسمت 14 است و تنها به ذکر تفاوت‌ها و نکات مرتبط اکتفاء خواهد شد.


حالت کلی پشتیبانی از بانک‌های اطلاعاتی مختلف توسط EF Code first

EF Code first با کلیه پروایدرهای تهیه شده برای ADO.NET 3.5 که پشتیبانی از EF را لحاظ کرده باشند،‌ به خوبی کار می‌کند. پروایدرهای مخصوص ADO.NET 4.0، تنها سه گزینه DeleteDatabase/CreateDatabase/DatabaseExists را نسبت به نگارش قبلی بیشتر دارند و EF Code first ویژگی‌های بیشتری را طلب نمی‌کند.
بنابراین اگر حین استفاده از پروایدر ADO.NET مخصوص بانک اطلاعاتی خاصی با پیغام «CreateDatabase is not supported by the provider» مواجه شدید، به این معنا است که این پروایدر برای دات نت 4 به روز نشده است. اما به این معنا نیست که با EF Code first کار نمی‌کند. فقط باید یک دیتابیس خالی از پیش تهیه شده را به برنامه معرفی کنید تا مباحث Database Migrations به خوبی کار کنند؛ یا اینکه کلا می‌توانید Database Migrations را خاموش کرده (متد Database.SetInitializer را با پارامتر نال فراخوانی کنید) و فیلدها و جداول را دستی ایجاد کنید.


استفاده از EF Code first با SQLite

برای استفاده از SQLite در دات نت ابتدا نیاز به پروایدر ADO.NET آن است: «مکان دریافت درایور‌های جدید SQLite مخصوص دات نت»
ضمن اینکه به نکته «استفاده از اسمبلی‌های دات نت 2 در یک پروژه دات نت 4» نیز باید دقت داشت.
و یکی از بهترین management studio هایی که برای آن تهیه شده: «SQLite Manager»
پس از دریافت پروایدر آن، ارجاعی را به اسمبلی System.Data.SQLite.dll به برنامه اضافه کنید.
سپس فایل کانفیگ برنامه را به نحو زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0"/>
</startup>

<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=CodeFirst.db"
providerName="System.Data.SQLite"/>
</connectionStrings>
</configuration>

همانطور که ملاحظه می‌کنید، تفاوت آن با قبل، تغییر connectionString و providerName است.
اکنون اگر همان برنامه قسمت قبل را اجرا کنیم به خطای زیر برخواهیم خورد:
«The given key was not present in the dictionary»
در این مورد هم توضیح داده شد. سه گزینه DeleteDatabase/CreateDatabase/DatabaseExists در پروایدر جاری SQLite برای دات نت وجود ندارد. به همین جهت نیاز است فایل «CodeFirst.db» ذکر شده در کانکشن استرینگ را ابتدا دستی درست کرد.
برای مثال از افزونه SQLite Manager استفاده کنید. ابتدا یک بانک اطلاعاتی خالی را درست کرده و سپس دستورات زیر را بر روی بانک اطلاعاتی اجرا کنید تا دو جدول خالی را ایجاد کند (در برگه Execute sql افزونه SQLite Manager):

CREATE TABLE [Payees](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Name] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL
);

CREATE TABLE [Bills](
[Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
[Amount] [float](18, 2) NOT NULL,
[Description] [text] NULL,
[CreatedOn] [datetime] NOT NULL,
[CreatedBy] [text] NULL,
[ModifiedOn] [datetime] NOT NULL,
[ModifiedBy] [text] NULL,
[Payee_Id] [integer] NULL
);

سپس سطر زیر را نیز به ابتدای برنامه اضافه کنید:

Database.SetInitializer<Sample09Context>(null);

به این ترتیب database migrations خاموش می‌شود و اکنون برنامه بدون مشکل کار خواهد کرد.
فقط باید به یک سری نکات مانند نوع داده‌ها در بانک‌های اطلاعاتی مختلف دقت داشت. برای مثال integer در اینجا از نوع Int64 است؛ بنابراین در برنامه نیز باید به همین ترتیب تعریف شود تا نگاشت‌ها به درستی انجام شوند.

در کل تنها مشکل پروایدر فعلی SQLite عدم پشتیبانی از مباحث database migrations است. این مورد را خاموش کرده و تغییرات ساختار بانک اطلاعاتی را به صورت دستی به بانک اطلاعاتی اعمال کنید. بدون مشکل کار خواهد کرد.

البته اگر به دنبال پروایدری تجاری با پشتیبانی از آخرین نگارش EF Code first هستید، گزینه زیر نیز مهیا است:
http://devart.com/dotconnect/sqlite/
برای مثال اگر علاقمند به استفاده از حالت تشکیل بانک اطلاعاتی SQLite در حافظه هستید (با رشته اتصالی ویژه Data Source=:memory:;Version=3;New=True;)،‌ فعلا تنها گزینه مهیا استفاده از پروایدر تجاری فوق است؛ زیرا مبحث Database Migrations را به خوبی پشتیبانی می‌کند.



استفاده از EF Code first با SQL Server CE

قبلا در مورد «استفاده از SQL-CE به کمک NHibernate» مطلبی را در این سایت مطالعه کرده‌اید. سه مورد اول آن با EF Code first یکی است و تفاوتی نمی‌کند (یک سری بحث عمومی مشترک است). البته با یک تفاوت؛ در اینجا EF Code first قادر است یک بانک اطلاعاتی خالی SQL Server CE را به صورت خودکار ایجاد کند و نیازی نیست تا آن‌را دستی ایجاد کرد. مباحث database migrations و به روز رسانی خودکار ساختار بانک اطلاعاتی نیز در اینجا پشتیبانی می‌شود.
برای استفاده از آن ابتدا ارجاعی را به اسمبلی System.Data.SqlServerCe.dll قرار گرفته در مسیر Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop اضافه کنید.
سپس رشته اتصالی به بانک اطلاعاتی و providerName را به نحو زیر تغییر دهید:

<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Data Source=mydb.sdf;Password=1234;Encrypt Database=True"
providerName="System.Data.SqlServerCE.4.0"/>
</connectionStrings>


بدون نیاز به هیچگونه تغییری در کدهای برنامه، همین مقدار تغییر در تنظیمات ابتدایی برنامه برای کار با SQL Server CE کافی است.
ضمنا مشکلی هم با فیلد Identity در آخرین نگارش EF Code first وجود ندارد؛ برخلاف حالت database first آن که پیشتر این اجازه را نمی‌داد و خطای «Server-generated keys and server-generated values are not supported by SQL Server Compact» را ظاهر می‌کرد.



استفاده از EF Code first با MySQL

برای استفاده از EF Code first با MySQL (نگارش 5 به بعد البته) ابتدا نیاز است پروایدر مخصوص ADO.NET آن‌را دریافت کرد: (^)
که از EF نیز پشتیبانی می‌کند. پس از نصب آن، ارجاعی را به اسمبلی MySql.Data.dll قرار گرفته در مسیر Program Files\MySQL\MySQL Connector Net 6.5.4\Assemblies\v4.0 به پروژه اضافه نمائید.
سپس رشته اتصالی و providerName را به نحو زیر تغییر دهید:

<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb2; Uid=root; Pwd=123;"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>

<system.data>
<DbProviderFactories>
<remove invariant="MySql.Data.MySqlClient"/>
<add name="MySQL Data Provider"
invariant="MySql.Data.MySqlClient"
description=".Net Framework Data Provider for MySQL"
type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>

همانطور که مشاهده می‌کنید در اینجا شماره نگارش دقیق پروایدر مورد استفاده نیز ذکر شده است. برای مثال اگر چندین پروایدر روی سیستم نصب است، با مقدار دهی DbProviderFactories می‌توان از نگارش مخصوصی استفاده کرد.

با این تغییرات پس از اجرای برنامه قسمت قبل، به خطای زیر برخواهیم خورد:
The given key was not present in the dictionary

توضیحات این مورد با قسمت SQLite یکی است؛ به عبارتی نیاز است بانک اطلاعاتی testdb را دستی درست کرد. همچنین جداول و فیلدها را نیز باید دستی ایجاد کرد و database migrations را نیز باید خاموش کرد (پارامتر Database.SetInitializer را به نال مقدار دهی کنید).
برای این منظور یک دیتابیس خالی را ایجاد کرده و سپس دو جدول زیر را به آن اضافه کنید:

CREATE TABLE IF NOT EXISTS `bills` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Amount` float DEFAULT NULL,
`Description` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`Payee_Id` int(11) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `payees` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`CreatedOn` datetime NOT NULL,
`CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
`ModifiedOn` datetime NOT NULL,
`ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;

پس از این تغییرات، برنامه بدون مشکل اجرا خواهد شد (ایجاد بانک اطلاعاتی خالی به همراه ایجاد ساختار جداول و خاموش کردن database migrations که توسط این پروایدر پشتیبانی نمی‌شود).

به علاوه پروایدر تجاری دیگری هم در سایت devart.com برای MySQL و EF Code first مهیا است که مباحث database migrations را به خوبی مدیریت می‌کند.


مشکل!
اگر به همین نحو برنامه را اجرا کنیم، فیلدهای یونیکد فارسی ثبت شده در MySQL با «??????? ?? ????» مقدار دهی خواهند شد و تنظیم CHARACTER SET utf8 COLLATE utf8_persian_ci نیز کافی نبوده است (این مورد با SQLite یا نگارش‌های مختلف SQL Server بدون مشکل کار می‌کند و نیاز به تنظیم اضافه‌تری ندارد):

ALTER TABLE `bills` DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci

برای رفع این مشکل توصیه شده است که CharSet=UTF8 را به رشته اتصالی به بانک اطلاعاتی اضافه کنیم. اما در این حالت خطای زیر ظاهر می‌شود:
The provider did not return a ProviderManifestToken string
این مورد فقط به اشتباه بودن تعاریف رشته اتصالی بر می‌گردد؛‌ یا عدم پشتیبانی از تنظیم اضافه‌ای که در رشته اتصالی ذکر شده است.
مقدار صحیح آن دقیقا مساوی CHARSET=utf8 است (با همین نگارش و رعایت کوچکی و بزرگی حروف؛ مهم!):

<connectionStrings>
<clear/>
<add name="Sample09Context"
connectionString="Datasource=localhost; Database=testdb; Uid=root; Pwd=123;CHARSET=utf8"
providerName="MySql.Data.MySqlClient"/>
</connectionStrings>


به این ترتیب، مشکل ثبت عبارات یونیکد فارسی برطرف می‌شود (البته جدول هم بهتر است به DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci تغییر پیدا کند؛ مطابق دستور Alter ایی که در بالا ذکر شد).

مطالب
فعال سازی سطح دوم کش در Fluent NHibernate

سطح اول کش در NHibernate در یک تراکنش معنا پیدا می‌کند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنش‌های جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.

الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریم‌ها امکان دریافت فایل‌های مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، می‌خواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامه‌های وب و دسکتاپ بدون مشکل کار می‌کند).

ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate‌ می‌باشد. این‌کار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعریف نوع کش در هنگام ایجاد نگاشت‌ها
اگر از ClassMap‌ها برای تعریف نگاشت‌ها استفاده می‌کنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده می‌کنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعریف یک سطر فوق هم مهم است؛ زیرا در غیراینصورت فقط primary key حاصل از بار اول فراخوانی کوئری‌های مرتبط کش می‌شوند؛ نه نتیجه عملیات. هرچند این مورد هم یک قدم مثبت به شمار می‌رود از این لحاظ که برای مثال تهیه نتایج کوئری بر روی فیلدی که ایندکس بر روی آن تعریف نشده است همیشه از حالت تهیه کوئری بر روی فیلد دارای ایندکس کندتر است. اما هدف ما در اینجا این است که پس از بار اول فراخوانی کوئری، بار‌های دوم و بعدی دیگر کوئری خاصی را به بانک اطلاعاتی ارسال نکرده و نتایج از کش خوانده شوند (جهت استفاده عموم کاربران در کلیه تراکنش‌های جاری برنامه).

د) اعمال متد Cacheable به کوئر‌ی‌ها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان می‌کنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنش‌های جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینه‌تر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).

مطالب
LocalDB FAQ
SQL Server Express LocalDB یا به صورت خلاصه LocalDB، یک بانک اطلاعاتی‌است که به صورت متصل به پروسه‌ی برنامه‌ی جاری اجرا می‌شود؛ برخلاف رویه‌ی متداول بانک‌های اطلاعاتی که به صورت یک سرویس مستقل اجرا می‌شوند. هدف آن، جایگزین کردن نگارش Express نیست و بیشتر حجم کم و سهولت توزیع آن مدنظر بوده‌است. برای مثال نگارش Express به صورت یک سرویس مجزا و مستقل بر روی سیستم نصب می‌شود؛ اما LocalDB به همراه و متصل به برنامه‌ی نوشته شده، اجرا می‌شود:


اگر به تصویر فوق دقت کنید، یک child process جدید به نام sqlservr.exe نیز به همراه برنامه‌ی آزمایشی ما به صورت خودکار اجرا شده‌است. این child process به همراه پارامترهای ذیل است (که توسط NET Framework. مقدار دهی می‌شوند و مدیریت نهایی آن خودکار است):
 "C:\Program Files\Microsoft SQL Server\120\LocalDB\Binn\\sqlservr.exe"   
-c -SMSSQL12E.LOCALDB
-sLOCALDB#5657074F
-d"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\master.mdf"
-l"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\mastlog.ldf"
-e"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\error.log"
بنابراین LocalDB برخلاف SQL Server CE، یک بانک اطلاعاتی in-process نیست و به صورت یک پروسه‌ی مجزا اجرا می‌شود. زمانیکه از SQL Server CE استفاده می‌شود، موتور این بانک اطلاعاتی چند فایل DLL بیشتر نیستند و نهایتا اجرای آن داخل پروسه‌ی برنامه‌ی ما و همانند اجرای سایر DLLهای متصل و مورد استفاده‌ی به آن است.
اما LocalDB یک بانک اطلاعاتی user-mode است و در پروفایل کاربر جاری سیستم اجرا می‌شود. این بانک اطلاعاتی یک بار بر روی سیستم نصب می‌شود و در هر برنامه‌ای که از آن استفاده می‌کنید، یک child process مجزای خاص خودش را (sqlservr.exe) اجرا خواهد کرد. اجرا و خاتمه‌ی این child processها نیز خودکار هستند و نیازی به دخالت مستقیم برنامه ندارند.
البته به نظر توسعه‌ی SQL Server CE متوقف شده‌است و دیگر پشتیبانی نمی‌شود. بنابراین گزینه‌ی ترجیح داده شده‌ی برای کارهایی با حجم‌های بانک اطلاعاتی زیر 10 گیگابایت ، می‌تواند LocalDB باشد. به علاوه اینکه قابلیت‌های T-SQL بیشتری را نیز پشتیبانی می‌کند و همچنین پشتیبانی منظمی نیز از آن وجود دارد. برای مثال پیش نمایش نگارش 2016 آن نیز موجود است.

در ادامه، یک سری پرسش و پاسخ متداول جهت کار با LocalDB را مرور خواهیم کرد.


محل دریافت آخرین نگارش مستقل آن کجاست؟

همانطور که عنوان شد، یکی از مهم‌ترین اهداف LocalDB، سهولت توزیع آن است و عدم نیاز به یک Admin سیستم، برای نصب و نگهداری آن. نگارش 2014 SP1 آن‌را از آدرس ذیل می‌توانید دریافت کنید:
https://www.microsoft.com/en-us/download/details.aspx?id=46697

در اینجا نسخه‌های متعددی وجود دارند. برای مثال اگر سیستم شما 64 بیتی است، تنها نیاز است ENU\x64\SqlLocalDB.msi را دریافت و نصب کنید:



پارامترهای نصب خاموش آن برای توزیع ساده‌ی برنامه کدامند؟

اگر می‌خواهید نصاب LocalDB را به همراه setup برنامه‌ی خود توزیع کنید، می‌توانید روش توزیع خاموش را با ذکر پارامترهای ذیل، مورد استفاده قرار دهید:
 msiexec /i SqlLocalDB.msi /qn IACCEPTSQLLOCALDBLICENSETERMS=YES


رشته‌ی اتصالی مخصوص آن کدام است؟

  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
اگر نگارش 2014 SP1 آن‌را نصب کرده باشید، رشته‌ی اتصالی فوق، تمام آن‌چیزی است که برای شروع به کار با آن، نیاز دارید و دارای دو قسمت مهم است:
الف) ذکر وهله‌ی مدنظر
در اینجا وهله‌ی MSSQLLocalDB ذکر شده‌است؛ اما چه وهله‌هایی بر روی سیستم نصب هستند و چطور می‌توان وهله‌ی دیگری را ایجاد کرد؟ برای این منظور، به پارامترهای sqlservr.exe ابتدای بحث دقت کنید. اکثر آن‌ها به پوشه‌ی ذیل اشاره می‌کنند:
 C:\Users\your_user_name_here\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
با یک چنین محتوایی

در این پوشه، وهله‌‌های موجود و نصب شده‌ی بر روی سیستم شما نمایش داده می‌شوند که یکی از آن‌ها را می‌توانید در رشته‌ی اتصالی فوق ذکر کنید.
به علاوه، این لیست را توسط برنامه‌ی کمکی SqlLocalDB.exe، به همراه پارامتر info یا i نیز می‌توانید دریافت و بررسی کنید:


برنامه‌ی کمکی SqlLocalDB.exe به همراه نصاب LocalDB، نصب می‌شود و توسط آن می‌توان نگارش‌های مختلف نصب شده‌را با پارامتر v و وهله‌ی مختلف موجود را با پارامتر i مشاهده کرد.
همچنین اگر می‌خواهید وهله‌ی جدیدی را بجز وهله‌ی پیش فرض MSSQLLocalDB ایجاد کنید، می‌توانید از پارامتر create آن به نحو ذیل استفاده نمائید:
For LocalDB SQL EXPRESS 2014
 "C:\Program Files\Microsoft SQL Server\120\Tools\Binn\SqlLocalDB.exe" create "v12.0" 12.0 -s

For LocalDB SQL Express 2012
 "C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SqlLocalDB.exe" create "v11.0" 11.0 -s

ب) ذکر DataDirectory
در رشته‌ی اتصالی فوق، پارامتر DataDirectory نیز ذکر شده‌است تا بتوان مسیر بانک اطلاعاتی را به صورت نسبی و بدون ذکر عبارت دقیق آن که ممکن است در سیستم‌های دیگر متفاوت باشد، پردازش کرد. این پارامتر در برنامه‌های وب به پوشه‌ی استاندارد app_data اشاره می‌کند و نیازی به تنظیم اضافه‌تری ندارد. اما در برنامه‌های دسکتاپ باید به نحو ذیل به صورت دستی، در آغاز برنامه مقدار دهی شود:
 AppDomain.CurrentDomain.SetData("DataDirectory", AppDomain.CurrentDomain.BaseDirectory);
به این ترتیب DataDirectory به محل قرارگیری فایل exe برنامه اشاره می‌کند. بدیهی است در اینجا هر پوشه‌ی دیگری را نیز می‌توانید ذکر کنید:
 AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "db"));
برای نمونه تنظیم فوق به زیر پوشه‌ی db، در کنار فایل exe برنامه اشاره می‌کند.


محل نصب بانک‌های اطلاعاتی پیش فرض آن کدام است؟

ذکر AttachDbFilename در رشته‌ی اتصالی فوق، اختیاری است. در صورت عدم ذکر آن، بانک اطلاعاتی ایجاد شده را در یکی از مسیرهای ذیل می‌توانید جستجو کنید:
 C:\Users\USERNAME\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
C:\Users\USERNAME\AppData\Local\Microsoft\VisualStudio\SSDT
همچنین در این محل‌ها فایل‌های log متنی خطاهای این بانک اطلاعاتی را نیز می‌توان مشاهده کرد. بنابراین اگر به خطای خاصی برخوردید، بهترین کار، بررسی این فایل‌‌ها است.


آیا می‌توان فایل‌های mdf و ldf آن‌را به نگارش کامل SQL Server متصل (attach) کرد؟

بله. اما باید دقت داشته باشید که SQL Server به محض اتصال یک بانک اطلاعاتی با نگارش پایین‌تر به آن، ابتدا شماره نگارش آن‌‌را به روز می‌کند. یعنی دیگر نخواهید توانست این بانک اطلاعاتی را با نگارش پایین‌تر LocalDB باز کنید و یک چنین پیام خطایی را دریافت خواهید کرد:
 The database xyz cannot be opened because it is version 706. This server supports version 663 and earlier. A downgrade path is not supported.


چگونه محتوای بانک‌های اطلاعاتی LocalDB را با VS.NET مشاهده کنیم؟

از منوی view گزینه‌ی server explorer را انتخاب کنید. بر روی data connections کلیک راست کرده و گزینه‌ی Add connection را انتخاب کنید.


در صفحه‌ی باز شده، گزینه‌ی Microsoft SQL server  را انتخاب کنید. در صفحه‌ی بعد، ذکر server name مطابق data source رشته‌ی اتصالی بحث شده و سپس انتخاب گزینه‌ی attach a database file کفایت می‌کند:


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



چگونه از LocalDB با EF استفاده کنیم؟

EF 6.x به صورت پیش فرض از بانک اطلاعاتی LocalDB استفاده می‌کند و تنها داشتن یک چنین تنظیمی در فایل کانفیگ برنامه، برای کار با آن کافی است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>
یک قسمت آن ذکر رشته‌ی اتصالی است که در مورد آن بحث شد و قسمت دوم آن، ذکر connection factory مخصوص localdb است که به صورت فوق می‌باشد. تنظیم دیگری برای کار با LocalDB و EF 6.x نیازی نیست.
البته باید دقت داشت که اسمبلی EntityFramework.SqlServer نیز به صورت خودکار به همراه بسته‌ی نیوگت EF 6.x به برنامه اضافه می‌شود که استفاده‌ی از connection factory ذکر شده را میسر می‌کند.


استفاده‌ی از LocalDB به همراه برنامه‌های وب چگونه است؟

سه نکته را باید در حین استفاده‌ی از LocalDB، در برنامه‌های وب اجرا شده‌ی بر روی IIS مدنظر داشت:
الف) LocalDB یک بانک اطلاعاتی user-mode است و child process آن تحت مجوز اکانت تنظیم شده‌ی برای آن کار می‌کند.
ب) همانطور که عنوان شد، در رشته‌ی اتصالی ذکر شده، پارامتر DataDirectory به پوشه‌ی استاندارد app_data اشاره می‌کند که فایل‌های قرار گرفته‌ی در آن توسط IIS محافظت می‌شوند و از طریق وب قابل دسترسی و دانلود نیستند.
ج) child process مربوط به LocalDB، نیاز به دسترسی write، برای کار با فایل‌های mdf و ldf خود دارد.

برای مورد الف نیاز است تا به تنظیمات application pool برنامه مراجعه کرده و سپس بر روی آن کلیک راست کرد و گزینه‌ی advanced settings را انتخاب نمود. در اینجا گزینه‌ی load user profile باید true باشد:


تنظیم load user profile ضروری است اما کافی نیست. پس از آن باید setProfileEnvironment را نیز به true تنظیم کرد. تنظیم این مورد در کنسول مدیریتی IIS به صورت زیر است.
ابتدا ریشه‌ی اصلی سرور را انتخاب کنید و سپس به configuration editor آن وارد شوید:


در ادامه از دارپ داون آن، گزینه‌ی system.applicationHost و زیر شاخه‌ی applicationPools آن‌را انتخاب کنید:


در اینجا application pool defaults و سپس در آن processModel را نیز باز کنید:


اکنون امکان ویرایش setProfileEnvironment را به true خواهید داشت:


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


در ادامه برای تنظیم دسترسی write (موارد ب و ج)، ابتدا بر روی پوشه‌ی app_data برنامه، کلیک راست کرده و برگه‌ی security آن‌را باز کنید. سپس بر روی دکمه‌ی edit کلیک کرده و در صفحه‌ی باز شده بر روی دکمه‌ی add کلیک کنید تا بتوان به کاربر application pool برنامه دسترسی write داد:


در اینجا iis apppool\TestLocalDB را وارد کرده و بر روی دکمه‌ی check name کلیک کنید.

iis apppool آن که مشخص است. عبارت TestLocalDB نام application pool ایی است که برای برنامه‌ی وب خود ایجاد کرده‌ایم (بهتر است به ازای هر برنامه‌ی وب، یک application pool مجزا تعریف شود).


در اینجا بر روی OK کلیک کرده و به این کاربر جدید اضافه شده، دسترسی full control را بدهید تا برنامه و یوزر آن بتواند فایل‌های mdf و ldf را ایجاد کرده و به روز رسانی کنند.

پس از تنظیم load user profile و همچنین set profile environment و دادن دسترسی write به کاربر application pool برنامه، اکنون child process مربوط به local db را می‌توان ذیل پروسه‌ی IIS مشاهده کرد و برنامه قادر به استفاده‌ی از LocalDB خواهد بود: