مطالب
Roslyn #4
بررسی API کامپایل Roslyn

Compilation API، یک abstraction سطح بالا از فعالیت‌های کامپایل Roslyn است. برای مثال در اینجا می‌توان یک اسمبلی را از Syntax tree موجود، تولید کرد و یا جایگزین‌هایی را برای APIهای قدیمی CodeDOM و Reflection Emit ارائه داد. به علاوه این API امکان دسترسی به گزارشات خطاهای کامپایل را میسر می‌کند؛ به همراه دسترسی به اطلاعات Semantic analysis. در مورد تفاوت Syntax tree و Semantics در قسمت قبل بیشتر بحث شد.
با ارائه‌ی Roslyn، اینبار کامپایلرهای خط فرمان تولید شده مانند csc.exe، صرفا یک پوسته بر فراز Compilation API آن هستند. بنابراین دیگر نیازی به فراخوانی Process.Start بر روی فایل اجرایی csc.exe مانند یک سری کتابخانه‌های قدیمی نیست. در اینجا با کدنویسی، به تمام اجزاء و تنظیمات کامپایلر، دسترسی وجود دارد.


کامپایل پویای کد توسط Roslyn

برای کار با API کامپایل، سورس کد، به صورت یک رشته در اختیار کامپایلر قرار می‌گیرد؛ به همراه تنظیمات ارجاعاتی به اسمبلی‌هایی که نیاز دارد. سپس کار کامپایلر شروع خواهد شد و شامل مواردی است مانند تبدیل متن دریافتی به Syntax tree و همچنین تبدیل مواردی که اصطلاحا به آن‌ها Syntax sugars گفته می‌شود مانند خواص get و set دار به معادل‌های اصلی آن‌ها. در اینجا کار Semantic analysis هم انجام می‌شود و شامل تشخیص حوزه‌ی دید متغیرها، تشخیص overloadها و بررسی نوع‌های بکار رفته‌است. در نهایت کار تولید فایل باینری اسمبلی، از اطلاعات آنالیز شده صورت می‌گیرد. البته خروجی کامپایلر می‌تواند اسمبلی‌های exe یا dll، فایل XML مستندات اسمبلی و یا فایل‌های .netmudule و .winmdobj مخصوص WinRT هم باشد.
در ادامه، اولین مثال کار با Compilation API را مشاهده می‌کنید. پیشنیاز اجرای آن همان مواردی هستند که در قسمت قبل بحث شدند. یک برنامه‌ی کنسول ساده‌ی .NET 4.6 را آغاز کرده و سپس بسته‌ی نیوگت Microsoft.CodeAnalysis را در آن نصب کنید. در ادامه کدهای ذیل را به پروژه‌ی آماده شده اضافه کنید:
static void firstCompilation()
{
    var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }");
 
    var mscorlibReference = MetadataReference.CreateFromFile(typeof (object).Assembly.Location);
 
    var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
 
    var comp = CSharpCompilation.Create("Demo")
                                .AddSyntaxTrees(tree)
                                .AddReferences(mscorlibReference)
                                .WithOptions(compilationOptions);
 
    var res = comp.Emit("Demo.dll");
 
    if (!res.Success)
    {
        foreach (var diagnostic in res.Diagnostics)
        {
            Console.WriteLine(diagnostic.GetMessage());
        }
    }
}
در اینجا نحوه‌ی کامپایل پویای یک قطعه کد متنی سی‌شارپ را به DLL معادل آن مشاهده می‌کنید. مرحله‌ی اول اینکار، تولید Syntax tree از رشته‌ی متنی دریافتی است. سپس متد CSharpCompilation.Create یک وهله از Compilation API مخصوص #C را آغاز می‌کند. این API به صورت Fluent طراحی شده‌است و می‌توان سایر قسمت‌های آن‌را به همراه یک دات پس از ذکر متد، به طول زنجیره‌ی فراخوانی، اضافه کرد. برای نمونه در این مثال، نحوه‌ی افزودن ارجاعی را به اسمبلی mscorlib که System.Object در آن قرار دارد و همچنین ذکر نوع خروجی DLL یا DynamicallyLinkedLibrary را ملاحظه می‌کنید. اگر این تنظیم ذکر نشود، خروجی پیش فرض از نوع .exe خواهد بود و اگر mscorlib را اضافه نکنیم، نوع int سورس کد ورودی، شناسایی نشده و برنامه کامپایل نمی‌شود.
متدهای تعریف شده توسط Compilation API به یک s جمع، ختم می‌شوند؛ به این معنا که در اینجا در صورت نیاز، چندین Syntax tree یا ارجاع را می‌توان تعریف کرد.
پس از وهله سازی Compilation API و تنظیم آن، اکنون با فراخوانی متد Emit، کار تولید فایل اسمبلی نهایی صورت می‌گیرد. در اینجا اگر خطایی وجود داشته باشد، استثنایی را دریافت نخواهید کرد. بلکه باید خاصیت Success نتیجه‌ی آن‌را بررسی کرده و درصورت موفقیت آمیز نبودن عملیات، خطاهای دریافتی را از مجموعه‌ی Diagnostics آن دریافت کرد. کلاس Diagnostic، شامل اطلاعاتی مانند محل سطر و ستون وقوع مشکل و یا پیام متناظر با آن است.


معرفی مقدمات Semantic analysis

Compilation API به اطلاعات Semantics نیز دسترسی دارد. برای مثال آیا Type A قابل تبدیل به Type B هست یا اصلا نیازی به تبدیل ندارد و به صورت مستقیم قابل انتساب هستند؟ برای درک بهتر این مفهوم نیاز است یک مثال را بررسی کنیم:
        static void semanticQuestions()
        {
            var tree = CSharpSyntaxTree.ParseText(@"
using System;
 
class Foo
{
    public static explicit operator DateTime(Foo f)
    {
        throw new NotImplementedException();
    }
 
    void Bar(int x)
    {
    }
}");
 
            var mscorlib = MetadataReference.CreateFromFile(typeof (object).Assembly.Location);
            var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
            var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib).WithOptions(options);
            // var res = comp.Emit("Demo.dll");
 
            // boxing
            var conv1 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Int32),
                comp.GetSpecialType(SpecialType.System_Object)
                );
 
            // unboxing
            var conv2 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Object),
                comp.GetSpecialType(SpecialType.System_Int32)
                );
 
            // explicit reference conversion
            var conv3 = comp.ClassifyConversion(
                comp.GetSpecialType(SpecialType.System_Object),
                comp.GetTypeByMetadataName("Foo")
                );
 
            // explicit user-supplied conversion
            var conv4 = comp.ClassifyConversion(
                comp.GetTypeByMetadataName("Foo"),
                comp.GetSpecialType(SpecialType.System_DateTime)
                );
        }
تا سطر CSharpCompilation.Create این مثال، مانند قبل است و تا اینجا به Compilation API دسترسی پیدا کرده‌ایم. پس از آن می‌خواهیم یک Semantic analysis مقدماتی را انجام دهیم. برای این منظور می‌توان از متد ClassifyConversion استفاده کرد. این متد یک نوع مبداء و یک نوع مقصد را دریافت می‌کند و بر اساس اطلاعاتی که از Compilation API بدست می‌آورد، می‌تواند مشخص کند که برای مثال آیا نوع کلاس Foo قابل تبدیل به DateTime هست یا خیر و اگر هست چه نوع تبدیلی را نیاز دارد؟


