Quantcast
Channel: ‫فید مطالب وحید نصیری .NET Tips
Viewing all 271 articles
Browse latest View live

‫ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها

$
0
0
تا اینجا یک پروژه‌ی خالی ASP.NET Core 1.0 را به مرحله‌ی فعال سازی ASP.NET MVC و تنظیمات مسیریابی‌های اولیه‌یآن رسانده‌ایم. مرحله‌ی بعد، افزودن Viewها، نمایش اطلاعاتی به کاربران و دریافت اطلاعات از آن‌ها است و همانطور که پیشتر نیز عنوان شد، برای «ارتقاء» نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامه‌ی این سری مطالعه کنید.

معرفی فایل جدید ViewImports

پروژه‌ی خالی ASP.NET Core 1.0 فاقد پوشه‌ی Views به همراه فایل‌های آغازین آن است. بنابراین ابتدا در ریشه‌ی پروژه، پوشه‌ی جدید Views را ایجاد کنید.
فایل‌های آغازین این پوشه هم در مقایسه‌ی با نگارش‌های قبلی ASP.NET MVC اندکی تغییر کرده‌اند. برای مثال در نگارش قبلی، فایل web.config ایی در ریشه‌ی پوشه‌ی Views قرار داشت و چندین مقصود را فراهم می‌کرد:
الف) در آن تنظیم شده بود که هر نوع درخواستی به فایل‌های موجود در پوشه‌ی Views، برگشت خورده و قابل پردازش نباشند. این مورد هم از لحاظ مسایل امنیتی اضافه شده بود و هم اینکه در ASP.NET MVC، برخلاف وب فرم‌ها، شروع پردازش یک درخواست، از فایل‌های View شروع نمی‌شود. به همین جهت است که درخواست مستقیم آن‌ها بی‌معنا است.
در ASP.NET Core، فایل web.config از این پوشه حذف شده‌است؛ چون دیگر نیازی به آن نیست. اگر مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» را به خاطر داشته باشید، هر پوشه‌ای که توسط میان افزار Static Files عمومی نشود، توسط کاربران برنامه قابل دسترسی نخواهد بود و چون پوشه‌ی Views هم به صورت پیش فرض توسط این میان افزار عمومی نمی‌شود، نیازی به فایل web.config، جهت قطع دسترسی به فایل‌های موجود در آن وجود ندارد.

ب) کاربرد دیگر این فایل web.config، تعریف فضاهای نام پیش فرضی بود که در فایل‌های View مورد استفاده قرار می‌گرفتند. برای مثال چون فضای نام HTML Helperهای استاندارد ASP.NET MVC در این فایل web.config قید شده بود، دیگر نیازی به تکرار آن در تمام فایل‌های View برنامه وجود نداشت. در ASP.NET Core، برای جایگزین کردن این قابلیت، فایل جدیدی را به نام ViewImports.cshtml_ معرفی کرده‌اند، تا دیگر نیازی به ارث بری از فایل web.config وجود نداشته باشد.


برای مثال اگر می‌خواهید بالای Viewهای خود، مدام ذکر using مربوط به فضای نام مدل‌ها برنامه را انجام ندهید، این سطر تکراری را به فایل جدید view imports منتقل کنید:
 @using MyProject.Models

و این فضاهای نام به صورت پیش فرض برای تمام viewها مهیا هستند و نیازی به تعریف مجدد، ندارند:
• System
• System.Linq
• System.Collections.Generic
• Microsoft.AspNetCore.Mvc
• Microsoft.AspNetCore.Mvc.Rendering


افزودن یک View جدید

در نگارش‌های پیشین ASP.NET MVC، اگر بر روی نام یک اکشن متد کلیک راست می‌کردیم، در منوی ظاهر شده، گزینه‌ی Add view وجود داشت. چنین گزینه‌ای در نگارش RTM اول ASP.NET Core وجود ندارد و مراحل ایجاد یک View جدید را باید دستی طی کنید. برای مثال اگر نام کلاس کنترلر شما PersonController است، پوشه‌ی Person را به عنوان زیر پوشه‌ی Views ایجاد کرده و سپس بر روی این پوشه کلیک راست کنید، گزینه‌ی add new item را انتخاب و سپس واژه‌ی view را جستجو کنید:


البته یک دلیل این مساله می‌تواند امکان سفارشی سازی محل قرارگیری این پوشه‌ها در ASP.NET Core نیز باشد که در ادامه آن‌را بررسی خواهیم کرد (و ابزارهای از پیش تعریف شده عموما با مکان‌های از پیش تعریف شده کار می‌کنند).


امکان پوشه بندی بهتر فایل‌های یک پروژه‌ی ASP.NET Core نسبت به مفهوم Areas در نگارش‌های پیشین ASP.NET MVC

حالت پیش فرض پوشه بندی فایل‌های اصلی برنامه‌های ASP.NET MVC، مبتنی بر فناوری‌ها است؛ برای مثال پوشه‌های views و Controllers و امثال آن تعریف شده‌اند.
Project   
- Controllers
- Models
- Services
- ViewModels
- Views
روش دیگری را که برای پوشه بندی پروژه‌های ASP.NET MVC پیشنهاد می‌کنند (که Area توکار آن نیز زیر مجموعه‌ی آن محسوب می‌شود)، اصطلاحا Feature Folder Structure نام دارد. در این حالت برنامه بر اساس ویژگی‌ها و قابلیت‌های مختلف آن پوشه بندی می‌شود؛ بجای اینکه یک پوشه‌ی کلی کنترلرها را داشته باشیم و یک پوشه‌ی کلی views را که پس از مدتی، ارتباط دادن بین این‌ها واقعا مشکل می‌شود.
هرکسی که مدتی با ASP.NET MVC کار کرده باشد حتما به این مشکل برخورده‌است. درحال پیاده سازی قابلیتی هستید و برای اینکار نیاز خواهید داشت مدام بین پوشه‌های مختلف برنامه سوئیچ کنید؛ از پوشه‌ی کنترلرها به پوشه‌ی ویووها، به پوشه‌ی اسکریپت‌ها، پوشه‌ی اشتراکی ویووها و غیره. پس از رشد برنامه به جایی خواهید رسید که دیگر نمی‌توانید تشخیص دهید این فایلی که اضافه شده‌است ارتباطش با سایر قسمت‌ها چیست؟
ایده‌ی «پوشه بندی بر اساس ویژگی‌ها»، بر مبنای قرار دادن تمام نیازهای یک ویژگی، درون یک پوشه‌ی خاص آن است:


همانطور که مشاهده می‌کنید، در این حالت تمام اجزای یک ویژگی، داخل یک پوشه قرار گرفته‌اند؛ از کنترلر مرتبط با Viewهای آن تا فایل‌های css و js خاص آن.
برای پیاده سازی آن:
1) نام پوشه‌ی Views را به Features تغییر دهید.
2) پوشه‌ای را به نام StartupCustomizations به برنامه اضافه کرده و سپس کلاس ذیل را به آن اضافه کنید:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;
namespace Core1RtmEmptyTest.StartupCustomizations
{ public class FeatureLocationExpander : IViewLocationExpander {
   public void PopulateValues(ViewLocationExpanderContext context)
   {
    context.Values["customviewlocation"] = nameof(FeatureLocationExpander);
   }
   public IEnumerable<string> ExpandViewLocations(
    ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
   {
    return new[]
    { "/Features/{1}/{0}.cshtml", "/Features/Shared/{0}.cshtml"
    };
   } }
}
حالت پیش فرض ASP.NET MVC، یافتن فایل‌ها در مسیرهای Views/{1}/{0}.cshtml و Views/Shared/{0}.cshtml است؛ که در اینجا {0} نام view است و {1} نام کنترلر. این ساختار هم در اینجا حفظ شده‌است؛ اما اینبار به پوشه‌ی جدید Features اشاره می‌کند.
RazorViewEngine برنامه، بر اساس وهله‌ی پیش فرضی از اینترفیس IViewLocationExpander، محل یافتن Viewها را دریافت می‌کند. با استفاده از پیاه سازی فوق، این پیش فرض‌ها را به پوشه‌ی features هدایت کرده‌ایم.
3) در ادامه به کلاس آغازین برنامه مراجعه کرده و پس از فعال سازی ASP.NET MVC، این قابلیت را فعال سازی می‌کنیم:
public void ConfigureServices(IServiceCollection services)
{ services.AddMvc(); services.Configure<RazorViewEngineOptions>(options => {
   options.ViewLocationExpanders.Add(new FeatureLocationExpander()); });
4) اکنون تمام فایل‌های مرتبط با یک ویژگی را به پوشه‌ی خاص آن انتقال دهید. منظور از این فایل‌ها، کنترلر، فایل‌های مدل و ویوومدل، فایل‌های ویوو و فایل‌های js و css هستند و نه مورد دیگری.
5) اکنون باید پوشه‌ی Controllers خالی شده باشد. این پوشه را کلا حذف کنید. از این جهت که کنترلرها بر اساس پیش فرض‌های ASP.NET MVC (کلاس ختم شده‌ی به کلمه‌ی Controller واقع در اسمبلی که از وابستگی‌های ASP.NET MVC استفاده می‌کند) در هر مکانی که تعریف شده باشند، یافت خواهند شد و پوشه‌ی واقع شدن آن‌ها مهم نیست.
6) در آخر به فایل project.json مراجعه کرده و قسمت publish آن‌را جهت درج نام پوشه‌ی Features اصلاح کنید (تا در حین توزیع نهایی استفاده شود):
"publishOptions": {
 "include": [
  "wwwroot",
  "Features",
  "appsettings.json",
  "web.config"
 ]
},


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


امکان ارائه‌ی برنامه بدون ارائه‌ی فایل‌های View آن

ASP.NET Core به همراه یک EmbeddedFileProviderنیز هست. حالت پیش فرض آن PhysicalFileProvider است که بر اساس تنظیمات IViewLocationExpander توکار (و یا نمونه‌ی سفارشی فوق در مبحث پوشه‌ی ویژگی‌ها) کار می‌کند.
برای راه اندازی آن ابتدا نیاز است بسته‌ی نیوگت ذیل را به فایل project.json اضافه کرد:
{ "dependencies": {
        //same as before
   "Microsoft.Extensions.FileProviders.Embedded": "1.0.0" },
سپس تنظیمات متد ConfigureServices کلاس آغازین برنامه را به صورت ذیل جهت معرفی EmbeddedFileProvider تغییر می‌دهیم:
services.AddMvc();
services.Configure<RazorViewEngineOptions>(options =>
{ options.ViewLocationExpanders.Add(new FeatureLocationExpander()); var thisAssembly = typeof(Startup).GetTypeInfo().Assembly;  options.FileProviders.Clear(); options.FileProviders.Add(new EmbeddedFileProvider(thisAssembly, baseNamespace: "Core1RtmEmptyTest"));
});
و در آخر در فایل project.json، در قسمت build options، گزینه‌ی embed را مقدار دهی می‌کنیم:
"buildOptions": { "emitEntryPoint": true, "preserveCompilationContext": true, "embed": "Features/**/*.cshtml"
},
در اینجا چند نکته را باید مدنظر داشت:
1) اگر نام پوشه‌ی Views را به Features تغییر داده‌اید، نیاز به ثبت ViewLocationExpanders آن‌را دارید (وگرنه، خیر).
2) در اینجا جهت مثال و بررسی اینکه واقعا این فایل‌ها از اسمبلی برنامه خوانده می‌شوند، متد options.FileProviders.Clear فراخوانی شده‌است. این متد PhysicalFileProvider  پیش فرض را حذف می‌کند. کار PhysicalFileProvider  خواندن فایل‌های ویووها از فایل سیستم به صورت متداول است.
3) کار قسمت embed در تنظیمات build، افزودن فایل‌های cshtml به قسمت منابع اسمبلی است (به همین جهت دیگر نیازی به توزیع آن‌ها نخواهد بود). اگر صرفا **/Features را ذکر کنید، تمام فایل‌های موجود را پیوست می‌کند. همچنین اگر نام پوشه‌ی Views را تغییر نداده‌اید، این مقدار همان Views/**/*.cshtml خواهد بود و یا **/Views


4) در EmbeddedFileProvider می‌توان هر نوع اسمبلی را ذکر کرد. یعنی می‌توان برنامه را به صورت چندین و چند ماژول تهیه و سپس سرهم و یکپارچه کرد (options.FileProviders یک لیست قابل تکمیل است). در اینجا ذکر baseNamespace نیز مهم است. در غیر اینصورت منبع مورد نظر از اسمبلی یاد شده، قابل استخراج نخواهد بود (چون نام اسمبلی، قسمت اول نام هر منبعی است).


فعال سازی کامپایل خودکار فایل‌های View در ASP.NET Core 1.0

این قابلیت به زودی جهت یافتن مشکلات موجود در فایل‌های razor پیش از اجرای آن‌ها، اضافه خواهد شد. اطلاعات بیشتر

‫ارتقاء به ASP.NET Core 1.0 - قسمت 11 - بررسی بهبودهای Razor

$
0
0
زبان Razor نیز در ASP.NET Core به همراه بهبودها و اضافات قابل توجهی است که در این قسمت تعدادی از آن‌ها را مانند امکان ارث بری و تزریق وابستگی‌ها، بررسی خواهیم کرد.

نحوه‌ی سفارشی سازی کلاس پایه‌ی تمام Viewهای برنامه و معرفی inherits@

