نظرات مطالب
EF Code First #2
بله؛ این امکان وجود دارد که سیستم را افزونه پذیر کرد و مدل‌ها رو در زمان اجرا به DbContext اضافه کرد.
جزئیات آن خارج است از بحث «قسمت دوم» ما.
نظرات مطالب
Ankh-SVN-2.0.6347
آیا برای استفاده از این افزونه و یا Visual SVN Srver راهنمایی که برای دانلو گذاشتید مفید هست یعنی برای شروع کار کافیه؟
مطالب
بهینه سازی برنامه‌های وب ASP.NET برای موتورهای جستجو (SEO)
می‌دانیم بهینه‌سازی موتورهای جستجو (به انگلیسی: ‎Search engine optimization (SEO)‎)‏ که گاهی در فارسی به آن سئو نیز گفته می‌شود، عملیاتی است برای بهبود دید یک وب‌گاه یا یک صفحهٔ وب، در صفحه نتایج موتورهای جستجو که می‌تواند طبیعی و یا الگوریتمی باشد. این عملیات برای وبمسترها یکی از عوامل مهم و حیاتی بدست آوردن کاربران جدید از موتورهای جستجو است.

اگر چک لیست‌های SEO وب سایت ها را مشاهده کنیم، می‌توانیم آن‌ها را در دو دسته‌ی کلی بهینه سازی درونی و برونی وب سایت در نظر بگیریم:
Off-Page Optimization
یا برونی ، که بیشتر بر دوش مشاوران سئو و خود مدیران وب سایت است.(link building ، فعالیت در شبکه اجتماعی و ...)
و اما در حوزه On-Page Optimization یا درونی که بخش‌های مهمی از آن وظیفه‌ی مابرنامه نویس‌ها است.(H1 Tag ، URL Naming ، Meta Tags ، عنوان صفحه و ...)
[البته عامل درونی بهینه سازی محتوا (Content Optimization)  که مهمترین عامل در الگوریتم‌های نسل جدید موتورهای جستجو و همچنین الگوریتم جدید گوگل+) به حساب می‌آید بر عهده مشاوران سئو و خود مدیران وب سایت می‌باشد]


در ادامه به ارائه چند راهکار جهت بهینه سازی برنامه‌های وب ASP.NET مان برای موتورهای جستجو می‌پردازیم:


1.متدی برای ایجاد عنوان سایت
    private const string SeparatorTitle = " - ";
    private const int MaxLenghtTitle = 60;
    public static string GeneratePageTitle(params string[] crumbs)
    {
        var title = "";

        for (int i = 0; i < crumbs.Length; i++)
        {
            title += string.Format
                        (
                            "{0}{1}",
                            crumbs[i],
                            (i < crumbs.Length - 1) ? SeparatorTitle : string.Empty
                        );
        }

        title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim();

        return title;
    }
نکته :
  • MaxLenghtTitle پیشنهادی برای عنوان سایت 60 می‌باشد.