برای مثال نتیجه‌ی بررسی آخرین تبدیل انجام شده در تصویر فوق مشخص است. با توجه به تعریف public static explicit operator DateTime در سورس کد مورد آنالیز، این تبدیل explicit بوده و همچنین user defined. به علاوه متدی هم که این تبدیل را انجام می‌دهد، مشخص کرده‌است.
نظرات مطالب
افزودن و اعتبارسنجی خودکار Anti-Forgery Tokens در برنامه‌های Angular مبتنی بر ASP.NET Core
پاسخ دادن به این سؤال بدون مطالعه‌ی سورس https://github.com/aspnet/Antiforgery غیرممکن است. علت اینکه روش توضیح داده شده‌ی در این مطلب، برای حالت برنامه‌های دارای اعتبارسنجی کار نمی‌کند و خطای «The provided antiforgery token was meant for a different claims-based user than the current user» را دریافت می‌کنید، این صورت است:
 میان‌افزار طراحی شده‌ی در این مطلب، پیش از لاگین و با اولین درخواست تک صفحه‌ی برنامه، کوکی‌های antiforgery را دریافت می‌کند. اما ... سیستم antiforgery طوری طراحی شده‌است که پیش از تولید کوکی، مشخصات دقیق this.HttpContext.User را دریافت و هش می‌کند (لیست Claims آن‌را به صورت رشته در می‌آورد و هش SHA256 آن را محاسبه می‌کند). از این هش هم جهت تولید محتوای کوکی نهایی خود استفاده می‌کند. بنابراین در بار اولی که صفحه درخواست شده‌است، یک کوکی antiforgery مخصوص کاربر null و اعتبارسنجی نشده، تولید خواهد شد. بعد از آن پس از لاگین، اگر میان‌افزار یاد شده مجددا نیز فراخوانی شود، هیچ اتفاق خاصی رخ نخواهد داد. از این جهت که در طراحی متد GetAndStoreTokens آن، به ازای یک صفحه، فقط یکبار این کوکی تولید می‌شود و اگر هزار بار دیگر هم این متد را جهت برنامه‌ی تک صفحه‌ای خود فراخوانی کنیم، به این معنا نخواهد بود که مشخصات this.HttpContext.User را به کوکی جدیدی اضافه می‌کند؛ چون اصلا کوکی جدیدی را تولید نمی‌کند!

بنابراین راه حل نهایی به این صورت است:
الف) میان افزار AngularAntiforgeryTokenMiddleware فوق را حذف کنید. این میان‌افزار عملا کاربردی برای برنامه‌های SPA دارای اعتبارسنجی ندارد.
ب) امضای متد Login را به این صورت تغییر دهید که شامل IgnoreAntiforgeryToken باشد:
[AllowAnonymous]
[IgnoreAntiforgeryToken]
[HttpPost("[action]")]
public async Task<IActionResult> Login([FromBody] User loginUser)
از این جهت که نمی‌توانیم پیش از لاگین، درخواست تولید کوکی antiforgery را برای کاربر null صادر کنیم؛ چون این کوکی تا پایان عمر مرورگر تغییری نخواهد کرد.
ج) در متد لاگین، پس از تولید توکن‌ها، اکنون کار تولید کوکی را به صورت زیر انجام می‌دهیم:
private void regenerateAntiForgeryCookie(IEnumerable<Claim> claims)
{
    this.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, JwtBearerDefaults.AuthenticationScheme));
    var tokens = _antiforgery.GetAndStoreTokens(this.HttpContext);
    this.HttpContext.Response.Cookies.Append(
                  key: "XSRF-TOKEN",
                  value: tokens.RequestToken,
                  options: new CookieOptions
                  {
                      HttpOnly = false // Now JavaScript is able to read the cookie
                  });
}
در این‌جا لیست claims ایی را که در حین تولید توکن‌ها ایجاد کردیم، تبدیل به یک ClaimsPrincipal می‌کنیم تا بتوانیم this.HttpContext.User را مقدار دهی کنیم (دقیقا با مشخصات اصلی کاربر لاگین شده) و سپس متد GetAndStoreTokens را فراخوانی می‌کنیم تا این User غیرنال را خوانده و کوکی صحیحی را تولید کند. به این صورت است که دیگر پیام خطای «این antiforgery توکن، متعلق به کاربر دیگری است» را دریافت نخواهیم کرد. این «کاربر دیگر» منظورش همان کاربر null ابتدای کار است که پیش از لاگین تنظیم می‌شد و با کاربر جدیدی که دارای claims است یکی نبود.

نکته‌ی مهم! جائیکه Claim‌های برنامه را تولید می‌کنید، باید حتما Issuer را هم ذکر کنید:
 new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString(),
                  ClaimValueTypes.String, _configuration.Value.Issuer),
از این جهت که هش SHA256 ایی که توضیح داده شده، بر اساس این Issuer، مقدار و نوع Claim محاسبه می‌شود. اگر Issuer را ذکر نکنید، به local authority تنظیم می‌شود که با Issuer نهایی توکن تولید شده یکی نیست. به همین جهت حتی اگر this.HttpContext.User را هم مقدار دهی کنید، کار نمی‌کند؛ چون هش claim‌های تولیدی، یکی نخواهند بود و باز هم پیام «این antiforgery توکن متعلق به کاربر دیگری است» را دریافت خواهید کرد.


خلاصه این تغییرات به پروژه‌ی ASPNETCore2JwtAuthentication اعمال شده‌اند.
مطالب
آموزش (jQuery) جی کوئری 2#
در ادامه مطلب قبلی آموزش (jQuery) جی کوئری 1# به ادامه بحث می‌پردازیم.

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

فراخوانی و استفاده از این توابع در ابتدا ممکن است کمی عجیب به نطر برسد. به مثال زیر دقت کنید که تابع سودمند () trim را فراخوانی کرده ایم.
$.trim(someString);
 
در صورتی که نوشتن علامت $ برای شما عجیب به نطر می‌رسد می‌توانید شناسه دیگر با نام jQuery به کار ببرید. کد زیر دقیقا مانند بالا عمل می‌کند شاید درک آن راحت‌تر هم باشد.
jQuery.trim(someString);

بدیهی است که از  jQuery یا $ تنها به عنوان فضای نامی که تابع ()trim در آن تعریف شده اند، استفاده شده باشد.

نکته: اگر چه در نوشته‌های آنلاین jQuery، این عناصر به عنوان توابع سودمند در معرفی شده اند اما در حقیقت آنها متدهایی برای تابع ()$ می‌باشند.

عملکرد صفحه آماده (The document ready handler)
هنگامی که از Unobtrusive JavaScriptاستفاده می‌کنیم، رفتار از ساختار جدا می‌شود، بنابراین برای انجام عملیات روی عناصر صفحه باید منتظر بمانیم تا انها ایجاد شوند. برای رسیدن به این هدف، ما نیاز به راهی داریم که تا زمان ایجاد عناصر DOM روی صفحه منتظر بماند قبل از آن عملیات را اجرا کند.
به طور معمول از onload برای نمونه‌های window استفاده می‌شود، که پس از لود شدن کامل صفحه ، دستور‌ها قابل اجرا می‌باشند. بنابراین ساختار کلی آن کدی مانند زیر خواهد بود:
window.onload = function() {
    $("table tr:nth-child(even)").addClass("even");
};