در نگارش‌های پیشین ASP.NET MVC، امکان تعویض کلاس پایه‌ی Viewها، در فایل web.config واقع در پوشه‌ی ریشه‌ی Views وجود داشت. با حذف این فایلو ساده سازی و محول کردن مسئولیت‌های آن به فایل جدید view imports، اینبار برای تعریف کلاس پایه‌ی viewها می‌توان به صورت ذیل عمل کرد:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor;
namespace Core1RtmEmptyTest.StartupCustomizations
{    public abstract class MyCustomBaseView<TModel> : RazorPage<TModel>    {        public bool IsAuthenticated()        {            return Context.User.Identity.IsAuthenticated;        }
#pragma warning disable 1998        public override async Task ExecuteAsync()        {        }
#pragma warning restore 1998    }
}
به صورت پیش فرض تمام viewهای برنامه از کلاس <RazorPage<T ارث بری می‌کنند؛ که در اینجا T، نوع مدلی است که توسط model@ تنظیم می‌شود. اگر نیاز به سفارشی سازی این کلاس وجود داشت، برای مثال بجای اینکه هربار در viewها مقدار Context.User.Identity.IsAuthenticated را جهت نمایش قسمتی از صفحه، به کاربران اعتبارسنجی شده بررسی کنیم، می‌توان این قطعه کد را به یک کلاس پایه‌ی سفارشی منتقل و از آن در تمام Viewها استفاده کرد که نمونه‌ای از آن‌را در کدهای فوق مشاهده می‌کنید.
پس از تعریف این کلاس، برای ثبت و معرفی آن به فایل ViewImports.cshtml_ مراجعه کنید و این یک سطر را به ابتدای آن اضافه نمائید:
@inherits Core1RtmEmptyTest.StartupCustomizations.MyCustomBaseView<TModel>
inherits@ از تازه‌های razor بوده و جهت تعریف ارث بری‌ها کاربرد دارد. البته ممکن است در حین تعریف فوق، TModel را قرمز رنگ مشاهده کنید که مهم نیست و بیشتر مشکل ReSharper است و برنامه بدون مشکل اجرا می‌شود.
برای نمونه پس از سفارشی سازی صفحه‌ی پایه‌ی تمام Viewها، اکنون یک سطر ذیل را در هر view ایی می‌توان تعریف و استفاده کرد:
 Is Current User Authenticated? @IsAuthenticated()


معرفی functions@

دایرکتیو جدید functions@، بسیار شبیه است به دایرکتیو قدیمی و حذف شده‌ی helper@، که در نگارش‌های پیشین Razor معرفی شده بود:
@functions {    public string Test()    {        return message;    }    readonly string message = "test";
}
ASP.NET Core در حین پردازش یک View، کدهای آن‌را تبدیل به یک کلاس می‌کند و در اینجا تمام کدهای داخل بدنه‌ی functions@ را نیز به صورت اعضای این کلاس تعریف خواهد کرد. به این ترتیب یک چنین فراخوانی‌هایی در View میسر می‌شوند:
 @Test()<br />
@message


معرفی inject@

توسط دایرکتیو جدید inject@، یک خاصیت عمومی به ASP.NET Core اعلام می‌شود و سپس مقدار دهی آن بر اساس تنظیمات IoC Containerبرنامه به صورت خودکار صورت خواهد گرفت. برای مثال زمانیکه می‌خواهیم به سرویس توکار HostingEnvironment در یک  View دسترسی پیدا کنیم، می‌توان در ابتدای آن نوشت:
 @inject Microsoft.AspNetCore.Hosting.IHostingEnvironment Host;
در این حالت کد فوق از دیدگاه ASP.NET Core به صورت ذیل تفسیر می‌شود:
 [Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
public Microsoft.AspNetCore.Hosting.IHostingEnvironment Host { get; private set; }
این خاصیت عمومی نیز با توجه به از پیش ثبت شده بودن سرویس IHostingEnvironment  و مشخص شدن توسط RazorInjectAttribute، توسط IoC Container آن شناسایی شده و تامین می‌شود.
اکنون برای استفاده‌ی از آن خواهیم داشت:
<div>
 Running in @Host.EnvironmentName</div>
البته استفاده‌ی از inject@ شاید به نوعی سؤ استفاده‌ی از الگوی MVC به شما رود؛ از این جهت که اطلاعات مورد نیاز یک View، باید از طریق کنترلر آن تامین شود و خود View نباید به صورت مستقیم درخواست تامین آ‌ن‌ها را بدهد. اما باید دقت داشت که در نهایت View نیاز دارد تا کدها را اجرا کرده و خروجی را تولید کند و برای این منظور، در پشت صحنه سرویس‌های زیادی مانند IUrlHelper ، IViewComponentHelper ، IHtmlHelper و غیره به همین ترتیب در اختیار آن قرار می‌گیرند. به علاوه استفاده‌ی از تزریق وابستگی‌ها بهتر است از روش ارث بری صفحات پایه، از این جهت که انتخاب composition همواره مقدم است بر inheritance و سبب انعطاف پذیری بیشتری نسبت به قبل می‌گردد. داشتن یک صفحه‌ی پایه که بتواند تمام نیازهای انواع و اقسام Viewها را تامین کند، دور از انتظار و گاهی از اوقات، سبب سنگینی بیش از حد پردازش تمام Viewها خواهد شد. اما تزریق سرویس‌هایی اینچنینی جهت تامین نیازهای اولیه و تکراری یک یا چند View خاص، کل برنامه را سنگین نکرده و همچنین انعطاف پذیری بیشتری را در جهت تامین آن‌ها فراهم می‌کند.
به علاوه باید دقت داشت اگر تعریف inject@ فوق را در فایل view import قرار دهیم، این سرویس در اختیار تمام Viewهای برنامه قرار خواهد گرفت و دیگر نیازی به قرار دادن آن در یک کلاس پایه‌ی سفارشی نیست.
یکی از مفیدترین استفاده‌های از قابلیت تزریق سرویس‌ها در Viewها می‌تواند دسترسی به سرویس تامین تنظیمات برنامه باشد (که در مورد نحوه‌ی تامین آن در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» بیشتر بحث شد):
 @inject IOptions<SmtpConfig> Settings;

‫ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers

$
0
0
یکی دیگر از تغییرات مهم Razor در ASP.NET Core، معرفی Tag Helpers است که همانند HTML Helpers نگارش‌های پیشین ASP.NET MVC، کار رندر کردن HTML را انجام می‌دهند و در اغلب موارد می‌توان آن‌ها را جایگزین HTML Helpers کرد. مزیت استفاده‌ی از Tag helpers، شبیه بودن آن‌ها به المان‌ها و ویژگی‌های HTML است. در کل اینکه باید از HTML Helpers استفاده کرد و یا از Tag Helpers، بیشتر یک انتخاب شخصی و سلیقه‌ای است.


فعال سازی استفاده‌ی از Tag Helpers برای تمام Viewهای برنامه

برای اینکه تمام Viewهای سایت بتوانند به امکانات Tag Helpers دسترسی پیدا کنند، باید یک سطر ذیل را به فایل ViewImports.cshtml_اضافه کرد:
 @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
در اینجا * به معنای استفاده‌ی از تمام Tag Helpers موجود در اسمبلی ذکر شده‌است.

Microsoft.AspNetCore.Mvc.TagHelpers به همراه افزودن وابستگی Microsoft.AspNetCore.Mvc در حین فعال سازی ASP.NET MVC، به پروژه اضافه می‌شود:



فعال سازی Intellisense مربوط به Tag Helpers در ویژوال استودیو

هرچند فعال سازی ASP.NET MVC، تنها وابستگی است که برای کار با Tag Helpers نیاز است، اما برای فعال سازی Intellisense آن‌ها باید بسته‌ی Microsoft.AspNetCore.Razor.Tools را نیز به فایل prject.json برنامه، جهت نصب معرفی کرد:
{    "dependencies": {
         //same as before
         "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0",
         "Microsoft.AspNetCore.Razor.Runtime": "1.0.0",        "Microsoft.AspNetCore.Razor.Tools": {            "version": "1.0.0-preview2-final",            "type": "build"        }    },    "tools": {
         //same as before        "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"    } 
}
ضمنا اگر از ReSharper استفاده می‌کنید (تا نگارش resharper-2016.1)، فعلا مجبور هستید که آن‌را غیرفعال کنید. اطلاعات بیشتر


یک مثال: ایجاد لینکی به یک اکشن متد
<a asp-controller="Home" asp-action="Index" asp-route-id="123">Home</a>
در اینجا نحوه‌ی ایجاد لینکی را مشاهده می‌کنید که به کنترلر Home و اکشن متد Index آن اشاره می‌کند. این syntax جدید، جایگزین ActionLink مربوط به HTML Helperها است. در اینجا asp-route-id را نیز مشاهده می‌کنید. قسمت asp-route آن جهت مقدار دهی پارامترهای مسیریابی است و قسمت id- بنابر نام پارامتری که قرار است مقدار دهی شود، متغیر خواهد بود.
اگر نیاز به اشاره‌ی به مسیریابی خاصی از طریق نام آن وجود دارد (همان نام‌هایی که در حین تعریف یک مسیریابیذکر می‌شوند) می‌توان به صورت ذیل عمل کرد:
<a asp-route="login">Login</a>
و یا برای مشخص سازی پروتکل خاصی و یا ذکر دقیق نام هاست، می‌توان از روش زیر استفاده کرد:
<a asp-controller="Account"
   asp-action="Register"
   asp-protocol="https"
   asp-host="asepecificdomain.com"
   asp-fragment="fragment">Register</a>


راهنمای تبدیل HTML Helpers به Tag Helpers

در جدول ذیل، مثال‌هایی را از HTML Helpers متداول و معادل‌های Tag Helper آن‌ها مشاهده می‌کنید:

Tag Helper
HTML Helper
<label asp-for="Email" class="col-md-2 control-label"></label>
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<a asp-controller="MyController" asp-action="MyAction" 
class="my-css-classname" my-attr="my-attribute">Click me</a>
@Html.ActionLink("Click me", "MyController", "MyAction", 
{ @class="my-css-classname", data_my_attr="my-attribute"})
<input asp-for="FirstName" style="width:100px;"/>
@Html.TextBox("FirstName", Model.FirstName, new { style = "width: 100px;" })
<input asp-for="Email" class="form-control" />
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
<input asp-for="Password" class="form-control" />
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
<input asp-for="UserName" class="form-control" />
@Html.EditorFor(l => l.UserName,
 new { htmlAttributes = new { @class = "form-control" } })
<form asp-controller="Account" asp-action="Register" 
method="post" class="form-horizontal" role="form">
@using (Html.BeginForm("Register", "Account",
 FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
<span asp-validation-for="UserName" class="text-danger"></span>
@Html.ValidationMessageFor(m => m.UserName, "",
 new { @class = "text-danger" })
<div asp-validation-summary="ValidationSummary.All" class="text-danger"></div>
@Html.ValidationSummary("", new { @class = "text-danger" })


نکات تکمیلی کار با فرم‌ها توسط Tag Helpers

نمونه‌ای از مثال Tag helper کار با فرم‌ها را در جدول فوق ملاحظه می‌کنید. چند نکته‌ی تکمیلی ذیل را می‌توان به آن اضافه کرد:
- در حین کار با Tag Helpers، درج anti forgery token به صورت خودکار صورت می‌گیرد. اگر می‌خواهید که این توکن ذکر نشود، آن‌را توسط ویژگی "asp-anti-forgery="false خاموش کنید.
- برای درج پارامترهای مسیریابی خاص، از asp-route به همراه نام پارامتر مدنظر استفاده کنید:
<form asp-controller="Account"
      asp-action="Login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" ></form>
که در نهایت به یک چنین حالتی رندر می‌شود
<form action="/Account/Login?returnurl=%2FHome%2FAbout" method="post">
- همانند action linkها در اینجا نیز برای اشاره‌ی به یک مسیریابی از طریق نام آن می‌توان از ویژگی asp-route استفاده کرد
<form asp-route="login"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      method="post" ></form>


Tag helpers مخصوص تعریف اسکریپت‌ها و CSSها

 در اینجا Tag Helpers صرفا به عنوان جایگزین‌های HTML Helpers مطرح نیستند. توسط آن‌ها قابلیت‌های جدیدی نیز ارائه شده‌است. برای مثال اگر تگ اسکریپت را به صورت ذیل تعریف کنیم:
<script asp-src-include="~/app/**/*.js"></script>
یک چنین خروجی فرضی را تولید می‌کند:
<script src="/app/app.js"></script><script src="/app/controllers/controller1.js"></script><script src="/app/controllers/controller2.js"></script><script src="/app/controllers/controller3.js"></script><script src="/app/controllers/controller4.js"></script><script src="/app/services/service1.js"></script><script src="/app/services/service2.js"></script>
به این معنا که یک سطر asp-src-include، بر اساس الگویی که دریافت می‌کند، تمام فایل‌های اسکریپت موجود در یک پوشه را یافته و برای آن‌ها، تگ اسکریپت تولید می‌کند. دراینجا ذکر ** به معنای بررسی تمام زیرپوشه‌های app است. اگر تنها پوشه‌ی خاصی مدنظر است، باید ** را حذف کرد.
در این بین اگر می‌خواهید از پوشه‌ی خاصی صرفنظر کنید، از asp-src-exclude استفاده کنید:
<script asp-src-include="~/app/**/*.js"
        asp-src-exclude="~/app/services/**/*.js"></script>
همچنین در اینجا امکان تعریف CDN و fallback هم وجود دارد. استفاده‌ی از CDNها جهت کاهش ترافیک سرور و بهبود کارآیی برنامه با ارائه‌ی نمونه‌های کش شده‌ی فریم ورک‌های معروف، متداول هستند که در اینجا نمونه‌ای از نحوه‌ی تعریف آن‌ها را مشاهده می‌کنید. همچنین تعریف fallback در اینجا به این معنا است که اگر CDN در دسترس نبود، به نمونه‌ی محلی موجود بر روی سرور مراجعه شود.
<link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.min.css"
      asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
      asp-fallback-test-class="hidden"
      asp-fallback-test-property="visibility"
      asp-fallback-test-value="hidden" /><script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min.js"
        asp-fallback-src="~/lib/bootstrap/js/bootstrap.min.js"
        asp-fallback-test="window.jQuery"></script>

به علاوه اگر ویژگی asp-file-version را نیز ذکر کنید:
<link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/>
یک چنین لینکی تولید می‌شود:
<link rel="stylesheet" href="/css/site.min.css?v=UdxKHVNJA5vb1EsG9O9uURFDfEE3j1E3DgwL6NiDGMc" />
هدف آن نیز اصطلاحا cache busting است. به این معنا که با تغییر محتوای این فایل‌ها، کوئری استرینگ تولید شده، مجددا محاسبه شده و مرورگر همواره آخرین نگارش موجود را دریافت خواهد کرد و دیگر از نمونه‌ی کش شده‌ی قدیمی استفاده نمی‌کند.

یک نکته:ویژگی asp-file-version را برای تصاویر هم می‌توان بکار برد:
<img src="~/images/logo.png"
     alt="company logo"
     asp-file-version="true" />
که یک چنین خروجی را تولید می‌کند و هدف آن نیز جلوگیری از کش شدن تصویر، با تغییر محتوای آن است:
<img src="/images/logo.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk"
     alt="company logo"/>


بررسی Environment Tag Helper

با متغیرهای محیطی و نحوه‌ی تعریف آن‌ها در قسمت‌های قبلآشنا شدیم. در اینجا tag helper سفارشی خاصی برای کار با آن‌ها ارائه شده‌است که شیبه به if/else عمل می‌کنند:
<environment names="Development">    <link rel="stylesheet" href="~/css/site1.css" /><link rel="stylesheet" href="~/css/site2.css" /></environment><environment names="Staging,Production"><link rel="stylesheet" href="~/css/site.min.css" asp-file-version="true"/></environment>
هدف این است که اگر متغیر محیطی به Development تنظیم شده بود، لینک‌های ساده و اصلی فایل‌های css یا اسکریپت در HTML نهایی درج شوند و اگر حالت توسعه تنظیم شده بود، لینک‌های min یا فشرده شده‌ی آن‌ها ارائه شوند؛ به همراه asp-file-version که cache busting را فعال می‌کند.


کار با دراپ داون‌ها توسط Tag helpers

فرض کنید ViewModel یک view جهت نمایش یک دراپ داون به این صورت تنظیم شده‌است:
public class CustomerViewModel
{
   public string Vehicle { get; set; }  
   public List<SelectListItem> Vehicles { get; set; }
برای نمایش SelectListItem توسط tag helpers می‌توان به صورت ذیل عمل کرد:
<select asp-for="Vehicle" asp-items="Model.Vehicles"></select>
asp-for به نام خاصیتی اشاره می‌کند که در نهایت مقدار انتخاب شده را دریافت می‌کند و asp-items لیست آیتم‌های دراپ داون را رندر می‌کند.

‫ارتقاء به ASP.NET Core 1.0 - قسمت 13 - معرفی View Components

$
0
0
روش رندر یک View در ASP.NET MVC، بر مبنای اطلاعاتی است که از کنترلر، در اختیار View آن قرار می‌گیرد. اما گاهی از اوقات نیاز است بعضی از قسمت‌های صفحه همواره نمایش داده شوند (مانند نمایش تعداد کاربران آنلاین، سخن روز، منوهای کنار صفحه و امثال آن). یک راه حل برای این مساله، اضافه کردن اطلاعات مورد نیاز View در ViewModel ارائه شده‌ی توسط کنترلر است. هرچند این روش کار می‌کند اما پس از مدتی به ViewModel هایی خواهیم رسید که تشکیل شده‌اند از چندین و چند خاصیت اضافی که الزاما مرتبط با تعریف آن ViewModel نیستند. راه حل بهتر، قرار دادن قسمت‌های مشترک صفحات در فایل layout برنامه است؛ اما فایل layout، به سادگی نمی‌تواند از دایرکتیو model@ برای مشخص سازی مدل و یا مدل‌های مورد نیاز خود استفاده کند (هر چند ممکن است؛ اما بیش از اندازه پیچیده خواهد شد).
در نگارش‌های پیشین ASP.NET MVC، یک چنین مسائلی را با معرفی Child Actionها
    public partial class SidebarMenuController : Controller
    {
        const int Min15 = 900;

        [ChildActionOnly]
        [OutputCache(Duration = Min15)]
        public virtual ActionResult Index()
        {
            return PartialView("_SidebarMenu");
        }
    }
و سپس نمایش آن‌ها توسط Html.RenderAction در فایل layout برنامه، حل می‌کنند. در ASP.NET Core، جایگزین Child Actionها، مفهوم جدیدی است به نام View Components.


یک مثال: تهیه‌ی اولین View Component

ساختار یک View Component، بسیار شبیه است به ساختار یک Controller، اما با عملکردی محدود. به همین جهت کار تعریف آن با افزودن یک کلاس سی‌شارپ شروع می‌شود و این کلاس را می‌توان در پوشه‌ای به نام ViewComponents در ریشه‌ی پروژه قرار داد (اختیاری).


سپس برای نمونه، کلاس ذیل را به این پوشه اضافه کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Core1RtmEmptyTest.Services;
namespace Core1RtmEmptyTest.ViewComponents
{    public class SiteCopyright : ViewComponent    {        private readonly IMessagesService _messagesService;        public SiteCopyright(IMessagesService messagesService)        {            _messagesService = messagesService;        }        public IViewComponentResult Invoke(int numberToTake)        {            var name = _messagesService.GetSiteName();            return View(viewName: "Default", model: name);        }        //public async Task<IViewComponentResult> InvokeAsync(int numberToTake)        //{        //    return View();        //}    }
}
همانطور که پیشترنیز عنوان شد، تزریق وابستگی‌ها در تمام قسمت‌های ASP.NET Core در دسترس هستند. در اینجا نیز از سرویس MessagesService بررسی شده‌ی در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها» برای نمایش نام سایت استفاده می‌کنیم.

ساختار کلی یک کلاس ViewComponent شامل دو جزء اصلی است:
الف) از کلاس پایه ViewComponent مشتق می‌شود. به این ترتیب توسط ASP.NET Core قابل شناسایی خواهد شد.
ب) دارای متد Invoke ایی است که بجای Html.RenderAction در نگارش‌های پیشین ASP.NET MVC، قابل فراخوانی است. این متد یک View را باز می‌گرداند.
ج) در اینجا امکان تعریف نمونه‌ی Async متد Invoke نیز وجود دارد (برای مثال جهت کار با متدهای Async بانک اطلاعاتی).
روش فراخوانی این متدها نیز به این صورت است: ابتدا به دنبال نمونه‌ی async می‌گردد. اگر یافت شد، همینجا کار خاتمه می‌یابد. اگر یافت نشد، نمونه‌ی sync یا معمولی آن فراخوانی می‌شود و اگر این هم یافت نشد، یک استثناء صادر خواهد شد.
د) متد Invoke می‌تواند دارای پارامترهای دلخواهی نیز باشد و حالت پیش فرض آن بدون پارامتر است.

روش یافتن یک view component توسط ASP.NET Core به این صورت است:
الف) این کلاس باید عمومی بوده و همچنین abstract نباشد.
ب) «یکی» از مشخصه‌های ذیل را داشته باشد:
1) نامش به ViewComponent ختم شده باشد.
2) از کلاس ViewComponent ارث بری کرده باشد.
3) با ویژگی ViewComponent مزین شده باشد.


نحوه و محل تعریف View یک View Component

پس از تعریف کلاس ViewComponent مورد نظر، اکنون نیاز است View آن‌را اضافه کرد. روش یافتن این Viewها توسط ASP.NET Core نیز بر این مبنا است که
الف) اگر این View Component عمومی و سراسری است، باید درون پوشه‌ی shared، پوشه‌ی جدیدی را به نام Components ایجاد کرده و سپس ذیل این پوشه، بر اساس نام کلاس ViewComponent، یک زیر پوشه‌ی دیگر را ایجاد و داخل آن، View مدنظر را اضافه کرد (تصویر ذیل).
 /Views/Shared/Components/[NameOfComponent]/Default.cshtml
ب) اگر این View Component تنها باید از طریق Viewهای یک کنترلر خاص قابل دسترسی باشند، زیر پوشه‌ی Component یاد شده را ذیل پوشه‌ی View همان کنترلر قرار دهید (و آن‌را از قسمت Shared خارج کنید).
 /Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml


یک نکته:اگر نام کلاسی به ViewComponent  ختم شده بود، نیازی نیست تا ViewComponent  را هم در حین ساخت پوشه‌ی آن ذکر کرد.


نحوه‌ی استفاده‌ی از View Component تعریف شده و ارسال پارامتر به آن

و در آخر برای استفاده‌ی از این View Component تعریف شده، به فایل layout برنامه مراجعه کرده و آن‌را به نحو ذیل فراخوانی کنید:
<footer><p>@await Component.InvokeAsync("SiteCopyright", new { numberToTake = 5 })</p></footer>
اولین پارامتر متد InvokeAsync، همان نام کلاس View Component است. اگر خواستید پارامتر(های) دلخواهی را به متد Invoke کلاس View Component ارسال کنید (مانند پارامتر int numberToTake در مثال فوق)، آن‌را در همینجا می‌توان ذکر کرد (با فرمت dictionary و یا  anonymous type).

یک نکته:متدهای قدیمی Component.Invoke و Component.Renderدر اینجا حذف شده‌اند (اگر مقالات پیش از RTM را مطالعه کردید) و روش توصیه شده‌ی در اینجا، کار با متدهای async است.


تفاوت‌های View Components با Child Actions نگارش‌های پیشین ASP.NET MVC

پارامترهای یک View Component از طریق یک HTTP Request تامین نمی‌شوند و همانطور که ملاحظه کردید در همان زمان فراخوانی آن‌ها به صورت مستقیم فراهم خواهند شد. بنابراین مباحث model binding در اینجا دیگر وجود خارجی ندارند. همچنین View Components جزئی از طول عمر یک کنترلر نیستند. بنابراین اکشن فیلترهای مختلف تعریف شده، تاثیری را بر روی آن‌ها نخواهند داشت (این مشکلی بود که با Child Actions در نگارش‌های قبلی مشاهده می‌شد). همچنین View Components به صورت مستقیم از طریق درخواست‌های HTTP قابل دسترسی نیستند. به علاوه Child actions قدیمی، از فراخوانی‌های async پشتیبانی نمی‌کنند.
زمانیکه کلاسی از کلاس پایه ViewComponent ارث بری می‌کند، تنها به این خواص عمومی از درخواست HTTP جاری دسترسی خواهد داشت:
[ViewComponent]
public abstract class ViewComponent
{
   protected ViewComponent();
   public HttpContext HttpContext { get; }
   public ModelStateDictionary ModelState { get; }
   public HttpRequest Request { get; }
   public RouteData RouteData { get; }
   public IUrlHelper Url { get; set; }
   public IPrincipal User { get; }

   [Dynamic]  
   public dynamic ViewBag { get; }
   [ViewComponentContext]
   public ViewComponentContext ViewComponentContext { get; set; }
   public ViewContext ViewContext { get; }
   public ViewDataDictionary ViewData { get; }
   public ICompositeViewEngine ViewEngine { get; set; }

   //...
}


فراخوانی Ajax ایی یک View Component

در ASP.NET Core، یک اکشن متد می‌تواند خروجی ViewComponent نیز داشته باشد و این تنها روشی است که می‌توان یک View Component را از طریق درخواست‌های HTTP، مستقیما قابل دسترسی کرد:
public IActionResult AddURLTest()
{
   return ViewComponent("AddURL");
}
در این حالت می‌توان این اکشن متد را به صورت Ajax ایی نیز بارگذاری و به صفحه اضافه کرد:
$(document).ready (function(){
    $("#LoadSignIn").click(function(){
         $('#UserControl').load("/Home/AddURLTest");
    });
});


امکان بارگذاری View Components از اسمبلی‌های دیگر نیز وجود دارد

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها» روش دسترسی به Viewهای برنامه را که در اسمبلی آن قرار گرفته بودند، بررسی کردیم. دقیقا همان روش در مورد view components نیز صادق است و کاربرد دارد. جهت یادآوری، این مراحل باید طی شوند:
الف) اسمبلی ثالث حاوی View Component‌های برنامه باید ارجاعاتی را به ASP.NET Core و قابلیت‌های Razor آن داشته باشد:
"dependencies": {
   "NETStandard.Library": "1.6.0",
   "Microsoft.AspNetCore.Mvc": "1.0.0",
   "Microsoft.AspNetCore.Razor.Tools": {
   "version": "1.0.0-preview2-final",
   "type": "build"
  }
},
"tools": {
   "Microsoft.AspNetCore.Razor.Tools": "1.0.0-preview2-final"
}
ب) محل قرارگیری viewهای این اسمبلی ثالث نیز همانند قسمت «نحوه و محل تعریف View یک View Component» مطلب جاری است و تفاوتی نمی‌کند. فقط برای  قرار دادن این Viewها در اسمبلی برنامه باید گزینه‌ی embed را مقدار دهی کرد:
"buildOptions": {
   "embed": "Views/**/*.cshtml"
}
ج) مرحله‌ی آخر هم معرفی این اسمبلی ثالث، به RazorViewEngineOptions به صورت یک EmbeddedFileProvider جدید است. در این مثال، ViewComponentLibrary نام فضای نام این اسمبلی است.
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
   //Get a reference to the assembly that contains the view components
   var assembly = typeof(ViewComponentLibrary.ViewComponents.SimpleViewComponent).GetTypeInfo().Assembly;
   //Create an EmbeddedFileProvider for that assembly
   var embeddedFileProvider = new EmbeddedFileProvider(assembly,"ViewComponentLibrary");
   //Add the file provider to the Razor view engine
   services.Configure<RazorViewEngineOptions>(options =>
   {
      options.FileProviders.Add(embeddedFileProvider);
   });
د) جهت رفع تداخلات احتمالی این اسمبلی با سایر اسمبلی‌ها بهتر است ویژگی ViewComponent را به همراه نامی مشخص ذکر کرد (در حین تعریف کلاس View Component):
 [ViewComponent(Name = "ViewComponentLibrary.Simple")]
public class SimpleViewComponent : ViewComponent
و در آخر فراخوانی این View Component بر اساس این نام صورت خواهد گرفت:
 @await Component.InvokeAsync("ViewComponentLibrary.Simple", new { number = 5 })

‫ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران

$
0
0
مباحث پایه‌ای اعتبارسنجی کاربران در ASP.NET Core با نگارش‌های پیشین ASP.NET MVC، آنچنان تفاوت ساختاری مهمی ندارند و یکی هستند. عمده‌ی تفاوت‌ها در نحوه‌ی برپایی تنظیمات اولیه‌ی اسکریپت‌های آن‌ها و همچنین یک سری Tag Helper جدید معادل با HTML Helperهای متداول اعتبارسنجی هستند.


دریافت وابستگی‌های سمت کاربر مباحث اعتبارسنجی

زمانیکه گزینه‌ی ایجاد یک پروژه‌ی جدید ASP.NET Core را در VS.NET انتخاب می‌کنیم، علاوه بر قالب empty آن، قالب دیگری به نام web application نیز در آن موجود است. با انتخاب این قالب، فایلی را به نام bower.json نیز با این محتوا مشاهده می‌کنید:
 {
  "name": "asp.net",
  "private": true,
  "dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.0",
"jquery-validation": "1.14.0",
"jquery-validation-unobtrusive": "3.2.6"
  }
}
این مداخل تمام پیشنیازهای مباحث اعتبارسنجی کاربران را به صورت خودکار به پروژه اضافه می‌کنند. بنابراین افزودن فایل جدید bower.json به پروژه با محتوای فوق و سپس مراجعه به solution explorer و کلیک راست بر روی گره dependencies و در آخر انتخاب restore packages، سبب دریافت خودکار این بسته‌ها می‌شود.


این بسته‌ها را پس از دریافت، در پوشه‌ی bower_components خواهید یافت:


البته باید دقت داشت که استفاده از bower در اینجا الزامی نیست. اگر علاقمند بودید از npm و node.js استفاده کنید.


افزودن وابستگی‌های سمت کاربر مباحث اعتبارسنجی و عمومی کردن آن‌ها

پس از دریافت وابستگی‌های مورد نیاز توسط bower، به فایل layout برنامه مراجعه کرده و سپس آن‌ها را به ترتیب ذیل اضافه می‌کنیم:
    <script src="~/bower_components/jquery/dist/jquery.min.js"></script>    <script src="~/bower_components/jquery-validation/dist/jquery.validate.min.js"></script>    <script src="~/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>    @RenderSection("scripts", required: false)</body></html>
هرچند این ترتیب درست است، اما ... کار نمی‌کند. علت را هم در مبحث «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» بررسی کردیم: تمام پوشه‌های موجود در ریشه‌ی یک برنامه‌ی ASP.NET Core، غیرعمومی هستند (و توسط کاربران قابل درخواست نیستند)، مگر اینکه آن‌ها‌را توسط میان افزار static files، عمومی کنیم. برای این منظور به کلاس آغازین برنامه مراجعه کرده و سپس به متد Configure آن، تنظیمات ذیل را اضافه کنید:
// Serve wwwroot as root
app.UseFileServer();
// Serve /bower_components as a separate root
app.UseFileServer(new FileServerOptions
{    // Set root of file server    FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "bower_components")),    // Only react to requests that match this path    RequestPath = "/bower_components",    // Don't expose file system    EnableDirectoryBrowsing = false
});
به این ترتیب فایل‌های موجود در پوشه‌ی bower_components به مسیر bower_components/ نگاشت می‌شوند. البته انتخاب مقدار RequestPath در اینجا اختیاری است و برای مثال می‌توانید مسیر scripts/ را نیز معرفی کنید (نگاشت مسیر فیزیکی و واقعی فایل‌ها به URL خاص دیگری که توسط وب سرور ارائه خواهد شد).
اگر RequestPath را به مسیر دیگری تنظیم کردید، نیاز است ابتدای سه مدخل ذکر شده را بر این اساس اصلاح کنید، تا فایل‌ها توسط وب سرور قابل ارائه شوند.


استفاده از CDN برای توزیع اسکریپت‌های اعتبارسنجی مورد نیاز

در مورد environment tag helper در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» پیشتر بحث شد. در اینجا نیز می‌توان برای مثال در حال توسعه، از اسکریپت‌های محلی
<environment name="Development"><script src="~/bower_components/jquery/dist/jquery.min.js"></script><script src="~/bower_components/jquery-validation/dist/jquery.validate.min.js"></script><script src="~/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script></environment>
و در حالت نهایی توزیع برنامه، از CDNهای ارائه‌ی اسکریپت‌ها، جهت کاهش بار سرور استفاده کرد:
<environment names="Staging, Production"><script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
asp-fallback-src="/bower_components/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"></script><script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"
asp-fallback-src="bower_components/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator"></script><script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js"
asp-fallback-src="/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"></script></environment>
در اینجا fallback به آدرس محلی دریافت اسکریپت‌های مدنظر، در صورت در دسترس نبودن CDN، تنظیم شده‌است.
روش عملکرد fallback هم به این صورت است که بررسی می‌شود آیا عبارت ذکر شده‌ی در قسمت asp-fallback-test قابل اجرا است یا خیر؟ اگر خیر، یعنی CDN قابل دسترسی نیست و از نمونه‌ی محلی استفاده می‌کند.


