مطالب
تبدیل خودکار استثنای HttpRequestValidationException به یک ModelError در ASP.NET MVC
فرم هایی که اطلاعاتی را از یک کاربر دریافت کرده و به سمت سرور Post می‌کنند، از مهمترین اجزای لاینفک یک وب سایت می‌باشند. بی شک همه‌ی ما از چنین فرمهایی حتی در یک پروژه‌ی هرچند کوچک استفاده کرده‌ایم. ممکن است هنگام ارسال این فرمها، کاربری شیطنت به خرج داده و درون یکی از فیلدهای فرم، از عبارت‌های HTML و یا یک اسکریپت استفاده کرده باشد. در ASP.Net + MVC از مکانیزم ValidationRequest برای مقابله با چنین حملاتی (XSS) استفاده شده است.
یک فرم با یک Textbox در یک صفحه قرار دهید و درون Textbox یک تگ HTML بنویسید و آنرا به سرور ارسال کنید، در حالت پیش فرض اتفاقی که خواهد افتاد اینست که با یک صفحه‌ی خطا روبرو خواهید شد که به شما هشدار می‌دهد داده‌های ارسال شده، دارای مقادیر غیرمجازی می‌باشند. شما می‌توانید این مکانیزم اعتبارسنجی-امنیتی را غیرفعال کنید. برای غیرفعال کردن آن هم در لینکی که در فوق معرفی گردید توضیحات کافی داده شده است. ولی توصیه میشود برای جلوگیری از حملات XSS هیچگاه این مکانیزم را غیرفعال نکنید، مگر اینکه هیچ داده‌ای را بدون Encode کردن در صفحه نمایش ندهید.
راه‌های زیادی برای هندل کردن این خطا و مطلع کردن کاربر وجود دارند و معمولا از صفحه‌ی خطای سفارشی برای اینکار استفاده میشود. اما ایده‌ی بهتری نیز برای مقابله با این خطا وجود دارد!
فرض کنید اطلاعات فرم شما از طریق Ajax به سرور ارسال می‌شود و نتیجه بصورت Json به مروگر برگشت داده می‌شود. در حالت معمول در صورت رخ دادن خطای فوق، سرور کد 500 را برگشت می‌دهد و تنها راه هندل کردن آن در رویداد error شئ Ajax شما می‌باشد؛ آن‌هم با یک پیغام ساده. ولی من ترجیح می‌دهم به جای صادر کردن خطای 500، در صورت وقوع این خطا آن‌را بصورت یک خطای ModelState به کاربر نمایش دهم. به نظر من این‌کار وجهه‌ی بهتری دارد و ضمنا لازم نیست در هر رویدادی این خطا را هندل کنید. برای رسیدن به این هدف هم چندین راه وجود دارد که ساده‌ترین آن‌ها اینست که یک ModelBinder سفارشی ایجاد کنید و با هندل کردن این خطا، یک پیام خطا را به ModelState اضافه کنید و بعد به MVC بگویید که از این ModelBinder برای پروژه‌ی من استفاده کن. با این روش هرگاه کاربر داده‌ای حاوی کدهای HTML درون فیلدهای فرم وارد کرده باشد، بدون هیچ کار اضافه‌ای وضعیت ModelState شما Invalid می‌شود و خطای مدل به کاربر نمایش داده خواهد شد.
ابتدا کلاسی برای ModelBinder سفارشی خود ایجاد کنید و از کلاس DefaultModelBinder ارث بری کنید. سپس با بازنویسی متد BindModel آن منطق خود را پیاده سازی کنید:
using System.Web;
using System.Web.Mvc;
using System.Web.Helpers;
using System.Globalization;

namespace Parsnet
{
    public class ParsnetModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            try
            {
                return base.BindModel(controllerContext, bindingContext);
            }
            catch (HttpRequestValidationException ex)
            {
                var modelState = new ModelState();
                modelState.Errors.Add("اطلاعات ارسالی شما دارای کدهای HTML می‌باشند.");
                var key = bindingContext.ModelName;
                var value = controllerContext.RequestContext.HttpContext.Request.Unvalidated().Form[key];
                modelState.Value = new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
                bindingContext.ModelState.Add(key, modelState);
            }

            return null;
        }
    }
}

در این کلاس ابتدا سعی میکنیم بطور عادی کار را به متد BindModel کلاس پایه بسپاریم. اگر داده‌های ارسال شده حاوی کد HTML نباشد، بدون هیچ خطایی Model Binding صورت می‌گیرد. ولی در صورتیکه کاربر در فیلدی، از کدهای HTML استفاده کرده باشد، یک خطای HttpRequestValidationException  رخ خواهد داد که با هندل کردن آن هدف خود را تامین می‌کنیم.
برای اینکار در قسمت catch بلوک مدیریت خطا، ابتدا یک نمونه از کلاس ModelState را میسازیم. بعد پیام خطای مورد نظر خود را به Errors‌های آن اضافه میکنیم. حال باید یک زوج کلید/مقدار برای این ModelState تعریف کنیم و به bindingContext اضافه کنیم. کلید ما در اینجا نام مدل جاری و مقدار آن هم نام فیلدی از فرم است که سبب بروز این خطا شده است.
حالا نهایت کاری که باید انجام دهیم اینست که در رویداد Application_Start این مدل بایندیگ سفارشی را جایگزین مدل بایندیگ پیش فرض کنیم:
    ModelBinders.Binders.DefaultBinder = new ParsnetModelBinder();