نوشتن کد به صورت بالا سبب می‌شود که کد پس از بارگذاری کامل صفحه اجرا شود. متاسفانه، مرورگرها تا بعد از ساخته شدن عناصر صفحه صبر نمی‌کنند، بلکه پس از ساخت درخت عناصر صفحه منتظر بارگذاری کامل منابع خارجی صفحه مانند تصاویر نیز می‌مانند و سپس آنها را در پنجره مرورگر نمایش می‌دهند. در نتیجه بازدید کننده زمان زیادی منتظر می‌ماند تا رویداد onload تکمیل شود.
حتی بدتر از آن، زمانی است که اگر به طور مثال یکی از تصاویر با مشکل مواجه شود که زمان قابل توجهی صرف بارگذاری آن شود، کاربر باید تمام این مدت را صبر کند تا پس از آن بتواند با این صفحه کار کند. این نکته می‌تواند دلیلی برای استفاده نکردن از Unobtrusive JavaScriptبرای شروع کار باشد.
اما راه بهتری نیز وجود دارد، می‌توانیم تنها زمانی که قسمت ساختار عناصر صفحه ترجمه شده و HTML به درخت عناصر تبدیل می‌شود، صبر کنیم . پس از آن کد مربوط به رفتار‌ها را اجرا کنیم. رسیدن به این روش برای استفاده از Cross-Browser کمی مشکل است، اما به لطف jQuery و قدرت آن، این امر به سادگی امکان پذیر است و دیگر نیازی به منتظر ماندن برای بارگذاری منابع صفحه مانند تصاویر و ویدیوها نمی‌باشد. Syntax زیر نمونه ای از چنین حالتی است:
$(document).ready(function() {
   $("table tr:nth-child(even)").addClass("even");
});

ابتدا صفحه مورد نظر را به تابع ()$ ارسال کرده ایم، سپس هر زمان که آن صفحه آماده شد (Ready) ، تابع ارسال شده به آن اجرا خواهد شد. البته می‌توان کد نوشته شده بالا را به شکل مختصرتری هم نوشت:
$(function() {
    $("table tr:nth-child(even)").addClass("even");
});

با ارسال تابع به ()$، ما مرورگر را مجبور می‌کنیم که برای اجرای کد تا زمانی که DOM کامل لود شود (فقط DOM لود شود) منتظر بماند. حتی بهتر از آن ما می‌توانیم از این تکنیک چندین با در همان سند HTML استفاده کرده و مرورگر تمامی تابع‌های مشخص شده توسط ما را به ترتیب اجرا خواهد کرد. (یعنی من در دیک صفحه می‌توانم چنین بار تابع ()ready را فراخوانی کنم). در مقابل روش OnLoad پنجره فقط اجازه اجرای یکبار تابع را به ما می‌دهد.
این هم یکی دیگر از کارکردهای دیگر تابع ()$ می‌باشد. حال به یکی دیگر از امکاناتی که این تابع برای ما فراهم می‌کند دقت کنید.

ساختن اجزای DOM (ساختن عناصر صفحه)
یکی دیگر از کارهایی که تابع ()$ می تواند برای ما انجام دهد ایجاد کردن عناصر صفحه است. به این منظور ورودی تابع ()$ را یک رشته که حاوی دستور HTML مربوط به ساخت یک عنصر می‌باشد، قرار می‌دهیم. برای مثال دستور زیر یک تگ p ایجاد می‌کند:
$("<p>Hi there!</p>")

اما ایجاد یک عنصر DOM یا (سلسله مراتب عناصر DOM) برای ما به تنهایی سودمند نیست، و هدف ما چیز دیگری است. ایجاد اشیا صفحه توسط ()$ زمانی برای ما مفید خواهد بود که بخواهیم به هنگام ساخت، تابعی بروی آن اعمال کنیم یا به محض ساخت آن را به تابعی ارسال کنیم به کد زیر دقت کنید:
<html>
   <head>
        <title>Follow me!</title>
        <script type="text/javascript" src="../scripts/jquery-1.2.js"></script>
        <script type="text/javascript">
           // در زمان Reday بودن صفحه عنصر مورد نظر ایجاد می‌شود
           $(function(){
              $("<p>Hi there!</p>").insertAfter("#followMe");
           });
        </script>
    </head>
<body>
     <p id="followMe">Follow me!</p>
</body>
</html>

در کد بالا زمانی که صفحه مورد نظر Ready شد تابع مورد نظر ما اجرا شده و در عناصر صفحه بعد از عنصری که id آن followMe می‌باشد یک عنصر p را ایجاد می‌کند. که خروجی آن شبیه تصویر زیر خواهد بود.

مزیت دیگر jQuery این است که در صورتی که امکانی را ندارد شما به آسانی می‌توانید آن را توسعه داده و برای آن پلاگین طراحی کنید.

برای پایان دادن به این پست همانطور که دیدیم jQuery قادر به انجام کارهای زیر است:
  • انتخاب عناصر و ایجاد مجموعه ای از آنها که آماده اعمال متد‌های مختلف می‌باشند.
  • استفاده به عنوان یک فضای نام برای توابع سودمند.
  • ایجاد اشیا مختلف HTML بروی صفحه.
  • اجرای کد به محض آماده شدن اشیای صفحه.

موفق وموید باشید

مطالب
نحوه راه اندازی مجدد یک دیتابیس اس کیوال سرور پس از پر شدن هارد دیسک

امروز یکی از برنامه‌ها (برنامه ASP.Net) با مشکل زیر مواجه شده بود:

پیغام خطا:
اتصال با سرور اس کیوال قطع شده است. لطفا با مسئول مربوطه هماهنگ نمائید.
SQLErr:4060

این خطا به معنای عدم امکان باز کردن دیتابیس است.

در طی این مدت با موارد زیادی از این دست (مشکلات مختلف عدم امکان برقراری ارتباط با اس کیوال سرور) برخورد داشتم که خلاصه تمام آن‌ها تابع زیر شده است:
public void CheckSQLServerStat(Exception ex)
{
try
{
SqlException ar = (SqlException) ex;
switch (ar.Number)
{
case 2:
case 11:
case 17:
case 40:
case 4060:
case 1326:
case 17142:
case 18456:
HttpContext.Current.Response.Write("<br/>" + "اتصال با سرور اس کیوال قطع شده است. لطفا با مسئول مربوطه هماهنگ نمائید." + "<br/> SQLErr:" + ar.Number + "<br/>");
break;
}
}catch{}
}

هنگام رخ‌دادن یک خطا (در توابعی که با اس کیوال سرور کار می‌کنند)، exception حاصل را به این تابع پاس کرده و خطای حاصل را به صورت مختصر و مفید به کاربر نشان می‌دهم. کاربر متوجه می‌شود که یک مشکل اساسی در سیستم رخ داده است. برنامه نویس هم با جستجو در مورد شماره خطا می‌تواند مشکل یابی کند. تابع Response.Write هم بالاتر از هر المان دیگری در صفحه، این خطا را به شکل واضحی نمایش می‌دهد. (کلا ریخت صفحه را به هم می‌ریزد که از لحاظ روانی لازم است! چون عملا در این حالت سیستم از کار افتاده است)

به management studio اس کیوال سرور که مراجعه کردم، علامت خاصی کنار نام دیتابیس نبود فقط برخلاف سایر دیتابیس‌ها که آیکون + مربوط به باز شدن tree آن وجود دارد، این یک مورد آن‌را نداشت. بر روی نام دیتابیس کلیک راست کردم و انتخاب خواص، خطای زیر نمایش داده شد:
------------------------------
An exception occurred while executing a Transact-SQL statement or batch. (Microsoft.SqlServer.ConnectionInfo)
------------------------------
Database 'dbName' cannot be opened due to inaccessible files or insufficient memory or disk space. See the SQL Server errorlog for details. (Microsoft SQL Server, Error: 945)
------------------------------

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

