مطالب
بررسی تغییرات Blazor 8x - قسمت یازدهم - قالب جدید پیاده سازی اعتبارسنجی و احراز هویت - بخش اول
قالب‌های پیش‌فرض Blazor 8x، به همراه قسمت بازنویسی شده‌ی ASP.NET Core Identity برای Blazor هم هستند که در این قسمت به بررسی نحوه‌ی عملکرد آن‌ها می‌پردازیم.


معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8 به همراه قسمت Identity

در قسمت دوم این سری، با قالب‌های جدید شروع پروژه‌های Blazor 8x آشنا شدیم و هدف ما در آنجا بیشتر بررسی حالت‌های مختلف رندر Blazor در دات نت 8 بود. تمام این قالب‌ها به همراه یک سوئیچ دیگر هم به نام auth-- هستند که توسط آن و با مقدار دهی Individual که به معنای Individual accounts است، می‌توان کدهای پیش‌فرض و ابتدایی Identity UI جدید را نیز به قالب در حال ایجاد، به صورت خودکار اضافه کرد؛ یعنی به صورت زیر:

اجرای قسمت‌های تعاملی برنامه بر روی سرور؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Server --auth Individual

اجرای قسمت‌های تعاملی برنامه در مرورگر، توسط فناوری وب‌اسمبلی؛ به همراه کدهای Identity:
dotnet new blazor --interactivity WebAssembly --auth Individual

برای اجرای قسمت‌های تعاملی برنامه، ابتدا حالت Server فعالسازی می‌شود تا فایل‌های WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده می‌کند؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Auto --auth Individual

فقط از حالت SSR یا همان static server rendering استفاده می‌شود (این نوع برنامه‌ها تعاملی نیستند)؛ به همراه کدهای Identity:
dotnet new blazor --interactivity None --auth Individual

 و یا توسط پرچم all-interactive--، که interactive render mode را در ریشه‌ی برنامه قرار می‌دهد؛ به همراه کدهای Identity:
 dotnet new blazor --all-interactive --auth Individual

یک نکته: بانک اطلاعاتی پیش‌فرض مورد استفاده‌ی در این نوع پروژه‌ها، SQLite است. برای تغییر آن می‌توانید از پرچم use-local-db-- هم استفاده کنید تا از LocalDB بجای SQLite استفاده کند.


Identity UI جدید Blazor در دات نت 8، یک بازنویسی کامل است


در نگارش‌های قبلی Blazor هم امکان افزودن Identity UI، به پروژه‌های Blazor وجود داشت (اطلاعات بیشتر)، اما ... آن پیاده سازی، کاملا مبتنی بر Razor pages بود. یعنی کاربر، برای کار با آن مجبور بود از فضای برای مثال Blazor Server خارج شده و وارد فضای جدید ASP.NET Core شود تا بتواند، Identity UI نوشته شده‌ی با صفحات cshtml. را اجرا کند و به اجزای آن دسترسی پیدا کند (یعنی عملا آن قسمت UI اعتبارسنجی، Blazor ای نبود) و پس از اینکار، مجددا به سمت برنامه‌ی Blazor هدایت شود؛ اما ... این مشکل در دات نت 8 با ارائه‌ی صفحات SSR برطرف شده‌است.
همانطور که در قسمت قبل نیز بررسی کردیم، صفحات SSR، طول عمر کوتاهی دارند و هدف آن‌ها تنها ارسال یک HTML استاتیک به مرورگر کاربر است؛ اما ... دسترسی کاملی را به HttpContext برنامه‌ی سمت سرور دارند و این دقیقا چیزی است که زیر ساخت Identity، برای کار و تامین کوکی‌های مورد نیاز خودش، احتیاج دارد. صفحات Identity UI از یک طرف از HttpContext برای نوشتن کوکی لاگین موفقیت آمیز در مرورگر کاربر استفاده می‌کنند (در این سیستم، هرجائی متدهای XyzSignInAsync وجود دارد، در پشت صحنه و در پایان کار، سبب درج یک کوکی اعتبارسنجی و احراز هویت در مرورگر کاربر نیز خواهد شد) و از طرف دیگر با استفاده از میان‌افزارهای اعتبارسنجی و احراز هویت ASP.NET Core، این کوکی‌ها را به صورت خودکار پردازش کرده و از آن‌ها جهت ساخت خودکار شیء User قابل دسترسی در این صفحات (شیء context.User.Identity@)، استفاده می‌کنند.
به همین جهت تمام صفحات Identity UI ارائه شده در Blazor 8x، از نوع SSR هستند و اگر در آن‌ها از فرمی استفاده شده، دقیقا همان فرم‌های تعاملی است که در قسمت چهارم این سری بررسی کردیم. یعنی صرفا بخاطر داشتن یک فرم، ضرورتی به استفاده‌ی از جزایر تعاملی Blazor Server و یا Blazor WASM را ندیده‌اند و اینگونه فرم‌ها را بر اساس مدل جدید فرم‌های تعاملی Blazor 8x پیاده سازی کرده‌اند. بنابراین این صفحات کاملا یکدست هستند و از ابتدا تا انتها، به صورت یکپارچه بر اساس مدل SSR کار می‌کنند (که در اصل خیلی شبیه به Razor pages یا Viewهای MVC هستند) و جزیره، جزیره‌ای، طراحی نشده‌اند.

 
روش دسترسی به HttpContext در صفحات SSR

اگر به کدهای Identity UI قالب آغازین یک پروژه که در ابتدای بحث روش تولید آن‌ها بیان شد، مراجعه کنید، استفاده از یک چنین «پارامترهای آبشاری» را می‌توان مشاهده کرد:

