مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS
روش کار برنامه‌های ASP.NET Core در IIS کاملا متفاوت است با تمام نگارش‌های پیشین ASP.NET؛ از این جهت که برنامه‌های ASP.NET Core در اصل یک برنامه‌ی متکی به خود از نوع Console می‌باشند. به همین جهت برای هاست شدن نیازی به IIS ندارند. این نوع برنامه‌ها به همراه یک self-hosted Web server ارائه می‌شوند (به نام Kestrel) و این وب سرور توکار است که تمام درخواست‌های رسیده را دریافت و پردازش می‌کند. هرچند در اینجا می‌توان از IIS صرفا به عنوان یک «front end proxy» استفاده کرد؛ از این جهت که Kestrel تنها یک وب سرور خام است و تمام امکانات و افزونه‌های مختلف IIS را شامل نمی‌شود.
بر روی ماشین‌های ویندوزی و ویندوزهای سرور، استفاده‌ی از IIS به عنوان پروکسی درخواست‌ها و ارسال آن‌ها به Kestrel، روش توصیه شده‌است؛ از این جهت که حداقل قابلیت‌هایی مانند «port 80/443 forwarding»، مدیریت طول عمر برنامه، مدیریت مجوزهای SLL آن و خیلی از موارد دیگر توسط Kestrel پشتیبانی نمی‌شود.


معماری پردازش نگارش‌های پیشین ASP.NET در IIS


در نگارش‌های پیشین ASP.NET، همه چیز داخل پروسه‌‌ای به نام w3wp.exe و یا IIS Worker Process پردازش می‌شود که در اصل چیزی نیست بجز همان IIS Application Pool. این AppPoolها، برنامه‌های ASP.NET شما را هاست می‌کنند و همچنین سبب وهله سازی و اجرای آن‌ها نیز خواهند شد.
در اینجا درایور http.sys ویندوز، درخواست‌های رسیده را دریافت کرده و سپس آن‌ها را به سمت سایت‌هایی نگاشت شده‌ی به AppPoolهای مشخص، هدایت می‌کند.


معماری پردازش برنامه‌های ASP.NET Core در IIS

روش اجرای برنامه‌های ASP.NET Core با نگارش‌های پیشین آن‌ها کاملا متفاوت هستند؛ از این جهت که داخل پروسه‌ی w3wp.exe اجرا نمی‌شوند. این برنامه‌ها در یک پروسه‌ی مجزای کنسول خارج از پروسه‌ی w3wp.exe اجرا می‌شوند و حاوی وب سرور توکاری به نام کسترل (Kestrel) هستند.


 این وب سرور، وب سروری است تماما دات نتی و به شدت برای پردازش تعداد بالای درخواست‌ها بهینه سازی شده‌است؛ تا جایی که کارآیی آن در این یک مورد چند 10 برابر IIS است. هرچند این وب سرور فوق العاده سریع است، اما «تنها» یک وب سرور خام است و به همراه سرویس‌های مدیریت وب، مانند IIS نیست.


در تصویر فوق مفهوم «پروکسی» بودن IIS را در حین پردازش برنامه‌های ASP.NET Core بهتر می‌توان درک کرد. ابتدا درخواست‌های رسیده به IIS می‌رسند و سپس IIS آن‌ها را به طرف Kestrel هدایت می‌کند.
برنامه‌های ASP.NET Core، برنامه‌های کنسول متکی به خودی هستند که توسط دستور خط فرمان dotnet اجرا می‌شوند. این اجرا توسط ماژولی ویژه به نام AspNetCoreModule در IIS انجام می‌شود.


همانطور که در تصویر نیز مشخص است، AspNetCoreModule یک ماژول بومی IIS است و هنوز برای اجرا نیاز به IIS Application Pool دارد؛ با این تفاوت که در تنظیم AppPoolهای برنامه‌های ASP.NET Core، باید NET CLR Version. را به No managed code تنظیم کرد.


اینکار از این جهت صورت می‌گیرد که IIS در اینجا تنها نقش یک پروکسی هدایت درخواست‌ها را به پروسه‌ی برنامه‌ی حاوی وب سرور Kestrel، دارد و کار آن وهله سازی NET Runtime. نیست. کار AspNetCoreModule این است که با اولین درخواست رسیده‌ی به برنامه‌ی شما، آن‌را بارگذاری کند. سپس درخواست‌های رسیده را دریافت و به سمت برنامه‌ی ASP.NET Core شما هدایت می‌کند (به این عملیات reverse proxy هم می‌گویند).


اگر دقت کرده باشید، برنامه‌های ASP.NET Core، هنوز دارای فایل web.config ایی با محتوای ذیل هستند:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*"
           modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%"
                arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false"
                stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
  </system.webServer>
</configuration>
توسط این تنظیمات است که AspNetCoreModule فایل‌های dll برنامه‌ی شما را یافته و سپس برنامه را به عنوان یک برنامه‌ی کنسول بارگذاری می‌کند (با توجه به اینکه حاوی کلاس Program و متد Main هستند).
یک نکته: در زمان publish برنامه، تنظیم و تبدیل مقادیر LAUNCHER_PATH و LAUNCHER_ARGS به معادل‌های اصلی آن‌ها صورت می‌گیرد (در ادامه مطلب بحث خواهد شد).


آیا واقعا هنوز نیازی به استفاده‌ی از IIS وجود دارد؟

هرچند می‌توان Kestrel را توسط یک IP و پورت مشخص، عمومی کرد و استفاده نمود، اما حداقل در ویندوز چنین توصیه‌ای نمی‌شود و بهتر است از IIS به عنوان یک front end proxy استفاده کرد؛ به این دلایل:
- اگر می‌خواهید چندین برنامه را بر روی یک وب سرور که از طریق پورت‌های 80 و 443 ارائه می‌شوند داشته باشید، نمی‌توانید از Kestrel  به صورت مستقیم استفاده کنید؛ زیرا از مفهوم host header routing که قابلیت ارائه‌ی چندین برنامه را از طریق پورت 80 و توسط یک IP میسر می‌کند، پشتیبانی نمی‌کند. برای اینکار نیاز به IIS و یا در حقیقت درایور http.sys ویندوز است.
- IIS خدمات قابل توجهی را به برنامه‌ی شما ارائه می‌کند. برای مثال با اولین درخواست رسیده، به صورت خودکار آن‌را اجرا و بارگذاری می‌کند؛ به همراه تمام مدیریت‌های پروسه‌ای که در اختیار برنامه‌های ASP.NET در طی سالیان سال قرار داشته‌است. برای مثال اگر پروسه‌ی برنامه‌ی شما در اثر استثنایی کرش کرد، دوباره با درخواست بعدی رسیده، حتما برنامه را بارگذاری و آماده‌ی خدمات دهی مجدد می‌کند.
- در اینجا می‌توان تنظیمات SSL را بر روی IIS انجام داد و سپس درخواست‌های معمولی را به Kestrel  ارسال کرد. به این ترتیب با یک مجوز می‌توان چندین برنامه‌ی Kestrel را مدیریت کرد.
- IISهای جدید به همراه ماژول‌های بومی بسیار بهینه و کم مصرفی برای مواردی مانند gzip compression of static content, static file caching, Url Rewriting هستند که با فعال سازی آن‌ها می‌توان از این قابلیت‌ها، در برنامه‌های ASP.NET Core نیز استفاده کرد.


نحوه‌ی توزیع برنامه‌های ASP.NET Core به IIS

روش اول: استفاده از دستور خط فرمان dotnet publish

برای این منظور به ریشه‌ی پروژه‌ی خود وارد شده و دستور dotnet publish را با توجه به پارامترهای ذیل اجرا کنید:
 dotnet publish --framework netcoreapp1.0 --output "c:\temp\mysite" --configuration Release
در اینجا برنامه کامپایل شده و همچنین مراحلی که در فایل project.json نیز ذکر شده‌اند، اعمال می‌شود. برای مثال در اینجا پوشه‌ها و فایل‌هایی که در قسمت include ذکر شده‌اند به خروجی کپی خواهند شد. همچین در قسمت scripts تمام مراحل ذکر شده مانند یکی کردن و فشرده سازی اسکریپت‌ها نیز انجام خواهد شد. قسمت postpublish تنها کاری را که انجام می‌دهد، ویرایش فایل web.config برنامه و تنظیم LAUNCHER_PATH و LAUNCHER_ARGS آن به مقادیر واقعی آن‌ها است.
{

    "publishOptions": {
        "include": [
            "wwwroot",
            "Features",
            "appsettings.json",
            "web.config"
        ]
    },
 
    "scripts": {
        "precompile": [
            "dotnet bundle"
        ],
        "prepublish": [
            //"bower install"
        ],
        "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
    }
}
و در نهایت اگر به پوشه‌ی output ذکر شده‌ی در فرمان فوق مراجعه کنید، این خروجی نهایی است که باید به صورت دستی به وب سرور خود برای اجرا انتقال دهید که به همراه تمام DLLهای مورد نیاز برای برنامه نیز هست.


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


برنامه‌های ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آن‌ها No Managed Code است. همچنین بهتر است به ازای هر برنامه‌ی جدید یک AppPool مجزا را ایجاد کنید تا کرش یک برنامه تاثیر منفی را بر روی برنامه‌ی دیگری نگذارد.

روش دوم: استفاده از ابزار Publish خود ویژوال استودیو

اگر علاقمند هستید که روش خط فرمان فوق را توسط ابزار publish ویژوال استودیو انجام دهید، بر روی پروژه در solution explorer کلیک راست کرده و گزینه‌ی publish را انتخاب کنید. در صفحه‌ای که باز می‌شود، بر روی گزینه‌ی custom کلیک کرده و نامی را وارد کنید. از این نام پروفایل، جهت ساده سازی مراحل publish، در دفعات آتی فراخوانی آن استفاده می‌شود.


در صفحه‌ی بعدی اگر گزینه‌ی file system را انتخاب کنید، دقیقا همان مراحل روش اول تکرار می‌شوند:


سپس می‌توانید فریم ورک برنامه و نوع ارائه را مشخص کنید:


و در آخر کار، Publish به این پوشه‌ی مشخص شده که به صورت پیش فرض در ذیل پوشه‌ی bin برنامه‌است، صورت می‌گیرد.


روش عیب یابی راه اندازی اولیه‌ی برنامه‌های ASP.NET Core

در اولین سعی در اجرای برنامه‌ی ASP.NET Core بر روی IIS به این خطا رسیدم:


در event viewer ویندوز چیزی ثبت نشده بود. اولین کاری را که در این موارد می‌توان انجام داد به این صورت است. از طریق خط فرمان به پوشه‌ی publish برنامه وارد شوید (همان پوشه‌ای که توسط IIS عمومی شده‌است). سپس دستور dotnet prog.dll را صادر کنید. در اینجا prog.dll نام dll اصلی برنامه یا همان نام پروژه است:


همانطور که مشاهده می‌کنید، برنامه به دنبال پوشه‌ی bower_components ایی می‌گردد که کار publish آن انجام نشده‌است (این پوشه در تنظیمات آغازین برنامه عمومی شده‌است و در لیست include قسمت publishOptions فایل project.json فراموش شده‌است).

روش دوم، فعال سازی stdoutLogEnabled موجود در فایل وب کانفیگ، به true است. در اینجا web.config نهایی تولیدی توسط عملیات publish را مشاهده می‌کنید که در آن پارامترهای  processPath و arguments مقدار دهی شده‌اند (همان قسمت postpublish فایل project.json). در اینجا مقدار stdoutLogEnabled به صورت پیش فرض false است. اگر true شود، همان خروجی تصویر فوق را در پوشه‌ی logs خواهید یافت:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet" arguments=".\Core1RtmEmptyTest.dll" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" />
  </system.webServer>
</configuration>
البته اگر کاربر منتسب به AppPool برنامه، دسترسی نوشتن در این پوشه را نداشته باشد، خطای ذیل را در event viewer ویندوز مشاهده خواهید کرد:
 Warning: Could not create stdoutLogFile \\?\D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest\bin\Release\PublishOutput\logs\stdout_10064_201672893654.log, ErrorCode = -2147024893.
همچنین پوشه‌ی logs را هم باید خودتان از پیش ایجاد کنید. به عبارتی کاربر منتسب به AppPool برنامه باید دسترسی نوشتن در پوشه‌ی logs واقع در ریشه‌ی برنامه را داشته باشد. این کاربر  IIS AppPool\DefaultAppPool نام دارد.



حداقل‌های یک هاست ویندوزی که می‌خواهد برنامه‌های ASP.NET Core را ارائه دهد