use master;
alter database dbName set OFFLINE;
alter database dbName set online;

به این صورت دیتابیس مربوطه به حالت عادی بازگشت و قابل استفاده شد.

مطالب
ایندکس منحصر به فرد با استفاده از Data Annotation در EF Code First
در حال حاضر امکان خاصی برای ایجاد ایندکس منحصر به فرد در EF First Code وجود ندارد, برای این کار راه‌های زیادی وجود دارد مانند پست قبلی آقای نصیری, در این آموزش از Data Annotation و یا همان Attribute  هایی که بالای Property‌های مدل‌ها قرار می‌دهیم, مانند کد زیر : 
public class User
    {
        public int Id { get; set; }

        [Unique]
        public string Email { get; set; }

        [Unique("MyUniqueIndex",UniqueIndexOrder.ASC)]
        public string Username { get; set; }

        [Unique(UniqueIndexOrder.DESC)]
        public string PersonalCode{ get; set; }

        public string Password { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

همانطور که در کد بالا می‌بینید با استفاده از Attribute Unique ایندکس منحصر به فرد آن در دیتابیس ساخته خواهد شد.
ابتدا یک کلاس برای Attribute Unique به صورت زیر ایحاد کنید : 
using System;

namespace SampleUniqueIndex
{
    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class UniqueAttribute : Attribute
    {
        public UniqueAttribute(UniqueIndexOrder order = UniqueIndexOrder.ASC) {
            Order = order;
        }
        public UniqueAttribute(string indexName,UniqueIndexOrder order = UniqueIndexOrder.ASC)
        {
            IndexName = indexName;
            Order = order;
        }
        public string IndexName { get; private set; }
        public UniqueIndexOrder Order { get; set; }
    }

    public enum UniqueIndexOrder
    {
        ASC,
        DESC
    }
}
در کد بالا یک Enum برای مرتب سازی ایندکس به دو صورت صعودی و نزولی قرار دارد, همانند کد ابتدای آموزش که مشاهده می‌کنید امکان تعریف این Attribute به سه صورت امکان دارد که به صورت زیر می‌باشد:
1. ایجاد Attribute بدون هیچ پارامتری که در این صورت نام ایندکس با استفاده از نام جدول و آن فیلد ساخته خواهد شد :  IX_Unique_TableName_FieldName و مرتب ساری آن به صورت صعودی می‌باشد.
2.نامی برای ایندکس انتخاب کنید تا با آن نام در دیتابیس ذخبره شود, در این حالت مرتب سازی آن هم به صورت صعودی می‌باشد.
3. در حالت سوم شما ضمن وارد کردن نام ایندکس مرتب سازی آن را نیز وارد می‌کنید.
بعد از کلاس Attribute حالا نوبت به کلاس اصلی میرسد که به صورت زیر می‌باشد:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Metadata.Edm;
using System.Linq;
using System.Reflection;

namespace SampleUniqueIndex
{
    public static class DbContextExtention
    {
        private static BindingFlags PublicInstance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;

        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
                foreach (var uniqueProp in GetUniqueProperties(context, entityType, table))
                {
                    var indexName = string.IsNullOrWhiteSpace(uniqueProp.IndexName) ?
                        "IX_Unique_" + uniqueProp.TableName + "_" + uniqueProp.FieldName :
                        uniqueProp.IndexName;

                    if (!currentIndexes.Contains(indexName))
                    {
                        query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] ADD CONSTRAINT [" + indexName + "] UNIQUE ([" + uniqueProp.FieldName + "] " + uniqueProp.Order + "); ";
                    }
                    else
                    {
                        currentIndexes.Remove(indexName);
                    }
                }
                foreach (var index in currentIndexes)
                {
                    query += "ALTER TABLE [" + table.TableSchema + "].[" + table.TableName + "] DROP CONSTRAINT " + index + "; ";
                }
            }

            if (query.Length > 0)
                context.Database.ExecuteSqlCommand(query);
        }

        private static List<string> GetCurrentUniqueIndexes(DbContext context, string tableName)
        {
            var sql = "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS where table_name = '"
                      + tableName + "' and CONSTRAINT_TYPE = 'UNIQUE'";
            var result = context.Database.SqlQuery<string>(sql).ToList();
            return result;
        }
        private static IEnumerable<PropertyDescriptor> GetDbSets(DbContext context)
        {
            foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(context))
            {
                var notMapped = prop.GetType().GetCustomAttributes(typeof(NotMappedAttribute),true);
                if (prop.PropertyType.Name == typeof(DbSet<>).Name && notMapped.Length == 0)
                    yield return prop;
            }
        }
        private static List<UniqueProperty> GetUniqueProperties(DbContext context, Type entity, TableInfo tableInfo)
        {
            var indexedProperties = new List<UniqueProperty>();
            var properties = entity.GetProperties(PublicInstance);
            var tableName = tableInfo.TableName;
            foreach (var prop in properties)
            {
                if (!prop.PropertyType.IsValueType && prop.PropertyType != typeof(string)) continue;

                UniqueAttribute[] uniqueAttributes = (UniqueAttribute[])prop.GetCustomAttributes(typeof(UniqueAttribute), true);
                NotMappedAttribute[] notMappedAttributes = (NotMappedAttribute[])prop.GetCustomAttributes(typeof(NotMappedAttribute), true);
                if (uniqueAttributes.Length > 0 && notMappedAttributes.Length == 0)
                {
                    var fieldName = GetFieldName(context, entity, prop.Name);
                    if (fieldName != null)
                    {
                        indexedProperties.Add(new UniqueProperty
                        {
                            TableName = tableName,
                            IndexName = uniqueAttributes[0].IndexName,
                            FieldName = fieldName,
                            Order = uniqueAttributes[0].Order.ToString()
                        });
                    }
                }
            }
            return indexedProperties;
        }
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }

        internal class UniqueProperty
        {
            public string TableName { get; set; }
            public string FieldName { get; set; }
            public string IndexName { get; set; }
            public string Order { get; set; }
        }
        internal class TableInfo
        {
            public string EntityName { get; set; }
            public string TableName { get; set; }
            public string TableSchema { get; set; }
        }
    }
}
در کد بالا با استفاده از Extension Method برای کلاس DbContext یک متد با نام ExecuteUniqueIndexes  ایجاد می‌کنیم تا برای ایجاد ایندکس‌ها در دیتابیس از آن استفاده کنیم.
روند اجرای کلاس بالا به صورت زیر می‌باشد:
در ابتدای متد ()ExecuteUniqueIndexes  :
 public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            ...
        }
با استفاده از متد ()GetTables ما تمام جداول ساخته توسط دیتایس توسط DbContext را گرفنه:
        private static Dictionary<string, TableInfo> GetTables(DbContext context)
        {
            var tablesInfo = new Dictionary<string, TableInfo>();
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var tables = metadata.GetItemCollection(DataSpace.SSpace)
              .GetItems<EntityContainer>()
              .Single()
              .BaseEntitySets
              .OfType<EntitySet>()
              .Where(s => !s.MetadataProperties.Contains("Type")
                || s.MetadataProperties["Type"].ToString() == "Tables");
            foreach (var table in tables)
            {
                var tableName = table.MetadataProperties.Contains("Table")
                    && table.MetadataProperties["Table"].Value != null
                  ? table.MetadataProperties["Table"].Value.ToString()
                  : table.Name;
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();
                tablesInfo.Add(table.Name, new TableInfo
                {
                    EntityName = table.Name,
                    TableName = tableName,
                    TableSchema = tableSchema,
                });
            }

            return tablesInfo;
        }
