مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
در مطلب قبلی «استفاده از تامین کننده‌های هویت خارجی»، نحوه‌ی استفاده از اکانت‌های ویندوزی کاربران یک شبکه، به عنوان یک تامین کننده‌ی هویت خارجی بررسی شد. در ادامه می‌خواهیم از اطلاعات اکانت گوگل و IDP مبتنی بر OAuth 2.0 آن به عنوان یک تامین کننده‌ی هویت خارجی دیگر استفاده کنیم.


ثبت یک برنامه‌ی جدید در گوگل

اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامه‌ی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:

1- مراجعه به developer console گوگل و ایجاد یک پروژه‌ی جدید
https://console.developers.google.com
در صفحه‌ی باز شده، بر روی دکمه‌ی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمه‌ی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحه‌ی زیر ختم می‌شوند:


در اینجا نامی دلخواه را وارد کرده و بر روی دکمه‌ی create کلیک کنید.

2- فعالسازی API بر روی این پروژه‌ی جدید


در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحه‌ی بعدی، آن‌را با کلیک بر روی دکمه‌ی enable، فعال کنید.

3- ایجاد credentials


در اینجا بر روی دکمه‌ی create credentials کلیک کرده و در صفحه‌ی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API
• Where will you be calling the API from? – Web server (e.g. node.js, Tomcat)
• What data will you be accessing? – User data
سپس در ذیل این صفحه بر روی دکمه‌ی «What credentials do I need» کلیک کنید تا به صفحه‌ی پس از آن هدایت شوید. اینجا است که مشخصات کلاینت OAuth 2.0 تکمیل می‌شوند. در این صفحه، سه گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
• نام: همان مقدار پیش‌فرض آن
• Authorized JavaScript origins: آن‌را خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آن‌را با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
این آدرس، به آدرس IDP لوکال ما اشاره می‌کند و مسیر signin-google/ آن باید به همین نحو تنظیم شود تا توسط برنامه شناسایی شود.
سپس در ذیل این صفحه بر روی دکمه‌ی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحه‌ی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینه‌ی آن‌را به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامه‌ی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمه‌ی Continue کلیک نمائید.

4- دریافت credentials
در پایان این گردش کاری، به صفحه‌ی نهایی «Download credentials» می‌رسیم. در اینجا بر روی دکمه‌ی download کلیک کنید تا ClientId  و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمه‌ی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.


تنظیم برنامه‌ی IDP برای استفاده‌ی از محتویات فایل client_id.json

پس از پایان عملیات ایجاد یک برنامه‌ی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت می‌کنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامه‌ی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{
  "Authentication": {
    "Google": {
      "ClientId": "xxxx",
      "ClientSecret": "xxxx"
    }
  }
}
در اینجا مقادیر خواص client_secret و client_id موجود در فایل client_id.json دریافت شده‌ی از گوگل را به صورت فوق به فایل appsettings.json اضافه می‌کنیم.
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
   // ... 

            services.AddAuthentication()
                .AddGoogle(authenticationScheme: "Google", configureOptions: options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.ClientId = Configuration["Authentication:Google:ClientId"];
                    options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                });
        }
خود ASP.NET Core از تعریف اطلاعات اکانت‌های Google, Facebook, Twitter, Microsoft Account و  OpenID Connect پشتیبانی می‌کند که در اینجا نحوه‌ی تنظیم اکانت گوگل آن‌را مشاهده می‌کنید.
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
 public const string ExternalCookieAuthenticationScheme = "idsrv.external";
از این نام برای تشکیل قسمتی از نام کوکی که اطلاعات اعتبارسنجی گوگل در آن ذخیره می‌شود، کمک گرفته خواهد شد.


آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آن‌ها

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


در اینجا با کلیک بر روی دکمه‌ی گوگل، به صفحه‌ی لاگین آن که به همراه نام برنامه‌ی ما است و انتخاب اکانتی از آن هدایت می‌شویم:


پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخ‌داده، ما را به سمت صفحه‌ی ثبت اطلاعات کاربر جدید هدایت می‌کند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class ExternalController : Controller
    {
        public async Task<IActionResult> Callback()
        {
            var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
            var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user = AutoProvisionUser(provider, providerUserId, claims);
                
                var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl });
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" ,
                    new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId });
                return Redirect(continueWithUrl);
            }
اگر خروجی متد FindUserFromExternalProvider آن null باشد، یعنی کاربری که از سمت تامین کننده‌ی هویت خارجی/گوگل به برنامه‌ی ما وارد شده‌است، دارای اکانتی در سمت IDP نیست. به همین جهت او را به سمت صفحه‌ی ثبت نام کاربر هدایت می‌کنیم.
در اینجا نحوه‌ی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحه‌ی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده می‌کنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره می‌کند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد می‌شود.