پس از نصب IIS، نیاز است ASP.NET Core Module نیز نصب گردد. برای این‌کار اگر بسته‌ی NET Core Windows Server Hosting. را نصب کنید، کافی است:
https://go.microsoft.com/fwlink/?LinkId=817246

این بسته به همراه NET Core Runtime, .NET Core Library. و ASP.NET Core Module است. همچنین همانطور که عنوان شد، برنامه‌های ASP.NET Core باید به AppPool ایی تنظیم شوند که NET CLR Version. آن‌ها No Managed Code است. این‌ها حداقل‌های راه اندازی یک برنامه‌ی ASP.NET Core بر روی سرورهای ویندوزی هستند.


هنوز فایل app_offline.htm نیز در اینجا معتبر است

یکی از خواص ASP.NET Core Module، پردازش فایل خاصی است به نام app_offline.htm. اگر این فایل را در ریشه‌ی سایت قرار دهید، برنامه پردازش تمام درخواست‌های رسیده را قطع خواهد کرد و سپس پروسه‌ی برنامه خاتمه می‌یابد. هر زمانیکه این فایل حذف شد، مجددا با درخواست بعدی رسیده، برنامه آماده‌ی پاسخگویی می‌شود.
مطالب
OpenID چیست؟
چند وقتی میشه که دنبال روش‌های OpenID هستم که ببینم چطوری کار می‌کنند، خودم هم تازه شروع کردم خوب قبل از هر چیزی اول ببینیم مفهوم OpenID چی هست؟ و کم کم جلو میریم و مثال هایی معرفی می‌کنیم.

OpenID به شما اجازه می‌دهد با استفاده از اکانت (نام کاربری) که در یک سایت دارید بتوانید به سایت‌های متفاوتی وارد شوید (لاگین کنید) بدون این که نیاز به ثبت نام دوباره در آن سایت‌ها داشته باشید.

نمونه بارز آن می‌توان به سایت هایی مانند  StackOverflowاشاره کرد.

OpenID به شما اجازه می‌دهد شما در یک نام کاربری که برای خود ایجاد کرده اید اطلاعاتی را که دارید با دیگر وبسایت‌ها به اشتراک بگذارید.
با OpenID کلمه عبور شما فقط توسط سرویس دهنده گرفته میشود و سرویس دهنده هویت شما را برای بازید از سایت دیگر تایید می‌کند.از طرف دیگر سرویس دهنده شما تا شما اجازه ندهید هیچ وب سایتی کلمه عبور شما را به هیچ وب سایتی نمی‌بیند. بنابراین نیازی نیست در مورد کلمه عبور خود نگرانی به خود راه دهید.

OpenID به سرعت در حال گسترش بروی وب استف در حال حاضر بیش از یک میلیارد نام کاربری (اکانت) وجود دارد و بیش از 50000 سایت OpenID را پذیرفته و با آن کار می‌کنند. چندین سازمان بزرگ موضوع OpenID را پذیرفته اند، سازمان هایی مانند Google, FaceBook,Yahoo!,Microsoft,AOL,MySpace,Sears, Universal Music Group, France Telecom, Novell, Sun, Telecom Italia 
و بسیاری از سازمان‌های دیگر.

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

OpenID

در پست‌های آینده مثال‌ها و کامپوننت‌های سورس بازی جهت این کار به شما معرفی خواهیم کرد. جهت مطالعات بیشتر می‌توانید به این لینک‌ها مراجعه کنین (^ و ^ و ^ ).
مطالب
امکان انجام محاسبات سمت کلاینت در EF Core
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی.  در حالت اول با <IEnumerable<T‌ها کار می‌کنیم که تمام عملیات در حافظه انجام می‌شود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبان‌های متفاوتی مانند SQL ترجمه می‌شوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع داده‌ی آن‌ها دقت کنیم، می‌توانیم نوع آن‌ها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که می‌تواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
var blogs = from blog in Blogs
   where blog.Name.Contains("Development")
   select blog;
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه می‌تواند بر اساس بانک‌های اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام می‌شود.
اما اکنون چطور؟
var blogs = from blog in Blogs
   where blog.Name.ComputeHash() == 0
   select blog;
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کرده‌ایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمه‌ی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئری‌هایی بهبود یافته‌است که به آن، محاسبات سمت کلاینت گفته می‌شود.


یک مثال: بررسی تاثیر ارزیابی‌های سمت کلاینت در EF Core

فرض کنید ساختار جدول بلاگ‌ها به صورت زیر است:
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }  
}
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کرده‌ایم:
    public static class StringExtensions
    {
        public static int ComputeHash(this string str)
        {
            var hash = 0;
            foreach (var ch in str)
            {
                hash += (int)ch;
            }
            return hash;
        }
    }
اکنون می‌خواهیم بلاگ‌هایی را پیدا کنیم که Hash مربوط به Url آن‌ها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
using (var context = new BloggingContext())
{
   var blogs = context.Blogs
     .Where(blog => blog.Url.ComputeHash() >= 10)
     .ToList();
   Console.WriteLine(blogs.First().Url);
}
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمی‌کند:
SELECT [blog].[BlogId], [blog].[Url]
   FROM [Blogs] AS [blog]
به این معنا که در ارزیابی‌های سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده می‌کند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آن‌را از کوئری حذف می‌کند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده می‌شود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کرده‌است، متد ComputeHash را در سمت کلاینت بر روی این نتیجه‌ی دریافتی اعمال می‌کند. یعنی مرحله‌ی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.


چگونه متوجه شویم که ارزیابی سمت کلاینت رخ داده‌است؟

EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابی‌های سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs
   .Select(b => new
   {
      IdUrlString = string.Join(", ", b.BlogId, b.Url),
   }).ToList();
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا می‌شود، اما بهتر است از وقوع یک چنین رخ‌دادهایی مطلع شویم:
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;");
                optionsBuilder.ConfigureWarnings(warnings =>
                {
                    warnings.Log(CoreEventId.IncludeIgnoredWarning);
                    warnings.Log(RelationalEventId.QueryClientEvaluationWarning);
                });
            }
        }
    }
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ می‌شود:
 warn: Microsoft.EntityFrameworkCore.Query[200500]
The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
عنوان کرده‌است که قابلیت ترجمه‌ی ComputeHash را به SQL نداشته و آن‌را در نهایت به صورت محلی و در سمت کلاینت محاسبه می‌کند.

اگر می‌خواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابی‌ها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).


تاثیر ارزیابی‌های سمت کلاینت بر روی کارآیی برنامه

هرچند قابلیت ارزیابی‌های سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبه‌ی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال می‌شود.
ب) این قابلیت ممکن است برنامه نویس‌ها را از تفکر در مورد یافتن روش‌های محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا می‌شود، اما اگر آن‌را به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs
   .Select(b => new
   {
     IdUrlString = b.BlogId + "," + b.Url
   }).ToList();
اینبار به یک خروجی SQL قابل محاسبه‌ی در سمت سرور، خواهیم رسید:
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString]
FROM [Blogs] AS [b]
به همین جهت حداقل لاگ کردن ارزیابی‌های سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: ClientSideEvaluation.zip
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت اول
قصد داریم طی یک سری مقالات به توسعه یک سیستم مدیریت محتوا بپردازیم. مسلما فاصله‌ی زمانی بین انتشار مقالات این سری، کمی زیاد خواهد بود. ولی سعی خواهیم کرد تا قدم به قدم و با تحلیل و توضیح کافی هر بخش به این هدف برسیم.
همکاران این قسمت:
پیشنیاز‌ها:
در زیر بخش هایی که برای این سیستم تا به امروز در نظر گرفتیم (مسلما با ارائه ایده‌ها و بازخورد‌های دوستان این امکانات دستخوش تغییر خواهند شد) ، به شرح زیر میباشد:
  • انجمن
  • ارتباط دوستی
  • سیستم ترفیع رتبه
  • Themeable
  • سیستم Following
  • صفحات داینامیک
  • سیستم پیام رسانی
  • امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی مختلف
  • پیغام خصوصی
  • وبلاگ
  • نظرسنجی ها
  • مدیریت کاربران با دسترسی‌ها داینامیک
  • اخبار
  • آگهی ها
در این قسمت مدل‌های مربوط به بخش وبلاگ را بررسی میکنیم. ابتدا قصد داشتیم تا امکان نگارش یک پست توسط چندین کاربر را به طور همزمان، در سیستم قرار دهیم و ایده کار هم که توسط یکی دوستان (محمد شریفی) پیشنهاد شد به صورت زیر بود:
ابتدا کاربری به عنوان ایجاد کننده اصلی پست، بخش‌ها مختلفی را برای یک پست در نظر بگیرد و هر قسمت را به یک همکار نسبت دهد و با اتمام تمام بخش‌ها و اعلام آن توسط همکاران، کاربر اصلی ایجاد کننده پست بتواند بخش‌ها را باهم ادغام کند و برای آماده انتشار نهایی باشد.
ولی خب به نظر بنده الان سیستم‌های ارتباطی قدرتمندتری هم هست که همکاران در مورد نگارش یک پست به صورت مشارکتی بپردازند و صرفا در مرحله‌ی انتشار، این کاربران به عنوان کاربران همکار به پست منتشر شده نسبت داده شوند تا در زیر پست مشخصات کامل آنها قابل مشاهد باشد (راه حلی که پیاده سازی خواهیم کرد).
مدل‌های در نظر گرفته شده برای سیستم وبلاگ به صورت زیر می‌باشد:

مخزن برچسب ها
/// <summary>
    /// Represents the lable 
    /// </summary>
    public class Tag
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="Tag"/>
        /// </summary>
        public Tag()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets Tag's identifier
        /// </summary>
        public virtual Guid  Id { get; set; }
        /// <summary>
        /// sets or gets Tag's name
        /// </summary>
        public virtual string Name { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets Tag's posts
        /// </summary>
        public virtual ICollection<BlogPost> BlogPosts { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی مخزن برچسب‌های سیستم ما خواهد بود. جدول حاصل از این مدل با جداول سایر بخش‌ها که نیاز به داشتن برچسب خواهند داشت، ارتباط چند به چند خواهد داشت. برای این منظور همانطور که مشخص است در قسمت NavigationProperties یک لیست از BlogPost معرفی شده است و در ادامه خواهیم دید که لیستی از کلاس Tag هم در کلاس BlogPost معرفی خواهد شد.

مدل پیش نویس ها
  /// <summary>
    /// Represents the Post's Draft
    /// </summary>
    public  class BlogDraft
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="BlogDraft"/>
        /// </summary>
        public  BlogDraft()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of post's draft
        /// </summary>
        public virtual  Guid Id { get; set; }
        /// <summary>
        /// gets or sets body of post's draft
        /// </summary>
        public virtual  string Body { get; set; }
        /// <summary>
        /// gets or set title of post's draft
        /// </summary>
        public virtual  string Title { get; set; }
        /// <summary>
        /// gets or sets tags of post's draft that seperated using ','
        /// </summary>
        public virtual  string TagNames { get; set; }
        /// <summary>
        /// gets or sets value indicating whether this draft is ready to publish
        /// </summary>
        public virtual  bool IsReadyForPublish { get; set; }
        /// <summary>
        /// ges ro sets DateTime that this draft added
        /// </summary>
        public virtual  DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets date that this draft publish as ready
        /// </summary>
        public virtual DateTime? ReadyForPublishOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Id of user that he is owner of this draft
        /// </summary>
        public virtual  long OwnerId { get; set; }
        /// <summary>
        /// gets or sets user that he is owner of this draft
        /// </summary>
        public virtual  User Owner { get; set; }
        #endregion
    }
کلاس فوق برای ذخیره سازی پست‌های پیشنویس کاربران در نظر گرفته شده است. شاید بهتر بود این پیش نویس‌ها هم در همان مدل پستی که در ادامه مشاهده می‌کنید ادغام شوند. ولی این یک تصمیم شخصی برای این کار بوده و برای سیستی بزرگی که امکان دارد حذف و درج پیشنویس‌ها زیاد باشد و فرض بر اینکه long هم برای آی دی جواب گو نخواهد بود و نیاز است از Guid ای که به صورت متوالی افزایش میابد به منظور جلوگیری از Fragmentation، استفاده کرد. بقیه فیلد‌ها هم مشخص هستند و نیازی به توضیح اضافی نخواهد بود. مسلما هر کاربر می‌تواند چندین پیشنویس داشته باشد. لذا ارتباط یک به چند مابین کاربر و پیشنویس پست، خواهیم داشت.
مدل امتیاز دهی کاربران
/// <summary>
    /// Section of Rating
    /// </summary>
    public enum RatingSection
    {
        News,
        Announcement,
        ForumTopic,
        BlogComment,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        ...
    }


    /// <summary>
    /// Represents Rating Record regard by section type for Rating System
    /// </summary>
    public class UserRating
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="UserRating"/>
        /// </summary>
        public UserRating()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Id of Rating Record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets value of rate
        /// </summary>
        public virtual double RatingValue { get; set; }
        /// <summary>
        /// gets or sets Section's Id 
        /// </summary>
        public virtual long SectionId { get; set; }
        /// <summary>
        /// gets or sets Section 
        /// </summary>
        public virtual RatingSection Section { get; set; }
        #endregion

        #region Navigation Properties
        /// <summary>
        /// gets or sets user that rate one section
        /// </summary>
        public virtual User Rater { get; set; }
        /// <summary>
        /// gets or sets Rater Id that rate one section
        /// </summary>
        public virtual long RaterId { get; set; }

        #endregion
    }