با استفاده از این طریق چنانچه کاربر نام دیگری برای هر جدول در نظر بگیرد مشکلی ایجاد نمی‌شود و همینطور Schema جدول نیز گرفته می‌شود, سه مشخصه نام مدل و نام جدول و Schema جدول در کلاس TableInfo قرار داده می‌شود و در انتها تمام جداول در یک Collection قرار داده میشوند و به عنوان خروجی متد استفاده می‌شوند.
بعد از آنکه نام جداول متناظر با نام مدل آنها را در اختیار داریم نوبت به گرفتن تمام DbSet‌ها در DbContext می‌باشد که با استفاده از متد ()GetDbSets :
public static void ExecuteUniqueIndexes(this DbContext context)
        {
            var tables = GetTables(context);
            var query = "";
            foreach (var dbSet in GetDbSets(context))
            {
            ....
        }
در این متد چنانچه Property دارای Attribute NotMapped باشد در لیست خروجی متد قرار داده نمی‌شود. 
سپس داخل چرخه DbSet‌ها نوبت به گرفتن ایندکس‌های موجود با استفاده از متد ()GetCurrentUniqueIndexes برای این مدل می‌باشد تا از ایجاد دوباره آن جلوگیری شود و البته اگر ایندکس هایی را در مدل تعربف نکرده باشید از دیتابیس حذف شوند.
        public static void ExecuteUniqueIndexes(this DbContext context)
        {
            ...
            foreach (var dbSet in GetDbSets(context))
            {
                var entityType = dbSet.PropertyType.GetGenericArguments().First();
                var table = tables[entityType.Name];
                var currentIndexes = GetCurrentUniqueIndexes(context, table.TableName);
            }
        }
بعد از آن نوبت به گرفتن Property‌های دارای Attribute Unique می‌باشد که این کار نیز با استفاده از متد ()GetUniqueProperties انجام خواهد شد.
در متد ()GetUniqueProperties چند شرط بررسی خواهد شد از جمله اینکه Property از نوع Value Type باشد و نه یک کلاس سپس Attribute NotMapped را نداشته باشد و بعد از آن می‌بایست نام متناظر با آن Property را در دیتابیس به دست بیاریم برای این کار از متد ()GetFieldName استفاده می‌کنیم:
        public static string GetFieldName(DbContext context, Type entityModel, string propertyName)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var osMembers = metadata.GetItem<EntityType>(entityModel.FullName, DataSpace.OSpace).Properties;
            var ssMebers = metadata.GetItem<EntityType>("CodeFirstDatabaseSchema." + entityModel.Name, DataSpace.SSpace).Properties;
            
            if (!osMembers.Contains(propertyName)) return null;

            var index = osMembers.IndexOf(osMembers[propertyName]);
            return ssMebers[index].Name;
        }
برای این کار با استفاده از MetadataWorkspace در DbContext دو لیست SSpace و OSpace استفاده می‌کنیم که در ادامه در مورد این گونه لیست ها بیشتر توضیح می‌دهیم , سپس با استفاده از Member‌های این دو لیست و ایندکس‌های متناظر در این دو لیست نام متناظر با Property را در دیتابیس پیدا خواهیم کرد, البته یک نکته مهم هست چنانچه برای فیلد‌های دیتابیس OrderColumn قرار داده باشید دو لیست Member‌ها از نظر ایندکس متناظر متفاوت خواهند شد پس در نتیجه ایندکس به اشتباه برروی یک فیلد دیگر اعمال خواهد شد.
لیست‌ها در MetadataWorkspace:
1. CSpace : این لیست شامل آبجکت‌های Conceptual از مدل‌های شما می‌باشد تا برای Mapping دیتابیس با مدل‌های شما مانند مبدلی این بین عمل کند.
2. OSpace : این لیست شامل آبجکت‌های مدل‌های شما می‌باشد.
3. SSpace : این لیست نیز شامل آبجکت‌های مربوط به دیتابیس از مدل‌های شما می‌باشد
4. CSSpace : این لیست شامل تنظیمات Mapping بین دو لیست SSpace و CSpace می‌باشد.
5. OCSpace : این لیست شامل تنظیمات Mapping بین دو لیست OSpace و CSpace می‌باشد.
روند Mapping مدل‌های شما از OSpace شروع شده و به SSpace ختم میشود که سه لیست دیگز شامل تنظیماتی برای این کار می‌باشند.
و حالا در متد اصلی ()ExecuteUniqueIndexes ما کوئری مورد نیاز برای ساخت ایندکس‌ها را ساخته ایم.

حال برای استفاده از متد()ExecuteUniqueIndexes می‌بایست در متد Seed آن را صدا بزنیم تا کار ساخت ایندکس‌ها شروع شود، مانند کد زیر:
protected override void Seed(myDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
            context.ExecuteUniqueIndexes();
        }
چند نکته برای ایجاد ایندکس منحصر به فرد وجود دارد که در زیر به آنها اشاره می‌کنیم:
1. فیلد‌های متنی باید حداکثر تا 350 کاراکتر باشند تا ایندکس اعمال شود.
2. همانطور که بالاتر اشاره شد برای فیلد‌های دیتابیس OrderColumn اعمال نکنید که علت آن در بالا توضیح داده شد

دانلود فایل پروژه:
Sample_UniqueIndex.zip
مطالب
بررسی تغییرات Blazor 8x - قسمت پنجم - امکان تعریف جزیره‌های تعاملی Blazor Server
در Blazor 8x می‌توان صفحات SSR ای را به همراه Blazor server islands و یا Blazor WASM islands داشت؛ یعنی یک کامپوننت Blazor Server که داخل یک صفحه‌ی معمولی SSR قرار گرفته و با سرور، ارتباط SiganlR برقرار می‌کند و یا یک کامپوننت Blazor WASM که در قسمتی از صفحه‌ی SSR درج شده و درون مرورگر کاربر اجرا می‌شود. به هر کدام از این‌ها یک «جزیره‌ی تعاملی» گفته می‌شود (interactive island). در این قسمت، نکات مرتبط با جزایر تعاملی Blazor Server را بررسی می‌کنیم.


بررسی یک مثال: تهیه یک برنامه‌ی Blazor 8x برای نمایش لیست محصولات، به همراه جزئیات آن‌ها

به لطف وجود SSR در Blazor 8x، می‌توان HTML نهایی کامپوننت‌ها و صفحات Blazor را همانند صفحات MVC و یا Razor pages، در سمت سرور تهیه و بازگشت داد. این خروجی در نهایت یک static HTML بیشتر نیست و گاهی از اوقات ما به بیش از یک خروجی ساده HTML ای نیاز داریم.
در این مثال که بر اساس قالب dotnet new blazor --interactivity Server تهیه می‌شود، قصد داریم موارد زیر را پیاده سازی کنیم:
- صفحه‌ای که یک لیست محصولات فرضی را نمایش می‌دهد : بر اساس SSR
- صفحه‌ای که جزئیات یک محصول را نمایش می‌دهد: بر اساس SSR
- دکمه‌ای در ذیل قسمت نمایش جزئیات یک محصول، برای دریافت و نمایش لیست محصولات مشابه و مرتبط: بر اساس Blazor server islands

