نظرات مطالب
خودکارسازی فرآیند نگاشت اشیاء در AutoMapper
سلام؛ با چه  روشی می‌توان وقتی که اطلاعات را از دیتابیس فراخوانی می‌کنم، AddressViewModel داخل SupplierViewModel از نگاشت  <CreateMap<Address, AddressViewModel استفاده کند.
public class SupplierViewModel
{  
   public string name { get; set; }   
   public AddressViewModel AddressViewModel { get; set; }=new AddressViewModel();    }

public class AddressViewModel
{
   public string phone_mobile { get; set; }
   public string address1 { get; set; }
}

public class Supplier 
{      
   public int Id { get; set; }   
   public string name { get; set; } 
   public virtual Address Address { get; set; } 
} 

CreateMap<Supplier, SupplierViewModel>().IgnoreAllUnmapped()  });
CreateMap<Address, AddressViewModel>()
var viewModel =
         await  _suppliers.AsNoTracking()
                   .ProjectTo<SupplierViewModel>(parameters:null,configuration: _mappingEngine.ConfigurationProvider)
                   .FirstOrDefaultAsync(a => a.Id == id);
مطالب
کاهش پیچیدگی؛ قسمت اول: الگوی مورد خاص (Special Case Pattern)
مهمترین دستاورد  الگوی شیء نال ( Null Object Pattern ) این است که جریان کنترل (branch ) برای شاخه مثبت و منفی یکسان است و هیچگونه انشعاب شرطی بر اساس آزمون‌های null وجود ندارد. شی‌ءهای حقیقی دارای یک سری از رفتار‌ها هستند؛ ولی Null Object معمولا هیچ کاری را انجام نمی‌دهد. 

Null Object دارای هیچگونه اطلاعاتی نیست. اگر ما یک برنامه تجارت داشتیم که در آن درخواست خرید، Null Object را برگرداند، در واقع تمام سر نخ‌هایی را که چرا عملیات با شکست مواجه شد‌ه‌است، کنار می‌گذاریم. آیا این مبلغ در حساب کاربری کاربر کافی نیست و یا آیا آیتمی موجود نیست؟ 

در این مقاله حالت‌هایی را که الگوی طراحی Null Object، قادر به تشخیص آنها نیست را به وسیله الگوی طراحی Special case رفع می‌کنیم.


 الگوی طراحی Special Case 

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

بجای برگشت دادن شیء Null در تمام این موارد، ما می‌توانیم نتیجه را اصلاح کنیم و اساسا هر بار یک شیء مختلف را بازگردانیم. اینها هنوز هم نوعی از اشیاء Null هستند؛ ولی اینبار دارای معانی هستند. یکی از انها برای «حساب کاربری ناکافی» است، یکی دیگر برای «سایت در دست تعمیر و نگهداری» است و یا یکی دیگر از آنها «موجود نبودن در انبار» خواهد بود. 

چنین اشیائی به موارد خاص ( Special Case ) اشاره می‌کنند. ما می‌توانیم اشیاء مورد خاص ( Special Case ) را به عنوان نتایج واقعی عملیات بسازیم. فقط اگر تمام بررسی‌های کسب و کار به پایان برسند و عملیات موفقیت آمیز باشد، آنگاه شیء واقعی نتیجه را باز می‌گردانیم.


نمونه‌ای از پیاده سازی موارد خاص

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

public interface IApplicationServices
{
    ...
    IReceiptViewModel LoggedInUserPurchase(string itemName);
}

لایه نمایش انتظار دارد که لایه نرم افزار یک ویو مدل به خصوصی را برای آن تولید کند. در حال حاضر ما یک سناریوی موفقیت آمیز را داریم که در آن ویو مدل شامل اطلاعات واقعی از خرید است و چندین سناریوی شکست. 

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

       1)  سایت در دست تعمیر و نگهداری باشد.

       2)  کاربر ثبت نشده و یا فعال نیست.

       3) آیتمی موجود نیست و یا وجود ندارد.

       4)  موجودی کاربر کم باشد.

 برای هر یک از این موارد یک کلاس خاص را ایجاد می‌کنیم که رابط کاربری IReceiptViewModel را پیاده سازی خواهد کرد. 
public class DownForMaintenance: IReceiptViewModel
{
}
این ویو مدل نشان دهنده این می‌باشد که سایت در دست تعمیر و نگهداری است. در حال حاضر هیچ اطلاعات اضافی همراه آن نیست؛ اما بعدها می‌توانیم بعضی از ویژگی‌های آن را اضافه کنیم. برای مثال زمان برآورد که سایت چه زمانی دوباره باز می‌شود. 
public class InvalidUser: IReceiptViewModel
{
    public string UserName { get; private set; }

    public InvalidUser(string userName)
    {
        this.UserName = userName;
    }
}
این کلاس برای مواقعی که کاربر وجود نداشته باشد و یا غیر فعال باشد، مورد استفاده قرار می‌گیرد. اینبار شیء مورد خاص (Special case) دارای اطلاعات اضافی مانند نام کاربری که خرید آن شکست خورده است، می‌باشد. 
توجه داشته باشید موارد خاص InvalidUser زمانی تولید می‌شوند که حالت DownForMaintenance را با موفقیت گذرانده باشیم. این دقیقا همان لحظه‌ای است که برنامه ما می‌داند کاربر وارد سیستم شده‌است. 
این قاعده کلی در الگوی طراحی مورد خاص ( Special case ) است. همانطور که از طریق منطق دامنه ( Domain Logic ) پیشرفت می‌کنید، اطلاعات بیشتری جمع آوری می‌شود که هر کدام از آنها برای تولید یک مورد مورد خاص با ارزش‌تر، مورد استفاده قرار می‌گیرند.
public class OutOfStock: IReceiptViewModel
{
    public string UserName { get; private set; }
    public string ItemName { get; private set; }

    public OutOfStock(string userName, string itemName)
    {
        this.UserName = userName;
        this.ItemName = itemName;
    }
}
این ویو مدل نشان دهنده این است که آیتمی موجود نیست. اینبار ما نام کاربری و نام آیتم (معتبر) را می‌دانیم. این شیء شامل اطلاعاتی است که آیتم مشخص شده توسط کاربر ثبت شده موجود در انبار موجود نیست. 
public class InsufficientFunds: IReceiptViewModel
{
    public string UserName { get; private set; }
    public decimal Amount { get; private set; }
    public string ItemName { get; private set; }