نوع داده‌ی شمارشی RatingSection مشخص کننده‌ی بخش‌های مختلف سیستم می‌باشد که نیاز به امتیاز دهی دارند. چون سیستم ما یک سیستم بسته می‌باشد و برای امتیاز دهی نیاز به احراز هویت خواهد بود، لذا باید از امتیاز دادن بیش از یک بار برای هر بخش جلوگیری کنیم. برای این کار می‌توان بین تمام بخش‌های موجود ارتباط‌های چند به چند در نظر گرفت. برای مثال یک ارتباط چند به چند بین کاربر و نظرات که مشخص کننده‌ی این است که یک کاربر می‌تواند به چند نظر امتیاز دهد و هر نظر هم میتواند چندین امتیاز دهنده داشته باشد ولی تعداد جداول بالا خواهد رفت و کار آن هم زیاد خواهد شد. برای این منظور میتوان یک جدول به مانند UserRating در نظر گرفت که صرفا با جدول کاربر ما یک ارتباط یک به چند دارد و با استفاده از خصوصیت RatingSection موجود در این کلاس وخصوصیت SectionId و همچنین در نظر گرفتن یک کلید منحصر به فرد بر روی خصوصیت‌های RatingSection ، RaterId و SectionId، از امتیاز دادن چند باره‌ی کاربر به یک بخش جلوگیری کرد.
/// <summary>
    /// Represent the rating as ComplexType
    /// </summary>
    [ComplexType]
    public class Rating
    {
        /// <summary>
        /// sets or gets total of rating
        /// </summary>
        public virtual double? TotalRating { get; set; }
        /// <summary>
        /// sets or gets rater's count
        /// </summary>
        public virtual long? RatersCount { get; set; }
        /// <summary>
        /// sets or gets average of rating
        /// </summary>
        public virtual double? AverageRating { get; set; }
    }
کلاس بالا یک ComplexType می‌باشد که در نهایت به هیچ جدولی مپ نخواهد شد و صرفا به منظور کپسوله کردن یکسری فیلد مورد استفاده قرار گرفته است و از این کلاس می‌توان در هر بخش که لازم است امتیاز دهی داشته باشیم، به عنوان یک خصوصیت، استفاده کرد.

کلاس پایه‌ی محتوا
/// <summary>
    /// Represents a base class for every content in system
    /// </summary>
    public abstract class BaseContent
    {
        #region Properties

        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of publishing content
        /// </summary>
        public virtual DateTime PublishedOn { get; set; }
        /// <summary>
        /// gets or sets Last Update's Date
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets meta title for seo
        /// </summary>
        public virtual string MetaTitle { get; set; }
        /// <summary>
        /// gets or sets meta keywords for seo
        /// </summary>
        public virtual string MetaKeywords { get; set; }
        /// <summary>
        /// gets or sets meta description of the content
        /// </summary>
        public virtual string MetaDescription { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual string FocusKeyword { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content use CanonicalUrl
        /// </summary>
        public virtual bool UseCanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets CanonicalUrl That the Post Point to it
        /// </summary>
        public virtual string CanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Follow for Seo
        /// </summary>
        public virtual bool UseNoFollow { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Index for Seo
        /// </summary>
        public virtual bool UseNoIndex { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content in sitemap
        /// </summary>
        public virtual bool IsInSitemap { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed for anonymouses 
        /// </summary>
        public virtual bool AllowCommentForAnonymous { get; set; }
        /// <summary>
        /// gets or sets  viewed count by rss
        /// </summary>
        public virtual long ViewCountByRss { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.Approved).Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  comments
        /// <remarks>The same as if we run Item.Comments.where(a=>a.Status==Status.UnApproved).Count()
        /// We use this property for performance optimization (no SQL command executed)</remarks></summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets value  indicating whether the content is logical deleted or hidden
        /// </summary>
        public virtual bool IsDeleted { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content show with rssFeed
        /// </summary>
        public virtual bool ShowWithRss { get; set; }
        /// <summary>
        /// gets or sets value indicating maximum days count that users can send comment
        /// </summary>
        public virtual int DaysCountForSupportComment { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets icon name with size 200*200 px for snippet 
        /// </summary>
        public virtual string SocialSnippetIconName { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public virtual string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public virtual string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets body of content's comment
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Author { get; set; }

        /// <summary>
        /// gets or sets Id of user that create this record
        /// </summary>
        public virtual long AuthorId { get; set; }
        /// <summary>
        /// get or set  the tags integrated with content
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        #endregion

    }

بخش‌های مختلفی که در ابتدای مقاله مطرح شدند، دارای یکسری خصوصیات مشترک می‌باشند و برای این منظور این خصوصیات را در یک کلاس پایه کپسوله کرده‌ایم. شاید تفکر شما این باشد که میخواهیم ارث بری TPH یا TPT را اعمال کنیم. ولی با توجه به سلیقه‌ی شخصی، در این بخش قصد استفاده از ارث بری را ندارم. 

نکته‌ای که وجود دارد فیلد‌های ApprovedCommentsCount  UnApprovedCommentsCount و TagNames می‌باشند که هنگام درج نظر جدید باید تعداد نظرات ذخیره شده را  ویرایش کنیم و هنگام ویرایش خود پست یا خبر با ... و یا حتی ویرایش خود تگ یا حذف آن تگ باید TagNames که لیست برچسب‌های محتوا را به صورت جدا شده با (,) از هم دیگر می‌باشد، ویرایش کنیم (جای بحث دارد).

مشخص است که هر یک از مطالب منتشر شده در بخش‌های وبلاگ، اخبار، نظرسنجی و آگهی‌ها، یک کابر ایجاد کننده (Author نامیده‌ایم) خواهد داشت و هر کاربر هم می‌تواند چندین مطلب را ایجاد کند. لذا رابطه‌ی یک به چند بین تمام این بخش‌ها مذکور و کاربر ایجاد خواهد شد.

مدل  LinkBack

 /// <summary>
    /// Represents link for implemention linkback
    /// </summary>
    public class LinkBack
    {

        #region Ctor
        /// <summary>
        /// create one instance of <see cref="LinkBack"/>
        /// </summary>
        public LinkBack()
        {
            CreatedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets link's Id
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets text for show Link
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets link's address 
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or set value indicating whether this link is internal o external
        /// </summary>
        public virtual LinkBackType Type { get; set; }
        /// <summary>
        /// gets or sets date that this record is added
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Post that associated
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets id of Post that associated
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }


 /// <summary>
    /// represents Type of ReferrerLinks
    /// </summary>
    public enum LinkBackType
    {
        /// <summary>
        /// Internal link
        /// </summary>
        Internal,
        /// <summary>
        /// External Link
        /// </summary>
        External
    }

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

مدل پست ها

  /// <summary> 
    /// Represents a blog post
    /// </summary>
    public  class BlogPost : BaseContent
    {
        #region Ctor
        /// <summary>
        /// Create one Instance of <see cref="BlogPost"/>
        /// </summary>
        public BlogPost()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

       #region Properties
        /// <summary>
        /// gets or sets Status of LinkBack Notifications
        /// </summary>
        public virtual LinkBackStatus LinkBackStatus { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set  blog post's Reviews
        /// </summary>
        public virtual ICollection<BlogComment> Comments { get; set; }
        /// <summary>
        /// get or set collection of links that reference to this blog post
        /// </summary>
        public virtual ICollection<LinkBack> LinkBacks { get; set; }
        /// <summary>
        /// get or set Collection of Users that Contribute on this post
        /// </summary>
        public virtual ICollection<User> Contributors  { get; set; }
        
        #endregion
    }


  /// <summary>
    /// Represents Status for ReferrerLinks
    /// </summary>
    public enum  LinkBackStatus
    {
        [Display(Name ="غیرفعال")]
        Disable,
        [Display(Name = "فعال")]
        Enable,
        [Display(Name = "لینک‌ها داخلی")]
        JustInternal,
        [Display(Name = "لینک‌ها خارجی")]
        JustExternal
    }
 کلاس بالا نشان دهنده‌ی مدل پست‌های ما در سیستم می‌باشد که بیشتر خصوصیات خود را از کلاس پایه‌ی BaseContent به ارث برده و لازم به تکرار آنها نیست. به علاوه یکسری خصوصیات دیگر، از جلمه‌ی آنها یک لیست از کلاس LinkBack، لیستی از کاربران مشارکت کننده به منظور نمایش مشخصات آنها در زیر پست و لیستی از کلاس BlogComment که نشان دهنده‌ی نظرات پست می‌باشد، هم خواهد داشت. خصوصیتی از نوع داده‌ی LinkBackStatus هم صرفا به منظور تنظیمات مدیریتی در نظر گرفته شده است. 

کلاس پایه نظرات
  /// <summary>
    /// Represents a base class for every comment in system 
    /// </summary>

    public abstract class BaseComment
    {
        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets displayName of this comment's Creator if  he/she is Anonymous
        /// </summary>
        public virtual string CreatorDisplayName { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// gets or sets siteUrl of Creator if he/she is Anonymous
        /// </summary>
        public virtual string SiteUrl { get; set; }
        /// <summary>
        /// gets or sets Email of Creator if he/she is anonymous
        /// </summary>
        public virtual string Email { get; set; }
        /// <summary>
        /// gets or sets status of comment
        /// </summary>
        public virtual CommentStatus Status { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }

        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long? CreatorId { get; set; }
        #endregion
    }

public enum CommentStatus
    {
        /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */
        [Display(Name = "تأیید شده")]
        Approved = 0,
        [Display(Name = "در انتظار بررسی")]
        Pending = 1,
        [Display(Name = "جفنگ")]
        Spam = 2,
        [Display(Name = "زباله دان")]
        Trash = -1
    }
به مانند BaseContent که برای کپسوله کردن خصوصیات تکراری و نه برای اعمال ارث بری TPH یا TPT، کلاس BaseComment را در نظر گرفته‌ایم. نکته‌ی مهم این است که هر نظری یک درج کننده دارد. ولی اگر فیلد AllowCommentForAnonymous مربوط به مطلب True باشد، این امکان وجود خواهد داشت تا کاربران احراز هویت نشده نیز نظر ارسال کنند. لذا CreatorId در کلاس BaseComment به صورت Nullable در نظر گرفته شده است و یک سری خصوصیت‌ها از قبیل SiteUrl ، CreatorDisplayName ، Email برای کاربران احراز هویت نشده در نظر گرفته شده است. نوع شمارشی CommentStatus نیز برای اعمال مدیریتی در نظر گرفته شده است.
مدل نظرات پست ها
/// <summary>
    /// Represents a blog post's comment
    /// </summary>
    public class BlogComment : BaseComment
    {
        #region Ctor
        /// <summary>
        /// Create One Instance for <see cref="BlogComment"/>
        /// </summary>
        public BlogComment()
        {
            Rating = new Rating();
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets BlogComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual BlogComment Reply { get; set; }
        /// <summary>
        /// get or set collection of blog's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<BlogComment> Children { get; set; }
        /// <summary>
        /// gets or sets post that this comment sent to it
        /// </summary>
        public virtual BlogPost Post { get; set; }
        /// <summary>
        /// gets or sets post'Id that this comment sent to it
        /// </summary>
        public virtual long PostId { get; set; }
        #endregion
    }
کلاس بالا مشخص کننده‌ی نظرات پست‌های وبلاگ می‌باشند که بیشتر خصوصیت‌های خود را از کلاس پایه‌ی BaseComment به ارث برده است و همچنین ساختار سلسله مراتبی آن نیز  قابل مشاهده است. 
برای سیستم Report یا همان گزارش خطا هم سیستمی شبیه به امتیاز دهی ستاره‌ای در نظر گرفته ایم. برای اینکه مقاله خیلی طولانی شد، بهتر است در مقاله‌ای جدا کار را ادامه داد.
نتیجه این قسمت

مطالب
Blazor 5x - قسمت 30 - برنامه‌ی Blazor WASM - افزودن پرداخت آنلاین توسط درگاه مجازی پرباد
در ادامه‌ی تمرین قسمت قبل که مقدمات ثبت درخواست رزرو یک اتاق را فراهم کردیم، اکنون می‌خواهیم اگر کاربری بر روی دکمه‌ی checkout now یک اتاق کلیک کرد، به درگاه مجازی پرباد منتقل شده، پرداخت را تکمیل کند، به برنامه هدایت شود و در آخر درخواست او در سیستم ثبت گردد. مزیت کار کردن با درگاه مجازی پرباد، امکان آزمایش محلی برنامه، بدون نیاز به یک درگاه بانکی واقعی است و زمانیکه قرار است با یک درگاه بانکی واقعی کار شود، فقط قسمت معرفی و تنظیمات ابتدایی مشخصات درگاه بانکی آن باید تغییر کند و نه هیچ قسمت دیگری از کدهای برنامه.


نصب پرباد و انجام تنظیمات اولیه‌ی آن

بسته‌های نیوگت پرباد را در دو پروژه‌ی زیر نصب خواهیم کرد:
الف) پروژه‌ی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" />
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که شامل بسته‌ها‌ی ASP.NET Core آن و همچنین محل ذخیره سازی مبتنی بر EF-Core آن است.

ب) پروژه‌ای که محل قرارگیری فایل‌های Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" />
  </ItemGroup>
</Project>
که در اینجا فقط نیاز به بسته‌ی EF-Core آن است تا بتوان Context مخصوص پرباد را در حین اعمال مهاجرت‌ها شناسایی کرد.

پس از نصب این بسته‌ها، به کلاس آغازین پروژه‌ی Web API مراجعه کرده و تنظیمات سرویس‌ها و همچنین میان‌افزار پرباد را انجام می‌دهیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
           // ...

            var connectionString = Configuration.GetConnectionString("DefaultConnection");

            services.AddParbad()
                    .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore())
                    .ConfigureGateways(gatewayBuilder =>
                    {
                        gatewayBuilder
                            .AddParbadVirtual()
                            .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway");
                    })
                    .ConfigureStorage(storageBuilder =>
                    {
                        storageBuilder.UseEfCore(efCoreOptions =>
                            {
                                var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name;
                                efCoreOptions.ConfigureDbContext = db =>
                                    db.UseSqlServer(
                                        connectionString,
                                        sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName)
                                    );
                            });
                    })
                    .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1)
                    .ConfigureOptions(parbadOptions =>
                    {
                        // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE";
                    });

           // ...
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // ...

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            if (env.IsDevelopment())
            {
                app.UseParbadVirtualGatewayWhenDeveloping();
            }
            else
            {
                app.UseParbadVirtualGateway();
            }
        }
    }
}
چند نکته:
- در متد ConfigureGateways می‌توان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شده‌است.
- در متد ConfigureStorage، تنظیمات EF-Core آن‌را مشاهده می‌کنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامه‌ی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- می‌خواهیم شماره‌ی تراکنش‌ها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت می‌توان عدد ابتدای آن‌را توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میان‌افزار آن‌را مشاهده می‌کنید که می‌تواند برای حالت توسعه و ارائه‌ی نهایی متفاوت باشد.


تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد

موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت می‌گیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا می‌توانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیف‌های جدول جزئیات سفارشات اتاق‌های هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class RoomOrderDetail
    {
        // ...

        [Required]
        public long ParbadTrackingNumber { get; set; }

        public bool IsPaymentSuccessful { get; set; }

        public string Status { get; set; }
    }
}
همچنین در پایان عملیات هم فیلدهای IsPaymentSuccessful و وضعیت اتاق را تکمیل می‌کنیم.


ایجاد جداول متناظر با ParbadDataContext

همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرت‌ها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرت‌های متناظر با ParbadDataContext، از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build

dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext

dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
چون برنامه دو Context ای است، نیاز است دوبار دستور تولید مهاجرت‌ها و دوبار دستور اعمال آن‌ها را به بانک اطلاعاتی صادر کرد که روش آن‌را در دستورات فوق مشاهده می‌کنید. پس از این دستورات، بانک اطلاعاتی برنامه شامل دو جدول جدید مخصوص پرباد خواهد بود:



روش یکپارچه سازی پرباد با یک برنامه‌ی SPA

روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال می‌کنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت می‌کند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژه‌ی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام می‌دهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیه‌ی نهایی صورت می‌گیرد و همینجا کار به پایان می‌رسد. این روش هرچند برای برنامه‌های سمت سرور ASP.NET Core کار می‌کند، اما ... به همین نحو با برنامه‌های تک صفحه‌ای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویس‌های برنامه از طریق یک HttpClient است و در این حالت دیگر نمی‌توان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت می‌گیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال می‌کنیم و در پاسخ، فقط یک شیء را دریافت می‌کنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آن‌را از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال می‌کنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگ‌هایی به اکشن متد PayRoomOrder ارسال می‌شوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
برنامه‌ی کلاینت برای اینکه بتواند این هدایت را انجام دهد، نیاز به نکته‌ی خاصی را دارد که در ادامه توضیح داده خواهد شد.
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت می‌شویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامه‌ی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
در اینجا شماره سفارش ابتدایی که مشخص است. همان شماره‌ای است که کار را با آن از سمت کلاینت آغاز کردیم. نکته‌ی مهم، TrackingNumber تراکنش است که بر اساس آن رکورد متناظری یافت شده و وضعیت نهایی آن‌را به کاربر نمایش می‌دهیم.

بنابراین روش یکپارچه سازی پربابد با برنامه‌های SPA، بر اساس Redirect‌های کامل است که سبب بارگذاری مجدد کل صفحه و آدرس‌ها می‌شوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آن‌را از دست خواهیم داد و مجبور به بازنویسی آن‌ها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirect‌ها و ارسال اطلاعات به درگاه‌های بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.


آشنایی با گردش کار برنامه

در این مثال، مراحل زیر را طی خواهیم کرد:

1- شروع به انتخاب یک بازه‌ی زمانی و تعداد شب اقامت


2- انتخاب یک اتاق از لیست اتاق‌ها با کلیک بر روی دکمه‌ی Book آن


3- کلیک بر روی دکمه‌ی checkout، در صفحه‌ی مشاهده‌ی جزئیات اتاق و شروع به پرداخت


4- هدایت به درگاه مجازی پرباد در سمت برنامه‌ی Web API


5- پرداخت و هدایت خودکار به سمت برنامه‌ی Web API، جهت تائید نهایی


6- هدایت نهایی به سمت برنامه‌ی کلاینت، جهت نمایش اطلاعات پرداخت



ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد

پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامه‌ی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
این اکشن متد و کنترلر آن به صورت زیر تهیه می‌شود:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ParbadPaymentController : Controller
    {
        private readonly IConfiguration _configuration;
        private readonly IOnlinePayment _onlinePayment;
        private readonly IRoomOrderDetailsService _roomOrderService;

        public ParbadPaymentController(
            IConfiguration configuration,
            IOnlinePayment onlinePayment,
            IRoomOrderDetailsService roomOrderService)
        {
            _configuration = configuration;
            _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment));
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpGet]
        public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
        {
            var verifyUrl = Url.Action(
                    action: nameof(ParbadPaymentController.VerifyRoomOrderPayment),
                    controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty),
                    values: null, protocol: Request.Scheme);

            var result = await _onlinePayment.RequestAsync(invoiceBuilder =>
                invoiceBuilder.UseAutoIncrementTrackingNumber()
                            .SetAmount(amount)
                            .SetCallbackUrl(verifyUrl)
                            .UseParbadVirtual()
            );

            if (result.IsSucceed)
            {
                await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber);

                // It will redirect the client to the gateway.
                return result.GatewayTransporter.TransportToGateway();
            }
            else
            {
                return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message));
            }
        }

        [HttpGet, HttpPost]
        public async Task<IActionResult> VerifyRoomOrderPayment()
        {
            var invoice = await _onlinePayment.FetchAsync();
            var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber);
            if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed)
            {
                return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed."));
            }

            var verifyResult = await _onlinePayment.VerifyAsync(invoice);
            if (verifyResult.Status == PaymentVerifyResultStatus.Succeed)
            {
                var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount);
                if (result == null)
                {
                    return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful"));
                }
                return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
            }
            return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message));
        }

        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
    }
}
توضیحات:
در اینجا کدهای کامل ParbadPaymentController مشاهده می‌کنید.

- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع می‌شود که دو پارامتر شماره سفارش و مبلغ آن‌را دریافت می‌کند.
[HttpGet]
public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
نوع آن هم عمدا، HttpGet درنظر گرفته شده‌است تا دقیقا مشخص باشد که فقط با Redirect کامل به آن (هدایت کامل از سمت کلاینت به سمت سرور)، کار خواهد کرد و هدف دیگری را دنبال نمی‌کند.

- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامه‌ی سمت کلاینت هدایت می‌شود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام می‌دهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال می‌کنیم.

- استفاده از UseAutoIncrementTrackingNumber سبب می‌شود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.

- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت می‌کند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آن‌را با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام می‌شود:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber)
        {
            var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId);
            if (order == null)
            {
                return;
            }

            order.ParbadTrackingNumber = trackingNumber;
            _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
        }
    }
}
بر اساس شماره سفارشی که داریم، رکورد متناظر با آن‌را یافته و سپس trackingNumber تولیدی را در آن به روز رسانی می‌کنیم.

- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ می‌دهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامه‌ی Blazor WASM مدیریت و بازنویسی کنیم.

- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت می‌شویم. یعنی اکنون به مرحله‌ی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیده‌ایم.
[HttpGet, HttpPost]
public async Task<IActionResult> VerifyRoomOrderPayment()
در اینجا ابتدا invoice.TrackingNumber در حال پردازش را دریافت می‌کنیم. به کمک این عدد می‌توان رکورد سفارش متناظر با آن‌را یافت. به همین جهت است که آن‌را به لیست فیلدهای جدول سفارشات اضافه کردیم. اینکار هم توسط متد GetOrderDetailByTrackingNumberAsync صورت می‌گیرد:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }
    }
}
- در ادامه پرباد کار تصدیق اطلاعات دریافتی از درگاه بانکی را انجام می‌دهد. دراینجا اگر عملیات با موفقیت مواجه شود، سه فیلدی را که در ابتدای بحث در مورد ثبت اطلاعات تراکنش اضافه کردیم، به روز رسانی می‌کنیم:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount)
        {
            var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber);
            if (order?.IsPaymentSuccessful != false || order.TotalCost != amount)
            {
                return null;
            }

            order.IsPaymentSuccessful = true;
            order.Status = BookingStatus.Booked;
            var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity);
        }
    }
}
- در اینجا بر اساس trackingNumber، سند متناظری را یافته و سپس بررسی می‌کنیم که آیا مبلغ سند، با مبلغ تائید شده، یکی هست یا خیر؟ اگر خیر، نیاز هست پرداخت را برگشت بزنیم که اینکار توسط متد کنسل پرباد قابل انجام است.

- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت می‌گیرد که Url آن به صورت زیر تامین می‌شود:
        private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage)
        {
            var clientBaseUrl = _configuration.GetValue<string>("Client_URL");
            return new Uri(new Uri(clientBaseUrl),
                $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString();
        }
در این متد Client_URL را از فایل appsettings.json برنامه‌ی Web API دریافت می‌کنیم که به آدرس ریشه‌ی برنامه‌ی کلاینت اشاره می‌کند:
{
   "Client_URL": "https://localhost:5002/"
}


تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد

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

الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعه‌ی آن‌را تا جائی پیش بردیم که اعتبارسنجی‌های آن‌را به علت استفاده‌ی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحله‌ی اعتبارسنجی، اکنون می‌خواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}"

@inject IJSRuntime JsRuntime
@inject ILocalStorageService LocalStorage
@inject IClientHotelRoomService HotelRoomService
@inject IClientRoomOrderDetailsService RoomOrderDetailsService
@inject NavigationManager NavigationManager
@inject HttpClient HttpClient

// ...