یعنی تا جائیکه ممکن است قصد نداریم تمام صفحات و تمام قسمت‌های برنامه را با فعالسازی سراسری حالت تعاملی Blazor server که در قسمت‌های قبل در مورد آن توضیح داده شد، پیاده سازی کنیم. می‌خواهیم فقط قسمت کوچکی از این سناریو را که واقعا نیاز به یک چنین قابلیتی را دارد، توسط یک جزیره‌ی تعاملی Blazor server واقع شده‌ی در قسمتی از یک صفحه‌ی استاتیک SSR، مدیریت کنیم.


مدل برنامه: رکوردی برای ذخیره سازی اطلاعات یک محصول

namespace BlazorDemoApp.Models;

public record Product
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public required string Description { get; set; }
    public decimal Price { get; set; }

    public List<int> Related { get; set; } = new();
}
در اینجا، هدف تعریف لیستی از محصولات فرضی است؛ به همراه خاصیتی که Id محصولات مشابه را نگهداری می‌کند (خاصیت Related).


سرویس برنامه: سرویسی برای بازگشت لیست محصولات

چون Blazor Server و SSR هر دو بر روی سرور اجرا می‌شوند، از لحاظ دسترسی به اطلاعات و کار با سرویس‌ها، هماهنگی کاملی وجود داشته و می‌توان کدهای یکسان و یکدستی را در اینجا بکار گرفت.
در ادامه کدهای کامل سرویس Services\ProductStore.cs را مشاهده می‌کنید:
using BlazorDemoApp.Models;

namespace BlazorDemoApp.Services;

public interface IProductStore
{
    IList<Product> GetAllProducts();
    Product GetProduct(int id);
    IList<Product> GetRelatedProducts(int productId);
}

public class ProductStore : IProductStore
{
    private static readonly List<Product> ProductsDataSource =
        new()
        {
            new Product
            {
                Id = 1, Title = "Smart speaker", Price = 22m,
                Description =
                    "This smart speaker delivers excellent sound quality and comes with built-in voice control, offering an impressive music listening experience.",
                Related = new List<int> { 2, 3 },
            },
            new Product
            {
                Id = 2, Title = "Regular speaker", Price = 89m,
                Description =
                    "Enjoy room-filling sound with this regular speaker. With its slick design, it perfectly fits into any room in your house.",
                Related = new List<int> { 1, 3 },
            },
            new Product
            {
                Id = 3, Title = "Speaker cable", Price = 12m,
                Description =
                    "This high-quality speaker cable ensures a reliable and clear audio connection for your sound system.",
            },
        };

    public IList<Product> GetAllProducts() => ProductsDataSource;

    public Product GetProduct(int id) => ProductsDataSource.Single(p => p.Id == id);

    public IList<Product> GetRelatedProducts(int productId)
    {
        var product = ProductsDataSource.Single(x => x.Id == productId);
        return ProductsDataSource.Where(p => product.Related.Contains(p.Id)).ToList();
    }
}
هدف از این سرویس، ارائه‌ی لیست تمام محصولات، دریافت اطلاعات یک محصول و همچنین یافتن لیست محصولات مشابه یک محصول خاص است.
این سرویس را باید در فایل Program.cs برنامه به صورت زیر معرفی کرد تا در فایل‌های razor برنامه‌ی جاری قابل دسترسی شود:
builder.Services.AddScoped<IProductStore, ProductStore>();


تکمیل صفحه‌ی نمایش لیست محصولات

قصد داریم زمانیکه کاربر برای مثال به آدرس فرضی http://localhost:5136/products مراجعه کرد، با تصویر لیستی از محصولات مواجه شود:


کدهای این صفحه را که در فایل Components\Pages\Store\ProductsList.razor قرار می‌گیرند، در ادامه مشاهده می‌کنید:

@page "/Products"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

<h3>Products</h3>

@if (_products == null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var item in _products)
    {
        <a href="/ProductDetails/@item.Id">
            <div>
                <div>
                    <h5>@item.Title</h5>
                </div>
                <div>
                    <h5>@item.Price.ToString("c")</h5>
                </div>
            </div>
        </a>
    }
}

@code {
    private IList<Product>? _products;

    protected override Task OnInitializedAsync() => GetProductsAsync();

    private async Task GetProductsAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _products = Store.GetAllProducts();
    }

}
توضیحات:
- جهت دسترسی به سرویس لیست محصولات، ابتدا سرویس IProductStore به این صفحه تزریق شده‌است.
- سپس در روال رویدادگردان آغازین OnInitializedAsync، کار دریافت اطلاعات و انتساب آن به لیستی، صورت گرفته‌است.
- در این متد جهت شبیه سازی یک عملیات async از یک Task.Delay استفاده شده‌است.
- چون این صفحه، یک صفحه‌ی SSR عادی است، بدون تعریف ویژگی StreamRendering در آن، پس از اجرای برنامه، هیچگاه قسمت loading که در حالت products == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد.
- لینک‌های نمایش داده شده‌ی در اینجا، به صفحه‌ی ProductDetails اشاره می‌کنند که در آن، جزئیات محصول انتخابی نمایش داده می‌شوند.


تکمیل صفحه‌ی نمایش جزئیات یک محصول


در صفحه‌ی کامپوننت Components\Pages\Store\ProductDetails.razor، کار نمایش جزئیات محصول انتخابی صورت می‌گیرد:

@page "/ProductDetails/{ProductId}"
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services

@inject IProductStore Store

@attribute [StreamRendering]

@if (_product == null)
{
    <p>Loading...</p>
}
else
{
    <div>
        <div>
            <h5>
                @_product.Title (@_product.Price.ToString("C"))
            </h5>
            <p>
                @_product.Description
            </p>
        </div>
        @if (_product.Related.Count > 0)
        {
            <div>
                <RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
            </div>
        }
    </div>
    <NavLink href="/Products">Back</NavLink>
}

@code {
    private Product? _product;

    [Parameter]
    public string? ProductId { get; set; }

    protected override Task OnInitializedAsync() => GetProductAsync();

    private async Task GetProductAsync()
    {
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate streaming rendering
        _product = Store.GetProduct(Convert.ToInt32(ProductId));
    }

}
توضیحات:
- باتوجه به نحوه‌ی تعریف مسیریابی این صفحه، پارامتر ProductId از طریق آدرسی مانند http://localhost:5136/ProductDetails/1 دریافت می‌شود.
- سپس این ProductId را در روال رخ‌دادگردان OnInitializedAsync، برای یافتن جزئیات محصول انتخابی از سرویس تزریقی IProductStore، بکار می‌گیریم.
- در اینجا نیز از Task.Delay برای شبیه سازی یک عملیات طولانی async مانند دریافت اطلاعات از یک بانک اطلاعاتی، کمک گرفته شده‌است.
- همچنین برای نمایش قسمت loading صفحه در حالت SSR، بازهم از StreamRendering استفاده کرده‌ایم.
- اگر دقت کرده باشید، ذیل تصویر اطلاعات محصول، دکمه‌ای نیز جهت بارگذاری اطلاعات محصولات مشابه، قرار دارد که ProductId محصول انتخابی را دریافت می‌کند:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" />
بنابراین در ادامه کامپوننت RelatedProducts فوق را تکمیل می‌کنیم.