2.متدی برای ایجاد متاتگ صفحات سایت
public enum CacheControlType
{
    [Description("public")]
    _public,
    [Description("private")]
    _private,
    [Description("no-cache")]
    _nocache,
    [Description("no-store")]
    _nostore
}  
private const int MaxLenghtTitle = 60; private const int MaxLenghtDescription = 170; private const string FaviconPath = "~/cdn/ui/favicon.ico"; public static string GenerateMetaTag(string title, string description, bool allowIndexPage, bool allowFollowLinks, string author = "", string lastmodified = "", string expires = "never", string language = "fa", CacheControlType cacheControlType = CacheControlType._private) { title = title.Substring(0, title.Length <= MaxLenghtTitle ? title.Length : MaxLenghtTitle).Trim(); description = description.Substring(0, description.Length <= MaxLenghtDescription ? description.Length : MaxLenghtDescription).Trim(); var meta = ""; meta += string.Format("<title>{0}</title>\n", title); meta += string.Format("<link rel=\"shortcut icon\" href=\"{0}\"/>\n", FaviconPath); meta += string.Format("<meta http-equiv=\"content-language\" content=\"{0}\"/>\n", language); meta += string.Format("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>\n"); meta += string.Format("<meta charset=\"utf-8\"/>\n"); meta += string.Format("<meta name=\"description\" content=\"{0}\"/>\n", description); meta += string.Format("<meta http-equiv=\"Cache-control\" content=\"{0}\"/>\n", EnumExtensions.EnumHelper<CacheControlType>.GetEnumDescription(cacheControlType.ToString())); meta += string.Format("<meta name=\"robots\" content=\"{0}, {1}\" />\n", allowIndexPage ? "index" : "noindex", allowFollowLinks ? "follow" : "nofollow"); meta += string.Format("<meta name=\"expires\" content=\"{0}\"/>\n", expires); if (!string.IsNullOrEmpty(lastmodified)) meta += string.Format("<meta name=\"last-modified\" content=\"{0}\"/>\n", lastmodified); if (!string.IsNullOrEmpty(author)) meta += string.Format("<meta name=\"author\" content=\"{0}\"/>\n", author); //------------------------------------Google & Bing Doesn't Use Meta Keywords ... //meta += string.Format("<meta name=\"keywords\" content=\"{0}\"/>\n", keywords); return meta; }
چند نکته :

3.متدی برای ایجاد Slug ( اسلاگ آدرسی با مفهوم برای بکار بردن در URL ها است که دوست‌دار موتورهای جستجو می‌باشد)
private const int MaxLenghtSlug = 45;
public static string GenerateSlug(string title)
{
        var slug = RemoveAccent(title).ToLower();
        slug = Regex.Replace(slug, @"[^a-z0-9-\u0600-\u06FF]", "-");
        slug = Regex.Replace(slug, @"\s+", "-").Trim();
        slug = Regex.Replace(slug, @"-+", "-");
        slug = slug.Substring(0, slug.Length <= MaxLenghtSlug ? slug.Length : MaxLenghtSlug).Trim();

        return slug;
}
    
private static string RemoveAccent(string text)
{
        var bytes = Encoding.GetEncoding("UTF-8").GetBytes(text);
        return Encoding.UTF8.GetString(bytes);
}
نکته :
  • MaxLenghtSlug پیشنهادی برای عنوان سایت 45 می‌باشد. 

نمونه ای از کاربرد توابع :
   Head.InnerHtml = SEO.GenerateMetaTag
                            (
                                title: SEO.GeneratePageTitle(".NET Tips", "آرشیو مطالب", "ASP.NET MVC #1"),
                                description: "چرا ASP.NET MVC با وجود فریم ورک پخته‌ای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح می‌شود این است: «برای چی؟». بنابراین تا به این سؤال پاسخ داده نشود، هر نوع بحث فنی در این مورد بی فایده است.",
                                allowIndexPage: true,
                                allowFollowLinks: true,
                                author: "وحید نصیری",
                                cacheControlType: SEO.CacheControlType._private
                            );
و خروجی در Page Source :
<title>.NET Tips - آرشیو مطالب - ASP.NET MVC #1</title>
<link rel="shortcut icon" href="../../cdn/images/ui/favicon.ico"/>
<meta http-equiv="content-language" content="fa"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<meta charset="utf-8"/>
<meta name="description" content="چرا ASP.NET MVC ؟با وجود فریم ورک پخته‌ای به نام ASP.NET web forms، اولین سؤالی که حین سوئیچ به ASP.NET MVC مطرح می‌شود این است: &#171;برای چی؟&#187;. بن ..."/>
<meta http-equiv="Cache-control" content="private"/>
<meta name="robots" content="index, follow" />
<meta name="expires" content="never"/>
<meta name="author" content="وحید نصیری"/>
موفق باشید
مطالب
شروع به کار با EF Core 1.0 - قسمت 2 - به روز رسانی ساختار بانک اطلاعاتی
پس از برپایی تنظیمات اولیه‌ی کار با 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 برای حذف این بانک اطلاعاتی موقتی استفاده کرد.



در قسمت بعد، مطالب تکمیلی مهاجرت‌ها را بررسی خواهیم کرد. برای مثال چگونه می‌توان کلاس‌های موجودیت‌ها را به اسمبلی‌های دیگری منتقل کرد.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی
- کار رندر اطلاعات در ASP.NET MVC از اکشن متد یک کنترلر شروع می‌شود و به View ختم خواهد شد. سفارشی سازی آن توسط اکشن‌فیلترها میسر است:
    public class FaLanguageActionFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            CultureInfo.CurrentCulture = new CultureInfo("fa-IR");
            CultureInfo.CurrentUICulture = new CultureInfo("fa-IR");
            base.OnActionExecuting(context);
        }
    }