اتصال کاربر وارد شده‌ی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است

تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحه‌ی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد می‌کنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت می‌شود، دیگر صفحه‌ی ثبت نام و تکمیل اطلاعات را مشاهده نمی‌کند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شده‌است، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفته‌است. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);  
foreach (var claim in claims)
{
   _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}");
}
در اینجا پس از فراخوانی FindUserFromExternalProvider، لیست Claims بازگشت داده شده‌ی توسط IDP خارجی را لاگ می‌کنیم که در حالت استفاده‌ی از گوگل چنین خروجی را دارد:
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N.
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N.
External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
بنابراین اگر بخواهیم بر اساس این claims بازگشتی از گوگل، کاربر جاری در بانک اطلاعاتی خود را بیابیم، فقط کافی است اطلاعات claim مخصوص emailaddress آن‌را مورد استفاده قرار دهیم:
        [HttpGet]
        public async Task<IActionResult> Callback()
        {
            // ...

            var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
            if (user == null)
            {
                // user wasn't found by provider, but maybe one exists with the same email address?  
                if (provider == "Google")
                {
                    // email claim from Google
                    var email = claims.FirstOrDefault(c =>
                        c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
                    if (email != null)
                    {
                        var userByEmail = await _usersService.GetUserByEmailAsync(email.Value);
                        if (userByEmail != null)
                        {
                            // add Google as a provider for this user
                            await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId);

                            // redirect to ExternalLoginCallback
                            var continueWithUrlAfterAddingUserLogin =
                                Url.Action("Callback", new {returnUrl = returnUrl});
                            return Redirect(continueWithUrlAfterAddingUserLogin);
                        }
                    }
                }


                var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl});
                var continueWithUrl = Url.Action("RegisterUser", "UserRegistration",
                    new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId});
                return Redirect(continueWithUrl);
            }
در اینجا ابتدا بررسی شده‌است که آیا کاربر جاری واکشی شده‌ی از بانک اطلاعاتی نال است؟ اگر بله، اینبار بجای هدایت مستقیم او به صفحه‌ی ثبت کاربر و تکمیل مشخصات او، مقدار email این کاربر را از لیست claims بازگشتی او از طرف گوگل، استخراج می‌کنیم. سپس بر این اساس اگر کاربری در بانک اطلاعاتی وجود داشت، تنها اطلاعات تکمیلی UserLogin او را که در اینجا خالی است، به اکانت گوگل او متصل می‌کنیم. به این ترتیب دیگر کاربر نیازی نخواهد داشت تا به صفحه‌ی ثبت اطلاعات تکمیلی هدایت شود و یا اینکه بی‌جهت رکورد User جدیدی را مخصوص او به بانک اطلاعاتی اضافه کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
اشتراک‌ها
Bootstrap v4.6.0 منتشر شد

Bootstrap v4.6.0 is here with a couple new features, several bugfixes, and some awesome documentation updates to make v4 more maintainable alongside our development of v5. 

Bootstrap v4.6.0 منتشر شد
مطالب
مدیریت ساده‌تر امور Async (غیر همزمان) در نسخه‌ی بعدی زبان‌های دات نتی

چندی قبل مطلبی را در این سایت در مورد معرفی الگویی که توسط آن می‌توان اعمال غیر همزمان را به صورت پی در پی انجام داد، مطالعه کردید:

و بحث اصلی مطالب فوق هم این است:
"در برنامه نویسی متداول همیشه عادت داریم که اعمال به صورت A –> B –> C انجام شوند. اما در Async programming ممکن است ابتدا C انجام شود، سپس A و بعد B یا هر حالت دیگری صرفنظر از تقدم و تاخر آن‌ها در حین معرفی متدهای مرتبط در یک قطعه کد. همچنین میزان خوانایی این نوع کدنویسی نیز مطلوب نیست...."

خبر خوش آن است که پشتیبانی از این نوع مدل پی در پی برنامه نویسی در نگارش‌های بعدی سی شارپ و VB.NET اضافه شده است.



لیستی از مقالات منتشر شده در این مورد را در ادامه ملاحظه خواهید کرد:

علاوه بر آن یک سری ویدیوی مرتبط با این بحث نیز منتشر شده است:

اشتراک‌ها
نسخه RTM برای 1.0 ASP.NET Core ارایه شد

We are excited to announce the release of .NET Core 1.0, ASP.NET Core 1.0 and Entity Framework 1.0, available on Windows, OS X and Linux! .NET Core is a cross-platform, open source, and modular .NET platform for creating modern web apps, microservices, libraries and console applications.

This release includes the .NET Core runtime, libraries and tools and the ASP.NET Core libraries. We are also releasing Visual Studio and Visual Studio Code extensions that enable you to create .NET Core projects. You can get started at https://dot.net/core. Read the release notes for detailed release information. 