کار تمام است. از حالا به بعد در صورتیکه کاربر در فیلدهای هر فرمی از سایت شما از کدهای HTML استفاده کند دیگر خطایی رخ نمی‌دهد و فقط ModelState در وضعیت Invalid قرار میگیرد که میتوانید با قرار دادن یک ValidationMessage یا ValidationSummary به راحتی خطا را به کاربر نشان دهید.
مطالب
آشنایی با CLR: قسمت هشتم
در قسمت پنجم در مورد ابزار Ngen کمی صحبت کردیم و در این قسمت هم در مورد آن صحبت هایی خواهیم کرد. گفتیم که این ابزار در زمان نصب، اسمبلی‌ها را کامپایل می‌کند تا در زمان اجرا JIT وقتی برای آن نگذارد. این کار دو مزیت به همراه دارد:
  1. بهینه سازی زمان آغاز به کار برنامه
  2. کاهش صفحات کاری برنامه: از آنجا که برنامه از قبل کامپایل شده، فراهم کردن صفحه بندی از ابتدای کار امر چندان دشواری نخواهد بود؛ لذا در این حالت صفحه بندی حافظه به صورت پویاتری انجام می‌گردد. شیوه‌ی کار به این صورت است که اسمبلی‌ها به چندین پروسه‌ی کاری کوچک‌تر تبدیل شده تا صفحه بندی هر کدام جدا صورت گیرد و محدوده‌ی صفحه بندی کوچکتر می‌شود. در نتیجه کمتر نقصی در صفحه بندی دیده شده یا کلا دیده نخواهد شد. نتیجه‌ی کار هم در یک فایل ذخیره می‌گردد که این فایل می‌تواند نگاشت به حافظه شود تا این قسمت از حافظه به طور اشتراکی مورد استفاده قرار گیرد و بدین صورت نیست که هر پروسه‌ای برای خودش قسمتی را گرفته باشد.
موقعی که اسمبلی، کد IL آن به کد بومی تبدیل می‌شود، یک اسمبلی جدید ایجاد شده که این فایل جدید در مسیر زیر قرار می‌گیرد:
%SystemRoot%\Assembly\NativeImages_v4.0.#####_64
نام دایرکتوری اطلاعاتی شامل نسخه CLR و اطلاعاتی مثل اینکه برنامه بر اساس چه نسخه‌ای 32 یا 64 بیت کامپایل شده است.

معایب
احتمالا شما پیش خود می‌گویید این مورد فوق العاده امکان جالبی هست. کدها از قبل تبدیل شده‌اند و دیگر فرآیند جیت صورت نمی‌گیرد. در صورتیکه ما تمامی امکانات یک CLR مثل مدیریت استثناءها و GC و ... را داریم، ولی غیر از این یک مشکلاتی هم به کارمان اضافه می‌شود که در زیر به آنها اشاره می‌کنیم:

عدم محافظت از کد در برابر بیگانگان: بعضی‌ها تصور می‌کنند که این کد را می‌توانند روی ماشین شخصی خود کامپایل کرده و فایل ngen را همراه با آن ارسال کنند. در این صورت کد IL نخواهد بود ولی موضوع این هست اینکار غیر ممکن است و هنوز استفاده از اطلاعات متادیتا‌ها پابرجاست به خصوص در مورد اطلاعات چون reflection و serialization‌ها . پس کد IL کماکان همراهش هست. نکته‌ی بعدی اینکه انتقال هم ممکن نیست؛ بنا به شرایطی که در مورد بعدی دلیل آن را متوجه خواهید شد.

از سینک با سیستم خارج میشوند: موقعیکه CLR، اسمبلی‌ها را به داخل حافظه بار می‌کند، یک سری خصوصیات محیط فعلی را با زمانیکه عملیات تبدیل IL به کد ماشین صورت گرفته است، چک می‌کند. اگر این خصوصیات هیچ تطابقی نداشته باشند، عملیات JIT همانند سابق انجام می‌گردد. خصوصیات و ویژگی‌هایی که چک می‌شوند به شرح زیر هستند:
  • ورژن CLR: در صورت تغییر، حتی با پچ‌ها و سرویس پک ها.
  • نوع پردازنده: در صورت تغییر پردازنده یا ارتقا سخت افزاری.
  • نسخه سیستم عامل : ارتقاء با سرویس پک ها.
  • MVID یا Assemblies Identity module Version Id: در صورت کامپایل مجدد تغییر می‌کند.
  • Referenced Assembly's version ID: در صورت کامپایل مجدد اسمبلی ارجاع شده.
  • تغییر مجوزها: در صورتی که تغییری نسبت به اولین بار رخ دهد؛ مثلا در قسمت قبلی در مورد اجازه نامه اجرای کدهای ناامن صحبت کردیم. برای نمونه اگر در همین اجازه نامه تغییری رخ دهد، یا هر نوع اجازه نامه دیگری، برنامه مثل سابق (جیت) اجرا خواهد شد.
پی نوشت: در آپدیت‌های دات نت فریم ورک به طور خودکار ابزار ngen صدا زده شده و اسمبلی‌ها مجددا کمپایل و دخیره میشوند و برنامه سینک و آپدیت باقی خواهد ماند. 

کارایی پایین کد در زمان اجرا: استفاده از ngen از ابتدا قرار بود کارآیی را با حذف جیت بالا ببرد، ولی گاهی اوقات در بعضی شرایط ممکن نیست. کدهایی که ngen تولید می‌کند به اندازه‌ی جیت بهینه نیستند. برای مثال ngen نمی‌تواند بسیاری از دستورات خاص پردازنده را جز در زمان runtime مشخص کند. همچنین فیلدهایی چون static را از آنجا که نیاز است آدرس واقعی آن‌ها در زمان اجرا به دست بیاید، مجبور به تکنیک و ترفند میشود و موارد دیگری از این قبیل.
پس حتما نسخه‌ی ngen شده و غیر ngen را بررسی کنید و کارآیی هر دو را با هم مقایسه کنید. برای بسیاری از برنامه‌ها کاهش صفحه بندی یک مزیت و باعث بهبود کارآیی می‌شود. در نتیجه در این قسمت ngen برنده اعلام می‌شود.

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

برای بسیاری از برنامه‌های کلاینت که تجربه‌ی startup طولانی دارند، مایکروسافت ابزاری را به نام Managed Profile Guided Optimization Tool یا MPGO .exe دارد. این ابزار به تحلیل اجرای برنامه شما پرداخته و بررسی می‌کند که در زمان آغازین برنامه چه چیزهایی نیاز است. اطلاعات به دست آمده از تحلیل به سمت ngen فرستاده شده تا کد بومی بهینه‌تری تولید گردد. موقعیکه شما آماده ارائه برنامه خود هستید، برنامه را از طریق این تحلیل و اجرا کرده و با قسمت‌های اساسی برنامه کار کنید. با این کار اطلاعاتی در مورد اجرای برنامه در داخل یک پروفایل embed شده در اسمبلی، قرار گرفته و ngen موقع تولید کد، این پروفایل را جهت تولید کد بهینه مطالعه خواهد کرد.