    public InsufficientFunds(string userName, decimal amount, string itemName)
    {
        this.UserName = userName;
        this.Amount = amount;
        this.ItemName = itemName;
    }
}
در نهایت، این مدل اطلاعاتی را ارائه می‌دهد که کاربری خاص، موجودی کافی برای پرداخت اقلام درخواستی را ندارد. بازگشت یک مورد خاص ( Special case) بجای Null Object ساده که حاوی اطلاعات اضافی نیست، به ما اجازه می‌دهد که اطلاعات قابل تامل‌تری را به کاربر بازگشت دهیم.

مثال استفاده از موارد خاص ( Special case)
با این حال ما تعدادی از کلاس‌های مورد خاص را پیاده سازی کرده‌ایم که هر کدام برای سناریوهای منفی در برنامه است. در هر موردی که ما شیء null یا Null را استفاده می‌کردیم وآن را بازگشت می‌دادیم، اکنون دیگر فقط مورد خاص را ایجاد و باز می‌گردانیم.
public class ApplicationServices: IApplicationServices
{
    ...
    public IReceiptViewModel LoggedInUserPurchase(string itemName)
    {
        if (IsDownForMaintenance())
            return new DownForMaintenance();
        return this.domain.Purchase(Session.LoggedInUserName, itemName);
    }

    private bool IsDownForMaintenance()
    {
        return File.Exists("maintenance.lock");
    }
}
این پیاده سازی سرویس نرم افزاری است که می‌تواند برای موارد تعمیر و نگهداری در مواردی که برنامه در دست نیست، بازگرداننده شود. در غیر این صورت سرویس برنامه، سرویس  دامین را فراخوانی کرده و شیءای را که آنجا تولید می‌شود، بازگشت می‌دهد. 
در داخل سرویس دامنه، مسائل ممکن است بسیار پیچیده‌تر باشند که در اینجا پیاده سازی شده است:
public class DomainServices: IDomainServices
{
    public IReceiptViewModel Purchase(string userName, string itemName)
    {
        User user = this.userRepository.Find(userName);
        if (user == null)
            return new InvalidUser(userName);

        Account account = this.accountRepository.FindByUser(user);
        return this.Purchase(user, account, itemName);
    }

    private IReceiptViewModel Purchase(User user, Account account, string itemName)
    {
        Product item = this.productRepository.Find(itemName);
        if (item == null)
            return new OutOfStock(user.UserName, itemName);

        ReceiptDto receipt = user.Purchase(item);
        MoneyTransaction transaction = account.Withdraw(receipt.Price);
        if (transaction == null)
            return new InsufficientFunds(user.UserName, receipt.Price, itemName);

        return receipt;
    }
}
در این مورد، اگر در سرویس‌های دامنه، چیزی اشتباه برآورده شود، موارد خاصی را باز می‌گردانند. فقط در پایان اجرا، اگر همه چیز خوب پیش رود، سرویس دامنه یک شیء واقعی را بازگشت خواهد داد.
به این ترتیب زمانیکه کاربر درخواست خریدی را می‌کند، خدمات برنامه و دامنه، اطلاعات و اتفاقات لازم را به لایه نمایش ارسال می‌کنند. اگر خرید با خطا رو به رو شد، لایه نمایش یک شیء را که حاوی تمام داده‌های موجود در مورد دلایل شکست است، دریافت می‌کند. اگر خرید با موفقیت باشد، لایه نمایش یک شیء را دریافت خواهد کرد که قادر به نمایش آن به کاربر خواهد بود.

نتیجه گیری
الگوی طراحی مورد خاص، مکمل ایده الگوی Null Object است. در بسیاری از موارد، Null Object واقعا قابل اجرا نیست؛ زیرا اطلاعاتی را درباره آنکه چرا هیچ خاصیت خاصی تولید نمی‌شود، ارائه نمی‌دهد. special case شیء خاصی است که اطلاعات بیشتری را ارائه می‌دهد.
نتیجه این پیچیدگی در کد این است که تماس گیرنده مجبور به انجام if-then-else بر اساس اینکه آیا شیء null یا غیر null است، دیگر نیست. رفرنس‌ها همیشه غیر Null خواهند بود. تنها تفاوت قابل ملاحظه‌ای که بین الگوهای Null Object و Special case وجود دارد این است که الگوی خاص ( Special Case) رفتارهای پیچیده‌تری را از خود نشان می‌دهد. از این رو، الگوی مورد خاص را می‌توان به سناریوهای پیچیده‌تری اعمال کرد که تماس گیرنده را از منطق if-then-else محافظت می‌کند. 
مطالب
هاست سرویس های Asp.Net Web Api با استفاده از OWIN و TopShelf
زمانیکه از Template‌های پیش فرض تدارک دیده شده در VS.Net برای اپلیکیشن‌های وب خود استفاده می‌کنید، وب اپلیکیشن و سرور با هم یکپارچه هستند و تحت IIS  اجرا می‌شوند. به وسیله Owin می‌توان این دو مورد را بدون وابستگی به IIS به صورت مجزا اجرا کرد. در این پست قصد داریم سرویس‌های Web Api را در قالب یک Windows Service با استفاده از کتابخانه‌ی TopShelf هاست نماییم.
پیش نیاز ها:
»Owin چیست
»تبدیل برنامه‌های کنسول ویندوز به سرویس ویندوز ان تی

  برای شروع یک برنامه Console Application ایجاد کرده و اقدام به نصب پکیج‌های زیر نمایید:
Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package TopShelf

حال یک کلاس Startup برای پیاده سازی Configuration‌های مورد نیاز ایجاد می‌کنیم
در این قسمت می‌توانید تنظیمات زیر را پیاده سازی نمایید:
»سیستم Routing؛
»تنظیم  Dependency Resolver برای تزریق وابستگی کنترلر‌های Web Api؛
»تنظیمات hub‌های SignalR(در حال حاضر SignalR به صورت پیش فرض نیاز به Owin برای اجرا دارد)؛
»رجیستر کردن Owin Middleware‌های نوشته شده؛
»تغییر در Asp.Net PipeLine؛
»و...