@code {

    // ...

    private async Task HandleCheckout()
    {
        if (!await HandleValidation())
        {
            return;
        }

        try
        {
            HotelBooking.OrderDetails.ParbadTrackingNumber = -1;
            HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id;
            HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount;
            var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails);

            await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved);

            var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
                            .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
                            .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
                            .Uri;
            NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    // ...
}
متد HandleValidation را در انتهای قسمت قبل تکمیل کردیم. اکنون OrderDetails را بر اساس اطلاعات فرم و انتخاب‌های کاربر، تکمیل کرده و به متد SaveRoomOrderDetailsAsync ارسال می‌کنیم تا Id سفارش را تولید کنیم. این همان Id ای است که قرار است به سمت سرور و Web API ارسال کنیم تا بر اساس آن تراکنش و Tracking Number ای را بتوان به رکورد جاری انتساب داد. بنابراین نیاز به کنترلر سمت Web API ای را داریم که بتواند این‌کار را انجام دهد:
namespace BlazorWasm.WebApi.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class RoomOrderController : Controller
    {
        private readonly IRoomOrderDetailsService _roomOrderService;

        public RoomOrderController(IRoomOrderDetailsService roomOrderService)
        {
            _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService));
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details)
        {
            var result = await _roomOrderService.CreateAsync(details);
            return Ok(result);
        }

        [HttpGet]
        public async Task<IActionResult> GetOrderDetail(int trackingNumber)
        {
            var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber);
            return Ok(result);
        }
    }
}
- متد Create، بر اساس اطلاعات وارد شده‌ی توسط کاربر، آن‌ها را تبدیل به یک رکورد سفارش جدید می‌کند و به سمت کلاینت بازگشت می‌دهد.
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام می‌دهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده می‌کنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شده‌اند:
namespace BlazorServer.Services
{
    public class RoomOrderDetailsService : IRoomOrderDetailsService
    {
        // ...

        public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details)
        {
            var roomOrder = _mapper.Map<RoomOrderDetail>(details);
            roomOrder.Status = BookingStatus.Pending;
            var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder);
            await _dbContext.SaveChangesAsync();
            return _mapper.Map<RoomOrderDetailsDTO>(result.Entity);
        }


        public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber)
        {
            var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails
                                            .Include(u => u.HotelRoom)
                                                .ThenInclude(x => x.HotelRoomImages)
                                            .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration)
                                            .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber);

            roomOrderDetailsDTO.HotelRoomDTO.TotalDays =
                roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days;
            return roomOrderDetailsDTO;
        }

       // ...
    }
}
اکنون که Web API Endpoint مدنظر را ایجاد کردیم، نیاز است سرویس سمت کلاینتی را نیز جهت تعامل با آن تهیه کنیم:
namespace BlazorWasm.Client.Services
{
    public interface IClientRoomOrderDetailsService
    {
        Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details);
        Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber);
    }
}

namespace BlazorWasm.Client.Services
{
    public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService
    {
        private readonly HttpClient _httpClient;

        public ClientRoomOrderDetailsService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail"))
                            .AddParameter("trackingNumber", trackingNumber.ToString())
                            .Uri;
            return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri);

        }

        public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
        {
            details.UserId = "unknown user!";
            var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details);
            var responseContent = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent);
            }
            else
            {
                //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent);
                throw new InvalidOperationException(responseContent);
            }
        }
    }
}
- متد GetOrderDetailAsync بر اساس trackingNumber دریافتی پس از عملیات تصدیق پرداخت، کار بازگشت جزئیات رکورد متناظری را انجام می‌دهد.
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد می‌کند. در اینجا با روش مشاهده‌ی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شده‌ایم که در مواقع لزوم می‌تواند راه‌گشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کرده‌ایم. این مورد را در قسمت‌های بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.

این سرویس جدید را هم باید به سیستم تزریق وابستگی‌های برنامه‌ی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            // ...
            builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
بنابراین در متد HandleCheckout ای که در حال بررسی آن هستیم، ابتدا متد SaveRoomOrderDetailsAsync فوق فراخوانی می‌شود تا توسط Web API Endpoint متناظری، یک رکورد سفارش ابتدایی را ایجاد کرده و Id آن‌را در اختیار ما قرار دهد.
سپس به قطعه کد مهم زیر می‌رسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder"))
    .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString())
    .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString())
    .Uri;
NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
اینجا است که برای نمونه آدرس https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000 ساخته شده و توسط متد NavigateTo فراخوانی می‌شود. فراخوانی متداول متد NavigateTo در اینجا کارساز نیست؛ چون سبب reload آدرس درخواستی نمی‌شود. یعنی هدایت‌های صورت گرفته‌ی توسط آن، در همان داخل مرورگر رخ می‌دهند و سبب ارسال درخواستی به سمت سرور نخواهند شد. می‌توان این رفتار را با ذکر پارامتر دوم آن تغییر داد. در اینجا اگر پارامتر forceLoad را به true تنظیم کنیم، ابتدا سبب هدایت به آدرس درخواستی و سپس reload کامل صفحه می‌شود (دقیقا مثل اینکه شخصی، آدرسی را در نوار آدرس مرورگر وارد کند و سپس دکمه‌ی enter را بفشارد). این reload است که برنامه‌ی کلاینت را اکنون به سمت برنامه‌ی Web API هدایت می‌کند.


نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن

پس از این مراحل، مرحله‌ی آخر کار باقی مانده‌است؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجه‌ی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
@inject ILocalStorageService LocalStorage
@inject IClientRoomOrderDetailsService RoomOrderDetailService
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager

@if (IsLoading)
{
    <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;">
        <img src="images/ajax-loader.gif" />
    </div>
}
else
{
    <div class="container">
        <div class="row mt-4 pt-4">
            <div class="col-10 offset-1 text-center">
            @if(IsPaymentSuccessful)
            {
                <h2 class="text-success">Booking Confirmed!</h2>
                <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p>
            }
            else
            {
                <h2 class="text-warning">Booking Failed!</h2>
                <p>@Message</p>
            }
            <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a>
            </div>
        </div>
    </div>
}

@code
{
    private bool IsLoading;
    private bool IsPaymentSuccessful;

    [Parameter] public int OrderId { set; get; }
    [Parameter] public long TrackingNumber { set; get; }
    [Parameter] public string Message { set; get; }

    protected override async Task OnInitializedAsync()
    {
        IsLoading = true;
        try
        {
            var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber);
            var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails);
            if(finalOrderDetail is not null &&
                finalOrderDetail.IsPaymentSuccessful &&
                finalOrderDetail.Status == BookingStatus.Booked &&
                localOrderDetail is not null &&
                localOrderDetail.TotalCost == finalOrderDetail.TotalCost)
            {
                IsPaymentSuccessful = true;
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails);
                await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking);
            }
            else
            {
                IsPaymentSuccessful = false;
            }
        }
        catch(Exception ex)
        {
            await JsRuntime.ToastrError(ex.Message);
        }
        finally
        {
            IsLoading = false;
        }
    }
}
این کامپوننت بر اساس مسیریابی که دارد:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
سه پارامتر شماره سفارش، شماره تراکنش و پیامی را پس از پایان عملیات تصدیق پرداخت، از Web API، در طی یک redirect کامل دریافت می‌کند. در ادامه به کمک متد RoomOrderDetailService.GetOrderDetailAsync که آن‌را پیشتر توسعه دادیم، اصل رکورد متناظر با این سفارش را بازیابی کرده و فیلدهای IsPaymentSuccessful و Status آن‌را بررسی می‌کنیم (این فیلدها در زمان تصدیق پرداخت، در همان سمت سرور مقدار دهی می‌شوند). همچنین جهت محکم‌کاری، قسمتی از این اطلاعات را با Local Storage نیز انطباق داده‌ایم. اگر پرداخت، موفقیت آمیز باشد، شماره سفارش و همچنین شماره تراکنش را به کاربر نمایش می‌دهیم و یا پیام دریافتی از سرور را در صفحه درج می‌کنیم.


جلوگیری از ثبت سفارش اتاقی که رزرو شده‌است


پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را می‌توان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاق‌ها انجام داد:
    public class HotelRoomDTO
    {
        public bool IsBooked { get; set; }

        // ...
    }
این خاصیت را در متدهای بازگشت لیست تمام اتاق‌ها و یا بازگشت اطلاعات یک اتاق، به صورت زیر محاسبه و مقدار دهی می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
       // ...

        public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr)
        {
            var hotelRooms = await _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .Include(x => x.RoomOrderDetails)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .ToListAsync();

            foreach (var hotelRoom in hotelRooms)
            {
                hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr);
            }

            return hotelRooms;
        }

        public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
            var hotelRoom = await _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .Include(x => x.RoomOrderDetails)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
            hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate);
            return hotelRoom;
        }

        private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate)
        {
            if (checkInDate == null || checkOutDate == null)
            {
                return false;
            }

            return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful &&
                        //check if checkin date that user wants does not fall in between any dates for room that is booked
                        ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate)
                        //check if checkout date that user wants does not fall in between any dates for room that is booked
                        || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date))
                    );
        }
    }
}
متد isRoomBooked، یک محاسبه‌ی سمت سرور محسوب نمی‌شود؛ چون با استفاده از Include‌های نوشته شده، اطلاعات کامل اتاق و وابستگی‌های آن (سرهای دیگر رابطه‌ی تشکیل شده) را داریم و این محاسبات سبب رفت و برگشتی به سمت سرور نمی‌شوند.

