پاسخ به بازخورد‌های پروژه‌ها
خطای هم نامی خواص راهبری
public interface IFoo
{
        IPerson Person
        {
            get;
        }

        ICompany Company
        {
            get;
        }

        DateTime Date
        {
            get;
        }

        long NormalShareCount
        {
            get;
        }
}

public interface IPerson
{
    string Name{get;}
}

public interface ICompany
{
   string Name{get;}
}
نظرات مطالب
WF:Windows Workflow #۶
 می توانید پروژه wf را به صورت WCF WorkFlow Service Application در Solution مورد نظر اضافه کنید پس از ان سرویس را بر روی ویندوز سرور هاست کنید به کمک برنامه AppFabric که می‌توانید ان را از لینک زیر دانلود کنید .
روش دیگر این است که شما مستقیما از کلاس‌های WF در پروژه خود استفاده کنید و Activity‌های خود را تولید کنید بدون اینکه احتیاج به Model Designer داشته باشید مانند کد زیر:
namespace LeadGenerator
{

public sealed class CreateLead : CodeActivity
{
public InArgument<string> ContactName { get; set; }
public InArgument<string> ContactPhone { get; set; }
public InArgument<string> Interests { get; set; }
public InArgument<string> Notes { get; set; }
public InArgument<string> ConnectionString { get; set; }
public OutArgument<Lead> Lead { get; set; }
protected override void Execute(CodeActivityContext context)
{
// Create a Lead class and populate it with the input arguments
Lead l = new Lead();
l.ContactName = ContactName.Get(context);
l.ContactPhone = ContactPhone.Get(context);
l.Interests = Interests.Get(context);
l.Comments = Notes.Get(context);
l.WorkflowID = context.WorkflowInstanceId;
l.Status = "Open";
// Insert a record into the Lead table
LeadDataDataContext dc =
new LeadDataDataContext(ConnectionString.Get(context));
dc.Leads.InsertOnSubmit(l);
dc.SubmitChanges();
// Store the request in the OutArgument
Lead.Set(context, l);
}
}
}

مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت سوم - بهبود کیفیت جستجوهای FTS توسط یک غلط یاب املایی
فرض کنید کاربری برای جستجوی رکورد زیر:
context.Chapters.Add(new Chapter
{
    Title = "آزمایش متن فارسی",
    Text = "برای نمونه تهیه شده‌است",
    User = user1.Entity
});
بجای «فارسی»، واژه‌ی «فارشی» را وارد کند و یا بجای «آزمایش»، بنویسد «آزمایس». در هر دو حالت نتیجه‌ی جستجوی او خروجی را به همراه نخواهد داشت. برای بهبود تجربه‌ی کاربری جستجوی تمام متنی SQLite، افزونه‌ای به نام spell fix1 برای آن تهیه شده‌است که بر اساس توکن‌های ایندکس شده‌ی FTS، یک واژه‌نامه، تشکیل می‌شود و سپس بر اساس الگوریتم‌های غلط‌یابی املایی آن، از این توکن‌های از پیش موجود که واقعا در فیلدهای متنی بانک اطلاعاتی جاری وجود خارجی دارند، نزدیک‌ترین واژه‌های ممکن را پیشنهاد می‌کند تا بتوان بر اساس آن‌ها، جستجوی دقیق‌تری را ارائه کرد.


کامپایل افزونه‌ی spell fix1

افزونه‌ی spell fix، به همراه هیچکدام از توزیع‌های باینری SQLite ارائه نمی‌شود. ارائه‌ی آن فقط به صورت سورس کد است و باید خودتان آن‌را کامپایل کنید!


برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آن‌را دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحه‌ی بعدی، لینک download آن هم وجود دارد.

سپس به صفحه‌ی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بسته‌ی amalgamation آن‌را دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونه‌های آن قرار گیرند تا بتوان این افزونه‌ها را کامپایل کرد. بنابراین پس از دریافت بسته‌ی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشه‌ی آن کپی کنید:


اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آن‌را به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای می‌توانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بسته‌ی codeblocks-20.03mingw-setup.exe را دریافت کنید (بسته‌ای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin می‌توانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن می‌توان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
 "C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll


روش معرفی افزونه‌های SQLite به Microsoft.Data.Sqlite

EF Core، از بسته‌ی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده می‌کند و در اینجا هم برای معرفی افزونه‌ی کامپایل شده، باید ابتدا آن‌را به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونه‌هایش را بر اساس معرفی مستقیم مسیر فایل dll آن‌ها بارگذاری نمی‌کند. بلکه path ویندوز را برای جستجوی آن‌ها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آن‌را بارگذاری می‌کند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا می‌توان توسط قطعه کد زیر، آن‌را به صورت پویایی معرفی کرد:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class LoadSqliteExtensions
    {
        public static void AddToSystemPath(string extensionsDirectory)
        {
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app.");
            }

            var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator));
            if (path.Add(extensionsDirectory))
            {
                Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path));
            }
        }
    }
}
در این متد extensionsDirectory، همان پوشه‌ای است که فایل dll کامپایل شده، در آن قرار دارد. مابقی آن، معرفی این مسیر به صورت پویا به PATH سیستم عامل است.

در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشه‌ی افزونه‌ها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونه‌ی spellfix1 به آن معرفی شده‌است:
LoadSqliteExtensions.AddToSystemPath("path to .dll file");
services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
    {
        var connection = new SqliteConnection(connectionString);
        connection.Open();

        connection.LoadExtension("spellfix1");
        // Passing in an already open connection will keep the connection open between requests.
        optionsBuilder.UseSqlite(connection);
    });
همانطور که عنوان شد، متد LoadExtension، مسیری را دریافت نمی‌کند. این متد فقط نام افزونه را دریافت می‌کند و مسیر آن‌را از PATH سیستم عامل می‌خواند.


ایجاد جداول ویژه‌ی spell fix در برنامه

در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونه‌ی spell fix به صورت زیر تکمیل می‌کنیم:
        private static void createFtsTables(ApplicationDbContext context)
        {
            // For SQLite FTS
            // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
            context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
                                    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");

            // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ...
            // EditCost for unicode support
            context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');");
            context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);");
            context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);");
        }