public class Startup 
    {       
        public void Configuration(IAppBuilder appBuilder) 
        {          
            HttpConfiguration config = new HttpConfiguration(); 
            config.Routes.MapHttpRoute( 
                name: "DefaultApi", 
                routeTemplate: "api/{controller}/{id}", 
                defaults: new { id = RouteParameter.Optional } 
            ); 

            appBuilder.UseWebApi(config); 
        } 
    }
* به صورت پیش فرض نام این کلاس باید Startup و نام متد آن نیز باید Configuration باشد.

در این مرحله یک کنترلر Api به صورت زیر به پروژه اضافه نمایید:
public class ValuesController : ApiController 
    {        
        public IEnumerable<string> Get() 
        { 
            return new string[] { "value1", "value2" }; 
        } 
      
        public string Get(int id) 
        { 
            return "value"; 
        } 

        public void Post([FromBody]string value) 
        { 
        } 

        public void Put(int id, [FromBody]string value) 
        { 
        }        
    }
کلاسی به نام ServiceHost ایجاد نمایید و کد‌های زیر را در آن کپی کنید:
public class ServiceHost
    {     
        private IDisposable webApp;             

        public static string BaseAddress 
        {
            get
            {
                return "http://localhost:8000/";
            }
        }

        public void Start()
        {           
            webApp = WebApp.Start<Startup>(BaseAddress);          
        }

        public void Stop()
        {           
            webApp.Dispose();          
        }
    }
واضح است که متد Start در کلاس بالا با استفاده از متد Start کلاس WebApp، سرویس‌های Web Api را در آدرس مورد نظر هاست خواهد کرد. با فراخوانی متد Stop این سرویس‌ها نیز dispose خواهند شد.
در مرحله آخر باید شروع و توقف سرویس‌ها را تحت کنترل کلاس HostFactory کتابخانه TopShelf در آوریم. برای این کار کافیست کلاسی به نام ServiceHostFactory ایجاد کرده و کد‌های زیر را در آن کپی نمایید:
public class ServiceHostFactory
    {
        public static void Run()
        {
            HostFactory.Run( config =>
            {
                config.SetServiceName( "ApiServices" );
                config.SetDisplayName( "Api Services ]" );
                config.SetDescription( "No Description" );

                config.RunAsLocalService();

                config.Service<ServiceHost>( cfg =>
                {
                    cfg.ConstructUsing( builder => new ServiceHost() );

                    cfg.WhenStarted( service => service.Start() );
                    cfg.WhenStopped( service => service.Stop());
                } );
            } );
        }
    }
توضیح کد‌های بالا:
ابتدا با فراخوانی متد Run سرویس مورد نظر اجرا خواهد شد. تنظیمات نام سرویس و نام مورد نظر جهت نمایش  و همچنین توضیحات در این قسمت انجام می‌گیرد.
با استفاده از متد ConstructUsing عملیات وهله سازی از سرویس انجام خواهد گرفت. در پایان نیز متد Start  و Stop کلاس ServiceHost، به عنوان عملیات شروع و پایان سرویس ویندوز مورد نظر تعیین شد.

حال اگر در فایل Program پروژه، دستور زیر را فراخوانی کرده و برنامه را ایجاد کنید خروجی زیر قابل مشاهده است.
ServiceHostFactory.Run();

در حالیکه سرویس مورد نظر در حال اجراست، Browser را گشوده و آدرس http://localhost:8000/api/values/get را در AddressBar وارد کنید. خروجی زیر را مشاهده خواهید کرد:

مطالب
فعال‌سازی Multiple Active Result Sets
(Multiple Active Result Sets (MARS یکی از قابلیتهای SQL SERVER است. این قابلیت در واقع این امکان را برای ما فراهم می‌کند تا بر روی یک Connection همزمان چندین کوئری را به صورت موازی ارسال کنیم. در این حالت برای هر کوئری یک سشن مجزا در نظر گرفته می‌شود. 
مدل:
namespace EnablingMARS.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Desc { get; set; }
        public float Price { get; set; }
        public Category Category { get; set; }

    }

    public enum Category
    {
        Cate1,
        Cate2,
        Cate3
    }
}
کلاس Context:
namespace EnablingMARS.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext() : base("EnablingMARS") {}
        public DbSet<Product> Products { get; set; }

    }
}
ابتدا یک سطر جدید را توسط کد زیر به دیتابیس اضافه می‌کنیم:
MyContext.Products.Add(new Product()
 {
                Title = "title1",
                Desc = "desc",
                Price = 4500f,
                Category = Category.Cate1
   });
MyContext.SaveChanges();
اکنون می‌خواهیم قیمت محصولاتی را که در دسته‌بندی Cate1 قرار دارند، تغییر دهیم:
foreach (var product in _dvContext.Products.Where(category => category.Category == Category.Cate1))
{
     product.Price = 50000;
     MyContext.SaveChanges();
}
خوب؛ اکنون اگر برنامه را اجرا کنیم با خطای زیر مواجه می‌شویم:
There is already an open DataReader associated with this Command which must be closed first.
این استثناء زمانی اتفاق می‌افتد که بر روی نتایج حاصل از یک کوئری، یک کوئری دیگر را ارسال کنیم. البته استثنای صادر شده بستگی به کوئری دوم شما دارد ولی در حالت کلی و با مشاهده Stack Trace، پیام فوق نمایش داده می‌شود. همانطور که در کد بالا ملاحظه می‌کنید درون حلقه‌ی forach ما به پراپرتی Price دسترسی پیدا کرده‌ایم، در حالیکه کوئری اصلی ما هنوز فعال (Active) است. MARS در اینجا به ما کمک می‌کند که بر روی یک Connection، بیشتر از یک کوئری فعال داشته باشیم. در حالت عادی Entity Framework Code First این ویژگی را به صورت پیش‌فرض برای ما فعال نمی‌کند. اما اگر خودمان کانکشن‌استرینگ را اصلاح کنیم، این ویژگی SQL SERVER فعال می‌گردد. برای حل این مشکل کافی است به کانکشن‌استرینگ، MultipleActiveResultSets=true را اضافه کنیم:
"Data Source=(LocalDB)\v11.0;Initial Catalog=EnablingMARS; MultipleActiveResultSets=true"
لازم به ذکر است که این قابلیت از نسخه SQL SERVER 2005 به بالا در دسترس می‌باشد. همچنین در هنگام استفاده از این قابلیت می‌بایستی موارد زیر را در نظر داشته باشید:
  • وقتی کانکشنی در حالت MARS برقرار می‌شود، یک سشن نیز همراه با یکسری اطلاعات اضافی برای آن ایجاد شده که باعث ایجاد Overhead خواهد شد.
  • دستورات مارس  thread-safe  نیستند.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهارم - نصب و راه اندازی IdentityServer