تکمیل کامپوننت نمایش لیست محصولات مشابه و مرتبط

در فایل Components\Pages\Store\RelatedProducts.razor، کار نمایش یک دکمه و سپس نمایش لیستی از محصولات مشابه، صورت می‌گیرد:
@using BlazorDemoApp.Models
@using BlazorDemoApp.Services
@inject IProductStore Store

<button @onclick="LoadRelatedProducts">Related products</button>

@if (_loadRelatedProducts)
{
    @if (_relatedProducts == null)
    {
        <p>Loading...</p>
    }
    else
    {
        <div>
            @foreach (var item in _relatedProducts)
            {
                <a href="/ProductDetails/@item.Id">
                    <div>
                        <h5>@item.Title (@item.Price.ToString("C"))</h5>
                    </div>
                </a>
            }
        </div>
    }
}

@code{

    private IList<Product>? _relatedProducts;
    private bool _loadRelatedProducts;

    [Parameter]
    public int ProductId { get; set; }

    private async Task LoadRelatedProducts()
    {
        _loadRelatedProducts = true;
        await Task.Delay(1000); // Simulates asynchronous loading to demonstrate InteractiveServer mode
        _relatedProducts = Store.GetRelatedProducts(ProductId);
    }

}

تعاملی کردن کامپوننت نمایش لیست محصولات مشابه

مشکل! اگر در این حالت برنامه را اجرا کرده و بر روی دکمه‌ی related products کلیک کنیم، هیچ اتفاقی رخ نمی‌دهد! یعنی روال رویدادگران LoadRelatedProducts اصلا اجرا نمی‌شود. علت اینجا است که صفحات SSR، در نهایت یک static HTML بیشتر نیستند و فاقد قابلیت‌های تعاملی، مانند واکنش نشان دادن به کلیک بر روی یک دکمه هستند.
محدودیتی که به همراه صفحات SSR وجود دارد این است: این نوع کامپوننت‌ها و صفحات فقط یکبار رندر می‌شوند و نه بیشتر. بله می‌توان بر روی آن‌ها ده‌ها دکمه، نوارهای لغزان، دراپ‌داون و غیره را قرار داد، اما ... نمی‌توان هیچگونه تعاملی را با آن‌ها داشت. کامپوننت نهایی رندر شده و نمایش داده شده، دیگر در هیچ‌جائی اجرا نمی‌شود. در این حالت است که می‌توان تصمیم گرفت که نیاز است قسمتی از این صفحه، تعاملی شود.
به همین جهت باید نحوه‌ی رندر کامپوننت RelatedProducts را به صورت یک جزیره‌ی تعاملی Blazor server درآورد تا رویداد منتسب به دکمه‌ی related products موجود در آن، پردازش شود. بنابراین به صفحه‌ی ProductDetails.razor مراجعه کرده و rendermode@ این کامپوننت را به صورت زیر به حالت InteractiveServer تغییر می‌دهیم:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@InteractiveServer"/>
اکنون اگر برنامه را مجددا اجرا کرده و بر روی دکمه‌ی نمایش محصولات مشابه قرار گرفته در ذیل جزئیات یک محصول کلیک کنیم، بدون مشکل کار می‌کند:


نحوه‌ی پردازش پشت صحنه‌ی این نوع صفحات هم جالب است. برای اینکار به برگه‌ی network مخصوص developer tools مرورگر مراجعه کرده و مراحل رسیدن به صفحه‌ی نمایش جزئیات محصول را طی می‌کنیم:


- اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.
این موارد توسط فایل blazor.web.js درج شده‌ی در کامپوننت آغازین App.razor، به صورت خودکار مدیریت می‌شوند:
<script src="_framework/blazor.web.js"></script>

به علاوه در این حالت ای‌جکسی fetch، کار دریافت مجدد فایل‌های استاتیک مرتبط یک صفحه، مانند فایل‌های js.، css.، تصاویر و غیره، مجددا انجام نمی‌شود که این مورد خود مزیتی است نسبت به حالت متداول برنامه‌های ASP.NET Core MVC و یا Razor pages. در حالت Blazor 8x SSR، فقط یک partial update از نوع Ajax ای انجام می‌شود.
به این قابلیت، enhanced navigation هم گفته می‌شود. برای مثال زمانیکه یک فرم SSR را در Blazor 8x به سمت سرور ارسال می‌کنیم، موقعیت scroll به صورت خودکار ذخیره و بازیابی می‌شود تا کاربر با یک full post back مواجه نشده و موقعیت جاری خود را در صفحه از دست ندهد (چنین ایده‌ای، یک زمانی در برنامه‌های ASP.NET Web forms هم برقرار بود و هست! به نظر مایکروسافت هنوز دلتنگ طراحی قدیمی ASP.NET Web forms است!).

- همچنین به محض نمایش صفحه‌ی جزئیات محصول، پس از پایان کار نمایش آن، یک اتصال وب‌سوکت هم برقرار شده که مرتبط با جزیره‌ی تعاملی Blazor server تعریف شده، یا همان کامپوننت RelatedProducts است.

- یک disconnect را هم در اینجا مشاهده می‌کنید. اگر به یک صفحه‌ی تعاملی مراجعه کنیم، همانطور که مشخص است، یک اتصال SignalR برقرار می‌شود (که به آن در اینجا circuit هم می‌گویند). اما اگر از این صفحه به سمت یک صفحه‌ی SSR حرکت کنیم، پس از نمایش آن صفحه، اتصال SignalR قبلی که دیگر نیازی به آن نیست، بسته خواهد شد تا منابع سمت سرور، رها شوند.


در حین disconnect، شماره ID اتصال SignalR ای که دیگر به آن نیازی نیست، به برنامه ارسال می‌شود تا به صورت خودکار در سمت سرور بسته شود. تمام این موارد توسط blazor.web.js فریم‌ورک، مدیریت می‌شوند.
در این تصویر ابتدا به آدرس http://localhost:5136/ProductDetails/1 مراجعه کرده‌ایم که سبب برقراری اتصال یک وب‌سوکت شده‌است. سپس با کلیک بر روی دکمه‌ی back، به صفحه‌ی SSR مشاهده‌ی لیست محصولات برگشته‌ایم. در این حالت، دستور قطع اتصال SignalR قبلی صادر شده‌است.


نحوه‌ی مدیریت Pre-rendering در جزایر تعاملی Blazor 8x

به صورت پیش‌فرض زمانیکه از حالت رندر InteractiveServer استفاده می‌کنیم، قابلیت pre-rendering آن نیز فعال است. یعنی ابتدا حداقل قالب و قسمت‌های ثابت کامپوننت، در سمت سرور پردازش و رندر شده و سپس به سمت کلاینت ارسال می‌شوند. در این حالت کاربر، تجربه‌ی کاربری روان‌تری را شاهد خواهد بود؛ چون برای مدتی نباید منتظر آماده شدن کل UI مرتبط باشد و حداقل، قسمت‌هایی از صفحه که تعاملی نیستند، قابل دسترسی و مشاهده هستند.
اگر به هر دلیلی نیاز به غیرفعال کردن این قابلیت را دارید، باید به صورت زیر عمل کرد:
<RelatedProducts ProductId="Convert.ToInt32(ProductId)" @rendermode="@(new InteractiveServerRenderMode(false))"/>
در این حالت اگر برنامه را اجرا کنید، در حین نمایش صفحه‌ی اصلی در برگیرنده‌ی از نوع SSR، فقط جای این کامپوننت در صفحه مشخص می‌شود و پس از برقراری اتصال با سرور از طریق اتصال SignalR، شاهد UI کامپوننت RelatedProducts خواهیم بود، که نسبت به قبل، وقفه‌ای را سبب خواهد شد.