خلاصه‌ای از Tag helpers اعتبارسنجی

در جدول «راهنمای تبدیل HTML Helpers به Tag Helpers» مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers»، معادل‌های HTML Helpers مباحث اعتبارسنجی را نیز ملاحظه کردید. خلاصه‌ی تکمیلی آن به صورت ذیل است:

ValidationSummary.All سبب نمایش خطاهای اعتبارسنجی خواص و همچنین کل مدل می‌شود:
@Html.ValidationSummary(false)
معادل است با
<div asp-validation-summary="All"></div>

ValidationSummary.ModelOnly صرفا خطاهای اعتبارسنجی در سطح مدل را نمایش می‌دهد:
@Html.ValidationSummary(true)
معادل است با
<div asp-validation-summary="ModelOnly"></div>

و برای تعیین نمایش خطاهای اعتبارسنجی یک خاصیت از مدل:
@Html.ValidationMessageFor(m => m.UserName, "", new { @class = "text-danger" })
معادل است با
<span asp-validation-for="UserName" class="text-danger"></span>

‫ارتقاء به ASP.NET Core 1.0 - قسمت 15 - بررسی تغییرات Caching

$
0
0
در نگارش‌های پیشین ASP.NET MVC با استفاده از Output Cache، امکان کش کردن خروجی یک اکشن متد، وجود دارد. مکانیزم Output Cache از ASP.NET Core حذف شده‌است؛ اما جایگزین‌های قابل توجهی برای آن تدارک دیده شده‌اند.


معرفی Response Cache

جایگزین ویژگی حذف شده‌ی OutputCache در ASP.NET Core، ویژگی جدیدی است به نام ResponseCache و هدف آن تنظیم هدرهای مرتبط با caching مخصوص HTTP Response ارائه شده‌است. به همین جهت با مکانیزم OutputCache قدیمی ASP.NET MVC که اطلاعات را در حافظه‌ی سرور کش می‌کرد، کاملا متفاوت است.
البته قرار است میان افزار OutputCache را در نگارش‌های آتی ASP.NET Core نیز ارائه کنند.
[ResponseCache(Duration = 60)]
public IActionResult Contact()
{
   ViewData["Message"] = "Your contact page.";
   return View();
}
در اینجا مثالی را از نحوه‌ی تعریف این ویژگی جدید، ملاحظه می‌کنید که در آن مقدار خاصیت مدت زمان کش شدن، برحسب ثانیه است. استفاده‌ی از آن سبب خواهد شد تا هدر HTTP ذیل به خروجی از سرور اضافه شود:
 Cache-Control: public,max-age=60

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


تعیین مکان کش شدن خروجی یک اکشن متد

در هدر فوق، عبارت public را مشاهده می‌کنید. این public بودن به این معنا است که امکان کش شدن این خروجی، توسط کش سرورهای اشتراکی بین راه هم وجود دارد.
اگر می‌خواهید این امکان را غیرفعال کنید، نیاز است این public به private تنظیم شود:
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
تنظیم Location فوق به Client به معنای private شدن هدر تنظیم شده و صرفا کش شدن خروجی، توسط کش مرورگر کاربر می‌باشد.


غیرفعال کردن کش شدن خروجی یک اکشن متد

اگر خواستید از کش شدن خروجی یک اکشن متد تحت هر حالتی جلوگیری کنید، مکان آن‌را به None و NoStore آن‌را به true تنظیم کنید:
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
   return View();
}
این تنظیم سبب افزوده شدن یک چنین هدر HTTP ایی به خروجی از سرور می‌شود:
Cache-Control: no-store,no-cache
Pragma: no-cache


امکان تعریف پروفایل‌های کش

بجای اینکه تنظیمات کش کردن تکراری را به انواع و اقسام اکشن متدها اعمال کنیم، می‌توان برای آن‌ها پروفایل ایجاد کرده و از نام این پروفایل، جهت به اشتراک گذاری تنظیمات استفاده کنیم. برای این منظور به کلاس آغازین برنامه مراجعه کرده و جایی که سرویس ASP.NET MVC را فعال سازی کرده‌اید، پروفایل کش جدیدی را تعریف کنید:
public void ConfigureServices(IServiceCollection services)
{    services.AddMvc(options =>    {        options.CacheProfiles.Add("PrivateCache",            new CacheProfile            {                Duration = 60,                Location = ResponseCacheLocation.Client            });     });
پس از آن برای استفاده‌ی از این تنظیمات اشتراکی، فقط کافی است تا نام پروفایل مرتبطی را ذکر کنیم:
[ResponseCache(CacheProfileName = "PrivateCache")]


معرفی سرویس کش درون حافظه‌ای

در نگارش‌های پیشین ASP.NET، متدهایی برای کش کردن موقتی اطلاعات در حافظه و سپس بازیابی آن‌ها وجود داشتند. در ASP.NET Core، این متدها توسط سرویس ارائه کننده‌ی IMemoryCache در اختیار برنامه قرار می‌گیرند. برای فعال سازی این سرویس جدید باید مراحل ذیل طی شوند:
الف) ابتدا بسته‌ی Microsoft.Extensions.Caching.Memory را به لیست وابستگی‌های پروژه در فایل project.json اضافه کنید:
{
    "dependencies": {
      //same as before
      "Microsoft.Extensions.Caching.Memory": "1.0.0"
 },
ب) سپس به کلاس آغازین برنامه مراجعه کرده و سرویس آ‌ن‌را معرفی و ثبت کنید:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMemoryCache();
ج) پس از آن سرویس پیاده سازی کننده‌ی IMemoryCache،  در تمام اجزای برنامه در دسترس خواهد بود. برای مثال:
[Route("DNT/[controller]")]
public class AboutController : Controller
{    private readonly IMemoryCache _memoryCache;    public AboutController(IMemoryCache memoryCache)    {        _memoryCache = memoryCache;    }    [Route("")]    public ActionResult Hello()    {        string cacheKey = "my-cache-key";        string greeting;        if (!_memoryCache.TryGetValue(cacheKey, out greeting))        {
             greeting = "Hello";            // store in the cache            _memoryCache.Set(cacheKey, greeting,                new MemoryCacheEntryOptions()                .SetAbsoluteExpiration(TimeSpan.FromMinutes(1)));        }        return Content($"{greeting} from DNT!");    }
در مثال فوق، ابتدا وابستگی سرویس کش درون حافظه‌ای، به سازنده‌ی کنترلر تزریق شده‌است. تامین آن هم توسط سرویسی که در کلاس آغازین برنامه ثبت کردیم، انجام می‌شود. پس از آن در اکشن متد Hello، سعی کرده‌ایم بر اساس کلید کشی که مشخص کرده‌ایم، مقداری را بازیابی کنیم. اگر این مقدار وجود نداشته باشد، آن‌را توسط متد Set تنظیم خواهیم کرد تا برای دفعات آتی فراخوانی این متد، مورد استفاده قرار گیرد.
تنظیمات منقضی شدن کش نیز به حالت absolute تنظیم شده‌است. یعنی پس از یک دقیقه حتما منقضی می‌شود. اگر فراخوانی‌های این متد زیاد است، می‌توان حالت منقضی شدن sliding را تنظیم کرد:
 new MemoryCacheEntryOptions()
  .SetSlidingExpiration(TimeSpan.FromMinutes(5))
در این حالت اگر پیش از اتمام 5 دقیقه‌ی تنظیم شده، درخواستی به سرور رسید، این کش برای 5 دقیقه‌ی بعد نیز مجددا تمدید می‌شود.
اگر خواستیم تا این کش سر ساعت منقضی شود، اما در طی این یک ساعت به صورت sliding عمل کند، می‌توان از ترکیب دو حالت مطلق و لغزشی استفاده کرد:
 new MemoryCacheEntryOptions()
  .SetSlidingExpiration(TimeSpan.FromMinutes(5))
  .SetAbsoluteExpiration(TimeSpan.FromHours(1))

یک نکته:اگر فشار حافظه‌ی سرور زیاد شود، مدیر حافظه‌ی این کش، شروع به منقضی کردن آیتم‌هایی با حق تقدم پایین می‌کند. بالاترین حق تقدم را حالت NeverRemove ذیل دارد:
 new MemoryCacheEntryOptions()
  .SetPriority(CacheItemPriority.NeverRemove))


معرفی Tag Helpers مخصوص کش کردن قسمتی از صفحه

در ادامه‌ی مبحث معرفی Tag Helpers، تعدادی از آن‌ها جهت کش کردن محتوای قسمتی از صفحه، طراحی شده‌اند:
<cache expires-after="@TimeSpan.FromMinutes(10)">
    @Html.Partial("_WhatsNew")
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
تگ جدید cache محتوای دربرگیرنده‌ی آن‌را «در حافظه‌ی سرور» کش می‌کند (و در پشت صحنه از همان کش درون حافظه‌ای که پیشتر بحث شد، استفاده می‌کند). تگ cache در خروجی HTML نهایی مشاهده نمی‌شود و صرفا مفهومی سمت سرور است.
برای نمونه در مثال فوق، محتوای پارشال ویوو رندر شده و همچنین تاریخی که پس از آن نمایش داده شده‌است، به مدت 10 دقیقه در حافظه‌ی سرور کش می‌شوند. اگر این زمان تنظیم نشود، تا زمانیکه برنامه در سرور مشغول به کار است، این قسمت منقضی نخواهد شد.
در اینجا اگر expires-after ذکر شده بود، یعنی پس از این مدت زمان، کش منقضی می‌شود.
<cache expires-after="@TimeSpan.FromSeconds(5)"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
 اگر expires-on آن ذکر شود، می‌توان تاریخ و زمان مشخصی را در اینجا ذکر کرد (برای مثال فردا ساعت 10، با فراخوانی DateTime.Today.AddDays).
<cache expires-on="@DateTime.Today.AddDays(1).AddTicks(-1)"><!--View Component or something that gets data from the database-->
 *last updated  @DateTime.Now.ToLongTimeString()</cache>
همچنین می‌توان از expires-sliding نیز استفاده کرد. به این معنا که اگر در طی مدتی خاص این صفحه درخواست نشد، آنگاه این کشی منقضی می‌شود.
<cache expires-sliding="@TimeSpan.FromMinutes(5)"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
همچنین در اینجا می‌توان کش کردن را به ازای کاربران مختلف، کوئری استرینگ‌های مختلف و امثال آن انجام داد (با ارائه‌ی محتوای متفاوتی به ازای پارامترهای مختلف):
<cache vary-by-user="true"><!--View Component or something that gets data from the database-->
    *last updated @DateTime.Now.ToLongTimeString()</cache>
در این حالت دیگر نیازی نیست تا نگران این باشیم که آیا محتوای قسمت کش شده‌ی از صفحه برای تمام کاربران در دسترس است یا خیر؟ در اینجا هر کاربر لاگین شده‌ی به سیستم، نگارش کش شده‌ی خاص خودش را دریافت می‌کند.

<cache vary-by-route="id"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
در اینجا به ازای پارامتر آی‌دی مسیریابی، نگارش‌های مختلف کش شده‌ای از صفحه تامین می‌شوند. در اینجا می‌توان لیستی از پارامترهای جدا شده‌ی با کاما را مشخص کرد.

<cache vary-by-query="search"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
امکان کش کردن محتوای صفحه به ازای کوئری استرینگ‌های مختلف تنظیم شده نیز وجود دارد.

<cache vary-by-cookie="MyAppCookie"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
در اینجا به ازای محتواهای مختلف کوکی خاصی به نام MyAppCookie، نگارش‌های مختلف کش شده‌ای از صفحه ذخیره می‌شوند.

<cache vary-by-header="User-Agent"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
در اینجا می‌توان به ازای هدرهای مختلف پروتکل HTTP نگارش‌های کش شده‌ی متفاوتی را ارائه داد.

<cache vary-by="@ViewBag.ProductId"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
اگر خواستید کلید کش را خودتان تعیین کنید از vary-by استفاده کنید.

<cache vary-by-user="true" vary-by-route="id"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>
امکان ترکیب این موارد با هم نیز وجود دارد.

به علاوه چون زیر ساخت این Tag Helper همان Microsoft.Extensions.Caching.Memory است، امکان تنظیم حق تقدم حذف شدن آیتم‌های کش شده نیز وجود دارد:
<cache expires-sliding="@TimeSpan.FromMinutes(10)"
priority="@Microsoft.Extensions.Caching.Memory.CacheItemPriority.NeverRemove"><!--View Component or something that gets data from the database-->
    *last updated  @DateTime.Now.ToLongTimeString()</cache>


مبحث تکمیلی

امکان ذخیره سازی آیتم‌های کش شده در بانک اطلاعاتی (بجای حافظه‌ی فرار) نیز پیش بینی شده‌است که تحت عنوان «کش توزیع شده» در دسترس است.
Working with a Distributed Cache

‫ارتقاء به ASP.NET Core 1.0 - قسمت 16 - کار با Sessions

$
0
0
سشن‌ها نیز همانند تمام قسمت‌های دیگر یک برنامه‌ی ASP.NET Core، به صورت پیش فرض غیرفعال هستند و نیاز به مراحل خاصی است تا امکان استفاده‌ی از آن‌ها فراهم شود. همچنین روش کار کردن با آن‌ها نیز متفاوت است با نگارش‌های قبلی ASP.NET (تمام نگارش‌ها).


سشن چیست؟

شیء سشن، مجموعه‌ای از اشیاء serialized مرتبط با جلسه‌ی کاری جاری یک کاربر است. این اشیاء عموما در حافظه‌ی محلی سرور ذخیره می‌شوند؛ اما امکان ذخیره سازی توزیع شده‌ی آن‌ها در بانک‌های اطلاعاتی نیز پیش بینی شده‌است.
عموما استفاده‌ی از اشیاء سشن توصیه نمی‌شوند. از این جهت که این نوع اشیاء بسیار شبیه هستند به متغیرهای سراسری و وجود این نوع متغیرها اساسا ضعف طراحی شیء‌گرا به حساب می‌آیند. اما با توجه به ماهیت stateless بودن برنامه‌های وب، به این معنا که با پایان رندر یک صفحه، تمام اشیاء مرتبط با آن‌ها نیز در سمت سرور تخریب می‌شوند، نیاز است برای یک سری از داده‌های عمومی کاربر، راه حلی را پیدا کرد تا بتوان از اطلاعات آن‌ها استفاده‌ی مجدد کرد. برای مثال نگهداری رشته‌ی اتصالی بانک اطلاعاتی که کاربر در حین لاگین به سیستم آن‌را انتخاب کرده‌است (اگر برنامه به ازای هر سال از یک بانک اطلاعاتی مجزا استفاده می‌کند) و یا زمانیکه کاربری captcha را پر می‌کند و مقدار آن‌را به سمت سرور ارسال می‌کند، نیاز است مقدار ارسالی او را با مقدار ابتدایی captcha مقایسه کرد. یک چنین اطلاعاتی نباید با پایان رندر صفحه تخریب شوند و نیاز است تا زمانیکه جلسه‌ی کاری کاربر به پایان نرسیده‌است، در دسترس باشند. به همین جهت است که مفهومی را به نام «اشیاء سشن» طراحی کرده‌اند.
درکل خارج از این موارد بهتر است از سشن استفاده نکنید و در جای جای برنامه‌ی خود ردپای آن‌را باقی نگذارید و به خاطر داشته باشید:
متغیر سشن = متغیر سراسری = ضعف طراحی شی‌‌ءگرا


توصیه‌ی به استفاده‌ی از روش‌های سبک وزن‌تر

سشن‌ها تنها روش به اشتراک گذاری اطلاعات نیستند. اگر می‌خواهید اطلاعاتی را در بین میان افزارهای برنامه در طی یک درخواست به اشتراک بگذارید، شاید سشن هم یک راه حل باشد؛ اما راه حلی سنگین وزن. راه حل بهتر برای این موارد، استفاده‌ی از HttpContext.Items است. HttpContext.Items نیز همانند سشن، یک key/value store است؛ اما طول عمر آن محدود است به طول عمر درخواست جاری و در تمام میان افزارهای برنامه در دسترس است.
برای مثال در یک میان افزار آن‌را تنظیم می‌کنید:
app.Use(async (context, next) =>
{
   context.Items["isVerified"] = true;
   await next.Invoke();
});
و سپس در میان افزاری دیگر از آن استفاده خواهید کرد:
app.Run(async (context) =>
{
   await context.Response.WriteAsync("Verified request? " + context.Items["isVerified"]);
});


فعال سازی سشن‌ها در ASP.NET Core

ASP.NET Core یک choose-what-you-need framework است. به این معنا که تا زمانیکه قابلیتی را به صورت صریح فعال سازی نکرده باشید، در دسترس نخواهد بود. همین مساله در نهایت به کاهش مصرف منابع این نوع برنامه‌ها و همچنین طراحی ماژولار سیستم ختم می‌شوند. برای مثال در نگارش‌های قبلی ASP.NET (تمام نگارش‌ها)، سشن‌ها به صورت پیش فرض فعال هستند، مگر آنکه HTTP Module آن‌را در فایل web.config حذف کنید؛ اما در اینجا برعکس است.
اگر تنها در موارد خاصی که ذکر شد، نیاز به استفاده‌ی از متغیرهای سشن را داشتید، روش فعال سازی آن به صورت ذیل است:
الف) نصب بسته‌ی نیوگت Microsoft.AspNetCore.Session
برای این منظور وابستگی ذیل را به فایل project.json اضافه کنید:
{
    "dependencies": {
      //same as before
      "Microsoft.AspNetCore.Session": "1.0.0"
    }
}
ب) انجام تنظیمات آغازین برنامه
برای این کار به کلاس آغازین برنامه مراجعه کرده و ابتدا سرویس سشن‌ها را فعال کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddSession();
و سپس در متد Configure، استفاده‌ی از سشن‌ها را نیز باید ذکر کنید:
public void Configure(IApplicationBuilder app)
{
    app.UseSession();
در اینجا امکان سفارشی سازی مقادیر پیش فرض مدیریت سشن‌ها نیز وجود دارند. برای مثال زمان پیش فرض انقضای سشن کاربر، پس از 20 دقیقه عدم فعالیت او است که قابل تغییر می‌باشد:
app.UseSession(options: new SessionOptions
{
    IdleTimeout = TimeSpan.FromMinutes(30),
    CookieName = ".MyApplication"
});


روش استفاده‌ی از سشن‌ها

در مثال ذیل نحوه‌ی ذخیره سازی اطلاعات را در شیء سشن جلسه‌ی جاری یک کاربر، ملاحظه می‌کنید:
public ActionResult TestSession()
{
   this.HttpContext.Session.Set("key-1", BitConverter.GetBytes(DateTime.Now.Ticks));
   this.HttpContext.Session.SetInt32("key-2", 1);
   this.HttpContext.Session.SetString("key-3", "DNT");
   return Content("OK!");
}
به همراه نحوه‌ی بازیابی این اطلاعات در متدهای دیگر برنامه:
 public IActionResult Index()
{
   byte[] key1 = this.HttpContext.Session.Get("key-1");
   long key1Value = BitConverter.ToInt64(key1, 0);
   int? key2Value = this.HttpContext.Session.GetInt32("key-2");
   string key3Value = this.HttpContext.Session.GetString("key-3");
   return Content("OK!");
}
همانطور که ملاحظه می‌کنید، سه متد Set، SetInt32 و SetString در اینجا برای تنظیم key/valueهای سشن، از پیش موجود هستند.
حالت Set آن آرایه‌ای از بایت‌ها را دریافت می‌کند و می‌توان برای حالت serialization اشیاء، مفید باشد.
دقیقا معادل همین سه متد، متدهای Get، GetInt32 و GetString برای بازیابی مقادیر سشن طراحی شده‌اند و باید دقت داشت که خروجی‌های این‌ها می‌توانند نال نیز باشند. به همین جهت خروجی GetInt32 آن نال پذیر است.


توسعه‌ی متدهای پیش فرض کار با سشن‌ها

سه متد یاد شده‌ی کار با سشن‌ها در ASP.NET Core هرچند ضروری هستند، اما کافی نیستند. برای توسعه‌ی آن‌ها می‌توان متدهای الحاقی را تدارک دید که نمونه‌ای از آن‌ها را ذیل مشاهده می‌کنید:
using System;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace Core1RtmEmptyTest.StartupCustomizations
{    public static class SessionExts    {        public static void SetDateTime(this ISession collection, string key, DateTime value)        {            collection.Set(key, BitConverter.GetBytes(value.Ticks));        }        public static DateTime? GetDateTime(this ISession collection, string key)        {            var data = collection.Get(key);            if (data == null)            {                return null;            }            var dateInt = BitConverter.ToInt64(data, 0);            return new DateTime(dateInt);        }        public static void SetObject(this ISession session, string key, object value)        {            var stringValue = JsonConvert.SerializeObject(value);            session.SetString(key, stringValue);        }        public static T GetObject<T>(this ISession session, string key)        {            var stringValue = session.GetString(key);            return JsonConvert.DeserializeObject<T>(stringValue);        }    }
}
در اینجا با استفاده از کلاس BitConverter و امکان سریالایز مقادیر توسط آن به آرایه‌ای از بایت‌ها، امکان کار با متد‌های عمومی Set و Get را یافته‌ایم.
و یا جهت کار با اشیاء پیچیده‌تر می‌توان از کتابخانه‌ی JSON.NET استفاده کرد. به عبارتی در این نگارش از ASP.NET، کار سریالایز و دی‌سریالایز اشیاء، به برنامه نویس واگذار شده‌است و اینکه در پشت صحنه از چه کتابخانه‌ای می‌خواهید استفاده کنید، در اختیار خودتان است.
البته باید دقت داشت که در اینجا وابستگی JSON.NET به صورت خودکار در دسترس است. از این جهت که بسیاری از وابستگی‌های ASP.NET Core مانند مورد ذیل، به JSON.NET وابسته‌اند و نصب آن‌ها به معنای نصب خودکار JSON.NET نیز هست:
{
    "dependencies": {
     //same as before
     "Microsoft.Extensions.Configuration.Json": "1.0.0"
   }
}
اگر لیست بسته‌های وابسته‌ی به JSON.NET را می‌خواهید مشاهده کنید، فایل project.lock.json را گشوده و در آن Newtonsoft.Json را جستجو کنید.


یک مطلب تکمیلی

در اینجا نیز امکان ذخیره سازی سشن‌ها در بانک اطلاعاتی بجای حافظه‌ی فرار سرور درنظر گرفته شده‌است و برای این حالت، بانک‌های اطلاعاتی NoSQL ویژه‌ای به نام key/value stores مانند بانک اطلاعاتی فوق سریع Redis پیشنهاد می‌شود؛ هرچند امکان کار با SQL Server نیز در اینجا وجود دارد، اما برای کش سرورهای مبتنی بر key/value ها، بانک اطلاعاتی Redis، انتخاب اول است.
Managing Application State

‫ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging

$
0
0
ASP.NET Core به همراه یک فریم ورک توکار ثبت وقایع (Logging) ارائه شده‌ی توسط تزریق وابستگی‌ها است که به صورت پیش فرض نیز فعال است.


این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها» مشاهده کرده‌اید. در اینجا لیست سرویس‌هایی را مشاهده می‌کنید که به صورت پیش فرض، ثبت شده‌اند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آن‌ها هستند. بنابراین نیازی به فعال سازی آن‌ها نیست؛ اما برای استفاده‌ی از آن‌ها نیاز به انجام یک سری تنظیمات است.


پیاده سازی ثبت وقایع در ASP.NET Core

اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env)
{
   loggerFactory.AddConsole(Configuration.GetSection("Logging"));
   loggerFactory.AddDebug();
متد Configure امضای مشخصی را ندارد و در اینجا به هر تعداد سرویسی که نیاز باشد، می‌توان اینترفیس‌های آن‌ها را جهت تزریق وابستگی‌های متناظر توسط IoC Containser توکار ASP.NET Core، معرفی کرد. در اینجا برای تنظیم ویژگی‌های سرویس ثبت وقایع، تزریق وابستگی ILoggerFactory  صورت گرفته‌است.
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه می‌خواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» بیشتر بحث شد) و لاگ کردن ویژه‌ی در کنسول NET Core. را فعال می‌کند:
{    "Logging": {        "IncludeScopes": false,        "LogLevel": {            "Default": "Debug",            "System": "Information",            "Microsoft": "Information"        }    }
}
در مورد Log Level و یا سطوح ثبت وقایع، در ادامه‌ی مطلب بحث خواهد شد.