معرفی IdentityServer 4


اگر استاندارد OpenID Connect را بررسی کنیم، از مجموعه‌ای از دستورات و رهنمودها تشکیل شده‌است. بنابراین نیاز به کامپوننتی داریم که این استاندارد را پیاده سازی کرده باشد تا بتوان بر اساس آن یک Identity Provider را تشکیل داد و پیاده سازی مباحثی که در قسمت قبل بررسی شدند مانند توکن‌ها، Flow، انواع کلاینت‌ها، انواع Endpoints و غیره چیزی نیستند که به سادگی قابل انجام باشند. اینجا است که IdentityServer 4، به عنوان یک فریم ورک پیاده سازی کننده‌ی استانداردهای OAuth 2 و OpenID Connect مخصوص ASP.NET Core ارائه شده‌است. این فریم ورک توسط OpenID Foundation تائید شده و داری مجوز رسمی از آن است. همچنین جزئی از NET Foundation. نیز می‌باشد. به علاوه باید دقت داشت که این فریم ورک کاملا سورس باز است.


نصب و راه اندازی IdentityServer 4

همان مثال «قسمت دوم - ایجاد ساختار اولیه‌ی مثال این سری» را در نظر بگیرید. داخل آن پوشه‌های جدید src\IDP\DNT.IDP را ایجاد می‌کنیم.


نام دلخواه DNT.IDP، به پوشه‌ی جدیدی اشاره می‌کند که قصد داریم IDP خود را در آن برپا کنیم. نام آن را نیز در ادامه‌ی نام‌های پروژه‌های قبلی که با ImageGallery شروع شده‌اند نیز انتخاب نکرده‌ایم؛ از این جهت که یک IDP را قرار است برای بیش از یک برنامه‌ی کلاینت مورد استفاده قرار دهیم. برای مثال می‌توانید از نام شرکت خود برای نامگذاری این IDP استفاده کنید.

اکنون از طریق خط فرمان به پوشه‌ی src\IDP\DNT.IDP وارد شده و دستور زیر را صادر کنید:
dotnet new web
این دستور، یک پروژه‌ی جدیدی را از نوع «ASP.NET Core Empty»، در این پوشه، بر اساس آخرین نگارش SDK نصب شده‌ی بر روی سیستم شما، ایجاد می‌کند. از این جهت نوع پروژه خالی درنظر گرفته شده‌است که قرار است توسط اجزای IdentityServer 4 به مرور تکمیل شود.

اولین کاری را که در اینجا انجام خواهیم داد، مراجعه به فایل Properties\launchSettings.json آن و تغییر شماره پورت‌های پیش‌فرض آن است تا با سایر پروژه‌های وبی که تاکنون ایجاد کرده‌ایم، تداخل نکند. برای مثال در اینجا شماره پورت SSL آن‌را به 6001 تغییر داده‌ایم.

اکنون نوبت به افزودن میان‌افزار IdentityServer 4 به پروژه‌ی خالی وب است. اینکار را نیز توسط اجرای دستور زیر در پوشه‌ی src\IDP\DNT.IDP انجام می‌دهیم:
 dotnet add package IdentityServer4

در ادامه نیاز است این میان‌افزار جدید را معرفی و تنظیم کرد. به همین جهت فایل Startup.cs پروژه‌ی خالی وب را گشوده و به صورت زیر تکمیل می‌کنیم:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
                .AddDeveloperSigningCredential();
        }
- متد الحاقی AddIdentityServer، کار ثبت و معرفی سرویس‌های توکار IdentityServer را به سرویس توکار تزریق وابستگی‌های ASP.NET Core انجام می‌دهد.
- متد الحاقی AddDeveloperSigningCredential کار تنظیمات کلید امضای دیجیتال توکن‌ها را انجام می‌دهد. در نگارش‌های قبلی IdentityServer، اینکار با معرفی یک مجوز امضاء کردن توکن‌ها انجام می‌شد. اما در این نگارش دیگر نیازی به آن نیست. در طول توسعه‌ی برنامه می‌توان از نگارش Developer این مجوز استفاده کرد. البته در حین توزیع برنامه به محیط ارائه‌ی نهایی، باید به یک مجوز واقعی تغییر پیدا کند.


تعریف کاربران، منابع و کلاینت‌ها

مرحله‌ی بعدی تنظیمات میان‌افزار IdentityServer4، تعریف کاربران، منابع و کلاینت‌های این IDP است. به همین جهت یک کلاس جدید را به نام Config، در ریشه‌ی پروژه ایجاد و به صورت زیر تکمیل می‌کنیم:
using System.Collections.Generic;
using System.Security.Claims;
using IdentityServer4.Models;
using IdentityServer4.Test;

namespace DNT.IDP
{
    public static class Config
    {
        // test users
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
                    SubjectId = "d860efca-22d9-47fd-8249-791ba61b07c7",
                    Username = "User 1",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("given_name", "Vahid"),
                        new Claim("family_name", "N"),
                    }
                },
                new TestUser
                {
                    SubjectId = "b7539694-97e7-4dfe-84da-b4256e1ff5c7",
                    Username = "User 2",
                    Password = "password",

                    Claims = new List<Claim>
                    {
                        new Claim("given_name", "User 2"),
                        new Claim("family_name", "Test"),
                    }
                }
            };
        }

        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile()
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>();
        }
    }
}
توضیحات:
- این کلاس استاتیک، اطلاعاتی درون حافظه‌ای را برای تکمیل دموی جاری ارائه می‌دهد.