در مقاله‌ی بعدی در مورد FCL صحبت‌هایی خواهیم کرد.
اشتراک‌ها
نقشه های رویداد پذیر Asp.net + SVG

This article explains step-by-step the server-side manipulation of SVG using Microsoft ASP.NET and C#. The example server-side application demonstrates how simple and complex maps can easily be represented using SVGs.  

نقشه های رویداد پذیر Asp.net + SVG
بازخوردهای پروژه‌ها
اعمال نشدن حذف رکورد بر روی دیتاتیبل
با سلام و تشکر

در کلیه قسمت‌های مدیریت که اطلاعات به شکل یک جدول نمایش داده شده است(به عنوان مثال لیست کاربران سایت) هنگام حذف یک رکورد،یک دیالوگ تایید نمایش داده می‌شود و پس از تایید رکورد از بانک اطلاعاتی حذف می‌گردد اما کماکان در لیست رکوردها نشان داده میشود و پس از رفرش رکورد مورد نظر از لیست حذف می‌گردد
آیا این امکان وجود دارد که موقع حذف لیست مورد نظر بروز گردد
مطالب
افزودن SQL Server CE 4.0 به لیست توزیع مجدد در InstallShield 2010
در برنامه‌ی ساخت نصاب InstallShield، در قسمت افزودن بسته‌های نصبی برای برنامه‌ی ساخته شده


بسته‌ی نصب  SQL Server CE 3.5 SP2 وجود دارد:


اما برای برنامه‌های جدیدتر نیاز به افزودن بسته‌ی نصب دیتابیس SQL Server CE نسخه 4 است که با عدم وجود این بسته روبرو هستیم. در ادامه با نحوه‌ی افزودن این بسته‌ها آشنا خواهید شد.

اینگونه بسته‌ها در کنار برنامه‌ی ساخت نصاب و در پوشه‌ی SetupPrerequisites نگهداری شده و با نوع *.prq ذخیره می‌شوند. این نوع فایل‌ها از نوع xml هستند و در واقع یک نوع کار نگاشت را انجام می‌دهند. برای نمونه محتویات یکی از این فایل‌ها را در زیر می‌بینید:
<?xml version="1.0" encoding="UTF-8"?>
<SetupPrereq>
<conditions>
<condition Type="32" Comparison="2" Path="HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server Compact Edition\v3.5\ENU" FileName="DesktopRuntimeVersion" ReturnValue="3.5.8080.0"></condition>
</conditions>
<operatingsystemconditions>
<operatingsystemcondition MajorVersion="5" MinorVersion="0" PlatformId="2" CSDVersion="" ServicePackMajorMin="3"></operatingsystemcondition>
<operatingsystemcondition MajorVersion="5" MinorVersion="1" PlatformId="2" CSDVersion="" Bits="1" ProductType="1"></operatingsystemcondition>
<operatingsystemcondition MajorVersion="6" MinorVersion="0" PlatformId="2" CSDVersion="" Bits="1"></operatingsystemcondition>
<operatingsystemcondition MajorVersion="5" MinorVersion="2" PlatformId="2" CSDVersion="" Bits="1" ProductType="2|3"></operatingsystemcondition>
<operatingsystemcondition MajorVersion="6" MinorVersion="1" PlatformId="2" CSDVersion="" Bits="1"></operatingsystemcondition>
<operatingsystemcondition MajorVersion="6" MinorVersion="0" PlatformId="2" CSDVersion="" Bits="1" ProductType="2|3"></operatingsystemcondition>
</operatingsystemconditions>
<files>
<file LocalFile="&lt;ISProductFolder&gt;\SetupPrerequisites\SQL CE 3.5\SSCERuntime_x86-ENU.msi" URL="http://go.microsoft.com/fwlink/?LinkId=166085&amp;clcid=0x409" CheckSum="86AF6D36DFF214718DCD35D851249D3D" FileSize="0,3164160"></file>
</files>
<execute file="SSCERuntime_x86-ENU.msi" cmdline="/q /norestart" cmdlinesilent="/q /norestart" returncodetoreboot="1641,3010,4123" requiresmsiengine="1"></execute>
<properties Id="{A7C4B3C0-F3A0-426A-A043-E13DBA123E52}" Description="This prerequisite installs the Microsoft SQL Server Compact 3.5 SP2." AltPrqURL="http://saturn.installshield.com/is/prerequisites/microsoft sql ce 3.5 sp2.prq"></properties>
<behavior Reboot="2"></behavior>
</SetupPrereq>
کافیست به ازای هر نسخه‌ی 32 و یا 64 بیتی، فایل xml مورد نظر، با پسوند prq در پوشه‌ی SetupPrerequisites ذخیره شود.
برای نسخه 32 بیتی(Microsoft SQL CE 4.0 x86.prq ):
<?xml version="1.0" encoding="UTF-8"?>
<SetupPrereq>
    <conditions>
        <condition Type="32" Comparison="2" Path="HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server Compact Edition\v4.0\ENU" FileName="DesktopRuntimeVersion" ReturnValue="4.0.8482.1"></condition>
    </conditions>
    <operatingsystemconditions>
        <operatingsystemcondition CSDVersion="" Bits="1"></operatingsystemcondition>
    </operatingsystemconditions>
    <files>
        <file LocalFile=".\SSCERuntime_x86-ENU.exe" URL="http://download.microsoft.com/download/0/5/D/05DCCDB5-57E0-4314-A016-874F228A8FAD/SSCERuntime_x86-ENU.exe" CheckSum="0A55733CF406FBD05DFCFF5A27A0B4F7" FileSize="0,2379544"></file>
    </files>
    <execute file="SSCERuntime_x86-ENU.exe"></execute>
    <properties Id="{2754916B-119B-4428-9F94-DC9E45072CCC}"></properties>
    <behavior Failure="4" Reboot="2"></behavior>