اکنون که خاصیت IsBooked مقدار دهی شده‌است، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاق‌ها
@if (room.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a>
}
ب) در کامپوننت نمایش جزئیات یک اتاق
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked)
{
    <button disabled class="btn btn-secondary btn-block">Sold Out</button>
}
else
{
    <button type="submit" class="btn btn-success form-control">Checkout Now</button>
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-30.zip
نظرات مطالب
چه زمان‌هایی یک برنامه‌ی ASP.NET ری استارت می‌شود؟
یک نکته‌ی تکمیلی: چگونه یک برنامه‌ی هاست شده‌ی در IIS را همواره زنده (در حال اجرا) نگه داریم؟

application pool برنامه‌های هاست شده‌ی در IIS، پس از مدتی بیکاری، خاموش می‌شوند. راه اندازی مجدد آن‌ها هم وابسته به رسیدن یک درخواست جدید به آن‌ها است. یعنی اگر برای مثال بخواهید یک کار پس زمینه‌ای را توسط یک برنامه‌ی ASP.NET مدیریت کنید، احتمال از کار افتادن آن به علت خاموش شدن و عدم راه اندازی خودکار application pool، بسیار زیاد است. برای رفع این مشکل و زنده نگه‌داشتن برنامه اگر از یک سیستم توسعه‌ی معمولی استفاده می‌کنید، ابتدا مطمئن شوید که Application Initialization Module که جزئی از Windows Features است، نصب شده‌است:

و اگر از ویندوز سرور استفاده می‌کنید، باید Application Initialization را در قسمت Application Development فعال کنید:

- Open the Add Roles and Features Wizard.
- In the Select role services panel, open the Application Development node.
- Select the checkbox for Application Initialization.

سپس به تنظیمات application pool برنامه مراجعه کرده و دو تغییر زیر را اعمال کنید:
Start Mode -> AlwaysRunning
Idle Time-Out (minutes) -> 0


همچنین حالت Preload برنامه را نیز باید فعال کرد و برنامه هم باید در حالت In-Process hosting model اجرا شود:
Connections panel -> Sites node -> Right-click the app ->
Manage Website > Advanced Settings -> Set Preload Enabled -> True

مطالب
Resource Governor در 2008 SQL Server
مقدمه
Resource  Governor، اجازه می‌دهد تا انواع مختلف Session را بر روی Server طبقه بندی کنید که به نوبه خود چگونگی کنترل تخصیص منابع سرور به فعالیت داده شده را به شما اعطا می‌کند. این قابلیت کمک می‌کند که ادامه فرآیند‌های OLTP تضمین شود و یک عملکرد قابل پیش بینی فراهم می‌کند تا توسط فرآیند‌های غیر قابل پیش بینی، تحت تاثیر منفی قرار نگیرد. با استفاده از Resource  Governor، قادر خواهید بود نحوه دستیابی به Session را به منظور محدود کردن منابع خاص برای SQL Server مشخص کنید. به عنوان مثال می‌توانید مشخص کنید که بیش از 20 درصد از پردازنده یا منابع حافظه به گزارش‌های در حال اجرا اختصاص داده نشود. هنگامیکه این ویژگی فعال باشد، مهم نیست چه تعداد گزارش در حال اجرا است، آنها هرگز نمی‌توانند از تخصیص منابع تعیین شده تجاوز کنند. البته این موضوع عملکرد گزارش گیری را کاهش می‌دهد ولی عملکرد فرآیند‌های OLTP حداقل توسط گزارش ها، دیگر تحت تاثیر منفی قرار نمی‌گیرد.

1- بررسی اجمالی Resource Governor:

Resource Governor، با کنترل تخصیص منابع بر حسب Workload کار می‌کند. هنگامی که یک درخواست اتصال به موتور بانک اطلاعاتی ارسال می‌شود درخواست براساس یک تابع رده بندی (Classification function) طبقه بندی می‌شود. تابع رده بندی یک تابع اسکالر است که از طریق T-SQL تعریف می‌شود. تابع رده بندی، اطلاعات را درباره یک اتصال (برای مثال  login ID، application name، hostname، server role ) ارزیابی می‌کند، به منظور تشخیص اینکه چگونه آنها را دسته بندی کند. پس از دسته بندی درخواست اتصال، آنها به گروه‌های حجم کاری (Workload Group) که برای رده بندی تعریف شده اند، شکسته می‌شوند. هر Workload Group مرتبط با یک مخزن منابع (Resource Pool) است.
یک Resource Pool، منابع فیزیکی SQL Server را نمایش می‌دهد (در حال حاضر در SQL Server 2008، تنها منابع فیزیکی موجود برای پیکربندی پردازنده و حافظه است) و مقدار حداکثر پردازنده و یا منابع حافظه را که به نوع خاصی از Workload اختصاص داده می‌شود، تعیین می‌کند. هنگامی که یک اتصال طبقه بندی شده و در Workload Group صحیح خود قرار می‌گیرد به این اتصال، پردازنده و منابع حافظه به اندازه نسبت داده شده به آن تخصیص داده می‌شود و سپس Query به Query Optimizer برای اجرا داده می‌شود. 

2-  اجزای Resource Governor:
Resource Governor، از سه قسمت اصلی تشکیل شده است: Classification، Workload Groups و Resource Pools. درک این سه قسمت و چگونگی تعامل آنها به درک و استفاده از Resource Governor کمک می‌کند.

2-1- Classification:
Classification، فرآیند ارزیابی اتصالات ورودی کاربر و اختصاص آن به یک Workload Group است که توسط منطق موجود در یک تابع تعریف شده توسط کاربر (user-defined function) انجام می‌شود. تابع نام یک Workload Group را برمی گرداند که Resource Governor از آن برای مسیر دهی Session به Workload Group مناسب استفاده می‌کند.
هنگامی که Resource Governor پیکربندی می‌شود فرآیند ورود به سیستم برای یک Session شامل گام‌های زیر است:
• Login authentication
• LOGON trigger execution
• Classification
2-2- Workload Groups:
Workload Groups، ظروفی برای اتصالات مشابه هستند که با توجه به معیارهای طبقه بندی برای هر اتصال گروه بندی می‌شوند. Workload Groups همچنین مکانیسمی برای تجمیع نظارت بر روی منابع مصرفی فراهم می‌کند.
Resource Governor دو Workload Group از پیش تعریف شده دارد: یک گروه داخلی (internal group) و یک گروه پیش فرض (default group).
 Internal Workload Group، تنها توسط فرآیندهای داخلی موتور بانک اطلاعاتی استفاده می‌شود. معیارهای طبقه بندی را برای گروه‌های داخلی نمی‌توانید تغییر دهید و همچنین هیچ یک از درخواست‌های کاربران را برای انتقال به گروه داخلی نمی‌توانید رده بندی کنید، با این حال بر گروه  داخلی می‌توانید نظارت کنید.
درخواست‌های اتصال به طور خودکار هنگامی که شرایط زیر وجود دارد، به  Default Workload Group رده بندی می‌شوند:
• معیاری برای طبقه بندی درخواست وجود ندارد.
• کوششی برای رده بندی درخواستی به گروهی که وجود ندارد.
• خرابی کلی Classification
Resource Governor، در مجموع 20 عدد Workload Group را پشتیبانی می‌کند. از آنجائی که دو عدد از آنها برای Workload Group‌های داخلی و پیش فرض ذخیره شده اند در مجموع 18 عدد Workload Group تعریف شده توسط کاربر (user-defined) می‌توان تعریف نمود.

2-3- Resource pools:
Resource Pool (مخزن منابع)، نشان دهنده تخصیص منابع فیزیکی به SQL Server است. یک Resource Pool از دو بخش تشکیل شده است:
• در بخش نخستین حداقل رزرو منابع را مشخص می‌کنیم، این بخش از مخزن منابع با مخازن دیگر همپوشانی نمی‌کند.
• در بخش دیگر حداکثر ممکن رزرو منابع را برای مخزن مشخص می‌کنیم، تخصیص منابع با مخازن دیگر مشترک است.
در 2008 SQL Server مخزن منابع با تعیین حداقل و حداکثر تخصیص CPU و حداقل و حداکثر تخصیص حافظه تنظیم می‌گردد. با تنظیم حداقل، در دسترس بودن منبع از مخزن تضمین می‌شود. از آنجائی که در هر رزرو حداقل منابع تداخلی نمی‌تواند وجود داشته باشد، مجموع مقادیر حداقل در تمام مخازن از 100% کل منابع Server نمی‌تواند تجاوز کند.
مقدار حداکثر در محدوده بین حداقل و شامل 100% مقدار می‌تواند تنظیم گردد. تنظیم حداکثر نشان دهنده مقدار حداکثری است که یک Session می‌تواند مصرف کند، مادامی که منابع در دسترس باشند و توسط مخزن دیگر که با حداقل مقدار غیر صفر پیکربندی شده، استفاده نشود. هنگامی که یک مخزن با حداقل مقدار غیر صفر تعریف شده، مقدار حداکثر موثر از مخزن‌های دیگر دوباره تنظیم می‌شوند، در صورت لزوم حداکثر مقدار موجود از جمع کل حداقل منابع مخازن دیگر کسر می‌گردد.
برای مثال، دو مخزن تعریف شده توسط کاربر (user-defined) را در نظر بگیرید. مخزن اول Pool1 با مقدار حداقل 20% و مقدار حداکثر 100% تعریف شده، مخزن دیگری Pool2 با مقدار حداقل 50% و مقدار حداکثر 70% تعریف شده است. حداکثر مقدار موثر برایPool1 برابر 50% است (100% منهای مقدار حداقل 50% مخزن Pool2) و حداکثر مقدار موثر برای Pool2، 70% است زیرا حداکثر مقداری است که پیکربندی شده است، گر چه 80% باقی می‌ماند.
بخش مشترکی از مخزن (مقدارش بین مقدار حداقل و مقدار حداکثر موثر است) که برای تعیین مقدار منابع مورد استفاده است، توسط مخزن می‌تواند مصرف شود اگر منابعی موجود باشد و توسط مخازن دیگر مصرف نشده باشد. هنگامی که منابعی توسط یک مخزن مصرف می‌شوند، آنها به یک مخزن مشخص نسبت داده می‌شوند، به بیان دیگر اشتراکی نیستند تا زمانی که فرآیند در آن مخزن به اتمام برسد.
برای توضیح بیشتر یک سناریو که در آن سه مخزن تعریف شده توسط کاربر (user-defined) وجود دارد، را در نظر بگیرید:
PoolA با حداقل مقدار 10% و حداکثر مقدار 100% تعریف می‌شود.
PoolB با حداقل مقدار 35% و حداکثر مقدار 90% تعریف می‌شود.
PoolC با حداقل مقدار 30% و حداکثر مقدار 80% تعریف می‌شود.
مقدار موثر PoolA و مجموع در صد منابع به اشتراک گذاشته PoolA به شرح زیر محاسبه خواهد شد:
( حداکثر مقدار PoolA ) -  ( حداقل مقدار PoolB ) -  ( حداقل مقدار PoolC ) =  ( حداکثر مقدار موثر PoolA )
(حداکثر مقدار موثر PoolA ) –  ( حداقل مقدار PoolA ) = ( اشتراک PoolA )
جدول زیر مقدار حداکثر موثر و اشتراکی را برای هر مخزن در این پیکربندی نمایش می‌دهد:



  Internal Pool، منابع مصرف شده توسط فرآیندهای داخلی موتور بانک اطلاعاتی را نشان می‌دهد. این مخزن تنها شامل گروه‌های داخلی است و به هیچ وجه قابل تغییر نیست. مخزن داخلی مقدار ثابت حداقل صفر و حداکثر 100% را دارد و مصرف منابع توسط مخزن داخلی، از طریق تنظیمات در هر مخزن دیگر محدود یا کاسته نمی‌شود.
به عبارت دیگر حداکثر مقدار موثر مخزن داخلی همیشه 100% است. هر workloads در مخزن داخلی برای عملکرد Server حیاتی در نظر گرفته می‌شود و Resource Governor در صورت لزوم اجازه می‌دهد تا مخازن داخلی 100% منابع موجود را مصرف کند حتی اگر به معنی نقض نیازمندیهای منابع از سایر مخازن باشد.
Default Pool، اولین مخزن تعریف شده کاربر است. قبل از هرگونه پیکربندی، Default Pool تنها حاوی Default group است. Default Pool نمی‌تواند ایجاد یا حذف شود اما می‌تواند تغییر کند. Default Pool علاوه بر Default group می‌تواند شامل گروه‌های تعریف شده توسط کاربر (user-defined) نیز باشد.

3- پیکر بندی Resource Governor :
پیکربندی Resource Governor شامل مراحل زیر است:
- فعال کردن Resource Governor
- ایجاد مخازن منابع (Resource Pools) تعریف شده توسط کاربر (user-defined)
- تعریف Workload Groups و نسبت دادن آن به مخازن
- ایجاد Classification function
- ثبت Classification function به Resource Governor
3-1-  فعال کردن Resource Governor
پیش از اینکه بتوانید یک Resource Pool را ایجاد کنید، نیاز است تا نخست Resource Governor را فعال کنید.

3-2-  تعریف Resource Pool

ویژگی‌های موجود برای یک Resource Pool عبارتند از:
Name، Minimum CPU %، Maximum CPU%، Min Memory%، Max Memory%

3-3-  تعریف Workload Group 

پس از اینکه Resource Pool را تعریف کردید، گام بعدی ایجاد یک Workload Group و اختصاص آن به Resource Pool مناسب است. چندین workgroup را می‌توان به مخزن (Pool) یکسان نسبت داد اما یک workgroup را به چندین Resource Pool نمی‌توان نسبت داد. خواص انتخابی موجود برای Workload Groups به شما اجازه می‌دهد سطح بهتری از کنترل را روی اجرای دستورات یک Workload Group تنظیم کنید. انتخاب‌های موجود عبارتند از:
3-3-1- Importance :
اهمیت نسبی (کم، متوسط یا بالا) Workload Group درون Resource Pool را تعیین می‌کند. اگر چندین Workload Group را در یک Resource Pool تعریف کنید این تنظیمات تعیین می‌کند که درخواست‌ها در عرض یک Workload Group در اولویت بالاتر یا پائین‌تری از Workload Group‌های دیگر درون همان Resource Pool اجرا شوند، مقدار متوسط تنظیم پیش فرض است. در حال حاضر فاکتورهای وزنی برای هر تنظیم کم برابر 1، متوسط برابر3 و زیاد برابر 9 است. به این معنی که زمانبند به اجرای Session‌های درون workgroup هائی با اهمیت بالا، سه برابر بیشتر از workgroup‌های با اهمیت متوسط و نه برابر بیشتر از workgroup‌های کم اهمیت، مبادرت خواهد کرد.
3-3-2- Maximum Request  :
حداکثر تعداد درخواست‌های همزمان که اجازه دارند در یک Workload Group اجرا شوند را مشخص می‌کند. تنظیم پیش فرض، صفر، تعداد نامحدود دستور را اجازه می‌دهد.
3-3-3-  CPU Time :
حداکثر مقدار زمان پردازنده در ثانیه را مشخص می‌کند که یک درخواست درون Workload Group می‌تواند استفاده کند. تنظیم پیش فرض، صفر، به معنی نامحدود است.
3-3-4- Memory Grant %:
به صورت در صد، حداکثر مقدار اعطا حافظه برای اجرا (Execution grant memory)، که یک تک دستور از Resource Pool می‌تواند اخذ کند را مشخص می‌کند. این درصد نسبی است از مقدار حافظه ای که به Resource Pool نسبت داده می‌شود. محدوده مجاز مقادیر از 0 تا 100 است. تنظیم پیش فرض 25 است.
 Execution grant memory، مقدار حافظه ای است که برای اجرای query استفاده می‌شود (نه برای Buffer کردن یا cache کردن) که می‌تواند صرفه نظر از Resource Pool یا Workload Group توسط تعدادی از Session‌ها به اشتراک گذاشته شود. توجه شود که تنظیم این مقدار به صفر از اجرای عملیات Hash Join و دستورات مرتب سازی در Workload Group‌های تعریف شده توسط کاربر (user-defined)جلوگیری می‌کند. همچنین این مقدار توصیه نمی‌شود بیشتر از 70 باشد زیرا ممکن است Server قادر نباشد، اگر Query‌های همزمان در حال اجرا باشند، حافظه آزاد کافی اختصاص دهد.
3-3-5- Grant Time-out :
حداکثر زمان، به ثانیه، که یک query برای یک منبع منتظر می‌ماند تا در دسترس شود را مشخص می‌کند. اگر منبع در دسترس نباشد، فرآیند ممکن است با یک خطای time-out مواجه شود. تنظیم پیش فرض، صفر، به معنی این است که سرور time-out را با استفاده از محاسبات داخلی بر مبنای هزینه پرس و جو ( query cost ) با تعیین حداکثر زمان برآورد می‌کند.
3-3-6- Degree of Parallelism  :
حداکثر درجه موازی سازی (DOP) را برای پرس و جو‌های موازی تعیین می‌کند. محدوده مجاز مقادیر از 0 تا 64 است. تنظیم پیش فرض، صفر، به معنی این است که فرآیند‌ها از تنظیمات عمومی استفاده می‌کنند.

3-4- ایجاد یک Classification function
پس از تعریف Resource Pool و Workload Group، به یک Classification function نیاز است که شامل منطق ارزیابی اتصالات و نسبت دادن آنها به Workload Group مناسب است. Classification function برای هر اتصال Session جدید به SQL Server بکار می‌رود. هر Session در Workload Group نسبت داده شده به آن باقی می‌ماند تا زمانی که به پایان برسد، مگر اینکه صراحتاً به یک گروه متفاوت دوباره نسبت داده شود. فقط یک Classification function فعال در هر زمان می‌تواند وجود داشته باشد. در صورت عدم تعریف شدن یا عدم فعال بودن Classification function همه اتصالات به Workload Group Default نسبت داده می‌شوند. Classification function یک نام workgroup که نوع آن SYSNAME است (که یک نام مستعار برای دیتا تایپ nvarchar 128 است.) برمی گرداند. اگر تابع تعریف شده مقدار 'NULL ،'Default یا نام گروهی که وجود ندارد را برگرداند، Session به Workload Group Default نسبت داده می‌شود. همچنین اگر به هر دلیلی تابع با موفقیت خاتمه نیابد Session به Workload Group Default نسبت داده می‌شود.
منطق Classification function معمولاً مبتنی بر ویژگی‌های اتصال است و اغلب از طریق مقدار بازگشتی توابع سیستمی از قبیل:
 ()SUSER_NAME() ،SUSER_SNAME() ،IS_MEMBER() ،IS_SERVERROLEMEMBER() ،HOST_NAME و یا ()APP_NAME، نام Workload Group اتصال مشخص می‌شود. علاوه بر این توابع می‌توانید از ویژگی‌های توابع دیگر برای ساخت منطق رده بندی  استفاده کنید. تابع ()LOGINPROPERTY شامل دو  ویژگی (DefaultDatabase و DefaultLanguage) می‌باشد  که می‌تواند برای Classification function استفاده شود. بعلاوه تابع ()CONNECTIONPROPERTY پروتکل‌ها و دسترسی به نقل و انتقالات در شبکه، همچنین جزئیات طرح احراز هویت،  Local IP address و  TCP Port و Client’s IP Address را برای استفاده اتصالات فراهم می‌کند. برای مثال می‌توانید برای یک اتصال، یک Workload Group نسبت دهید، مبتنی بر اینکه subnet یک اتصال ازکجا می‌آید.
نکته: اگر قصد دارید از هر یک از توابع ()HOST_NAME و یا ()APP_NAME در تابع رده بندی تان استفاده کنید، توجه داشته باشید این امکان وجود دارد مقادیر بازگردانده شده توسط این توابع توسط کاربران تغییر داده شوند، گر چه به طور کلی گرایش به استفاده از تابع ()APP_NAME برای رده بندی اتصالات بیشتر است.
 
4- بررسی نمونه ای از پیکربندی Resource Governor
برای سادگی، در این قسمت مثالی ارائه می‌شود که از تابع ()SUSER_NAME استفاده می‌کند: در گام نخست، دو Resource Pool ایجاد می‌شود (  ReportPool و OLTPPool ) 

  

در گام بعدی، دو Workload Group ایجاد می‌شود ( ReportWG1 و OLTPWG1 ) 

سپس دو Login ایجاد می‌شود ( report_user و oltp_user ) که در تابع رده بندی استفاده خواهند شد برای مشخص کردن این که اتصالات Seesion به کدام  Workload Group نسبت داده شوند. پس از اضافه کردن Login‌ها به عنوان User‌ها به Database مورد نظر مان، در بانک اطلاعاتی Master تابع رده بندی (Classification function ) را ایجاد می‌کنیم:  

می توان تابع ()WorkgroupClassifier را در محیط SSMS با اجرای دستور زیر برای Login‌های متفاوت تست نمود: 

در  ادامه دستور زیر برای پیکربندی تابع رده بندی به Resource Governor استفاده می‌شود: 


5- اصلاح پیکربندی Resource Governor:
می‌توانید درمحیط SSMS تنظیمات Resource Pool و Workload Group را تغییر دهید ( برای مثال حداکثر استفاده CPU برای یک Resource Pool و یا درجه اهمیت یک Workload Group). متناوباً می‌توان از دستورات T-SQL استفاده نمود.
نکته: پس از اجرای دستورات ALTER RESOURCE POOL یا ALTER WORKLOAD GROUP، برای اعمال کردن تغییرات اجرای دستور ALTER RESOURCE GOVERNOR RECONFIGURE نیاز می‌باشد.
5-1-  حذف Workload Group :
یک Workload Group را اگر هر نوع Session فعال نسبت داده شده به آن وجود داشته باشد، نمی‌توان حذف نمود. اگر یک Workload Group شامل Sessionهای فعال باشد، حذف Workload Groupو یا جابجائی آن به یک Resource Pool متفاوت، هنگامی که دستور ALTER RESOURCE GOVERNOR RECONFIGURE برای اعمال نمودن تغییرات فراخوانی می‌شود، با خطا مواجه خواهد شد.
5-2-  حذف Resource Pools:
یک Resource Pool را اگر هر نوع Workload Group نسبت داده شده به آن وجود داشته باشد، نمی‌توان حذف نمود. نخست نیاز دارید Workload Group حذف شود و یا به Resource Pool دیگری جابجا گردد.
5-3-  اصلاح Classification function:
اگر نیاز دارید تغییراتی در تابع رده بندی ایجاد نمائید، مهم است توجه داشته باشید که تابع رده بندی تا زمانی که مشخص شده (marked) برای Resource Governor است، نمی‌توان آنرا حذف و یا تغییر داد. پیش از اینکه بتوان تابع رده بندی را اصلاح و یا حذف نمود نخست نیاز دارید Resource Governor را غیر فعال نمائید. متناوباً می‌توان تابع رده بندی را جایگزین کرد با اجرای دستور ALTER RESOURCE GOVERNOR و فرستادن (passing) یک اسم متفاوت برای CLASSIFIER_FUNCTION،همچنین می‌توان با اجرای دستور زیر تابع رده بندی جاری را غیر فعال نمود:

تابع رده بندی می‌توان تعریف کرد که نام Workload Group را از جداول یک بانک اطلاعاتی جستجو کند به جای اینکه نام Workload Group به صورت hard-coding و مطابق با ضوابط درون تابع باشد. عملکرد،  در موقع دسترسی به جدول برای جستجو کردن نام Workload Group، نباید تا حد زیادی تحت تاثیر قرار گیرد. 

6- نظارت بر Resource Governor
با استفاده از Performance Monitor، events و (Dynamic Management View (DMV  می‌توان Workload Group و Resource Pool را نظارت (Monitor) کرد. دو شی Performance برای این کار موجود است: SQL Server:Workload Group Stats و SQL Server:Resource Pool Stats
شکل زیر مربوط به پیکر بندی مثال مورد نظرمان می‌باشد:
 

7- نتیجه گیری
Resource Governor چندین مزیت بالقوه ارائه می‌دهد، در درجه اول قابلیت اولویت بندی منابع Server برای کاربران و برنامه‌های کاربردی (applications) بحرانی، جلوگیری از “runaway” یا درخواست‌های غیر منتظره ای که به شدت و بطور قابل توجهی روی کارائی Server تاثیر منفی می‌گذارند.
ضمناً Resource Governor چندین مشکل بالقوه نیز عرضه می‌کند، برای مثال پیکربندی اشتباه Resource Governor تنها به عملکرد کلی Server آسیب نمی‌رساند بلکه به طور بالقوه روی سرور قفل (Lock) می‌تواند ایجاد کند و نیاز به استفاده از اتصال اختصاصی Administrator برای متصل شدن به SQL Server به منظور اشکال یابی و رفع مشکل  می‌باشد. بنابراین توصیه شده است که تنها در صورتی که DBA با تجربه ای هستید و درک خوب و آشنائی خوبی با Workload هائی که روی بانک اطلاعاتی اجرا می‌شوند دارید، Resource Governor را بکار برید. حتی در این صورت، ضروری است که پیکربندی تان را روی یک Server تستی پیش از اینکه روی محیط تولیدی بگسترانید، تست نمائید.
Resource Governor به عنوان یک ویژگی با نام تجاری جدید در SQL Server 2008، با تعدادی محدودیت همراه است که احتمالاً در نسخه‌های بعدی SQL Server حذف خواهد شد، از محدودیت های  بارز :
- محدودیت منابع (Resource)، که به CPU و حافظه محدود می‌شوند. I/O Disk و منابع شبکه را در  SQL Server 2008 نمی‌توان محدود کرد.
- استفاده از منابع برای Reporting Service، Analysis Service و Integeration Service را نمی‌توان محدود کرد . در این نسخه محدودیت‌های منابع تنها روی هسته موتور بانک اطلاعاتی بکار برده می‌شود.
- محدودیت‌های Resource Governor روی یک SQL Instance تعریف و بکار برده می‌شود.

اشتراک‌ها
بررسی server-side WebAssembly در دات نت 8

Experiments with the new WASI workload in .NET 8 Preview 4
.NET 8 adds a new "wasi-experimental" workload, replacing the earlier Wasi.Sdk. This is a step towards giving .NET native, built-in support for server-side WebAssembly scenarios.
This video shows how to get started, then demonstrates how .NET+WASI creates possibilities for new kinds of apps and new ways to use .NET. 

بررسی server-side WebAssembly در دات نت 8
اشتراک‌ها
امکان استفاده از کامپوننت‌های Blazor در برنامه‌های SPA مانند React و Angular در دات نت 6

Once created, these custom elements -- a custom counter, for example -- can also be used in other single-page application (SPA) web frameworks such as React and Angular. A sample project, aptly titled Blazor Custom Elements, shows how to do just that, providing examples about how to work with those frameworks and the client-side Blazor WebAssembly component as well as Blazor Server.  

امکان استفاده از کامپوننت‌های Blazor در برنامه‌های SPA مانند React و Angular در دات نت 6
مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش اول
سیستم مدیریت محتوای IRIS از سیستم‌های اعتبار سنجی و مدیریت کاربران رایج نظیر ASP.NET Membership و یا ASP.NET Simple Membership استفاده نمی‌کند و از یک سیستم احراز هویت سفارشی شده مبتنی بر FormsAuthentication بهره می‌برد. زمانیکه در حال نوشتن پروژه‌ی IRIS بودم هنوز ASP.NET Identity معرفی نشده بود و به دلیل مشکلاتی که سیستم‌های قدیمی ذکر شده داشت، یک سیستم اعتبار سنجی کاربران سفارشی شده را در پروژه پیاده سازی کردم.
برای اینکه با معایب سیستم‌های مدیریت کاربران پیشین و مزایای ASP.NET Identity آشنا شوید، مقاله زیر می‌تواند شروع خیلی خوبی باشد:


به این نکته نیز اشاره کنم که  هنوز هم می‌توان از FormsAuthentication مبتنی بر OWIN استفاده کرد؛ ولی چیزی که برای من بیشتر اهمیت دارد خود سیستم Identity هست، نه نحوه‌ی ورود و خروج به سایت و تولید کوکی اعتبارسنجی.
باید اعتراف کرد که سیستم جدید مدیریت کاربران مایکروسافت خیلی خوب طراحی شده است و اشکالات سیستم‌های پیشین خود را ندارد. به راحتی می‌توان آن را توسعه داد و یا قسمتی از آن را تغییر داد و به جرات می‌توان گفت که پایه و اساس هر سیستم اعتبار سنجی و مدیریت کاربری را که در نظر داشته باشید، به خوبی پیاده سازی کرده است.
در ادامه قصد دارم، چگونگی مهاجرت از سیستم فعلی به سیستم Identity را بدون از دست دادن اطلاعات فعلی شرح دهم.

رفع باگ ثبت کاربرهای تکراری نسخه‌ی کنونی

قبل از این که سراغ پیاده سازی Identity برویم، ابتدا باید یک باگ مهم را در نسخه‌ی قبلی، برطرف نماییم. نسخه‌ی کنونی مدیریت کاربران اجازه‌ی ثبت کاربر با ایمیل و یا نام کاربری تکراری را نمی‌دهد. جلوگیری از ثبت نام کاربر جدید با ایمیل یا نام کاربری تکراری از طریق کدهای زیر صورت گرفته است؛ اما در عمل، همیشه هم درست کار نمی‌کند.
        public AddUserStatus Add(User user)
        {
            if (ExistsByEmail(user.Email))
                return AddUserStatus.EmailExist;
            if (ExistsByUserName(user.UserName))
                return AddUserStatus.UserNameExist;

            _users.Add(user);
            return AddUserStatus.AddingUserSuccessfully;
        }
شاید الان با خود بگویید که چرا برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف نشده است؟ دلیلش این بوده که در زمان نگارش پروژه، Entity Framework پشتیبانی پیش فرضی از تعریف ایندکس نداشت و نوشتن همین شرط‌ها کافی به نظر می‌رسید. باز هم ممکن است بگویید که مسائل همزمانی چگونه مدیریت شده است و اگر دو کاربر مختلف در یک لحظه، نام کاربری یکسانی را انتخاب کنند، سیستم چگونه از ثبت دو کاربر مختلف با نام کاربری یکسان ممانعت می‌کند؟ 
جواب این است که ممانعتی نمی‌کند و دو کاربر با نام‌های کاربری یکسان ثبت می‌شوند؛ اما من برای وبسایت خودم که تعداد کاربرانش محدود بود این سناریو را محتمل نمی‌دانستم و کد خاصی برای جلوگیری از این اتفاق پیاده سازی نکرده بودم.
با این حال، در حال حاضر نزدیک به 20 کاربر تکراری در دیتابیسی که این سیستم استفاده می‌کند ثبت شده است. اما واقعا آیا دو کاربر مختلف اطلاعات یکسانی وارد کرده‌اند؟
 دلیل رخ دادن این اتفاق این است که کاربری که در حال ثبت نام در سایت است، وقتی که بر روی دکمه‌ی ثبت نام کلیک می‌کند و اطلاعات به سرور ارسال می‌شوند، در سمت سرور بعد از رد شدن از شرطهای تکراری نبودن UserName و Email، قبل از رسیدن به متد SaveChanges برای ذخیره شدن اطلاعات کاربر جدید در دیتابیس، وقفه ای در ترد این درخواست به وجود می‌آید. کاربر که احساس می‌کند اتفاقی رخ نداده است، دوباره بر روی دکمه‌ی ثبت نام کلیک می‌کند و  همان اطلاعات قبلی به سرور ارسال می‌شود و این درخواست نیز دوباره شرط‌های تکراری نبودن اطلاعات را با موفقیت رد می‌کند( چون هنوز SaveChanges درخواست اول فراخوانی نشده است) و این بار SaveChanges درخواست دوم با موفقیت فراخوانی می‌شود و کاربر ثبت می‌شود. در نهایت هم ترد درخواست اول به ادامه‌ی کار خود می‌پردازد و SaveChanges درخواست اول نیز فراخوانی می‌شود و خیلی راحت دو کاربر با اطلاعات یکسان ثبت می‌شود. این سناریو را در ویژوال استادیو با قرار دادن یک break point قبل از فراخوانی متد SaveChanges می‌توانید شبیه سازی کنید.
احتمالا این سناریو با مباحث همزمانی در سیستم عامل و context switch‌های بین ترد‌ها مرتبط است و این context switch‌ها  بین درخواست‌ها و atomic نبودن روند چک کردن اطلاعات و ثبت آن ها، سبب بروز چنین مشکلی میشود.
برای رفع این مشکل می‌توان از غیر فعال کردن یک دکمه در حین انجام پردازش‌های سمت سرور استفاده کرد تا کاربر بی حوصله، نتواند چندین بار بر روی یک دکمه کلیک کند و یا راه حل اصولی‌تر این است که ایندکس منحصر به فرد برای فیلدهای مورد نظر تعریف کنیم.
به طور پیش فرض در ASP.NET Identity برای فیلدهای UserName و Email ایندکس منحصر به فرد تعریف شده است. اما مشکل این است که به دلیل وجود کاربرانی با Email و UserName تکراری در دیتابیس کنونی، امکان تعریف Index منحصر به فرد وجود ندارد و پیش از انجام هر کاری باید این ناهنجاری را در دیتابیس برطرف نماییم.
   
به شخصه معمولا برای انجام کارهایی از این دست، یک کنترلر در برنامه خود تعریف می‌کنم و در آنجا کارهای لازم را انجام می‌دهم.
در اینجا من برای حذف کاربران با اطلاعات تکراری، یک کنترلر به نام Migration و اکشن متدی به نام RemoveDuplicateUsers تدارک دیدم.
using System.Linq;
using System.Web.Mvc;
using Iris.Datalayer.Context;

namespace Iris.Web.Controllers
{
    public class MigrationController : Controller
    {
        public ActionResult RemoveDuplicateUsers()
        {
            var db = new IrisDbContext();

            var lstDuplicateUserGroup = db.Users
                                               .GroupBy(u => u.UserName)
                                               .Where(g => g.Count() > 1)
                                               .ToList();

            foreach (var duplicateUserGroup in lstDuplicateUserGroup)
            {
                foreach (var user in duplicateUserGroup.Skip(1).Where(user => user.UserMetaData != null))
                {
                    db.UserMetaDatas.Remove(user.UserMetaData);
                }
                
                db.Users.RemoveRange(duplicateUserGroup.Skip(1));
            }

            db.SaveChanges();

            return new EmptyResult();
        }
    }
}
در اینجا کاربران بر اساس نام کاربری گروه بندی می‌شوند و گروه‌هایی که بیش از یک عضو داشته باشند، یعنی کاربران آن گروه دارای نام کاربری یکسان هستند و به غیر از کاربر اول گروه، بقیه باید حذف شوند. البته این را متذکر شوم که منطق وبسایت من به این شکل بوده است و اگر منطق کدهای شما فرق می‌کند، مطابق با منطق خودتان این کدها را تغییر دهید.

تذکر: اینجا شاید بگویید که چرا cascade delete برای UserMetaData فعال نیست و بخواهید که آن را اصلاح کنید. پیشنهاد من این است که اکنون از هدف اصلی منحرف نشوید و تمام تمرکز خود را بر روی انتقال به ASP Identity با حداقل تغییرات بگذارید. این گونه نباشد که در اواسط کار با خود بگویید که بد نیست حالا فلان کتابخانه را آپدیت کنم یا این تغییر را بدهم بهتر می‌شود. سعی کنید با حداقل تغییرات رو به جلو حرکت کنید؛ آپدیت کردن کتابخانه‌ها را هم بعدا می‌شود انجام داد.
   

 مقایسه ساختار جداول دیتابیس کاربران IRIS با ASP.NET Identity

ساختار جداول ASP.NET Identity به شکل زیر است:

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

همان طور که مشخص است در هر دو سیستم، بین ساختار جداول و رابطه‌ی بین آن‌ها شباهت‌ها و تفاوت هایی وجود دارد. سیستم Identity دو جدول بیشتر از IRIS دارد و برای جداولی که در سیستم کنونی وجود ندارند نیاز به انجام کاری نیست و به هنگام پیاده سازی Identity، این جداول به صورت خودکار به دیتابیس اضافه خواهند شد.

دو جدول مشترک در این دو سیستم، جداول Users و Roles هستندکه نحوه‌ی ارتباطشان با یکدیگر متفاوت است. در Iris بین User و Role رابطه‌ی یک به چند وجود دارد ولی در Identity، رابطه‌ی بین این دو جدول چند به چند است و جدول واسط بین آن‌ها نیز UserRoles نام دارد.

از آن جایی که من قصد دارم در سیستم جدید هم رابطه‌ی بین کاربر و نقش چند به چند باشد، به پیش فرض‌های Identity کاری ندارم. به رابطه‌ی کنونی یک به چند کاربر و نقشش نیز دست نمی‌گذارم تا در انتها با یک کوئری از دیتابیس، اطلاعات نقش‌های کاربران را به جدول جدیدش منتقل کنم.

جدولی که در هر دو سیستم مشترک است و هسته‌ی آن‌ها را تشکیل می‌دهد، جدول Users است. اگر دقت کنید می‌بینید که این جدول در هر دو سیستم، دارای یک سری فیلد مشترک است که دقیقا هم نام هستند مثل Id، UserName و Email؛ پس این فیلد‌ها از نظر کاربرد در هر دو سیستم یکسان هستند و مشکلی ایجاد نمی‌کنند.

یک سری فیلد هم در جدول User در سیستم IRIS هست که در Identity نیست و بلعکس. با این فیلد‌ها نیز کاری نداریم چون در هر دو سیستم کار مخصوص به خود را انجام می‌دهند و تداخلی در کار یکدیگر ایجاد نمی‌کنند.

اما فیلدی که برای ذخیره سازی پسورد در هر دو سیستم استفاده می‌شود دارای نام‌های متفاوتی است. در Iris این فیلد Password نام دارد و در Identity نامش PasswordHash است.

برای اینکه در سیستم کنونی، نام  فیلد Password جدول User را به PasswordHash تغییر دهیم قدم‌های زیر را بر می‌داریم:

وارد پروژه‌ی DomainClasses شده و کلاس User را باز کنید. سپس نام خاصیت Password را به PasswordHash تغییر دهید.  پس از این تغییر بلافاصله یک گزینه زیر آن نمایان می‌شود که می‌خواهد در تمام جاهایی که از این نام استفاده شده است را به نام جدید تغییر دهد؛ آن را انتخاب کرده تا همه جا Password به PasswordHash تغییر کند.

برای این که این تغییر نام بر روی دیتابیس نیز اعمال شود باید از Migration استفاده کرد. در اینجا من از مهاجرت دستی که بر اساس کد هست استفاده می‌کنم تا هم بتوانم کدهای مهاجرت را پیش از اعمال بررسی و هم تاریخچه‌ای از تغییرات را ثبت کنم.

برای این کار، Package Manager Console را باز کرده و از نوار بالایی آن، پروژه پیش فرض را بر روی DataLayer قرار دهید. سپس در کنسول، دستور زیر را وارد کنید:

Add-Migration Rename_PasswordToPasswordHash_User

اگر وارد پوشه Migrations پروژه DataLayer خود شوید، باید کلاسی با نامی شبیه به 201510090808056_Rename_PasswordToPasswordHash_User ببینید. اگر آن را باز کنید کدهای زیر را خواهید دید: 

  public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            AddColumn("dbo.Users", "PasswordHash", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "Password");
        }
        
        public override void Down()
        {
            AddColumn("dbo.Users", "Password", c => c.String(nullable: false, maxLength: 200));
            DropColumn("dbo.Users", "PasswordHash");
        }
    }

بدیهی هست که این کدها عمل حذف ستون Password را انجام میدهند که سبب از دست رفتن اطلاعات می‌شود. کد‌های فوق را به شکل زیر ویرایش کنید تا تنها سبب تغییر نام ستون Password به PasswordHash شود.

    public partial class Rename_PasswordToPasswordHash_User : DbMigration
    {
        public override void Up()
        {
            RenameColumn("dbo.Users", "Password", "PasswordHash");
        }
        
        public override void Down()
        {
            RenameColumn("dbo.Users", "PasswordHash", "Password");
        }
    }

سپس باز در کنسول دستور Update-Database  را وارد کنید تا تغییرات بر روی دیتابیس اعمال شود.

دلیل اینکه این قسمت را مفصل بیان کردم این بود که می‌خواستم در مهاجرت از سیستم اعتبارسنجی خودتان به ASP.NET Identity دید بهتری داشته باشید.

تا به این جای کار فقط پایگاه داده سیستم کنونی را برای مهاجرت آماده کردیم و هنوز ASP.NET Identity را وارد پروژه نکردیم. در بخش‌های بعدی Identity را نصب کرده و تغییرات لازم را هم انجام می‌دهیم.