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

WebStorage در برابر کوکی ها
یکی از روش‌های سنتی ذخیره اطلاعات در سیستم کاربر، کوکی‌ها در بستر Http هستند. تفاوت‌های زیادی بین این دو وجود دارد که تعدادی از آن‌ها را در زیر بررسی می‌کنیم:

  • مکانیزم ذخیره سازی:
کوکی‌ها داده‌های ساخت یافته‌ای هستند که از وب سرور به سمت مرورگر کاربر به عنوان پاسخی در ازای درخواستی ارسال می‌شوند. درخواست و پاسخ کوکی‌ها شامل بخش هدر بوده که اطلاعات آن باعث شناسایی کوکی برای مدیریت و شناسایی آن‌ها می‌گردد تا هر موقع درخواستی صورت بگیرد، به سمت سرور برگشت خواهد خورد.
به طور خلاصه اینکه کوکی‌ها توسط درخواست‌ها و پاسخ‌ها ایجاد یا به روز می‌شوند. در نتیجه داده‌ها چه تغییر کرده باشند چه تغییر نکرده باشند، همیشه بخشی از هدر Http هستند. در سوی دیگر webstorage به طور کامل به صورت کلاینتی پیاده سازی گشته است و وب سرور را درگیر کار خودشان نمی‌کنند و کارایی بهتری را ارائه می‌دهند. از آنجا که همه چیز در خود سیستم کاربر اتفاق می‌افتد، در صورت از دست دادن کانکشن شبکه، کاربر می‌تواند همچنان به فعالیت‌های به روزرسانی و تغییر ادامه دهد.

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

  • محدودیت حجمی
محدودیت کوکی‌ها و webstorage از نظر حجم ذخیره سازی بین مروگرهای مختلف، متفاوت است. ولی در حالت کلی در بیشتر مرورگرها محدودیت حجم 4 کیلوبایت برای کوکی‌ها موجود است. (این ابزار می تواند نهایت حجمی را که که مرورگر شما از کوکی پشتیبانی می‌کند، نشان دهد.)
در مورد webstorage استاندارد W3C محدودیتی اعلام نکرده است و تصمیم گیری بر سر این موضوع را به سازندگان مرورگرها واگذار کرده است. ولی در حالت کلی حجم بیشتری از کوکی را ذخیره میکند و عموما تا 5 مگابایت توانایی ذخیره سازی وجود دارد. بدین ترتیب حجم آن 124,527% بیشتر از کوکی‌ها است. (این ابزار می‌تواند نهایت حجمی را که مرورگر شما از webstorage پشتیبانی می‌کند، ببینید).

انواع Webstorage 
دو نوع متد ذخیره سازی در webstorage داریم:
  • session storage
  • local storage

  • SessionStorage
داده‌هایی که بدین صورت ذخیره می‌شوند تنها تا زمانی دوام آورده و پایدار هستند که session مرورگر فعال است؛ یعنی تا زمانیکه کاربر در سایت فعلی حضور دارد.
استفاده از این روش برای ذخیره سازی‌های موقت عالی است. برای نمونه مقادیر ورودی فرمی که کاربر در حال کار با آن است، میتواند به طور موقت ذخیره شوند تا زمانی که کاربر بتواند تمامی مراحل را طی کرده، بدون اینکه ارجاعی به دیتابیس سمت سرور داشته باشد. همچنین ذخیره موقت داده‌ها می‌تواند به کاربر کمک کند تا در صورت reresh‌های ناگهانی یا بسته شدن‌های ناگهانی مرورگرها، نیازی به ورود مجدد داده‌ها نداشته باشد.
  • LocalStorage
در این روش داده‌ها با از دست رفتن session مرورگر جاری از بین نمی‌رود و برای بازدیدهای آتی کاربر از سایت، داده‌ها همچنان در دسترس هستند.
پشتیبانی مرورگرها
وب سایت caniuse گزارش می‌دهد که اکثر مرورگرها پشتیبانی خوبی از webstorage دارند.

با اینکه توصیه نامه W3C از پایان کار پیاده سازی این قابلیت خبر میدهد ولی در حال حاضر که این مقاله تدوین شده است هنوز نهایی اعلام نشده است. برای پشتیبانی مرورگرهای قدیمی از webstorage می‌توان از فایل جاوااسکریپتی Store.js کمک گرفت.


مفاهیم امنیتی و محافظت از داده ها

محدودیت‌های حمایتی و حفاظتی webstorage دقیقا همانند کوکی هاست. به این معنی که وب سایت‌های دیگر توانایی اتصال به webstorage سایت دیگری را ندارند. البته این مورد ممکن است برای وب سایت هایی که بر ساب دومین تکیه کرده‌اند ایجاد مشکل کند. برای حل این مسائل می‌توانید از کتابخانه‌های سورس بازی چون Cross Storage که توسط Zendesk ارائه شده است، استفاده کرد.

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

Data Integrity یا یکپارچگی داده‌ها نیز در نظر گرفته شده است. باید حفاظتی در برابر عدم موفقیت ذخیره سازی داده‌ها نیز وجود داشته باشد. این عدم موفقیت‌ها میتواند به دلایل زیر رخ دهد:

  • اگر کاربر قابلیت webstorage را غیرفعال کرده باشد.
  • اگر فضایی برای کاربر باقی نمانده باشد.
  • با محدودیت حجمی webstorage مواجه شده است.
  • با مواجه شدن با خطاها یک استثنا صادر می‌شود که می‌توانید آن را دریافت و کنترلی را روی برنامه تحت وب داشته باشید. یک نمونه استثنا  QuotaExceededError  


IndexedDB

یکی از فرایندهای ذخیره سازی داده‌ها که همان مزایای webstorage را ارائه میدهد indexed Database API است. این قابلیت از HTML 5 اضافه شده است و قسمتی از مشخصات webstorage شناخته نمی‌شود. برای همین مستنداتی در حوزه‌ی webstorage برای آن پیدا نخواهید کرد ولی قابلیت‌هایی فراتر از webstorage  دارد.

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

در قسمت آتی نحوه کدنویسی آن را فرا خواهیم گرفت.

مطالب
آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT
برای مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» و پروژه‌ی آن، یک چنین رابط کاربری آزمایشی تهیه شده‌است:


اکنون در ادامه قصد داریم این موارد را تبدیل به چندین درخواست به هم مرتبط postman کرده و در نهایت آن‌ها را به صورت یک collection قابل آزمایش مجدد، ذخیره کنیم.

مرحله 1: خاموش کردن بررسی مجوز SSL برنامه

چون مجوز SSL برنامه‌های ASP.NET Core که در حالت local اجرا می‌شوند از نوع self-signed certificate است، توسط postman پردازش نخواهند شد. به همین جهت نیاز است به منوی File -> Settings آن مراجعه کرده و این بررسی را خاموش کنیم:



مرحله 2: ایجاد درخواست login و دریافت توکن‌ها


در اینجا این مراحل طی شده‌اند:
- ابتدا آدرس درخواست به https://localhost:5001/api/account/login تنظیم شده‌است.
- سپس نوع درخواست نیز به Post تغییر کرده‌است.
- چون اکنون نوع درخواست، Post است، می‌توان بدنه‌ی آن‌را مقدار دهی کرد و چون نوع آن JSON است، گزینه‌ی raw و سپس contentType صحیح انتخاب شده‌اند. در ادامه مقدار زیر تنظیم شده‌است:
{
    "username": "Vahid",
    "password": "12345"
}
این مقداری‌است که اکشن متد login می‌پذیرد. البته اگر برنامه را اجرا کنید، کلمه‌ی عبور پیش‌فرض آن 1234 است.
- پس از این تنظیمات اگر بر روی دکمه‌ی send کلیک کنیم، توکن‌های دریافتی را در قسمت response می‌توان مشاهده کرد.