- اگر در حین اجرای این دستورات خطای «no such module: spellfix1» را دریافت کردید، یعنی متد LoadExtension را به درستی فراخوانی نکرده‌اید.
- همانطور که مشاهده می‌کنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد می‌شود. کار آن استخراج توکن‌های FTS و آماده سازی آن‌ها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژه‌ی EditCost را مشاهده می‌کنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار می‌کند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده می‌کند، ایجاد شده‌است.

اجرای این دستورات، جداول زیر را ایجاد می‌کنند (که ساختار آن‌ها استاندارد است و باید مطابق فرمول‌های مستندات آن‌ها باشد):



به روز رسانی جدول واژه نامه‌ی غلط یابی برنامه

آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت می‌کند:


  هر بار که بانک اطلاعاتی را به روز می‌کنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank)
    SELECT term, cnt FROM Chapters_FTS_Vocab
    WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
البته خود SQLite اطلاعات این جدول را فقط یکبار بارگذاری می‌کند. برای اجبار آن به بارگذاری مجدد، می‌توان دستور reset زیر را صادر کرد:
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");


کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix

تا اینجا افزونه‌ی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامه‌ی آن‌را نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئری‌های آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities
{
    public class SpellCheck
    {
        public string Word { get; set; }
        public decimal Rank { get; set; }
        public decimal Distance { get; set; }
        public decimal Score { get; set; }
        public decimal Matchlen { get; set; }
    }
}
و آن‌را توسط متد HasNoKey به EF Core معرفی می‌کنیم:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        //...

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<SpellCheck>().HasNoKey().ToView(null);
        }

        //...
    }
}
در اینجا SpellCheck تهیه شده با متد HasNoKey علامتگذاری می‌شود تا آن‌را بتوان بدون مشکل در کوئری‌های EF استفاده کرد. همچنین فراخوانی ToView(null) سبب می‌شود تا EF Core جدولی را در حین Migration از روی این موجودیت ایجاد نکند و آن‌را به همین حال رها کند.

در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw(
          @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix
            WHERE word MATCH {0} and top=6", "فارشی"))
{
    Console.WriteLine($"Word: {item.Word}");
    Console.WriteLine($"Distance: {item.Distance}");
}
با این خروجی:


top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.

یک نکته: اگر می‌خواهید کوئری فوق را توسط برنامه‌ی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینه‌ی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.


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

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

namespace jQueryMvcSample07.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}

using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample07.Models;
using jQueryMvcSample07.Security;

namespace jQueryMvcSample07.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View(); //نمایش یک منوی ساده در ابتدای کار
        }

        [HttpGet]
        public ActionResult ShowSynchronous()
        {
            var model = getModel();
            return View(model); //نمایش همزمان
        }

        private static BlogPost getModel()
        {
            //شبیه سازی یک عملیات طولانی
            System.Threading.Thread.Sleep(3000);
            var model = new BlogPost
            {
                Title = "عنوان ... ",
                Body = "مطلب... "
            };
            return model;
        }

        [HttpGet]
        public ActionResult ShowAsynchronous()
        {
            return View(); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult RenderAsynchronous()
        {
            //دریافت اطلاعات به صورت غیرهمزمان
            var model = getModel();
            return PartialView(viewName: "_Post", model: model);
        }
    }
}
مدل برنامه، بیانگر ساختار اطلاعات مطلبی است که قرار است نمایش یابد.
در کنترلر Home، ابتدا اکشن متد Index آن فراخوانی شده و در این حالت دو لینک زیر نمایش داده می‌شوند:
@{
    ViewBag.Title = "Index";
}
<h2>
    نمایش اطلاعات به صورت همزمان و غیرهمزمان</h2>
<ul>
    <li>
        @Html.ActionLink(linkText: "نمایش همزمان", actionName: "ShowSynchronous", controllerName: "Home")
    </li>
    <li>
        @Html.ActionLink(linkText: "نمایش غیر همزمان", actionName: "ShowAsynchronous", controllerName: "Home")
    </li>
</ul>
لینک اول، به اکشن متد ShowSynchronous اشاره می‌کند و لینک دوم به اکشن متد ShowAsynchronous.
در هر دو صفحه نهایتا از یک Partial View به نام _Post.cshtml برای نمایش اطلاعات استفاده خواهد شد:
@model jQueryMvcSample07.Models.BlogPost
<fieldset>
    <legend>@Model.Title</legend>
    @Model.Body
</fieldset>
زمانیکه کاربر بر روی لینک نمایش همزمان کلیک می‌کند، به صفحه زیر هدایت می‌شود:
@model jQueryMvcSample07.Models.BlogPost
@{
    ViewBag.Title = "ShowSynchronous";
}

<h2>نمایش همزمان</h2>
@{ Html.RenderPartial("_Post", Model); }
این صفحه، یک صفحه متداول است و اطلاعات آن دقیقا در زمان نمایش صفحه اخذ شده و چون در اینجا از یک Sleep عمدی برای تولید اطلاعات استفاده گردیده است، نمایش آن حداقل سه ثانیه طول خواهد کشید.
در حالتیکه کاربر بر روی لینک نمایش غیرهمزمان کلیک می‌کند، صفحه زیر را مشاهده خواهد کرد:
@{
    ViewBag.Title = "ShowAsynchronous";
    var loadInfoUrl = Url.Action(actionName: "RenderAsynchronous", controllerName: "Home");
}
<h2>
    نمایش غیر همزمان</h2>
<div id="info" align="center">
</div>
<div id="progress" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#progress").css("display", "block");
            $.ajax({
                type: "POST",
                url: '@loadInfoUrl',
                complete: function (xhr, status) {
                    var data = xhr.responseText;
                    if (xhr.status == 403) {
                        window.location = "/login";
                    }
                    else if (status === 'error' || !data || data == "nok") {
                        alert('خطایی رخ داده است');
                    }
                    else {
                        $("#progress").css("display", "none");
                        $("#info").html(data);
                    }
                }
            });
        });
    </script>
}
نمایش ابتدایی این صفحه بسیار سریع است. در ابتدای کار progress bar ایی فعال شده و سپس از طریق jQuery Ajax درخواست دریافت اطلاعات رندر شده اکشن متدی به نام RenderAsynchronous به سرور ارسال می‌شود. چون عملیات Ajax غیرهمزمان است، کاربر نیازی نیست تا رندر شدن کامل صفحه ابتدا صبر کند و سپس کل صفحه به او نمایش داده شود. در اینجا ابتدا صفحه به صورت کامل نمایان شده و سپس درخواستی Ajax ایی به سرور ارسال می‌گردد. در پایان عملیات، Partial View یاد شده رندر گردیده و در div ایی با id مساوی info نمایش داده می‌شود.
به این ترتیب می‌توان حس سریع بودن سایت را زمانیکه قسمتی از صفحه نیاز به زمان بیشتری برای نمایش اطلاعات دارد، به کاربر منتقل کرد.