و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو می‌شود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگی‌های ذیل در فایل project.json برنامه را دارد:
{    "dependencies": {
        //same as before         "Microsoft.Extensions.Logging": "1.0.0",        "Microsoft.Extensions.Logging.Console": "1.0.0",        "Microsoft.Extensions.Logging.Debug": "1.0.0"     }
}
پس از این تنظیمات، برنامه را اجرا کنید.


در اینجا می‌توانید ریز وقایعی را که توسط ASP.NET Core لاگ شده‌است، مشاهده کنید. برای مثال چه درخواستی صورت گرفته‌است و چقدر اجرای آن زمان‌برده‌است.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر می‌خواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشه‌ی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:


دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شده‌است که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز می‌توان حالت‌های مختلف اطلاعات لاگ شده را مشاهده کرد و تمام این‌ها مرتبط هستند به ذکر متد AddConsole .


کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگی‌ها

برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمت‌های مختلف برنامه، می‌توان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")]
public class AboutController : Controller
{    private readonly ILogger<AboutController> _logger;    public AboutController(ILogger<AboutController> logger)    {        _logger = logger;    }    [Route("")]    public ActionResult Hello()    {        _logger.LogInformation("Running Hello");        return Content("Hello from DNT!");    }
در این کنترلر، وابستگی اینترفیس ILogger با پارامتری از نوع کنترلر جاری به سازنده‌ی کلاس تزریق شده‌است. علت ذکر این پارامتر جنریک این است که ILoggerFactory بداند چگونه باید متد CreateLogger خود را در پشت صحنه وهله سازی کند.
سپس با توجه به اینکه این سرویس جزو سرویس‌های از پیش ثبت شده‌ی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شده‌است و خروجی عبارت لاگ شده‌ی آن‌را در اینجا می‌توانید مشاهده کنید:



سطوح مختلف ثبت وقایع

اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آن‌ها به شرح زیر هستند:
Debug (1): ثبت واقعه‌ای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز می‌باشد. بنابراین نباید در حالت ارائه‌ی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعه‌ی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمت‌های مختلف برنامه مورد استفاده قرار می‌گیرند.
(4) Warning: جهت ثبت واقعه‌ای نامطلوب در سیستم بکار می‌رود و سبب قطع اجرای برنامه نمی‌شود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شده‌اند را ثبت می‌کند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شده‌اند.

برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو می‌شود، می‌توان حداقل سطح ثبت وقایع را نیز ذکر کرد:
 loggerFactory.AddDebug(minLevel: LogLevel.Information);
این حداقل مرتبط است با اعدادی که در کنار سطوح فوق ملاحظه می‌کنید. برای مثال اگر حداقل سطح ثبت وقایع به Information تنظیم شود، چون سطح آن 3 است، دیگر سطوح پایین‌تر از آن لاگ نخواهند شد. اهمیت این مساله در اینجا است که اگر صرفا نیاز به اطلاعات Critical داشتیم، نیازی نیست تا با انبوهی از اطلاعات لاگ شده سر و کار داشته باشیم و به این ترتیب می‌توان حجم اطلاعات نمایش داده شده را کاهش داد.

البته ترتیب واقعی این سطوح را در enum مرتبط با آن‌ها بهتر می‌توان مشاهده کرد:
  public enum LogLevel
  {
    Trace,
    Debug,
    Information,
    Warning,
    Error,
    Critical,
    None,
  }

یک نکته:زمانیکه متد AddDebug را بدون پارامتر فراخوانی می‌کنید، حداقل سطح ثبت وقایع آن به Information تنظیم شده‌است. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایین‌تر است از Information).  بنابراین اگر می‌خواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آن‌را به LogLevel.Debug تنظیم نمائید.


امکان استفاده‌ی از پروایدرهای ثبت وقایع ثالث

تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمت‌های مختلف و یا در بانک اطلاعاتی وجود دارد، می‌توان به تامین کننده‌های ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library

‫ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API

$
0
0
در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی

و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آن‌ها را بررسی خواهیم کرد.


تعریف مسیریابی کلی کنترلر

در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، می‌توان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواست‌های آن‌را محدود به درخواست‌هایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{    private readonly ILogger<TestController> _logger;    public TestController(ILogger<TestController> logger)    {        _logger = logger;    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن می‌توان برای ثبت استثناءهای رخ داده استفاده کرد.


یک کنترلر ، اما با قابلیت‌های متعدد

همانطور که ملاحظه می‌کنید، اینبار کلاس پایه‌ی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شده‌است و نه Api Controller سابق. تمام قابلیت‌های موجود در این‌دو توسط همان Controller ارائه می‌شوند.


هنوز پیش فرض‌های سابق Web API برقرار هستند

در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواست‌های Get ختم شده‌ی به نام کنترلر پاسخ می‌دهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا می‌شود.
- در اینجا می‌توان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف  <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، می‌توانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده می‌شود.
- تمام مسیریابی‌ها را توسط ویژگی Route و یا نوع‌های درخواستی مانند HttpGet، می‌توان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگی‌های مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{    private readonly ILogger<TestController> _logger;    public TestController(ILogger<TestController> logger)    {        _logger = logger;    }    [HttpGet]    public IEnumerable<string> Get() // http://localhost:7742/api/test    {        return new [] { "value1", "value2" };    }    [HttpGet("{page:int}")]    public IActionResult Get(int page) // http://localhost:7742/api/test/1    {        return Json(new[] { "value3", "value4" });    }    [HttpGet("/api/path1")]    public IActionResult GetEpisodes1() // http://localhost:7742/api/path1    {        return Json(new[] { "value5", "value6" });    }    [HttpGet("/api/path2")]    public IActionResult GetEpisodes2() // http://localhost:7742/api/path2    {        try        {            // get data from the DB ...            return Ok(new[] { "value7", "value8" });        }        catch (Exception ex)        {            _logger.LogError("Failed to get data from the API", ex);            return BadRequest();        }    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
// GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
// POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
// PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
// DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته:اگر می‌خواهید خروجی Web API شما همواره JSON باشد، می‌توانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
 [Produces("application/json")]
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller


تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API

فرض کنید مدل زیر را به برنامه اضافه کرده‌اید:
namespace Core1RtmEmptyTest.Models
{    public class Person    {        public string FirstName { get; set; }        public string LastName { get; set; }        public int Age { get; set; }    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
using Core1RtmEmptyTest.Models;
using Microsoft.AspNetCore.Mvc;
namespace Core1RtmEmptyTest.Controllers
{    public class PersonController : Controller    {        public IActionResult Index()        {            return View();        }        [HttpPost]        public IActionResult Index(Person person)        {            return Json(person);        }    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@section scripts
{    <script type="text/javascript">        $(function () {            $.ajax({                type: 'POST',                url: '/Person/Index',                dataType: 'json',                contentType: 'application/json; charset=utf-8',                data: JSON.stringify({                    FirstName: 'F1',                    LastName: 'L1',                    Age: 23                }),                success: function (result) {                    console.log('Data received: ');                    console.log(result);                }            });        });    </script>
}


همانطور که مشاهده می‌کنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکرده‌است و مقادیر دریافتی نال هستند.
این مورد یکی از مهم‌ترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارش‌های قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمی‌شوند و باید به نحو ذیل، محل دریافت و تفسیر آن‌ها را دقیقا مشخص کرد:
 public IActionResult Index([FromBody]Person person)
زمانیکه ویژگی FromBody را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء Person نگاشت خواهند شد.


نکته‌ی مهم:حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.


حالت‌های دیگر تغییرات Model Binding در ASP.NET Core

تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامه‌ی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیده‌ی سمت سرور bind نمی‌شود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال:اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرم‌ها به سرور، همواره شامل دو تغییر ذیل است:
  var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
 var data = $('form').serialize();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [HttpPost]
public IActionResult Index([FromForm]Person person)
حالت‌های دیگر ممکن را در تصویر ذیل ملاحظه می‌کنید:


علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارش‌های قبلی، تمام مکان‌ها و حالت‌های میسر جستجو می‌شوند و اگر یکی از آن‌ها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان می‌رسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا می‌کند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین می‌شوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالت‌ها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آن‌ها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل می‌کند:
در این حالت می‌توان در سازنده‌ی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel
{
    public ProductModel(IProductService prodService)
    {
        Value = prodService.Get(productId);
    }
    public IProduct Value { get; private set; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Containerخود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API

در اینجا حالت ارائه‌ی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوه‌ی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده می‌کنید:
public void ConfigureServices(IServiceCollection services)
{    services.AddMvc(options =>    {        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml"));     }).AddJsonOptions(options =>    {        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.

‫ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی

$
0
0
هدف از زیر ساخت بومی سازی در ASP.NET Core، حذف عبارات و رشته‌های درج شده‌ی در کلاس‌ها و ویووهای مختلف برنامه و انتقال آن‌ها به فایل‌های منبع resx است و سپس استفاده‌ی از آن‌ها توسط تزریق وابستگی‌ها. به این ترتیب می‌توان بر اساس نوع فرهنگ درخواستی کاربر جاری، رشته‌های درج شده را به صورت پویا، در زمان اجرای برنامه، بر اساس ترجمه‌های آن‌ها به کاربر نمایش داد.


نحوه‌ی تعیین فرهنگ ترد جاری در ASP.NET Core

در نگارش‌های پیشین ASP.NET، برای تعیین فرهنگ ترد جاری، از یکی از دو روش ذیل استفاده می‌شود:
الف) افزودن مدخل بومی سازی به فایل web.config
<system.web><globalization uiCulture="fa-IR" culture="fa-IR" /></system.web>
ب) و یا تعیین فرهنگ ترد با کدنویسی مستقیم در فایل global.asax
protected void Application_BeginRequest()
{
   Thread.CurrentThread.CurrentCulture = new CultureInfo("fa-IR");
   Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
}
در ASP.NET Core با حذف شدن System.Web و همچنین فایل global.asax، برای تعیین فرهنگ پیش فرض ترد جاری، به همراه فرهنگ‌هایی که برنامه از آن‌ها پشتیبانی می‌کند، به صورت ذیل عمل می‌شود:
public void Configure(IApplicationBuilder app)
{    app.UseRequestLocalization(new RequestLocalizationOptions    {        DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),        SupportedCultures = new[]        {            new CultureInfo("en-US"),            new CultureInfo("fa-IR")        },        SupportedUICultures = new[]        {            new CultureInfo("en-US"),            new CultureInfo("fa-IR")        }    });
در اینجا با مراجعه به کلاس آغازین برنامه و افزودن تنظیمات میان افزار RequestLocalization، می‌توان فرهنگ پیش فرض درخواست جاری و یا فرهنگ‌های پشتیبانی شده را مشخص کرد.
- تنظیمات SupportedCultures بر روی نمایش تاریخ، ساعت و واحد پولی تاثیر دارند. همچنین می‌توانند بر روی نحوه‌ی مقایسه‌ی حروف و مرتب سازی آن‌ها تاثیر داشته باشند.
- تنظیمات SupportedUICultures مشخص می‌کنند که کدامیک از فایل‌های resx برنامه که مداخل ترجمه‌های آن‌را به زبان‌های مختلف مشخص می‌کنند، باید بارگذاری شوند.
- تنظیم DefaultRequestCulture در صورت مشخص نشدن فرهنگ ترد جاری مورد استفاده قرار می‌گیرد.

یک مثال:هر ترد در دات نت دارای اشیاء CurrentCulture و CurrentUICulture است. اگر فرهنگ ترد جاری به en-US تنظیم شده باشد، متد DateTime.Now.ToLongDateString، خروجی نمونه Thursday, February 18, 2016 را نمایش می‌دهد.


زمانیکه میان افزار RequestLocalization فعال می‌شود، سه تامین کننده‌ی پیش فرض (مقدار‌های پیش فرض خاصیت RequestCultureProviders شیء RequestLocalizationOptions فوق)، جهت مشخص ساختن فرهنگ ترد جاری بکار گرفته خواهند شد:
الف) از طریق کوئری استرینگ با فعال سازی QueryStringRequestCultureProvider
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
http://localhost:5000/?culture=es-MX
برای مثال در اینجا QueryStringRequestCultureProvider به دنبال کوئری استرینگ‌های culture و یا ui-culture گشته و با رسیدن به es-MX، فرهنگ جاری را به اسپانیایی مکزیکی تنظیم می‌کند. در این حالت اگر فقط culture ذکر شود، ui-culture نیز به همان مقدار تنظیم خواهد شد.
ب) از طریق نام کوکی با فعال سازی CookieRequestCultureProvider
CookieRequestCultureProvider کوکی ویژه‌ای را با نام پیش فرض AspNetCore.Culture. ایجاد می‌کند. این کوکی برای ردیابی اطلاعات بومی سازی انتخابی کاربر بکار می‌رود. برای مثال اگر به مقدار ذیل تنظیم شود:
 c='en-UK'|uic='en-US'
c آن به معنای culture و uic آن به معنای ui-culture خواهد بود.
ج) از طریق هدر مخصوص Accept-Language با فعال سازی AcceptLanguageHeaderRequestCultureProvider که می‌تواند به همراه درخواست HTTP ارسال شود.

اگر تمام این حالت‌ها تنظیم نشده بودند، آنگاه از مقدارDefaultRequestCulture  استفاده می‌شود. برای مثال اگر مرورگربه صورت پیش فرض هدر Accept-Language را en-US ارسال می‌کند :


دیگر کار به پردازش مقدارDefaultRequestCulture  نخواهد رسید.

اکنون اگر علاقمند بودید تا به کاربر امکان انتخاب زبانی را بدهید، یک چنین اکشن متدی را طراحی کنید:
public IActionResult SetFaLanguage()
{    Response.Cookies.Append(        CookieRequestCultureProvider.DefaultCookieName,        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }    );    return RedirectToAction("GetTitle");
}
این اکشن متد بر اساس تامین کننده‌ی کوکی ردیابی زبان انتخاب شده‌ی توسط کاربر و یا CookieRequestCultureProvider کار می‌کند و توسط آن، فرهنگ جاری برنامه به زبان فارسی تنظیم می‌شود. هرگاه که این اکشن متد فراخوانی شود، کوکی AspNetCore.Culture. به مقدار c=fa-IR|uic=fa-IR تنظیم می‌شود:


از اینجا به بعد است که اگر نام کنترلر شما TestLocalController باشد، فایل منبع متناظر با آن یعنی Controllers.TestLocalController.fa.resx، به صورت خودکار بارگذاری و پردازش خواهد شد. در غیر اینصورت فایل نمونه‌ی ختم شده‌ی به en.resx پردازش می‌شود؛ چون این زبان به صورت پیش فرض در هدر Accept-Language قید شده‌است.


آماده سازی برنامه برای کار با فایل‌های منبع زبان‌های مختلف

ابتدا پوشه‌ی جدیدی را به نام Resources به ریشه‌ی پروژه اضافه کنید. سپس به کلاس آغازین برنامه مراجعه کرده و محل یافت شدن این پوشه را معرفی کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => options.ResourcesPath = "Resources");
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();
در اینجا سرویس جدید Localization، به لیست سرویس‌های ثبت شده‌ی در IoC Container اضافه می‌شود. همچنین توسط خاصیت  ResourcesPath  آن مشخص شده‌است که فایل‌های resx را باید از کجا دریافت کند.
به علاوه به سرویس ASP.NET MVC، تنظیمات بومی سازی Viewها و DataAnnotations نیز اضافه شده‌اند. تنظیم suffix به معنای  view file suffix و یا مثلا fr در نام فایل Index.fr.cshtml است.


نحوه‌ی تعریف و پوشه بندی فایل‌های منبع زبان‌های مختلف

تا اینجا پوشه‌ی جدید Resources را به پروژه اضافه، معرفی و سرویس‌های مرتبط را نیز فعال کردیم. پس از آن نوبت به افزودن فایل‌های resx است. برای این منظور بر روی پوشه‌ی منابع کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید.


در اینجا با جستجوی resource، می‌توان فایل resx جدیدی را به پروژه اضافه کرد؛ اما ... انتخاب نام آن باید بر اساس نکات ذیل باشد:
الف) برای کنترلرها یکی از دو مسیر / دار و یا نقطه دار جستجو می‌شوند:
Resources/Controllers.HomeController.fr.resx
Resources/Controllers/HomeController.fr.resx

در اینجا fr ذکر شده، همان LanguageViewLocationExpanderFormat.Suffix است که پیشتر بحث شد. قسمت ابتدایی Controllers همیشه ثابت است (یا به صورت نام یک پوشه و یا به عنوان قسمت اول نام فایل). سپس نام کلاس کنترلر به همراه نام فرهنگ مدنظر باید ذکر شوند. قسمت نام پوشه‌ی Resources را نیز به services.AddLocalization معرفی کرده‌ایم.

ب) برای Viewها نیز همان حالت‌های / دار و یا نقطه دار بررسی می‌شوند:
Resources/Views.Home.About.fr.resx
Resources/Views/Home/About.fr.resx


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

در این نگارش از ASP.NET، در حالت کلی، نام یک فایل منبع، همان نام کامل کلاس آن است؛ منهای فضای نام آن (اگر این فایل منبع در همان اسمبلی قرار گیرد). برای مثال اگر می‌خواهید برای کلاس Startup برنامه، فایل منبعی را درست کنید و نام کامل آن با درنظر گرفتن فضای نام، معادل LocalizationWebsite.Web.Startup است، ابتدای فضای نام آن‌را حذف کنید و سپس آن‌را ختم به fa.resx کنید؛ مثلا Startup.fa.resx
اگر محل واقع شدن فایل‌های resx در همان اسمبلی اصلی پروژه باشند، نیازی به ذکر فضای نام پیش فرض پروژه نیست. برای مثال اگر فضای نام پیش فرض پروژه‌ی وب جاری MyLocalizationWebsite.Web است، بجای نام فایل MyLocalizationWebsite.Web.Controllers.HomeController.fr.resx می‌توانید به صورت خلاصه بنویسید Controllers.HomeController.fr.resx. در غیراینصورت (استفاده از اسمبلی‌های دیگر)، ذکر کامل فضای نام مرتبط هم الزامی است.


چند نکته:
- اگر ResourcesPath را در services.AddLocalization معرفی نکنید، مسیر پیش فرض یافتن فایل‌های resx مربوط به کنترلرها، پوشه‌ی ریشه‌ی پروژه است و برای Viewها، همان پوشه‌ی محل واقع شدن View متناظر خواهد بود.
- اینکه کدام فایل منبع در برنامه بارگذاری می‌شود، دقیقا مرتبط است با فرهنگ ترد جاری و این فرهنگ به صورت پیش فرض en-US است (چون همواره در هدر Accept-Language ارسالی توسط مرورگر وجود دارد). برای تغییر آن، از نکته‌ی اکشن متد public IActionResult SetFaLanguage ابتدای بحث استفاده کنید (در غیراینصورت در آزمایشات خود شاهد بارگذاری فایل‌های منبع دیگری بجز en.resx‌ها نخواهید بود).
- فایل‌های منبع را به صورت کامپایل شده در پوشه‌ی bin برنامه خواهید یافت:



خواندن اطلاعات منابع در کنترلرهای برنامه

فرض کنید کنترلری را به نام TestLocalController ایجاد کرده‌ایم. بنابراین فایل منبع فارسی متناظر با آن Controllers.TestLocalController.fa.resx خواهد بود؛ با این محتوای نمونه:


محتوای این کنترلر نیز به صورت ذیل است:
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;
namespace Core1RtmEmptyTest.Controllers
{    public class TestLocalController : Controller    {        private readonly IStringLocalizer<TestLocalController> _stringLocalizer;        private readonly IHtmlLocalizer<TestLocalController> _htmlLocalizer;        public TestLocalController(            IStringLocalizer<TestLocalController> stringLocalizer,            IHtmlLocalizer<TestLocalController> htmlLocalizer)        {            _stringLocalizer = stringLocalizer;            _htmlLocalizer = htmlLocalizer;        }        public IActionResult Index()        {            var name = "DNT";            var message = _htmlLocalizer["<b>Hello</b><i> {0}</i>", name];            ViewData["Message"] = message;            return View();        }        [HttpGet]        public string GetTitle()        {            var about = _stringLocalizer["About Title"];            return about;        }        public IActionResult SetFaLanguage()        {            Response.Cookies.Append(                CookieRequestCultureProvider.DefaultCookieName,                CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),                new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }            );            return RedirectToAction("GetTitle");        }    }
}
در اینجا نحوه‌ی دسترسی به فایل‌های منبع را در کنترلرها مشاهده می‌کنید. سرویس IStringLocalizer برای خواندن key/valueهای معمولی طراحی شده‌است و سرویس IHtmlLocalizer برای خواندن key/valueهای تگ دار، بکار می‌رود. علت تنظیم شدن پارامتر جنریک آن‌ها به نام کنترلر جاری این است که این سرویس‌ها بدانند دقیقا چه نوعی را قرار است بارگذاری کنند و دقیقا باید به دنبال کدام فایل بگردند. این سرویس‌ها یک کلید را می‌گیرند و یک خروجی و مقدار را باز می‌گردانند.
اگر برنامه را در حالت معمولی اجرا کنید و سپس آدرس http://localhost:7742/testlocal/gettitle را درخواست کنید، عبارت About Title را مشاهده می‌کنید؛ به دو علت:
الف) هنوز فرهنگ پیش فرض ترد جاری همان en-US است که توسط مرورگر ارسال شده‌است.
ب) چون فایل resx متناظر با فرهنگ پیش فرض ترد جاری یافت نشده‌است، مقدار همان کلید درخواستی بازگشت داده می‌شود؛ یعنی همان About Title.

