مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
در «قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی»، اطلاعات TestUser تنظیم شده‌ی در کلاس Config برنامه‌ی IDP را به بانک اطلاعاتی منتقل کردیم که در نتیجه‌ی آن سه جدول Users، UserClaims و UserLogins، تشکیل شدند. در اینجا می‌خواهیم سایر قسمت‌های کلاس Config را نیز به بانک اطلاعاتی منتقل کنیم.


تنظیم مجوز امضای توکن‌های IDP

namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
تا اینجا تنظیمات کلاس آغازین برنامه چنین شکلی را دارد که AddCustomUserStore آن‌را در قسمت دهم به آن افزودیم.
مرحله‌ی بعد، تغییر AddDeveloperSigningCredential به یک نمونه‌ی واقعی است. استفاده‌ی از روش فعلی آن چنین مشکلاتی را ایجاد می‌کند:
- اگر برنامه‌ی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا می‌کند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست می‌کند. یعنی با ری‌استارت شدن Application pool، کلیدهای مجوز امضای توکن‌ها تغییر می‌کنند که در نهایت سبب شکست اعتبارسنجی توکن‌های صادر شده‌ی توسط IDP می‌شوند.

بنابراین برای انتشار نهایی برنامه نمی‌توان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامه‌ی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده می‌کنیم. این فایل را از پوشه‌ی src\IDP\DNT.IDP\MakeCert نیز می‌توانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
 makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
در اینجا تاریخ شروع و پایان اعتبار مجوز ذکر شده‌اند. همچنین نتیجه‌ی آن به صورت خودکار در LocalMachine certificate store ذخیره می‌شود. به همین جهت اجرای آن نیاز به دسترسی admin را دارد.
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینه‌ی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحه‌ی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون می‌توانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:


در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.

چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفته‌است که باید این انتقال را انجام داد. در غیراینصورت می‌توان توسط آن توکن‌های صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینه‌ی All tasks -> export را انتخاب کنید. نکته‌ی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینه‌ی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمی‌کند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینه‌ی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.

پس از ایجاد مجوز امضای توکن‌ها و انتقال آن به Trusted Root Certification Authorities، نحوه‌ی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
        }

        private X509Certificate2 loadCertificateFromStore()
        {
            var thumbPrint = Configuration["CertificateThumbPrint"];
            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
                if (certCollection.Count == 0)
                {
                    throw new Exception("The specified certificate wasn't found.");
                }
                return certCollection[0];
            }
        }

        private X509Certificate2 loadCertificateFromFile()
        {
            // NOTE:
            // You should check out the identity of your application pool and make sure
            // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.
            var certificate = new X509Certificate2(
                fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", 
                    Configuration["X509Certificate:FileName"]),
                password: Configuration["X509Certificate:Password"],
                keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
                                 X509KeyStorageFlags.Exportable);
            return certificate;
        }
    }
}
متد کمکی loadCertificateFromStore، بر اساس thumbPrint مجوز تولید شده، آن‌را بارگذاری می‌کند. سپس این مجوز، توسط متد AddSigningCredential به IdentityServer معرفی خواهد شد و یا اگر فایل pfx ای را دارید، می‌توانید از متد loadCertificateFromFile استفاده کنید. این متد برای اینکه در IIS به درستی کار کند، نیاز است در خواص Application pool سایت IDP، گزینه‌ی Load user profile را انتخاب کرده باشید (مهم!).

پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آن‌را در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration
https://localhost:6001/.well-known/openid-configuration/jwks


انتقال سایر قسمت‌های فایل Config برنامه‌ی IDP به بانک اطلاعاتی

قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر داده‌های فایل Config، مانند Resources و Clients برنامه‌ی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامه‌های متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آن‌ها داخل فایل Config برنامه‌ی IDP، مشکلی را ایجاد نمی‌کند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامه‌ی IDP توسط یک صفحه‌ی مدیریتی داشته باشیم، نیاز است آن‌ها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر می‌کند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام این‌ها اکنون در حافظه ذخیره می‌شوند که با ری‌استارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفاده‌ی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمده‌ی این کار توسط خود تیم IdentityServer توسط بسته‌ی IdentityServer4.EntityFramework انجام شده‌است که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بسته‌ی نیوگت را مستقیما مورد استفاده قرار نمی‌دهیم. از این جهت که نیاز به 2 رشته‌ی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شده‌است و ترجیح می‌دهیم که اطلاعات آن‌را با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آن‌را از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases

انتقال موجودیت‌ها به پروژه‌ی DNT.IDP.DomainClasses

در این بسته‌ی دریافتی، در پوشه‌ی src\IdentityServer4.EntityFramework\Entities آن، کلاس‌های تعاریف موجودیت‌های متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایل‌ها را از این پروژه استخراج کرده و به پروژه‌ی DNT.IDP.DomainClasses در پوشه‌ی جدید IdentityServer4Entities اضافه می‌کنیم.
البته در این حالت پروژه‌ی DNT.IDP.DomainClasses نیاز به این وابستگی‌ها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" />
    <PackageReference Include="IdentityServer4" Version="2.2.0" />
  </ItemGroup>
</Project>
پس از این انتقال، فضاهای نام این کلاس‌ها را نیز اصلاح می‌کنیم؛ تا با پروژه‌ی جاری تطابق پیدا کنند.


انتقال تنظیمات روابط بین موجودیت‌ها، به پروژه‌ی DNT.IDP.DataLayer

در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بسته‌ی دریافتی، تعاریف تنظیمات این موجودیت‌ها به همراه نحوه‌ی برقراری ارتباطات بین آن‌ها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژه‌ی DNT.IDP.DataLayer اضافه می‌کنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کرده‌ایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایل‌های IEntityTypeConfiguration را معرفی می‌کنیم.


تعاریف DbSetهای متناظر با موجودیت‌های منتقل و تنظیم شده در پروژه‌ی DNT.IDP.DataLayer

پس از انتقال موجودیت‌ها و روابط بین آن‌ها، دو فایل DbContext را در این بسته‌ی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیت‌های تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار می‌دهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیت‌های ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار می‌دهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.

ما در اینجا DbSetهای هر دوی این DbContext‌ها را در ApplicationDbContext خود، خلاصه و ادغام می‌کنیم.


انتقال نگاشت‌های AutoMapper بسته‌ی دریافتی به پروژه‌ی جدید DNT.IDP.Mappings

در پوشه‌ی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشت‌های AutoMapper، برای تبدیلات بین موجودیت‌های برنامه و IdentityServer4.Models انجام شده‌است. کل محتویات این پوشه را به یک پروژه‌ی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آن‌را نیز اصلاح می‌کنیم.


انتقال src\IdentityServer4.EntityFramework\Options به پروژه‌ی DNT.IDP.Models

در پوشه‌ی Options بسته‌ی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینت‌ها است. نیازی به آن نداریم؛ چون زمانیکه موجودیت‌ها و تنظیمات آن‌ها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکن‌ها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دوره‌ای توکن‌های قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options
{
    public class OperationalStoreOptions
    {
        public bool EnableTokenCleanup { get; set; } = false;
        public int TokenCleanupInterval { get; set; } = 3600;
        public int TokenCleanupBatchSize { get; set; } = 100;
    }
}
از این تنظیمات در سرویس TokenCleanup استفاده می‌شود. به همین جهت همین سه مورد را به پروژه‌ی DNT.IDP.Models منتقل کرده و سپس بجای اینکه این کلاس را مستقیما در سرویس TokenCleanup تزریق کنیم، آن‌را از طریق سیستم Configuration و فایل appsettings.json به این سرویس تزریق می‌کنیم؛ به کمک سرویس توکار IOptions خود ASP.NET Core:
public TokenCleanup(
  IServiceProvider serviceProvider, 
  ILogger<TokenCleanup> logger, 
  IOptions<OperationalStoreOptions> options)
ج) Options\TableConfiguration.cs
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفته‌است. نیازی به آن نداریم.


انتقال سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP.Services

بسته‌ی دریافتی، شامل دو پوشه‌ی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویس‌های آن‌را تشکیل می‌دهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویس‌ها را نیز مستقیما از این پوشه‌ها به پروژه‌ی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بسته‌ی دریافتی در سرویس‌های آن، تعریف شده‌اند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایه‌ی سرویس قرار می‌گیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.


انتقال متدهای الحاقی معرفی سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP

پس از انتقال قسمت‌های مختلف IdentityServer4.EntityFramework به لایه‌های مختلف برنامه‌ی جاری، اکنون نیاز است سرویس‌های آن‌را به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظه‌ای کلاس آغازین برنامه‌ی IDP می‌شوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام می‌دهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژه‌ی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاس‌های تنظیمات را حذف کردیم و نیازی به آن‌ها نداریم، آن‌ها را نیز به طور کامل از این فایل حذف می‌کنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آن‌را از فایل appsettings.json دریافت می‌کنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بسته‌ی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفته‌اند. ما در اینجا آن‌ها را نیز با تک Context برنامه‌ی خود تعویض می‌کنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شده‌است. آن‌را نیز به لایه‌ی سرویس‌ها منتقل می‌کنیم. همچنین در امضای سازنده‌ی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شده‌ی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده می‌کنید.


افزودن متدهای الحاقی جدید به فایل آغازین برنامه‌ی IDP

پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژه‌ی DNT.IDP، اکنون نوبت به استفاده‌ی از آن است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddConfigurationStore()
                .AddOperationalStore();
به این ترتیب متدهای الحاقی جدید AddConfigurationStore و AddOperationalStore جهت معرفی محل‌های ذخیره سازی اطلاعات کاربران، منابع و توکن‌های IdentityServer مورد استفاده قرار می‌گیرند.


اجرای Migrations در پروژه‌ی DNT.IDP.DataLayer

پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیت‌ها و روابط جدید بین آن‌ها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشه‌ی src\IDP\DNT.IDP.DataLayer را اجرا می‌کنیم تا کلاس‌های Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا می‌کنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.


انتقال اطلاعات فایل درون حافظه‌ای Config، به بانک اطلاعاتی برنامه

تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمی‌کند. چون جداول کلاینت‌ها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظه‌ای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کرده‌ایم:
    public interface IConfigSeedDataService
    {
        void EnsureSeedDataForContext(
            IEnumerable<IdentityServer4.Models.Client> clients,
            IEnumerable<IdentityServer4.Models.ApiResource> apiResources,
            IEnumerable<IdentityServer4.Models.IdentityResource> identityResources);
    }
این سرویس به کمک اطلاعات Mappings مخصوص AutoMapper این پروژه، IdentityServer4.Models تعریف شده‌ی در کلاس Config درون حافظه‌ای را به موجودیت‌های اصلی DNT.IDP.DomainClasses.IdentityServer4Entities تبدیل و سپس در بانک اطلاعاتی ذخیره می‌کند.
برای استفاده‌ی از آن، به کلاس آغازین برنامه‌ی IDP مراجعه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            initializeDb(app);
            seedDb(app);
    // ...
        }
        
        private static void seedDb(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>();
                configSeedDataService.EnsureSeedDataForContext(
                    Config.GetClients(),
                    Config.GetApiResources(),
                    Config.GetIdentityResources()
                    );
            }
        }
در اینجا توسط متد seedDb، متدهای درون حافظه‌ای کلاس Config به سرویس ConfigSeedDataService ارسال شده، توسط Mappings تعریف شده، به معادل‌های موجودیت‌های برنامه تبدیل و سپس در بانک اطلاعاتی ذخیره می‌شوند.


آزمایش برنامه

پس از اعمال این تغییرات، برنامه‌ها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شده‌اند ملاحظه می‌کنید:


همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شده‌ی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:


و یا قسمت ذخیره سازی خودکار توکن‌های تولیدی توسط آن نیز به درستی کار می‌کند:





کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران
باسلام. اگر بخواهیم متدهای اعتبارسنجی را از مدل بگیرد و بصورت جاوااسکریپتی و قبل از مراجعه مجدد به سرور اعتبارسنجی را انجام دهد چکار باید بکنیم؟
[Required]
public string Title { get; set; }

