نظرات مطالب
ASP.NET MVC #5
فرق
 public string FirstName { get; set; }
با
       private string firstName;

       public string FirstName
        {
            get { return firstName; }
            set { firstName = value; }
        }
چیه
آخه تو دانشگاه به ما یاد دادند شبیه دومی بنویسیم اما اولی راحتتره حالا کلا با هم چه فرقی دارند؟
نظرات مطالب
بازسازی کد: ارتباط یک طرفه و دو طرفه بین کلاس ها
در EF Code First اگر به این شکل تعریف کنیم
public class Customer
{
    public string Name { get; set; }
}
public class Order
{
    public Customer Customer { get; set; }
}
تکلیف  Navigation Property  چی میشه ؟
بالاخره مجبوریم برای کوئری و همینطور Relation  از حالت زیر که شما انتقاد داشتین استفاده کنیم
public class Customer
{
    public string Name { get; set; }
    public ICollection<Order> Orders { get; set; }
}
public class Order
{
    public Customer Customer { get; set; }
}

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


اعتبار سنجی کاربران در SignalR

تمام مباحث عنوان شده در مورد نحوه‌ی کار با Forms Authentication استاندارد یک برنامه وب، در SignalR نیز قابل دسترسی است. پس از اینکه کاربری به سایت وارد شد (با استفاده از روش‌های متداول؛ مانند یک صفحه‌ی لاگین)، اطلاعات او در یک Hub نیز قابل استفاده است. برای مثال می‌توان به خاصیت this.Context.User.Identity.IsAuthenticated دسترسی داشت.
به علاوه در این حالت برای محدود کردن دسترسی کاربران اعتبار سنجی نشده به یک هاب فقط کافی است فیلتر Authorize را به هاب اعمال کنیم. باید دقت داشت که این فیلتر در فضای نام Microsoft.AspNet.SignalR تعریف شده است.
[Authorize]
public class ChatHub : Hub
{
  //...
}


نگاشت اتصالات، به کاربران واقعی سیستم

public class User
    {
        public int Id { set; get; }
        public string Name { get; set; }
        // سایر خواص کاربر
        

        public HashSet<string> ConnectionIds { get; set; }
    }
با توجه به توضیحات ابتدای بحث، هر کاربر با چندین ConnectionId می‌تواند به سیستم متصل شود. بنابراین کلاس کاربران، دارای یک خاصیت اضافی که نیازی هم نیست تا به بانک اطلاعاتی نگاشت شود، به نام ConnectionIds همانند کلاس فوق خواهد بود.
سپس باید لیست اتصالات کاربر را در هربار اتصال و قطع اتصال او به روز کرد:
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace SignalR05.Common
{
    public class User
    {
        public int Id { set; get; }
        public string Name { get; set; }
        // سایر خواص کاربر


        public HashSet<string> ConnectionIds { get; set; }
    }

    public class ChatHubHub : Hub
    {
        private static readonly ConcurrentDictionary<string, User> Users = new ConcurrentDictionary<string, User>();

        public override Task OnConnected()
        {
            connect();
            return base.OnConnected();
        }

        private void connect()
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;

            var user = Users.GetOrAdd(userName,
                _ => new User
                {
                    Name = userName,
                    ConnectionIds = new HashSet<string>()
                });
            lock (user.ConnectionIds)
            {
                user.ConnectionIds.Add(connectionId);
            }
        }

        public override Task OnReconnected()
        {
            connect();
            return base.OnReconnected();
        }

        public override Task OnDisconnected()
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;

            User user;
            Users.TryGetValue(userName, out user);
            if (user != null)
            {
                lock (user.ConnectionIds)
                {
                    user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));

                    if (!user.ConnectionIds.Any())
                    {
                        User removedUser;
                        Users.TryRemove(userName, out removedUser);

                        ///Clients.Others.userDisconnected(userName);
                    }
                }
            }

            return base.OnDisconnected();
        }
    }
}
در این مثال با بازنویسی متدهای اتصال، اتصال مجدد و قطع اتصال یک کاربر، توانسته‌ایم:
الف) نگاشتی را بین یک Id اتصال و یک User واقعی سیستم برقرار کنیم.
ب) لیست اتصالات یک کاربر را نیز در اختیار داشته و در زمان قطع اتصال یکی از برگه‌های مرورگر او، تنها یکی از این Idهای اتصال را از لیست حذف خواهیم کرد.

اگر این لیست دیگر Id متصلی نداشت، با فراخوانی متد فرضی Clients.Others.userDisconnected، می‌توان به سایر کاربران مثلا یک Chat، خروج کامل این کاربر را اطلاع رسانی کرد.
با داشتن لیست اتصالات یک کاربر، می‌توان به سایر کاربران اطلاع داد که مثلا کاربر جدیدی به Chat room وارد شده است:
  Clients.AllExcept(user.ConnectionIds.ToArray()).userConnected(userName);
AllExcept در اینجا یعنی سایر کاربران منهای کاربرانی که Id اتصالات آن‌ها ذکر می‌شود. چون این Idها تمامی متعلق به یک کاربر هستند، فراخوانی فوق به معنای اطلاع رسانی به همه، منهای کاربر جاری متصل است.
مطالب
طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD

هدف، ارائه راه‌حلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار می‌باشد.

پیش نیازها:

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


ایجاد مدل‌های پایه

همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationService‌ها از الگوی Request/Response استفاده می‌کنیم. برای این منظور واسط و کلاس‌های زیر را خواهیم داشت:

واسط IPagedQueryModel

    public interface IPagedQueryModel
    {
        int Page { get; set; }
        int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        string SortExpression { get; set; }
    }

این واسط قراردادی می‌باشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال می‌شود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص می‌کند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.

برای جلوگیری از تکرار این خصوصیات در مدل‌های کوئری مربوط به موجودیت‌ها، میتوان کلاس پایه‌ای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا می‌باشد:

  public class PagedQueryModel : IPagedQueryModel, IShouldNormalize
    {
        public int Page { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        public string SortExpression { get; set; }

        public virtual void Normalize()
        {
            if (Page < 1)
                Page = 1;

            if (PageSize < 1)
                PageSize = 10;

            if (SortExpression.IsEmpty())
                SortExpression = "Id_DESC";
        }
    }

مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز می‌باشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:

پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.

کلاس PagedQueryResult

    public class PagedQueryResult<TModel>
    {
        public PagedQueryResult()
        {
            Items = new List<TModel>();
        }
        public IEnumerable<TModel> Items { get; set; }
        public long TotalCount { get; set; }
    }

دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:

عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.

کلاس PagedListModel

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

    public class PagedListModel<TModel>
    {
        public IPagedQueryModel Query { get; set; }

        public PagedQueryResult<TModel> Result { get; set; }
    }

پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List می‌باشد که پراپرتی‌های آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شده‌اند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag می‌شود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمی‌شود.


متد GetPagedListAsync موجود در CrudApplicationService

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedQueryModel, TDynamicQueryModel> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel>
        where TEntity : Entity, new()
        where TCreateModel : class
        where TEditModel : class, IModel
        where TModel : class, IModel
        where TDeleteModel : class, IModel
        where TPagedQueryModel : PagedQueryModel, new()
        where TDynamicQueryModel : DynamicQueryModel

    {

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        public IUnitOfWork UnitOfWork { get; set; }
        public IMapper Mapper { get; set; }
        protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>();

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var query = ApplyFiltering(model);

            var totalCount = await query.LongCountAsync().ConfigureAwait(false);

            var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider);

            result = result.ApplySorting(model);
            result = result.ApplyPaging(model);

            return new PagedQueryResult<TModel>
            {
                Items = await result.ToListAsync().ConfigureAwait(false),
                TotalCount = totalCount
            };
        }
        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            return UnTrackedEntitySet;
        }
        #endregion
    }