نحوه‌ی تعریف خواص استاتیک InteractiveServer بکار گرفته شده و یا کلاس InteractiveServerRenderMode را در ادامه مشاهده می‌کنید. جهت سهولت تعریف این موارد، سطر زیر که یک using static است، به فایل Imports.razor_ اضافه شده‌است:
@using static Microsoft.AspNetCore.Components.Web.RenderMode

public static class RenderMode
  {
    public static InteractiveServerRenderMode InteractiveServer { get; } = new InteractiveServerRenderMode();

    public static InteractiveWebAssemblyRenderMode InteractiveWebAssembly { get; } = new InteractiveWebAssemblyRenderMode();

    public static InteractiveAutoRenderMode InteractiveAuto { get; } = new InteractiveAutoRenderMode();
  }


public class InteractiveServerRenderMode : IComponentRenderMode
  {
    public InteractiveServerRenderMode()
      : this(true)
    {
    }

    public InteractiveServerRenderMode(bool prerender) => this.Prerender = prerender;

    public bool Prerender { get; }
  }


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: Blazor8x-Server-Normal.zip  
مطالب
خواندنی‌های 17 فروردین
  • - نقشه‌ای شبیه به نقشه‌های مترو!



  • - Ubuntu پرتابل! (قابل اجرا به صورت یک برنامه ویندوزی)




  • - MSXML 4.0 Service Pack 3 که اخیرا ارائه شده.

  • -دیدگاه مایکروسافت در مورد سال 2019 اکنون در قالب سیلورلایت به همراه سورس کد




مطالب
طراحی جدول فایل‌های پیوستی پایگاه داده
سناریو‌ی زیر را در نظر بگیرید:
می‌خواهید پروژه‌ای را انجام دهید که شامل جداول زیر است:
مقالات، اخبار، گالری تصاویر، گالری ویدیو، اسلایدشو، تبلیغات و ... و تمامی این جداول حداقل شامل یک فایل پیوست (عکس، فیلم، ...) می‌باشند. به طور مثال جدول مقالات دارای یک عکس نیز می‌باشد. قصد داریم تمام فایل‌ها را بر روی هاست ذخیره کرده و فقط آدرس و نام فایل را در دیتابیس ذخیره نمایم.

روش اول : استفاده از یک فیلد در هر جدول برای نگه دارای اسم فایل

مثال:
    public class Article
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Body { get; set; }
        public string RegisterDate { get; set; }
        public string FileName { get; set; }
    }
معایب:
این روش فقط در صورتی پاسخگو می‌باشد که هر رکورد فقط شامل یک فایل باشد. به طور مثال ممکن است برای یک مقاله، چندین عکس و فایل را ضمیمه‌ی آن کنیم. در این حالت این روش پاسخ گو نمی‌باشد؛ ولی می‌توانیم به صورت زیر نیز عمل کنیم:
ایجاد جدولی برای نگهداری فایل‌های هر رکورد از مقاله :


public class ArticleFiles
{
        public int Id { get; set; }
        public string FielName { get; set; }
        public string FileExtension { get; set; } 
        public Article Article { get; set; }
        public int FileSize { get; set; }
}
 
روش دوم : ایجاد جدولی پایه برای نگهدارای تمام فایل‌های آپلود شده

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


public class Attachment
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string FileName { get; set; }
        public string Extension { get; set; }
        public DateTime RegisterDate { get; set; }
        public int Size { get; set; }
        public ICollection<Article> ArticleFiles { get; set; }
        public ICollection<News> NewsFiles { get; set; }
        public int Viewed { get; set; }
    }
در این حالت باید بین تمام جداولی که نیاز به فایل دارند، رابطه ای با جدول Attachment داشته باشد. به طور مثال بین جدول مقالات و جدول Attachment یک رابطه‌ی یک به چند برای لیست فایل‌ها وجود خواهد داشت.


روش سوم : جدولی برای نگه داری اسم فایل‌ها، بدون رابطه

جدول Attachment در این روش، همانند روش دوم می‌باشد؛ با دو تفاوت:
1- با هیچ جدولی رابطه‌ای ندارد.
2- دو فیلد به عنوان نام جدول و Id رکورد به آن اضافه شده است.
تفاوت نسبت به روش دوم:
در روش دوم، ثبت یک رکورد، وابسته‌ی به ثبت رکورد در جدول Attachment بود و ابتدا می‌بایستی فایل در Attachment ذخیره می‌شد و بعد از بدست آوردن Id آن، رکورد مورد نظر (مقاله) را درج می‌کردیم. ولی در این روش ابتدا مقاله درج شده و بعد از آن فایل را با اسم جدول و ID رکورد مورد نظر ذخیره می‌کنیم:
public class Attachment
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string FileName { get; set; }
        public string Extension { get; set; }
        public DateTime RegisterDate { get; set; }
        public int Size { get; set; }
        public string TableName { get; set; }
        public int RowId { get; set; }
        public int Viewed { get; set; }        
    }

حالت پنجم :

ایجاد یک کلاس پایه و ارث بری سایر کلاس‌ها از کلاس پایه و ایجاد رابطه‌ای بین کلاس پایه و کلاس‌های مشتق شده.

نظراتی پیرامون حالت‌های مختلف:

1- داشتن یک جدول الحاقات برای هر جدول
  • اضافه کردن یک فیلد: بعضی‌ها این روش را ترجیح می‌دهند. به این دلیل که هر جدول، یک جدول attachment مختص به خود دارد؛ با توجه به فیلدهایی که لازم است. به طور مثال ممکن است بعد از گذشت مدتی، نیاز باشد تا دو فیلد برای فایل‌های هر مقاله اضافه شوند که در این حالت فقط به جدول attachment مقاله اضافه خواهند شد.

2- داشتن یک جدول پایه که کل فایل‌ها در آن ذخیره شوند (روش‌های دوم و سوم)

  • متمرکز شدن کل فایل‌ها در یک جدول: بیشتر پروژه‌ها و یا برنامه نویسان (طبق تجربه‌ی بنده) یک جدول پایه را برای این منظور دوست دارند. به دلیل اینکه تمام اطلاعات یکجا باشد.
  • عدم آپلود چندین باره‌ی یک فایل: در این حالت می‌توان از یک فایل چندین بار در چند جای مختلف استفاده نمود و در فضای هاست صرفه جویی می‌شود. این روش مدیریت سختی دارد و نیازمند کوئری‌های بیشتری می‌باشد.
  • وجود فیلد‌های زیاد null در جدول: در این حالت ممکن است ردیف‌هایی با ستون‌های مقدار null در جدول زیاد شوند. فرض کنید دو فیلد در جدول attachment وجود دارند که فقط توسط جدول مقالات مورد استفاده قرار می‌گیرند و در بقیه‌ی جداول بدون استفاده می‌باشند.


از کدام روش استفاده کنیم؟

نمی توان پیشنهاد کرد که الزاما از کدامیک از روش‌های بالا باید استفاده کنیم؛ چون نیازمندهای‌های هر پروژه با هم متفات است و نمی‌توان نسخه‌ای خاص را برای همه تجویز کرد.