دریافت پروژه کامل این قسمت
jQueryMvcSample07.zip 
نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
اگر خاصیتی را به صورت زیر تعریف کرده باشید:
public string Summary { get; private set; }
در نگارش 5، باید حتما به همراه ویژگی [JsonInclude] هم باشد تا به درستی serialize و deserialize شود. همچنین ذکر private set هم ضروری است. نگارش 5، خواصی را که فقط به صورت get دار یا read-only تنظیم شده باشند، deserialize نمی‌کند.
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت پنجم
مقدمات ساخت بلاگ مبتنی بر ember.js در قسمت قبل به پایان رسید. در این قسمت صرفا قصد داریم بجای استفاده از HTML 5 local storage از یک REST web service مانند یک ASP.NET Web API Controller و یا یک ASP.NET MVC Controller استفاده کنیم و اطلاعات نهایی را به سرور ارسال و یا از آن دریافت کنیم.


تنظیم Ember data برای کار با سرور

Ember data به صورت پیش فرض و در پشت صحنه با استفاده از Ajax برای کار با یک REST Web Service طراحی شده‌است و کلیه تبادلات آن نیز با فرمت JSON انجام می‌شود. بنابراین تمام کدهای سمت کاربر قسمت قبل نیز در این حالت کار خواهند کرد. تنها کاری که باید انجام شود، حذف تنظیمات ابتدایی آن برای کار با HTML 5 local storage است.
برای این منظور ابتدا فایل index.html را گشوده و سپس مدخل localstorage_adapter.js را از آن حذف کنید:
 <!--<script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script>-->
همچنین دیگر نیازی به store.js نیز نمی‌باشد:
 <!--<script src="Scripts/App/store.js" type="text/javascript"></script>-->

اکنون برنامه را اجرا کنید، چنین پیام خطایی را مشاهده خواهید کرد:

همانطور که عنوان شد، ember data به صورت پیش فرض با سرور کار می‌کند و در اینجا به صورت خودکار، یک درخواست Get را به آدرس http://localhost:25918/posts جهت دریافت آخرین مطالب ثبت شده، ارسال کرده‌است و چون هنوز وب سرویسی در برنامه تعریف نشده، با خطای 404 و یا یافت نشد، مواجه شده‌است.
این درخواست نیز بر اساس تعاریف موجود در فایل Scripts\Routes\posts.js، به سرور ارسال شده‌است:
Blogger.PostsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('post');
    }
});
Ember data شبیه به یک ORM عمل می‌کند. تنظیمات ابتدایی آن‌را تغییر دهید، بدون نیازی به تغییر در کدهای اصلی برنامه، می‌تواند با یک منبع داده جدید کار کند.


تغییر تنظیمات پیش فرض آغازین Ember data

آدرس درخواستی http://localhost:25918/posts به این معنا است که کلیه درخواست‌ها، به همان آدرس و پورت ریشه‌ی اصلی سایت ارسال می‌شوند. اما اگر یک ASP.NET Web API Controller را تعریف کنیم، نیاز است این درخواست‌ها، برای مثال به آدرس api/posts ارسال شوند؛ بجای /posts.
برای این منظور پوشه‌ی جدید Scripts\Adapters را ایجاد کرده و فایل web_api_adapter.js را با این محتوا به آن اضافه کنید:
 DS.RESTAdapter.reopen({
      namespace: 'api'
});
سپس تعریف مدخل آن‌را نیز به فایل index.html اضافه نمائید:
 <script src="Scripts/Adapters/web_api_adapter.js" type="text/javascript"></script>
تعریف فضای نام در اینجا سبب خواهد شد تا درخواست‌های جدید به آدرس api/posts ارسال شوند.


تغییر تنظیمات پیش فرض ASP.NET Web API

در سمت سرور، بنابر اصول نامگذاری خواص، نام‌ها با حروف بزرگ شروع می‌شوند:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
اما در سمت کاربر و کدهای اسکریپتی، عکس آن صادق است. به همین جهت نیاز است که CamelCasePropertyNamesContractResolver را در JSON.NET تنظیم کرد تا به صورت خودکار اطلاعات ارسالی به کلاینت‌ها را به صورت camel case تولید کند:
using System;
using System.Web.Http;
using System.Web.Routing;
using Newtonsoft.Json.Serialization;
 
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}


نحوه‌ی صحیح بازگشت اطلاعات از یک ASP.NET Web API جهت استفاده در Ember data

با تنظیمات فوق، اگر کنترلر جدیدی را به صورت ذیل جهت بازگشت لیست مطالب تهیه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public IEnumerable<Post> Get()
        {
            return DataSource.PostsList;
        } 
    }
}
با یک چنین خطایی در سمت کاربر مواجه خواهیم شد:
  WARNING: Encountered "0" in payload, but no model was found for model name "0" (resolved model name using DS.RESTSerializer.typeForRoot("0"))
این خطا از آنجا ناشی می‌شود که Ember data، اطلاعات دریافتی از سرور را بر اساس قرارداد JSON API دریافت می‌کند. برای حل این مشکل راه‌حل‌های زیادی مطرح شده‌اند که تعدادی از آن‌ها را در لینک‌های زیر می‌توانید مطالعه کنید:
http://jsonapi.codeplex.com
https://github.com/xqiu/MVCSPAWithEmberjs
https://github.com/rmichela/EmberDataAdapter
https://github.com/MilkyWayJoe/Ember-WebAPI-Adapter
http://blog.yodersolutions.com/using-ember-data-with-asp-net-web-api
http://emadibrahim.com/2014/04/09/emberjs-and-asp-net-web-api-and-json-serialization