نسخه RTM برای 1.0 ASP.NET Core ارایه شد
مطالب
معماری module based در TypeScript قسمت اول
در صورت استفاده از TypeScript، قطعا با module‌ها و هدف استفاده‌ی از آن‌ها آشنایی دارید. در این مقاله می‌خواهیم با متداول‌ترین روش‌های بسته بندی آن‌ها آشنا شده و به صورت عملیاتی آن را پیاده نماییم. اولین روش commonjs میباشد. از آنجایی که این روش بیشتر برای برنامه‌های خارج از مرورگر میباشد، به همین قدر معرفی آن بسنده میکنیم. اما دو روش مهم دیگری که در typeScript برای ماژول‌ها اهمیت فراوانی دارند:

1) AMD یا Asynchronous Module Definition
2) در این روش از امکانات توکار کامپایلر typescript برای combine کردن فایل‌ها استفاده میشود

ابتدا AMD را بررسی مینماییم
این روش بدین صورت عمل میکند که هر ماژول، در صورتیکه بطور صریح نیاز به ماژول دیگری داشته باشد، آن را import کرده و به صورت async آن ماژول درخواستی را دریافت مینماید. بهترین ابزار برای انجام اینکار requirejs میباشد و در وبسایت رسمی آن، خودش را اینگونه معرفی کرده است:
RequireJS is a JavaScript file and module loader ، ما عملیات load کردن فایل‌ها را به requirejs واگذار میکنیم.
حال میخواهیم به پیاده سازی AMD با استفاده از typescript و البته requirejs بپردازیم.
بنده برای اینکار از IDE محبوب خود visual studio استفاده خواهم کرد. قطعا شما از IDE/Text editor مورد نظر خود میتوانید استفاده نمایید (البته با کمی تغییرات جزیی در محتوای آموزشی این مقاله).
ابتدا یک پروژه‌ی asp.net از نوع empty را بدون هیچ وابستگی ایجاد کرده و index.html را بدان اضافه مینماییم:

نام پروژه‌ی من AMD و فایل index.html بدان اضافه شده است. فرض کنید یک پوشه‌ی جدید را به نام modules به آن اضافه میکنیم و در آن دو فایل typescript ی را به نام‌های module1.ts و module2.ts، اضافه میکنیم.

محتویات module1 را اینگونه مینویسیم:

export module module1 {
    export abstract class firstCls {
        static f1(str: string) {
            console.log(str);
        }
    }
}

و همچنین module2 به شکل زیر خواهد بود:

import * as Amd from 'module1';

module module2 {
    export class secondCls {
        f2(str: string) {
            Amd.module1.firstCls.f1(str);
        }
    }
}

new module2.secondCls().f2(`amd work's`);

(دقت کنید بعد از کامپایل شدن لفظ import تبدیل به define میشود)

از طریق add - new item فایل tsconfig.json را به مسیر اصلی پروژه اضافه کنید. در صورتی که آن را پیدا نکردید، به صورت دستی فایل آن را ساخته و محتویات زیر را در آن کپی نمایید:

{
  "compilerOptions": {
    "noImplicitAny": false,
    "noEmitOnError": true,
    "removeComments": false,
    "sourceMap": false,
    "target": "es6",
    "module": "amd"
  },
  "exclude": [
    "node_modules"
  ]
}
exclude مسیرهایی هستند که قرار نیست کامپایل شوند. target نوع کد تولید شده را مشخص مینماید (در صورتیکه مرورگر شما از es6 پشتیبانی نمیکند، میتوانید target را به es5 تغییر دهید) و module نیز از نوع amd تعریف میشود. کاری که قرار است انجام دهیم این است که به محض فراخوانی module2 به صورت async ، ماژول module1 را load کرده تا بتوان از آن بهره برد. متاسفانه در حال حاضر هیچ مرورگری به صورت توکار این ویژگی را پشتیبانی نمیکند! به همین دلیل فعلا مجبور به استفاده از ابزار‌های third party همچون requirejs هستیم.

در ادامه به root پروژه رفته و دستور npm init را ارسال کرده تا فایل package.json تولید شود. همچنین برای requirejs نیز دستور زیر را ارسال مینماییم:

npm install requirejs --save-dev

حال requirejs به پروژه‌ی شما اضافه شده است.


برای مدیریت کردن فراخوانی initial module در پوشه‌ی modules که قبلا ساخته بودیم فایل main.js راساخته و کد‌های زیر را بدان اضافه مینماییم.

(لازم به ذکر است این فایل را میتوانیم با استفاده از typescript نوشته و  requirejs definitely typed  را به پروژه اضافه کرده و از مزایای intellisense بودن بهره ببریم)

کد‌های زیر را درون main.js مینویسیم:

require(['modules/module2.js'], modules_module2());

function modules_module2() {
    //additionals config goes here
}