مرحله‌ی 3: ذخیره سازی توکن‌های دریافتی در متغیرهای سراسری

برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است access_token را به همراه هر درخواست، به سمت سرور ارسال کرد. بنابراین نیاز است در همینجا این دو توکن را در متغیرهایی برای دسترسی بعدی، ذخیره کنیم:

var jsonData = pm.response.json();
pm.globals.set("access_token", jsonData.access_token);
pm.globals.set("refresh_token", jsonData.refresh_token);
محل تعریف این کدها نیز در قسمت Tests درخواست جاری است تا پس از پایان کار درخواست، اطلاعات آن استخراج شده و ذخیره شوند.


مرحله‌ی 3: ذخیره سازی مراحل انجام شده

برای این منظور، بر روی دکمه‌ی Save کنار Send کلیک کرده، نام Login را وارد و سپس یک Collection جدید را با نام دلخواه JWT Sample ایجاد می‌کنیم:


سپس این درخواست را در این مجموعه ذخیره خواهیم کرد.


مرحله‌ی 4: دسترسی به منابع محافظت شده‌ی سمت سرور

برای این منظور بر روی دکمه‌ی + موجود در کنار اولین برگه‌ای که مشغول به تکمیل آن هستیم، کلیک می‌کنیم تا یک برگه‌ی جدید ایجاد شود. سپس مشخصات آن را به صورت زیر تکمیل خواهیم کرد:


ابتدا آدرس درخواست از نوع Get را به https://localhost:5001/api/MyProtectedApi تنظیم خواهیم کرد.
سپس گزینه‌ی هدرهای این درخواست را انتخاب کرده و key/value جدیدی با مقادیر Authorization و Bearer {{access_token}} ایجاد می‌کنیم. در اینجا {{access_token}} همان متغیر سراسری است که پس از لاگین، تنظیم می‌شود. اکنون از این متغیر جهت تنظیم هدر Authorization استفاده کرده‌ایم.
در آخر اگر بر روی دکمه‌ی Send این درخواست کلیک کنیم، response فوق را می‌توان مشاهده کرد.

فراخوانی مسیر api/MyProtectedAdminApi نیز دقیقا به همین نحو است.

یک نکته: روش دومی نیز برای تنظیم هدر Authorization در postman وجود دارد. برای این منظور، گزینه‌ی Authorization یک درخواست را انتخاب کنید (تصویر زیر). سپس نوع آن‌را به Bearer token تغییر دهید و مقدار آن‌را به همان متغیر سراسری {{access_token}} تنظیم کنید. به این صورت هدر مخصوص JWT را به صورت خودکار ایجاد و ارسال می‌کند و در این حالت دیگر نیازی به تنظیم دستی برگه‌ی هدرها نیست.



مرحله‌ی 5: ارسال Refresh token و دریافت یک سری توکن جدید

این مرحله، نیاز به ارسال anti-forgery token را هم دارد و گرنه از طرف سرور و برنامه، برگشت خواهد خورد. اگر به کوکی‌های تنظیم شده‌ی توسط برگه‌ی لاگین دقت کنیم:


کوکی با نام XSRF-TOKEN نیز تنظیم شده‌است. بنابراین آن‌را توسط متد pm.cookies.get، در قسمت Tests برگه‌ی لاگین خوانده و در یک متغیر سراسری تنظیم می‌کنیم:
var jsonData = pm.response.json();
pm.globals.set("access_token", jsonData.access_token);
pm.globals.set("refresh_token", jsonData.refresh_token);
pm.globals.set('XSRF-TOKEN', pm.cookies.get('XSRF-TOKEN'));
سپس برگه‌ی جدید ایجاد درخواست refresh token به صورت زیر تنظیم می‌شود:
ابتدا در قسمت بدنه‌ی درخواست از نوع post به آدرس https://localhost:5001/api/account/RefreshToken، در قسمت raw آن، با انتخاب نوع json، این refresh token را که در قسمت لاگین خوانده و ذخیره کرده بودیم، به سمت سرور ارسال خواهیم کرد:


همچنین دو هدر زیر را نیز باید ارسال کنیم:


یکی هدر مخصوص Authorization است که در مورد آن بحث شد و دیگر هدر X-XSRF-TOKEN که در سمت سرور بجای anti-forgery token پردازش می‌شود. مقدار آن‌را نیز در برگه‌ی login با خواندن کوکی مرتبطی، پیشتر تنظیم کرده‌ایم که در اینجا از متغیر سراسری آن استفاده شده‌است.

اکنون اگر بر روی دکمه‌ی send این برگه کلیک کنید، دو توکن جدید را دریافت خواهیم کرد:


که نیاز است مجددا در برگه‌ی Tests آن، متغیرهای سراسری پیشین را بازنویسی کنند. به همین جهت دقیقا همان 4 سطری که اکنون در برگه‌ی Tests درخواست لاگین وجود دارند، باید در اینجا نیز تکرار شوند تا عملیات refresh token واقعا به تمام برگه‌های موجود، اعمال شود.


مرحله‌ی آخر: پیاده سازی logout

در اینجا نیاز است refreshToken را به صورت یک کوئری استرینگ به سمت سرور ارسال کرد که به کمک متغیر {{refresh_token}}، در برگه‌ی params تعریف می‌شود:


همچنین هدر Authorization را نیز باید درج کرد:


پس از آن اگر درخواست را رسال کنیم، یک true را باید بتوان در response مشاهده کرد:



ذخیره سازی مجموعه‌ی جاری به صورت یک فایل JSON

برای گرفتن خروجی از این مجموعه و به اشتراک گذاشتن آن‌، اگر اشاره‌گر ماوس را بر روی نام یک مجموعه حرکت دهیم، یک دکمه‌ی جدید با برچسب ... ظاهر می‌شود. با کلیک بر روی آن، یکی از گزینه‌های آن، export نام دارد که جزئیات تمام درخواست‌های یک مجموعه را به صورت یک فایل JSON ذخیره می‌کند. برای نمونه فایل JSON خروجی این قسمت را می‌توانید از اینجا دریافت کنید: JWT-Sample.postman_collection.json
اشتراک‌ها
یک ابزار رایگان تجمیع پیامهای (لاگهای) شبکه

WhatsUp Gold Syslog Server 

  • یک ابزار غنی تجمیع پیام‌های (لاگهای) شبکه ای با کمک پروتکلهای SNMP و Syslog
  • مشاهده پیام‌های شبکه در زمان واقعی همراه با فیلتر اطلاعات
  • قابل پردازش تا 6 میلیون پیام در ساعت (حدودا 100000 تراکنش در دقیقه)
  • ارسال پیامهای شبکه به طور مستقیم و آنلاین به فایل ویندوز
  • دریافت پیام از دستگاه‌های مختلف از طریق پروتکل‌های UDP و TCP
  • ارسال پیامهای شبکه به صورت آنلاین به سرورهای دیگر با استفاده از پروتکل UDP و TCP
  • حفظ آدرس منبع در هنگام ارسال پیام به یک syslog Server دیگر
  • اجرا به عنوان یک سرویس ویندوزی (Windows Service) و یا در حالت Windows Application
  • فیلتر و مرتب سازی پیام‌های syslog برای نمایش داده شدن
  • همه این قابلیتها در این نرم افزار تحت دات نت فریم ورک 4 پیاده سازی شده است


File size: 5.14MB
 
Operating system: Windows XP/Vista/7/Server2008