برای توضیحات بیشتر و نحو‌ه‌ی ثبت سراسری آن‌ها مراجعه کنید به مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 20 - بررسی تغییرات فیلترها»
+ برای تنظیم اطلاعات تکراری Viewها می‌توان کلاس پایه آن‌ها را نیز سفارشی سازی کرد:
    public abstract class MyCustomBaseView<TModel> : RazorPage<TModel>
    {
        protected MyCustomBaseView()
        {
            CultureInfo.CurrentCulture = new CultureInfo("fa-IR");
            CultureInfo.CurrentUICulture = new CultureInfo("fa-IR");
        }
یک مثال تکمیلی
مطالب دوره‌ها
به روز رسانی خواص راهبری و مجموعه‌های Entity Framework توسط AutoMapper
فرض کنید مدل‌های بانک اطلاعاتی ما چنین ساختاری را دارند:
public abstract class BaseEntity
{
    public int Id { set; get; }
}

public class User : BaseEntity
{
        public string Name { set; get; }
 
        public virtual ICollection<Advertisement> Advertisements { get; set; }
}

public class Advertisement : BaseEntity
{
    public string Title { get; set; }
    public string Description { get; set; }
 
    [ForeignKey("UserId")]
    public virtual User User { get; set; }
    public int UserId { get; set; }
}
و همچنین مدل‌های رابط کاربری یا ViewModel‌های برنامه نیز به صورت ذیل تعریف شده‌اند:
public class AdvertisementViewModel
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int UserId { get; set; }
}
 
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public List<AdvertisementViewModel> Advertisements { get; set; }
}


به روز رسانی خواص راهبری Entity framework توسط AutoMapper

در کلاس‌های فوق، یک کاربر، تعدادی تبلیغات را می‌تواند ثبت کند. در این حالت اگر بخواهیم خاصیت User کلاس Advertisement را توسط AutoMapper به روز کنیم، با رعایت دو نکته، اینکار به سادگی انجام خواهد شد:
الف) همانطور که در کلاس Advertisement جهت تعریف کلید خارجی مشخص است، UserId نیز علاوه بر User ذکر شده‌است. این مورد کار نگاشت UserId اطلاعات دریافتی از کاربر را ساده کرده و در این حالت نیازی به یافتن اصل User این UserId از بانک اطلاعاتی نخواهد بود.
ب) چون در اطلاعات دریافتی از کاربر تنها Id او را داریم و نه کل شیء مرتبط را، بنابراین باید به AutoMapper اعلام کنیم تا از این خاصیت صرفنظر کند که اینکار توسط متد Ignore به نحو ذیل قابل انجام است:
this.CreateMap<AdvertisementViewModel, Advertisement>()
      .ForMember(advertisement => advertisement.Description, opt => opt.Ignore())
      .ForMember(advertisement => advertisement.User, opt => opt.Ignore());


به روز رسانی مجموعه‌های Entity Framework توسط AutoMapper