- ابتدا در متد GetUsers، تعدادی کاربر آزمایشی اضافه شده‌اند. کلاس TestUser در فضای نام IdentityServer4.Test قرار دارد. در کلاس TestUser، خاصیت SubjectId، بیانگر Id منحصربفرد هر کاربر در کل این IDP است. سپس نام کاربری، کلمه‌ی عبور و تعدادی Claim برای هر کاربر تعریف شده‌اند که بیانگر اطلاعاتی اضافی در مورد هر کدام از آن‌ها هستند. برای مثال نام و نام خانوادگی جزو خواص کلاس TestUser نیستند؛ اما منعی هم برای تعریف آن‌ها وجود ندارد. اینگونه اطلاعات اضافی را می‌توان توسط Claims به سیستم اضافه کرد.

- بازگشت Claims توسط یک IDP مرتبط است به مفهوم Scopes. برای این منظور متد دیگری به نام GetIdentityResources تعریف شده‌است تا لیستی از IdentityResource‌ها را بازگشت دهد که در فضای نام IdentityServer4.Models قرار دارد. هر IdentityResource، به یک Scope که سبب دسترسی به اطلاعات Identity کاربران می‌شود، نگاشت خواهد شد. در اینجا چون از پروتکل OpenID Connect استفاده می‌کنیم، ذکر IdentityResources.OpenId اجباری است. به این ترتیب مطمئن خواهیم شد که SubjectId به سمت برنامه‌ی کلاینت بازگشت داده می‌شود. برای بازگشت Claims نیز باید IdentityResources.Profile را به عنوان یک Scope دیگری مشخص کرد که در متد GetIdentityResources مشخص شده‌است.

- در آخر نیاز است کلاینت‌های این IDP را نیز مشخص کنیم (در مورد مفهوم Clients در قسمت قبل بیشتر توضیح داده شد) که اینکار در متد GetClients انجام می‌شود. فعلا یک لیست خالی را بازگشت می‌دهیم و آن‌را در قسمت‌های بعدی تکمیل خواهیم کرد.


افزودن کاربران، منابع و کلاینت‌ها به سیستم

پس از تعریف و تکمیل کلاس Config، برای معرفی آن به IDP، به کلاس آغازین برنامه مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddTestUsers(Config.GetUsers())
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryClients(Config.GetClients());
        }
در اینجا لیست کاربران و اطلاعات آن‌ها توسط متد AddTestUsers، لیست منابع و Scopes توسط متد AddInMemoryIdentityResources و لیست کلاینت‌ها توسط متد AddInMemoryClients به تنظیمات IdentityServer اضافه شده‌اند.


افزودن میان افزار IdentityServer به برنامه

پس از انجام تنظیمات مقدماتی سرویس‌های برنامه، اکنون نوبت به افزودن میان‌افزار IdentityServer است که در کلاس آغازین برنامه به صورت زیر تعریف می‌شود:
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseIdentityServer();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }

آزمایش IDP

اکنون برای آزمایش IDP، به پوشه‌ی src\IDP\DNT.IDP وارد شده و دستور dotnet run را اجرا کنید:


همانطور که ملاحظه می‌کنید، برنامه‌ی IDP بر روی پورت 6001 قابل دسترسی است. برای آزمایش Web API آن، آدرس discovery endpoint این IDP را به صورت زیر در مرورگر وارد کنید:
 https://localhost:6001/.well-known/openid-configuration


در این تصویر، مفاهیمی را که در قسمت قبل بررسی کردیم مانند authorization_endpoint ،token_endpoint و غیره، مشاهده می‌کنید.


افزودن UI به IdentityServer

تا اینجا میان‌افزار IdentityServer را نصب و راه اندازی کردیم. در نگارش‌های قبلی آن، UI به صورت پیش‌فرض جزئی از این سیستم بود. در این نگارش آن‌را می‌توان به صورت جداگانه دریافت و به برنامه اضافه کرد. برای این منظور به آدرس IdentityServer4.Quickstart.UI مراجعه کرده و همانطور که در readme آن ذکر شده‌است می‌توان از یکی از دستورات زیر برای افزودن آن به پروژه‌ی IDP استفاده کرد:
الف) در ویندوز از طریق کنسول پاورشل به پوشه‌ی src\IDP\DNT.IDP وارد شده و سپس دستور زیر را وارد کنید:
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.ps1'))
ب) و یا درmacOS و یا Linux، دستور زیر را اجرا کنید:
\curl -L https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/release/get.sh | bash

یک نکته: در ویندوز اگر در نوار آدرس هر پوشه، عبارت cmd را وارد و enter کنید، کنسول خط فرمان ویندوز در همان پوشه باز خواهد شد. همچنین در اینجا از ورود عبارت powershell هم پشتیبانی می‌شود:


بنابراین در نوار آدرس پوشه‌ی src\IDP\DNT.IDP، عبارت powershell را وارد کرده و سپس enter کنید. پس از آن دستور الف را وارد (copy/paste) و اجرا کنید.


به این ترتیب فایل‌های IdentityServer4.Quickstart.UI به پروژه‌ی IDP جاری اضافه می‌شوند.
- پس از آن اگر به پوشه‌ی Views مراجعه کنید، برای نمونه ذیل پوشه‌ی Account آن، Viewهای ورود و خروج به سیستم قابل مشاهده هستند.
- در پوشه‌ی Quickstart آن، کدهای کامل کنترلرهای متناظر با این Viewها قرار دارند.
بنابراین اگر نیاز به سفارشی سازی این Viewها را داشته باشید، کدهای کامل کنترلرها و Viewهای آن هم اکنون در پروژه‌ی IDP جاری در دسترس هستند.

نکته‌ی مهم: این UI اضافه شده، یک برنامه‌ی ASP.NET Core MVC است. به همین جهت در انتهای متد Configure، ذکر میان افزارهای UseStaticFiles و همچنین UseMvcWithDefaultRoute انجام شدند.