@code {

    [CascadingParameter]
    public HttpContext HttpContext { get; set; } = default!;
متد ()AddRazorComponents ای که در فایل Program.cs اضافه می‌شود، کار ثبت CascadingParameter ویژه‌ی فوق را به صورت زیر انجام می‌دهد که در Blazor 8x به آن یک Root-level cascading value می‌گویند:
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
مقادیر آبشاری برای ارسال اطلاعاتی بین درختی از کامپوننت‌ها، از یک والد به چندین فرزند که چندین لایه ذیل آن واقع شده‌‌اند، مفید است. برای مثال فرض کنید می‌خواهید اطلاعات عمومی تنظیمات یک کاربر را به چندین زیر کامپوننت، ارسال کنید. یک روش آن، ارسال شیء آن به صورت پارامتر، به تک تک آن‌ها است و راه دیگر، تعریف یک CascadingParameter است؛ شبیه به کاری که در اینجا انجام شده‌است تا دیگر نیازی نباشد تا تک تک زیر کامپوننت‌ها این شیء را به یک لایه زیریرین خود، یکی یکی منتقل کنند.

در کدهای Identity UI ارائه شده، از این CascadingParameter برای مثال جهت خروج از برنامه (HttpContext.SignOutAsync) و یا دسترسی به اطلاعات هدرهای رسید (if (HttpMethods.IsGet(HttpContext.Request.Method)))، دسترسی به سرویس‌ها (()<HttpContext.Features.Get<ITrackingConsentFeature)، تامین مقدار Cancellation Token به کمک HttpContext.RequestAborted و یا حتی در صفحه‌ی جدید Error.razor که آن نیز یک صفحه‌ی SSR است، برای دریافت HttpContext?.TraceIdentifier استفاده‌ی مستقیم شده‌است.

نکته‌ی مهم: باید به‌خاطر داشت که فقط و فقط در صفحات SSR است که می‌توان به HttpContext به نحو آبشاری فوق دسترسی یافت و همانطور که در قسمت قبل نیز بررسی شد، سایر حالات رندر Blazor از دسترسی به آن، پشتیبانی نمی‌کنند و اگر چنین پارامتری را تنظیم کردید، نال دریافت می‌کنید.


بررسی تفاوت‌های تنظیمات ابتدایی قالب جدید Identity UI در Blazor 8x با نگارش‌های قبلی آن

مطالب و نکات مرتبط با قالب قبلی را در مطلب «Blazor 5x - قسمت 22 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 2 - ورود به سیستم و خروج از آن» می‌توانید مشاهده کنید.

در قسمت سوم این سری، روش ارتقاء یک برنامه‌ی قدیمی Blazor Server را به نگارش 8x آن بررسی کردیم و یکی از تغییرات مهم آن، حذف فایل‌های cshtml. ای آغاز برنامه و انتقال وظایف آن، به فایل جدید App.razor و انتقال مسیریاب Blazor به فایل جدید Routes.razor است که پیشتر در فایل App.razor نگارش‌های قبلی Blazor وجود داشت.
در این نگارش جدید، محتوای فایل Routes.razor به همراه قالب Identity UI به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>
در نگارش‌های قبلی، این مسیریاب داخل کامپوننت CascadingAuthenticationState محصور می‌شد تا توسط آن بتوان AuthenticationState را به تمام کامپوننت‌های برنامه ارسال کرد. به این ترتیب برای مثال کامپوننت AuthorizeView، شروع به کار می‌کند و یا توسط شیء context.User می‌توان به User claims دسترسی یافت و یا به کمک ویژگی [Authorize]، دسترسی به صفحه‌ای را محدود به کاربران اعتبارسنجی شده کرد.
اما ... در اینجا با یک نگارش ساده شده سروکار داریم؛ از این جهت که برنامه‌های جدید، به همراه جزایر تعاملی هم می‌توانند باشند و باید بتوان این AuthenticationState را به آن‌ها هم ارسال کرد. به همین جهت مفهوم جدید مقادیر آبشاری سطح ریشه (Root-level cascading values) که پیشتر در این بحث معرفی شد، در اینجا برای معرفی AuthenticationState استفاده شده‌است.

در اینجا کامپوننت جدید FocusOnNavigate را هم مشاهده می‌کنید. با استفاده از این کامپوننت و براساس CSS Selector معرفی شده، پس از هدایت به یک صفحه‌ی جدید، این المان مشخص شده دارای focus خواهد شد. هدف از آن، اطلاع رسانی به screen readerها در مورد هدایت به صفحه‌ای دیگر است (برای مثال، کمک به نابیناها برای درک بهتر وضعیت جاری).

همچنین در اینجا المان NotFound را هم مشاهده نمی‌کنید. این المان فقط در برنامه‌های WASM جهت سازگاری با نگارش‌های قبلی، هنوز هم قابل استفاده‌است. جایگزین آن‌را در قسمت سوم این سری، برای برنامه‌های Blazor server در بخش «ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده» آن بررسی کردیم.


روش انتقال اطلاعات سراسری اعتبارسنجی یک کاربر به کامپوننت‌ها و جزایر تعاملی واقع در صفحات SSR

مشکل! زمانیکه از ترکیب صفحات SSR و جزایر تعاملی واقع در آن استفاده می‌کنیم ... جزایر واقع در آن‌ها دیگر این مقادیر آبشاری را دریافت نمی‌کنند و این مقادیر در آن‌ها نال است. برای حل این مشکل در Blazor 8x، باید مقادیر آبشاری سطح ریشه را (Root-level cascading values) به صورت زیر در فایل Program.cs برنامه ثبت کرد:
builder.Services.AddCascadingValue(sp =>new Preferences { Theme ="Dark" });
پس از این تغییر، اکنون نه فقط صفحات SSR، بلکه جزایر واقع در آن‌ها نیز توسط ویژگی [CascadingParameter] می‌توانند به این مقدار آبشاری، دسترسی داشته باشند. به این ترتیب است که در برنامه‌های Blazor، کامپوننت‌های تعاملی هم دسترسی به اطلاعات شخص لاگین شده‌ی توسط صفحات SSR را دارند. برای مثال اگر به فایل Program.cs قالب جدید Identity UI عنوان شده مراجعه کنید، چنین سطوری در آن قابل مشاهده هستند
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
متد جدید AddCascadingAuthenticationState، فقط کار ثبت AuthenticationStateProvider برنامه را به صورت آبشاری انجام می‌دهد.
این AuthenticationStateProvider سفارشی جدید هم در فایل اختصاصی IdentityRevalidatingAuthenticationStateProvider.cs پوشه‌ی Identity قالب پروژه‌های جدید Blazor 8x که به همراه اعتبارسنجی هستند، قابل مشاهده‌است.

یا اگر به قالب‌های پروژه‌های WASM دار مراجعه کنید، تعریف به این صورت تغییر کرده‌است؛ اما مفهوم آن یکی است:
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
در این پروژه‌ها، یک AuthenticationStateProvider سفارشی دیگری در فایل PersistingServerAuthenticationStateProvider.cs ارائه شده‌است (این فایل‌ها، جزو استاندارد قالب‌های جدید Identity UI پروژه‌های Blazor 8x هستند؛ فقط کافی است، یک نمونه پروژه‌ی آزمایشی را با سوئیچ جدید auth Individual-- ایجاد کنید، تا بتوانید این فایل‌های یاد شده را مشاهده نمائید).

AuthenticationStateProviderهای سفارشی مانند کلاس‌های IdentityRevalidatingAuthenticationStateProvider و PersistingServerAuthenticationStateProvider که در این قالب‌ها موجود هستند، چون به صورت آبشاری کار تامین AuthenticationState را انجام می‌دهند، به این ترتیب می‌توان به شیء context.User.Identity@ در جزایر تعاملی نیز به سادگی دسترسی داشت.

یعنی به صورت خلاصه، یکبار قرارداد AuthenticationStateProvider عمومی (بدون هیچ نوع پیاده سازی) به صورت یک Root-level cascading value ثبت می‌شود تا در کل برنامه قابل دسترسی شود. سپس یک پیاده سازی اختصاصی از آن توسط ()<builder.Services.AddScoped<AuthenticationStateProvider, XyzProvider به برنامه معرفی می‌شود تا آن‌را تامین کند. به این ترتیب زیر ساخت امن سازی صفحات با استفاده از ویژگی attribute [Authorize]@ و یا دسترسی به اطلاعات کاربر جاری با استفاده از شیء context.User@ و یا امکان استفاده از کامپوننت AuthorizeView برای نمایش اطلاعاتی ویژه به کاربران اعتبارسنجی شده مانند صفحه‌ی Auth.razor زیر، مهیا می‌شود:
@page "/auth"

@using Microsoft.AspNetCore.Authorization

@attribute [Authorize]

<PageTitle>Auth</PageTitle>

<h1>You are authenticated</h1>

<AuthorizeView>
    Hello @context.User.Identity?.Name!
</AuthorizeView>

سؤال: چگونه یک پروژه‌ی سمت سرور، اطلاعات اعتبارسنجی خودش را با یک پروژه‌ی WASM سمت کلاینت به اشتراک می‌گذارد (برای مثال در حالت رندر Auto)؟ این انتقال اطلاعات باتوجه به یکسان نبودن محل اجرای این دو پروژه (یکی بر روی کامپیوتر سرور و دیگری بر روی مرورگر کلاینت، در کامپیوتری دیگر) چگونه انجام می‌شود؟ این مورد را در قسمت بعد، با معرفی PersistentComponentState و «فیلدهای مخفی» مرتبط با آن، بررسی می‌کنیم.
مطالب
آشنایی با FileTable در SQL Server 2012 بخش 1
پیش از آشنایی با FileTable نیاز است که پیشینه‌ای از شیوه‌های ذخیره‌سازی فایل و یا بهتر بگویم BLOB در SQL Server را داشته باشیم. نخستین شیوه‌ی نگه‌داری فایل استفاده از Image است که در SQL Server 2000 کاربرد داشت و هم‌اکنون استفاده از آن به دلیل کاهش بسیار کارآیی منسوخ‌شده است. به دلایل مشکلات بسیار فراوان Image هم‌زمان بسیاری از طراحان پایگاه داده‌ها، جهت کاهش حجم جدول‌ها و پیروی آن حجم پایگاه داده‌ها، فایل را در سیستم‌فایل نگه‌داری می‌کردند و تنها مسیر آن را در فیلدی از نوع کاراکتری در پایگاه‌داده‌ها ذخیره می‌کردند. این روش هرچند از حجم پایگاه داده‌ها می‌کاست ولی به دلیل عدم دخالت SQL Server در مدیریت فایل‌ها مشکلات دیگری را به وجود آورد.
از SQL Server 2005 نوع داده‌ی varbinary(max) معرفی شد که برخی از چالش‌های به‌کاربری Image را کاست و درباره‌ی بسیاری از موارد مانند ذخیره‌ی عکس پرسنلی هنوز هم کاربرد دارد؛ ولی توجه داشته باشید که استفاده از این فیلد فقط برای فایل‌های کم‌تر از 256 کیلوبایت سفارش شده است و برای بالاتر از آن، کارآیی کاهش فراوانی خواهد یافت.
در  SQL Server 2008 نوع داده‌ی جدیدی به نام FileStream به وجود آمد به این شکل که یک FileGroup از نوع  Data FileStream به پایگاه‌داده افزوده می‌شود و در واقع با یک پوشه در سیستم فایل در پیوند است. از این پس هنگام ساخت یک جدول به جای استفاده از نوع داده‌ی varbinary از نوع FileStream استفاده می‌کنیم با مد نظر داشتن این نکته که حتماً باید یک فیلد از نوع Uniqueidentifier هم در آن جدول تعریف شده باشد. شیوه‌ی کار نیز به این صورت خواهد بود که خود رکورد در جدول ذخیره می‌شود و فقط محتوای فایل در آن مسیری از NTFS ذخیره می‌شود. برخلاف روش درج مسیر فایل در جدول که پس از حذف رکورد، فایل هم‌چنان در سیستم فایل می‌ماند؛ این بار با حذف رکورد فایل مربوطه نیز حذف خواهد شد. افزون بر این مدیریت پشتیبانی از فایل‌ها نیز برعهده‌ی پایگاه داده‌ها خواهد بود. اندازه‌ی فایل‌ها در FileStream محدودیت‌های پیشین را نخواهد داشت و شما به اندازه‌ی حجم درایو هارددیسک می‌توانید فایل در آن ذخیره کنید. نکته‌ی دیگر درباره‌ی فایل‌های با حجم سنگین که می‌توانید Stream مربوط به یک فایل را به صورت بخش‌بخش در سمت مشتری بارگذاری کنید و به او نشان دهید. در FileStream امنیت و تراکنش فایل‌ها برعهده‌ی SQL Server است و از این دیدگاه بسیار ساده‌تر و کارآتر از FileSystem است. (برای آشنایی بیشتر با FileStream، این نوشتار از مهندس وحید نصیری را مطالعه کنید.)
گونه‌ی FileTable از ویژگی‌های نوین SQL Server 2012 است که تکمیل‌کننده‌ی FileStream است. FileTable آمیزشی از FileStream با hierarchyid و سیستم فایل ویندوز برای ارائه‌ی توانایی‌های نوین مدیریت BLOB در  SQL Server است. FileTable همان‌گونه که از دو واژه‌ی تشکیل‌دهنده‌اش پیداست؛ هم‌زمان یک جدول و یک سیستم فایل معمولی است.
FileTable به هر روی یک جدول از پایگاه‌داده‌های SQL Server است با یک تفاوت که ساختار آن از پیش تعریف‌شده است. ستون‌های FileTable و نوع داده‌ی آن از پیش توسط SQL Server  مشخص شده است. ستون‌های تشکیل‌دهنده‌ی FileTable دربرگیرنده‌ی جدول زیر است:

هر ردیف از FileTable نماینده‌ی یک فایل یا پوشه در File System است. ستون path_locator که از نوع hierarchyid است نشان‌دهنده‌ی مسیر یک فایل یا پوشه است. hierarchyid که از SQL Server 2008 معرفی شده است؛ بهترین نوع داده برای نگه‌داری ارتباط ساختار سلسله‌مراتبی مانند چارت سازمانی، درخت تجهیزات یک کارخانه و یا در همین نمونه درخت فایل‌ها و پوشه‌ها است. پس می‌توانیم از همه‌ی امکانات hierarchyid در FileTable نیز برخوردار شویم. این‌که این فایل به ترتیب در چه پوشه‌هایی قرار گرفته است یا این‌که این پوشه شامل چه فایل‌ها یا پوشه‌هایی خواهد بود. این‌که پوشه‌های هم‌فرزند پوشه‌ی جاری کدام است و یا یا توابع مربوط به جابه‌جایی فایل‌ها و پوشه‌ها.

دنباله دارد...

مسیرراه‌ها
ASP.NET MVC
              مطالب
              اعتبارسنجی سایتهای چند زبانه در ASP.NET MVC - قسمت اول

              اگر در حال تهیه یک سایت چند زبانه هستید و همچنین سری مقالات Globalization در ASP.NET MVC رو دنبال کرده باشید میدانید که با تغییر Culture فایلهای Resource مورد نظر بارگذاری و نوشته‌های سایت تغییر میابند ولی با تغییر Culture رفتار اعتبارسنجی در سمت سرور نیز تغییر و اعتبارسنجی بر اساس Culture فعلی سایت انجام میگیرد. بررسی این موضوع را با یک مثال شروع میکنیم.

              یک پروژه وب بسازید سپس به پوشه Models یک کلاس با نام ValueModel اضافه کنید. تعریف کلاس به شکل زیر هست: 

              public class ValueModel
              {
                  [Required]
                  [Display(Name = "Decimal Value")]
                  public decimal DecimalValue { get; set; }
              
                  [Required]
                  [Display(Name = "Double Value")]
                  public double DoubleValue { get; set; }
              
                  [Required]
                  [Display(Name = "Integer Value")]
                  public int IntegerValue { get; set; }
              
                  [Required]
                  [Display(Name = "Date Value")]
                  public DateTime DateValue { get; set; }
              }

              به سراغ کلاس HomeController بروید و کدهای زیر را اضافه کنید: 

              [HttpPost]
              public ActionResult Index(ValueModel valueModel)
              {
                  if (ModelState.IsValid)
                  {
                      return Redirect("Index");
                  }
              
                  return View(valueModel);
              }

              Culture را به fa-IR تغییر میدهیم، برای اینکار در فایل web.config در بخش system.web کد زیر اضافه نمایید: 

              <globalization culture="fa-IR" uiCulture="fa-IR" />

              و در نهایت به سراغ فایل Index.cshtml بروید کدهای زیر رو اضافه کنید:

              @using (Html.BeginForm())
              {
                  <ol>
                      <li>
                          @Html.LabelFor(m => m.DecimalValue)
                          @Html.TextBoxFor(m => m.DecimalValue)
                          @Html.ValidationMessageFor(m => m.DecimalValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.DoubleValue)
                          @Html.TextBoxFor(m => m.DoubleValue)
                          @Html.ValidationMessageFor(m => m.DoubleValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.IntegerValue)
                          @Html.TextBoxFor(m => m.IntegerValue)
                          @Html.ValidationMessageFor(m => m.IntegerValue)
                      </li>
                      <li>
                          @Html.LabelFor(m => m.DateValue)
                          @Html.TextBoxFor(m => m.DateValue)
                          @Html.ValidationMessageFor(m => m.DateValue)
                      </li>
                      <li>
                          <input type="submit" value="Submit"/>
                      </li>
                  </ol>
              }

              پرژه را اجرا نمایید و در ٢ تکست باکس اول ٢ عدد اعشاری را و در ٢ تکست باکس آخر یک عدد صحیح و یک تاریخ وارد نمایید و سپس دکمه Submit را بزنید. پس از بازگشت صفحه از سمت سرور در در ٢ تکست باکس اول با این پیامها روبرو میشوید که مقادیر وارد شده نامعتبر میباشند. 

              اگر پروژه رو در حالت دیباگ اجرا کنیم و نگاهی به داخل ModelState بیاندازیم، میبینیم که کاراکتر جدا کننده قسمت اعشاری برای fa-IR '/' میباشد که در اینجا برای اعداد مورد نظر کاراکتر '.' وارد شده است. 

              برای فایق شدن بر این مشکل یا باید سمت سرور اقدام کرد یا در سمت کلاینت. در بخش اول راه حل سمت کلاینت را بررسی مینماییم. 

              در سمت کلاینت برای اینکه کاربر را مجبور به وارد کردن کاراکترهای مربوط به Culture فعلی سایت نماییم باید مقادیر وارد شده را اعتبارسنجی و در صورت معتبر نبودن مقادیر پیام مناسب نشان داده شود. برای اینکار از کتابخانه jQuery Globalize استفاده میکنیم. برای اضافه کردن jQuery Globalize از طریق کنسول nuget فرمان زیر اجرا نمایید: 

              PM> Install-Package jquery-globalize

               پس از نصب کتابخانه  اگر به پوشه Scripts نگاهی بیاندازید میبینید که پوشەای با نام jquery.globalize اضافه شده است. درداخل پوشه زیر پوشەی دیگری با نام cultures وجود دارد که در آن Cultureهای مختلف وجود دارد و بسته به نیاز میتوان از آنها استفاده کرد. دوباره به سراغ فایل Index.cshtm بروید و فایلهای جاوا اسکریپتی زیر را به صفحه اضافه کنید:

              <script src="~/Scripts/jquery.validate.js"> </script>
              <script src="~/Scripts/jquery.validate.unobtrusive.js"> </script>
              <script src="~/Scripts/jquery.globalize/globalize.js"> </script>
              <script src="~/Scripts/jquery.globalize/cultures/globalize.culture.fa-IR.js"> </script>

              در فایل globalize.culture.fa-IR.js کاراکتر جدا کننده اعشاری '.' در نظر گرفته شده است که مجبور به تغییر آن هسیتم. برای اینکار فایل را باز کرده و numberFormat را پیدا کنید و آن را به شکل زیر تغییر دهید: 

              numberFormat: {
                  pattern: ["n-"],
                  ".": "/",
                  currency: {
                      pattern: ["$n-", "$ n"],
                      ".": "/",
                      symbol: "ریال"
                  }
              },

              و در نهایت کدهای زیر را به فایل Index.cshtml اضافه کنید و برنامه را دوباره اجرا نمایید:

              Globalize.culture('fa-IR');
              $.validator.methods.number = function(value, element) {
                  if (value.indexOf('.') > 0) {
                      return false;
                  }
                  var splitedValue = value.split('/');
                  if (splitedValue.length === 1) {
                      return !isNaN(Globalize.parseInt(value));
                  } else if (splitedValue.length === 2 && $.trim(splitedValue[1]).length === 0) {
                      return false;
                  }
                  return !isNaN(Globalize.parseFloat(value));
              };
              };

              در خط اول Culture را ست مینمایم و در ادامه نحوه اعتبارسنجی را در unobtrusive validation تغییر میدهیم. از آنجایی که برای اعتبارسنجی عدد وارد شده از تابع parseFloat استفاده میشود، کاراکتر جدا کننده قسمت اعشاری قابل قبول برای این تابع '.' است پس در داخل تابع دوباره '/' به '.' تبدیل میشود و سپس اعتبارسنجی انجام میشود از اینرو اگر کاربر '.' را نیز وارد نماید قابل قبول است به همین دلیل با این خط کد if (value.indexOf('.') > 0) وجود نقطه را بررسی میکنیم تا در صورت وجود '.' پیغام خطا نشان داده شود.در خط بعدی بررسی مینماییم که اگر عدد وارد شده اعشاری نباشد از تابع parseInt  استفاده نماییم. در خط بعدی این حالت را بررسی مینماییم که اگر کاربر عددی همچون /١٢ وارد کرد پیغام خطا صادر شود. 

              برای اعتبارسنجی تاریخ شمسی متاسفانه توابع کمکی برای تبدیل تاریخ در فایل globalize.culture.fa-IR.js وجود ندارد ولی اگر نگاهی به فایلهای Culture عربی بیاندازید همه دارای توابع کمکی برای تبدیل تاریج هجری به میلادی هستند به همین دلیل امکان اعتبارسنجی تاریخ شمسی با استفاده از jQuery Globalize میسر نمیباشد. من خودم تعدادی توابع کمکی را به globalize.culture.fa-IR.js اضافه کردەام که از تقویم فارسی آقای علی فرهادی برداشت شده است و با آنها کار اعتبارسنجی را انجام میدهیم. لازم به ذکر است این روش ١٠٠% تست نشده است و شاید راه کاملا اصولی نباشد ولی به هر حال در اینجا توضیح میدهم. در فایل globalize.culture.fa-IR.js قسمت Gregorian_Localized را پیدا کنید و آن را با کدهای زیر جایگزین کنید: 

              Gregorian_Localized: {
                  firstDay: 6,
                  days: {
                      names: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"],
                      namesAbbr: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"],
                      namesShort: ["ی", "د", "س", "چ", "پ", "ج", "ش"]
                  },
                  months: {
                      names: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""],
                      namesAbbr: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""]
                  },
                  AM: ["ق.ظ", "ق.ظ", "ق.ظ"],
                  PM: ["ب.ظ", "ب.ظ", "ب.ظ"],
                  patterns: {
                      d: "yyyy/MM/dd",
                      D: "yyyy/MM/dd",
                      t: "hh:mm tt",
                      T: "hh:mm:ss tt",
                      f: "yyyy/MM/dd hh:mm tt",
                      F: "yyyy/MM/dd hh:mm:ss tt",
                      M: "dd MMMM"
                  },
                  JalaliDate: {
                      g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
                      j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
                  },
                  gregorianToJalali: function (gY, gM, gD) {
                      gY = parseInt(gY);
                      gM = parseInt(gM);
                      gD = parseInt(gD);
                      var gy = gY - 1600;
                      var gm = gM - 1;
                      var gd = gD - 1;
              
                      var gDayNo = 365 * gy + parseInt((gy + 3) / 4) - parseInt((gy + 99) / 100) + parseInt((gy + 399) / 400);
              
                      for (var i = 0; i < gm; ++i)
                          gDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i];
                      if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0) || (gy % 400 == 0)))
                          /* leap and after Feb */
                          ++gDayNo;
                      gDayNo += gd;
              
                      var jDayNo = gDayNo - 79;
              
                      var jNp = parseInt(jDayNo / 12053);
                      jDayNo %= 12053;
              
                      var jy = 979 + 33 * jNp + 4 * parseInt(jDayNo / 1461);
              
                      jDayNo %= 1461;
              
                      if (jDayNo >= 366) {
                          jy += parseInt((jDayNo - 1) / 365);
                          jDayNo = (jDayNo - 1) % 365;
                      }
              
                      for (var i = 0; i < 11 && jDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; ++i) {
                          jDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i];
                      }
                      var jm = i + 1;
                      var jd = jDayNo + 1;
              
                      return [jy, jm, jd];
                  },
                  jalaliToGregorian: function (jY, jM, jD) {
                      jY = parseInt(jY);
                      jM = parseInt(jM);
                      jD = parseInt(jD);
                      var jy = jY - 979;
                      var jm = jM - 1;
                      var jd = jD - 1;
              
                      var jDayNo = 365 * jy + parseInt(jy / 33) * 8 + parseInt((jy % 33 + 3) / 4);
                      for (var i = 0; i < jm; ++i) jDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i];
              
                      jDayNo += jd;
              
                      var gDayNo = jDayNo + 79;
              
                      var gy = 1600 + 400 * parseInt(gDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */
                      gDayNo = gDayNo % 146097;
              
                      var leap = true;
                      if (gDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ {
                          gDayNo--;
                          gy += 100 * parseInt(gDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */
                          gDayNo = gDayNo % 36524;
              
                          if (gDayNo >= 365)
                              gDayNo++;
                          else
                              leap = false;
                      }
              
                      gy += 4 * parseInt(gDayNo / 1461); /* 1461 = 365*4 + 4/4 */
                      gDayNo %= 1461;
              
                      if (gDayNo >= 366) {
                          leap = false;
              
                          gDayNo--;
                          gy += parseInt(gDayNo / 365);
                          gDayNo = gDayNo % 365;
                      }
              
                      for (var i = 0; gDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap) ; i++)
                          gDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap);
                      var gm = i + 1;
                      var gd = gDayNo + 1;
              
                      return [gy, gm, gd];
                  },
                  checkDate: function (jY, jM, jD) {
                      return !(jY < 0 || jY > 32767 || jM < 1 || jM > 12 || jD < 1 || jD >
                          (Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[jM - 1] + (jM == 12 && !((jY - 979) % 33 % 4))));
                  },
                  convert: function (value, format) {
                      var day, month, year;
              
                      var formatParts = format.split('/');
                      var dateParts = value.split('/');
                      if (formatParts.length !== 3 || dateParts.length !== 3) {
                          return false;
                      }
              
                      for (var j = 0; j < formatParts.length; j++) {
                          var currentFormat = formatParts[j];
                          var currentDate = dateParts[j];
                          switch (currentFormat) {
                              case 'dd':
                                  if (currentDate.length === 2 || currentDate.length === 1) {
                                      day = currentDate;
                                  } else {
                                      year = currentDate;
                                  }
                                  break;
                              case 'MM':
                                  month = currentDate;
                                  break;
                              case 'yyyy':
                                  if (currentDate.length === 4) {
                                      year = currentDate;
                                  } else {
                                      day = currentDate;
                                  }
                                  break;
                              default:
                                  return false;
                          }
                      }
              
                      year = parseInt(year);
                      month = parseInt(month);
                      day = parseInt(day);
                      var isValidDate = Globalize.culture().calendars.Gregorian_Localized.checkDate(year, month, day);
                      if (!isValidDate) {
                          return false;
                      }
              
                      var grDate = Globalize.culture().calendars.Gregorian_Localized.jalaliToGregorian(year, month, day);
                      var shDate = Globalize.culture().calendars.Gregorian_Localized.gregorianToJalali(grDate[0], grDate[1], grDate[2]);
              
                      if (year === shDate[0] && month === shDate[1] && day === shDate[2]) {
                          return true;
                      }
              
                      return false;
                  }
              },

              روال کار در تابع convert به اینصورت است که ابتدا تاریخ وارد شده را بررسی مینماید تا معتبر بودن آن معلوم شود به عنوان مثال اگر تاریخی مثل 1392/12/31 وارد شده باشد و در ادامه برای بررسی بیشتر تاریخ یک بار به میلادی و تاریخ میلادی دوباره به شمسی تبدیل میشود و با تاریخ وارد شده مقایسه میشود و در صورت برابری تاریخ معتبر اعلام میشود. در فایل Index.cshtml کدهای زیر اضافی نمایید:

              $.validator.methods.date = function (value, element) {
                  return Globalize.culture().calendars.Gregorian_Localized.convert(value, 'yyyy/MM/dd');
              };

              برای اعتبارسنجی تاریخ میتوانید از ٢ فرمت استفاده کنید:

              ١ – yyyy/MM/dd

              ٢ – dd/MM/yyyy

              البته از توابع اعتبارسنجی تاریخ میتوانید به صورت جدا استفاده نمایید و لزومی ندارد آنها را همراه با jQuery Globalize بکار ببرید. در آخر خروجی کار به این شکل است:

              در کل استفاده از jQuery Globalize برای اعتبارسنجی در سایتهای چند زبانه به نسبت خوب میباشد و برای هر زبان میتوانید از culture مورد نظر استفاده نمایید. در قسمت دوم این مطلب به بررسی بخش سمت سرور میپردازیم.

              مطالب
              نگاشت خودکار اشیاء توسط AutoMapper و Reflection - ایده شماره 2
              پیش نیاز این مطلب، قسمت قبل آن است. در قسمت قبل، یک کلاس جنریک را به نام BaseDto ایجاد کردیم که با ارث بری Dto‌های پروژه از این کلاس، علاوه بر متد‌های ToEntity و FromEntity جهت ساده سازی عملیات نگاشت، Mapping‌های لازم بین Dto‌ها و Entity‌های مربوطه، توسط Reflection به صورت خودکار انجام می‌شد.
              در این قسمت می‌خواهیم مکانیزم Mapping خودکار را کمی تغییر داده و قابلیت سفارشی سازی Mapping‌ها را فراهم کنیم. سورس کامل مثال را می‌توانید در این  ریپازیتوری  مشاهده کنید. 
              ابتدا یک اینترفیس را به نام IHaveCustomMapping به نحو زیر ایجاد می‌کنیم.
              public interface IHaveCustomMapping
              {
                  void CreateMappings(AutoMapper.Profile profile);
              }
              هر کلاسی که این اینترفیس را پیاده سازی کند، در متد CreateMappings آن، یک شیء از نوع Profile را دریافت می‌کند و می‌تواند تمامی کانفیگ Mapping‌های دلخواه را اعمال کند.
              به عنوان مثال کلاس زیر، Mapping لازم برای PostDto و Post را درون متد CreateMappings خود اعمال می‌کند.
              public class PostDtoMapping : IHaveCustomMapping
              {
                  public void CreateMappings(Profile profile)
                  {
                      profile.CreateMap<PostDto, Post>().ReverseMap();
                  }
              }
              اکنون لازم است تدبیری بیاندیشیم تا کلاس‌هایی را که از اینترفیس IHaveCustomMapping مشتق شده‌اند، به AutoMapper معرفی کنیم. در واقع باید کلاس‌های مذکور (مانند PostDtoMapping) را یافته، یک وهله از آنها را ایجاد کنیم، سپس متد CreateMappings آنها فراخوانی کرده و شیء ای از نوع Profile را به عنوان ورودی به آن پاس دهیم.
              بدین منظور کلاسی را به نام CustomMappingProfile به نحو زیر تعریف می‌کنیم.
              public class CustomMappingProfile : Profile
              {
                  public CustomMappingProfile(IEnumerable<IHaveCustomMapping> haveCustomMappings)
                  {
                      foreach (var item in haveCustomMappings)
                          item.CreateMappings(this);
                  }
              }
              • این کلاس از AutoMapper.Profile ارث بری کرده‌است.
              • درون سازنده‌ی خود لیستی از اشیاء اینترفیس IHaveCustomMapping را دریافت کرده و بر روی آنها گردش می‌کند.
              • و متد CreateMappings هرکدام را فراخوانی کرده و خودش (this : شی جاری) را (که از نوع Profile شده) به عنوان پارامتر ورودی پاس می‌دهد.
              اکنون کلاس AutoMapperConfiguration قسمت قبل را به نحو زیر اصلاح می‌کنیم.
              public static class AutoMapperConfiguration
              {
                  public static void InitializeAutoMapper()
                  {
                      Mapper.Initialize(config =>
                      {
                          config.AddCustomMappingProfile();
                      });
              
                      //Compile mapping after configuration to boost map speed
                      Mapper.Configuration.CompileMappings();
                  }
              
                  public static void AddCustomMappingProfile(this IMapperConfigurationExpression config)
                  {
                      config.AddCustomMappingProfile(Assembly.GetEntryAssembly());
                  }
              
                  public static void AddCustomMappingProfile(this IMapperConfigurationExpression config, params Assembly[] assemblies)
                  {
                      var allTypes = assemblies.SelectMany(a => a.ExportedTypes);
              
                      //Find all classes that implement IHaveCustomMapping inteface and create new instance of each
                      var list = allTypes.Where(type => type.IsClass && !type.IsAbstract &&
                          type.GetInterfaces().Contains(typeof(IHaveCustomMapping)))
                          .Select(type => (IHaveCustomMapping)Activator.CreateInstance(type));
              
                      //Create a new automapper Profile for this list to create mapping then add to the config
                      var profile = new CustomMappingProfile(list);
                      config.AddProfile(profile);
                  }
              }
              • توضیحات متد های InitializeAutoMapper و AddCustomMappingProfile، مشابه مطلب قبل است و لازم به ذکر مجدد نیست.
              • متد AddCustomMappingProfile آرایه‌ای از اسمبلی‌ها را دریافت و سپس تمامی نوع‌های قابل دسترس آنها را (ExportedTypes) واکشی می‌کند.
              • سپس توسط شرط Where، نوع‌هایی که کلاس بوده، abstract نیستند و از اینترفیس IHaveCustomMapping مشتق شده‌اند فیلتر می‌شوند. 
              • سپس توسط متد Activator.CreateInstance، وهله‌ای از آنها ایجاد و به نوع IHaveCustomMapping تبدیل می‌شوند و نهایتا لیستی از اشیاء وهله سازی شده را باز می‌گرداند.
              • سپس وهله‌ای از نوع CustomMappingProfile (که مسئول اعمال Mapping‌های اشیاء دریافتی است و قبلا بررسی کردیم) ایجاد می‌کنیم و لیست مذکور را به سازنده آن پاس می‌دهیم.
              • نهایتا profile ساخته شده (حاوی تمامی Mapping‌های اعمال شده) را توسط متد config.AddProfile به AutoMapper معرفی می‌کنیم (در این لحظه تمامی Mapping‌های تعریف شده داخل profile، به AutoMapper اعمال می‌شوند).
              توسط این مکانیزم، هر کلاسی که اینترفیس IHaveCustomMapping را پیاده سازی کرده باشد، به صورت خودکار یافت شده و Mapping به آنها اعمال می‌شود. حال می‌توان این مکانیزم را با BaseDto قسمت قبل ترکیب کرده و کلاس BaseDto را به نحو زیر اصلاح کنیم.
              public abstract class BaseDto<TDto, TEntity, TKey> : IHaveCustomMapping
                      where TEntity : BaseEntity<TKey>
              {
                  [Display(Name = "ردیف")]
                  public TKey Id { get; set; }
              
                  /// <summary>
                  /// Maps this dto to a new entity object.
                  /// </summary>
                  public TEntity ToEntity()
                  {
                      return Mapper.Map<TEntity>(CastToDerivedClass(this));
                  }
              
                  /// <summary>
                  /// Maps this dto to an exist entity object.
                  /// </summary>
                  public TEntity ToEntity(TEntity entity)
                  {
                      return Mapper.Map(CastToDerivedClass(this), entity);
                  }
              
                  /// <summary>
                  /// Maps the specified entity to a new dto object.
                  /// </summary>
                  public static TDto FromEntity(TEntity model)
                  {
                      return Mapper.Map<TDto>(model);
                  }
              
                  protected TDto CastToDerivedClass(BaseDto<TDto, TEntity, TKey> baseInstance)
                  {
                      return Mapper.Map<TDto>(baseInstance);
                  }
              
                  //Get automapper Profile then create mapping and ignore unmapped properties
                  public void CreateMappings(Profile profile)
                  {
                      var mappingExpression = profile.CreateMap<TDto, TEntity>();
              
                      var dtoType = typeof(TDto);
                      var entityType = typeof(TEntity);
              
                      //Ignore mapping to any property of source (like Post.Categroy) that dose not contains in destination (like PostDto)
                      //To prevent from wrong mapping. for example in mapping of "PostDto -> Post", automapper create a new instance for Category (with null catgeoryName) because we have CategoryName property that has null value
                      foreach (var property in entityType.GetProperties())
                      {
                          if (dtoType.GetProperty(property.Name) == null)
                              mappingExpression.ForMember(property.Name, opt => opt.Ignore());
                      }
              
                      //Pass mapping expressin to customize mapping in concrete class
                      CustomMappings(mappingExpression.ReverseMap());
                  }
              
                  //Concrete class can override this method to customize mapping
                  public virtual void CustomMappings(IMappingExpression<TEntity, TDto> mapping)
                  {
                  }
              }
              • کلاس جنریک BaseDto، متدCreateMappings اینترفیس IHaveCustomMapping را پیاده سازی می‌کند.
              • درون این متد، Mapping بین دو نوع TDto و TEntity، توسط ()<profile.CreateMap<TDto, TEntity کانفیگ می‌شود.
              • مانند مطلب قبل، خواصی را که نباید نگاشت شوند، توسط Reflection یافته و Ignore می‌کنیم.
              • سپس Mapping برعکس را توسط ReverseMap اعمال کرده و به متد زیرین آن که virtual نیز است، پاس می‌دهیم.
              متد CustomMappings ای که به صورت virtual تعریف شده‌است، این امکان را به ما می‌دهد که در کلاس‌هایی که از BaseDto ارث بری می‌کنند، در صورت لزوم آن را بازنویسی (override) کرده و سفارشی سازی دلخواه‌مان را بر روی Mapping دریافتی اعمال کنیم.
              مثال: کلاس PostDto زیر از BaseDto ارث بری کرده و چون سفارشی سازی‌ای لازم دارد، متد CustomMappings والد خود را override کرده است.
              public class PostDto : BaseDto<PostDto, Post, long>
              {
                  public string Title { get; set; }
                  public string Text { get; set; }
                  public int CategoryId { get; set; }
              
                  public string CategoryName { get; set; } //=> Category.Name
                  public string FullTitle { get; set; } //=> custom mapping for "Title (Category.Name)"
                      
                  public override void CustomMappings(IMappingExpression<Post, PostDto> mapping)
                  {
                      mapping.ForMember(
                              dest => dest.FullTitle,
                              config => config.MapFrom(src => $"{src.Title} ({src.Category.Name})"));
                  }
              }
              • این کلاس، خاصیتی به نام FullTitle دارد که معادلی (خاصیت همنامی) در کلاس Post برای آن وجود ندارد و قرار است مقدار ترکیبی حاصل از Title و Category.Name را نمایش دهد. 
              • به همین جهت متد CustomMappings را باز نویسی کرده، شیء mapping را دریافت و سفارشی سازی لازم را روی آن انجام داده‌ایم.
              • توسط متد ForMember مشخص کرده‌ایم که مقدار خاصیت FullTitle باید حاصلی از ترکیب Title و Category.Name به نحو مشخص شده باشد ( توسط متد MapFrom).
              پس در این روش علاوه بر امکانات BaseDto و Mapping خودکار، امکان سفارشی سازی دلخواه را نیز خواهیم داشت.
              برای کوئری گرفتن از دیتابیس نیز و تبدیل آنها به لیستی از Dto‌ها می‌توان از متد ProjectTo بر روی IQueryable استفاده کرد و حتی شرط Where را بر روی کوئری Dto‌ها اعمال کرد مانند زیر:
              List<PostDto> list =
                  //ProjectTo method select only needed properties (of PostDto) not all properties
                  //Also select only needed property of navigations (like Post.Category.Name) not all unlike Include
                  //This ability called "Projection"
                  await _applicationDbContext.Posts.ProjectTo<PostDto>()
                  //We can also use Where on IQuerable<PostDto>
                  .Where(p => p.Title.Contains("test") || p.CategoryName.Contains("test"))
                  .ToListAsync();
              • متد ProjectTo کوئری post را به IQueryable ای از postDto تبدیل می‌کند (این قابلیت Projection نامیده می‌شود).
              • نگاشت خودکار خواص موجود در postDto توسط AutoMapper به صورت خودکار انجام می‌شود و فقط خواص لازم برای postDto واکشی می‌شوند (نه همه خواص در جدول post، که این به لحاظ کارآیی بهتر است).
              • همچنین اگر خواصی را داخل Navigation Property‌ها مانند CategoryName داشته باشیم، موقع کوئری گرفتن از دیتابیس، آنها نیز اعمال شده و فقط خواص لازم از Category واکشی می‌شوند (فقط خاصیت Name، بر خلاف Include که همه ستون‌ها را واکشی می‌کند).
              • همچنین می‌توان بر روی خواص Dto شرط Where را قرار داد مانند p.CategoryName.Contains("test") و تماما به کوئری SQL معادل آن ترجمه و اجرا می‌شوند.
              مطالب
              Iterators در ES 6
              یکی از اهداف ES 6، استاندارد سازی کار با Iterators و Iterables است. فرض کنید شیءایی را داریم که مجموعه‌ای از عناصر را در بر دارد. این مجموعه می‌تواند آرایه‌ای از عناصر باشد و یا set و map اضافه شده به ES 6 و یا حتی اشیایی که در زمان اجرا ایجاد می‌شوند. اگر این مجموعه Iterable باشد، حرکت بر روی آن یک Iterator را تولید می‌کند که امکان حرکت در این مجموعه را آیتم به آیتم میسر خواهد کرد:
               


              هر Iterator شیءایی است که دارای متد next می‌باشد. هر بار که این متد فراخوانی می‌شود، عضو بعدی مجموعه، بازگشت داده خواهد شد. خروجی هر مرحله، درون یک شیء با دو خاصیت value و done قرار داده می‌شود. value‌، مقدار مرحله‌ی بعد است و done مشخص می‌کند که آیا به پایان مجموعه رسیده‌ایم یا خیر (بنابراین در اینجا تعداد اعضای Iterator مشخص نیست).


              مثالی از پیمایش یک آرایه با چندین روش مختلف

              در مثال زیر، آرایه‌ای از اعداد را داریم که نیاز است جمع اعضای آن محاسبه شود:
               let sum = 0;
              let numbers = [1,2,3,4];
              این‌کار را می‌توان با استفاده از یک حلقه‌ی for متداول انجام داد. از آنجائیکه آرایه دارای خاصیت length است، می‌توان از آن جهت تعیین حد بالایی آرایه استفاده کرد:
               // for loop
              sum = 0;
              for(let i =0; i < numbers.length; i++){
                   sum += numbers[i];
              }
              //sum = 10
              روش دوم انجام این‌کار، استفاده از حلقه‌ی for in است. این حلقه هربار ایندکس بعدی قابل استفاده‌ی آرایه را بازگشت می‌دهد که از آن می‌توان جهت دسترسی به اعضای آرایه استفاده کرد:
               // for in
              sum = 0;
              for(let i in numbers) {
                  sum += numbers[i];
              }
              //sum = 10
              روش دیگر انجام این‌کار با استفاده از Iterators است:
               // iterator
              sum = 0;
              
              let iterator = number.values();
              let next = iterator.next();
              while(!next.done){
                  sum += next.value;
                  next = iterator.next();
              }
              //sum = 10
              برای کار با Iterators، ابتدا باید یک شیء Iterator را از مجموعه‌ایی که در اختیار داریم، تولید کنیم. برای مثال آرایه‌ها دارای متدی به نام values هستند که با فراخوانی ()number.values، سبب تولید یک Iterator می‌شوند. این Iterator امکان حرکت بین مقادیر آرایه را در اینجا میسر می‌کند.
              مرحله‌ی بعدی، فراخوانی متد next این Iterator است. این عملیات باید در طی یک حلقه، تا پایان کار Iterator انجام شود. همانطور که در ابتدای بحث نیز عنوان شد، خروجی متد next یک شیء است که دارای خواص value و done می‌باشد. اگر done مساوی true شد، یعنی به پایان کار پیمایش رسیده‌ایم.
              البته هدف از این مثال، صرفا نمایش سطح پایین کار با Iterators بود. در عمل از حلقه‌ی جدیدی به نام for of برای انجام این پیمایش استفاده می‌شود.


              معرفی حلقه‌ی for of

              جاوا اسکریپت سال‌ها است که دارای حلقه‌ی for in می‌باشد و نمونه‌ای از کاربرد آن‌را در مثال قبل مشاهده کردید. اگر این حلقه بر روی آرایه‌ها فراخوانی شود، هربار ایندکس پیمایش شده را بازگشت می‌دهد و اگر بر روی یک شیء فراخوانی شود، خواص آن شیء را بازگشت می‌دهد:
               var person = { first: "Vahid", last "N" };
              for(let i in person) {
                 console.log(person[i]);
              }
              در این مثال، i بار اول به first و بار دوم به خاصیت last اشاره می‌کند.
              چون این حلقه صرفا ایندکس‌ها و کلیدها را بازگشت می‌دهد، جهت کار با Iterators که نیاز است به مقادیر اعضاء دسترسی پیدا کنیم، مناسب نیست. به همین جهت در ES 6، حلقه‌ی جدیدی به نام for of برای کار با Iterators معرفی شده‌است:
               let numbers = [1,2,3,4];
              for(let i of numbers) {
                 console.log(i);
              }
              این حلقه برخلاف for in، بر روی values، کار پیمایش را انجام می‌دهد. همچنین به صورت خودکار در پشت صحنه متد next یک Iterator را فراخوانی می‌کند و خاصیت done آن‌را بررسی کرده و زمانیکه این خاصیت مساوی true شد، حلقه را خاتمه می‌دهد. برای نمونه مثال سطح پایین while دار ابتدای بحث را به نحو ساده‌تری با حلقه‌ی for of ذیل می‌توان جایگزین کرد:
              let sum = 0;
              let numbers = [1,2,3,4];
              
              for(let n of numbers){
                 sum += n;
              }

              for of یکی از روش‌های پیمایش Iterators است. پارامترهای rest و همچنین Array.from نیز چنین قابلیتی را فراهم می‌کنند.

              امکان پیاده سازی Iterators سفارشی نیز وجود دارد که پیشنیاز آن، درک مبحث جدید Symbols است که به صورت جداگانه‌ای بررسی خواهد شد.
              مطالب
              مقدمه‌ای بر LINQ بخش اول
              کلمه‌ی LINQ مخفف Language Integrated Query یا زبان پرس و جوی یکپارچه می‌باشد. LINQ برای اولین بار در ویژوال استودیوی 2008 و دات نت فریم ورک 3.5 برای پرکردن خلع بین دنیای اشیاء برنامه نویسی (Object Oriented World) و دنیای داده‌ها (Data World) ارائه شد.

              چرا LINQ؟ 
              در نگاهی کلی، مزایایی که از طریق LINQ حاصل می‌شوند عبارتند از:
              • کاهش حجم کدنویسی 
              • درک بهتر از عملکرد کد‌های نوشته شده
              • پس از یادگیری اصول LINQ به راحتی می‌توانید از این اصول پرس و جو نویسی برای کار بر روی مجموعه داده‌های مختلف استفاده کنید
              • کنترل صحت کدهای پرس و جو‌ها در زمان کامپایل ( Compile-Time Type Checking )

              اجزای سازنده‌ی LINQ
              دو جزء اصلی سازنده‌ی LINQ عبارت است از:
              • Elements عناصر
              • Sequences توالی‌ها
              توالی‌ها می‌توانند لیستی از اطلاعات مختلف باشند. هر آیتم در لیست را عنصر می‌گوییم. توالی نمونه‌ای از یک کلاس است که اینترفیس <IEnumarable<T را پیاده سازی کرده باشد. این اینترفیس تضمین می‌کند که توالی قابلیت پیمایش عناصر را دارد.
              به آرایه‌ی تعریف شده‌ی زیر دقت کنید:
              int[] fibonacci = {0, 1, 1, 2, 3, 5};
              متغیر fibonacci در اینجا نشان دهنده‌ی توالی و هر یک از اعداد آرایه، یک عنصر محسوب می‌شوند.
              توالی می‌تواند درون حافظه‌ای باشد (In Memory Object) که به آن Local Sequence می‌گویند و یا می‌تواند یک بانک اطلاعاتی SQL Server باشد که به آن Remote Sequence می‌گویند.
              در حالت Remote باید اینترفیس <IQuerable<T پیاده سازی شده باشد.
              پرس و جو هایی را که بر روی توالی‌های محلی اجرا می‌شوند، اصطلاحا Local Query و یا LINQ-To-Object  نیز می‌نامند.
              عملگرهای پرس و جوی  زیادی به شکل متد الحاقی در کلاس System.Linq.Enumerable طراحی شده‌اند. این مجموعه از عملگرهای پرس جو را اصطلاحا Standard Query Operator می‌گویند.
              نکته‌ی مهم این است که عملگرهای پرس و جو تغییری را در توالی ورودی نمی‌دهند و نتیجه‌ی خروجی یک مجموعه جدید و یا یک مقدار عددی می‌باشد.

              توالی خروجی و مقدار بازگشتی Scalar
              در بخش قبل گفتیم که خروجی یک پرس و جو می‌تواند یک مجموعه و یا یک مقدار عددی باشد. در مثال زیر عملگر Count را بر روی مجموعه‌ی fibonacci  اعمال کردیم و عددی که نشان دهنده‌ی تعداد عناصر مجموعه است، بعنوان خروجی بازگردانده شده است.
              int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
              int numberOfElements = fibonacci.Count();
              Console.WriteLine($"{numberOfElements}");
              IEnumerable<int> distinictNumbers = fibonacci.Distinct();
              Console.WriteLine("Elements in output sequence:");
              foreach (var number in distinictNumbers)
              {
                  Console.WriteLine(number);
              }
              در کد بالا توسط تابع Distinct، عناصر یکتا را از توالی ورودی استخراج کرده و بازگردانده‌ایم.
              خروجی برنامه‌ی فوق به شکل زیر است :
              6
              Elements in output sequence:
              0
              1
              2
              3
              5

              مفهوم Deffer Execution  (
              اجرای به تاخیر افتاده )
              عمده‌ی عملگر‌های پرس و جو بلافاصله پس از ایجاد، اجرا نمی‌شوند. این عملگرها در طول اجرای برنامه اجرا خواهند شد (اجرای با تاخیر). به همین خاطر می‌توان بعد از ساخت پرس و جو  تغییرات دلخواهی را به توالی ورودی اعمال کرد.
              در کد زیر  قبل از اجرای پرس و جو ، توالی ورودی ویرایش شده :
              int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
              // ایجاد پرس و جو 
              IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2);
              // در این مرحله پرس و جو ایجاد شده ولی هنوز اجرا نشده است
              // تغییر عنصر اول توالی
              fibonacci[0] = 99;
              // حرکت بر روی عناصر توالی باعث اجرای پرس و جو می‌شود
              foreach (var number in numbersGreaterThanTwoQuery)
              {
                 Console.WriteLine(number);
              }
              پرس و جو تا زمان اجرای حلقه‌ی Foreach اجرا نخواهد شد. خروجی مثال بالا به شکل زیر است :
              99
              3
              5

              به غیر از بعضی از عملگرها مثل Count,Min,Last سایر عملگر‌ها بصورت اجرای با تاخیر عمل می‌کنند. عملگری مثل Count باعث اجرای فوری پرس و جو می‌شود.
              تعدادی عملگر تبدیل (Conversion Operator) هم وجود دارد که باعث می‌شوند پرس و جو بلافاصله اجرا شود :
              • ToList
              • ToArray
              • ToLookup
              • ToDictionary
              عملگر‌های فوق پس از اجرا، خروجی را در یک ساختمان داده‌ی جدید باز می‌گردانند.
              در کد زیر اصلاح توالی متغیر Fibonacci بعد از اجرای تابع ToArray صورت گرفته است.
              int[] fibonacci = { 0, 1, 1, 2, 3, 5 };
              // ساخت پرس و جو
              IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2) .ToArray();
              // در این مرحله به خاطر عملگر استفاده شده پرس و جو اجرا می‌شود
              // تغییر اولین عنصر توالی
              fibonacci[0] = 99;
              // حرکت بر روی نتیجه
              foreach (var number in numbersGreaterThanTwoQuery)
              {
                 Console.WriteLine(number);
              }
              خروجی مثال بالا:
              3
              5
              همانطور که می‌بینید عدد 99 در خروجی مشاهده نمی‌شود. علت فراخوانی عملگر ToArray است که بلافاصله باعث اجرای پرس و جو شده و خروجی را باز می‌گرداند . به همین خاطر تغییر عنصر اول توالی ورودی، تاثیری بر روی نتیجه‌ی خروجی ندارد. 
              مطالب
              ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها
              پیشنیازها (الزامی)

              «بررسی مفاهیم معکوس سازی وابستگی‌ها و ابزارهای مرتبط با آن»
              «اصول طراحی SOLID»
              «مطالعه‌ی بیشتر»


              تزریق وابستگی‌ها (یا Dependency injection = DI) به معنای ارسال نمونه‌ای/وهله‌ای از وابستگی (یک سرویس) به شیء وابسته‌ی به آن (یک کلاینت) است. در فرآیند تزریق وابستگی‌ها، یک کلاس، وهله‌های کلاس‌های دیگر مورد نیاز خودش را بجای وهله سازی مستقیم، از یک تزریق کننده دریافت می‌کند. بنابراین بجای نوشتن newها در کلاس جاری، آن‌ها را به صورت وابستگی‌هایی در سازنده‌ی کلاس تعریف می‌کنیم تا توسط یک IoC Container تامین شوند. در اینجا به فریم ورک‌هایی که کار وهله سازی این وابستگی‌ها را انجام می‌دهند، IoC Container و یا DI container می‌گوییم (IoC =  inversion of control ).
              چندین نوع تزریق وابستگی‌ها وجود دارند که دو حالت زیر، عمومی‌ترین آن‌ها است:
              الف) تزریق در سازنده‌ی کلاس: لیست وابستگی‌های یک کلاس، به عنوان پارامترهای سازنده‌ی آن ذکر می‌شوند.
              ب) تزریق در خواص یا Setter injection: کلاینت خواصی get و set را به صورت public معرفی می‌کند و سپس IoC Container با وهله سازی آن‌ها، وابستگی‌های مورد نیاز را تامین خواهد کرد.


              تزریق وابستگی‌ها در ASP.NET Core

              برخلاف نگارش‌های قبلی ASP.NET، این نگارش جدید از ابتدا با دید پشتیبانی کامل از DI طراحی شده‌است و این مفهوم، در سراسر اجزای آن به صورت یکپارچه‌ای پشتیبانی می‌شود. همچنین به همراه یک minimalistic DI container توکار نیز هست .
              این IoC Container توکار از 4 حالت طول عمر ذیل پشتیبانی می‌کند:
              - instance: در هربار نیاز به یک وابستگی خاص، تنها یک وهله از آن در اختیار مصرف کننده قرار می‌گیرد و در اینجا شما هستید که مسئول تعریف نحوه‌ی وهله سازی این شیء خواهید بود (برای بار اول).
              - transient: هربار که نیاز به وابستگی خاصی بود، یک وهله‌ی جدید از آن توسط IoC Container تولید و ارائه می‌شود.
              - singleton: در این حالت تنها یک وهله از وابستگی درخواست شده در طول عمر برنامه تامین می‌شود.
              - scoped: در طول عمر یک scope خاص، تنها یک وهله از وابستگی درخواست شده، در اختیار مصرف کننده‌ها قرار می‌گیرد. برای مثال مرسوم است که به ازای یک درخواست وب، تنها یک وهله از شیء‌ایی خاص در اختیار تمام مصرف کننده‌های آن قرار گیرد (single instance per web request).

              طول عمر singleton، برای سرویس‌ها و کلاس‌های config مناسب هستند. به این ترتیب به کارآیی بالاتری خواهیم رسید و دیگر نیازی نخواهد بود تا هر بار این اطلاعات خوانده شوند. حالت scoped برای وهله سازی الگوی واحد کار و پیاده سازی تراکنش‌ها مناسب است. برای مثال در طی یک درخواست وب، یک تراکنش باید صورت گیرد.
              حالت scoped در حقیقت نوع خاصی از حالت transient است. در حالت transient صرفنظر از هر حالتی، هربار که وابستگی ویژه‌ای درخواست شود، یک وهله‌ی جدید از آن تولید خواهد شد. اما در حالت scoped فقط یک وهله‌ی از وابستگی مورد نظر، در بین تمام اشیاء وابسته‌ی به آن، در طول عمر آن scope تولید می‌شود.
              بنابراین در برنامه‌های وب دو نوع singleton برای معرفی کلاس‌های config و نوع scoped برای پیاده سازی تراکنش‌ها و همچنین بالابردن کارآیی برنامه در طی یک درخواست وب (با عدم وهله سازی بیش از اندازه‌ی از کلاس‌های مختلف مورد نیاز)، بیشتر از همه به کار برده می‌شوند.


              یک مثال کاربردی: بررسی نحوه‌ی تزریق یک سرویس سفارشی به کمک IoC Container توکار ASP.NET Core


              مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شده‌است، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشه‌ی src، کلیک راست کرده و گزینه‌ی Add new project را انتخاب کنید و سپس یک پروژه‌ی جدید از نوع NET Core -> Class library. را به آن، با نام Core1RtmEmptyTest.Services اضافه کنید (تصویر فوق).
              در ادامه کلاس نمونه‌ی سرویس پیام‌ها را به همراه اینترفیس آن، با محتوای زیر به آن اضافه کنید:
              namespace Core1RtmEmptyTest.Services
              {
                  public interface IMessagesService
                  {
                      string GetSiteName();
                  }
               
                  public class MessagesService : IMessagesService
                  {
                      public string GetSiteName()
                      {
                          return "DNT";
                      }
                  }
              }
              در ادامه به پروژه‌ی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.Services را انتخاب کنید، تا اسمبلی آن‌را بتوان در پروژه‌ی جاری استفاده کرد.


              انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
              {
                  "dependencies": {
                      // same as before
                      "Core1RtmEmptyTest.Services": "1.0.0-*"
                  },
              در ادامه قصد داریم این سرویس را به متد Configure کلاس Startup تزریق کرده و سپس خروجی رشته‌ای آن‌را توسط میان افزار Run آن نمایش دهیم. برای این منظور فایل Startup.cs را گشوده و امضای متد Configure را به نحو ذیل تغییر دهید:
              public void Configure(
                  IApplicationBuilder app,
                  IHostingEnvironment env,
                  IMessagesService messagesService)
              همانطور که در قسمت قبل نیز عنوان شد، متد Configure دارای امضای ثابتی نیست و هر تعداد سرویسی را که نیاز است، می‌توان در اینجا اضافه کرد. یک سری از سرویس‌ها مانند IApplicationBuilder و IHostingEnvironment پیشتر توسط IoC Container توکار ASP.NET Core معرفی و ثبت شده‌اند. به همین جهت، همینقدر که در اینجا ذکر شوند، کار می‌کنند و نیازی به تنظیمات اضافه‌تری ندارند. اما سرویس IMessagesService ما هنوز به این IoC Container معرفی نشده‌است. بنابراین نمی‌داند که چگونه باید این اینترفیس را وهله سازی کند.
              public void Configure(
                  IApplicationBuilder app,
                  IHostingEnvironment env,
                  IMessagesService messagesService)
              { 
                  app.Run(async context =>
                  {
                      var siteName = messagesService.GetSiteName();
                      await context.Response.WriteAsync($"Hello {siteName}");
                  });
              }
              در این حالت اگر برنامه را اجرا کنیم، به این خطا برخواهیم خورد:
               System.InvalidOperationException
              No service for type 'Core1RtmEmptyTest.Services.IMessagesService' has been registered.
              at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
              at Microsoft.AspNetCore.Hosting.Internal.ConfigureBuilder.Invoke(object instance, IApplicationBuilder builder)
              
              System.Exception
              Could not resolve a service of type 'Core1RtmEmptyTest.Services.IMessagesService' for the parameter 'messagesService' of method 'Configure' on type 'Core1RtmEmptyTest.Startup'.
              at Microsoft.AspNetCore.Hosting.Internal.ConfigureBuilder.Invoke(object instance, IApplicationBuilder builder)
              at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
              برای رفع این مشکل، به متد ConfigureServices کلاس Startup مراجعه کرده و سیم کشی‌های مرتبط را انجام می‌دهیم. در اینجا باید اعلام کنیم که «هر زمانیکه به IMessagesService رسیدی، یک وهله‌ی جدید (transient) از کلاس MessagesService را به صورت خودکار تولید کن و سپس در اختیار مصرف کننده قرار بده»:
              public class Startup
              {
                  public void ConfigureServices(IServiceCollection services)
                  {
                      services.AddTransient<IMessagesService, MessagesService>();
                  }
              در اینجا نحوه‌ی ثبت یک سرویس را در IoC Containser توکار ASP.NET Core ملاحظه می‌کنید. تمام حالت‌های طول عمری که در ابتدای بحث عنوان شدند، یک متد ویژه‌ی خاص خود را در اینجا دارند. برای مثال حالت transient دارای متد ویژه‌ی AddTransient است و همینطور برای سایر حالت‌ها. این متدها به صورت جنریک تعریف شده‌اند و آرگومان اول آن‌ها، اینترفیس سرویس و آرگومان دوم، پیاده سازی آن‌ها است (سیم کشی اینترفیس، به کلاس پیاده سازی کننده‌ی آن).

              پس از اینکار، مجددا برنامه را اجرا کنید. اکنون این خروجی باید مشاهده شود:


              و به این معنا است که اکنون IoC Cotanier توکار ASP.NET Core، می‌داند زمانیکه به IMessagesService رسید، چگونه باید آن‌را وهله سازی کند.


              چه سرویس‌هایی به صورت پیش فرض در IoC Container توکار ASP.NET Core ثبت شده‌اند؟

              در ابتدای متد ConfigureServices یک break point را قرار داده و برنامه را در حالت دیباگ اجرا کنید:


              همانطور که ملاحظه می‌کنید، به صورت پیش فرض 16 سرویس در اینجا ثبت شده‌اند که تاکنون با دو مورد از آن‌ها کار کرده‌ایم.


              امکان تزریق وابستگی‌ها در همه جا!

              در مثال فوق، سرویس سفارشی خود را در متد Configure کلاس آغازین برنامه تزریق کردیم. نکته‌ی مهم اینجا است که برخلاف نگارش‌های قبلی ASP.NET MVC (یعنی بدون نیاز به تنظیمات خاصی برای قسمت‌های مختلف برنامه)، می‌توان این تزریق‌ها را در کنترلرها، در میان افزارها، در فیلترها در ... همه جا و تمام اجزای ASP.NET Core 1.0 انجام داد و دیگر اینبار نیازی نیست تا نکته‌ی ویژه‌ی نحوه‌ی تزریق وابستگی‌ها در فیلترها یا کنترلرهای ASP.NET MVC را یافته و سپس اعمال کنید. تمام این‌ها از روز اول کار می‌کنند. همینقدر که کار ثبت سرویس خود را در متد ConfigureServices انجام دادید، این سرویس در سراسر اکوسیستم ASP.NET Core، قابل دسترسی است.


              نیاز به تعویض IoC Container توکار ASP.NET Core

              قابلیت تزریق وابستگی‌های توکار ASP.NET Core صرفا جهت برآورده کردن نیازمندی‌های اصلی آن طراحی شده‌است و نه بیشتر. بنابراین توسط آن قابلیت‌های پیشرفته‌ای را که سایر IoC Containers ارائه می‌دهند، نخواهید یافت. برای مثال تعویض امکانات تزریق وابستگی‌های توکار ASP.NET Core با StructureMap این مزایا را به همراه خواهد داشت:
               • امکان ایجاد child/nested containers (پشتیبانی از سناریوهای چند مستاجری)
               • پشتیبانی از Setter Injection
               • امکان انتخاب سازنده‌ای خاص (اگر چندین سازنده تعریف شده باشند)
               • سیم کشی خودکار یا Conventional "Auto" Registration (برای مثال اتصال اینترفیس IName به کلاس Name به صورت خودکار و کاهش تعداد تعاریف ابتدای برنامه)
               • پشتیبانی توکار از Lazy و Func
               • امکان وهله سازی از نوع‌های concrete (یا همان کلاس‌های معمولی)
               • پشتیبانی از مفاهیمی مانند Interception و AOP
               • امکان اسکن اسمبلی‌های مختلف جهت یافتن اینترفیس‌ها و اتصال خودکار آن‌ها (طراحی‌های افزونه پذیر)


              روش تعویض IoC Container توکار ASP.NET Core با StructureMap

              جزئیات این جایگزین کردن را در مطلب «جایگزین کردن StructureMap با سیستم توکار تزریق وابستگی‌ها در ASP.NET Core 1.0» می‌توانید مطالعه کنید.
              یا می‌توانید از روش فوق استفاده کنید و یا اکنون قسمتی از پروژه‌ی رسمی استراکچرمپ در آدرس https://github.com/structuremap/structuremap.dnx جهت کار با NET Core. طراحی شده‌است. برای کار با آن نیاز است این مراحل طی شوند:
              الف) دریافت بسته‌ی نیوگت StructureMap.Dnx
              برای این منظور بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن، StructureMap.Dnx را جستجو کرده و نصب نمائید (تیک مربوط به انتخاب pre releases هم باید انتخاب شده باشد):


              انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
              {
                  "dependencies": {
                      // same as before  
                      "StructureMap.Dnx": "0.5.1-rc2-final"
                  },
              ب) جایگزین کردن Container استراکچرمپ با Container توکار ASP.NET Core
              پس از نصب بسته‌ی StructureMap.Dnx، به کلاس آغازین برنامه مراجعه کرده و این تغییرات را اعمال کنید:
              public class Startup
              {
                  public IServiceProvider ConfigureServices(IServiceCollection services)
                  {
                      services.AddDirectoryBrowser();
               
                      var container = new Container();
                      container.Configure(config =>
                      {
                          config.Scan(_ =>
                          {
                              _.AssemblyContainingType<IMessagesService>();
                              _.WithDefaultConventions();
                          });
                          //config.For<IMessagesService>().Use<MessagesService>();
               
                          config.Populate(services);
                      });
                      container.Populate(services);
               
                      return container.GetInstance<IServiceProvider>();
                  }
              در اینجا ابتدا خروجی متد ConfigureServices، به IServiceProvider تغییر کرده‌است تا استراکچرمپ این تامین کننده‌ی سرویس‌ها را ارائه دهد. سپس Container مربوط به استراکچرمپ، وهله سازی شده و همانند روال متداول آن، یک سرویس و کلاس پیاده سازی کننده‌ی آن معرفی شده‌اند (و یا هر تنظیم دیگری را که لازم بود باید در اینجا اضافه کنید). در پایان کار متد Configure آن و پس از این متد، نیاز است متدهای Populate فراخوانی شوند (اولی تعاریف را اضافه می‌کند و دومی کار تنظیمات را نهایی خواهد کرد).
              سپس وهله‌ای از IServiceProvider، توسط استراکچرمپ تامین شده و بازگشت داده می‌شود تا بجای IoC Container توکار ASP.NET Core استفاده شود.
              در این مثال چون در متد Scan، کار بررسی اسمبلی لایه سرویس برنامه با قراردادهای پیش فرض استراکچرمپ انجام شده‌است، دیگر نیازی به سطر تعریف config.For نیست. در اینجا هرگاه IName ایی یافت شد، به کلاس Name متصل می‌شود (name هر نامی می‌تواند باشد).
              مطالب
              استفاده از Flash Uploader در ASP.NET MVC
              چندسال قبل یک کنترل آپلود فایل در برنامه‌های ASP.NET Web forms در سایت Code projects منتشر شد که من در چند پروژه از آن استفاده کردم.
              در ادامه نحوه سازگار سازی این مجموعه را با ASP.NET MVC مرور خواهیم کرد:

              الف) سورس‌های اصلی Flash کنترل ارسال فایل‌ها
              اگر علاقمند به تغییر اطلاعاتی در فایل فلش نهایی هستید به پوشه OriginalFlashSource پروژه پیوست شده مراجعه کنید. در اینجا برای مثال یک سری از برچسب‌های آن فارسی شده‌اند و کامپایل مجدد.


              ب) مزیت استفاده از Flash uploader
              با استفاده از Flash uploader امکان انتخاب چندین فایل با هم وجود دارد. همچنین در صفحه دیالوگ انتخاب فایل‌ها دقیقا می‌توان پسوند فایل‌های مورد نظر را نیز تعیین کرد. این دو مورد در حالت ارسال معمولی فایل‌ها به سرور و استفاده از امکانات معمولی HTML وجود ندارند. به علاوه امکان نمایش درصد پیشرفت آپلود فایل‌ها و همچنین حذف کلی لیست و حذف یک آیتم از لیست را هم درنظر بگیرید.



              ج) معادل کنترل Web forms را در ASP.NET MVC به شکل زیر می‌توان تهیه کرد:

              @helper AddFlashUploader(
                              string uploadUrl,
                              string queryParameters,
                              string flashUrl,
                              int totalUploadSizeLimit = 0,
                              int uploadFileSizeLimit = 0,
                              string fileTypes = "",
                              string fileTypeDescription = "",
                              string onUploadComplete = "")
                  {      
                      onUploadComplete = string.IsNullOrEmpty(onUploadComplete) ? "" : "completeFunction=" + onUploadComplete;
                      queryParameters = Server.UrlEncode(queryParameters);        
                      fileTypes = string.IsNullOrEmpty(fileTypes) ? "" : "&fileTypes=" + Server.UrlEncode(fileTypes);
                      fileTypeDescription = string.IsNullOrEmpty(fileTypeDescription) ? "" : "&fileTypeDescription=" + Server.UrlEncode(fileTypeDescription);
                      var totalUploadSizeLimitData = totalUploadSizeLimit > 0 ? "&totalUploadSize=" + totalUploadSizeLimit : "";
                      var uploadFileSizeLimitData = uploadFileSizeLimit > 0 ? "&fileSizeLimit=" + uploadFileSizeLimit : "";
                      var flashVars = onUploadComplete + fileTypes + fileTypeDescription + totalUploadSizeLimitData + uploadFileSizeLimitData + "&uploadPage=" + uploadUrl + "?" + queryParameters;
                  <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0"
                      width="575" height="375" id="fileUpload" align="middle">
                      <param name="allowScriptAccess" value="sameDomain" />
                      <param name="movie" value="@flashUrl" />
                      <param name="quality" value="high" />
                      <param name="wmode" value="transparent">
                      <param name=FlashVars value="@flashVars">
                      <embed src="@flashUrl"
                             FlashVars="@flashVars" 
                             quality="high" 
                             wmode="transparent" 
                             width="575" 
                             height="375" 
                             name="fileUpload" 
                             align="middle" 
                             allowScriptAccess="sameDomain" 
                             type="application/x-shockwave-flash" 
                             pluginspage="http://www.macromedia.com/go/getflashplayer" />
                  </object>                        
              }
              این اطلاعات در فایلی به نام FlashUploadHelper.cshtml در پوشه App_Code قرار خواهند گرفت.


              د) نحوه استفاده از HTML helper فوق:

              @{
                  ViewBag.Title = "Index";
                  var uploadUrl = Url.Action("Uploader", "Home");
                  var flashUrl = Url.Content("~/Content/FlashUpload/FlashFileUpload.swf");
              }
              <h2>
                  Flash Uploader</h2>
              <div style="background: #E0EBEF;">
                  @FlashUploadHelper.AddFlashUploader(
                              uploadUrl: uploadUrl,
                              queryParameters: "User=Vahid&Id=تست",
                              flashUrl: flashUrl,
                              fileTypeDescription: "Images",
                              fileTypes: "*.gif; *.png; *.jpg; *.jpeg",
                              uploadFileSizeLimit: 0,
                              totalUploadSizeLimit: 0,
                              onUploadComplete: "alert('انجام شد');")
              </div>
              با کدهای کنترلری معادل:
              using System.Collections.Generic;
              using System.IO;
              using System.Web;
              using System.Web.Mvc;
              
              namespace MvcFlashUpload.Controllers
              {
                  public class HomeController : Controller
                  {
                      public ActionResult Index()
                      {
                          return View();
                      }
              
                      public ActionResult Uploader(string User, string Id, IEnumerable<HttpPostedFileBase> FileData)
                      {
                          var queryParameter1 = User;
                          var queryParameter2 = Id;
                          // ...
              
                          foreach (var file in FileData)
                          {
                              if (file.ContentLength > 0)
                              {
                                  var fileName = Path.GetFileName(file.FileName);
                                  var path = Path.Combine(Server.MapPath("~/App_Data/Uploads"), fileName);
                                  file.SaveAs(path);
                              }
                          }
              
                          return Content(" ");
                      }
                  }
              }

              توضیحات:
              در اینجا uploadUrl، مسیر اکشن متدی است که قرار است اطلاعات فایل‌ها را دریافت کند. queryParameters اختیاری است. اگر تعریف شود تعدادی کوئری استرینگ دلخواه را می‌تواند به متد Uploader ارسال کند. برای نمونه در اینجا User و Id ارسال شده‌اند یا هر نوع کوئری استرینگ دیگری که مدنظر است.
              flashUrl مسیر فایل SWF را مشخص می‌کند. در اینجا فایل FlashFileUpload.swfدر پوشه Content/FlashUpload قرار گرفته است.
              fileTypeDescription برچسبی است که نوع فایل‌های قابل انتخاب را به کاربر نمایش می‌دهد و fileTypes نوع‌های مجاز قابل ارسال را دقیقا مشخص می‌کند.
              پارامترهای uploadFileSizeLimit و totalUploadSizeLimit در صورتیکه مساوی صفر وارد شوند، به معنای عدم محدودیت اندازه در فایل‌ها و جمع حجم ارسالی در هر بار است.
              استفاده از پارامتر onUploadComplete اختیاری است. در اینجا می‌توان پس از پایان عملیات از طریق جاوا اسکریپت عملیاتی را انجام داد. برای مثال اگر خواستید کاربر را به صفحه خاصی هدایت کنید، window.locationرا مقدار دهی نمائید.
              در متد Uploader کنترلر فوق، پارامترهای User و id اختیاری بوده و بر اساس queryParameters متد FlashUploadHelper.AddFlashUploader مشخص می‌شوند. اما نام FileData نباید تغییری کند؛ از این لحاظ که دقیقا همین نام در فایل فلش، مورد استفاده قرار گرفته است.
              در اکشن متد دریافت فایل‌ها، لیستی از فایل‌های ارسالی به سرور دریافت شده و سپس بر این اساس می‌توان آن‌ها را در مکانی مشخص ذخیره نمود.


              دریافت پروژه
              MvcFlashUploader.zip
              نظرات مطالب
              ساخت منوهای چند سطحی در ASP.NET MVC
              نمیشه کاری کرد زیرمنوها زمان hover  روی منوهای ریشه، روی منوهای ریشه باز نشن، مثلا پایین بیفتن؟