در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام می‌شود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمی‌کند؛ مگر اینکه توسط زیر کلاس‌ها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتم‌های فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام می‌گیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شده‌است:

    public static class QueryableExtensions
    {
        public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return query.OrderBy(request.SortExpression.Replace('_', ' '));
        }

        public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.Page - 1) * request.PageSize, request.PageSize)
                : query;
        }
    }

به منظور مرتب سازی از کتابخانه  System.Liq.Dynamic کمک گرفته شده‌است.

نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.


پیاده سازی اکشن متدهای Index و List

public partial class RolesController : BaseController
{
    #region Fields
        private readonly IRoleService _service;
        private readonly ILookupService _lookupService;

        #endregion

    #region Constractor
        public RolesController(IRoleService service,  ILookupService lookupService)
        {
            Guard.ArgumentNotNull(service, nameof(service));
            Guard.ArgumentNotNull(lookupService, nameof(lookupService));

            _service = service;
            _lookupService = lookupService;
        }
        #endregion

    #region Index / List
    [HttpGet]
    public virtual async Task<ActionResult> Index()
    {
        var query = new RolePagedQueryModel();
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var pagedList = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        var model = new RoleIndexViewModel
        {
            PagedListModel = pagedList,
            Permissions = _lookupService.GetPermissions()
        };
        return View(model);
    }
    [HttpGet, AjaxOnly, NoOutputCache]
    public virtual async Task<ActionResult> List(RolePagedQueryModel query)
    {
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var model = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        return PartialView(MVC.Administration.Roles.Views._List, model);
    }
    #endregion
}

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

    public class RolePagedQueryModel : PagedQueryModel
    {
        public string Name { get; set; }
        public string Permission { get; set; }
    }

در این مورد خاص لازم است لیست دسترسی‌های موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار می‌گیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر می‌باشد:

    public class RoleIndexViewModel
    {   
        public RoleIndexViewModel()
        {
            Permissions = new List<LookupItem>();
        }
        public IReadOnlyList<LookupItem> Permissions { get; set; }
        public PagedListModel<RoleModel> PagedListModel { get; set; }
    }

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


ویو Index.cshtml

@model RoleIndexViewModel

@{
    ViewBag.Title = L("Administration.Views.Role.Index.Title");
    ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement;
}

<div class="row">
    <div class="col-md-12">
        <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel">
            <div class="panel panel-default margin-bottom-5">

                <div class="panel-body">
                    @using (Ajax.BeginForm(MVC.Administration.Roles.List(),
new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" }))
                    {
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" />
                            </div>
                            <div class="col-md-3">
                                @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})
                            </div>
                            <div class="col-md-3">

                                <button type="submit"
                                        role="button"
                                        class="btn btn-info">
                                    @L("Commands.Filter")
                                </button>
                                <button type="reset"
                                        role="button"
                                        class="btn btn-default">
                                    <i class="fa fa-close"></i>
                                    @L("Commands.Reset")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-12" id="RolesList">
        @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}
    </div>
</div>

فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:

  $(document).on("reset", "form[data-submit-on-reset]",
            function () {
                var form = this;
                setTimeout(function () {
                    $(form).submit();
                });
            });

در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام می‌رسد.


پارشال ویو List.cshtml_

@model PagedListModel<RoleModel>
@{
    Layout = null;
    var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1;
    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
}

<div class="panel panel-default margin-bottom-5">
    <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
        <thead>
            <tr>
                <th style="width: 5%;">
                    #
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
               
                <th style="width: 5%;"></th>
            </tr>
        </thead>

        <tbody>
           @foreach (var role in Model.Result.Items)
            {
                <tr>
                    <td>@(rowNumber++.ToPersianNumbers())</td>
                    <td>@role.Name</td>
                    <td>@role.DisplayName</td>
                    <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td>
                    <td class="text-center operations">
                      
                        <div class="btn-group">

                            <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
                            <ul class="dropdown-menu dropdown-menu-left">
                                <li>
                                    <a href="#"
                                       role="button"
                                       data-ajax="true"
                                       data-ajax-method="GET"
                                       data-ajax-update="#main-modal div.modal-content"
                                       data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))"
                                       data-toggle="modal"
                                       data-target="#main-modal">
                                        <i class="fa fa-pencil"></i>
                                        @L("Commands.Edit")
                                    </a>
                                </li>
                                <li>
                                    <a href="#"
                                       role="button"
                                       id="delete-@role.Id"
                                       data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())"
                                       data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'>
                                        <i class="fa fa-trash"></i>
                                        @L("Commands.Delete")
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>

به ترتیب  فایل بالا را بررسی می‌کنیم:

    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));

refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شده‌است که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتم‌های موجود در هر صفحه) را دارد که حالت فعلی گرید را می‌توانیم دوباره از سرور درخواست کنیم.

<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">

دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم می‌باشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.

<th class="col-md-3 sortable">
    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
</th>

ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک ‎Func<RouteValueDictionary,string>‎ دریافت می‌کند.


پیاده سازی هلپر SortableColumn

        public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName,
            string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
        {
            var dictionary = queryModel.ToDictionary();

            var routeValueDictionary = new RouteValueDictionary(dictionary)
            {
                ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
                    ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
                        ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                            ? string.Empty : $"{columnName}_DESC"
            };

            var url = urlFactory(routeValueDictionary);

            var aTag = new TagBuilder("a");
            aTag.Attributes.Add("href", "#");
            aTag.Attributes.Add("data-ajax", "true");
            aTag.Attributes.Add("data-ajax-method", "GET");
            aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
            aTag.Attributes.Add("data-ajax-url", url);
            aTag.InnerHtml = columnDisplayName;

            var iconCssClass = !queryModel.SortExpression.StartsWith(columnName)
                ? "fa-sort"
                : queryModel.SortExpression.EndsWith("DESC")
                    ? "fa-sort-down"
                    : "fa-sort-up";

            var iTag = new TagBuilder("i");
            iTag.AddCssClass($"fa {iconCssClass}");

            return new MvcHtmlString($"{aTag}\n{iTag}");
        }

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