اکنون اگر برنامه‌ی IDP را مجددا با دستور dotnet run اجرا کنیم، تصویر زیر را می‌توان در ریشه‌ی سایت، مشاهده کرد که برای مثال لینک discovery endpoint در همان سطر اول آن ذکر شده‌است:


همچنین همانطور که در قسمت قبل نیز ذکر شد، یک IDP حتما باید از طریق پروتکل HTTPS در دسترس قرار گیرد که در پروژه‌های ASP.NET Core 2.1 این حالت، جزو تنظیمات پیش‌فرض است.


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

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

var eyeColors = new List<string>
{
    "Black", "Green", "Brown"
};
var specificUsers = _context.Users.Where(x => eyeColors.Contains(x.EyeColor)).ToList();

حالا اگه لیست ما ترکیبی باشه این سرچ باید به چه صورت انجام بشه:

public class User
{
    public string FullName { get; set; }

    public string EyeColor { get; set; }
}
var customUsers = new List<User>
{
    new User{FullName = "Ali Ahmadi", EyeColor = "Brown"},
    new User{FullName = "Milad Rezaei", EyeColor = "Green"}
};

var specificUsers = _context.Users.Where(x => customUsers.Contains(new User(){EyeColor = x.EyeColor, FullName = x.FullName})).ToList();

روش فوق و روش های زیادی رو سرچ کردم ولی به جواب نرسیدم. آیا میشه از EF استفاده کرد یا راه حل دیگری دارد ؟

اشتراک‌ها
احراز هویت (Identity) در ASP.Net Core با استفاده از بانک اطلاعاتی Redis

برای انتقال جداول احراز هویت (Identity) از SQL Server به بانک اطلاعاتی Redis و نحوه استفاده از آن در ASP.Net Core از سورس نمونه در لینک بالا استفاده کنید.
همچنین می‌توانید از پکیج Aguacongas.Identity.Redis استفاده کنید.  

احراز هویت (Identity) در ASP.Net Core با استفاده از بانک اطلاعاتی Redis
مطالب
تست کردن متدهای یک Controller به کمک PowerShell

ابزارهای زیادی جهت تست کردن API ‌های برنامه‌های وب موجود است. یکی از معروفترین آنها  Fiddler است که ابزاری مستقل جهت دیباگ تحت پروتکل HTTP می‌باشد. یکی دیگر از این نرم افزارها Swashbuckle است که از Nuget قابل دریافت است و صفحه‌ای را به برنامه اضافه می‌کند که به اختصار API ‌های برنامه را نمایش داده و امکان اجرای آنها را فراهم میکند.

اما ساده‌ترین راه جهت کردن تست کردن API ‌های یک برنامه، استفاده از PowerShell است که عمل ایجاد درخواست‌های HTTP را به راحتی از طریق Command line فراهم میکند و به شما اجازه میدهد بدون وارد شدن به جزئیات، بر روی نتیجه API مورد نظر تمرکز کنید. PowerShell ابتدا فقط برای محیط ویندوز طراحی شده بود ولی در حال حاضر قابلیت اجرای تحت Linux و Mac را نیز دارد.

در این بخش به شما نشان خواهم داد که چگونه متدهای یک Controller را با استفاده از PowerShell تست نمایید.

بدین منظور ابتدا کلاسی را به نام Reservation با مشخصات زیر، در یک پروژه ASP.NET Core Web Application  ایجاد نمایید.
public class Reservation
{
        public int ReservationId { get; set; }
        public string ClientName { get; set; }
        public string Location { get; set; }
}
و سپس Interface زیر را جهت انجام عملیات CRUD اضافه نمائید:
public interface IRepository
{
        IEnumerable<Reservation> Reservations { get; }
        Reservation this[int id] { get; }
        Reservation AddReservation(Reservation reservation);
        Reservation UpdateReservation(Reservation reservation);
        void DeleteReservation(int id);
}
و کلاسی که Interface فوق را پیاده سازی خواهد کرد:
public class MemoryRepository : IRepository
 {
        private Dictionary<int, Reservation> items;
        public MemoryRepository()
        {
            items = new Dictionary<int, Reservation>();
            new List<Reservation> {
                new Reservation { ClientName = "Alice", Location = "Board Room" },
                new Reservation { ClientName = "Bob", Location = "Lecture Hall" },
                new Reservation { ClientName = "Joe", Location = "Meeting Room 1" }
            }.ForEach(r => AddReservation(r));
        }

        public Reservation this[int id] => items.ContainsKey(id) ? items[id] : null;
        public IEnumerable<Reservation> Reservations => items.Values;

        public Reservation AddReservation(Reservation reservation)
        {
            if (reservation.ReservationId == 0)
            {
                int key = items.Count;
                while (items.ContainsKey(key)) { key++; };
                reservation.ReservationId = key;
            }
            items[reservation.ReservationId] = reservation;
            return reservation;
        }

        public void DeleteReservation(int id) => items.Remove(id);
        public Reservation UpdateReservation(Reservation reservation) => AddReservation(reservation);
}
در کلاس فوق به جهت سهولت در کار، از یک لیست، برای نگهداری تعدادی رکورد اطلاعاتی جهت نمایش استفاده شده است.

سپس کنترلری را به نام ReservationController به پروژه اضافه کنید که شامل متدهای زیر باشد:
[Route("api/[controller]")]
public class ReservationController : Controller
    {
        private IRepository repository;
        public ReservationController(IRepository repo) => repository = repo;

        [HttpGet]
        public IEnumerable<Reservation> Get() => repository.Reservations;

        [HttpGet("{id}")]
        public Reservation Get(int id) => repository[id];

        [HttpPost]
        public Reservation Post([FromBody] Reservation res) =>
                    repository.AddReservation(new Reservation
                    {
                        ClientName = res.ClientName,
                        Location = res.Location
                    });
        [HttpPut]
        public Reservation Put([FromBody] Reservation res) => repository.UpdateReservation(res);

        [HttpPatch("{id}")]
        public StatusCodeResult Patch(int id, [FromBody] JsonPatchDocument<Reservation> patch)
        {
            Reservation res = Get(id);
            if (res != null)
            {
                patch.ApplyTo(res);
                return Ok();
            }
            return NotFound();
        }

        [HttpDelete("{id}")]
        public void Delete(int id) => repository.DeleteReservation(id);
}
حالا جهت تست صحت این API‌ها توسط PowerShell لازم است ابتدا برنامه را اجرا کرده و منتظر بمانید تا صفحه Home ظاهر شود. پس از آن لازم است یک پنجره PowerShell را از طریق کلیک راست در یکی از فولدرهای نمایش داده شده در FileExplorer باز کنید.