برای رفع این مشکل آدرس http://localhost:7742/testlocal/SetFaLanguage را درخواست کنید. به این صورت با تنظیم کوکی ردیابی فرهنگ ترد جاری به زبان فارسی، خروجی GetTile این‌بار «درباره» خواهد بود.


خواندن اطلاعات منابع در Viewهای برنامه

فرض کنید فایل Views.TestLocal.Index.fa.resx (فایل منبع کنترلر TestLocal و ویوو Index آن به زبان فارسی) دقیقا همان محتوای فایل Controllers.TestLocalController.fa.resx فوق را دارد (اگر نام پوشه‌ی Views را تغییر داده‌اید، قسمت ابتدایی نام فایل Views را هم باید تغییر دهید). برای دسترسی به اطلاعات آن در یک ویوو، می‌توان از سرویس IViewLocalizer  به نحو ذیل استفاده کرد:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
}
Message @ViewData["Message"]<br/>
@Localizer["<b>Hello</b><i> {0}</i>", "DNT"]<br/>
@Localizer["About Title"]
در اینجا ViewData، از همان اطلاعات اکشن متد Index استفاده می‌کند.
Localizer از طریق تزریق سرویس IViewLocalizer  به View برنامه تامین می‌شود. این سرویس در پشت صحنه از همان IHtmlLocalizer استفاده می‌کند و در حین استفاده‌ی از آن، اطلاعات تگ‌ها انکد (encoded) نخواهند شد (به همین جهت برای کار با کلیدها و مقادیر تگ‌دار توصیه می‌شود).


استفاده از اطلاعات منابع در DataAnnotations

قسمت اول فعال سازی بومی سازی DataAnnotations با ذکر AddDataAnnotationsLocalization در متد ConfigureServices، در ابتدای بحث انجام شد و همانطور که پیشتر نیز عنوان گردید، در این نگارش از ASP.NET، برای تمام کلاس‌های برنامه می‌توان فایل منبع ایجاد کرد. برای مثال اگر کلاس RegisterViewModel در فضای نام ViewModels.Account قرار گرفته‌است، نام فایل منبع آن یکی از دو حالت / دار و یا نقطه دار ذیل می‌تواند باشد:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx

محتوای این کلاس را در ذیل مشاهده می‌کنید:
using System.ComponentModel.DataAnnotations;
namespace Core1RtmEmptyTest.ViewModels.Account
{    public class RegisterViewModel    {        [Required(ErrorMessage = "EmailReq")]        [EmailAddress(ErrorMessage = "EmailType")]        [Display(Name = "Email")]        public string Email { get; set; }    }
}
در این حالت مقداری که برای ErrorMessage ذکر می‌شود، کلیدی است که باید در فایل منبع جستجو شود:


یک نکته:هیچ الزامی ندارد که کلیدها را به این شکل وارد کنید. از این جهت که اگر این کلید در فایل منبع یافت نشد و یا فرهنگ ترد جاری با فایل‌های منبع مهیا تطابقی نداشت، عبارتی را که کاربر مشاهده می‌کند، دقیقا معادل «EmailReq» خواهد بود. بنابراین در اینجا می‌توانید کلید را به صورت کامل، مثلا مساوی «The Email field is required» وارد کنید و همین عبارت را به عنوان کلید در فایل منبع ذکر کرده و مقدار آن‌را مساوی ترجمه‌ی آن قرار دهید. این نکته در تمام حالات کار با کنترلرها و ویووها نیز صادق است.


استفاده از یک منبع اشتراکی

اگر می‌خواهید تعدادی از منابع را در همه‌جا در اختیار داشته باشید، روش کار به این صورت است:
الف) یک کلاس خالی را به نام SharedResource دقیقا با فرمت ذیل در پوشه‌ی Resources ایجاد کنید:
// Dummy class to group shared resources
namespace Core1RtmEmptyTest
{
   public class SharedResource
   {
   }
}
ب) اکنون فایل‌های منبع خود را در پوشه‌ی Resources، دقیقا با این نام‌های خاص ایجاد کنید:
SharedResource.fa.resx
SharedResource.en-US.resx
و امثال آن

ج) برای استفاده‌ی از این منبع اشتراکی در کلاس‌های مختلف برنامه تنها کافی است در حین تزریق وابستگی‌ها، نوع آرگومان جنریک IStringLocalizer را به SharedResource تنظیم کنید:
 IStringLocalizer<SharedResource> sharedLocalizer
و یا حتی در ویووهای برنامه نیز می‌توان از آن استفاده کرد:
 @inject IHtmlLocalizer<SharedResource> SharedLocalizer

‫ارتقاء به ASP.NET Core 1.0 - قسمت 20 - بررسی تغییرات فیلترها

$
0
0
پیشنیازها

- فیلترها در MVC
- ASP.NET MVC #15


فیلترها در ASP.NET MVC، امکان اجرای کدهایی را پیش و یا پس از مرحله‌ی خاصی از طول اجرای pipeline آن فراهم می‌کنند. کلیات فیلترها در ASP.NET Core با نگارش‌های قبلی ASP.NET MVC (پیشنیازهای فوق) تفاوت چندانی را ندارد و بیشتر تغییراتی مانند نحوه‌ی معرفی سراسری آن‌ها، اکشن فیلترهای Async و یا تزریق وابستگی‌ها در آن‌ها، جدید هستند.


امکان تعریف فیلترهای Async در ASP.NET Core

حالت کلی تعریف یک فیلتر در ASP.NET MVC که در ASP.NET Core نیز همچنان معتبر است، پیاده سازی اینترفیس کلی IActionFilter می‌باشد که توسط آن می‌توان به مراحل پیش و پس از اجرای قطعه‌ای از کدهای برنامه دسترسی پیدا کرد:
namespace FiltersSample.Filters
{
    public class SampleActionFilter : IActionFilter
    {
        public void OnActionExecuting(ActionExecutingContext context)
        {
            // انجام کاری پیش از اجرای اکشن متد
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
            // انجام کاری پس از اجرای اکشن متد
        }
    }
}
در اینجا اینترفیس IAsyncActionFilter نیز معرفی شده‌است که توسط آن می‌توان فراخوانی‌های غیرهمزمان و async را نیز مدیریت کرد:
namespace FiltersSample.Filters
{
    public class SampleAsyncActionFilter : IAsyncActionFilter
    {
        public async Task OnActionExecutionAsync(
            ActionExecutingContext context,
            ActionExecutionDelegate next)
        {
            // انجام کاری پیش از اجرای اکشن متد
            await next();
            // انجام کاری پس از اجرای اکشن متد
        }
    }
}
به کامنت‌های نوشته شده‌ی در بدنه‌ی متد OnActionExecutionAsync دقت کنید. در اینجا کدهای پیش از await next معادل OnActionExecuting و کدهای پس از await next معادل OnActionExecuted حالت همزمان و یا همان حالت متداول هستند. بنابراین جایی که اکشن متد اجرا می‌شود، همان await next است.

یک نکته:توصیه شده‌است که تنها یکی از حالت‌های همزمان و یا غیرهمزمان را پیاده سازی کنید و نه هر دوی آن‌ها را. اگر هر دوی این‌ها را در طی یک کلاس پیاده سازی کنید (تک کلاسی که هر دوی اینترفیس‌های IActionFilter و IAsyncActionFilter را با هم پیاده سازی می‌کند)، تنها نگارش Async آن توسط ASP.NET Core فراخوانی و استفاده خواهد شد. همچنین مهم نیست که اکشن متد شما Async هست یا خیر؛ برای هر دو حالت می‌توان از فیلترهای async نیز استفاده کرد.


ساده سازی تعریف فیلترها

اگر مدتی با ASP.NET MVC کار کرده باشید، می‌دانید که عموما کسی از این اینترفیس‌های کلی برای پیاده سازی فیلترها استفاده نمی‌کند. روش کار با ارث بری از یکی از فیلترهای از پیش تعریف شده‌ی ASP.NET MVC صورت می‌گیرد؛ از این جهت که این فیلترها که در اصل همین اینترفیس‌ها را پیاده سازی کرده‌اند، یک سری جزئیات توکار protected را نیز به همراه دارند که با ارث بری از آن‌ها می‌توان به امکانات بیشتری دسترسی پیدا کرد و کدهای ساده‌تر و کم حجم‌تری را تولید نمود:
ActionFilterAttribute
ExceptionFilterAttribute
ResultFilterAttribute
FormatFilterAttribute
ServiceFilterAttribute
TypeFilterAttribute

برای مثال در اینجا فیلتری را مشاهده می‌کنید که با ارث بری از فیلتر توکار ResultFilterAttribute، سعی در تغییر Response برنامه و افزودن هدری به آن کرده‌است:
using Microsoft.AspNetCore.Mvc.Filters;
namespace FiltersSample.Filters
{
    public class AddHeaderAttribute : ResultFilterAttribute
    {
        private readonly string _name;
        private readonly string _value;
        public AddHeaderAttribute(string name, string value)
        {
            _name = name;
            _value = value;
        }

        public override void OnResultExecuting(ResultExecutingContext context)
        {
            context.HttpContext.Response.Headers.Add(
                _name, new string[] { _value });
            base.OnResultExecuting(context);
        }
    }
}
و برای استفاده‌ی از این فیلتر جدید خواهیم داشت:
[AddHeader("Author", "DNT")]
public class SampleController : Controller
{
    public IActionResult Index()
    {
        return Content("با فایرباگ هدر خروجی را بررسی کنید");
    }
}


نحوه‌ی تعریف میدان دید فیلترها

نحوه‌ی دید فیلترها در اینجا نیز همانند سابق، سه حالت را می‌تواند داشته باشد:
الف) اعمال شده‌ی به یک اکشن متد.
ب) اعمال شده‌ی به یک کنترلر که به تمام اکشن متدهای آن کنترلر اعمال خواهد شد.
ج) حالت تعریف سراسری و این مورد محل تعریف آن به کلاس آغازین برنامه منتقل شده‌است:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(SampleActionFilter)); // by type
        options.Filters.Add(new SampleGlobalActionFilter()); // an instance
    });
}
در اینجا دو روش معرفی فیلترهای سراسری را در متد ConfigureServices کلاس آغازین برنامه مشاهده می‌کنید:
الف) اگر توسط ارائه‌ی new ClassName معرفی شوند، یعنی وهله سازی را خودتان قرار است مدیریت کنید و در این حالت تزریق وابستگی‌هایی صورت نخواهند گرفت.
ب) اگر توسط typeof معرفی شوند، یعنی این وهله سازی توسط IoC Container توکار ASP.NET Core انجام خواهد شد و طول عمر آن Transient است. یعنی به ازای هربار نیاز به آن، یکبار وهله سازی خواهد شد.


ترتیب اجرای فیلترها

توسط خاصیت Order می‌توان ترتیب اجرای چندین فیلتر اجرا شده‌ی به یک اکشن متد را مشخص کرد. اگر این مقدار منفی وارد شود:
 [MyFilter(Name = "Method Level Attribute", Order=-1)]
این فیلتر پیش از فیلترهای سراسری و همچنین فیلترهای اعمال شده‌ی در سطح کلاس اجرا می‌شود.


تزریق وابستگی‌ها در فیلترها

فیلترهایی که به صورت ویژگی‌ها یا Attributes تعریف می‌شوند و قرار است به کنترلرها و یا اکشن متدها به صورت مستقیم اعمال شوند، نمی‌توانند دارای وابستگی‌های تزریق شده‌ی در سازنده‌ی خود باشند. این محدودیتی است که توسط زبان‌های برنامه نویسی اعمال می‌شود و نه ASP.NET Core. اگر ویژگی قرار است پارامتری در سازنده‌ی خود داشته باشد، هنگام تعریف و اعمال آن، این پارامترها باید مشخص بوده و تعریف شوند. به همین جهت آنچنان با تزریق وابستگی‌های از طریق سازنده‌ی کلاس قابل مدیریت نیستند. برای رفع این نقصیه، راه‌حل‌های متفاوتی در ASP.NET Core پیشنهاد و طراحی شده‌اند:
الف) استفاده‌ی از ServiceFilterAttribute
[ServiceFilter(typeof(AddHeaderFilterWithDi))]
public IActionResult Index()
{
   return View();
}
ویژگی جدید ServiceFilter، نوع کلاس فیلتر را دریافت می‌کند و سپس هر زمانیکه نیاز به اجرای این فیلتر خاص بود، کار وهله سازی‌های وابستگی‌های آن، در پشت صحنه توسط IoC Container توکار ASP.NET Core انجام خواهد شد.
همچنین باید دقت داشت که در این حالت ثبت کلاس فیلتر در متد ConfigureServices کلاس آغازین برنامه الزامی است.
 services.AddScoped<AddHeaderFilterWithDi>();
در غیراینصورت استثنای ذیل را دریافت خواهید کرد:
 System.InvalidOperationException: No service for type 'FiltersSample.Filters.AddHeaderFilterWithDI' has been registered.

ب) استفاده از TypeFilterAttribute
[TypeFilter(typeof(AddHeaderAttribute),  Arguments = new object[] { "Author", "DNT" })]
public IActionResult Hi(string name)
{
   return Content($"Hi {name}");
}
فیلتر و ویژگی TypeFilter بسیار شبیه است به عملکرد ServiceFilter، با این تفاوت که:
- نیازی نیست تا وابستگی آن‌را در متد ConfigureServices ثبت کرد (هرچند وابستگی‌های خود را از DI Container دریافت می‌کنند).
- امکان دریافت پارامترهای اضافی سازنده‌ی کلاس مدنظر را نیز دارند.


یک مثال تکمیلی: لاگ کردن تمام استثناءهای مدیریت نشده‌ی یک برنامه‌ی ASP.NET Core 1.0