public static IDictionary<string, object> ToDictionary(this object source)
{
    return source.ToDictionary<object>();
}

public static IDictionary<string, T> ToDictionary<T>(this object source)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    var dictionary = new Dictionary<string, T>();

    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
    {
        AddPropertyToDictionary(property, source, dictionary);
    }
    return dictionary;
}

private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source,
    IDictionary<string, T> dictionary)
{
    var value = property.GetValue(source);

    var items = value as IEnumerable;

    if (items != null && !(items is string))
    {
        var i = 0;
        foreach (var item in items)
        {
            dictionary.Add($"{property.Name}[{i++}]", (T)item);
        }
    }
    else if (value is T)
    {
        dictionary.Add(property.Name, (T)value);
    }

}

در متد بالا، از TypeDescriptor که یکی دیگر از ابزار‌های دسترسی به متا دیتای انوع داده‌ای است، استفاده شده و خروجی نهایی آن یک دیکشنری با کلیدهایی با اسامی پراپرتی‌های وهله ورودی می‌باشد.

در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته می‌شود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک می‌شود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالت‌های مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:

var routeValueDictionary = new RouteValueDictionary(dictionary)
{
    ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
        ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
            ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                ? string.Empty : $"{columnName}_DESC"
};

در ادامه urlFactory با routeValueDictionary حاصل، Invoke می‌شود تا url نهایی برای مرتب سازی‌های بعدی را  از طریق یک لینک تزئین شده با data اتریبیوت‌های Unobtrusive Ajax در th مربوطه قرار دهیم.

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

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>


پیاده سازی هلپر Pager

public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model,
        string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
{
    return html.PagedListPager(
        new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize,
            (int)model.Result.TotalCount), page =>
       {
           var dictionary = model.Query.ToDictionary();
           var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page };
           return urlFactory(routeValueDictionary);
       }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing(
            new PagedListRenderOptions
            {
                DisplayLinkToFirstPage = PagedListDisplayMode.Always,
                DisplayLinkToLastPage = PagedListDisplayMode.Always,
                DisplayLinkToPreviousPage = PagedListDisplayMode.Always,
                DisplayLinkToNextPage = PagedListDisplayMode.Always,
                MaximumPageNumbersToDisplay = 6,
                DisplayItemSliceAndTotal = true,
                DisplayEllipsesWhenNotShowingAllPageNumbers = true,
                ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}",
                FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(),
            },
            new AjaxOptions
            {
                AllowCache = false,
                HttpMethod = "GET",
                InsertionMode = InsertionMode.Replace,
                UpdateTargetId = updateTargetId
            }));
}

در متد بالا از کتابخانه PagedList.Mvc استفاده شده‌است. یکی از overload‌های متد PagedListPager آن، یک پارامتر از نوع Func<int, string>‎ به نام generatePageUrl را دریافت می‌کند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما می‌دهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینک‌های تولیدی استفاده کردیم و صرفا برای لینک‌های ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص می‌باشد.


پیاده سازی هلپر PageSize

public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers)
{
    if (numbers.Length == 0)
        numbers = new[] { 10, 20, 30, 50, 100 };

    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary)
    {
        [nameof(IPagedQueryModel.Page)] = 1
    };
    routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize));

    var url = urlFactory(routeValueDictionary);

    var formTag = new TagBuilder("form");
    formTag.Attributes.Add("action", url);
    formTag.Attributes.Add("method", "GET");
    formTag.Attributes.Add("data-ajax", "true");
    formTag.Attributes.Add("data-ajax-method", "GET");
    formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    formTag.Attributes.Add("data-ajax-url", url);

    if (htmlAttributes != null)
        formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));

    formTag.AddCssClass("form-inline inline");

    var items = numbers.Select(number =>
        new SelectListItem
        {
            Value = number.ToString(),
            Text = number.ToString().ToPersianNumbers(),
            Selected = queryModel.PageSize == number
        });

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

    if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}");

    // ReSharper disable once MustUseReturnValue
    var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";

    return new MvcHtmlString($"{formTag}\n{scriptBlock}");
}

ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار می‌گیرد و در زمان change آن، فرم مربوطه submit می‌شود.

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

در زمان تغییر تعداد نمایشی آیتم‌ها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.


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

  var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";


پیاده سازی هلپر Refresh

public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel,
    Func<RouteValueDictionary, string> urlFactory, string label = null)
{
    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary);

    var url = urlFactory(routeValueDictionary);

    var aTag = new TagBuilder("a");
    aTag.Attributes.Add("href", "#");
    aTag.Attributes.Add("role", "button");
    aTag.Attributes.Add("data-ajax", "true");
    aTag.Attributes.Add("data-ajax-method", "GET");
    aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    aTag.Attributes.Add("data-ajax-url", url);
    aTag.AddCssClass("btn btn-default");

    var iTag = new TagBuilder("i");
    iTag.AddCssClass("fa fa-refresh");

    aTag.InnerHtml = $"{iTag} {label}";

    return new MvcHtmlString(aTag.ToString());
}

متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده می‌شود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary  را تغییر نداده‌ایم.


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

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

مطالب
معرفی System.Text.Json در NET Core 3.0.
معروفترین کتابخانه‌ی کار با JSON در دات نت، Json.NET است که این روزها، جزء جدایی ناپذیر حداقل، تمام برنامه‌های وب مبتنی بر دات نت می‌باشد. برای مثال ASP.NET Core 2x/1x و همچنین ASP.NET Web API پیش از NET Core.، به صورت پیش‌فرض از این کتابخانه برای کار با JSON استفاده می‌کنند. این کتابخانه 10 سال پیش ایجاد شد و در طول زمان، قابلیت‌های زیادی به آن اضافه شده‌است. همین حجم بالای کار صورت گرفته سبب شده‌است که برای مثال شروع به استفاده‌ی از <Span<T در آن برای بالابردن کارآیی، بسیار مشکل شده و نیاز به تغییرات اساسی در آن داشته باشد. به همین جهت خود تیم CoreFX دات نت Core گزینه‌ی دیگری را برای کار با JSON در فضای نام جدید System.Text.Json ارائه داده‌است که برای کار با آن نیاز به نصب وابستگی ثالثی نیست و همچنین کارآیی آن به علت استفاده‌ی از ویژگی‌های جدید زبان، مانند ref struct و Span، به طور میانگین دو برابر کتابخانه‌ی Json.NET است. برای مثال استفاده‌ی از string (حالت پیش‌فرض کتابخانه‌ی Json.NET) یعنی کار با رشته‌هایی از نوع UTF-16؛ اما کار با Span، امکان دسترسی مستقیم به رشته‌هایی از نوع UTF-8 را میسر می‌کند که نیازی به تبدیل به رشته‌هایی از نوع UTF-16 را ندارند.