و خلاصه‌ی آن‌ها به این صورت است:
خروجی JSON تولیدی توسط ASP.NET Web API چنین شکلی را دارد:
[
  {
    Id: 1,
    Title: 'First Post'
  }, {
    Id: 2,
    Title: 'Second Post'
  }
]
اما Ember data نیاز به یک چنین خروجی دارد:
{
  posts: [{
    id: 1,
    title: 'First Post'
  }, {
    id: 2,
    title: 'Second Post'
  }]
}
به عبارتی آرایه‌ی مطالب را از ریشه‌ی posts باید دریافت کند (مطابق فرمت JSON API). برای انجام اینکار یا از لینک‌های معرفی شده استفاده کنید و یا راه حل ساده‌ی ذیل هم پاسخگو است:
using System.Web.Http;
using EmberJS03.Models;
 
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public object Get()
        {
            return new { posts = DataSource.PostsList };
        }
    }
}
در اینجا ریشه‌ی posts را توسط یک anonymous object ایجاد کرده‌ایم.
اکنون اگر برنامه را اجرا کنید، در صفحه‌ی اول آن، لیست عناوین مطالب را مشاهده خواهید کرد.


تاثیر قرارداد JSON API در حین ارسال اطلاعات به سرور توسط Ember data

در تکمیل کنترلرهای Web API مورد نیاز (کنترلرهای مطالب و نظرات)، نیاز به متدهای Post، Update و Delete هم خواهد بود. دقیقا فرامین ارسالی توسط Ember data توسط همین HTTP Verbs به سمت سرور ارسال می‌شوند. در این حالت اگر متد Post کنترلر نظرات را به این شکل طراحی کنیم:
 public HttpResponseMessage Post(Comment comment)
کار نخواهد کرد؛ چون مطابق فرمت JSON API ارسالی توسط Ember data، یک چنین شیء JSON ایی را دریافت خواهیم کرد:
{"comment":{"text":"data...","post":"3"}}
بنابراین Ember data چه در حین دریافت اطلاعات از سرور و چه در زمان ارسال اطلاعات به آن، اشیاء جاوا اسکریپتی را در یک ریشه‌ی هم نام آن شیء قرار می‌دهد.
برای پردازش آن، یا باید از راه حل‌های ثالث مطرح شده در ابتدای بحث استفاده کنید و یا می‌توان مطابق کدهای ذیل، کل اطلاعات JSON ارسالی را توسط کتابخانه‌ی JSON.NET نیز پردازش کرد:
namespace EmberJS03.Controllers
{
    public class CommentsController : ApiController
    {
        public HttpResponseMessage Post(HttpRequestMessage requestMessage)
        {
            var jsonContent = requestMessage.Content.ReadAsStringAsync().Result;
            // {"comment":{"text":"data...","post":"3"}}
            var jObj = JObject.Parse(jsonContent);
            var comment = jObj.SelectToken("comment", false).ToObject<Comment>();


            var id = 1;
            var lastItem = DataSource.CommentsList.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            comment.Id = id;
            DataSource.CommentsList.Add(comment);

            // ارسال آی دی با فرمت خاص مهم است
            return Request.CreateResponse(HttpStatusCode.Created, new { comment = comment });
        }
    }
}
در اینجا توسط requestMessage به محتوای ارسال شده‌ی به سرور که همان شیء JSON ارسالی است، دسترسی خواهیم داشت. سپس متد JObject.Parse، آن‌را به صورت عمومی تبدیل به یک شیء JSON می‌کند و نهایتا با استفاده از متد SelectToken آن می‌توان ریشه‌ی comment و یا در کنترلر مطالب، ریشه‌ی post را انتخاب و سپس تبدیل به شیء Comment و یا Post کرد.
همچنین فرمت return نهایی هم مهم است. در این حالت خروجی ارسالی به سمت کاربر، باید مجددا با فرمت JSON API باشد؛ یعنی باید comment اصلاح شده را به همراه ریشه‌ی comment ارسال کرد. در اینجا نیز anonymous object تهیه شده، چنین کاری را انجام می‌دهد.


Lazy loading در Ember data

تا اینجا اگر برنامه را اجرا کنید، لیست مطالب صفحه‌ی اول را مشاهده خواهید کرد، اما لیست نظرات آن‌ها را خیر؛ از این جهت که ضرورتی نداشت تا در بار اول ارسال لیست مطالب به سمت کاربر، تمام نظرات متناظر با آن‌ها را هم ارسال کرد. بهتر است زمانیکه کاربر یک مطلب خاص را مشاهده می‌کند، نظرات خاص آن‌را به سمت کاربر ارسال کنیم.
در تعاریف سمت کاربر Ember data، پارامتر دوم رابطه‌ی hasMany که با async:true مشخص شده‌است، دقیقا معنای lazy loading را دارد.
Blogger.Post = DS.Model.extend({
   title: DS.attr(),
   body: DS.attr(),
   comments: DS.hasMany('comment', { async: true } /* lazy loading */)
});
در سمت سرور، دو راه برای فعال سازی این lazy loading تعریف شده در سمت کاربر وجود دارد:
الف) Idهای نظرات هر مطلب را به صورت یک آرایه، در بار اول ارسال لیست نظرات به سمت کاربر، تهیه و ارسال کنیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // lazy loading via an array of IDs
        public int[] Comments { set; get; } 
    }
}
در اینجا خاصیت Comments، تنها کافی است لیستی از Idهای نظرات مرتبط با مطلب جاری باشد. در این حالت در سمت کاربر اگر مطلب خاصی جهت مشاهده‌ی جزئیات آن انتخاب شود، به ازای هر Id ذکر شده، یکبار دستور Get صادر خواهد شد.
ب) این روش به علت تعداد رفت و برگشت بیش از حد به سرور، کارآیی آنچنانی ندارد. بهتر است جهت مشاهده‌ی جزئیات یک مطلب، تنها یکبار درخواست Get کلیه نظرات آن صادر شود.
برای اینکار باید مدل برنامه را به شکل زیر تغییر دهیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // load related models via URLs instead of an array of IDs
        // ref. https://github.com/emberjs/data/pull/1371
        public object Links { set; get; }
 
        public Post()
        {
            Links = new { comments = "comments" }; // api/posts/id/comments
        }
    }
}
در اینجا یک خاصیت جدید به نام Links ارائه شده‌است. نام Links در Ember data استاندارد است و از آن برای دریافت کلیه اطلاعات لینک شده‌ی به یک مطلب استفاده می‌شود. با تعریف این خاصیت به نحوی که ملاحظه می‌کنید، اینبار Ember data تنها یکبار درخواست ویژه‌ای را با فرمت api/posts/id/comments، به سمت سرور ارسال می‌کند. برای مدیریت آن، قالب مسیریابی پیش فرض {api/{controller}/{id را می‌توان به صورت {api/{controller}/{id}/{name اصلاح کرد:
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}/{name}",
               defaults: new { id = RouteParameter.Optional, name = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}
اکنون دیگر درخواست جدید api/posts/3/comments با پیام 404 یا یافت نشد مواجه نمی‌شود.
در این حالت در طی یک درخواست می‌توان کلیه نظرات را به سمت کاربر ارسال کرد. در اینجا نیز ذکر ریشه‌ی comments همانند ریشه posts، الزامی است:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        //GET api/posts/id
        public object Get(int id)
        {
            return
                new
                {
                    posts = DataSource.PostsList.FirstOrDefault(post => post.Id == id),
                    comments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList()
                };
        }
    }
}


پردازش‌های async و متد transitionToRoute در Ember.js

اگر متد حذف مطالب را نیز به کنترلر Posts اضافه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = DataSource.PostsList.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            DataSource.PostsList.Remove(item);

            //حذف کامنت‌های مرتبط
            var relatedComments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList();
            relatedComments.ForEach(comment => DataSource.CommentsList.Remove(comment));

            return Request.CreateResponse(HttpStatusCode.OK, new { post = item });
        }
    }
}
قسمت سمت سرور کار تکمیل شده‌است. اما در سمت کاربر، چنین خطایی را دریافت خواهیم کرد:
 Attempted to handle event `pushedData` on  while in state root.deleted.inFlight.