Additional Requirements: Microsoft .NET Framework 4.0

یک ابزار رایگان تجمیع پیامهای (لاگهای) شبکه
مطالب
اجرای یک Script حاوی دستورات Go در سی شارپ
سلام ؛

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

از زمانی که استفاده از ORM‌های Code First رایج شده ، اجرای اسکریپت‌های طولانی جهت ایجاد دیتابیس خیلی استفاده ندارد، اما حالت خاص همیشه پیش می‌آید.
مثلا قصد داریم پیش از آغاز برنامه پس از ایجاد دیتابیس توسط Entity Framework به یک سری جداول فیلدی با نوع داده‌ی Geometry اضافه کنیم. یا باید به دیتابیس یک سری Stored Procedure و View اضافه کرد.
Script‌ها ی Generate شده توسط SQL Server حاوی دستور Go هستند. ADO.NET اجرای Script که حاوی Go باشد را پشتیبانی نمی‌کند.
اما روش‌ها و ترفند‌های زیادی برای اجرای یک فایل Script طولانی حاوی دستور Go روی دیتابیس وجود دارد :
        private static string GetScript()
        {
            string path = AppDomain.CurrentDomain.BaseDirectory +
                          @"Scripts\script.sql";
            var file = new FileInfo(path);

            string script = file.OpenText().ReadToEnd();
            return script;
        }

        private static void ExecuteScript()
        {
            string script = GetScript();

            //split the script on "GO" commands
            var splitter = new[] {"\r\nGO\r\n"};
            string[] commandTexts = script.Split(splitter,
                                                 StringSplitOptions.RemoveEmptyEntries);
            foreach (string commandText in commandTexts)
            {
                using (var ctx = new DbContext())
                {
                    if (!string.IsNullOrEmpty(commandText))
                    {
                        ctx.Database.ExecuteSqlCommand(commandText);
                    }
                }
            }
        }

در اینجا به جای آنکه تلاش کنیم یک فایل را روی دیتابیس یک جا اجرا کنیم دستورات را جدا کرده و تک به تک اجرا می‌کنیم. 
بروزرسانی:
نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 15 - نوشتن آزمون‌های واحد
فرض کنید که برای متد Create سرویس BookService، تست‌های مختلفی نوشته باشیم. حال اگر بخواهیم برای متد Update سرویس BookService نیز تست بنویسیم، بهتر است که در مرحله Arrange تست، از متد Create همان BookService که قبلا برای آن تست نوشته ایم استفاده کنیم یا مستقیما با استفاده از متد‌های Entity Framework، کتاب مورد نظر را مستقیما در دیتابیس درج کنیم و برای ویرایش آن تست بنویسیم؟
نظرات مطالب
ساخت یک اپلیکیشن ساده ToDo با ASP.NET Identity
سلام
اگر پروژه ای داشته باشیم با ساختار زیر
MyProject
MyProject.Service
MyProject.Model
MyProject.Data (Entity framework code first)
اگر بخوایم تو لایه MyProject از asp .net identity  استفاده کنیم، به چه نحوی باید این کار رو انجام داد که ساختار دیتابیس که در لایه دیتا دچار مشکل نشه و جداول مورد نیاز asp .net identity به دیتا بیس اضافه بشه؟
مطالب
شروع به کار با EF Core 1.0 - قسمت 7 - بررسی رابطه‌ی One-to-Many
در مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود»، نحوه‌ی مهندسی معکوس ساختار جداول و ارتباطات یک بانک اطلاعاتی از پیش موجود را به روش Code First بررسی کردیم. با توجه به رسمی بودن این ابزار، می‌توان از آن برای یافتن معادل‌های سمت بانک اطلاعاتی، در EF Core نیز استفاده کرد. برای مثال بررسی کرد، درک EF Core از بانک اطلاعاتی طراحی شده چیست و هر چند در آن مطلب عنوان شد که می‌توان با پارامتر data-annotations-- ، خروجی نهایی را بر اساس روش data-annotations، بجای Fluent API به دست آورد، اما در مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها» مشاهده کردیم که بسیاری از تنظیمات پیشرفته‌ی EF Core، اساسا معادل data-annotation ایی ندارند. بنابراین بهتر است این پارامتر را فعال سازی نکنید.


تنظیمات روابط یک به چند در EF Core

همان اسکریپت ابتدای مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانک‌های اطلاعاتی از پیش موجود» را درنظر بگیرید. رابطه‌ی تعریف شده‌ی در آن از نوع one-to-many است: یک بلاگ که می‌تواند چندین مطلب را داشته باشد.


اگر EF Core را وادار به تولید نگاشت‌های Code First معادل آن کنیم، به این خروجی‌ها خواهیم رسید:
الف) با استفاده از روش Fluent API
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
با خروجی:
using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }
        public string Url { get; set; }

        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>(entity =>
            {
                entity.Property(e => e.Url).IsRequired();
            });

            modelBuilder.Entity<Post>(entity =>
            {
                entity.HasOne(d => d.Blog)
                    .WithMany(p => p.Post)
                    .HasForeignKey(d => d.BlogId);
            });
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}

نحوه‌ی تشخیص خودکار روابط

EF Core به صورت پیش فرض، روابط را بر اساس ارجاعات بین کلاس‌ها تشخیص می‌دهد. در اینجا به خاصیت Blog نام navigation property را می‌دهند:
 public virtual Blog Blog { get; set; }
و به خاصیت Post نیز Collection navigation property می‌گویند:
 public virtual ICollection<Post> Post { get; set; }
در اینجا اگر تنها دو navigation property، در کلاس‌های به هم مرتبط شده، یافت شوند، به صورت خودکار به عنوان دو سر رابطه تنظیم می‌شوند. اگر بیشتر از یک navigation property در کلاسی وجود داشت، هیچ رابطه‌ای به صورت خودکار تشکیل نشده و باید ابتدا و انتهای روابط را به صورت دستی مشخص نمود.


نحوه‌ی تشخیص خودکار کلیدهای خارجی
اگر در یک طرف رابطه‌ی تشخیص داده شده، خاصیتی با یکی از سه نام زیر وجود داشت:
<primary key property name>
<navigation property name><primary key property name>
<principal entity name><primary key property name>
آنگاه این خاصیت به صورت خودکار به عنوان کلید خارجی تنظیم می‌شود. در رابطه‌ی فوق Blog از نوع principal است (پدر رابطه) و Post از نوع dependent (فرزند رابطه).
برای مثال در رابطه‌ی فوق، نام خاصیت BlogId دقیقا بر اساس همان الگوی <primary key property name> طرف دیگر رابطه‌است:
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
بنابراین به صورت خودکار به عنوان کلید خارجی درنظر گرفته می‌شود.

تا اینجا اگر مطلب را دنبال کرده باشید به این نتیجه خواهید رسید که دو کلاس فوق، اساسا نیازی به هیچ نوع تنظیم Fluent و یا Data annotations ایی برای برقراری ارتباط یک به چند ندارند. چون روابط بین آن‌ها بر اساس خواص راهبری (navigation property) و همچنین الگوی <primary key property name>، به صورت خودکار قابل تشخیص و تنظیم است. به علاوه ... در هر طرف رابطه، فقط یک navigation property وجود دارد و نیازی به تنظیم دستی سر دیگر رابطه نیست.


استفاده از Fluent API برای تنظیم رابطه‌ی One-to-Many