فرض کنید چنین اطلاعاتی از کاربر و رابط کاربری برنامه دریافت شده است:
var uiUser1 = new UserViewModel
{
    Id = 1,
    Name = "user 1",
    Advertisements = new List<AdvertisementViewModel>
    {
        new AdvertisementViewModel
        {
            Id = 1,
            Title = "Adv 1",
            UserId = 1
        },
        new AdvertisementViewModel
        {
            Id = 2,
            Title = "Adv 2",
            UserId = 1
        }
    }
};
اکنون می‌خواهیم معادل این رکورد را از بانک اطلاعاتی یافته و سپس اطلاعات آن‌را بر اساس اطلاعات UI به روز کنیم. شاید در نگاه اول چنین روشی پیشنهاد شود:
 var dbUser1 = ctx.Users.Include(user => user.Advertisements).First(x => x.Id == uiUser1.Id);
Mapper.Map(source: uiUser1, destination: dbUser1);
ابتدا کاربری را که Id آن مساوی uiUser1.Id است، یافته و سپس به AutoMapper اعلام می‌کنیم تا تمام اطلاعات آن‌را به صورت یکجا به روز کند. این نگاشت را نیز برای آن تعریف خواهیم کرد:
 this.CreateMap<UserViewModel, User>()
در یک چنین حالتی، ابتدا شیء user 1 از بانک اطلاعاتی دریافت شده (و با توجه به وجود Include، تمام تبلیغات او نیز دریافت می‌شوند)، سپس ... دو رکورد دریافتی از کاربر، کاملا جایگزین اطلاعات موجود می‌شوند. این جایگزینی سبب تخریب پروکسی‌های EF می‌گردند. برای مثال اگر پیشتر تبلیغی با Id=1 در بانک اطلاعاتی وجود داشته، اکنون با نمونه‌ی جدیدی جایگزین می‌شود که سیستم Tracking و ردیابی EF اطلاعاتی در مورد آن ندارد. به همین جهت اگر در این حالت ctx.SaveChanges فراخوانی شود، عملیات ثبت و یا به روز رسانی با شکست مواجه خواهد شد.

علت را در این دو تصویر بهتر می‌توان مشاهده کرد:



تصویر اول که مستقیما از بانک اطلاعاتی حاصل شده‌است، دارای پروکسی‌های EF است. اما در تصویر دوم، جایگزین شدن این پروکسی‌ها را مشاهده می‌کنید که سبب خواهد شد این اشیاء دیگر تحت نظارت EF نباشند.


راه حل:

در این مورد خاص باید به AutoMapper اعلام کنیم تا کاری با لیست تبلیغات کاربر دریافت شده‌ی از بانک اطلاعاتی نداشته باشد و آن‌را راسا جایگزین نکند:
this.CreateMap<UserViewModel, User>().ForMember(user => user.Advertisements, opt => opt.Ignore());
در اینجا متد Ignore را بر روی لیست تبلیغات کاربر بانک اطلاعاتی فراخوانی کرده‌ایم، تا اطلاعات آن پس از اولین نگاشت انجام شده‌ی توسط AutoMapper دست نخورده باقی بماند.
سپس کار ثبت یا به روز رسانی را به صورت نیمه خودکار مدیریت می‌کنیم:
using (var ctx = new MyContext())
{
    var dbUser1 = ctx.Users.Include(user => user.Advertisements).First(x => x.Id == uiUser1.Id);
    Mapper.Map(source: uiUser1, destination: dbUser1);
 
    foreach (var uiUserAdvertisement in uiUser1.Advertisements)
    {
        var dbUserAdvertisement = dbUser1.Advertisements.FirstOrDefault(ad => ad.Id == uiUserAdvertisement.Id);
        if (dbUserAdvertisement == null)
        {
            // Add new record
            var advertisement = Mapper.Map<AdvertisementViewModel, Advertisement>(uiUserAdvertisement);
            dbUser1.Advertisements.Add(advertisement);
        }
        else
        {
            // Update the existing record
            Mapper.Map(uiUserAdvertisement, dbUserAdvertisement);
        }
    }
 
    ctx.SaveChanges();
}
- در اینجا ابتدا db user معادل اطلاعات ui user از بانک اطلاعاتی، به همراه لیست تبلیغات او دریافت می‌شود و اطلاعات ابتدایی او نگاشت خواهند شد.
- سپس بر روی اطلاعات تبلیغات دریافتی از کاربر، یک حلقه را تشکیل خواهیم داد. در اینجا هربار بررسی می‌کنیم که آیا معادل این تبلیغ هم اکنون به شیء db user متصل است یا خیر؟ اگر متصل نبود، یعنی یک رکورد جدید است و باید Add شود. اگر متصل بود صرفا باید به روز رسانی صورت گیرد.
- برای حالت ایجاد شیء جدید بانک اطلاعاتی، بر اساس uiUserAdvertisement دریافتی، می‌توان از متد Mapper.Map استفاده کرد؛ خروجی این متد، یک شیء جدید تبلیغ است.
- برای حالت به روز رسانی اطلاعات db user موجود، بر اساس اطلاعات ارسالی کاربر نیز می‌توان از متد Mapper.Map کمک گرفت.