منظور از حالت inFlight در اینجا این است که هنوز کار حذف سمت سرور تمام نشده‌است که متد transitionToRoute را صادر کرده‌اید. برای اصلاح آن، فایل Scripts\Controllers\post.js را باز کرده و پس از متد destroyRecord، متد then را قرار دهید:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();
 
            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                var thisController = this;
                var post = this.get('model');
                post.destroyRecord().then(function () {
                    thisController.transitionToRoute('posts');
                });
            }
        }
    }
});
به این ترتیب پس از پایان عملیات حذف سمت سرور، قسمت then اجرا خواهد شد. همچنین باید دقت داشت که this اشاره کننده به کنترلر جاری را باید پیش از فراخوانی then ذخیره و استفاده کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_05.zip
مطالب
آشنایی با NHibernate - قسمت سوم

در ادامه، تعاریف سایر موجودیت‌های سیستم ثبت سفارشات و نگاشت آن‌ها را بررسی خواهیم کرد.

کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:

namespace NHSample1.Domain
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool Discontinued { get; set; }
}
}
کلاس ProductMapping تعریف شده در فایل جدید ProductMapping.cs (توصیه شده است که به ازای هر کلاس یک فایل جداگانه در نظر گرفته شود)، در پوشه Mappings برنامه:

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class ProductMapping : ClassMap<Product>
{
public ProductMapping()
{
Not.LazyLoad();
Id(p => p.Id).GeneratedBy.HiLo("1000");
Map(p => p.Name).Length(50).Not.Nullable();
Map(p => p.UnitPrice).Not.Nullable();
Map(p => p.Discontinued).Not.Nullable();
}
}
}
همانطور که ملاحظه می‌کنید، روش تعریف آن‌ها همانند شیء Customer است که در قسمت‌های قبل بررسی شد و نکته جدیدی ندارد.
آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آن‌را در قسمت قبل مشخص کردیم) خواهیم افزود:

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class ProductMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_product()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.Name, "Apples")
.CheckProperty(p => p.UnitPrice, 10.45m)
.CheckProperty(p => p.Discontinued, true)
.VerifyTheMappings();
}
}
}
و پس از اجرای این آزمون واحد، عبارات SQL ایی که به صورت خودکار توسط این ORM جهت بررسی عملیات نگاشت صورت خواهند گرفت به صورت زیر می‌باشند:

ProductMapping_Fixture.can_correctly_map_product : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001

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

using System;
namespace NHSample1.Domain
{
public class Employee
{
public int Id { set; get; }
public string LastName { get; set; }
public string FirstName { get; set; }
}
}


using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class EmployeeMapping : ClassMap<Employee>
{
public EmployeeMapping()
{
Not.LazyLoad();
Id(e => e.Id).GeneratedBy.Assigned();
Map(e => e.LastName).Length(50);
Map(e => e.FirstName).Length(50);
}
}
}


using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class EmployeeMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_employee()
{
new PersistenceSpecification<Employee>(Session)
.CheckProperty(p => p.Id, 1001)
.CheckProperty(p => p.FirstName, "name1")
.CheckProperty(p => p.LastName, "lname1")
.VerifyTheMappings();
}
}
}
خروجی SQL حاصل از موفقیت آزمون واحد آن:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001

همانطور که ملاحظه می‌کنید، این آزمون‌های واحد 4 مرحله را در یک سطر انجام می‌دهند:
الف) ایجاد یک وهله از کلاس Employee
ب) ثبت اطلاعات کارمند در دیتابیس
ج) دریافت اطلاعات کارمند در وهله‌ای جدید از شیء Employee
د) و در پایان بررسی می‌کند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر

اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل می‌رسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاس‌های OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:

کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل می‌شود و هر محصول می‌تواند در سفارشات متعدد و مختلفی درخواست شود.

namespace NHSample1.Domain
{
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public Product Product { get; set; }
}
}
کلاس OrderItemMapping تعریف شده در فایل جدید OrderItemMapping.cs :

using FluentNHibernate.Mapping;
using NHSample1.Domain;