در تنظیمات فوق، در متد OnModelCreating، ذکر صریح این روابط را صرفا جهت از بین بردن هرگونه ابهامی مشاهده می‌کنید:
modelBuilder.Entity<Post>(entity =>
{
    entity.HasOne(d => d.Blog)
             .WithMany(p => p.Post)
             .HasForeignKey(d => d.BlogId);
});
از هر طرفی که شروع می‌کنید، متدهای HasOne و یا HasMany، مشخص کننده‌ی navigation property هستند که در سمت موجودیت معرفی شده قرار دارند. در اینجا چون کار با موجودیت Post شروع شده‌است، متد HasOne به خاصیت راهبری در همان سمت و به خاصیت Blog آن اشاره می‌کند.
مرحله‌ی بعد، مشخص کردن سر دیگر رابطه (inverse navigation) است. این‌کار توسط یکی از متدهای WithOne و یا WithMany انجام می‌شود.
متدهایی که اسامی فرد دارند مانند HasOne/WithOne به یک navigation property ساده اشاره می‌کنند.
متدهایی که اسامی جمع دارند مانند HasMany/WithMany به collection navigation properties اشاره خواهند کرد.
متد HasForeignKey نیز برای ذکر صریح کلید خارجی بکار رفته‌است.


ب) با استفاده از روش data-annotations
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
 dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose -a
با خروجی:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Blog
    {
        public Blog()
        {
            Post = new HashSet<Post>();
        }

        public int BlogId { get; set; }

        [Required]
        public string Url { get; set; }

        [InverseProperty("Blog")]
        public virtual ICollection<Post> Post { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Core1RtmEmptyTest.Entities
{
    public partial class Post
    {
        public int PostId { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        [ForeignKey("BlogId")]
        [InverseProperty("Post")]
        public virtual Blog Blog { get; set; }
        public int BlogId { get; set; }
    }
}

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Core1RtmEmptyTest.Entities
{
    public partial class MyDBDataContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }

        public virtual DbSet<Blog> Blog { get; set; }
        public virtual DbSet<Post> Post { get; set; }
    }
}
همانطور که در توضیحات روش Fluent API عنوان شد، این مدل خاص، چون دقیقا بر اساس پیش فرض‌های EF Core طراحی شده‌است، نیازی به هیچگونه تنظیم اضافه‌تری ندارد. اما اگر کلید خارجی، مطابق سه الگویی که عنوان شد، قابل تشخیص نباشد، باید آن‌را در روش data annotations توسط ویژگی ForeignKey، به نحو صریحی مشخص کرد:
  [ForeignKey("BlogId")]
  [InverseProperty("Post")]
  public virtual Blog Blog { get; set; }
  public int BlogId { get; set; }
همچنین اگر بیش از یک خاصیت راهبری (navigation property) وجود داشت، ذکر InverseProperty نیز ضروری است تا مشخص شود سر دیگر این رابطه دقیقا کدام است.
در این حالت (داشتن بیش از یک خاصیت راهبری)، باید ویژگی InverseProperty را نیز به سر دوم رابطه، اعمال کرد.
   [InverseProperty("Blog")]
  public virtual ICollection<Post> Post { get; set; }

مطالب تکمیلی

علت virtual بودن خواص راهبری تولید شده

اگر دقت کنید، EF Core کدی را که تولید کرده‌است، به همراه خاصیت‌هایی virtual است:
public virtual Blog Blog { get; set; }
در اینجا تمام خاصیت‌های راهبری virtual تعریف شده‌اند. علت آن، به پیاده سازی مباحث AOP بر می‌گردد. زمانیکه خاصیتی به صورت virtual تعریف می‌شود، EF core می‌تواند آن‌را توسط یک شیء پروکسی شفاف احاطه کند. این پروکسی‌ها دو هدف را دنبال می‌کند:
الف) پیاده سازی lazy loading (بارگذاری خودکار اعضای مرتبط (همان خواص راهبری) با اولین دسترسی به آن‌ها)
ب) پیاده سازی change tracking

مبحث lazy loading فعلا در EF Core 1.0 پشتیبانی نمی‌شود. اما change tracking آن فعال است.
بنابراین اگر مشاهده کردید خواص راهبری به صورت virtual تعریف شده‌اند، علت آن فعال سازی lazy loading است و اگر سایر خواص به صورت virtual تعریف شده‌اند، هدف اصلی آن بهبود عملکرد سیستم change tracking است.
همچنین اگر دقت کرده باشید، نوع مجموعه‌ها نیز ICollection ذکر شده‌است. این مورد نیز یکی دیگر از پیش فرض‌های توکار EF Core است؛ در جهت تشکیل پروکسی‌ها بر روی خواص راهبری مجموعه‌ای (علاوه بر virtual تعریف کردن آن‌ها). عنوان شده‌است که اگر برای مثال از List استفاده کنید (پیاده سازی اینترفیس) یا هر اینترفیس دیگری که از ICollection  مشتق شده‌است، این پروکسی‌ها تشکیل نخواهند شد.


واکشی اعضای به هم مرتبط

همانطور که عنوان شد، نگارش اول EF Core برخلاف EF 6.x از Lazy loading پشتیبانی نمی‌کند. البته این مساله در کل مورد مثبتی است؛ خصوصا در برنامه‌های وب! چون استفاده‌ی نادرست از Lazy loading که به select n+1 نیز مشهور است، سبب رفت و برگشت‌های بی‌شماری به بانک اطلاعاتی می‌شود و عموم برنامه نویس‌های وب باید مدام توسط برنامه‌های Profiler بررسی کنند که آیا این مساله رخ داده‌است یا خیر. فعلا EF Core از این مشکل در امان است!
اما ... اگر به روش کار EF 6.x عادت کرده باشید، قطعه کد ذیل:
 var firstPost = context.Post.First();
Console.WriteLine(firstPost.Blog.Url);
چنین خطایی را صادر می‌کند:
 System.NullReferenceException
Object reference not set to an instance of an object.
علت اینجا است که چون Lazy loading غیرفعال است (هنوز در EF Core 1.0 پیاده سازی نشده‌است)، اولین دسترسی به شیء Blog، سبب وهله سازی خودکار آن نشده و این شیء نال است. به همین جهت استثنای فوق را مشاهده می‌کنیم.
برای رفع این مشکل باید توسط متد Include، سبب لغو عملیات Lazy loading و واکشی صریح Blog مرتبط شویم که اصطلاحا به آن eager loading می‌گویند:
 var firstPost = context.Post.Include(x => x.Blog).First();
Console.WriteLine(firstPost.Blog.Url);

نکته‌ای در مورد سطوح بارگذاری اعضای به هم مرتبط در EF Core

متد Include ایی را که تا اینجا مشاهده کردید، با EF 6.x تفاوتی ندارد. برای مثال اگر شیء Blog حاوی خواص راهبری Posts و همچنین Owner باشد، برای بارگذاری این اعضای مرتبط، می‌توان همانند قبل، متدهای Include را پشت سر هم ذکر کرد:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Include(blog => blog.Owner)
                              .ToList();
اما فرض کنید خاصیت Post، دارای یک خاصیت راهبری دیگری به نام Author نیز باشد و می‌خواهیم این خاصیت هم بارگذاری شود:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                      .ThenInclude(post => post.Author)
                              .ToList();
روش انجام چنین کاری در EF Core، توسط متد الحاقی جدید ThenInclude است. ابتدا لیست Blogها عنوان شده‌است. سپس در این لیست علاقمند به واکشی تمام مطالب این بلاگ‌ها هم بوده‌ایم. به علاوه در این مطالب، نیاز است خاصیت Author آن‌ها نیز از پیش مقدار دهی شده و قابل دسترسی باشد. به همین جهت برای دسترسی به چندین سطح مختلف از متد ThenInclude کمک گرفته شده‌است.
همچنین در اینجا امکان ذکر زنجیروار متدهای ThenInclude هم هست:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                 .ThenInclude(post => post.Author)
                                        .ThenInclude(author => author.Photo)
                              .ToList();