تست کردن درخواست از نوع GET

حالا دستور زیر را در پنجره PowerShell وارد کنید:
 Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
لازم است شماره پورت را مطابق با پورتی که برنامه شما بر روی آن اجرا شده تغییر دهید. این فرمان به کمک دستور Invoke-RestMethod درخواستی از نوع GET را به api/reservation ارسال می‌کند و نتیجه را پس از تجزیه و تحلیل و فرمت بندی، جهت سهولت در خوانده شدن، به شکل زیر نمایش میدهد:

location  clientName  reservationId 
Board Room 
Alice 
0                  
Lecture Hall 
Bob
1
Meeting Room 1 
Joe
2
فرمت اطلاعات دریافت شده از درخواست GET، اشیایی از نوع کلاس Reservation است و به فرمت Json می‌باشد؛ ولی Invoke-RestMethod آنرا به صورت جدول نمایش میدهد.
حال جهت نمایش فقط یک رکورد از اطلاعات فوق، دستور زیر را وارد نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation/1 -Method GET
این فرمان درخواستی جهت دریافت اطلاعات شیء Reservation است که کد داخلی آن برابر 1 است. پاسخ این درخواست به شکل زیر نمایش داده خواهد شد:

location  clientName   reservationId   
Lecture Hall  
        Bob
1                  


تست درخواست از نوع Post

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

Invoke-RestMethod http://localhost:7000/api/reservation -Method POST -Body
(@{clientName="Anne"; location="Meeting Room 4"} | ConvertTo-Json) -ContentType "application/json"
این دستور با استفاده از آرگومان Body- محتویات درخواست را که با فرمت Json است تعیین می‌کند. آرگومان Content-Type- هم نوع محتویات درخواستی را در هدر مشخص میکند. دستور فوق در صورت موفقیت نتیجه زیر را نمایش خواهد داد:

location  clientName   reservationId   
 Meeting Room 4
Anne      
1                
           
دستور Post ارسالی، با استفاده از دو فیلد clientName و location، یک رکورد جدید از نوع Reservation را به لیست موجود اضافه کرده و نتیجه را در قالب Json به سمت کلاینت برگشت خواهد داد که شامل ReservationId می‌باشد که به رکورد جدید اختصاص داده شده‌است. ممکن است چنین به نظر برسد که نتیجه برگشت داده شده، همان درخواست ارسالی است که به شکل جدول فوق نمایش داده می‌شود؛ ولی چنین نیست. جهت مشاهده تاثیر درخواست POST ارسالی بر روی منبع داده، کافی است یک بار دیگر دستور زیر را در command line وارد کنید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId   
 Board Room           Alice
0              
 Lecture Hall  Bob 1
 Meeting Room 1  Joe 2
 Meeting Room 4  Anne 3
داده‌هایی که به سمت کلاینت ارسال شده، نشان دهنده اضافه شدن رکورد جدیدی به منبع داده ماست.

تست درخواست از نوع PUT

درخواست از نوع PUT جهت جایگزینی داده‌ای موجود در لیست، با داده‌ی جدید است. بدین منظور کد داخلی شیء مورد نظر (در اینجا ReservationId) باید در URL گنجانده شود و مقادیر فیلدهای کلاس، در متن درخواست. مثالی جهت درخواست اصلاح داده موجود از طریق فرمان PowerShell :

Invoke-RestMethod http://localhost:7000/api/reservation -Method PUT -Body
(@{reservationId="1"; clientName="Bob"; location="Media Room"} | ConvertTo-Json)
-ContentType "application/json"
درخواست فوق، فیلد Location رکوردی با کد داخلی 1 را به مقدار جدیدی تغییر خواهد داد و نتیجه تغییر انجام شده را در پاسخ ارسال خواهد کرد:

location  clientName   reservationId   
Media Room
Bob        
1                

و باز با ارسال درخواست زیر می‌توان نتیجه کلی را مشاهده کرد:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId   
Board Room
Alice  
0  
 Media Room
 Bob  1
 Meeting Room 1
Joe 2
 Meeting Room 4
Anne
3


استفاده از دستور PATCH

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

ASP.NET Core MVC از استاندارد Json PATCH پشتیبانی می‌کند و این کار اجازه میدهد تا بتوان تغییرات را بطور یکنواختی انجام داد. جهت مشاهده جزئیات این دستور می‌توانید به این لینک مراجعه کنید. اما برای مثال فرض کنید می‌خواهید چنین درخواستی را که به فرمت Json است از طریق HTTP PATCH به سمت سرور ارسال کنید:

[
  { "op": "replace", "path": "clientName", "value": "Bob"},
  { "op": "replace", "path": "location", "value": "Lecture Hall"}
]

دربسیاری از مواقع فقط از دستور replace درخواست PATCH استفاده می‌شود و کد داخلی رکورد مورد نظر، جهت تغییر در Url درخواست ارسالی، فرستاده خواهد شد. ASP.NET Core MVC به طور اتوماتیک این دستور را پردازش کرده و آنرا به صورت آبجکتی از نوع <JsonPatchDocument<T به اکشن کنترلر قید شده، پاس خواهد داد که در اینجا نوع T، از نوع کلاس مورد نظر ما می‌باشد. فرمت دستور ارسالی از طریق Powershell به شکل زیر خواهد بود:

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method PATCH -Body (@
{ op="replace"; path="clientName"; value="Bob"},@{ op="replace"; path="location";
value="Lecture Hall"} | ConvertTo-Json) -ContentType "application/json"
دستور فوق درخواست تغییر دو فیلد clientname و location، با کد داخلی 2 می‌باشد.
جهت مشاهده تغییر ایجاد شده، دستور زیر را مجددا اجرا نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId  
Board Room
Alice  
0            
Media Room
Bob
 1