می‌توان با سفارشی سازی فیلتر توکار ExceptionFilterAttribute، امکان ثبت وقایع را توسط فریم ورک توکار Loggingاضافه کرد:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
namespace Core1RtmEmptyTest.StartupCustomizations
{    public class CustomExceptionLoggingFilterAttribute : ExceptionFilterAttribute    {        private readonly ILogger<CustomExceptionLoggingFilterAttribute> _logger;        public CustomExceptionLoggingFilterAttribute(ILogger<CustomExceptionLoggingFilterAttribute> logger)        {            _logger = logger;        }        public override void OnException(ExceptionContext context)        {            _logger.LogInformation($"OnException: {context.Exception}");            base.OnException(context);        }    }
}
و برای ثبت سراسری آن در کلاس آغازین برنامه خواهیم داشت:
public void ConfigureServices(IServiceCollection services)
{    services.AddMvc(options =>    {        options.Filters.Add(typeof(CustomExceptionLoggingFilterAttribute));
در اینجا از typeof استفاده شده‌است تا کار تزریق وابستگی‌های این فیلتر به صورت خودکار انجام شود.
در ادامه با این فرض که پیشتر تنظیمات ثبت وقایع صورت گرفته‌است:
public void Configure(ILoggerFactory loggerFactory)
{
   loggerFactory.AddDebug(minLevel: LogLevel.Debug);
اکنون اگر یک چنین اکشن متدی فراخوانی شود:
public IActionResult GetData()
{
  throw new Exception("throwing an exception!");
}
در پنجره‌ی دیباگ ویژوال استودیو، این استثناء قابل مشاهده خواهد بود:

‫ارتقاء به ASP.NET Core 1.0 - قسمت 21 - بررسی تغییرات Bundling و Minification

$
0
0
زیرساخت یکی کردن و فشرده سازی اسکریپت‌ها و فایل‌های CSS نگارش پیشین ASP.NET MVC، به طور کامل از ASP.NET Core حذف شده‌است. در ابتدا (تا نگارش RC2)، روش استفاده‌ی از Gulp را توصیه کردند و در زمان ارائه‌ی نگارش RTM، توصیه‌ی رسمی آن‌ها به Bundler Minifierتغییر کرد (و دیگر Gulp را توصیه نمی‌کنند).


یکی کردن و فشرده سازی فایل‌های استاتیک در ASP.NET Core

هدف از یکی کردن و فشرده سازی فایل‌های استاتیک مانند اسکریپت‌ها و فایل‌های CSS، بهبود کارآیی برنامه با کاهش حجم نهایی ارائه‌ی آن و همچنین کاهش تعداد رفت و برگشت‌های به سرور برای دریافت فایل‌های متعدد مرتبط به آن است. در عملیات Bundling، چندین فایل، به یک تک فایل تبدیل می‌شوند تا اتصالات مرورگر به وب سرور، جهت دریافت آن‌ها به نحو چشمگیری کاهش پیدا کند و در عملیات Minification، مراحل متعددی بر روی کدهای نوشته شده صورت می‌گیرد تا حجم نهایی آن‌ها کاهش پیدا کنند. مایکروسافت در ASP.NET Core RTM، ابزاری را به نام BundlerMinifier.Core جهت برآورده کردن این اهداف ارائه کرده‌است. بنابراین اولین قدم، نصب وابستگی‌های آن است.
برای اینکار یک سطر ذیل  را به فایل project.json اضافه کنید. این بسته باید به قسمت tools اضافه شود تا قابلیت فراخوانی از طریق خط فرمان را نیز پیدا کند:
"tools": {
    "BundlerMinifier.Core": "2.1.258"
},
در غیر اینصورت (ذکر آن در قسمت dependencies) خطاهای ذیل را دریافت خواهید کرد:
No executable found matching command "dotnet-bundle"
Version for package `BundlerMinifier.Core` could not be resolved.


اسکریپت نویسی برای کار با BundlerMinifier.Core

روش‌های زیادی برای کار با ابزار BundlerMinifier.Core وجود دارند؛ منجمله انتخاب فایل‌ها در solution explorer و سپس کلیک راست بر روی فایل‌های انتخاب شده و انتخاب گزینه‌ی bundler & minifier برای یکی کردن و فشرده سازی خودکار این فایل‌ها. برای این منظور افزونه‌ی Bundler & Minifierرا نیاز است نصب کنید.
اما روشی که قابلیت خودکارسازی را دارد، استفاده از فایل ویژه‌ی bundleconfig.json این ابزار است. برای این منظور فایل جدید bundleconfig.json را به ریشه‌ی پروژه اضافه کرده و سپس محتوای ذیل را به آن اضافه کنید:
[    {        "outputFileName": "wwwroot/css/site.min.css",        "inputFiles": [            "wwwroot/css/site.css"        ]    },    {        "outputFileName": "wwwroot/js/site.min.js",        "inputFiles": [            "bower_components/jquery/dist/jquery.min.js",            "bower_components/jquery-validation/dist/jquery.validate.min.js",            "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"        ],        "minify": {            "enabled": true,            "renameLocals": true        },        "sourceMap": false    }
]
فرمت این فایل بسیار خوانا است. برای مثال در یک مدخل آن، در ذیل خاصیت inputFiles، لیست فایل‌های css ذکر می‌شوند و سپس در outputFileName، محل نهایی فایل تولیدی باید ذکر شود. این محل نیز باید از پیش وجود داشته باشد. یعنی باید پوشه‌های js و css را در پوشه‌ی عمومی wwwroot پیشتر ایجاد کرده باشید.
با ذخیره سازی این فایل، کار یکی سازی و فشرده کردن مداخل آن به صورت خودکار صورت خواهد گرفت.


خودکار سازی فرآیند یکی کردن و فشرده سازی فایل‌های استاتیک

برای خودکار سازی این فرآیند، می‌توان به صورت زیر عمل کرد. فایل project.json را گشوده و قسمت scripts آن‌را به نحو ذیل تغییر دهید:
"scripts": {    "precompile": [        "dotnet bundle"    ],    "prepublish": [        "bower install"    ],    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
}
روش دستی کار با ابزار BundlerMinifier، مراجعه به خط فرمان و صدور دستور dotnet bundle است (ابتدا از طریق خط فرمان به ریشه‌ی پروژه وارد شده و سپس این دستور را صادر کنید). برای خودکار سازی آن می‌توان این دستور را در قسمت scripts فایل project.json نیز ذکر کرد تا پیش از کامپایل برنامه، کار یکی کردن، فشرده سازی و همچنین کپی فایل نهایی به پوشه‌ی wwwroot برنامه به صورت خودکار انجام شود.

یک نکته:به منوی Build گزینه‌ی Update all bundles نیز با نصب افزونه‌ی Bundler & Minifier اضافه می‌شود. همچنین اگر از منوی Tools گزینه‌ی Task runner explorer را انتخاب کنید، فایل bundleconfig.json توسط آن شناسایی شده و گزینه‌ی update all files را نیز در اینجا مشاهده خواهید کرد.



ساده سازی تعاریف فایل Layout برنامه

در یک چنین حالتی دیگر نباید در فایل layout شما، ارجاعات مستقیمی به پوشه‌ی مثلا bower_components وجود داشته باشند و یا در کلاس آغازین برنامه، نیازی نیست تا این پوشه را عمومی کنید. لیست مداخلی را که نیاز دارید، به ترتیب از پوشه‌های مختلفی تهیه و در فایل bundleconfig.json ذکر کنید تا یکی شده و خروجی js/site.min.js را تشکیل دهند. این مورد تنها مدخلی است که نیاز است در فایل layout برنامه ذکر شود (بجای چندین و چند مدخل مورد نیاز):
<script src="~/js/site.min.js" asp-append-version="true" type="text/javascript"></script>
در مورد ویژگی asp-append-version نیز پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بحث شد و به آن مکانیزم cache busting می‌گویند. این ویژگی سبب خواهد شد تا یک کوئری استرینگ v=xyz? مانند، به انتهای آدرس اسکریپت یا فایل css یا هر فایل استاتیک دیگری اضافه شود. با تغییر محتوای این فایل، قسمت xyz به صورت خودکار تغییر خواهد کرد و به این ترتیب مرورگر همواره آخرین نگارش این فایل را دریافت می‌کند.

‫ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS

$
0
0
روش کار برنامه‌های ASP.NET Core در IIS کاملا متفاوت است با تمام نگارش‌های پیشین ASP.NET؛ از این جهت که برنامه‌های ASP.NET Core در اصل یک برنامه‌ی متکی به خود از نوع Console می‌باشند. به همین جهت برای هاست شدن نیازی به IIS ندارند. این نوع برنامه‌ها به همراه یک self-hosted Web server ارائه می‌شوند (به نام Kestrel) و این وب سرور توکار است که تمام درخواست‌های رسیده را دریافت و پردازش می‌کند. هرچند در اینجا می‌توان از IIS صرفا به عنوان یک «front end proxy» استفاده کرد؛ از این جهت که Kestrel تنها یک وب سرور خام است و تمام امکانات و افزونه‌های مختلف IIS را شامل نمی‌شود.
بر روی ماشین‌های ویندوزی و ویندوزهای سرور، استفاده‌ی از IIS به عنوان پروکسی درخواست‌ها و ارسال آن‌ها به Kestrel، روش توصیه شده‌است؛ از این جهت که حداقل قابلیت‌هایی مانند «port 80/443 forwarding»، مدیریت طول عمر برنامه، مدیریت مجوزهای SLL آن و خیلی از موارد دیگر توسط Kestrel پشتیبانی نمی‌شود.


معماری پردازش نگارش‌های پیشین ASP.NET در IIS


در نگارش‌های پیشین ASP.NET، همه چیز داخل پروسه‌‌ای به نام w3wp.exe و یا IIS Worker Process پردازش می‌شود که در اصل چیزی نیست بجز همان IIS Application Pool. این AppPoolها، برنامه‌های ASP.NET شما را هاست می‌کنند و همچنین سبب وهله سازی و اجرای آن‌ها نیز خواهند شد.
در اینجا درایور http.sys ویندوز، درخواست‌های رسیده را دریافت کرده و سپس آن‌ها را به سمت سایت‌هایی نگاشت شده‌ی به AppPoolهای مشخص، هدایت می‌کند.


معماری پردازش برنامه‌های ASP.NET Core در IIS

روش اجرای برنامه‌های ASP.NET Core با نگارش‌های پیشین آن‌ها کاملا متفاوت هستند؛ از این جهت که داخل پروسه‌ی w3wp.exe اجرا نمی‌شوند. این برنامه‌ها در یک پروسه‌ی مجزای کنسول خارج از پروسه‌ی w3wp.exe اجرا می‌شوند و حاوی وب سرور توکاری به نام کسترل (Kestrel) هستند.


 این وب سرور، وب سروری است تماما دات نتی و به شدت برای پردازش تعداد بالای درخواست‌ها بهینه سازی شده‌است؛ تا جایی که کارآیی آن در این یک مورد چند 10 برابر IIS است. هرچند این وب سرور فوق العاده سریع است، اما «تنها» یک وب سرور خام است و به همراه سرویس‌های مدیریت وب، مانند IIS نیست.


در تصویر فوق مفهوم «پروکسی» بودن IIS را در حین پردازش برنامه‌های ASP.NET Core بهتر می‌توان درک کرد. ابتدا درخواست‌های رسیده به IIS می‌رسند و سپس IIS آن‌ها را به طرف Kestrel هدایت می‌کند.
برنامه‌های ASP.NET Core، برنامه‌های کنسول متکی به خودی هستند که توسط دستور خط فرمان dotnet اجرا می‌شوند. این اجرا توسط ماژولی ویژه به نام AspNetCoreModule در IIS انجام می‌شود.


همانطور که در تصویر نیز مشخص است، AspNetCoreModule یک ماژول بومی IIS است و هنوز برای اجرا نیاز به IIS Application Pool دارد؛ با این تفاوت که در تنظیم AppPoolهای برنامه‌های ASP.NET Core، باید NET CLR Version. را به No managed code تنظیم کرد.


اینکار از این جهت صورت می‌گیرد که IIS در اینجا تنها نقش یک پروکسی هدایت درخواست‌ها را به پروسه‌ی برنامه‌ی حاوی وب سرور Kestrel، دارد و کار آن وهله سازی NET Runtime. نیست. کار AspNetCoreModule این است که با اولین درخواست رسیده‌ی به برنامه‌ی شما، آن‌را بارگذاری کند. سپس درخواست‌های رسیده را دریافت و به سمت برنامه‌ی ASP.NET Core شما هدایت می‌کند (به این عملیات reverse proxy هم می‌گویند).


اگر دقت کرده باشید، برنامه‌های ASP.NET Core، هنوز دارای فایل web.config ایی با محتوای ذیل هستند:
<?xml version="1.0" encoding="utf-8"?><configuration>  <system.webServer>    <handlers>      <add name="aspNetCore" path="*" verb="*"           modules="AspNetCoreModule" resourceType="Unspecified"/>    </handlers>    <aspNetCore processPath="%LAUNCHER_PATH%"                arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false"                stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>  </system.webServer></configuration>
توسط این تنظیمات است که AspNetCoreModule فایل‌های dll برنامه‌ی شما را یافته و سپس برنامه را به عنوان یک برنامه‌ی کنسول بارگذاری می‌کند (با توجه به اینکه حاوی کلاس Program و متد Main هستند).
یک نکته:در زمان publish برنامه، تنظیم و تبدیل مقادیر LAUNCHER_PATH و LAUNCHER_ARGS به معادل‌های اصلی آن‌ها صورت می‌گیرد (در ادامه مطلب بحث خواهد شد).


آیا واقعا هنوز نیازی به استفاده‌ی از IIS وجود دارد؟

هرچند می‌توان Kestrel را توسط یک IP و پورت مشخص، عمومی کرد و استفاده نمود، اما حداقل در ویندوز چنین توصیه‌ای نمی‌شود و بهتر است از IIS به عنوان یک front end proxy استفاده کرد؛ به این دلایل:
- اگر می‌خواهید چندین برنامه را بر روی یک وب سرور که از طریق پورت‌های 80 و 443 ارائه می‌شوند داشته باشید، نمی‌توانید از Kestrel  به صورت مستقیم استفاده کنید؛ زیرا از مفهوم host header routing که قابلیت ارائه‌ی چندین برنامه را از طریق پورت 80 و توسط یک IP میسر می‌کند، پشتیبانی نمی‌کند. برای اینکار نیاز به IIS و یا در حقیقت درایور http.sys ویندوز است.
- IIS خدمات قابل توجهی را به برنامه‌ی شما ارائه می‌کند. برای مثال با اولین درخواست رسیده، به صورت خودکار آن‌را اجرا و بارگذاری می‌کند؛ به همراه تمام مدیریت‌های پروسه‌ای که در اختیار برنامه‌های ASP.NET در طی سالیان سال قرار داشته‌است. برای مثال اگر پروسه‌ی برنامه‌ی شما در اثر استثنایی کرش کرد، دوباره با درخواست بعدی رسیده، حتما برنامه را بارگذاری و آماده‌ی خدمات دهی مجدد می‌کند.
- در اینجا می‌توان تنظیمات SSL را بر روی IIS انجام داد و سپس درخواست‌های معمولی را به Kestrel  ارسال کرد. به این ترتیب با یک مجوز می‌توان چندین برنامه‌ی Kestrel را مدیریت کرد.
- IISهای جدید به همراه ماژول‌های بومی بسیار بهینه و کم مصرفی برای مواردی مانند gzip compression of static content, static file caching, Url Rewriting هستند که با فعال سازی آن‌ها می‌توان از این قابلیت‌ها، در برنامه‌های ASP.NET Core نیز استفاده کرد.


نحوه‌ی توزیع برنامه‌های ASP.NET Core به IIS

روش اول: استفاده از دستور خط فرمان dotnet publish

برای این منظور به ریشه‌ی پروژه‌ی خود وارد شده و دستور dotnet publish را با توجه به پارامترهای ذیل اجرا کنید:
 dotnet publish --framework netcoreapp1.0 --output "c:\temp\mysite" --configuration Release
در اینجا برنامه کامپایل شده و همچنین مراحلی که در فایل project.json نیز ذکر شده‌اند، اعمال می‌شود. برای مثال در اینجا پوشه‌ها و فایل‌هایی که در قسمت include ذکر شده‌اند به خروجی کپی خواهند شد. همچین در قسمت scripts تمام مراحل ذکر شده مانند یکی کردن و فشرده سازی اسکریپت‌هانیز انجام خواهد شد. قسمت postpublish تنها کاری را که انجام می‌دهد، ویرایش فایل web.config برنامه و تنظیم LAUNCHER_PATH و LAUNCHER_ARGS آن به مقادیر واقعی آن‌ها است.
{    "publishOptions": {        "include": [            "wwwroot",            "Features",            "appsettings.json",            "web.config"        ]    },    "scripts": {        "precompile": [            "dotnet bundle"        ],        "prepublish": [            //"bower install"        ],        "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]    }
}
و در نهایت اگر به پوشه‌ی output ذکر شده‌ی در فرمان فوق مراجعه کنید، این خروجی نهایی است که باید به صورت دستی به وب سرور خود برای اجرا انتقال دهید که به همراه تمام DLLهای مورد نیاز برای برنامه نیز هست.


پس از انتقال این فایل‌ها به سرور، مابقی مراحل آن مانند قبل است. یک Application جدید تعریف شده و سپس ابتدا مسیر آن مشخص می‌شود و نکته‌ی اصلی، انتخاب AppPool ایی است که پیشتر شرح داده شد:


برنامه‌های ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آن‌ها No Managed Code است. همچنین بهتر است به ازای هر برنامه‌ی جدید یک AppPool مجزا را ایجاد کنید تا کرش یک برنامه تاثیر منفی را بر روی برنامه‌ی دیگری نگذارد.

روش دوم: استفاده از ابزار Publish خود ویژوال استودیو

اگر علاقمند هستید که روش خط فرمان فوق را توسط ابزار publish ویژوال استودیو انجام دهید، بر روی پروژه در solution explorer کلیک راست کرده و گزینه‌ی publish را انتخاب کنید. در صفحه‌ای که باز می‌شود، بر روی گزینه‌ی custom کلیک کرده و نامی را وارد کنید. از این نام پروفایل، جهت ساده سازی مراحل publish، در دفعات آتی فراخوانی آن استفاده می‌شود.


در صفحه‌ی بعدی اگر گزینه‌ی file system را انتخاب کنید، دقیقا همان مراحل روش اول تکرار می‌شوند:


سپس می‌توانید فریم ورک برنامه و نوع ارائه را مشخص کنید:


و در آخر کار، Publish به این پوشه‌ی مشخص شده که به صورت پیش فرض در ذیل پوشه‌ی bin برنامه‌است، صورت می‌گیرد.


روش عیب یابی راه اندازی اولیه‌ی برنامه‌های ASP.NET Core

در اولین سعی در اجرای برنامه‌ی ASP.NET Core بر روی IIS به این خطا رسیدم:


در event viewer ویندوز چیزی ثبت نشده بود. اولین کاری را که در این موارد می‌توان انجام داد به این صورت است. از طریق خط فرمان به پوشه‌ی publish برنامه وارد شوید (همان پوشه‌ای که توسط IIS عمومی شده‌است). سپس دستور dotnet prog.dll را صادر کنید. در اینجا prog.dll نام dll اصلی برنامه یا همان نام پروژه است:


همانطور که مشاهده می‌کنید، برنامه به دنبال پوشه‌ی bower_components ایی می‌گردد که کار publish آن انجام نشده‌است (این پوشه در تنظیمات آغازین برنامه عمومی شده‌است و در لیست include قسمت publishOptions فایل project.json فراموش شده‌است).

روش دوم، فعال سازی stdoutLogEnabled موجود در فایل وب کانفیگ، به true است. در اینجا web.config نهایی تولیدی توسط عملیات publish را مشاهده می‌کنید که در آن پارامترهای  processPath و arguments مقدار دهی شده‌اند (همان قسمت postpublish فایل project.json). در اینجا مقدار stdoutLogEnabled به صورت پیش فرض false است. اگر true شود، همان خروجی تصویر فوق را در پوشه‌ی logs خواهید یافت:

<?xml version="1.0" encoding="utf-8"?><configuration><system.webServer><handlers><add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" /></handlers><aspNetCore processPath="dotnet" arguments=".\Core1RtmEmptyTest.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" /></system.webServer></configuration>
البته اگر کاربر منتسب به AppPool برنامه، دسترسی نوشتن در این پوشه را نداشته باشد، خطای ذیل را در event viewer ویندوز مشاهده خواهید کرد:
 Warning: Could not create stdoutLogFile \\?\D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest\bin\Release\PublishOutput\logs\stdout_10064_201672893654.log, ErrorCode = -2147024893.
همچنین پوشه‌ی logs را هم باید خودتان از پیش ایجاد کنید. به عبارتی کاربر منتسب به AppPool برنامه باید دسترسی نوشتن در پوشه‌ی logs واقع در ریشه‌ی برنامه را داشته باشد. این کاربر  IIS AppPool\DefaultAppPool نام دارد.



حداقل‌های یک هاست ویندوزی که می‌خواهد برنامه‌های ASP.NET Core را ارائه دهد

پس از نصب IIS، نیاز است ASP.NET Core Module نیز نصب گردد. برای این‌کار اگر بسته‌ی NET Core Windows Server Hosting. را نصب کنید، کافی است:
https://go.microsoft.com/fwlink/?LinkId=817246

این بسته به همراه NET Core Runtime, .NET Core Library. و ASP.NET Core Module است. همچنین همانطور که عنوان شد، برنامه‌های ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آن‌ها No Managed Code است. این‌ها حداقل‌های راه اندازی یک برنامه‌ی ASP.NET Core بر روی سرورهای ویندوزی هستند.


هنوز فایل app_offline.htm نیز در اینجا معتبر است

یکی از خواص ASP.NET Core Module، پردازش فایل خاصی است به نام app_offline.htm. اگر این فایل را در ریشه‌ی سایت قرار دهید، برنامه پردازش تمام درخواست‌های رسیده را قطع خواهد کرد و سپس پروسه‌ی برنامه خاتمه می‌یابد. هر زمانیکه این فایل حذف شد، مجددا با درخواست بعدی رسیده، برنامه آماده‌ی پاسخگویی می‌شود.

‫شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه

$
0
0
در ادامه‌ی سری «ارتقاء به 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 - قسمت 2 - به روز رسانی ساختار بانک اطلاعاتی

$
0
0
پس از برپایی تنظیمات اولیه‌ی کار با EF Core در ASP.NET Core، اکنون نوبت به تبدیل کلاس Person، به جدول معادل آن در بانک اطلاعاتی برنامه است. در EF Core نیز همانند EF Code First 6.x، برای انجام یک چنین اعمالی از مفهومی به نام Migrations استفاده می‌شود که در ادامه به آن خواهیم پرداخت.


پیشنیازهای کار با EF Core Migrations

در قسمت قبلدر حین بررسی «برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.


بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاس‌های برنامه

پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشه‌ی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته:در ویندوز اگر در پوشه‌ای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینه‌ی جدیدی را به نام Open command window here مشاهده خواهید کرد که می‌تواند به سرعت خط فرمان را از پوشه‌ی جاری شروع کند.

خروجی صدور فرمان dotnet ef را در ذیل مشاهده می‌کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef
                     _/\__
               ---==/    \\
         ___  ___   |.    \|\
        | __|| __|  |  )   \\\
        | _| | _|   \_/ |  //|\\
        |___||_|       /   \\\/\\
Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431
Usage: dotnet ef [options] [command]
Options:
  -h|--help                      Show help information
  -v|--verbose                   Enable verbose output
  --version                      Show version information
  --assembly <ASSEMBLY>          The assembly file to load.
  --startup-assembly <ASSEMBLY>  The assembly file containing the startup class.
  --data-dir <DIR>               The folder used as the data directory (defaults to current working directory).
  --project-dir <DIR>            The folder used as the project directory (defaults to current working directory).
  --content-root-path <DIR>      The folder used as the content root path for the application (defaults to application base directory).
  --root-namespace <NAMESPACE>   The root namespace of the target project (defaults to the project assembly name).
Commands:
  database    Commands to manage your database
  dbcontext   Commands to manage your DbContext types
  migrations  Commands to manage your migrations
Use "dotnet ef [command] --help" for more information about a command.
در قسمت Commands آن در انتهای لیست، از فرمان migrations آن استفاده خواهیم کرد. برای این منظور در همین پوشه‌ی جاری، دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
دستورات migrations با dotnet ef migrations شروع شده و سپس یک سری پارامتر را دریافت می‌کنند برای مثال در اینجا سوئیچ add، به همراه یک نام دلخواه ذکر شده‌است (نام این مرحله را InitialDatabase گذاشته‌ایم). پس از فراخوانی این دستور، اگر به Solution explorer مراجعه کنید، پوشه‌ی جدید Migrations، قابل مشاهده است:


نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده می‌کنید.
اگر قصد حذف این مرحله را داشته باشیم، می‌توان دستور dotnet ef migrations remove را مجددا صادر کرد.

فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوه‌ی ایجاد جدول جدیدی را از کلاس Person بیان می‌کند و متد Down نحوه‌ی Drop این جدول را پیاده سازی کرده‌است.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصه‌ای از تعاریف موجودیت‌های ذکر شده‌ی در DB Context برنامه را به همراه دارد و تفسیر آن‌ها را از دیدگاه  EF در اینجا می‌توان مشاهده کرد.

پس از مرحله‌ی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کرده‌است. اما هنوز آن‌ها را به بانک اطلاعاتی برنامه اعمال نکرده‌است. برای اینکار در پوشه‌ی جاری دستور ذیل را صادر کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
Applying migration '13950526050417_InitialDatabase'.
Done.
همانطور که ملاحظه می‌کنید، دستور dotnet ef database update سبب اعمال اطلاعات فایل 13950526050417_InitialDatabase به بانک اطلاعاتی شده‌است.
اکنون اگر به لیست بانک‌های اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person می‌توان مشاهده کرد:


در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهده‌است که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسه‌های آتی است. این جدول صرفا توسط ابزارهای EF استفاده می‌شود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.


مقدار دهی اولیه‌ی جداول بانک‌های اطلاعاتی در EF Core

در همین حالت اگر کنترلر TestDB مطرح شده‌ی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:


این تصویر بدین معنا است که کار Migrations موفقیت آمیز بوده‌است و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیه‌ای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیه‌ی جداول را می‌توان انجام داد (زمانیکه جدولی ایجاد می‌شود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشه‌ی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic;
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class ApplicationDbContextSeedData
    {
        public static void SeedData(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                if (!context.Persons.Any())
                {
                    var persons = new List<Person>
                    {
                        new Person
                        {
                            FirstName = "Admin",
                            LastName = "User"
                        }
                    };
                    context.AddRange(persons);
                    context.SaveChanges();
                }
            }
        }
    }
}
و سپس نحوه‌ی فراخوانی آن در متد Configure کلاس آغازین برنامه به صورت زیر است:
public void Configure(IServiceScopeFactory scopeFactory)
{
    scopeFactory.SeedData();
به همراه این تغییر در نحوه‌ی معرفی Db Context برنامه:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
توضیحات:
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آن‌را به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت می‌کند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شده‌است. همچنین در این مثال از نمونه‌ای که IConfigurationRoot به سازنده‌ی کلاس ApplicationDbContext تزریق شده (نکته‌ی انتهای بحث قسمت قبل)، استفاده شده‌است. به همین جهت تنظیمات options آن‌را ملاحظه نمی‌کنید.
- مرحله‌ی بعد نحوه‌ی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگی‌ها نداریم و باید کار را با  IServiceScopeFactory شروع کنیم. در اینجا می‌توانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویس‌های توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار می‌گیرد.
- نکته‌ی مهمی که در اینجا بکار رفته‌است، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواست‌ها توسط ASP.NET Core انجام می‌شود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آن‌را به صورت دستی انجام دهیم که نمونه‌ای از آن‌را در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه می‌کنید. در اینجا Scope ایی ایجاد شده‌است. سپس داده‌های اولیه‌ی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شده‌است.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شده‌است انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواست‌ها نرسیده‌است. بنابراین مدیریت Scopeها هنوز شروع نشده‌است و باید به صورت دستی انجام شود.

در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:


که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیه‌ای در بانک اطلاعاتی است.


اعمال تغییرات به مدل‌های برنامه و به روز رسانی ساختار بانک اطلاعاتی

فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کرده‌ایم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public int Age { get; set; }
    }
}
در این حالت اگر برنامه را اجرا کنیم، به استثنای ذیل خواهیم رسید:
 An unhandled exception occurred while processing the request.
SqlException: Invalid column name 'Age'.
برای رفع این مشکل نیاز است مجددا مراحل Migrations را اجرا کرد:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
در اینجا همان دستورات قبل را مجددا اجرا می‌کنیم. با این تفاوت که اینبار نام دلخواه این مرحله را مثلا v2، به معنای نگارش دوم وارد کرده‌ایم.
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشه‌ی Migrations اضافه می‌شود. این فایل حاوی نحوه‌ی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحله‌ی v2 انجام می‌شود.


بنابراین هر بار که تغییری را در مدل‌های خود ایجاد می‌کنید، یکبار باید کلاس مهاجرت آن‌را ایجاد کنید و سپس آن‌را به بانک اطلاعاتی اعمال نمائید.


تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF

شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار می‌توانید دستور ذیل را در پوشه‌ی جاری پروژه اجرا کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
در این حالت اسکریپت SQL تغییرات، در فایلی به نام v2.sql، در ریشه‌ی جاری پروژه تولید می‌شود.


تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0


در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچه‌ی تغییرات، بسیار پیچیده بود و شامل رشته‌ای gzip شده‌ی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحله‌ی migration بود. در این نگارش، این snapshot حذف شده‌است و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده می‌کنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شده‌اند و دیگر خبری از رشته‌ی gzip شده‌ی قبلی نیست (تصویر فوق).

در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده می‌کنید. در EF 6.x حتی فضای نام کلاس‌های موجودیت‌های برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد می‌شود:



مهاجرت خودکار از EF Core حذف شده‌است

در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration()
{
   AutomaticMigrationsEnabled = true;
}
کار آن مهاجرت خودکار اطلاعات context به بانک اطلاعاتی بود؛ بدون نیازی به استفاده از دستورات خط فرمان مرتبط. تمام این موارد از EF Core حذف شده‌اند و علت آن‌را می‌توانید در توضیحات یکی از اعضای تیم EF Core در اینجامطالعه کنید و خلاصه‌ی آن به این شرح است:
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچه‌ی مهاجرت‌های فوق).
- در حالت افزودن یک مرحله‌ی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- می‌توان چندین مرحله‌ی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آن‌ها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیم‌ها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدل‌ها در جدول __EFMigrationsHistory ذخیره نمی‌شود.

بنابراین در EF Core می‌توان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای این‌ها را به بانک اطلاعاتی انجام داد.

هرچند در اینجا اگر می‌خواهید مرحله‌ی اجرای دستور dotnet ef database update را حذف کنید، می‌توانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Core1RtmEmptyTest.Migrations
{
    public static class DbInitialization
    {
        public static void Initialize(this IServiceScopeFactory scopeFactory)
        {
            using (var serviceScope = scopeFactory.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                // Applies any pending migrations for the context to the database.
                // Will create the database if it does not already exist.
                context.Database.Migrate();
            }
        }
    }
}
روش فراخوانی آن نیز همانند روش فراخوانی متد SeedData است که پیشتر بحث شد.
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشده‌اند (دقیقا همان کار دستور dotnet ef database update را انجام می‌دهد).


تفاوت متد Database.EnsureCreated با متد Database.Migrate

اگر به متدهای context.Database دقت کنید، یکی از آن‌ها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه می‌شود. اما هدف آن صرفا استفاده‌ی از آن در آزمون‌های واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمی‌کند (برخلاف متد Migrate). بنابراین بجز آزمون‌های واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفاده‌ی از بانک اطلاعاتی تولید شده‌ی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز می‌توان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



در قسمت بعد، مطالب تکمیلی مهاجرت‌ها را بررسی خواهیم کرد. برای مثال چگونه می‌توان کلاس‌های موجودیت‌ها را به اسمبلی‌های دیگری منتقل کرد.

‫شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر

$
0
0
در قسمت قبل، تغییرات Migrations، در EF Core 1.0 بررسی و گردش کاری آن به همراه مثال‌هایی ارائه شدند. در این قسمت یک سری از نکات تکمیلی EF Core Migrations را بررسی خواهیم کرد.


انتقال Context و Migrations به یک اسمبلی دیگر

تا اینجا اگر مثال بررسی شده را دنبال کرده باشید، دو پوشه‌ی Entities و Migrations را به همراه فایل‌‌های موجودیت‌ها، Context برنامه و Migrations آن‌ها، در همان پروژه‌ی اصلی برنامه، خواهید داشت:


در ادامه قصد داریم بانک اطلاعاتی آزمایشی برنامه را drop کرده، پوشه‌ی Migrations را حذف و صرفا دو فایل ApplicationDbContextSeedData و DBInitialization آن‌را نگه داریم.
کلاس Person را به اسمبلی جدید Entities و کلاس ApplicationDbContext را به اسمبلی جدید DataLayer منتقل می‌کنیم:


اسمبلی جدید Core1RtmEmptyTest.Entities از نوع NET Core Class Library. است و صرفا حاوی کلاس‌های موجودیت‌های برنامه‌است.
اسمبلی جدید Core1RtmEmptyTest.DataLayer نیز از نوع NET Core Class Library. بوده و حاوی تعاریف Context برنامه، به همراه Migrations و تنظیمات آن خواهد بود.

تا اینجا با این نقل و انتقالات، نیاز است وابستگی‌های DataLayer را اصلاح کنیم. بنابراین فایل project.json آن‌را گشوده و به نحو ذیل تکمیل نمائید:
{
  "version": "1.0.0-*",

    "dependencies": {
        "Core1RtmEmptyTest.Entities": "1.0.0-*",
        "Microsoft.EntityFrameworkCore": "1.0.0",
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
        "NETStandard.Library": "1.6.0"
    },

  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}
به این صورت ارجاعی به اسمبلی Core1RtmEmptyTest.Entities به پروژه اضافه شده‌است (تا کلاس Person در ApplicationDbContext شناسایی شود) به همراه وابستگی‌های EF و SQL Server که مورد نیاز Context برنامه هستند.
وابستگی Microsoft.Extensions.Configuration.Abstractions برای کار با IConfigurationRoot اضافه شده‌است (دسترسی به تنظیمات برنامه از طریق تزریق وابستگی‌ها).

به علاوه اکنون به پروژه‌ی وب اصلی مراجعه کرده و فایل project.json آن‌را جهت افزودن ارجاعاتی به این دو اسمبلی جدید، ویرایش کنید:
{
    "dependencies": {
        // same as before
        "Core1RtmEmptyTest.Entities": "1.0.0-*",
        "Core1RtmEmptyTest.DataLayer": "1.0.0-*"
    }
}
به این ترتیب Startup برنامه می‌تواند محل جدید کلاس ApplicationDbContext را شناسایی کند و برنامه کامپایل شود.


فعال سازی Migrations و قرار دادن فایل‌های آن در اسمبلی Core1RtmEmptyTest.DataLayer

در ادامه اگر مانند قسمت قبلبخواهیم مهاجرت‌ها را اضافه کنیم، به خطای ذیل خواهیم رسید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
Your target project 'Core1RtmEmptyTest' doesn't match your migrations assembly 'Core1RtmEmptyTest.DataLayer'.
Either change your target project or change your migrations assembly.
برای حل این مشکل، بجای اینکه دستور فوق را از مسیر src\Core1RtmEmptyTest صادر کنیم که همان ریشه‌ی اصلی پروژه‌ی وب است، اینبار باید دستور را از ریشه‌ی پروژه DataLayer صادر کنیم. اما اگر چنین کاری را انجام دهیم، پیام یافتن نشدن فایل اجرایی ابزارهای خط فرمان EF را دریافت می‌کنیم:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef migrations add InitialDatabase
No executable found matching command "dotnet-ef"
علت اینجا است که باید مجددا فایل Core1RtmEmptyTest.DataLayer\project.json را گشوده و این ابزارها را در آن فعال کنیم:
{
     // same as before

    "tools": {
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }
    },

     // same as before
}
پس از فعال سازی ابزارهای EF در پروژه‌ی DataLayer، اکنون باز هم موفق به اجرای دستور فوق نخواهیم شد:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef migrations add InitialDatabase
Could not invoke this command on the startup project 'Core1RtmEmptyTest.DataLayer'.
This preview of Entity Framework tools does not support commands on class library projects in ASP.NET Core and .NET Core applications.
عنوان می‌کند که پروژه‌ی startup را نمی‌تواند پیدا کند، برای حل این مشکل، دستور را به نحو ذیل ویرایش کنید:
 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef --startup-project ../Core1RtmEmptyTest/ migrations add InitialDatabase
Done. To undo this action, use 'dotnet ef migrations remove'
در اینجا با ذکر صریح startup-project، عملیات تولید فایل‌های Migrations با موفقیت انجام شدند:



اعمال کلاس‌های Migrations تولید شده به بانک اطلاعاتی

پس از تولید موفقیت آمیز فایل‌های مهاجرت، برای اعمال آن‌ها به بانک اطلاعاتی، اینبار نیز دستور را از همان پوشه‌ی DataLayer با پارامتر پروژه‌ی آغازین اجرا می‌کنیم:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest.DataLayer>dotnet ef --startup-project ../Core1RtmEmptyTest/ database update
Applying migration '13950527070105_InitialDatabase'.
Done.
در اینجا نیز ذکر پارامتر startup-project جهت اجرای موفقیت آمیز دستور الزامی است.


بنابراین به صورت خلاصه

- ابتدا قسمت tools تنظیمات پروژه‌ی data layer را برای فعال سازی دستورات خط فرمان EF ویرایش کنید.
- سپس از طریق خط فرمان به پوشه‌ی data layer وارد شوید. اینبار باید دستورات EF را از ریشه‌ی این پوشه، بجای پوشه‌ی اصلی برنامه صادر کرد.
- در اینجا دستورات افزودن مهاجرت‌ها و به روز رسانی بانک اطلاعاتی، همانند قبل هستند. فقط ذکر محل واقع شدن پوشه‌ی آغازین برنامه توسط پارامتر startup-project الزامی است.

‫شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود

$
0
0
روش کار پیش فرض با EF Core همان روش Code First است. ابتدا کلاس‌ها و روابط بین آن‌ها را تنظیم می‌کنید. سپس با استفاده از ابزارهای Migrations، بانک اطلاعاتی متناظری تولید خواهد شد. این ابزارها به همراه روشی برای مهندسی معکوس ساختار یک بانک اطلاعاتی از پیش موجود، به روش Code First نیز هستند که در ادامه جزئیات آن‌را بررسی خواهیم کرد. بنابراین اگر به دنبال روش کاری Database first با EF Core هستید، در اینجا نیز امکان آن وجود دارد.


تهیه یک بانک اطلاعاتی نمونه

برای نمایش امکانات کار با روش Database first، نیاز است یک بانک اطلاعاتی را به صورت مستقل و متداولی ایجاد کنیم. به همین جهت اسکریپت SQL ذیل را توسط Management studio اجرا کنید تا بانک اطلاعاتی BloggingCore2016، به همراه دو جدول به هم وابسته، در آن ایجاد شوند:
CREATE DATABASE [BloggingCore2016]
GO

USE [BloggingCore2016]
GO

CREATE TABLE [Blog] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Blog] PRIMARY KEY ([BlogId])
);
GO