در این مثال یک سطح دیگر جلو رفته و شیء Photo مربوط به شیء Author را هم واکشی کرده‌ایم.
به علاوه امکان ذکر چندین ریشه و چندین زیر ریشه هم وجود دارند:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                                  .ThenInclude(post => post.Author)
                                      .ThenInclude(author => author.Photo)
                              .Include(blog => blog.Owner)
                                    .ThenInclude(owner => owner.Photo)
                              .ToList();

یک نکته: متد Include تنها زمانی درنظر گرفته خواهد شد که نوع خروجی نهایی کوئری، دقیقا از نوع موجودیتی باشد که با آن شروع به کار کرده‌ایم. برای مثال اگر در این بین یک Select اضافه شود و فقط تنها تعدادی از خواص Blog واکشی شوند، از تمام Includeهای ذکر شده صرفنظر می‌شود؛ مانند کوئری ذیل:
var blogs = context.Blogs
                              .Include(blog => blog.Posts)
                              .Select(blog => new
                               {
                                  Id = blog.BlogId,
                                  Url = blog.Url
                               })
                               .ToList();


تنظیمات حذف آبشاری در رابطه‌ی one-to-many

زمانیکه در رابطه‌ی one-to-many قسمت principal (والد رابطه) و یا همان Blog در مثال جاری حذف می‌شود، سه اتفاق برای فرزندان آن میسر خواهند بود:
الف) Cascade : در این حالت ردیف‌های فرزندان وابسته نیز حذف خواهند شد.
باید دقت داشت که حالت Cascade فقط برای موجودیت‌هایی اعمال می‌شود که توسط Context بارگذاری شده و در آن وجود دارند. اگر می‌خواهید سایر موجودیت‌های مرتبط نیز با این روش حذف شوند، باید در سمت دیتابیس نیز تنظیماتی مانند ON DELETE CASCADE زیر نیز وجود داشته باشند:
 CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE
و اگر با EF Core بانک اطلاعاتی خود را ایجاد می‌کنید (مباحث مهاجرت‌ها)، این تنظیم به صورت خودکار اعمال خواهد شد؛ اگر DeleteBehavior را به نحو ذیل مشخص کرده باشید:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .OnDelete(DeleteBehavior.Cascade);
ب) SetNull: در این حالت فرزندان وابسته حذف نمی‌شوند و تنها کلید خارجی آن‌ها به نال تنظیم می‌شود.
ج) Restrict: هیچ تغییری بر روی فرزندان رابطه رخ نمی‌دهد.

یک نکته: به صورت پیش فرض اگر رابطه‌ی one-to-many، به Required تنظیم شود، حالت حذف آن cascade خواهد بود. در غیراینصورت برای حالت‌های Optional، حالت SetNull تنظیم می‌گردد:
modelBuilder.Entity<Post>()
                    .HasOne(p => p.Blog)
                    .WithMany(b => b.Posts)
                    .IsRequired();
در اینجا ذکر صریح متد IsRequired به این معنا است که مقدار دهی کلید خارجی سر دیگر رابطه، اجباری است.
به علاوه باید دقت داشت، همان مباحث «تعیین اجباری بودن یا نبودن ستون‌ها در EF Core» در قسمت قبل، در اینجا هم صادق است. برای مثال چون BlogId (کلید خارجی در کلاس Post) از نوع int است و نال پذیر نیست، بنابراین از دیدگاه EF Core یک فیلد اجباری درنظر گرفته می‌شود. به همین جهت است که در کدهای تولید شده‌ی توسط EF Core در ابتدای بحث، ذکر متد IsRequired و یا OnDelete را مشاهده نمی‌کنید.
بنابراین اگر می‌خواهید حالت SetNull را فعال کنید، باید این کلید خارجی را نیز نال پذیر و به صورت int? BlogId ذکر کنید تا optional درنظر گرفته شود.
اشتراک‌ها
نمونه مثالی از ASP.NET Core و Entity Framework Core به همراه معماری DDD و CQRS و Event Sourcing

Full ASP.NET Core 2.2 application with DDD, CQRS and Event Sourcing  

Technologies implemented:

  • ASP.NET Core 2.2 (with .NET Core 2.2)
  • ASP.NET MVC Core
  • ASP.NET WebApi Core
  • ASP.NET Identity Core
  • Entity Framework Core 2.2
  • .NET Core Native DI
  • AutoMapper
  • FluentValidator
  • MediatR
  • Swagger UI 

Architecture:

  • Full architecture with responsibility separation concerns, SOLID and Clean Code
  • Domain Driven Design (Layers and Domain Model Pattern)
  • Domain Events
  • Domain Notification
  • CQRS (Imediate Consistency)
  • Event Sourcing
  • Unit of Work
  • Repository and Generic Repository 

نمونه مثالی از ASP.NET Core و Entity Framework Core به همراه معماری DDD و CQRS و Event Sourcing
مطالب
کار با نوع داده‌ی HierarchyID توسط Entity framework
نوع داده‌ی HierarchyID به همراه SQL Server 2008 برای کار با داده‌هایی با ساختار درختی ارائه شد. در حال حاضر هیچکدام از ORMهای موجود، پشتیبانی رسمی را از این نوع داده به عمل نمی‌آورند؛ اما با توجه به سورس باز بودن Entity framework، یک Fork مستقل از آن تهیه شده‌است و این نوع داده‌ی جدید به همراه متدهای مرتبط با آن، به این Fork اضافه شده‌اند.
- اصل Fork در اینجا
- تاریخچه‌ی این Fork غیر رسمی در اینجا
- بسته‌ی نیوگت آن در اینجا

چون تیم EF در نگارش فعلی این کتابخانه حاضر به افزودن این نوع جدید نشده‌است، بنابراین بجای بسته‌ی اصلی Entity framework نیاز است بسته‌ی EntityFrameworkWithHierarchyId را نصب کنید.
 PM> install-package EntityFrameworkWithHierarchyId

یک تذکر مهم:
چون امضای دیجیتال این بسته، با امضای دیجیتال بسته‌ی اصلی EF یکی نیست، اگر پروژه‌ی شما صرفا از EF استفاده می‌کند، مشکلی نخواهید داشت. اما اگر برای مثال از ASP.NET Identity کامپایل شده‌ی برای کار با EF اصلی استفاده کنید، پیام یافت نشدن DLL مرتبط را دریافت خواهید کرد.


تعریفی مدلی با خاصیتی از نوع جدید HierarchyId

public class Employee
{
    public int Id { get; set; }
 
    [Required, MaxLength(100)]
    public string Name { get; set; }
 
    [Required]
    public HierarchyId Node { get; set; } // نوع داده جدید
}
در اینجا مدلی را ملاحظه می‌کنید که از نوع داده‌ی جدید HierarchyId استفاده می‌کند. همانطور که عنوان شد این نوع در بسته‌ی EntityFrameworkWithHierarchyId موجود است.


تعریف Context و مقدار دهی اولیه‌ی آن

در این حالت Context برنامه به همراه تنظیمات اولیه‌ی Migrations آن یک چنین شکلی را پیدا خواهد کرد:
public class MyContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }
 
    public MyContext()
        : base("Connection1")
    {
        this.Database.Log = log => Console.WriteLine(log);
    }
}
 