Lecture Hall
 Bob 2
Meeting Room 4
 Anne 3


استفاده از دستور Delete

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

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method DELETE
توضیح اینکه در این حالت پاسخی از سمت سرور ارسال نخواهد شد. اما جهت مشاهده تاثیر این دستور بر روی اطلاعات موجود می‌توان مجددا دستور زیر را جهت نمایش داده‌ها بکار برد:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

کدهای این مقاله به پیوست موجود است: ApiControllers.zip
نظرات مطالب
معماری لایه بندی نرم افزار #3
بعد از پیاده سازی UOW و لایه‌بندی نرم‌افزار به این این شکل که در مطلب فعلی توضیح داده شد،
 فرض کنید دو ویومدل زیر را داریم:
 public class PersonFormViewModel
    {
        public long Id { get; set; }
        public long RequestId { get; set; }
        [DisplayName("نام کاربری"), Required(ErrorMessage = "نام کاربری الزامی می‌باشد.")]
        public string Username { get; set; }
        public bool Accepted { get; set; }
        [DisplayName("مدل")]
        public string DeviceModel { get; set; }
        public DateTime? ExpireDate { get; set; }
        public RequestViewModel RequestViewModel { get; set; }
    }
public class RequestViewModel
    {
        public long Id { get; set; }
        public string Username { get; set; }
        [DisplayName("توضیحات")]
        [DataType(DataType.MultilineText)]
        public string Description { get; set; }
        public DateTime CreateDate { get; set; }
        public Nullable<long> DeviceId { get; set; }
        public Nullable<long> ParentId { get; set; }
        public long RequestTypeId { get; set; }
        public bool IsFinalized { get; set; }
        public virtual  PersonFormViewModel PersonFormViewModel { get; set; }
    }
سناریو به این شکل است که ما فرمی داریم برای ایحاد یک درخواست (RequestViewModel) که با ایجاد آن در واقع اطلاعات شخص (PersonFormViewModel) را نیز دریافت می‌کنیم.
برای افزودن RequestٰViewModel به دیتابیس این دو روش قابل پیاده‌سازی است:
روش اول: 
تنها RequestViewModel را از طریق RequestService اضافه می‌کنیم و به دلیل وجود PersonFormViewModel داخل RequestViewModel اطلاعات شخص به خودی خود داخل entity مربوطه اضافه می‌شود:
_requestService.Add(requestViewModel);
_uow.SaveChanges();
روش دوم:
ابتدا RequestViewModel را از طریق سرویس مربوطه اضافه می‌کنیم و بعد به طور جداگانه PersonViewModel را از طریق سرویس PersonFormService اضافه می‌کنیم:
var addedRequest = _requestService.Add(requestViewModel ); 
var personViewModel = requestViewModel .PersonFormViewModel; 
_personFormService.Add(personViewModel);
_uow.SaveChanges();
در حال حاضر روش درست کدام است؟
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
با سلام

بنده مدل زیر را دارم که مربوط به صفحاتی هستند که والد هم دارند.

public class Page
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime? CreatedDate { get; set; }
        public virtual DateTime? ModifiedDate { get; set; }
        public virtual string Body { get; set; }
        public virtual string Keyword { get; set; }
        public virtual string Description { get; set; }
        public virtual string Status { get; set; }
        public virtual bool? CommentStatus { get; set; }
        public virtual int? Order { get; set; }
        public virtual User User { get; set; }
        public virtual User EditedByUser { get; set; }
        public virtual ICollection<Comment> Comments { get; set; }
        public virtual int? ParentId { get; set; }
        public virtual Page Parent { get; set; }
        public virtual ICollection<Page> Children { get; set; }
    }

و با دستور زیر می‌خواهم از آن کوئری بگیرم:

this._pages.ToList().Where(page => page.Parent == null).ToList();

دستور فوق به خوبی کار می‌کنه. ولی وقتی با که دستوراتی که توسط mini-profiler لاگ شده را می‌بینیم که اخطار duplicate reader را می‌دهد.
برای هر page موجود دستور زیر را صادر می‌کند
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate], 
[Extent1].[ModifiedDate] AS [ModifiedDate], 
[Extent1].[Body] AS [Body], 
[Extent1].[Keyword] AS [Keyword], 
[Extent1].[Description] AS [Description], 
[Extent1].[Status] AS [Status], 
[Extent1].[CommentStatus] AS [CommentStatus], 
[Extent1].[Order] AS [Order], 
[Extent1].[ParentId] AS [ParentId], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Title] AS [Title1], 
[Extent2].[CreatedDate] AS [CreatedDate1], 
[Extent2].[ModifiedDate] AS [ModifiedDate1], 
[Extent2].[Body] AS [Body1], 
[Extent2].[Keyword] AS [Keyword1], 
[Extent2].[Description] AS [Description1], 
[Extent2].[Status] AS [Status1], 
[Extent2].[CommentStatus] AS [CommentStatus1], 
[Extent2].[Order] AS [Order1], 
[Extent2].[ParentId] AS [ParentId1], 
[Extent2].[User_Id] AS [User_Id], 
[Extent2].[EditedByUser_Id] AS [EditedByUser_Id], 
[Extent1].[User_Id] AS [User_Id1], 
[Extent1].[EditedByUser_Id] AS [EditedByUser_Id1]
FROM  [dbo].[Pages] AS [Extent1]
LEFT OUTER JOIN [dbo].[Pages] AS [Extent2] ON [Extent1].[ParentId] = [Extent2].[Id

می‌خواستم ببینم کاری میشه کرد تا سربار این کوئری را کمتر کرد؟

در ضمن اگر بخواهم viewmodel را طوری تعریف کنم تا فیلدهای اضافی مانند createddate و user و... که در هنگام نمایش منوی آبشاری به آنها نیازی ندارم چه کار باید کرد؟ چون من هر کاری کردم نتونستم parent را برای viewmodel به خوبی تعریف کنم.
ممنون