</SetupPrereq>
و برای نسخه 64 بیتی(Microsoft SQL CE 4.0 x64.prq) : 
<?xml version="1.0" encoding="UTF-8"?>
<SetupPrereq>
    <conditions>
        <condition Type="32" Comparison="2" Path="HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server Compact Edition\v4.0\ENU" FileName="DesktopRuntimeVersion" ReturnValue="4.0.8482.1"></condition>
    </conditions>
    <operatingsystemconditions>
        <operatingsystemcondition CSDVersion="" Bits="2"></operatingsystemcondition>
    </operatingsystemconditions>
    <files>
        <file LocalFile=".\SSCERuntime_x64-ENU.exe" URL="http://download.microsoft.com/download/0/5/D/05DCCDB5-57E0-4314-A016-874F228A8FAD/SSCERuntime_x64-ENU.exe" CheckSum="A417082ECAEDD95AFB41F73DC140C350" FileSize="0,2621240"></file>
    </files>
    <execute file="SSCERuntime_x64-ENU.exe"></execute>
    <properties Id="{7CB7BE3C-614A-403F-94D9-5652285A3EDF}"></properties>
    <behavior Failure="4" Reboot="2"></behavior>
</SetupPrereq>
و در نهایت این دو بسته به لیست اضافه خواهد شد:



 
مطالب
استفاده از SQL-CE به کمک NHibernate

خلاصه‌ای را در مورد SQL Server CE قبلا در این سایت مطالعه‌ کرده‌اید. در ادامه خلاصه‌ای کاربردی را از تنظیمات و نکات مرتبط به کار با SQL-CE به کمک NHibernate ملاحظه خواهید نمود:

1) دریافت SQL-CE 4.0


همین مقدار برای استفاده از SQL-CE 4.0 به کمک NHibernate کفایت می‌کند و حتی نیازی به نصب سرویس پک یک VS 2010 هم نیست.

2) ابزار سازی جهت ایجاد یک بانک اطلاعاتی خالی SQL-CE

using System;
using System.IO;

namespace NHibernate.Helper.DbSpecific
{
public class SqlCEDbHelper
{
const string engineTypeName = "System.Data.SqlServerCe.SqlCeEngine, System.Data.SqlServerCe";

/// <summary>
/// note: this method will delete existing db and then creates a new one.
/// </summary>
/// <param name="filename"></param>
/// <param name="password"></param>
public static void CreateEmptyDatabaseFile(string filename, string password = "")
{
if (File.Exists(filename))
File.Delete(filename);

var type = System.Type.GetType(engineTypeName);
var localConnectionString = type.GetProperty("LocalConnectionString");
var createDatabase = type.GetMethod("CreateDatabase");

var engine = Activator.CreateInstance(type);

string connectionStr = string.Format("Data Source='{0}';Password={1};Encrypt Database=True", filename, password);
if (string.IsNullOrWhiteSpace(password))
connectionStr = string.Format("Data Source='{0}'", filename);

localConnectionString.SetValue(
obj: engine,
value: connectionStr,
index: null);
createDatabase.Invoke(engine, new object[0]);
}

/// <summary>
/// use this method to compact or encrypt existing db or decrypt it to a new db with all records
/// </summary>
/// <param name="sourceConnection"></param>
/// <param name="destConnection"></param>
public static void CompactDatabase(string sourceConnection, string destConnection)
{
var type = System.Type.GetType(engineTypeName);
var engine = Activator.CreateInstance(type);

var localConnectionString = type.GetProperty("LocalConnectionString");
localConnectionString.SetValue(
obj: engine,
value: sourceConnection,
index: null);

var compactDatabase = type.GetMethod("Compact");
compactDatabase.Invoke(engine, new object[] { destConnection });
}
}
}

کلاس فوق، یک کلاس عمومی است و مرتبط به NHibernate نیست و در همه جا قابل استفاده است.
متد CreateEmptyDatabaseFile یک فایل بانک اطلاعاتی خالی با فرمت مخصوص SQL-CE را برای شما تولید خواهد کرد. به این ترتیب می‌توان بدون نیاز به ابزار خاصی، سریعا یک بانک خالی را تولید و شروع به کار کرد. در این متد اگر کلمه عبوری را وارد نکنید، بانک اطلاعاتی رمزنگاری شده نخواهد بود و اگر کلمه عبور را وارد کنید، دیتابیس اولیه به همراه کلیه اعمال انجام شده بر روی آن در طول زمان، با کمک الگوریتم AES به صورت خودکار رمزنگاری خواهند شد. کل کاری را هم که باید انجام دهید ذکر این کلمه عبور در کانکشن استرینگ است.
متد CompactDatabase، یک متد چند منظوره است. اگر بانک اطلاعاتی SQL-CE رمزنگاری نشده‌ای دارید و می‌خواهید کل آن‌را به همراه تمام اطلاعات درون آن رمزنگاری کنید، می‌توانید جهت سهولت کار از این متد استفاده نمائید. آرگومان اول آن به کانکشن استرینگ بانکی موجود و آرگومان دوم به کانکشن استرینگ بانک جدیدی که تولید خواهد شد، اشاره می‌کند.
همچنین اگر یک بانک اطلاعاتی SQL-CE رمزنگاری شده دارید و می‌خواهید آن‌را به صورت یک بانک اطلاعاتی جدید به همراه تمام رکوردهای آن رمزگشایی کنید، باز هم می‌توان از این متد استفاده کرد. البته بدیهی است که کلمه عبور را باید داشته باشید و این کلمه عبور جایی درون فایل بانک اطلاعاتی ذخیره نمی‌شود. در این حالت در کانکشن استرینگ اول باید کلمه عبور ذکر شود و کانکشن استرینگ دوم نیازی به کلمه عبور نخواهد داشت.

فرمت کلی کانکشن استرینگ SQL-CE هم به شکل زیر است:

Data Source=c:\path\db.sdf;Password=1234;Encrypt Database=True

البته این برای حالتی است که قصد داشته باشید بانک اطلاعاتی مورد استفاده را رمزنگاری کنید یا از یک بانک اطلاعاتی رمزنگاری شده استفاده نمائید. اگر بانک اطلاعاتی شما کلمه عبوری ندارد، ذکر Data Source=c:\path\db.sdf کفایت می‌کند.