public class Configuration : DbMigrationsConfiguration<MyContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
 
    protected override void Seed(MyContext context)
    {
        if (context.Employees.Any())
            return;
 
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD NodePath as Node.ToString() persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD Level AS Node.GetLevel() persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD ManagerNode as Node.GetAncestor(1) persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD ManagerNodePath as Node.GetAncestor(1).ToString() persisted");
 
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD CONSTRAINT [UK_EmployeeNode] UNIQUE NONCLUSTERED (Node)");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees]  WITH CHECK ADD CONSTRAINT [EmployeeManagerNodeNodeFK] " +
            "FOREIGN KEY([ManagerNode]) REFERENCES [dbo].[Employees] ([Node])");
 
        context.Employees.Add(new Employee { Name = "Root", Node = new HierarchyId("/") });
        context.Employees.Add(new Employee { Name = "Emp1", Node = new HierarchyId("/1/") });
        context.Employees.Add(new Employee { Name = "Emp2", Node = new HierarchyId("/2/") });
        context.Employees.Add(new Employee { Name = "Emp3", Node = new HierarchyId("/1/1/") });
        context.Employees.Add(new Employee { Name = "Emp4", Node = new HierarchyId("/1/1/1/") });
        context.Employees.Add(new Employee { Name = "Emp5", Node = new HierarchyId("/2/1/") });
        context.Employees.Add(new Employee { Name = "Emp6", Node = new HierarchyId("/1/2/") });
 
        base.Seed(context);
    }
}
در اینجا نحوه‌ی تعریف رکوردهای جدید مبتنی بر HierarchyId را مشاهده می‌کنید که توسط آن‌ها تعدادی کارمند، در یک سازمان فرضی ثبت شده‌اند.
همچنین چند فیلد محاسباتی نیز بر اساس امکانات توکار SQL Server اضافه شده‌اند. متدهایی مانند ToString، GetLevel، GetAncestor و امثال آن جزئی از پیاده سازی توکار SQL Server هستند. همچنین این متدها توسط کتابخانه‌ی EntityFrameworkWithHierarchyId نیز ارائه شده‌اند.


کوئری نویسی

مرتب سازی رکوردها بر اساس HierarchyId آن‌ها

using (var context = new MyContext())
{
    Console.WriteLine("\ngetItems OrderByDescending(employee => employee.Node)");
 
    var employees = context.Employees.OrderByDescending(employee => employee.Node).ToList();
    foreach (var employee in employees)
    {
        Console.WriteLine("{0} {1}", employee.Id, employee.Node);
    }
 }
با این خروجی
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    ORDER BY [Extent1].[Node] DESC


6 /2/1/
3 /2/
7 /1/2/
5 /1/1/1/
4 /1/1/
2 /1/
1 /


یافتن یک HierarchyId خاص و سپس یافتن کلیه‌ی فرزندان آن در یک سطح پایین‌تر

using (var context = new MyContext())
{
    Console.WriteLine("\nGetAncestor(1) of /1/");
 
    var firstItem = context.Employees.Single(employee => employee.Node == new HierarchyId("/1/"));
    foreach (var item in context.Employees.Where(employee => firstItem.Node == employee.Node.GetAncestor(1)))
    {
        Console.WriteLine("{0} {1}", item.Id, item.Name);
    }
}
این کوئری را به این شکل نیز می‌توان عنوان کرد: یافتن یک HierarchyId و سپس یافتن کلیه نودهایی که والدشان (GetAncestor) این HierarchyId است. عدد یک در اینجا مشخص کننده‌ی Level یا سطح است.
با این خروجی:
SELECT TOP (2)
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE cast('/1/' as hierarchyid) = [Extent1].[Node]

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE (@p__linq__0 = ([Extent1].[Node].GetAncestor(1))) OR ((@p__linq__0 IS
NULL) AND ([Extent1].[Node].GetAncestor(1) IS NULL))
-- p__linq__0: '/1/' (Type = Object)

4 Emp3
7 Emp6

کوئری‌های فوق را می‌توان بجای استفاده از متد GetAncestor، با استفاده از متد IsDescendantOf به شکل زیر نیز نوشت:
var list = context.Employees.Where(
          employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")) &&
                              employee.Node.GetLevel() == 2).ToList();