CREATE TABLE [Post] (
    [PostId] int NOT NULL IDENTITY,
    [BlogId] int NOT NULL,
    [Content] nvarchar(max),
    [Title] nvarchar(max),
    CONSTRAINT [PK_Post] PRIMARY KEY ([PostId]),
    CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE
);
GO

INSERT INTO [Blog] (Url) VALUES 
('http://www.dotnettips.info/'), 
('http://blogs.msdn.com/dotnet'), 
('http://blogs.msdn.com/webdev'), 
('http://blogs.msdn.com/visualstudio')
GO



پیشنیازهای مهندسی معکوس ساختار بانک اطلاعاتی در EF Core

در قسمت اول در حین بررسی «برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است. همچنین وجود مدخل Microsoft.EntityFrameworkCore.SqlServer.Design برای تدارک امکانات مهندسی معکوس ساختار یک بانک اطلاعاتی SQL Server ضروری است.


تبدیل ساختار دیتابیس BloggingCore2016 به کدهای معادل EF Core آن

پس از فعال سازی ابزارهای خط فرمان EF Core، به پوشه‌ی اصلی پروژه مراجعه کرده، کلید shift را نگه دارید. سپس کلیک راست کرده و گزینه‌ی Open command window here را انتخاب کنید تا خط فرمان از این پوشه آغاز شود. در ادامه دستور ذیل را صادر کنید:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose


اجرا این دستور سبب اتصال به رشته‌ی اتصالی ذکر شده که به بانک اطلاعاتی BloggingCore2016 اشاره می‌کند، می‌شود. سپس پروایدر مدنظر ذکر شده‌است. سوئیچ o محل درج فایل‌های نهایی را مشخص می‌کند. برای مثال در اینجا فایل‌های نهایی مهندسی معکوس شده در پوشه‌ی Entities درج می‌شوند (تصویر فوق). همچنین در اینجا امکان ذکر فایل context تولیدی نیز وجود دارد. اگر علاقمند باشید تا تمام ریز جزئیات این عملیات را نیز مشاهده کنید، می‌توانید پارامتر اختیاری verbose را نیز به انتهای دستور اضافه نمائید.

بقیه مراحل کار با این فایل‌های تولید شده، با نکاتی که تاکنون عنوان شده‌اند یکی است. برای مثال اگر می‌خواهید رشته‌ی اتصالی پیش فرض را از این Context تولید شده خارج کنید:
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }
روش کار دقیقا همانی است که در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» بررسی شد.


بررسی پارامترهای دیگر ابزار مهندسی معکوس به Code First

اگر دستور dotnet ef dbcontext scaffold --help را صادر کنیم، خروجی راهنمای ذیل را می‌توان مشاهده کرد:
 Usage: dotnet ef dbcontext scaffold [arguments] [options]
Arguments:
  [connection]  The connection string of the database
  [provider] The provider to use. For example, Microsoft.EntityFrameworkCore.SqlServer
Options:
  -a|--data-annotations   Use DataAnnotation attributes to configure the model where possible. If omitted, the output code will use only the fluent API.
  -c|--context <name> Name of the generated DbContext class.
  -f|--force Force scaffolding to overwrite existing files. Otherwise, the code will only proceed if no output files would be overwritten.
  -o|--output-dir <path> Directory of the project where the classes should be output. If omitted, the top-level project directory is used.
  --schema <schema> Selects a schema for which to generate classes.
  -t|--table <schema.table> Selects a table for which to generate classes.
  -e|--environment <environment>  The environment to use. If omitted, "Development" is used.
  -h|--help   Show help information
  -v|--verbose   Enable verbose output
نکات تکمیلی مهمی را که از آن می‌توان استخراج کرد به این شرح هستند:
- حالت پیش فرض تنظیمات روابط مدل‌ها در این روش، حالت استفاده از Fluent API است. اگر می‌خواهید آن‌را به حالت استفاده‌ی از Data Annotations تغییر دهید، پارامتر a- و یا data-annotations-- را در دستور نهایی ذکر کنید.
- حالت پیش فرض تولید فایل‌های نهایی این روش، عدم بازنویسی فایل‌های موجود است. اگر می‌خواهید پس از تغییر بانک اطلاعاتی، مجددا این فایل‌ها را از صفر تولید کنید، پارامتر f- و یا force- را در دستور نهایی ذکر کنید.

بنابراین اگر می‌خواهید هربار فایل‌های نهایی را بازنویسی کنید و همچنین روش کار با Data Annotations را ترجیح می‌دهید، دستور نهایی، شکل زیر را پیدا خواهد کرد:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose --force --data-annotations


کار با یک بانک اطلاعاتی موجود، با روش مهاجرت‌های Code First

فرض کنید می‌خواهید از یک بانک اطلاعاتی از پیش موجود EF 6.x (یا هر بانک اطلاعاتی از پیش موجود دیگری)، به روش پیش فرض EF Core استفاده کنید. برای این منظور:
 - ابتدا جدول migration history قدیمی آن‌را حذف کنید؛ چون ساختار آن با EF Core یکی نیست.
 - سپس با استفاده از دستور dotnet ef dbcontext scaffold فوق، معادل کلاس‌ها، روابط و Context سازگار با EF Core آن‌را تولید کنید.
 - در ادامه رشته‌ی اتصالی پیش فرض آن‌را از کلاس Context تولیدی خارج کرده و از یکی از روش‌های مطرح شده‌ی در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» استفاده کنید.
 - سپس نیاز است این Context جدید را توسط متد services.AddDbContext به لیست سرویس‌های برنامه اضافه کنید. این مورد نیز در قسمت اولبررسی شده‌است.
 - مرحله‌ی بعد، افزودن جدول __EFMigrationsHistory جدید EF Core، به این بانک اطلاعاتی است. برای این منظور به روش متداول فعال کردن مهاجرت‌ها، دستور ذیل را صادر کنید:
dotnet ef migrations add InitialDatabase
تا اینجا کلاس آغازین مهاجرت‌ها تولید می‌شود. فایل آن‌را گشوده و محتوای متدهای Up و Down آن‌را خالی کنید:
using Microsoft.EntityFrameworkCore.Migrations;

namespace Core1RtmEmptyTest.DataLayer.Migrations
{
    public partial class InitialDatabase : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
        }
    }
}
متدهای up و down را از این جهت خالی می‌کنیم که علاقمند نیستیم تا ساختاری در بانک اطلاعاتی تشکیل شود و یا تغییر کند (چون این ساختار هم اکنون موجود است).
سپس دستور به روز رسانی بانک اطلاعاتی را صادر کنید:
dotnet ef database update
کار این دستور در اینجا با توجه به خالی بودن متدهای up و down، صرفا ساخت جدول مخصوص __EFMigrationsHistory در بانک اطلاعاتی است؛ بدون تغییری در جداول موجود آن.
پس از این مرحله، روش کار، Code first خواهد بود. برای مثال خاصیتی را به کلاسی اضافه می‌کنید و سپس دو دستور ذیل را صادر خواهید کرد که در آن v2 یک نام دلخواه است:
dotnet ef migrations add v2
dotnet ef database update

‫شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها

$
0
0
پس از بررسی نحوه‌ی انجام تنظیمات اولیه‌ی کار با EF Coreو همچنین آشنایی با مهاجرت‌های آن، مرحله‌ی بعد، مرحله‌ی مدلسازی داده‌ها است و اولین مرحله‌ی آن، نحوه‌ی تعیین کلید اصلی جداول است که در این زمینه، EF Core پیشرفت‌هایی قابل ملاحظه‌ای را نسبت به EF 6.x داشته‌است. در EF 6.x تنها دو حالت کلیدهای اصلی خود افزاینده که توسط بانک اطلاعاتی مدیریت می‌شوند و یا تولید کلید اصلی در سمت کلاینت و توسط برنامه، پشتیبانی می‌شوند. در EF Core، مواردی مانند Sequence و Alternate keys نیز اضافه شده‌اند.


پیش فرض‌های تعیین کلید اصلی در EF Core

به صورت پیش فرض هر خاصیتی که به نام Id و یا type name>Id> باشد، به عنوان primary key تفسیر خواهد شد؛ مانند:
public class Car
{
    public string Id { get; set; }
و یا
public class Car
{
   public string CarId { get; set; }
در مثال اول، نام خاصیت، Id است و در مثال دوم، جمع نام کلاس به همراه Id ذکر شده‌است. یک چنین مواردی، نیازی به تنظیم اضافه‌تری ندارند.


نحوه‌ی تعیین کلید اصلی به صورت صریح

اگر یکی از دو حالت فوق برقرار نباشند، باید کلید اصلی را به نحو صریحی مشخص کرد.
الف) از طریق ویژگی‌ها
public class Car
{
   [Key]
   public string LicensePlate { get; set; }
در اینجا چون LicensePlate نه Id نام دارد و نه جمع نام کلاس به همراه Id است، باید به نحو صریحی توسط ویژگی Key مشخص شود.
ب) با استفاده از روش Fluent API
public class MyContext : DbContext
{
    public DbSet<Car> Cars { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
         modelBuilder.Entity<Car>()
                 .HasKey(c => c.LicensePlate);
    }
 }
روش تنظیم کلید اصلی به صورت صریح، از طریق کدنویسی است که به آن Fluent API یا API روان هم گفته می‌شود. برای اینکار باید متد OnModelCreating کلاس Context برنامه را بازنویسی کرد و سپس از طریق متد HasKey، نام خاصیت کلید اصلی را ذکر نمود.


پیشنیاز کار با ویژگی‌ها در EF Core

در اسمبلی که مدل‌های موجودیت‌ها شما قرار دارند، نیاز است وابستگی System.ComponentModel.Annotations به فایل project.json پروژه اضافه شود، تا ویژگی‌هایی مانند Key، شناسایی و قابل استفاده شوند:
{
   "dependencies": {
          "System.ComponentModel.Annotations": "4.1.0"
   }
}


تعیین کلید ترکیبی و یا Composite key

اگر نیاز است چندین خاصیت را به صورت کلید اصلی معرفی کرد که به آن composite key هم می‌گویند، تنها روش ممکن، استفاده از Fluent API و به صورت زیر است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Car>()
                       .HasKey(c => new { c.State, c.LicensePlate });
}
در قسمت HasKey می‌توان چندین خاصیت را نیز جهت تعیین کلید ترکیبی مشخص کرد.


روش‌های مختلف تولید خودکار مقادیر خواص

حالت پیش فرض تولید مقدار فیلدهای Id عددی، همان حالت خود افزاینده‌ای است که توسط بانک اطلاعاتی کنترل می‌شود و یا کلید اصلی که از نوع Guid تعیین شود نیز به صورت خودکار توسط بانک اطلاعاتی در حین عملیات Add، مقدار دهی می‌شود (با استفاده از الگوریتم Guid سری در SQL Server).
 اگر این حالات مطلوب شما نیست، حالت‌های سه گانه‌ی ذیل را می‌توان استفاده کرد:

الف) هیچ داده‌ی خودکاری تولید نشود
برای اینکار می‌توان با استفاده از ویژگی DatabaseGenerated و تنظیم مقدار آن به None، جلوی تولید خودکار کلید اصلی را گرفت. در این حالت باید هم در حین عملیات Add و هم در حین عملیات Update، مقادیر را خودتان مقدار دهی کنید:
public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int BlogId { get; set; }

    public string Url { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
           .Property(b => b.BlogId)
           .ValueGeneratedNever();
}

ب) تولید داده‌های خودکار فقط در حالت Add
حالت Add به این معنا است که داده‌های خواص مشخصی، برای موجودیت‌های «جدید»، به صورت خودکار تولید خواهند شد. اینکه آیا واقعا این مقادیر به صورت خودکار تولید می‌شوند یا خیر، صرفا وابسته‌است به بانک اطلاعاتی در حال استفاده. برای مثال SQL Server برای نوع‌های Guid، به صورت خودکار با کمک الگوریتم SQL Server sequential GUID، کار مقدار دهی یک چنین فیلدهایی را انجام می‌دهد.
این فیلدها باید توسط ویژگی DatabaseGenerated و با مقدار Identity مشخص شوند. در اینجا Identity به معنای فیلدهایی است که به صورت خودکار توسط بانک اطلاعاتی مقدار دهی می‌شوند و الزاما به کلید اصلی اشاره نمی‌کنند. برای مثال در موجودیت ذیل، خاصیت تاریخ ثبت رکورد، از نوع Identity مشخص شده‌است. به این معنا که در حین ثبت اولیه‌ی رکورد آن، نیازی نیست تا خاصیت Inserted را مقدار دهی کرد. اما اینکه آیا SQL Server یک چنین کاری را به صورت خودکار انجام می‌دهد، پاسخ آن خیر است. SQL server فقط برای فیلدهای عددی و Guid ایی که با DatabaseGeneratedOption.Identity مزین شده باشند، مقادیر متناظری را به صورت خودکار تولید می‌کند. برای حالت DateTime نیاز است، مقدار پیش فرض فیلد را صریحا مشخص کرد که توسط ویژگی‌ها میسر نیست و فقط fluent API از آن پشتیبانی می‌کند.
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }

   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   public DateTime Inserted { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
           .Property(b => b.Inserted)
           .ValueGeneratedOnAdd();
}
برای تعیین مقدار پیش فرض خاصیت Inserted به نحوی که توسط SQL Server به صورت خودکار مقدار دهی شود، می‌توان از متد HasDefaultValueSql به نحو ذیل استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Inserted)
        .HasDefaultValueSql("getdate()");
}
البته باید درنظر داشت که اگر خاصیت DateTime تعریف شده در اینجا به همین نحو بکاربرده شود، اگر مقداری برای آن در حین تعریف یک وهله جدید از کلاس Blog درکدهای برنامه درنظر گرفته نشود، یک مقدار پیش فرض حداقل به آن انتساب داده خواهد شد (چون value type است). بنابراین نیاز است این خاصیت را از نوع nullable تعریف کرد (public DateTime? Inserted).

یک نکته:در حالت DatabaseGeneratedOption.Identity و یا ValueGeneratedOnAdd فوق، اگر مقداری به این نوع فیلدها انتساب داده شده باشد که با مقدار پیش فرض آن‌ها (property.ClrType.GetDefaultValue) متفاوت باشد، از این مقدار جدید، بجای تولید مقداری خودکار، استفاده خواهد شد. برای مثال مقدار پیش فرض رشته‌ها، نال، مقادیر عددی، صفر و برای Guid مقدار Guid.Empty است. اگر هر مقدار دیگری بجای این‌ها به فیلدهای فوق انتساب داده شوند، از آن‌ها استفاده می‌شود.

ج) تولید داده‌های خودکار در هر دو حالت Add و Update
تولید داده‌ها در حالت‌های Add و Update به این معنا است که یک چنین خواصی، همواره با فراخوانی متد SaveChanges، دارای مقدار خودکار جدیدی خواهند شد و نیازی نیست در کدها مقدار دهی شوند. برای مشخص سازی این نوع خواص، از ویژگی DatabaseGenerated با مقدار Computed و یا متد ValueGeneratedOnAddOrUpdate در حالت Fluent API می‌توان استفاده کرد:
public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public DateTime LastUpdated { get; set; }
}
و یا معادل این تنظیم با استفاده از Fluent API به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
       .Property(b => b.LastUpdated)
       .ValueGeneratedOnAddOrUpdate();
}
همانطور که پیشتر نیز عنوان شد، تولید خودکار مقادیر فیلدها فقط در حالت‌های int و Guid انجام می‌شود (که برای مثال SQL Server از آن‌ها پشتیبانی می‌کند). در مثال فوق، خاصیت LastUpdated از نوع DateTime اینگونه تعریف شده‌است و SQL Server برای یک چنین فیلدهای خاصی، مقدار خودکاری را تولید نکرده و به دنبال مقدار پیش فرض آن می‌گردد. بنابراین در اینجا نیز باید مشخص سازی HasDefaultValueSql("getdate()") را که در قسمت قبل عنوان کردیم، صراحتا در قسمت تنظیمات Fluent API ذکر و تنظیم کرد.

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


خواص محاسباتی (Computed Columns) و تفاوت آن‌ها با DatabaseGeneratedOption.Computed