namespace NHSample1.Mappings
{
public class OrderItemMapping : ClassMap<OrderItem>
{
public OrderItemMapping()
{
Not.LazyLoad();
Id(oi => oi.Id).GeneratedBy.Assigned();
Map(oi => oi.Quantity).Not.Nullable();
References(oi => oi.Product).Not.Nullable();
}
}
}
نکته جدیدی که در این کلاس نگاشت مطرح شده است، واژه کلیدی References می‌باشد که جهت بیان این ارجاعات و وابستگی‌ها بکار می‌رود. این ارجاع بیانگر یک رابطه many-to-one بین سفارشات و محصولات است. همچنین در ادامه آن Not.Nullable ذکر شده است تا این ارجاع را اجباری نمائید (در غیر اینصورت سفارش غیر معتبر خواهد بود).
نکته‌ی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index می‌تواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه می‌شود، مقدار دهی خواهد شد.

اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:

using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderItemMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_order_item()
{
var product = new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};

new PersistenceSpecification<OrderItem>(Session)
.CheckProperty(p => p.Id, 1)
.CheckProperty(p => p.Quantity, 5)
.CheckReference(p => p.Product, product)
.VerifyTheMappings();
}
}
}

مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکرده‌ایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کرده‌ایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوع‌های اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:

using System.Collections;
using NHSample1.Domain;

namespace UnitTests
{
public class CustomEqualityComparer : IEqualityComparer
{
public bool Equals(object x, object y)
{
if (ReferenceEquals(x, y)) return true;
if (x == null || y == null) return false;

if (x is Product && y is Product)
return (x as Product).Id == (y as Product).Id;

if (x is Customer && y is Customer)
return (x as Customer).Id == (y as Customer).Id;

if (x is Employee && y is Employee)
return (x as Employee).Id == (y as Employee).Id;

if (x is OrderItem && y is OrderItem)
return (x as OrderItem).Id == (y as OrderItem).Id;


return x.Equals(y);
}

public int GetHashCode(object obj)
{
//شاید وقتی دیگر
return obj.GetHashCode();
}
}
}
در اینجا فقط Id این اشیاء با هم مقایسه شده است. در صورت نیاز تمامی خاصیت‌های این اشیاء را نیز می‌توان با هم مقایسه کرد (یک سری از اشیاء بکار گرفته شده در این کلاس در ادامه بحث معرفی خواهند شد).
سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:

new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())

پس از این تغییرات و مشخص سازی نحوه‌ی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر می‌باشد:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1

قسمت پایانی کار تعاریف کلاس‌های نگاشت، مربوط به کلاس Order است که در ادامه بررسی خواهد شد.

using System;
using System.Collections.Generic;

namespace NHSample1.Domain
{
public class Order
{
public int Id { set; get; }
public DateTime OrderDate { get; set; }
public Employee Employee { get; set; }
public Customer Customer { get; set; }
public IList<OrderItem> OrderItems { get; set; }
}
}
نکته‌ی مهمی که در این کلاس وجود دارد استفاده از IList جهت معرفی مجموعه‌ای از آیتم‌های سفارشی است (بجای List و یا IEnumerable که در صورت استفاده خطای type cast exception در حین نگاشت حاصل می‌شد).

using NHSample1.Domain;
using FluentNHibernate.Mapping;

namespace NHSample1.Mappings
{
public class OrderMapping : ClassMap<Order>
{
public OrderMapping()
{
Not.LazyLoad();
Id(o => o.Id).GeneratedBy.GuidComb();
Map(o => o.OrderDate).Not.Nullable();
References(o => o.Employee).Not.Nullable();
References(o => o.Customer).Not.Nullable();
HasMany(o => o.OrderItems)
.AsList(index => index.Column("ListIndex").Type<int>());
}
}
}
در تعاریف نگاشت این کلاس نیز دو ارجاع به اشیاء کارمند و مشتری وجود دارد که با References مشخص شده‌اند.
قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت می‌کند، دارد. در اینجا مجموعه آیتم‌های یک سفارش به صورت یک لیست بازگشت داده می‌شود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int می‌باشد.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using NHSample1.Domain;
using FluentNHibernate.Testing;

namespace UnitTests
{
[TestFixture]
public class OrderMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_an_order()
{
{
var product1 =
new Product
{
Name = "Apples",
UnitPrice = 4.5m,
Discontinued = true
};
var product2 =
new Product
{
Name = "Pears",
UnitPrice = 3.5m,
Discontinued = false
};

Session.Save(product1);
Session.Save(product2);

var items = new List<OrderItem>
{
new OrderItem
{
Id = 1,
Quantity = 100,
Product = product1
},
new OrderItem
{
Id = 2,
Quantity = 200,
Product = product2
}
};

var customer = new Customer
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

var employee =
new Employee
{
FirstName = "name1",
LastName = "lname1"
};



var order = new Order
{
Customer = customer,
Employee = employee,
OrderDate = DateTime.Today,
OrderItems = items
};

new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
.CheckProperty(o => o.OrderDate, order.OrderDate)
.CheckReference(o => o.Customer, order.Customer)
.CheckReference(o => o.Employee, order.Employee)
.CheckList(o => o.OrderItems, order.OrderItems)
.VerifyTheMappings();
}
}
}
}
همانطور که ملاحظه می‌کنید در این متد آزمون واحد، نیاز به مشخص سازی منطق مقایسه اشیاء سفارش، مشتری و آیتم‌های سفارش داده شده نیز وجود دارد که پیشتر در کلاس CustomEqualityComparer معرفی شدند؛ درغیر اینصورت این آزمون واحد با شکست مواجه می‌شد.
متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص می‌شود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.

خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:

OrderMapping_Fixture.can_correctly_map_an_order : Passed
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 2002
NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0

تا اینجای کار تعاریف اشیاء ، نگاشت آن‌ها و همچنین بررسی صحت این نگاشت‌ها به پایان می‌رسد.

نکته:
دیتابیس برنامه را جهت آزمون‌های واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص می‌کند که schema ساخته شده، نمایش داده شود یا خیر):

SessionSource.BuildSchema(Session, true);
پس از این تغییر و انجام مجدد آزمون واحد، اسکریپت دیتابیس ما به صورت زیر خواهد بود (که جهت ایجاد یک دیتابیس SQLite می‌تواند مورد استفاده قرار گیرد):

drop table if exists "OrderItem"

drop table if exists "Order"

drop table if exists "Customer"

drop table if exists "Product"

drop table if exists "Employee"

drop table if exists hibernate_unique_key

create table "OrderItem" (
Id INTEGER not null,
Quantity INTEGER not null,
Product_id INTEGER not null,
Order_id INTEGER,
ListIndex INTEGER,
primary key (Id)
)