این کلاس هم از این جهت مطرح شد که NHibernate می‌تواند ساختار بانک اطلاعاتی را بر اساس تعاریف نگاشت‌ها به صورت خودکار تولید و اعمال کند، «اما» بر روی یک بانک اطلاعاتی خالی SQL-CE از قبل تهیه شده (در غیراینصورت خطای The database file cannot be found. Check the path to the database را دریافت خواهید کرد).

نکته:
اگر دقت کرده باشید در این کلاس engineTypeName به صورت رشته ذکر شده است. چرا؟
علت این است که با ذکر engineTypeName به صورت رشته، می‌توان از این کلاس در یک کتابخانه عمومی هم استفاده کرد، بدون اینکه مصرف کننده نیازی داشته باشد تا ارجاع مستقیمی را به اسمبلی SQL-CE به برنامه خود اضافه کند. اگر این ارجاع وجود داشت، متدهای یاد شده کار می‌کنند، در غیراینصورت در گوشه‌ای ساکت و بدون دردسر و بدون نیاز به اسمبلی خاصی برای روز مبادا قرار خواهند گرفت.


3) ابزار مرور اطلاعات بانک اطلاعاتی SQL-CE

با استفاده از management studio خود SQL Server هم می‌شود با بانک‌های اطلاعاتی SQL-CE کار کرد، اما ... اینبار برخلاف نگارش کامل اس کیوال سرور، با یک نسخه‌ی بسیار بدوی، که حتی امکان rename فیلدها را هم ندارد مواجه خواهید شد. به همین جهت به شخصه برنامه SqlCe40Toolbox را ترجیح می‌دهم و اطمینان داشته باشید که امکانات آن برای کار با SQL-CE از امکانات ارائه شده توسط management studio مایکروسافت، بیشتر و پیشرفته‌تر است!



4) تنظیمات NHibernate جهت کار با SQL-CE

الف) پس از نصب SQL-CE ، فایل‌های آن‌را در مسیر C:\Program Files\Microsoft SQL Server Compact Edition\v4.0 می‌توان یافت. درایور ADO.NET آن هم در مسیر C:\Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop قرار دارد. بنابراین در ابتدا نیاز است تا ارجاعی را به اسمبلی System.Data.SqlServerCe.dll به برنامه خود اضافه کنید (نام پوشه desktop آن هم غلط انداز است. از این جهت که نگارش 4 آن، به راحتی در برنامه‌های ذاتا چند ریسمانی ASP.Net بدون مشکل قابل استفاده است).
نکته مهم: در این حالت NHibernate قادر به یافتن فایل درایور یاد شده نخواهد بود و پیغام خطای «Could not create the driver from NHibernate.Driver.SqlServerCeDriver» را دریافت خواهید کرد. برای رفع آن، اسمبلی System.Data.SqlServerCe.dll را در لیست ارجاعات برنامه یافته و در برگه خواص آن، خاصیت «Copy Local» را true کنید. به این معنا که NHibernate این اسمبلی را در کنار فایل اجرایی برنامه شما جستجو خواهد کرد.

ب) مطلب بعد، تنظیمات ابتدایی NHibernate‌ است جهت شناساندن SQL-CE . مابقی مسایل (نکات mapping، کوئری‌ها و غیره) هیچ تفاوتی با سایر بانک‌های اطلاعاتی نخواهد داشت و یکی است. به این معنا که اگر برنامه شما از ویژگی‌های خاص بانک‌های اطلاعاتی استفاده نکند (مثلا اگر از رویه‌های ذخیره شده اس کیوال سرور استفاده نکرده باشد)، فقط با تغییر کانکشن استرینگ و معرفی dialect و driver جدید، به سادگی می‌تواند به یک بانک اطلاعاتی دیگر سوئیچ کند؛ بدون اینکه حتی بخواهید یک سطر از کدهای اصلی برنامه خود را تغییر دهید.



تنها نکته جدید آن این متد است:

private Configuration getConfig()
{
var configure = new Configuration();
configure.SessionFactoryName("BuildIt");

configure.DataBaseIntegration(db =>
{
db.ConnectionProvider<DriverConnectionProvider>();
db.Dialect<MsSqlCe40Dialect>();
db.Driver<SqlServerCeDriver>();
db.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
db.IsolationLevel = IsolationLevel.ReadCommitted;
db.ConnectionString = ConnectionString;
db.Timeout = 10;

//for testing ...
db.LogFormattedSql = true;
db.LogSqlInConsole = true;
});

return configure;
}

که در آن نحوه تعریف MsSqlCe40Dialect و SqlServerCeDriver مشخص شده است.

نکته حاشیه‌ای!
در این مثال primary key از نوع identity تعریف شده و بدون مشکل کار کرد. همین را اگر با EF تست کنید، این خطا را دریافت می‌کنید: «Server-generated keys and server-generated values are not supported by SQL Server Compact». بله، EF نمی‌تواند با primary key از نوع identity حین کار با SQL-CE کار کند. برای رفع آن توصیه شده است که از Guid استفاده کنید!

نکته تکمیلی:
استفاده از Dialect سفارشی در NHibernate


نکته پایانی!
و در پایان باید اشاره کرد که SQL-CE یک بانک اطلاعاتی نوشته شده با دات نت نیست (با CPP نوشته شده است و نصب آن هم نیاز به ران تایم به روز VC را دارد). به این معنا که جهت سیستم‌های 64 بیتی و 32 بیتی باید نسخه مناسب آن‌را توزیع کنید. یا اینکه Target platform پروژه جاری دات نت خود را بر روی X86 قرار دهید (نه بر روی Any CPU پیش فرض) و در این حالت تنها یک نسخه X86 بانک اطلاعاتی SQL-CE و همچنین برنامه خود را برای تمام سیستم‌ها توزیع کنید.

مطالب
نصب یک اسمبلی دات نت در GAC