مطالب
افزودن فایلهای تولیده شده پویا به صورت Bundle به سیستم Optimization
گاهی از اوقات نیاز است تا از یک محتوای پویا، برای تولید فایلهای CSS و اسکریپت‌های خود استفاده کنید. دلایل زیادی برای اینکار وجود دارند؛ همانند اسکریپت تولید شده در Signalr که بر اساس کلاس hub شما و متدهای پیاده سازی شده‌ی در آن تولید می‌شود. همچنین روش‌های زیادی برای تولید این محتوای پویا وجود دارد که یک نمونه‌ی آن در اینجا ذکر شده است.
قرار دادن این محتوای تولید شده در سیستم Bundling MVC به شکل مستقیم امکان پذیر نیست؛ زیرا این سیستم با فایل‌های استاتیک سر و کار دارد و افزودن یک url به آن مجاز نمی‌باشد. حال اگر در پروژه‌ی خود محتوای پویایی را تولید کرده و می‌خواهید از مزایای فشرده سازی سیستم Bundling بهره‌مند شوید، باید مراحل زیر را انجام دهید:
ابتدا متدی را برای دریافت محتوای تولید شده بنویسید. برای مثال برای دریافت محتوای تولیده شده‌ی فایل hubs.js می‌توانید از متد زیر استفاده کنید:
public static string GetSignalRContent()
{
    var resolver = new DefaultHubManager(new DefaultDependencyResolver());
    var proxy = new DefaultJavaScriptProxyGenerator(resolver, new NullJavaScriptMinifier());
    return proxy.GenerateProxy("/signalr");
}
سپس باید سیستم مسریابی پیش فرض سیستم Bundling را با سیستم مسیریابی سفارشی خود جایگزین کنید. کاری که باید انجام دهید اینست که در سیستم مسیریابی سفارشی خود چک کنید اگر مسیر درخواستی به مسیر مورد نظر شما اشاره دارد، مقدار true را برگشت دهید. در واقع در سیستم مسیریابی پیش فرض اگر فایلی بطور فیزیکی وجود نداشته باشد، مقدار برگشتی false خواهد بود.
همچنین در این سیستم مسیریابی سفارشی شما باید محتوای تولید شده را هم در اختیار داشته باشید. برای نمونه به کد زیر توجه کنید که ما از کلاس VirtualPathProvider، یک کلاس مشتق کرده و سیستم مسیریابی دلخواه خود را ایجاد می‌کنیم:

public class CustomVirtualPathProvider : VirtualPathProvider
{
    public CustomActionVirtualPathProvider(VirtualPathProvider virtualPathProvider)
    {
        // Wrap an existing virtual path provider
        VirtualPathProvider = virtualPathProvider;
    }

    protected VirtualPathProvider VirtualPathProvider { get; set; }

    public override string CombineVirtualPaths(string basePath, string relativePath)
    {
        return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath);
    }

    public override bool DirectoryExists(string virtualDir)
    {
        return VirtualPathProvider.DirectoryExists(virtualDir);
    }

    public override bool FileExists(string virtualPath)
    {
        if (virtualPath == "~/signalr/hubs")
        {
            return true;
        }

        return VirtualPathProvider.FileExists(virtualPath);
    }

    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        // BaseClass can't create a CacheDependency for your content, remove it
        // You could also add your own CacheDependency and aggregate it with the base dependency
        List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
        virtualPathDependenciesCopy.Remove("~/signalr/hubs");

        return VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy, utcStart);
    }

    public override string GetCacheKey(string virtualPath)
    {
        return VirtualPathProvider.GetCacheKey(virtualPath);
    }

    public override VirtualDirectory GetDirectory(string virtualDir)
    {
        return VirtualPathProvider.GetDirectory(virtualDir);
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        if (virtualPath == "~/signalr/hubs")
        {
            return new CustomVirtualFile(virtualPath,
                new MemoryStream(Encoding.Default.GetBytes(GetSignalRContent())));
        }

        return VirtualPathProvider.GetFile(virtualPath);
    }

    public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies)
    {
        return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies);
    }

    public override object InitializeLifetimeService()
    {
        return VirtualPathProvider.InitializeLifetimeService();
    }
}

public class CustomVirtualFile : VirtualFile
{
    public CustomVirtualFile (string virtualPath, Stream stream)
        : base(virtualPath)
    {
        Stream = stream;
    }

    public Stream Stream { get; private set; }

    public override Stream Open()
    {
         return Stream;
    }
}
ابتدا در بازنویسی متد FileExists باید چک کنیم اگر مسیر درخواستی به مسیر مورد نظر ما اشاره داشت، منطق خود را پیاده کنیم:
    public override bool FileExists(string virtualPath)
    {
        if (virtualPath == "~/signalr/hubs")
        {
            return true;
        }

        return VirtualPathProvider.FileExists(virtualPath);
    }
  در این متد اگر مسیر درخواستی مطابق مسیر مورد نظر ما بود باید true برگشت دهیم، در غیر اینصورت کار را به کلاس پایه می‌سپاریم.