ASP.NET Core 3x دیگر به صورت پیش‌فرض به همراه Json.NET ارائه نمی‌شود

در برنامه‌های ASP.NET Core 3x، وابستگی ثالث Json.NET حذف شده‌است و از این پس هر نوع خروجی JSON آن، مانند بازگشت مقادیر مختلف از اکشن متدهای کنترلرها، به صورت خودکار در پشت صحنه از امکانات ارائه شده‌ی در System.Text.Json استفاده می‌کند و دیگر Json.NET، کتابخانه‌ی پیش‌فرض کار با JSON آن نیست. بنابراین برای کار با آن نیاز به تنظیم خاصی نیست. همینقدر که یک پروژه‌ی جدید ASP.NET Core 3x را ایجاد کنید، یعنی در حال استفاده‌ی از System.Text.Json هستید.


روش بازگشت به Json.NET در ASP.NET Core 3x

اگر به هر دلیلی هنوز نیاز به استفاده‌ی از کتابخانه‌ی Json.NET را دارید، آداپتور ویژه‌ی آن نیز تدارک دیده شده‌است. برای اینکار:
الف) ابتدا باید بسته‌ی نیوگت Microsoft.AspNetCore.Mvc.NewtonsoftJson را نصب کنید.
ب) سپس در کلاس Startup، باید این کتابخانه را به صورت یک سرویس جدید، با فراخوانی متد AddNewtonsoftJson، معرفی کرد:
 public void ConfigureServices(IServiceCollection services)
 {
     services.AddControllers()
            .AddNewtonsoftJson()
     // ...
}
یکی از دلایل بازگشت به Json.NET می‌تواند عدم پشتیبانی از OpenAPI / Swagger در حین کار با System.Text.Json باشد و این مورد قرار نیست در نگارش نهایی 3.0، حضور داشته باشد و انطباق با آن به نگارش‌های بعدی موکول شده‌است.


روش کار مستقیم با System.Text.Json

اگر در قسمتی از برنامه‌ی خود نیاز به کار مستقیم با اشیاء JSON را داشته باشید و یا حتی بخواهید از این قابلیت در برنامه‌های کنسول و یا کتابخانه‌ها نیز استفاده کنید، روش انتقال کدهایی که از Json.NET استفاده می‌کنند به System.Text.Json، به صورت زیر است:
public class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public DateTime? BirthDay { get; set; }
}
تبدیل رشته‌ی JSON حاوی اطلاعات شخص، به شیء متناظر با آن و یا حالت عکس آن:
using System;
using System.Text.Json.Serialization;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = JsonSerializer.Parse<Person>(...);
            string json = JsonSerializer.ToString(person);
        }
    }
}
در اینجا از کلاس System.Text.Json.Serialization.JsonSerializer، روش کار با دو متد Parse را برای Deserialization و ToString را برای Serialization مشاهده می‌کنید.
کلاس JsonSerializer دارای overloadهای زیر برای کار با متدهای Parse و ToString است:
namespace System.Text.Json.Serialization
{
    public static class JsonSerializer
    {
        public static object Parse(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerOptions options = null);
        public static object Parse(string json, Type returnType, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(ReadOnlySpan<byte> utf8Json, JsonSerializerOptions options = null);
        public static TValue Parse<TValue>(string json, JsonSerializerOptions options = null);

        public static string ToString(object value, Type type, JsonSerializerOptions options = null);
        public static string ToString<TValue>(TValue value, JsonSerializerOptions options = null);
    }
}
یک نکته: کارآیی متد Parse با امضای ReadOnlySpan<byte> utf8Json، بیشتر است از نمونه‌ای که string json را می‌پذیرد. از این جهت که چون با آرایه‌ای از بایت‌های رشته‌ای از نوع UTF-8 کار می‌کند، نیاز به تبدیل به UTF-16 را مانند متدی که string را می‌پذیرد، ندارد. برای تولید آرایه‌ی بایت‌های utf8Json از روی یک شیء، می‌توانید از متد JsonSerializer.ToUtf8Bytes استفاده کنید و یا برای تولید آن از روی یک رشته، از متد Encoding.UTF8.GetBytes استفاده کنید.


سفارشی سازی JsonSerializer جدید

اگر به امضای متدهای Parse و ToString کلاس JsonSerializer دقت کنید، دارای یک پارامتر اختیاری از نوع JsonSerializerOptions نیز هستند که به صورت زیر تعریف شده‌است:
public sealed class JsonSerializerOptions
{
   public bool AllowTrailingCommas { get; set; }
   public int DefaultBufferSize { get; set; }
   public JsonNamingPolicy DictionaryKeyPolicy { get; set; }
   public bool IgnoreNullValues { get; set; }
   public bool IgnoreReadOnlyProperties { get; set; }
   public int MaxDepth { get; set; }
   public bool PropertyNameCaseInsensitive { get; set; }
   public JsonNamingPolicy PropertyNamingPolicy { get; set; }
   public JsonCommentHandling ReadCommentHandling { get; set; }
   public bool WriteIndented { get; set; }
}
برای نمونه معادل تنظیم NullValueHandling در Json.NET:
// Json.NET:
var settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore
};
string json = JsonConvert.SerializeObject(person, settings);
اینبار توسط خاصیت IgnoreNullValues صورت می‌گیرد:
// JsonSerializer:
var options = new JsonSerializerOptions
{
    IgnoreNullValues = true
};
string json = JsonSerializer.ToString(person, options);

در برنامه‌های ASP.NET Core که این نوع متدها در پشت صحنه فراخوانی می‌شوند، روش تنظیم JsonSerializerOptions به صورت زیر است:
services.AddControllers()
   .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);


نگاشت نام ویژه‌ی خواص در حین عملیات deserialization