افزونه فارسی به پارسی را قبل از ارائه در سایت، بر روی یک ماشین مجازی هم تست کردم. برای این منظور از Microsoft virtual pc استفاده شد. البته در مقابل امکانات VMware شاید حرفی برای گفتن نداشته باشد ولی خوب جهت مقاصد تست نرم افزار بر روی یک سیستم عاری از وسایل برنامه نویسی مناسب است. (برای نصب یک سیستم عامل توسط آن برای مثال می‌شود از سی دی آن OS یک فایل ISO تهیه کرد و مسیر این فایل ISO را به ماشین مجازی معرفی کرد . سپس سیستم بوت شده و روال نصب مطابق معمول خواهد بود)
اولین مشکلی که پس از تست بر روی سیستم مجازی رخ داد، پیغام یافت نشدن اسمبلی مربوط به SQLite بود. نرم افزار word هنگام اجرای افزونه‌های دات نت، آنها را در مسیری با یک نامگذاری منحصربفرد کپی می‌کند و تنها هم همان اسمبلی افزونه را کپی می‌کند و نه سایر موارد همراه را. برای پیدا کردن این مسیر می‌شود از روش زیر استفاده کرد:
using System.Reflection;
Assembly.GetExecutingAssembly().Location

در این مسیر اسمبلی SQLite وجود ندارد و به همین دلیل هم بارگذاری نخواهد شد. بهترین راه حل برای رفع این مشکل، نصب اسمبلی مربوطه در GAC یا global assembly cache است.
برای نصب اسمبلی در GAC استفاده از برنامه gacutil توصیه شده است. این برنامه به همراه SDK دات نت فریم ورک ارائه می‌شود و الزامی ندارد که کاربر نهایی این برنامه را داشته باشد. خوشبختانه با استفاده از برنامه نویسی هم می‌شود یک نمونه از برنامه Gacutil را خودمان ایجاد کنیم (برای مثال ایجاد یک برنامه کنسول و دریافت مسیر از طریق آرگومان‌های ارسالی به آن):

new System.EnterpriseServices.Internal.Publish().GacInstall(path);

در اینجا باید ارجاعی از System.EnterpriseServices نیز به برنامه اضافه شود.
این روش در مورد اسمبلی SQLite که دارای امضای دیجیتال است کار خواهد کرد. اما اگر قصد داشته باشید به صورت عمومی از آن استفاده کنید، باید ابتدا بررسی کرد که آیا فایل اسمبلی دارای امضای دیجیتال است یا خیر. برای این منظور می‌توان مقدار عبارت زیر را ارزیابی کرد:
Assembly.LoadFile(path).GetName().GetPublicKey().Length

اگر این طول بزرگتر از صفر بود به این معنا است که فایل اسمبلی دارای امضای دیجیتال است و می‌توان آنرا در GAC نصب کرد.
لازم به ذکر است که متد معرفی شده برای نصب در GAC در صورت عدم موفقیت هیچ پیغام خطا یا exception ایی را در برنامه تولید نخواهد کرد. اما پیغام خطای حاصل را در event log ویندوز می‌توان مشاهده کرد.


مطالب
کوئری های پیشرفته، Error Handling و Data Loader در GraphQL
در قسمت قبل یادگرفتیم که چگونه GraphQL را با ASP.NET Core یکپارچه کنیم و اولین GraphQL query را ایجاد و داده‌ها را از سرور بازیابی کردیم. البته ما به این query ‌های ساده بسنده نخواهیم کرد. در این قسمت می‌خواهیم یاد بگیریم که چگونه query ‌های پیشرفته‌ی GraphQL را بنویسیم و در زمان انجام این کار، نمایش دهیم که چگونه خطا‌ها را  مدیریت کنیم و علاوه بر این با queries, aliases, arguments, fragments نیز کار خواهیم کرد.

Creating Complex Types for GraphQL Queries 
اگر نگاهی به owners و query (در پایان قسمت قبل)  بیندازیم، متوجه خواهیم شد که یک لیست از خصوصیات مدل Owner که در OwnerType معرفی شده‌اند، نسبت به کوئری برگشت داده می‌شود. OwnerType شامل فیلد‌های Id , Name  و Address می‌باشد. یک Owner می‌تواند چندین account مرتبط با خود را داشته باشد. هدف این است که در owners ،query لیست account ‌های مربوط به هر owner را بازگشت دهیم. 
قبل از اضافه کردن فیلد Accounts در کلاس OwnerType نیاز است کلاس AccountType را ایجاد کنیم. در ادامه یک کلاس را به نام AccountType در پوشه GraphQLTypes ایجاد می‌کنیم. 
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the account object.");
        Field(x => x.Description).Description("Description property from the account object.");
        Field(x => x.OwnerId, type: typeof(IdGraphType)).Description("OwnerId property from the account object.");
    }
}

 همانطور که مشخص است، خصوصیت Type را از کلاس Account، معرفی نکرده‌ایم (در ادامه اینکار را انجام خواهیم داد). در ادامه، واسط IAccountRepository و کلاس AccountRepository را باز کرده و آن را مطابق زیر ویرایش می‌کنیم: 
public interface IAccountRepository
{
    IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId);
}

public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
       _context = context;
    }
 
    public IEnumerable<Account> GetAllAccountsPerOwner(Guid ownerId) => _context.Accounts
        .Where(a => a.OwnerId.Equals(ownerId))
        .ToList();
}

اکنون می‌توان لیست account‌ها را به نتیجه owners ، query اضافه کنیم. پس کلاس OwnerType را باز کرده و آن را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository)
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context => repository.GetAllAccountsPerOwner(context.Source.Id)
        );
    }
}

چیز خاصی در اینجا وجود ندارد که ما تا کنون ندیده باشیم. به همان روش که یک فیلد را در کلاس AppQuery  ایجاد کردیم، یک فیلد را با نام  accounts در کلاس OwnerType ایجاد می‌کنیم. همچنین متد GetAllAccountsPerOwner نیاز به پارامتر id را دارد و این پارامتر را از طریق context.Source.Id فراهم می‌کنیم. زیرا context شامل خصوصیت Source است که در این حالت مشخص نوع Owner می‌باشد.

اکنون پروژه را اجرا کنید و به آدرس زیر بروید:
https://localhost:5001/ui/playground
سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید که نتیجه آن علاوه بر owner‌ها، لیست account ‌های مربوط به هر owner هم می‌باشد:  
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      ownerId
    }
  }
}