همچنین باید توجه داشت که کلاس پایه، قادر به تولید CacheDependency  برای محتوای تولیدی شده ما نیست. بنابراین باید متد GetCacheDependency کلاس پایه‌ی ما بازنویسی و منطق مورد نظر را برای آن پیاده سازی کنیم. در این متد ابتدا مسیر مورد نظر خود را از لیست مسیرهایی که باید از CacheDependency استفاده کنند، حذف و سپس سناریوی خود را پیاده سازی می‌کنیم. در این مثال من از پیاده سازی آن خودداری کرده و فقط مسیر را از لیست مسیرها حذف کرده‌ام:
    public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList();
        virtualPathDependenciesCopy.Remove("~/signalr/hubs");

        return VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy, utcStart);
    }
سپس نوبت دریافت محتوای تولید شده‌ی پویا است که باید با بازنویسی متد GetFile منطق خود را اعمال کنیم:
    public override VirtualFile GetFile(string virtualPath)
    {
        if (virtualPath == "~/signalr/hubs")
        {
            return new CustomVirtualFile(virtualPath,
                new MemoryStream(Encoding.Default.GetBytes(GetSignalRContent())));
        }

        return VirtualPathProvider.GetFile(virtualPath);
    }
در این متد ابتدا چک می‌کنیم اگر مسیر درخواست شده به مسیر مورد نظر ما اشاره داشت، با استفاده از متد GetSignalRContent محتوای تولید شده‌ی پویا را دریافت و از طریق کلاس CustomVirtualFile که از کلاس VirtualFile مشتق کرده ایم، آن‌را باز می‌گردانیم. کار این کلاس هم فراهم کردن بستری برای اشیایی است که یک فایل فیزیکی را در قالب سیستم فایل مجازی بکار می‌گیرند. تنها نکته‌ی قابل توجه در این کلاس، بازنویسی متد Open کلاس پایه، برای بازگرداندن یک Stream فقط خواندنی از محتوای منبع مورد نظر ماست. از آنجا که منبع ما در اینجا به طور پویا تولید می‌شود، همانطور که دیدید ما در متد GetFile از یک MemoryStream استفاده کردیم؛ در صورتیکه اگر با یک فایل فیزیکی سر و کار داشتیم، ممکن بود از یک FileStream استفاده کنیم.
در نهایت باید کلاس سفارشی شده را با سیستم مسیریابی پیش فرض سیستم Bundling جایگزین کنیم:
public static void RegisterBundles(BundleCollection bundles)
{
    BundleTable.VirtualPathProvider =
        new CustomVirtualPathProvider(BundleTable.VirtualPathProvider);

    Bundle include = new Bundle("~/bundle")
        .Include("~/Content/static.js")
        .Include("~/signalr/hubs");

    bundles.Add(include);
}
مطالب
تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core
ASP.NET Core به همراه زیر ساختی‌است جهت خارج کردن امکانات Caching درون حافظه‌ای آن از سرور جاری و انتقال آن به سرورهای دیگر جهت کاهش بار سرور و برنامه. این کش توزیع شده را می‌توان به عنوان زیرساختی برای مدیریت سشن‌ها، مدیریت اطلاعات کش و همچنین مدیریت کوکی‌های حجیم ASP.NET Core Identity نیز بکار گرفت. برای مثال بجای ارسال یک کوکی حجیم بالای 5 کیلوبایت به کلاینت، فقط ID رمزنگاری شده‌ی آن‌را ارسال کرد و اصل کوکی را در داخل دیتابیس ذخیره و بازیابی نمود. این مساله هم مقیاس پذیری برنامه را افزایش خواهد داد و هم امنیت آن‌را با عدم ارسال اصل محتوای کوکی به سمت کلاینت‌ها و یا ذخیره سازی اطلاعات سشن‌ها در بانک اطلاعاتی، مشکلات راه اندازی مجدد برنامه را به طور کامل برطرف می‌کنند و در این حالت بازیابی Application pool و یا کرش برنامه و یا ری استارت شدن کل سرور، سبب از بین رفتن سشن‌های کاربران نخواهند شد. بنابراین آشنایی با نحوه‌ی راه اندازی این امکانات، حداقل از بعد امنیتی بسیار مفید هستند؛ حتی اگر سرور ذخیره کننده‌ی این اطلاعات، همان سرور و بانک اطلاعاتی اصلی برنامه باشند.