نکته‌ی مهم
چون در اینجا از متد Include استفاده شده‌است، فراخوانی‌های FirstOrDefault داخل حلقه، سبب رفت و برگشت اضافه‌تری به بانک اطلاعاتی نخواهند شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample04.zip
مطالب
C# 7 - More Expression-Bodied Members
یکی از امکانات جالب سی‌شارپ که در نسخه 6 معرفی شد، قابلیت Expression-Bodied Members بود. در نسخه 7 سی‌شارپ، امکانات جدیدتری اضافه شده است؛ به عنوان مثال اکنون می‌توان برای constructors, finalizers و همچنین get and set برای پراپرتی‌ها و ایندکسرها نیز از این قابلیت استفاده کرد.

 
استفاده از expression body برای constructors 
public class Person
{
    public string FirstName { get; set; }
    public Person(string firstName)
    {
        this.FirstName = firstName;
    }
}
به عنوان مثال اکنون سازنده‌ی کلاس فوق را می‌توانیم از روش block body متداول، به روش expression body، به صورت خلاصه‌تری بنویسیم:
public class Person
{
     public string FirstName { get; set; }
     public Person(string firstName) => this.FirstName = firstName;
}
البته محدودیت این روش این است که تنها برای یک پارامتر می‌توانیم به اینصورت عمل کنیم؛ اما در نسخه‌ 7.1  قرار است قابلیت استفاده از expression body برای بیشتر از یک پارامتر نیز اضافه شود:
public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age) => (Name, Age) = (name, age);
}

اما اگر نیاز داشتید برای بیشتر از دو متغیر از expression body استفاده کنید می‌توانید از Tuple برای شبیه‌سازی آن استفاده کنید(+):
public class Person
{
    private readonly (string name, int age) _tuple;    

    public string Name => _tuple.name;
    public int Age => _tuple.age;

    public Person(string name, int age) => _tuple = (name, age);
}

استفاده از expression body برای destructors 
public class Resource
{
    ~Resource() => Console.WriteLine("destructor");
}


 استفاده از expression body در get / set accessors 
 در سی‌شارپ 7 برای accessors نیز می‌توانیم از سینتکس جدید expression body استفاده کنیم. به عنوان مثال کد زیر را در نظر بگیرید:
private int _x;
public int X 
{
    get
    {
        return _x;
    }
    set
    {
        _x = value;
    }
}
کد فوق را می‌توانیم در سی‌شارپ 7 به صورت خلاصه‌تری بنویسیم:
private int _x;
public int X 
{
    get => _x;
    set => _x = value;
}

در ویژوال‌استودیوی 2017 نیز با قرار دادن ماوس بر روی پراپرتی x_، استفاده‌ی از سینتکس expression body به شما پیشنهاد داده خواهد شد:


همچنین برای Event Accessors نیز می‌توانیم از این قابلیت استفاده کنیم:

private EventHandler _someEvent;
public event EventHandler SomeEvent
{
    add => _someEvent += value;
    remove => _someEvent -= value;
}