Adding Enumerations in GraphQL Queries 
در کلاس AccountType  فیلد Type را اضافه نکرده‌ایم و این کار را عمدا انجام داده‌ایم. اکنون زمان انجام این کار می‌باشد. برای اضافه کردن گونه شمارشی به کلاس AccountType نیاز است تا در ابتدا یک کلاس تعریف شود که نسبت به type ‌های معمول در GraphQL متفاوت است. یک کلاس را به نام AccountTypeEnumType  در پوشه GraphQLTypes  ایجاد کرده و آن را مطابق زیر ویرایش می‌کنیم:  
public class AccountTypeEnumType : EnumerationGraphType<TypeOfAccount>
{
    public AccountTypeEnumType()
    {
        Name = "Type";
        Description = "Enumeration for the account type object.";
    }
}

کلاس AccountTypeEnumType باید از نوع جنریک کلاس EnumerationGraphType ارث بری کند و پارامتر جنریک آن، یک گونه شمارشی را دریافت می‌کند (که در قسمت قبل آن را ایجاد کردیم؛ TypeOfAccount).  همچنین مقدار خصوصیت Name نیز باید همان نام خصوصیت گونه شمارشی در کلاس Account باشد (نام آن در کلاس Account مساوی Type می‌باشد). سپس گونه شمارشی را در کلاس AccountType به صورت زیر اضافه می‌کنیم:
public class AccountType : ObjectGraphType<Account>
{
    public AccountType()
    {
        ...
        Field<AccountTypeEnumType>("Type", "Enumeration for the account type object.");
    }
}

اکنون پروژه را اجرا کنید و سپس owners ، query را در UI.Playground به صورت زیر اجرا کنید:
{
  owners{
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
}
که نتیجه آن اضافه شدن type  به هر account می‌باشد:


Implementing a Cache in the GraphQL Queries with Data Loader  
دیدم که query، نتیجه دلخواهی را برای ما بازگشت می‌دهد؛ اما این query هنوز به اندازه کافی بهینه نشده‌است. مشکل چیست؟
query  ایجاد شده به حالتی کار می‌کند که در ابتدا همه owner ‌ها را بازیابی می‌کند. سپس به ازای هر owner، یک Sql Query  را به سمت بانک اطلاعاتی ارسال می‌کند تا Account ‌های مربوط به آن Owner را بازگشت دهد که می‌توان log  آن را در Terminal مربوط به VS Code مشاهده کرد.
 


البته زمانیکه چند موجودیت owner را داشته باشیم، این مورد یک مشکل نمی‌باشد؛ ولی وقتی تعداد موجودیت‌ها زیاد باشد چطور؟
 owners ، query را می‌توان با استفاده از DataLoader که توسط GraphQL فراهم شده‌است، بهینه سازی کرد. جهت انجام اینکار در ابتدا واسط IAccountRepository و کلاس AccountRepository را همانند زیر ویرایش می‌کنیم:
public interface IAccountRepository
{
    ...
    Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds);
}
public class AccountRepository : IAccountRepository
{
    ...
    public async Task<ILookup<Guid, Account>> GetAccountsByOwnerIds(IEnumerable<Guid> ownerIds)
    {
        var accounts = await _context.Accounts.Where(a => ownerIds.Contains(a.OwnerId)).ToListAsync();
        return accounts.ToLookup(x => x.OwnerId);
    }
}

 نیاز است که یک متد داشته باشیم که <<Task<ILookup<TKey, T  را برگشت می‌دهد؛ زیرا DataLoader نیازمند یک متد با نوع برگشتی که در امضایش عنوان شده است می‌باشد .
در ادامه کلاس OwnerType را مطابق زیر ویرایش می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType(IAccountRepository repository, IDataLoaderContextAccessor dataLoader)
    {
       ...
        Field<ListGraphType<AccountType>>(
            "accounts",
            resolve: context =>
            {
                var loader = dataLoader.Context.GetOrAddCollectionBatchLoader<Guid, Account>("GetAccountsByOwnerIds", repository.GetAccountsByOwnerIds);
                return loader.LoadAsync(context.Source.Id);
            });
    }
}

در کلاس OwnerType، واسط IDataLoaderContextAccessor را در سازنده کلاس تزریق می‌کنیم و سپس متد Context.GetOrAddCollectionBatchLoader را فراخوانی می‌کنیم که در پارامتر اول آن، یک کلید و در پارامتر دوم آن، متد GetAccountsByOwnerIds را از IAccountRepository معرفی می‌کنیم.
سپس باید DataLoader را در متد ConfigureServices موجود در کلاس Startup ثبت کنیم. در ادامه services.AddGraphQL را مطابق زیر ویرایش می‌کنیم: 
services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped)
        .AddDataLoader(); 
اکنون پروژه را با دستور زیر اجرا کنید و سپس query قبلی را در UI.Playground اجرا کنید.
 اگر log  موجود در Terminal مربوط به  VS Code را مشاهده کنید، متوجه خواهید شد که در این حالت یک query برای تمام owner ‌ها و یک query برای تمام account ‌ها داریم.


Using Arguments in Queries and Handling Errors 
تا کنون ما  یک query را اجرا می‌کردیم که نتیجه آن بازیابی تمام owner ‌ها به همراه تمام account ‌های مربوط به هر owner بود. اکنون می‌خواهیم  براساس id، یک owner  مشخص را بازیابی کنیم. برای انجام این کار نیاز است که یک آرگومان را در query شامل کنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را همانند زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    ...
    Owner GetById(Guid id);
}