پیشنیازهای کار با کش توزیع شده‌ی مبتنی بر SQL Server

برای کار با کش توزیع شده‌ی با قابلیت ذخیره سازی در یک بانک اطلاعاتی SQL Server، نیاز است دو بسته‌ی ذیل را به فایل project.json برنامه اضافه کرد:
{
    "dependencies": {
        "Microsoft.Extensions.Caching.SqlServer": "1.1.0"
    },
    "tools": {
        "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
    }
}
وابستگی که در قسمت dependencies ذکر شده‌است، کلاس‌های اصلی کار با کش توزیع شده را به برنامه اضافه می‌کند. ذکر وابستگی قسمت tools، اختیاری است و کار آن، ایجاد جدول مورد نیاز برای ذخیره سازی اطلاعات، در یک بانک اطلاعاتی SQL Server می‌باشد.


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار sql-cache

پس از افزودن و بازیابی ارجاعات فوق، با استفاده از خط فرمان، به پوشه‌ی جاری برنامه وارد شده و دستور ذیل را صادر کنید:
 dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;" "dbo" "AppSqlCache"
توضیحات:
- در اینجا می‌توان هر نوع رشته‌ی اتصالی معتبری را به انواع و اقسام بانک‌های SQL Server ذکر کرد. برای نمونه در مثال فوق این رشته‌ی اتصالی به یک بانک اطلاعاتی از پیش ایجاد شده‌ی LocalDB اشاره می‌کند. نام دلخواه این بانک اطلاعاتی در اینجا sql_cache ذکر گردیده و نام دلخواه جدولی که قرار است این اطلاعات را ثبت کند AppSqlCache تنظیم شده‌است و dbo، نام اسکیمای جدول است:


در اینجا تصویر ساختار جدولی را که توسط ابزار dotnet sql-cache ایجاد شده‌است، مشاهده می‌کنید. اگر خواستید این جدول را خودتان دستی ایجاد کنید، یک چنین کوئری را باید بر روی دیتابیس مدنظرتان اجرا نمائید:
CREATE TABLE AppSqlCache (
    Id                         NVARCHAR (449)  COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    Value                      VARBINARY (MAX) NOT NULL,
    ExpiresAtTime              DATETIMEOFFSET  NOT NULL,
    SlidingExpirationInSeconds BIGINT          NULL,
    AbsoluteExpiration         DATETIMEOFFSET  NULL,
    CONSTRAINT pk_Id PRIMARY KEY (Id)
);

CREATE NONCLUSTERED INDEX Index_ExpiresAtTime
    ON AppSqlCache(ExpiresAtTime);


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار Migrations در EF Core

زیر ساخت کش توزیع شده‌ی مبتنی بر SQL Server هیچگونه وابستگی به EF Core ندارد و تمام اجزای آن توسط Async ADO.NET نوشته شده‌اند. اما اگر خواستید قسمت ایجاد جدول مورد نیاز آن‌را به ابزار Migrations در EF Core واگذار کنید، روش کار به صورت زیر است:
- ابتدا یک کلاس دلخواه جدید را با محتوای ذیل ایجاد کنید:
    public class AppSqlCache
    {
        public string Id { get; set; }
        public byte[] Value { get; set; }
        public DateTimeOffset ExpiresAtTime { get; set; }
        public long? SlidingExpirationInSeconds { get; set; }
        public DateTimeOffset? AbsoluteExpiration { get; set; }
    }
- سپس تنظیمات ایجاد جدول متناظر با آن را به نحو ذیل تنظیم نمائید:
    public class MyDBDataContext : DbContext
    {
        public virtual DbSet<AppSqlCache> AppSqlCache { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<AppSqlCache>(entity =>
            {
                entity.ToTable(name: "AppSqlCache", schema: "dbo");
                entity.HasIndex(e => e.ExpiresAtTime).HasName("Index_ExpiresAtTime");
                entity.Property(e => e.Id).HasMaxLength(449);
                entity.Property(e => e.Value).IsRequired();
            });
        }
    }