در مثال فوق، فرض شده‌است که نام خاصیت BirthDay، دقیقا با اطلاعاتی که از رشته‌ی JSON دریافتی پردازش می‌شود، تطابق دارد. اگر این نام در اطلاعات دریافتی متفاوت است، می‌توان از ویژگی JsonPropertyName برای تعریف این نگاشت استفاده کرد:
[JsonPropertyName("birthdate")]
public DateTime? BirthDay { get; set; }
روش دیگر اینکار، برای نمونه تنظیم PropertyNamingPolicy به حالت CamelCase است:
var options = new JsonSerializerOptions
{
   PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string json = JsonSerializer.ToString(person, options);
و یا اگر می‌خواهید حساسیت به بزرگی و کوچکی حروف را ندید بگیرید (مانند حالت پیش‌فرض JSON.NET) از تنظیم JsonSerializerOptions.PropertyNameCaseInsensitive استفاده کنید.

در این بین اگر نمی‌خواهید خاصیتی در عملیات serialization و یا برعکس آن پردازش شود، می‌توان از تعریف ویژگی [JsonIgnore] بر روی آن استفاده کرد.
نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
با تشکر فراوان، مشکل من با Newtonsoft.Json.JsonIgnore  حل شد. 
اما من برای انتخاب تعدادی خصوصیت از کد زیر استفاده کردم، در نتیجه نهایی که در عکس مشخص است، فقط رکورد اول شامل فیلدهای مشخص شده توسط من است، فرزندان این رکورد حاوی تمام فیلدها هستند.
البته در این کدها از using استفاده نشده چون باعث خطای The ObjectContext instance میشود.
باز هم ممنون برای اختصاص وقت. 
    public partial class BlogComment
    {
        public BlogComment()
        {
            this.Children = new HashSet<BlogComment>();
        }
    
        public int Id { get; set; }
        public string Body { get; set; }
        public Nullable<System.DateTime> DateSend { get; set; }
        public Nullable<System.DateTime> DateRead { get; set; }
        public bool IsDeleted { get; set; }
        public int UserId { get; set; }

        [Newtonsoft.Json.JsonIgnore]
        public virtual BlogComment Reply { get; set; }

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

public JsonNetResult Index()
        {
            var ctx = new testEntities();
            var list = ctx.BlogComments
                .Where(p => p.Id == 1)
                .Select(p => new
                               {
                                   p.Id,
                                   p.Body,
                                   p.Children
                               })
            .ToList();

            JsonNetResult jsonNetResult = new JsonNetResult();
            jsonNetResult.Formatting = Formatting.Indented;

            jsonNetResult.SerializerSettings = new JsonSerializerSettings()
                                                   {
                                                       ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                                                   };
            jsonNetResult.Data = list;
            return jsonNetResult;
        }

مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت سوم

در این قسمت به پیاده سازی و توضیح مدل‌های انجمن خواهیم پرداخت. قبل از شروع پیشنهاد می‌کنم مقالات قبلی را مطالعه کنید.
همکاران این قسمت:
سلمان معروفی 
سید مجتبی حسینی 
پیشنیاز این قسمت:
مقالات SQL Antipattern 

سعی کردیم چندین پروژه‌ی سورس باز را هم بررسی کنیم و در نهایت کاملترین و بهترین روش را پیاده سازی کنیم. NForum ، MyBB ، MVCForum ، بخش CMS مربوط به SmartStore و ساختار دیتابیس StackOverFlow ازجمله‌ی آنها هستند.

ساختار انجمن‌ها اغلب به شکل سلسله مراتبی می‌باشد و این مورد در دسته بندی آنها خیلی مفید خواهد بود. صرف اینکه بتوان برای این مورد یک مدل خود ارجاع در نظر گرفت کاری خاصی ندارد. ولی مشکل از آنجا شروع می‌شود که بخواهیم برای انجمن هایمان مدیرانی هم تعیین کنیم یا فقط تا عمق مشخصی را واکشی کنیم و خیلی چالش برانگیزتر از اینها، اگر لازم باشد دسترسی‌های مدیران یک انجمن قابلیت اعمال بر زیرشاخه‌ها را داشته باشد و در مقابل زیرشاخه‌ها هم بتوانند از این ارث بری ممانعت کنند و از این نوع چالش‌های شیرین دیگر.
مدل انجمن
  /// <summary>
    /// Represents the Forum
    /// </summary>
    public class Forum
    {

        #region Properties
        /// <summary>
        /// gets or sets Id that Identify Forum
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets Forum's title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets Description of forum
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// gets or sets value indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets order for display forum
        /// </summary>
        public virtual long DisplayOrder { get; set; }
        /// <summary>
        /// Indicating This Forum is Active or Not
        /// </summary>
        public virtual bool IsActive { get; set; }
        /// <summary>
        /// Indicating This Forum is Close or Not
        /// </summary>
        public virtual bool IsClose { get; set; }
        /// <summary>
        /// Indicating This Forum is Private or Not
        /// </summary>
        public virtual bool IsPrivate { get; set; }
        /// <summary>
        /// sets or gets password for login to Private forums
        /// </summary>
        public virtual string PasswordHash { get; set; }
        /// <summary>
        /// sets or gets depth of forum in tree structure of forums
        /// </summary>
        public virtual int Depth { get; set; }
        /// <summary>
        /// sets or gets Count of  posts That they are Approved
        /// </summary>
        public virtual long ApprovedPostsCount { get; set; }
        /// <summary>
        /// sets or gets Count of  topics That they are Approved
        /// </summary>
        public virtual long ApprovedTopicsCount { get; set; }
        /// <summary>
        /// Gets or sets the id of last topic
        /// </summary>
        public virtual long LastTopicId { get; set; }
        /// <summary>
        /// gets or sets date of creation of last topic
        /// </summary>
        public virtual DateTime? LastTopicCreatedOn { get; set; }
        /// <summary>
        /// gets or sets title of last topic
        /// </summary>
        public virtual string LastTopicTitle { get; set; }
        /// <summary>
        /// gets or sets creator of last topic
        /// </summary>
        public virtual string LastTopicCreator { get; set; }
        /// <summary>
        /// gets or sets id of creator that create last topic
        /// </summary>
        public virtual long LastTopicCreatorId { get; set; }
        /// <summary>
        /// Indicate in this Forum Moderate Topics Before Display 
        /// </summary>
        public virtual bool ModerateTopics { get; set; }
        /// <summary>
        /// Indicate in this Forum Moderate Posts Before Dipslay
        /// </summary>
        public virtual bool ModeratePosts { get; set; }
        /// <summary>
        /// gets or sets Count of posts that they are UnApproved
        /// </summary>
        public virtual long UnApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets Count of topics that they are UnApproved
        /// </summary>
        public virtual long UnApprovedTopicsCount { get; set; }
        /// <summary>
        /// gets or sets Rowversion
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets icon name with size 200*200 px for snippet 
        /// </summary>
        public virtual string SocialSnippetIconName { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public virtual string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public virtual string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets path for tree structure antipattern (1/3/4/23)
        /// </summary>
        public virtual string Path { get; set; }
        /// <summary>
        /// Indicate this forum inherit moderators from parent forum
        /// </summary>
        public virtual bool IsModeratorsInherited { get; set; }
        /// <summary>
        /// gets or set datetime that Last Post is Created In this forum. used for ForumTracking 
        /// </summary>
        public virtual DateTime? LastPostCreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets identifier forum's parent
        /// </summary>
        public virtual long? ParentId { get; set; }
        /// <summary>
        /// sets or gets forum's parent
        /// </summary>
        public virtual Forum Parent { get; set; }
        /// <summary>
        /// sets or gets sub forums of forum
        /// </summary>
        public virtual ICollection<Forum> Children { get; set; }
        /// <summary>
        /// set or get topics of forum
        /// </summary>
        public virtual ICollection<ForumTopic> Topics { get; set; }
        /// <summary>
        /// get or set moderators of this forum
        /// </summary>
        public virtual ICollection<ForumModerator> Moderators { get; set; }
        /// <summary>
        /// get or set Subscriptions List 
        /// </summary>
        public virtual ICollection<User> Subscribers { get; set; }
        /// <summary>
        /// get or set Announcements Collection of this Forum
        /// </summary>
        public virtual ICollection<ForumAnnouncement> Announcements { get; set; }
        /// <summary>
        /// get or set Trackers List Of this Forum
        /// </summary>
        public virtual ICollection<ForumTracker> Trackers  { get; set; }
        /// <summary>
        /// get or set Posts List that Posted in this forum for increase Performance for get Posts Count 
        /// </summary>
        public virtual ICollection<ForumPost> Posts  { get; set; }
        /// <summary>
        /// get or set 
        /// </summary>
        public virtual ICollection<ForumTopicTracker> TopicTrackers  { get; set; }
        #endregion
مدل بالا نشان دهنده‌ی ساختار انجمن‌های ما می‌باشد. خصوصیت هایی که نیاز به توضیح دارند به شکل زیر می‌باشند:
  1. IsActive : مشخص کننده‌ی این است که در این انجمن امکان ارسال تاپیک و پست وجود دارد و در صورت false بودن این خصوصیت، بر تمام زیر انجمن‌ها هم اعمال خواهد شد و برای زمانی مفید است که میخواهیم برای مدتی به هر دلیل خاصی امکان ارسال تاپیک و پست را برای انجمن خاصی، ندهیم. 
  2. IsColsed : خصوصت اولی که مطرح شد اگر مقدار false بگیرد، همچنان کاربران می‌توانند تایپک‌ها و پست‌های قبلی را مشاهده و مطالعه کنند. ولی با مقدار دهی این خصوصیت با مقدار false، امکان کلیه‌ی فعالیت‌ها و مشاهده‌ای را از محتوای این انجمن و زیر انجمن‌های آن نخواهیم داشت.
  3. IsPrivate : برای مواقعی که لازم است برای انجمن خاصی کلمه‌ی عبور در نظر بگیریم تا افراد خاص که کلمه‌ی عبور آن را دارند بتوانند در آن انجمن فعالیت کنند، در نظر گرفته شده است.
  4. ApprovedPostsCount , UnApprovedPostsCount,ApprovedTopicsCount,UnApprovedTopicsCount : برای بالا بردن کارآیی سیستم به مانند مدل‌های قبل در نظر گرفته شده‌اند.
  5. LastTopicId, LastTopicTitle , LastTopicCreator , LastTopicCreatorId , LastTopicCreatedOn: همچنین برای افزایش کارآیی سیستم و نمایش به عنوان قسمتی از مشخصات قابل مشاهده از هر انجمن، در نظر گرفته شده‌اند.
  6. Depth : برای نشان دادن عمق گره در درخت استفاده می‌شود که هنگام درج انجمن، این مورد از نتیجه‌ی جمع عمق پدر انتخاب شده و یک، به دست خواهد آمد. این مورد هنگام واکشی برای مثال 4 سطح اول برای نمایش آنها در صفحه‌ی اول انجمن به صورت سلسله مراتبی خیلی مفید خواهد بود.
  7. Path : برای استفاده از SQL Antipattern شمارش مسیر در نظر گرفته شده است. این مورد جزء Best Practice‌‌ها می‌تواند باشد. چون هم با استفاده از ساختار خود ارجاع، درخت خود را داریم و با این Antipattern کوئری‌های مربوط به درخت خیلی راحت خواهد بود.
  8. IsModeratorsInherited : اگر لازم است مدیران انجمن، پدر را به عنوان مدیر خود قبول کنند، این خصوصیت مقدار true خواهد گرفت.
  9. Subscribers : هر انجمنی می‌تواند یکسری مشترک نیز داشته باشد (به منظور اطلاع رسانی با درج یک تاپیک جدید در خود انجمن یا زیر انجمن‌های آن) .
  10. Posts : به منظور افزایش کارایی هنگام محاسبه تعداد پست‌های ارسالی در یک انجمن  ، در نظر گرفته شده است.
  11. TopicTrackers : در مقاله بعد توضیح داده خواهد شد.
  12. LastPostCreatedOn : به منظور استفاده از آن برای سیستم Tracking انجمن‌ها استفاده خواهد شد . 
ساختار درختی آن هم قابل مشاهده بوده  و نیاز به توضیح خاضی ندارد. در هر انجمن ما، یکسری تاپیک مطرح خواهد شد و برای این منظور لیستی از ForumTopic را در این کلاس معرفی کرده‌ایم. 
علاوه بر اینها انجمن‌های ما مدیرانی هم خواهند داشت که برای این منظور نیز لیستی از ForumModerator را در مدل بالا تعریف کرده‌ایم. همچنین تصمیم گرفتیم امکانی را برای سیستم انجمن در نظر بگیرم تا اگر لازم بود یک سری اعلان در بالای انجمن‌ها نشان داده شوند و در بخش مدیریت بتوان این امکان را هندل کرد؛ که لیستی  است از ForumAnnouncement‌های مدل بالا.

مدل مدیران انجمن
 /// <summary>
    /// Represents The Moderator For Forum
    /// </summary>
    public class ForumModerator
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets Forum
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets identifier of forum
        /// </summary>
        public virtual long ForumId { get; set; }
        /// <summary>
        /// gets or sets user that moderate forum
        /// </summary>
        public virtual User Moderator { get; set; }
        /// <summary>
        /// gets or sets id of user that moderate forum
        /// </summary>
        public virtual long ModeratorId { get; set; }
        /// <summary>
        /// gets or sets permission of user that moderate forum
        /// </summary>
        public virtual ForumModeratorPermissions Permissions { get; set; }
        /// <summary>
        /// indicate moderator's permissions in this forum apply with
        /// </summary>
        public virtual bool ApplyChildren { get; set; }
        #endregion
    }

    [Flags]
    public enum  ForumModeratorPermissions
    {
        CanEditPosts=1,
        CanDeletePosts=2,
        CanManageTopics=4,
        CanOpenCloseTopics=8,
       ...
    }
این مدل نشان می‌دهد که کاربر x به عنوان مدیر انجمن y می‌باشد و یکسری دسترسی‌ها را نیز در این انجمن خواهد داشت.
  1. ApplyChildren : برای اعمال دسترسی‌های مدیریتی کاربر x به زیر انجمن‌های انجمن y البته اگر خصوصیت IsModeratorsInherited زیر انجمن‌های مورد نظر با مقدار true مقدار دهی شده باشد.
  2. Permissions : از نوع ForumModeratorPermissions و نگهدارنده دسترسی‌های کاربر x به عنوان مدیر انجمن y، می‌باشد.
  3. نکته : برای این مدل آی دی در نظر گرفته نشده است و از کلید مرکب متشکل از ForumId و ModeratorId استفاده خواهیم کرد. 
مدل اعلان‌های انجمن
    /// <summary>
    /// Represents the Announcement that shown Top Of Forums
    /// </summary>
    public class ForumAnnouncement
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumAnnouncement"/>
        /// </summary>
        public ForumAnnouncement()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
           CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets Identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets DateTime That this Announcement Will be Shown
        /// </summary>
        public virtual DateTime StartOn { get; set; }
        /// <summary>
        /// gets or sets DateTime That this Announcement Will be Finished 
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        /// gets or sets Content of this Announcement
        /// </summary>
        public virtual string Message { get; set; }
        /// <summary>
        /// Indicate this Announcement Will be shown on Children Forums
        /// </summary>
        public virtual bool ApplyChildren { get; set; }
        /// <summary>
        /// gets or sets datetime that this record created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Forum that associated With this Announcement
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets Identifier of Forum that associated With this Announcement
        /// </summary>
        public virtual long ForumId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی اعلان‌هایی است که می‌توان با امکان تنظیم زمان آغاز و اتمام، آنها را در صفحات انجمن‌ها نمایش داد. این امکان هم از لحاظ مدیریتی می‌تواند مفید باشد و هم اگر لازم شد، تبلیغی انجام شود. برای اعمال رابطه‌ی یک به چند، یک خصوصیت از نوع Forum با همراه ForumId را در مدل بالا تعریف کرده‌ایم.
  1. ApplyChildren : برای مشخص کردین نمایش این اعلان در زیر انجمن‌های انجمن مورد نظر
  2. ExpireOn : به این دلیل نال پذیر در نظر گرفته شده است که اگر لازم بود، در زمان مشخصی به پایان نرسد و با null مقدار دهی شود.
کلاس پایه AuditBaseEntity 
 /// <summary>
    /// Represents a base class for AuditLog
    /// </summary>
    public abstract class AuditBaseEntity
    {
        #region Properties
        /// <summary>
        /// sets or gets identifier
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets datetime that is created
        /// </summary>
        public  virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? LastModifiedOn { get; set; }
        /// <summary>
        /// gets or sets reason of Last Update for increase performance
        /// </summary>
        public virtual string LastModifyReason { get; set; }
        /// <summary>
        /// gets or sets displayName of Last Modifier  for increase performance
        /// </summary>
        public virtual string LastModifier{ get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets rowversion for synchronization problem
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        /// <summary>
        /// gets or sets count of this content's Updates
        /// </summary>
        public virtual int ModifyCount { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets creator of this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// gets or sets creator's Id of this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        #endregion
    }
این کلاس به منظور کپسوله کردن یکسری فیلد تکراری برای مدل‌هایی که نیاز است آخرین تغییر دهنده و زمان آن را ذخیره کنند، در نظر گرفته شده است و هدف از آن هیچ گونه اعمال ارث بری TPH یا TPT هم نیست.
ModifyLocked : برای زمانی مفید است که مدیریت امکان ویرایش یک مطلب را به صورت دستی غیرفعال میکند.
مدل تاپیک ها
  /// <summary>
    /// Represents the Topic in the Forums
    /// </summary>
    public class ForumTopic 
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ForumTopic"/>
        /// </summary>
        public ForumTopic()
        {
            CreatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets datetime that is created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Title Of this topic
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets name of tags that assosiated with 
        /// this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// indicate this topic is Sticky and will be shown top of forum
        /// </summary>
        public virtual bool IsSticky { get; set; }
        /// <summary>
        /// indicate this topic is closed
        /// </summary>
        public virtual bool IsClosed { get; set; }
        /// <summary>
        /// gets or sets identifier of  last post in this topic
        /// </summary>
        public virtual long LastPostId { get; set; }
        /// <summary>
        /// gets or sets identifier of Last user that post in this topic
        /// </summary>
        public virtual long LastPosterId { get; set; }
        /// <summary>
        /// gets or sets title of last Post in this topic
        /// </summary>
        public virtual string LastPostTitle { get; set; }
        /// <summary>
        /// gets or sets displayName of user that create lastpost in this topic
        /// </summary>
        public virtual string LastPoster { get; set; }
        /// <summary>
        /// gets or sets datetime that last post posted in this topic
        /// </summary>
        public virtual DateTime? LastPostCreatedOn { get; set; }
        /// <summary>
        /// indicate this topic is approved
        /// </summary>
        public virtual bool IsApproved { get; set; }
        /// <summary>
        /// indicate this topic is type of Announcements and shown in Annoucements sections
        /// </summary>
        public virtual bool IsAnnouncement { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// gets or sets count of posts that they are approved
        /// </summary>
        public virtual int ApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets count of posts that they are Unapproved
        /// </summary>
        public virtual int UnApprovedPostsCount { get; set; }
        /// <summary>
        /// gets or sets specifications of this topic's rating
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets datetime that this topic closed
        /// </summary>
        public virtual DateTime? ClosedOn { get; set; }
        /// <summary>
        /// gets or sets reason that this topic colsed
        /// </summary>
        public virtual string ClosedReason { get; set; }
        /// <summary>
        /// gets or sets count of reports
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// indicate the posts of this topic should be Moderate Before Dipslay
        /// </summary>
        public virtual bool ModeratePosts { get; set; }
        /// <summary>
        /// gets or sets Level of this topic
        /// </summary>
        public virtual ForumTopicLevel Level { get; set; }
        /// <summary>
        /// gets or sets type of this topic
        /// </summary>
        public virtual ForumTopicType Type { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets Collection of tags that associated with this topic
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        /// <summary>
        /// gets or sets forum
        /// </summary>
        public virtual Forum Forum { get; set; }
        /// <summary>
        /// gets or sets identifier of Forum
        /// </summary>
        public virtual long ForumId { get; set; }
        /// <summary>
        /// gets or sets Posts Of this topic
        /// </summary>
        public virtual ICollection<ForumPost> Posts { get; set; }
        /// <summary>
        /// get or set Subscriptions List 
        /// </summary>
        public virtual ICollection<User> Subscribers { get; set; }
        /// <summary>
        /// get or set Trackkers list of this Topic
        /// </summary>
        public virtual ICollection<ForumTopicTracker> Trackers { get; set; }
        /// <summary>
        /// gets or sets creator of this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// gets or sets creator's Id of this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        #endregion
    }

 public enum ForumTopicType
    {
        Non,
        Tutorial,
        Conversation,
        Question,
        News,
        Article
    }

    public enum ForumTopicLevel
    {
        Professional,
        Intermediate,
        Beginner
    }
مدل بالا مشخص کننده‌ی تاپیک‌های انجمن می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  1. LastPostId , LastPosterId, LastPoster  , LastPostTitle  ,LastPostCreatedOn: برای افزایش کارآیی سیستم در نظر گرفته شده‌اند.
  2. ModeratePosts : اگر لازم است پست‌های یک تاپیک خاص، قبل از نمایش مدیریت شوند، با true مقدار دهی خواهد شد.
  3. Tags : لیستی از برچسب‌ها که برای اعمال رابطه‌ی چند به چند با مدل برچسب‌های معرفی شده‌ی در مقاله اول، در نظر گرفته شده است.
  4. Posts : در هر تاپیکی یک سری پست به عنوان جواب‌های آن ارسال خواهد شد.
  5. Subscribers  : به مانند انجمن‌ها، تاپیک‌های ما هم می‌توانند یک سری مشترک داشته باشند، تا از تغییرات این تاپیک مطلع شوند .
  6. Trackers : مربوط به سیستم Tracking تاپیک میباشد. و در مقاله بعد توضیح داده خواهد شد.

 حتما لازم خواهد بود تاریخچه‌ی تغییرات برای  پست‌های ارسالی ذخیره شوند؛ در مقاله‌ی بعدی به این موضوع هم خواهیم پرداخت.
نتیجه این قسمت

نظرات مطالب
استفاده از خواص راهبری در Entity framework بجای Join نویسی
با سلام
اگه یک کلاس مخاطب با کد زیر باشه:
  public class Contact
    {
        public int ContactId { get; set; }
        public string FName { get; set; }
        public string LName { get; set; }
        public string FatherName { get; set; }
        public string Email { get; set; }
       public virtual ICollection<Phone> Phones { get; set; } 
}
و یک کلاس هم برای شماره تلفن‌ها با کد زیر:
    public class Phone
    {
        public int PhoneId { get; set; }
        public string PhoneNumber { get; set; }
        public string PhoneNote { get; set; }
        public string PhoneAddress { get; set; }
        public int PhoneTypeId { get; set; }
        public virtual PhoneType PhoneType { get; set; }

        [ForeignKey("ContactId")]
        public virtual Contact Contact { get; set; }
        public int ContactId { get; set; }
    }
حالا در زمان جستجو من از کد زیر استفاده نموده ام :
var listContacts = db.Contacts.Include(p => p.Phones).AsQueryable();
            if (searchContact.ByName)
                listContacts = listContacts.Where(c => c.LName.Contains(searchContact.Name));
            if (searchContact.ByNumber)
            {
                listContacts = listContacts.Where(c=>c.);
               
            }
            var phonelistmodel = await
                 listContacts.OrderBy(p => p.ContactId)
                     .Skip(page * count)
                     .Take(count)
                     .Select(c => new ListPhoneNumberViewmodel()
                     {
                         ContactId = c.ContactId,
                         Email = c.Email,
                         Name = c.FName + " " + c.LName,
                         Phones = c.Phones
                     }).ToListAsync();
ولی اصلا به اطلاعات جدول phone دسترسی ندارم؟
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers
نکته تکمیلی
پر کردن مقدار SelectListItem سمت سرور با متود سفارشی :
public static class CommonExtensionMethods
{
    public static List<SelectListItem> CreateSelectListItem<T>(
        this List<T> items,
        object selectedItem = null,
        bool addChooseOneItem = true,
        string firstItemText = "انتخاب کنید",
        string firstItemValue = 0
    )
    {
        var modelType = items.First().GetType();

        var idProperty = modelType.GetProperty("Id");
        var titleProperty = modelType.GetProperty("Title");
        if (idProperty is null || titleProperty is null)
            throw new ArgumentNullException(
                $"{typeof(T).Name} must have ```Id``` and ```Title``` propeties");

        var result = new List<SelectListItem>();
        if (addChooseOneItem)
            result.Add(new SelectListItem(firstItemText, firstItemValue));
        foreach (var item in items)
        {
            var id = idProperty.GetValue(item)?.ToString();
            var text = titleProperty.GetValue(item)?.ToString();
            var selected = selectedItem?.ToString() == id;
            result.Add(new SelectListItem(text, id, selected));
        }

        return result;
    }
}  
نحوه استفاده :
مدلی که AllMainCategories برگشت میدهد:
public class ShowCategory
{
    public int Id { get; set; }

    public string Title { get; set; }
}
public async Task<IActionResult> Add()
{
    var categories = await _categoryService.AllMainCategories();
    ViewBag.MainCategories = categories.ToList().CreateSelectListItem(firstItemText: "خودش سر دسته باشد");
    return View();
}

[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Add(AddCategoryViewModel model)
{
    if (!ModelState.IsValid)
    {
        var categories = await _categoryService.AllMainCategories();
        ViewBag.MainCategories = categories.ToList()
            .CreateSelectListItem(model.ParentId, firstItemText: "خودش سر دسته باشد");
        ModelState.AddModelError(string.Empty, PublicConstantStrings.ModelStateErrorMessage);
        return View(model);
    }
    await _categoryService.AddAsync(new Category()
    {
        Title = model.Title,
        ParentId = model.ParentId == 0 ? null : model.ParentId
    });
    await _uow.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}
مطالب
آشنایی با Refactoring - قسمت 7


یکی دیگر از روش‌های Refactoring ، معرفی کردن یک کلاس بجای پارامترها است. عموما تعریف متدهایی با بیش از 5 پارامتر مزموم است:

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.Before
{
public class Registration
{
public void Create(string name, DateTime date, DateTime validUntil,
IEnumerable<string> courses, decimal credits)
{
// do work
}
}
}

در این حالت بجای تعریف این تعداد بالای پارامترهای مورد نیاز، تمام آن‌ها را تبدیل به یک کلاس کرده و استفاده می‌کنند:

using System;
using System.Collections.Generic;

namespace Refactoring.Day7.IntroduceParameterObject.After
{
public class RegistrationContext
{
public string Name {set;get;}
public DateTime Date {set;get;}
public DateTime ValidUntil {set;get;}
public IEnumerable<string> Courses {set;get;}
public decimal Credits { set; get; }
}
}

namespace Refactoring.Day7.IntroduceParameterObject.After
{
public class Registration
{
public void Create(RegistrationContext registrationContext)
{
// do work
}
}
}

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

روش دیگر تشخیص نیاز به این نوع Refactoring ، یافتن پارامترهایی هستند که در یک گروه قرار می‌گیرند. برای مثال:

public int GetIndex(int pageSize, int pageNumber, ...) { ...

همانطور که ملاحظه می‌کنید تعدادی از پارامترها در اینجا با کلمه page شروع شده‌اند. بهتر است این‌ پارامترهای مرتبط را به یک کلاس مجزا به نام Page انتقال داد.