public class OwnerRepository : IOwnerRepository
{
    ...
    Owner GetById(Guid id) => _context.Owners.SingleOrDefault(o => o.Id.Equals(id));
}
سپس کلاس AppQuery  را مطابق زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        ...

        Field<OwnerType>(
            "owner",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
            resolve: context =>
            {
                var id = context.GetArgument<Guid>("ownerId");
                return repository.GetById(id);
            }
        );
    }
}
در اینجا یک فیلد را ایجاد کرده‌ایم که مقدار برگشتی آن یک OwnerType می‌باشد. نام query را owner تعیین می‌کنیم و از بخش arguments، برای ایجاد کردن آرگومان‌های این query استفاده می‌کنیم. آرگومان این query نمی‌تواند NULL باشد و باید از نوع IdGraphType و با نام ownerId باشد و در نهایت بخش resolve است که کاملا گویا می‌باشد. 
اگر پارامتر id، از نوع Guid نباشد، بهتر است که یک پیام را به سمت کلاینت برگشت دهیم. جهت انجام این کار یک اصلاح کوچک در بخش resolve انجام میدهیم:  
Field<OwnerType>(
    "owner",
    arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }),
    resolve: context =>
    {
        Guid id;
        if (!Guid.TryParse(context.GetArgument<string>("ownerId"), out id))
        {
            context.Errors.Add(new ExecutionError("Wrong value for guid"));
            return null;
        }
 
         return repository.GetById(id);
     }
);

 اکنون پروژه را اجرا کنید و سپس یک query جدید را در  UI.Playground به صورت زیر ارسال  کنید: 
{
  owner(ownerId:"6f513773-be46-4001-8adc-2e7f17d52d83"){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type,
      ownerId
    }
  }
که نتیجه آن بازیابی یک owner  با ( Id=6f513773-be46-4001-8adc-2e7f17d52d83 ) می‌باشد.

نکته: 
در صورتیکه قصد داشته باشیم علاوه بر id، یک name را هم ارسال کنیم، در بخش resolve به صورت زیر آن را دریافت می‌کنیم: 
 string name = context.GetArgument<string>("name");
و در زمان ارسال query:
{
  owner(ownerId:"53270061-3ba1-4aa6-b937-1f6bc57d04d2", name:"ANDY") {
   ...
  }
}


Aliases, Fragments, Named Queries, Variables, Directives 
می توانیم برای query ‌های ارسال شده از سمت کلاینت با معرفی aliases، یک سری تغییرات را داشته باشیم. وقتی‌که می‌خواهیم نام نتیجه دریافتی یا هر فیلدی را در نتیجه دریافتی تغییر دهیم، بسیار کاربردی می‌باشند. اگر یک query داشته باشیم که یک آرگومان را دارد و بخواهیم دو تا از این query داشته باشیم، برای ایجاد تفاوت بین query ‌ها می‌توان از aliases  استفاده کرد. 
جهت استفاده باید نام مورد نظر را در ابتدای query یا فیلد قرار دهیم:
{
  first:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  },
  second:owners{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
  }
}
اینبار در خروجی بجای ownerId ، id و بجای ownerName ، name و ... را مشاهده خواهید کرد.


همانطور که از مثال بالا مشخص است، دو query با فیلد‌های یکسانی را داریم. اگر بجای 2  query یکسان (مانند مثال بالا) ولی با آرگومان‌های متفاوت، اینبار 10 query یکسان با آرگومان‌های متفاوتی را داشته باشیم، در این حالت خواندن query ‌ها مقداری سخت می‌باشد. در این صورت می‌توان این مشکل را با استفاده از fragment‌ها برطرف کرد. Fragment‌‌ها این اجازه را به ما می‌دهند تا فیلد‌ها را با استفاده از کاما ( ، ) از یکدیگر جدا و تبدیل به یک بخش مجزا کنیم و سپس استفاده مجدد از آن بخش را در تمام query ‌ها داشته باشیم. Syntax آن به حالت زیر می‌باشد: 
fragment SampleName on Type{
  ...
}
تعریف یک fragment به نام ownerFields  و استفاده از آن : 
{
  first:owners{
    ...ownerFields
  },
  second:owners{
    ...ownerFields
  },
  ...
}


fragment ownerFields on OwnerType{
    ownerId:id,
    ownerName:name,
    ownerAddress:address,
    ownerAccounts:accounts
    {
      accountId:id,
      accountDescription:description,
      accountType:type
    }
}

برای ایجاد کردن یک named query، مجبور هستیم از کلمه کلیدی query در آغاز کل query استفاده کنیم؛ به همراه نام query، که بعد از کلمه کلیدی query قرار میگیرد. اگر نیاز داشته باشیم می‌توان آرگومان‌ها را به query ارسال کرد.
نکته مهمی که در رابطه با named query ‌ها وجود دارد این است که اگر یک query آرگومان داشته باشد نیاز است از پنجره QUERY VARIABLES برای تخصیص مقدار به آن آرگومان استفاده کنیم. 
query OwnerQuery($ownerId:ID!)
{
  owner(ownerId:$ownerId){
    id,
    name,
    address,
    accounts{
      id,
      description,
      type
    }
  }
}
و سپس در قسمت QUERY VARIABLES 
{
  "ownerId":"6f513773-be46-4001-8adc-2e7f17d52d83"
}
اکنون اجرا کنید و خروجی را مشاهده کنید .

در نهایت می‌توان بعضی فیلد‌ها را از نتیجه دریافتی با استفاده از directive‌ها در query حذف یا اضافه کرد. دو directive وجود دارد که می‌توان از آن‌ها استفاده کرد (include  و skip). 


در قسمت بعد در رابطه با GraphQL Mutations صحبت خواهیم کرد.  
کد‌های مربوط به این قسمت را از اینجا دریافت کنید .  ASPCoreGraphQL_2.zip  
نظرات مطالب
روش‌هایی برای بهبود تجربه‌ی کاربری صفحات لاگین و ثبت نام
وجود ندارد به معنای عدم امکان افزودن آن‌ها نیست. تمام کامپوننت‌های استاندارد Blazor به همراه خاصیت زیر هم هستند:
[Parameter(CaptureUnmatchedValues = true)] 
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
با تعریف پارامتر از نوع CaptureUnmatchedValues = true، تمام ویژگی‌های اضافه شده که به صورت پارامتر عمومی کامپوننت نیستند، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار می‌گیرند. یعنی همینقدر که ویژگی "autocomplete="new-password را به تعریف کامپوننت اضافه کردید (و هر مورد مشابه دیگری را)، یک Unmatched Value است و به صورت خودکار در حین رندر نهایی، در المان اضافه شده‌ی به صفحه، ظاهر می‌شود.