به این ترتیب این جدول جدید به صورت خودکار در کنار سایر جداول برنامه ایجاد خواهند شد.
البته این مورد به شرطی است که بخواهید از یک دیتابیس، هم برای برنامه و هم برای ذخیره سازی اطلاعات کش استفاده کنید.


معرفی تنظیمات رشته‌ی اتصالی و نام جدول ذخیره سازی اطلاعات کش به برنامه

پس از ایجاد جدول مورد نیاز جهت ذخیره سازی اطلاعات کش، اکنون نیاز است این اطلاعات را به برنامه معرفی کرد. برای این منظور به کلاس آغازین برنامه مراجعه کرده و متد الحاقی AddDistributedSqlServerCache را بر روی مجموعه‌ی سرویس‌های موجود فراخوانی کنید؛ تا سرویس‌های این کش توزیع شده نیز به برنامه معرفی شوند:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;";
        options.SchemaName = "dbo";
        options.TableName = "AppSqlCache";
    });
باتوجه به توزیع شده بودن این کش، هیچ الزامی ندارد که ConnectionString ذکر شده‌ی در اینجا با رشته‌ی اتصالی مورد استفاده‌ی توسط EF Core یکی باشد (هرچند مشکلی هم ندارد).


آزمایش کش توزیع شده‌ی تنظیمی با فعال سازی سشن‌ها

سشن‌ها را همانند نکات ذکر شده‌ی در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 16 - کار با Sessions» فعال کنید و سپس مقداری را در آن بنویسید:
public IActionResult Index()
{
   HttpContext.Session.SetString("User", "VahidN");
   return Json(true);
}

public IActionResult About()
{
   var userContent = HttpContext.Session.GetString("User");
   return Json(userContent);
}
اکنون از جدول AppSqlCache کوئری بگیرید:


همانطور که مشاهده می‌کنید، سیستم سشن اینبار بجای حافظه، به صورت خودکار از جدول بانک اطلاعاتی SQL Server تنظیم شده‌، برای ذخیره سازی اطلاعات خود استفاده کرده‌است.


کار با کش توزیع شده از طریق برنامه نویسی

همانطور که در مقدمه‌ی بحث نیز عنوان شد، استفاده‌ی از زیر ساخت کش توزیع شده منحصر به استفاده‌ی از آن جهت ذخیره سازی اطلاعات سشن‌ها نیست و از آن می‌توان جهت انواع و اقسام سناریوهای مختلف مورد نیاز استفاده کرد. در این حالت روش دسترسی به این زیر ساخت، از طریق اینترفیس IDistributedCache است. زمانیکه متد AddDistributedSqlServerCache را فراخوانی می‌کنیم، در حقیقت کار ثبت یک چنین سرویسی به صورت خودکار انجام خواهد شد:
 services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>());
به عبارتی کلاس SqlServerCache به صورت singleton به مجموعه‌ی سرویس‌های برنامه اضافه شده‌است و برای دسترسی به آن تنها کافی است اینترفیس IDistributedCache را به کنترلرها و یا سرویس‌های برنامه تزریق و از امکانات آن استفاده کنیم.

در اینجا یک نمونه از این تزریق وابستگی و سپس استفاده‌ی از متدهای Set و Get اینترفیس IDistributedCache را مشاهده می‌کنید:
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class CacheTestController : Controller
    {
        readonly IDistributedCache _cache;
        public CacheTestController(IDistributedCache cache)
        {
            _cache = cache;
        }
 
        public IActionResult SetCacheData()
        {
            var time = DateTime.Now.ToLocalTime().ToString();
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddYears(1)
 
            };
            _cache.Set("Time", Encoding.UTF8.GetBytes(time), cacheOptions);
            return View();
        }
 
        public IActionResult GetCacheData()
        {
            var time = Encoding.UTF8.GetString(_cache.Get("Time"));
            ViewBag.data = time;
            return View();
        }
 
        public bool RemoveCacheData()
        {
            _cache.Remove("Time");
            return true;
        }
    }
}
در ابتدای بحث که ساختار جدول ذخیره سازی اطلاعات کش را بررسی کردیم، فیلد value آن یک چنین نوعی را دارد:
  Value  VARBINARY (MAX) NOT NULL,