create table "Order" (
Id INTEGER not null,
OrderDate DATETIME not null,
Employee_id INTEGER not null,
Customer_id INTEGER not null,
primary key (Id)
)

create table "Customer" (
Id INTEGER not null,
FirstName TEXT not null,
LastName TEXT not null,
AddressLine1 TEXT not null,
AddressLine2 TEXT,
PostalCode TEXT not null,
City TEXT not null,
CountryCode TEXT not null,
primary key (Id)
)

create table "Product" (
Id INTEGER not null,
Name TEXT not null,
UnitPrice NUMERIC not null,
Discontinued INTEGER not null,
primary key (Id)
)

create table "Employee" (
Id INTEGER not null,
LastName TEXT,
FirstName TEXT,
primary key (Id)
)

create table hibernate_unique_key (
next_hi INTEGER
)
البته اگر مستندات SQLite را مطالعه کرده باشید می‌دانید که مفهوم کلید خارجی در این دیتابیس وجود دارد اما اعمال نمی‌شود! (برای اعمال آن باید تریگر نوشت) به همین جهت در این اسکریپت تولیدی خبری از کلید خارجی نیست.

برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):

var cfg = Fluently.Configure().Database(
// SQLiteConfiguration.Standard.ShowSql().InMemory
MsSqlConfiguration
.MsSql2008
.ShowSql()
.ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
);

اکنون اگر مجددا آزمون واحد را اجرا نمائیم، اسکریپت تولیدی به صورت زیر خواهد بود (در اینجا مفهوم استقلال برنامه از نوع دیتابیس را به خوبی می‌توان درک کرد):

if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF88858466CFBF7


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
alter table [OrderItem] drop constraint FK3EF888589F32DE52


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099B1EBA72BC


if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
alter table [Order] drop constraint FK3117099BB2F9593A


if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]

if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]

if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]

if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]

if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]

if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key

create table [OrderItem] (
Id INT not null,
Quantity INT not null,
Product_id INT not null,
Order_id INT null,
ListIndex INT null,
primary key (Id)
)

create table [Order] (
Id INT not null,
OrderDate DATETIME not null,
Employee_id INT not null,
Customer_id INT not null,
primary key (Id)
)

create table [Customer] (
Id INT not null,
FirstName NVARCHAR(50) not null,
LastName NVARCHAR(50) not null,
AddressLine1 NVARCHAR(50) not null,
AddressLine2 NVARCHAR(50) null,
PostalCode NVARCHAR(10) not null,
City NVARCHAR(50) not null,
CountryCode NVARCHAR(2) not null,
primary key (Id)
)

create table [Product] (
Id INT not null,
Name NVARCHAR(50) not null,
UnitPrice DECIMAL(19,5) not null,
Discontinued BIT not null,
primary key (Id)
)

create table [Employee] (
Id INT not null,
LastName NVARCHAR(50) null,
FirstName NVARCHAR(50) null,
primary key (Id)
)

alter table [OrderItem]
add constraint FK3EF88858466CFBF7
foreign key (Product_id)
references [Product]

alter table [OrderItem]
add constraint FK3EF888589F32DE52
foreign key (Order_id)
references [Order]

alter table [Order]
add constraint FK3117099B1EBA72BC
foreign key (Employee_id)
references [Employee]

alter table [Order]
add constraint FK3117099BB2F9593A
foreign key (Customer_id)
references [Customer]

create table hibernate_unique_key (
next_hi INT
)
که نکات ذیل در مورد آن جالب توجه است:
الف) جداول مطابق نام کلاس‌های ما تولید شده‌اند.
ب) نام فیلدها دقیقا مطابق نام خواص کلاس‌های ما تشکیل شده‌اند.
ج) Id ها به صورت primary key تعریف شده‌اند (از آنجائیکه ما در هنگام تعریف نگاشت‌ها، آن‌ها را از نوع identity مشخص کرده بودیم).
د) رشته‌ها به نوع nvarchar با اندازه 50 نگاشت شده‌اند.
ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شده‌اند.




ادامه دارد ...


نظرات اشتراک‌ها
روش‌های مقابله با مشکل امنیتی Mass Assignment در ASP.NET Core
راه حل دیگر: استفاده از روش Containment بجای Inheritance
public class UserModel
{
    [MaxLength(200)]
    [Display(Name = "Full name")]
    [Required]
    public string Name { get; set; }
}

public class UserModalViewModel
{
    public UserModel Model { get; set; }
    public bool IsAdmin { get; set; }
    public IReadonlyList<lookupitem> Roles { get; set; }
}
‌‌‌
اکشن متد متناظر با درخواست GET
[HttpGet]
public async Task<IActionResult> Edit(int id)
{
    var user = await _service.FindAsync(id); //return Maybe<UserModel>
    if (!user.HasValue)
    {
        return NotFound();
    }

    // prepare model
    var model = new UserModalViewModel
    {
        Model = user.Value,
        IsAdmin = true,
        Roles = await _lookupService.ReadRolesAsync()
    };
    return View(model);
}

‌‌‌‌
اکشن متد متناظر با درخواست POST
[HttPost]
public async Task<IActionResult> Edit([Bind(Prefix = "Model")] UserModel model)
{
    //todo: check ModelState and save model
    await _service.EditAsync(model);
}

مطالب
امکان ساده سازی تعاریف اشیاء در C# 9.0 با Target Typing
ویژگی جدید مورد بحث در این قسمت، «Improved Target Typing» نام دارد. اما «Target Typing» چیست؟ حدس زدن نوع یک شیء بر اساس زمینه‌ای که توسط آن تعریف شده‌است، Target Typing نامیده می‌شود. نمونه‌ای از آن‌را سال‌هاست که با استفاده از واژه‌ی کلیدی var در #C استفاده می‌کنید. اما قابلیتی که در C# 9.0 اضافه شده‌است، تقریبا معکوس آن است.


Target Typing در C# 9.0