خواص محاسباتی (Computed Columns)، خواصی هستند که مقادیر آن‌ها در بانک اطلاعاتی محاسبه می‌شوند و کاملا متفاوت هستند با DatabaseGeneratedOption.Computed که مفهوم دیگری دارد. DatabaseGeneratedOption.Computed به این معنا است که این فیلد خاص، با هر بار فراخوانی SaveChanges باید مقدار محاسبه شده‌ی جدیدی را داشته باشد و روش تولید این مقدار خودکار، یا بر اساس Guidهای سری است، یا توسط فیلدهای خود افزاینده‌ی عددی و یا از طریق مقادیر پیش فرضی مانند getdate در حین ثبت یا به روز رسانی، مقدار دهی می‌شوند. اما خواص محاسباتی، یکی از امکانات «گزارشگیری سریع» SQL Server هستند و به نحو ذیل، تنها توسط Fluent API قابل تنظیم می‌باشند:
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string DisplayName { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
          modelBuilder.Entity<Person>()
              .Property(p => p.DisplayName)
               .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
     }
 }
در اینجا فیلد DisplayName یک فیلد محاسباتی بوده و از حاصل جمع دو فیلد دیگر در سمت دیتابیس تشکیل می‌شود. این نگاشت و محاسبه چون در سمت بانک اطلاعاتی انجام می‌شود، بازدهی بیشتری دارد نسبت به حالتی که ابتدا دو فیلد به کلاینت منتقل شده و سپس در این سمت جمع زده شوند.


امکان تعریف Sequence در EF Core 1.0

Sequence قابلیتی است که به SQL Server 2012 اضافه شده‌است و توضیحات بیشتر آن‌را در مطلب «نحوه ایجاد Sequence و استفاده آن در Sql Server 2012» می‌توانید مطالعه کنید.
در EF Core، امکان مدلسازی Sequence نیز پیش بینی شده‌است. آن‌ها به صورت پیش فرض در مدل‌ها ذکر نمی‌شوند و همچنین وابستگی به جدول خاصی ندارند. به همین جهت امکان تعریف آن‌ها صرفا توسط Fluent API وجود دارد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
     modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared") 
           .StartsAt(1000).IncrementsBy(5);

     modelBuilder.Entity<Order>()
         .Property(o => o.OrderNo)
         .HasDefaultValueSql("NEXT VALUE FOR shared.OrderNumbers");
}
پس از اینکه یک Sequence  تعریف شد، می‌توان برای نمونه از آن جهت تولید مقادیر پیش فرض ستون‌ها استفاده کرد.
در مثال فوق، ابتدا یک Sequence نمونه به نام OrderNumbers تعریف شده‌است که از عدد 1000 شروع شده و واحد افزایش آن 5 است. سپس از این نام در قسمت مقدار پیش فرض ستون OrderNo استفاده شده‌است.

و یا از Sequence ‌ها می‌توان برای تعیین مقدار پیش فرض Primary key بجای حالت identity خود افزایش یابنده استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasSequence<int>("PrimaryKeyWithSequenceSequence");
    modelBuilder.Entity<PrimaryKeyWithSequence>(entity =>
     {
       entity.Property(e => e.PrimaryKeyWithSequenceId).HasDefaultValueSql("NEXT VALUE FOR [PrimaryKeyWithSequenceSequence]");
     });
}
در اینجا یک توالی از نوع int تعریف شده و سپس هربار که قرار است رکوردی درج شود، مقدار id آن به صورت خودکار از طریق کوئری Select NEXT VALUE FOR
[PrimaryKeyWithSequenceSequence] دریافت و سپس بجای فیلد id درج می‌شود.

به این روش الگوریتم Hi-Low هم می‌گویندکه یکی از مهم‌ترین اهداف آن داشتن یک سری Id منحصربفرد، جهت بالابردن سرعت insertها در یک batch است. در حالت عادی insertها، ابتدا یک insert انجام می‌شود، سپس کوئری گرفته شده و آخرین Id درج شده به کلاینت بازگشت داده می‌شود. این روش، برای انجام تنها یک insert، سریع است. اما برای batch insert، به شدت کارآیی پایینی دارد. به همین جهت دسترسی به بازه‌ای از اعداد منحصربفرد، پیش از شروع به insert تعداد زیادی رکورد، سرعت نهایی کار را بالا می‌برد.


نحوه‌ی تعریف ایندکس‌ها در EF Core 1.0

برای افزودن ایندکس‌ها به EF Core 1.0، تنها روش میسر، استفاده از Fluent API است (و برخلاف EF 6.x از روش data annotations فعلا پشتیبانی نمی‌کند؛ هرچند API جدید آن نسبت به EF 6.x بسیار واضح‌تر است و با ابهامات کمتر).
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
      modelBuilder.Entity<Blog>()
          .HasIndex(b => b.Url)
          .HasName("Index_Url");
اگر قسمت HasName را ذکر نکنید، نام آن <IX_<type name>_<property name درنظر گرفته می‌شود و برای اینکه ایندکس منحصربفردی را تعریف کنید، می‌توان متد IsUnique را به انتهای این زنجیره اضافه کرد:
 modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasName("Index_Url").IsUnique();
همچنین می‌توان همانند composite keys، در اینجا نیز ترکیبی از خواص را به صورت یک ایندکس معرفی نمود:
modelBuilder.Entity<Person>()
   .HasIndex(idx => new { idx.FirstName, idx.LastName })
   .IsUnique();
در این حالت اگر HasName ذکر نشود، نام آن همانند الگویی است که پیشتر عنوان شد؛ با این تفاوت که قسمت property name آن، جمع نام تمام خواص ذکر شده و جدا شده‌ی با _ خواهد بود.

یک نکته:اگر از پروایدر SQL Server استفاده می‌کنید، می‌توان متد الحاقی ویژه‌ای را به نام ForSqlServerIsClustered نیز برای تعریف clustered indexes، در این زنجیره ذکر کرد.


امکان تعریف Alternate Keys در EF Core 1.0

به Unique Constraints در EF Core، نام Alternate Keys را داده‌اند و این مورد نیز تنها از طریق Fluent API قابل تنظیم است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Entity<Car>()
     .HasAlternateKey(c => c.LicensePlate)
     .HasName("AlteranteKey_LicensePlate");
}
برای یک Alternate Key به صورت خودکار هم ایندکس ایجاد می‌شود و هم اینکه این ایندکس منحصربفرد خواهد بود.
اگر متد HasName در اینجا ذکر نشود، نام پیش فرض آن  <type name>_<property name> خواهد بود و اگر همانند composite keys و یا ایندکس‌های ترکیبی، چند خاصیت ذکر شوند، قسمت property name به جمع نام تمام خواص ذکر شده و جدا شده‌ی با _ تنظیم می‌شود.
برای نمونه اگر یک Alternate Key ترکیبی را به صورت ذیل تعریف کنیم:
modelBuilder.Entity<Person>()
     .HasAlternateKey(x => new { x.FirstName, x.LastName });
در قسمت مهاجرت‌هایی که قرار است به بانک اطلاعاتی اعمال شوند، به یک UniqueConstraint ترجمه می‌شود:
 table.UniqueConstraint("AK_Persons_FirstName_LastName", x => new { x.FirstName, x.LastName });


سؤال: یک Unique Constraint با Unique Index چه تفاوتی دارد؟

در پشت صحنه، پیاده سازی یک Unique Constraint با Unique Index تفاوتی ندارند. فقط از دیدگاه روشن‌تر شدن مقصود، استفاده‌ی از Unique Constraint ترجیح داده می‌شود.
البته از دیدگاه بانک اطلاعاتی پیاده سازی کننده نیز برای نمونه SQL Server، این تفاوت‌ها وجود دارند:
الف) یک Unique Constraint را نمی‌توان غیرفعال کرد؛ برخلاف Unique Indexها.
ب) Unique Constraint‌ها موارد اضافه‌تری را مانند FILLFACTOR و IGNORE_DUP_KEY نیز می‌توانند تنظیم کنند.
ج) امکان تعریف فیلترها برای Unique Indexها وجود دارد؛ برخلاف Unique Constraint ها.

که البته از دیدگاه EF، این سه مورد اهمیتی ندارند و بیشتر روشن‌تر شدن مقصود، هدف اصلی آن‌ها است.

‫شروع به کار با EF Core 1.0 - قسمت 6 - تعیین نوع‌های داده و ویژگی‌های آن‌ها

$
0
0
یکی از مهم‌ترین قسمت‌های مدل سازی موجودیت‌ها، تعیین نوع‌های صحیح ستون‌ها و همچنین تعیین اندازه‌ی مناسبی برای آن‌ها است؛ به همراه تعیین اجباری بودن یا نبودن مقدار دهی آن‌ها.

تعیین اجباری بودن یا نبودن ستون‌ها در 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 ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر می‌شود.

‫شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many

$
0
0
در مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود»، نحوه‌ی مهندسی معکوس ساختار جداول و ارتباطات یک بانک اطلاعاتی از پیش موجود را به روش Code First بررسی کردیم. با توجه به رسمی بودن این ابزار، می‌توان از آن برای یافتن معادل‌های سمت بانک اطلاعاتی، در EF Core نیز استفاده کرد. برای مثال بررسی کرد، درک EF Core از بانک اطلاعاتی طراحی شده چیست و هر چند در آن مطلب عنوان شد که می‌توان با پارامتر data-annotations-- ، خروجی نهایی را بر اساس روش data-annotations، بجای Fluent API به دست آورد، اما در مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها» مشاهده کردیم که بسیاری از تنظیمات پیشرفته‌ی EF Core، اساسا معادل data-annotation ایی ندارند. بنابراین بهتر است این پارامتر را فعال سازی نکنید.


تنظیمات روابط یک به چند در EF Core

همان اسکریپت ابتدای مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود» را درنظر بگیرید. رابطه‌ی تعریف شده‌ی در آن از نوع one-to-many است: یک بلاگ که می‌تواند چندین مطلب را داشته باشد.


اگر EF Core را وادار به تولید نگاشت‌های Code First معادل آن کنیم، به این خروجی‌ها خواهیم رسید:
الف) با استفاده از روش Fluent API
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
با خروجی:
using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }
        public string Url { get; set; }

        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>(entity =>
            {
                entity.Property(e => e.Url).IsRequired();
            });

            modelBuilder.Entity<Post>(entity =>
            {
                entity.HasOne(d => d.Blog)
                    .WithMany(p => p.Post)
                    .HasForeignKey(d => d.BlogId);
            });
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}

نحوه‌ی تشخیص خودکار روابط

EF Core به صورت پیش فرض، روابط را بر اساس ارجاعات بین کلاس‌ها تشخیص می‌دهد. در اینجا به خاصیت Blog نام navigation property را می‌دهند:
 public virtual Blog Blog { get; set; }
و به خاصیت Post نیز Collection navigation property می‌گویند:
 public virtual ICollection<Post> Post { get; set; }
در اینجا اگر تنها دو navigation property، در کلاس‌های به هم مرتبط شده، یافت شوند، به صورت خودکار به عنوان دو سر رابطه تنظیم می‌شوند. اگر بیشتر از یک navigation property در کلاسی وجود داشت، هیچ رابطه‌ای به صورت خودکار تشکیل نشده و باید ابتدا و انتهای روابط را به صورت دستی مشخص نمود.


نحوه‌ی تشخیص خودکار کلیدهای خارجی
اگر در یک طرف رابطه‌ی تشخیص داده شده، خاصیتی با یکی از سه نام زیر وجود داشت:
<primary key property name><navigation property name><primary key property name><principal entity name><primary key property name>
آنگاه این خاصیت به صورت خودکار به عنوان کلید خارجی تنظیم می‌شود. در رابطه‌ی فوق Blog از نوع principal است (پدر رابطه) و Post از نوع dependent (فرزند رابطه).
برای مثال در رابطه‌ی فوق، نام خاصیت BlogId دقیقا بر اساس همان الگوی <primary key property name> طرف دیگر رابطه‌است:
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
بنابراین به صورت خودکار به عنوان کلید خارجی درنظر گرفته می‌شود.

تا اینجا اگر مطلب را دنبال کرده باشید به این نتیجه خواهید رسید که دو کلاس فوق، اساسا نیازی به هیچ نوع تنظیم Fluent و یا Data annotations ایی برای برقراری ارتباط یک به چند ندارند. چون روابط بین آن‌ها بر اساس خواص راهبری (navigation property) و همچنین الگوی <primary key property name>، به صورت خودکار قابل تشخیص و تنظیم است. به علاوه ... در هر طرف رابطه، فقط یک navigation property وجود دارد و نیازی به تنظیم دستی سر دیگر رابطه نیست.


استفاده از Fluent API برای تنظیم رابطه‌ی One-to-Many

در تنظیمات فوق، در متد OnModelCreating، ذکر صریح این روابط را صرفا جهت از بین بردن هرگونه ابهامی مشاهده می‌کنید:
modelBuilder.Entity<Post>(entity =>
{
    entity.HasOne(d => d.Blog)
             .WithMany(p => p.Post)
             .HasForeignKey(d => d.BlogId);
});
از هر طرفی که شروع می‌کنید، متدهای HasOne و یا HasMany، مشخص کننده‌ی navigation property هستند که در سمت موجودیت معرفی شده قرار دارند. در اینجا چون کار با موجودیت Post شروع شده‌است، متد HasOne به خاصیت راهبری در همان سمت و به خاصیت Blog آن اشاره می‌کند.
مرحله‌ی بعد، مشخص کردن سر دیگر رابطه (inverse navigation) است. این‌کار توسط یکی از متدهای WithOne و یا WithMany انجام می‌شود.
متدهایی که اسامی فرد دارند مانند HasOne/WithOne به یک navigation property ساده اشاره می‌کنند.
متدهایی که اسامی جمع دارند مانند HasMany/WithMany به collection navigation properties اشاره خواهند کرد.
متد HasForeignKey نیز برای ذکر صریح کلید خارجی بکار رفته‌است.


ب) با استفاده از روش data-annotations
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose -a
با خروجی:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }

        [Required]
        public string Url { get; set; }

        [InverseProperty("Blog")]
        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        [ForeignKey("BlogId")]
        [InverseProperty("Post")]
        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}
همانطور که در توضیحات روش Fluent API عنوان شد، این مدل خاص، چون دقیقا بر اساس پیش فرض‌های EF Core طراحی شده‌است، نیازی به هیچگونه تنظیم اضافه‌تری ندارد. اما اگر کلید خارجی، مطابق سه الگویی که عنوان شد، قابل تشخیص نباشد، باید آن‌را در روش data annotations توسط ویژگی ForeignKey، به نحو صریحی مشخص کرد:
  [ForeignKey("BlogId")]
  [InverseProperty("Post")]
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
همچنین اگر بیش از یک خاصیت راهبری (navigation property) وجود داشت، ذکر InverseProperty نیز ضروری است تا مشخص شود سر دیگر این رابطه دقیقا کدام است.
در این حالت (داشتن بیش از یک خاصیت راهبری)، باید ویژگی InverseProperty را نیز به سر دوم رابطه، اعمال کرد.
   [InverseProperty("Blog")]
  public virtual ICollection<Post> Post { get; set; }

مطالب تکمیلی

علت virtual بودن خواص راهبری تولید شده

اگر دقت کنید، EF Core کدی را که تولید کرده‌است، به همراه خاصیت‌هایی virtual است:
public virtual Blog Blog { get; set; }
در اینجا تمام خاصیت‌های راهبری virtual تعریف شده‌اند. علت آن، به پیاده سازی مباحث AOPبر می‌گردد. زمانیکه خاصیتی به صورت virtual تعریف می‌شود، EF core می‌تواند آن‌را توسط یک شیء پروکسی شفاف احاطه کند. این پروکسی‌ها دو هدف را دنبال می‌کند:
الف) پیاده سازی lazy loading (بارگذاری خودکار اعضای مرتبط (همان خواص راهبری) با اولین دسترسی به آن‌ها)
ب) پیاده سازی change tracking

مبحث lazy loading فعلا در EF Core 1.0 پشتیبانی نمی‌شود. اما change tracking آن فعال است.
بنابراین اگر مشاهده کردید خواص راهبری به صورت virtual تعریف شده‌اند، علت آن فعال سازی lazy loading است و اگر سایر خواص به صورت virtual تعریف شده‌اند، هدف اصلی آن بهبود عملکرد سیستم change tracking است.
همچنین اگر دقت کرده باشید، نوع مجموعه‌ها نیز ICollection ذکر شده‌است. این مورد نیز یکی دیگر از پیش فرض‌های توکار EF Core است؛ در جهت تشکیل پروکسی‌ها بر روی خواص راهبری مجموعه‌ای (علاوه بر virtual تعریف کردن آن‌ها). عنوان شده‌است که اگر برای مثال از List استفاده کنید (پیاده سازی اینترفیس) یا هر اینترفیس دیگری که از ICollection  مشتق شده‌است، این پروکسی‌ها تشکیل نخواهند شد.


واکشی اعضای به هم مرتبط

همانطور که عنوان شد، نگارش اول EF Core برخلاف EF 6.x از Lazy loading پشتیبانی نمی‌کند. البته این مساله در کل مورد مثبتی است؛ خصوصا در برنامه‌های وب! چون استفاده‌ی نادرست از Lazy loading که به select n+1 نیز مشهور است، سبب رفت و برگشت‌های بی‌شماری به بانک اطلاعاتی می‌شود و عموم برنامه نویس‌های وب باید مدام توسط برنامه‌های Profiler بررسی کنند که آیا این مساله رخ داده‌است یا خیر. فعلا EF Core از این مشکل در امان است!
اما ... اگر به روش کار EF 6.x عادت کرده باشید، قطعه کد ذیل:
 var firstPost = context.Post.First();
Console.WriteLine(firstPost.Blog.Url);
چنین خطایی را صادر می‌کند:
 System.NullReferenceException
Object reference not set to an instance of an object.
علت اینجا است که چون Lazy loading غیرفعال است (هنوز در EF Core 1.0 پیاده سازی نشده‌است)، اولین دسترسی به شیء Blog، سبب وهله سازی خودکار آن نشده و این شیء نال است. به همین جهت استثنای فوق را مشاهده می‌کنیم.
برای رفع این مشکل باید توسط متد Include، سبب لغو عملیات Lazy loading و واکشی صریح Blog مرتبط شویم که اصطلاحا به آن eager loading می‌گویند:
 var firstPost = context.Post.Include(x => x.Blog).First();
Console.WriteLine(firstPost.Blog.Url);

نکته‌ای در مورد سطوح بارگذاری اعضای به هم مرتبط در EF Core

متد Include ایی را که تا اینجا مشاهده کردید، با EF 6.x تفاوتی ندارد. برای مثال اگر شیء Blog حاوی خواص راهبری Posts و همچنین Owner باشد، برای بارگذاری این اعضای مرتبط، می‌توان همانند قبل، متدهای Include را پشت سر هم ذکر کرد:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Include(blog => blog.Owner)
                              .ToList();
اما فرض کنید خاصیت Post، دارای یک خاصیت راهبری دیگری به نام Author نیز باشد و می‌خواهیم این خاصیت هم بارگذاری شود:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                      .ThenInclude(post => post.Author)
                              .ToList();
روش انجام چنین کاری در EF Core، توسط متد الحاقی جدید ThenInclude است. ابتدا لیست Blogها عنوان شده‌است. سپس در این لیست علاقمند به واکشی تمام مطالب این بلاگ‌ها هم بوده‌ایم. به علاوه در این مطالب، نیاز است خاصیت Author آن‌ها نیز از پیش مقدار دهی شده و قابل دسترسی باشد. به همین جهت برای دسترسی به چندین سطح مختلف از متد ThenInclude کمک گرفته شده‌است.
همچنین در اینجا امکان ذکر زنجیروار متدهای ThenInclude هم هست:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                 .ThenInclude(post => post.Author)
                                        .ThenInclude(author => author.Photo)
                              .ToList();
در این مثال یک سطح دیگر جلو رفته و شیء Photo مربوط به شیء Author را هم واکشی کرده‌ایم.
به علاوه امکان ذکر چندین ریشه و چندین زیر ریشه هم وجود دارند:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                  .ThenInclude(post => post.Author)
                                      .ThenInclude(author => author.Photo)
                              .Include(blog => blog.Owner)
                                    .ThenInclude(owner => owner.Photo)
                              .ToList();

یک نکته:متد Include تنها زمانی درنظر گرفته خواهد شد که نوع خروجی نهایی کوئری، دقیقا از نوع موجودیتی باشد که با آن شروع به کار کرده‌ایم. برای مثال اگر در این بین یک Select اضافه شود و فقط تنها تعدادی از خواص Blog واکشی شوند، از تمام Includeهای ذکر شده صرفنظر می‌شود؛ مانند کوئری ذیل:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Select(blog => new
                               {
                                  Id = blog.BlogId,
                                  Url = blog.Url
                               })
                               .ToList();


تنظیمات حذف آبشاری در رابطه‌ی one-to-many

زمانیکه در رابطه‌ی one-to-many قسمت principal (والد رابطه) و یا همان Blog در مثال جاری حذف می‌شود، سه اتفاق برای فرزندان آن میسر خواهند بود:
الف) Cascade : در این حالت ردیف‌های فرزندان وابسته نیز حذف خواهند شد.
باید دقت داشت که حالت Cascade فقط برای موجودیت‌هایی اعمال می‌شود که توسط Context بارگذاری شده و در آن وجود دارند. اگر می‌خواهید سایر موجودیت‌های مرتبط نیز با این روش حذف شوند، باید در سمت دیتابیس نیز تنظیماتی مانند ON DELETE CASCADE زیر نیز وجود داشته باشند:
 CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE
و اگر با EF Core بانک اطلاعاتی خود را ایجاد می‌کنید (مباحث مهاجرت‌ها)، این تنظیم به صورت خودکار اعمال خواهد شد؛ اگر DeleteBehavior را به نحو ذیل مشخص کرده باشید:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .OnDelete(DeleteBehavior.Cascade);
ب) SetNull: در این حالت فرزندان وابسته حذف نمی‌شوند و تنها کلید خارجی آن‌ها به نال تنظیم می‌شود.
ج) Restrict: هیچ تغییری بر روی فرزندان رابطه رخ نمی‌دهد.

یک نکته:به صورت پیش فرض اگر رابطه‌ی one-to-many، به Required تنظیم شود، حالت حذف آن cascade خواهد بود. در غیراینصورت برای حالت‌های Optional، حالت SetNull تنظیم می‌گردد:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .IsRequired();
در اینجا ذکر صریح متد IsRequired به این معنا است که مقدار دهی کلید خارجی سر دیگر رابطه، اجباری است.
به علاوه باید دقت داشت، همان مباحث «تعیین اجباری بودن یا نبودن ستون‌ها در EF Core» در قسمت قبل، در اینجا هم صادق است. برای مثال چون BlogId (کلید خارجی در کلاس Post) از نوع int است و نال پذیر نیست، بنابراین از دیدگاه EF Core یک فیلد اجباری درنظر گرفته می‌شود. به همین جهت است که در کدهای تولید شده‌ی توسط EF Core در ابتدای بحث، ذکر متد IsRequired و یا OnDelete را مشاهده نمی‌کنید.
بنابراین اگر می‌خواهید حالت SetNull را فعال کنید، باید این کلید خارجی را نیز نال پذیر و به صورت int? BlogId ذکر کنید تا optional درنظر گرفته شود.
Viewing all 271 articles
Browse latest View live


Latest Images