که در سمت کدهای دات نتی، به شکل آرایه‌ای از بایت‌ها قابل بیان است.
  public byte[] Value { get; set; }
به همین جهت متد Set آن مقدار مدنظر را به صورت آرایه‌ای از بایت‌ها قبول می‌کند.
در این حالت اگر برنامه را اجرا و مسیر http://localhost:7742/CacheTest/SetCacheData را فراخوانی کنیم، اطلاعات ذخیره شده‌ی با کلید Test را می‌توان در بانک اطلاعاتی مشاهده کرد:



Tag helper مخصوص کش توزیع شده

در ASP.NET Core، می‌توان از یک Tag Helper جدید به نام distributed-cache برای کش سمت سرور توزیع شده‌ی محتوای قسمتی از یک View به نحو ذیل استفاده کرد:
<distributed-cache name="MyCacheItem2" expires-sliding="TimeSpan.FromMinutes(30)">
    <p>From distributed-cache</p>
    @DateTime.Now.ToString()
</distributed-cache>
که اطلاعات آن در بانک اطلاعاتی به نحو ذیل ذخیره می‌شود:


در اینجا name به صورت هش شده به صورت کلید کش مورد استفاده قرار می‌گیرد. سپس محتوای تگ distributed-cache رندر شده، تبدیل به آرایه‌ای از بایت‌ها گردیده و در بانک اطلاعاتی ذخیره می‌گردد.
ذکر name در اینجا اجباری است و باید دقت داشت که چون به عنوان کلید بازیابی کش مورد استفاده قرار خواهد گرفت، نباید به اشتباه در قسمت‌های دیگر برنامه با همین نام وارد شود. در غیر اینصورت دو قسمتی که name یکسانی داشته باشند، یک محتوا را نمایش خواهند داد.
مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در 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;
            }
//...

 
نظرات مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft
با کد زیر
    public class Book
    {
        public string Title { get; set; }
        public int Id { get; set; }
        public Author Author { get; set; }
        public Publisher Publisher { get; set; }

        public IList<Book> GetBooks()
        {
            var books = new List<Book>();

            for (int i = 0; i < 10; i++)
            {
                var book = new Book()
                {
                    Id = i,
                    Title = $"Title {i}",
                    Author = new Author()
                    {
                        FirstName = $"Author {i} First Name",
                        LastName = $"Author {i} Last Name",
                        Person = new Person()
                        {
                            Type = $"Type {i}",
                            Value = $"Value {i}"
                        }
                    },
                    Publisher = new Report.Publisher()
                    {
                        Name = $"Publiser {i}"
                    }
                };
                books.Add(book);
            }
            return books;
        }
    }

    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Person Person { get; set; }
    }

    public class Person
    {
        public string Type { get; set; }
        public string Value { get; set; }
    }

    public class Publisher
    {
        public string Name { get; set; }
    }
و فایل mrt زیر مشکلی دیده نشد.
نظرات مطالب
T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
با سلام.
من T4MVC را بر روی پروژه اصلی قرار دادم.
فایل‌های resource من در یک پروژه جدا قرار دارند. مدلهای پروژه من هم در یک پروژه جدا قرار دارند. حال برای تعریف خاصیت اعتبارسنجی روی یکی از خاصیت‌های ویومدل آیا می‌شود T4MVC را بکار برد؟ (بجای استفاده از magic string)
 [StringLength(25, ErrorMessageResourceType = typeof(ValidationErrorsResource), ErrorMessageResourceName = "InvalidStringLength")]
public string FatherName { get; set; }

نظرات اشتراک‌ها
اعتبارسنجی Domain Model‎
با توجه به مطلب «طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer» می‌توان طراحی زیر را انجام داد:
public class Customer
{
    public string Email { get; private set; }
    public string Name { get; private set; }

    private Customer(email, name)
    {
        Email = email;
        Name = name;
    }

    public static Result<Customer> New(string email, string name, INewCustomerPolicy policy)
    {
        var isUnique = policy.IsUnique(email);
        if (!isUnique)
        {
            return Result.Fail<Customer>("Customer with this email already exists.");
        }

        var customer = new Customer(email, name);

        //customer.AddDomainEvent(new CustomerRegistered(customer));

        return Result.Ok(customer);
    }
}
کلاس Result در زیرساخت DNTFrameworkCore قابل دسترس می‌باشد.