مطالب
ASP.NET MVC #6

آشنایی با انواع ActionResult

در قسمت چهارم، اولین متد یا اکشنی که به صورت خودکار توسط VS.NET به برنامه اضافه شد، اینچنین بود:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

توضیحات تکمیلی مرتبط با خروجی از نوع ActionResult ایی را که مشاهده می‌کنید، در این قسمت ارائه خواهد شد.
رفتار یک کنترلر توسط متدهایی که در آن کلاس تعریف می‌شوند، مشخص می‌گردد. هر متد هم از طریق یک URL مجزا قابل دسترسی و فراخوانی خواهد بود. این متدها که به آن‌ها اکشن نیز گفته می‌شود باید عمومی بوده، استاتیک یا متد الحاقی (extension method) نباشند و همچنین دارای پارامترهایی از نوع ref و out نیز نباشند.
هر درخواست رسیده، به یک کنترلر و متدی عمومی در آن توسط سیستم مسیریابی، نگاشت خواهد شد. اگر علاقمند باشید که در یک کلاس کنترلر، متدی عمومی را از این سیستم خارج کنید، تنها کافی است آن‌را با ویژگی (attribute) به نام NonAction مزین کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class HomeController : Controller
{
[NonAction]
public string ShowData()
{
return "Text";
}

public ActionResult Index()
{
ViewBag.Message = string.Format("{0}/{1}/{2}",
RouteData.Values["controller"],
RouteData.Values["action"],
RouteData.Values["id"]);
return View();
}

public ActionResult Search(string data = "*")
{
// do something ...
return View();
}
}
}

چند نکته در این مثال قابل ذکر است:
الف) در اینجا اگر شخصی آدرس http://localhost/home/showdata را درخواست نماید، با توجه به استفاده از ویژگی NonAction، با پیغام یافت نشد یا 404 مواجه می‌گردد.
ب) صرفنظر از پارامترهای یک متد و ساختار کلاس جاری، اطلاعات مسیریابی از طریق شیء RouteData.Values نیز در دسترس هستند که نمونه‌ای از آن‌را در اینجا بر اساس مقادیر پیش فرض تعاریف مسیریابی یک پروژه ASP.NET MVC ملاحظه می‌نمائید.
ج) در متد Search، از قابلیت امکان تعریف مقداری پیش فرض جهت آرگومان‌ها در سی شارپ 4 استفاده شده است. به این ترتیب اگر شخصی آدرس http://localhost/home/search را وارد کند، چون پارامتری را ذکر نکرده است، به صورت خودکار از مقدار پیش فرض آرگومان data استفاده می‌گردد.


انواع Action Results در ASP.NET MVC

در ASP.NET MVC بجای استفاده مستقیم از شیء Response، از شیء ActionResult جهت ارائه خروجی یک متد استفاده می‌شود و مهم‌ترین دلیل آن هم مشکل بودن نوشتن آزمون‌های واحد برای شیء Response است که وهله سازی آن مساوی است با به کار اندازی موتور ASP.NET و Http Runtime آن توسط یک وب سرور (بنابراین در ASP.NET MVC سعی کنید شیء Response را فراموش کنید).
سلسه مراتب ActionResult‌های قابل استفاده در ASP.NET در تصویر زیر مشخص شده‌اند:


و در مثال زیر تقریبا انواع و اقسام ActionResult‌های مهم و کاربردی ASP.NET MVC را می‌توانید مشاهده کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class ActionResultsController : Controller
{
//http://localhost/actionresults/welcome
public string Welcome()
{
return "Hello, World";
}

//http://localhost/actionresults/index
public ActionResult Index() // or ContentResult
{
return Content("Hello, World");
}

//http://localhost/actionresults/SendMail
public void SendMail()
{
}

public ActionResult SendMailCompleted() // or EmptyResult
{
// do whatever
return new EmptyResult();
}

public ActionResult GetFile() // or FilePathResult
{
return File(Server.MapPath("~/content/site.css"), "text/css", "mySite.css");
}

public ActionResult UnauthorizedStatus() // or HttpStatusCodeResult/HttpUnauthorizedResult
{
return new HttpUnauthorizedResult("You need to login first.");
}

public ActionResult Status() // or HttpStatusCodeResult
{
return new HttpStatusCodeResult(501, "Server Error");
}

public ActionResult GetJavaScript() // or JavaScriptResult
{
return JavaScript("...JavaScript...");
}

public ActionResult GetJson() // or JsonResult
{
var obj = new { prop1 = 1, prop2 = "data" };
return Json(obj, JsonRequestBehavior.AllowGet);
}

public ActionResult RedirectTo() // or RedirectResult
{
return RedirectPermanent("http://www.site.com");
//return RedirectToAction("Home", "Index");
}

public ActionResult ShowView() // or ViewResult
{
return View();
}
}
}

چند نکته در این مثال وجود دارد:
1) مثلا متد GetJavaScript را درنظر بگیرید. در این متد خاص، چه بنویسید public ActionResult GetJavaScript یا بنویسید public JavaScriptResult GetJavaScript تفاوتی نمی‌کند. در سایر موارد هم به همین ترتیب است. علت را در تصویر سلسله مراتبی ActionResult‌ها می‌توان جستجو کرد. تمام این کلاس‌ها نوعی ActionResult هستند و از یک کلاس پایه به ارث رسیده‌اند.
2) مثلا ContentResult شبیه به همان Response.Write سابق ASP.NET عمل می‌کند. علت وجودی آن هم عدم وابستگی مستقیم به شیء Response و ساده‌تر سازی نوشتن آزمون‌های واحد برای این نوع اکشن متدها است.
3) منهای متد آخری که نمایش داده شده (ShowView)، هیچکدام از متدهای دیگر نیازی به View متناظر ندارند. یعنی نیازی نیست تا روی متد کلیک راست کرده و Add view را انتخاب کنیم. چون در همین متد کنترلر، کار Response به پایان می‌رسد و مرحله بعدی ندارد. مثلا در حالت return File، یک فایل به درون مرورگر کاربر Flush خواهد شد و تمام.
4) متد Welcome و متد Index در اینجا به یک صورت تفسیر می‌شوند. به این معنا که اگر خروجی متد تعریف شده در یک کنترلر از نوع ActionResult نباشد، به صورت پیش فرض درون یک ContentResult محصور خواهد شد.
5) اگر خروجی متدی در اینجا از نوع void باشد، با ActionResult ایی به نام EmptyResult یکسان خواهد بود. بنابراین با متدهای SendMail و SendMailCompleted به یک نحو رفتار می‌گردد.
6) return Json یاد شده که خروجی‌اش از نوع JsonResultاست در پیاده سازی‌های Ajax ایی کاربرد دارد.
7) جهت بازگرداندن حالت وضعیت 403 یا غیرمجاز می‌توان از return new HttpUnauthorizedResult استفاده کرد.
8) یا جهت اعلام مشکلی در سمت سرور به کمک return new HttpStatusCodeResultکد ویژه‌ای را می‌توان به کاربر نمایش داد.
9) به کمک return RedirectToAction می‌توان به یک کنترلر و متدی خاص در آن، کاربر را هدایت کرد.

و خلاصه اینکه تمام کارهایی را که پیشتر در ASP.NET Web forms ، مستقیما به کمک شیء Response انجام می‌دادید (Response.Write، Response.End، Response.Redirect و غیره)، اینبار به کمک یکی از ActionResult‌های یاد شده انجام دهید تا بتوان بدون نیاز به راه اندازی یک وب سرور، برای متدهای کنترلرها آزمون واحد نوشت. برای مثال:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var controller = new ActionResultsController();

    // Act
    var result = controller.Index() as ContentResult;

    // Assert
    Assert.NotNull(result);
    Assert.AreEqual( "Hello, World", result.Content);
}