با این خروجی SQL (یک کوئری بجای دو کوئری):
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE (([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1) 
    AND (2 = ([Extent1].[Node].GetLevel()))


جابجا کردن نودها توسط متد GetReparentedValue

در کوئری ذیل، تمامی فرزندان ریشه‌ی /1/ یافت شده و سپس والد آن‌ها به صورت پویا تغییر داده می‌شود:
var items = context.Employees.Where(employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")))
    .Select(employee => new
    {
        Id = employee.Id,
        OrigPath = employee.Node,
        ReparentedValue = employee.Node.GetReparentedValue(new HierarchyId("/1/"), HierarchyId.GetRoot()),
        Level = employee.Node.GetLevel()
    }).ToList();
 
foreach (var item in items)
{
    Console.WriteLine("Id:{0}; OrigPath:{1}; ReparentedValue:{2}; Level:{3}", item.Id, item.OrigPath, item.ReparentedValue, item.Level);
}
با این خروجی
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Node] AS [Node],
    [Extent1].[Node].GetReparentedValue(cast('/1/' as hierarchyid), hierarchyid::GetRoot()) AS [C1],
    [Extent1].[Node].GetLevel() AS [C2]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE ([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1


Id:2; OrigPath:/1/; ReparentedValue:/; Level:1
Id:4; OrigPath:/1/1/; ReparentedValue:/1/; Level:2
Id:5; OrigPath:/1/1/1/; ReparentedValue:/1/1/; Level:3
Id:7; OrigPath:/1/2/; ReparentedValue:/2/; Level:2

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
HierarcyIdTests.zip
مطالب
کوئری نویسی در EF Core - قسمت اول - تشکیل بانک اطلاعاتی و مقدار دهی اولیه‌ی آن
عموم کسانیکه برای بار اول با LINQ آشنا می‌شوند، مشکل ترجمه‌ی کوئری‌های قبلی SQL خود را به آن دارند. به همین جهت پس از چند سعی و خطا ترجیح می‌دهند تا از ORMها استفاده نکنند؛ چون در کوئری نویسی با آن‌ها مشکل دارند. در این سری، تمام مثال‌های سایت PostgreSQL Exercises با EF Core و LINQ to Entities آن پیاده سازی خواهند شد تا بتواند به عنوان راهنمایی برای تازه‌کاران مورد استفاده قرار گیرد.


بررسی ساختار بانک اطلاعاتی تمرین‌های سایت PostgreSQL Exercises

بانک اطلاعاتی مثال‌های سایت PostgreSQL Exercises از سه جدول با مشخصات زیر تشکیل می‌شود:

جدول کاربران
 CREATE TABLE cd.members
    (
       memid integer NOT NULL, 
       surname character varying(200) NOT NULL, 
       firstname character varying(200) NOT NULL, 
       address character varying(300) NOT NULL, 
       zipcode integer NOT NULL, 
       telephone character varying(20) NOT NULL, 
       recommendedby integer,
       joindate timestamp not null,
       CONSTRAINT members_pk PRIMARY KEY (memid),
       CONSTRAINT fk_members_recommendedby FOREIGN KEY (recommendedby)
            REFERENCES cd.members(memid) ON DELETE SET NULL
    );
هر کاربر در اینجا به همراه یک ID و آدرس است. همچنین به همراه اطلاعات کاربری که او را توصیه کرده‌است (یک جدول خود ارجاع دهنده‌است).


جدول امکانات قابل ارائه‌ی به کاربران
   CREATE TABLE cd.facilities
    (
       facid integer NOT NULL, 
       name character varying(100) NOT NULL, 
       membercost numeric NOT NULL, 
       guestcost numeric NOT NULL, 
       initialoutlay numeric NOT NULL, 
       monthlymaintenance numeric NOT NULL, 
       CONSTRAINT facilities_pk PRIMARY KEY (facid)
    );
در این جدول، امکاناتی مانند «زمین تنیس» و امثال آن ثبت می‌شوند؛ به همراه اطلاعاتی مانند هزینه‌ی اجاره‌ی آن توسط کاربران و یا مهمان‌ها که این دو هزینه، با هم متفاوت هستند. همچنین اطلاعاتی مانند هزینه‌ی راه‌اندازی اولیه‌ی آن‌ها، به همراه هزینه‌ی نگهداری ماهیانه‌ی هر کدام از امکانات نیز ثبت می‌شوند؛ تا در آینده بتوان یک سری محاسبات مالی را نیز در مورد امکانات مهیای مجموعه انجام داد تا مشخص شود که آیا برای مثال داشتن مجموعه‌ای خاص، مقرون به صرفه هست یا خیر.


جدول سوابق استفاده‌ی کاربران از امکانات مجموعه
CREATE TABLE cd.bookings
    (
       bookid integer NOT NULL, 
       facid integer NOT NULL, 
       memid integer NOT NULL, 
       starttime timestamp NOT NULL,
       slots integer NOT NULL,
       CONSTRAINT bookings_pk PRIMARY KEY (bookid),
       CONSTRAINT fk_bookings_facid FOREIGN KEY (facid) REFERENCES cd.facilities(facid),
       CONSTRAINT fk_bookings_memid FOREIGN KEY (memid) REFERENCES cd.members(memid)
    );
در این جدول با ثبت ID کاربر و امکاناتی را که درخواست داده، سوابق رزرو آن‌ها نگهداری می‌شوند.
هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعت‌های رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته می‌شود و زمان شروع این رزرو نیز ثبت می‌شود.


تبدیل ساختار بانک اطلاعاتی سایت PostgreSQL Exercises به EF Core Code First


در این دیاگرام، دیتابیس متشکل از سه جدول یاد شده را ملاحظه می‌کنید. برای تبدیل آن‌ها به موجودیت‌های EF Core، می‌توان به صورت زیر عمل کرد:

موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class Member
    {
        public int MemId { set; get; }

        public string Surname { set; get; }

        public string FirstName { set; get; }

        public string Address { set; get; }

        public int ZipCode { set; get; }

        public string Telephone { set; get; }

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

        public DateTime JoinDate { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
خواص این کلاس دقیقا بر اساس فیلدهای جدول کاربران مثال‌های سایت تهیه شده‌است. تنها تفاوت آن، داشتن خواص راهبری (navigation properties) مانند Children، Member و Bookings است که نوع روابط این موجودیت را با سایر موجودیت‌ها مشخص می‌کنند:
- خاصیت‌های Children و Recommender برای تعریف رابطه‌ی «خود ارجاعی» اضافه شده‌اند. در اینجا هر کاربر می‌تواند توسط کاربر دیگری توصیه شده باشد.
- خاصیت Bookings برای بیان رابطه‌ی یک به چند با موجودیت Booking، تعریف شده‌است؛ هر یک کاربر می‌تواند به هر تعدادی رزرو امکانات داشته باشد.


موجودیت Facility

namespace EFCorePgExercises.Entities
{
    public class Facility
    {
        public int FacId { set; get; }

        public string Name { set; get; }

        public decimal MemberCost { set; get; }

        public decimal GuestCost { set; get; }

        public decimal InitialOutlay { set; get; }

        public decimal MonthlyMaintenance { set; get; }

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
- در این جدول، خواص از نوع پولی، توسط نوع decimal معرفی شده‌اند. برای این موارد هیچگاه از double و یا float استفاده نکنید؛ اطلاعات بیشتر.
- خاصیت راهبری Bookings، بیانگر رابطه‌ی یک به چند هرکدام از امکانات مجموعه با تعداد بار و سوابق رزرو شدن آن‌ها است.


موجودیت Booking

namespace EFCorePgExercises.Entities
{
    public class Booking
    {
        public int BookId { set; get; }

        public int FacId { set; get; }
        public virtual Facility Facility { set; get; }

        public int MemId { set; get; }
        public virtual Member Member { set; get; }

        public DateTime StartTime { set; get; }

        public int Slots { set; get; }
    }
}
در جدول ثبت وقایع این مجموعه، اطلاعات کاربر و اطلاعات امکانات درخواستی توسط او ثبت می‌شوند. به همین جهت دو خاصیت راهبری Facility و Member نیز به ازای هر کدام از این Idها تعریف شده‌اند. وجود آن‌ها، جوین نویسی را در آینده بسیار ساده خواهند کرد.


تنظیمات هر کدام از موجودیت‌ها و روابط بین آن‌ها در EF Core Code First

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

تنظیمات موجودیت کاربران

namespace EFCorePgExercises.Entities
{
    public class MemberConfiguration : IEntityTypeConfiguration<Member>
    {
        public void Configure(EntityTypeBuilder<Member> builder)
        {
            builder.HasKey(member => member.MemId);
            builder.Property(member => member.MemId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(member => member.Surname).HasMaxLength(200).IsRequired();
            builder.Property(member => member.FirstName).HasMaxLength(200).IsRequired();
            builder.Property(member => member.Address).HasMaxLength(300).IsRequired();
            builder.Property(member => member.ZipCode).IsRequired();
            builder.Property(member => member.Telephone).HasMaxLength(20).IsRequired();

            builder.HasIndex(member => member.RecommendedBy);
            builder.HasOne(member => member.Recommender)
                    .WithMany(member => member.Children)
                    .HasForeignKey(member => member.RecommendedBy);

            builder.Property(member => member.JoinDate).IsRequired();

            builder.HasIndex(member => member.JoinDate).HasName("IX_JoinDate");
            builder.HasIndex(member => member.RecommendedBy).HasName("IX_RecommendedBy");
        }
    }
}
- در اینجا بر اساس تعاریفی که در ابتدای بحث مشاهده کردید، برای مثال طول هر کدام از فیلدهای رشته‌ای متناظر تعریف شده‌اند.
- سپس نحوه‌ی تعریف رابطه‌ی خود راجاعی این موجودیت را مشاهده می‌کنید.
- دو ایندکس هم در اینجا تعریف شده‌اند که جزو اطلاعات موجود در فایل SQL این سری از مثال‌ها هستند.

نکته‌ی مهم: در اینجا یک UseIdentityColumn(seed: 0, increment: 1) را نیز مشاهده می‌کنید که شاید برای شما تازگی داشته باشد. فیلد ID تمام جداول این مجموعه برخلاف معمول که از 1 شروع می‌شود، از صفر شروع می‌شود و ID مساوی صفر را برای کاربران مهمان درنظر گرفته‌است. روش تعریف چنین تنظیم خاصی را توسط متد UseIdentityColumn و دو پارامتر آن در اینجا مشاهده می‌کنید. این ID مساوی صفر، نکات خاصی را هم در حین ثبت اطلاعات اولیه‌ی هر جدول، به همراه دارد که در ادامه بررسی خواهد شد.


تنظیمات موجودیت امکانات مجموعه

namespace EFCorePgExercises.Entities
{
    public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
    {
        public void Configure(EntityTypeBuilder<Facility> builder)
        {
            builder.HasKey(facility => facility.FacId);
            builder.Property(facility => facility.FacId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(facility => facility.Name).HasMaxLength(100).IsRequired();

            builder.Property(facility => facility.MemberCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.GuestCost).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.InitialOutlay).IsRequired().HasColumnType("decimal(18, 6)");

            builder.Property(facility => facility.MonthlyMaintenance).IsRequired().HasColumnType("decimal(18, 6)");
        }
    }
}
تنها نکته‌ی مهم این تنظیمات، ذکر دقت نوع decimal است؛ بدون تنظیم آن، EF Core در حین اجرای Migrations، اخطاری را صادر می‌کند.


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

namespace EFCorePgExercises.Entities
{
    public class BookingConfiguration : IEntityTypeConfiguration<Booking>
    {
        public void Configure(EntityTypeBuilder<Booking> builder)
        {
            builder.HasKey(booking => booking.BookId);
            builder.Property(booking => booking.BookId).IsRequired().UseIdentityColumn(seed: 0, increment: 1);

            builder.Property(booking => booking.FacId).IsRequired();
            builder.HasOne(booking => booking.Facility)
                    .WithMany(facility => facility.Bookings)
                    .HasForeignKey(booking => booking.FacId);

            builder.Property(booking => booking.MemId).IsRequired();
            builder.HasOne(booking => booking.Member)
                    .WithMany(member => member.Bookings)
                    .HasForeignKey(booking => booking.MemId);

            builder.Property(booking => booking.StartTime).IsRequired();

            builder.Property(booking => booking.Slots).IsRequired();

            builder.HasIndex(booking => new { booking.MemId, booking.FacId }).HasName("IX_memid_facid");
            builder.HasIndex(booking => new { booking.FacId, booking.StartTime }).HasName("IX_facid_starttime");
            builder.HasIndex(booking => new { booking.MemId, booking.StartTime }).HasName("IX_memid_starttime");
            builder.HasIndex(booking => booking.StartTime).HasName("IX_starttime");
        }
    }
}
روابط یک به چند بین امکانات و رزروها و کاربران و رزروها، در تنظیمات فوق بیان شده‌اند و ذکر آن‌ها در یک سمت رابطه کافی است.


ایجاد Context و معرفی موجودیت‌ها و تنظیمات آن‌ها

در ادامه توسط ApplicationDbContext که از DbContext ارث‌بری می‌کند، سه موجودیت تعریف شده را در معرض دید EF Core قرار می‌دهیم:
namespace EFCorePgExercises.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Member> Members { get; set; }

        public DbSet<Booking> Bookings { get; set; }

        public DbSet<Facility> Facilities { get; set; }

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

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(MemberConfiguration).Assembly);
        }
    }
}
همچنین تمام تنظیماتی را که تعریف کردیم، توسط یک سطر ApplyConfigurationsFromAssembly می‌توان از اسمبلی دربرگیرنده‌ی آن‌ها خواند و به Context اضافه کرد.


اجرای Migrations جهت تشکیل ساختار بانک اطلاعاتی

اکنون که موجودیت‌ها، روابط بین آن‌ها و Context برنامه مشخص شدند، می‌توان با اجرای دستوارت زیر، سبب تولید کدهای Migration شد که با اجرای آن‌ها، بانک اطلاعاتی متناظری به صورت خودکار تولید می‌شود:
dotnet tool install --global dotnet-ef --version 3.1.6
dotnet tool update --global dotnet-ef --version 3.1.6
dotnet build
dotnet ef migrations add Init --context ApplicationDbContext
در نگارش EF Core 3x، نیاز است ابزار dotnet-ef را به صورت جداگانه‌ای دریافت و یا به روز رسانی کرد (دو دستور اول) و سپس دستور dotnet ef را اجرا نمود.


مقدار دهی اولیه‌ی بانک اطلاعاتی

سایت PostgreSQL Exercises به همراه فایل SQL ایجاد جداول و مقدار دهی اولیه‌ی آن‌ها نیز هست. شاید عنوان کنید که چرا این اطلاعات به صورت متدهای HasData، به تنظیمات موجودیت‌ها اضافه نشدند؟ علت آن به همان ID مساوی صفر بر می‌گردد! در حین استفاده‌ی از متد HasData نمی‌توانید ID ای داشته باشید که مقدار آن با مقدار پیش‌فرض آن نوع، یکی باشد. برای مثال مقدار پیش فرض int، مساوی صفر است. به همین جهت حتی با تنظیم UseIdentityColumn(seed: 0, increment: 1)، اجازه‌ی ثبت Id مساوی صفر را نمی‌دهد؛ چون نمی‌تواند تشخیص دهد که این مقدار، یک مقدار صریح است یا خیر (^). بنابراین مجبور هستیم تا آن‌ها را به صورت معمولی ثبت کنیم:
context.Facilities.Add(new Facility { Name = "Tennis Court 1", MemberCost = 5, GuestCost = 25, InitialOutlay = 10000, MonthlyMaintenance = 200 });
// مابقی موارد
context.SaveChanges();
در این حالت، اول رکورد ثبت شده، Id مساوی صفر را خواهد داشت و مابقی هم یکی یکی افزایش می‌یابند.
این روش برای ثبت اطلاعات Facilities و Booking کار می‌کند؛ اما ... چون Idهای کاربران پشت سر هم نیست و بین آن‌ها فاصله وجود دارد، دیگر نمی‌توان از روش فوق استفاده کرد و نیاز است بتوان مقدار Id را به صورت صریحی تعیین کرد که این مورد نکات جالبی را به همراه دارد:
- در حین کار با SQL Server نیاز است دستور SET IDENTITY_INSERT Members ON را در ابتدای کار، فراخوانی کرد تا بتوان مقدار فیلد ID خود افزایش دهنده را به صورت دستی مقدار دهی کرد.
- در هر زمان، فقط یک جدول و فقط یک سشن (یک اتصال) را می‌توان توسط IDENTITY_INSERT در حالت ثبت و مقدار دهی ID آن قرار داد.
- EF Core، به ازای هر batch اطلاعاتی که ثبت می‌کند، یکبار اتصال را باز و بسته می‌کند. این مورد سبب می‌شود که فراخوانی ExecuteSqlCommand با دستور یاد شده، تاثیری نداشته باشد. برای رفع این مشکل باید یک تراکنش را باز کرد، تا اتصال به بانک اطلاعاتی، در طول آن باز باقی بماند.

در اینجا برای ثبت کاربر با ID مساوی صفر، باز هم می‌توان به صورت معمولی عمل کرد:
context.Members.Add(new Member { ... });
context.SaveChanges(); // For id = 0 = Int's CLR Default Value!
چون اولین رکورد است، ID آن مساوی صفر خواهد شد. برای مابقی از روش ویژه‌ی زیر استفاده می‌کنیم:
using (var transaction = context.Database.BeginTransaction())
{
    try
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members ON");

        context.Members.Add(new Member { ... });
        // مابقی موارد

        context.SaveChanges();

        transaction.Commit();
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
    finally
    {
        context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members OFF");
    }
}
ابتدا یک تراکنش را بر روی context ایجاد می‌کنیم تا اتصال باز شده، در طول آن ثابت باقی بماند. اکنون اجرای دستور SET IDENTITY_INSERT، مؤثر واقع می‌شود. سپس تمام رکوردها را با ذکر ID صریح آن‌ها به context اضافه کرد، آن‌ها را ذخیره نموده و تراکنش را Commit می‌کنیم. در پایان کار هم باید دستور خاموش کردن SET IDENTITY_INSERT صادر شود.


کدهای کامل موجودیت‌های این قسمت به همراه تنظیمات آن‌ها
کدهای کامل تنظیم Context و همچنین مقدار دهی اولیه‌ی بانک اطلاعاتی