مشکلی که بعضی‌ها با واژه‌ی کلیدی var دارند، این است که اندکی خوانایی کدها را کاهش می‌دهد و در این حالت بلافاصله مشخص نیست که نوع شیء در حال استفاده چیست. در C# 9.0 برای این دسته از برنامه نویس‌ها راه حل دیگری را پیشنهاد داده‌اند: نوع ابتدایی را مشخص کنید، اما نیازی به ذکر نوع پس از واژه‌ی کلیدی new نیست و همانند var، خود کامپایلر آن‌را حدس خواهد زد! برای توضیح آن دو کلاس ساده‌ی زیر را درنظر بگیرید:
    public class Person
    {
        public string FirstName { get; set; }
    }

    public class PersonWithCtor
    {
        public PersonWithCtor(string firstName)
        {
            this.FirstName = firstName;
        }

        public string FirstName { get; set; }
    }
روش متداول استفاده‌ی از کلاس Person ساده که بدون سازنده‌است، از ابتدایی‌ترین نگارش #C به صورت زیر است:
Person person = new Person();
این روش در C# 3.0 به صورت زیر خلاصه شد:
var person = new Person();
که در این حالت کامپایلر در زمان کامپایل، واژه‌ی کلیدی var را به صورت خودکار به نمونه‌ی قبلی تبدیل کرده و عملیات کامپایل را تکمیل می‌کند. اگر با این روش تعریف متغیرها و اشیاء مشکل دارید و به نظرتان خوانایی آن کاهش یافته، می‌توانید در C# 9.0 به صورت زیر عمل کنید:
Person person = new();
در این حالت ابتدا نوع متغیر و یا شیء ذکر می‌شود. سپس در جائیکه قرار است new صورت گیرد، دیگر نیازی به تکرار آن نیست که به آن «Improved Target Typing» هم گفته می‌شود.


Target Typing و پارامترهای سازنده‌ی کلاس‌ها در C# 9.0

در مثال فوق، کلاس PersonWithCtor به همراه یک سازنده‌ی پارامتردار تعریف شده‌است. در این حالت Target Typing آن به صورت زیر خواهد بود:
Person person = new("User 1");
و یا نمونه‌ای از آن در حین تعریف مقادیر اولیه‌ی Listها است:
var personList = new List<Person>
        {
            new ("User 1"),
            new ("User 2"),
            // ...
        };
و یا حتی در حین تعریف پارامترهای یک متد نیز می‌توان از target typing استفاده کرد و تنها به ذکر new بسنده نمود:
public void Adopt(Person p)
{
    //...
}

public void CallerMethod()
{
    this.Adopt(new Person("User 1"));
    // C# 9.0
    this.Adopt(new("User 1"));
}
نمونه‌ی دیگری از این مثال را در حین مقدار دهی پارامتر دوم متد XmlReader.Create، در اینجا مشاهده می‌کنید:
XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true });
// C# 9.0
XmlReader.Create(reader, new() { IgnoreWhitespace = true });


Target Typing و استفاده از خواص کلاس‌ها در C# 9.0

در همان مثال اول، اگر بخواهیم خاصیت FirstName را مقدار دهی کنیم و همچنین از Target Typing نیز استفاده کنیم ... روش زیر کامپایل نخواهد شد:
Person person = new
{
   FirstName = "User 2"
};
علت اینجا است که شیء‌ای که پس از علامت انتساب قرارگرفته‌است، یک anonymous object است و قابلیت انتساب به نوع Person را ندارد. در این حالت تنها کافی است ذکر () را پس از new، فراموش نکرد؛ تا قطعه کد زیر بدون مشکل کامپایل شود:
Person person = new()
{
   FirstName = "User 2"
};


امکان استفاده‌ی از Target typing با فیلدها در C# 9.0

امکان تعریف var با فیلدهای یک کلاس در زبان #C وجود ندارد. به همین جهت مجبور هستیم یک چنین تعاریف طولانی را در سطح کلاس‌ها داشته باشیم:
private ConcurrentDictionary<string, ObservableList<Cat>> _catsBefore = new ConcurrentDictionary<string, ObservableList<Cat>>();
اما با ارائه‌ی C# 9.0، می‌توان از target typing بر روی فیلدها نیز استفاده کرد و قطعه کد فوق را به صورت زیر خلاصه کرد:
private ConcurrentDictionary<string, ObservableList<Cat>> _cats = new(); // C# 9.0
این نکته در مورد مقدار دهی اولیه‌ی خواص نیز صدق می‌کند:
public ObservableCollection<Friend> Friends { get; } = new();


امکان ترکیب null-coalescing operator با target typing در C# 9.0

null-coalescing operator یا همان ?? به این معنا است که اگر متغیر سمت چپ آن نال نبود، همان مقدار درنظر گرفته شود و اگر نال بود، متغیر سمت راست آن بازگشت داده شود. در این حالت مثال زیر را در نظر بگیرید که در آن سگ و گربه از نوع پایه‌ی حیوان تعریف شده‌اند:
public interface IAnimal
{
}

public class Dog : IAnimal
{
}

public class Cat : IAnimal
{
}
در اینجا می‌خواهیم اگر برای مثال cat نال بود، حاصل عملگر ?? به متغیری از نوع IAnimal قابل انتساب باشد:
Cat cat = null;
Dog dog = new();
IAnimal animal = cat ?? dog;
یک چنین کاری در نگارش‌های پیشین #C مجاز نیست؛ اما در C# 9.0، چون target typeهای تعریف شده، قابل تبدیل به هم هستند، کامپایلر آن‌را بدون مشکل کامپایل می‌کند (البته قرار است در نگارش نهایی آن این امر محقق شود؛ هنوز نه!).


دانستنی‌هایی در مورد Target Typing

- نوشتن ()throw new مجاز است و نوع پیش‌فرض آن، System.Exception در نظر گرفته می‌شود.
- در حالت کار با tuples، نوشتن new اضافی است:
(int a, int b) t = new(1, 2); // "new" is redundant
و همچنین اگر پارامترهای آن ذکر نشوند، با مقدار پیش‌فرض آن نوع جایگزین خواهند شد:
(int a, int b) t = new(); // OK; same as (0, 0)


محدودیت‌های Target Typing در C# 9.0

- امکان نوشتن ()var dog = new وجود ندارد؛ چون نوع سمت راست این انتساب دیگر قابل حدس زدن نیست. نمونه‌ی دیگر آن anonymous type properties است؛ مانند new { Prop = new() } که در آن برای مثال نوع خاصیت Prop قابل حدس زدن نیست.
- target typing با binary operators قابل استفاده نیست.
- به عنوان ref قابل استفاده نیست.