از آنجاییکه ممکن است تعداد وابستگی فایل‌ها زیاد باشد و ترتیب load شدن آن‌ها نیز اهمیت داشته باشد، در این قسمت میتوان configهای بیشتری را همچون sequence در load کردن فایل‌ها، تعریف کرد که میتوانید در وبسایت رسمی requirejs آن را مطالعه بفرمایید.


حال فایل index.html را باز کرده و config برای فراخوانی requirejs, main.js را مینویسیم؛ به صورت زیر:

<h1>Hello requirejs and amd modules</h1>

<!--src means require js address-->
<!--data-main means initial require config-->

<script src="node_modules/requirejs/require.js" data-main="modules/main.js"></script>

<script></script>

پر واضح است که src آدرس فایل require.js و همچنین data-main آدرس initial require config پروژه را مشخص می‌کند.

اکنون پروژه را run کرده و می‌بینید که فایل‌های مورد نیاز به صورت async برای ما load میشوند. اگر از مرورگر کروم استفاده می‌نمایید، بدین صورت میتوانید network و همچنین console را مشاهد نمایید:


مشاهد میکنید که فایل‌های مورد نیاز load شده‌اند و همچنین amd work's در console نمایش داده شده است.

requirejs بدین صورت عمل میکند: بعد از یافتن هر فایل، با استفاده از regex کل فایل را بررسی کرده و به دنبال وابستگی‌های آن فایل میگردد (منظور همان import‌ها میباشد و آن فایل به صورت async لود میشود) و در فایل‌های بعدی نیز همین روال ادامه خواهد یافت. هر چند راهکارهایی برای بهبود کارآیی در آن اندیشیده شده است؛ بدین صورت که اساس کارش با استفاده از singleton میباشد و بعد از require کردن فایلی، هر دفعه که فراخوانی میشود، نیاز به پردازش مجدد ندارد. با این وجود ممکن است در بعضی مواقع و مخصوصا با اشتباهات سهوی برنامه نویسان از کارآیی نرم افزار مطبوع شما بکاهد.


کد‌های این برنامه را میتوانید از اینجا دریافت نمایید (ضمن اینکه وابستگی‌های اضافه‌تری مانند پوشه‌ی node_modules حذف شده‌اند؛ بنابراین npm install فراموش نشود)

دانلود AMD.zip  


در قسمت بعد به امکانات توکار کامپایلر typescript برای معماری ماژول‌ها میپردازیم

مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کرده‌اید .
یک مثال دیگر آن می‌تواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمی‌کند؛ اما ... زمانیکه نوبت به نمایش آن فرا می‌رسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace EFGeneral
{
    public class BlogComment
    {
        public int Id { set; get; }

        [MaxLength]
        public string Body { set; get; }

        public virtual BlogComment Reply { set; get; }
        public int? ReplyId { get; set; }
        public ICollection<BlogComment> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<BlogComment> BlogComments { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<BlogComment>()
                        .HasOptional(x => x.Reply)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ReplyId)
                        .WillCascadeOnDelete(false);

            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var comment1 = new BlogComment { Body = "نظر من این است که" };
            var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 };
            var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 };

            context.BlogComments.Add(comment121);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var list = ctx.BlogComments
                          //.where ...
                          .ToList() // fills the childs list too
                          .Where(x => x.Reply == null) // for TreeViewHelper                        
                          .ToList();

                if (list.Any())
                {

                }
            }
        }
    }
}

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


یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.

اولین نکته اضافه‌‌تری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر می‌باشد:
    public class BlogComment
    {
        // ... 
        public ICollection<BlogComment> Children { get; set; }
    }
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر می‌گردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب می‌شود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت می‌شود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشه‌ها فیلتر می‌شوند (مواردی که reply آن‌ها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:


سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آن‌ها در یک برنامه ASP.NET MVC می‌توان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty)
        {
            var children = childrenProperty(parentItem);
            if (children == null || !children.Any())
            {
                return;
            }
//...

 
اشتراک‌ها
معرفی Stack only json deserialization

The StackOnlyJsonParser combines the System.Text.Json library with C# 9 code generators to allow for fast and GC-friendly JSON deserialization. 

معرفی Stack only json deserialization
اشتراک‌ها
ASP.NET AJAX Control Toolkit v19.1.0 منتشر شد

A few years ago, we took over maintenance and guidance for the ASP.NET AJAX Control Toolkit project. Please refer to this blog post for more information on the project and why we stepped in to assist.
As part of our ongoing commitment to the project, we’ve released an update (ASP.NET AJAX Control Toolkit v19.1.0) to address the following issues.
Improvements
- Visual Studio 2019 Support
- Security hardening: HTML-encode file names in AjaxFileUpload (#483)
 

ASP.NET AJAX Control Toolkit v19.1.0 منتشر شد