c# - 将域模型子计数投影到 View 模型上,无需额外查询

标签 c# asp.net-mvc entity-framework

假设我使用以下 Entity Framework 实体:

public class Country : DomainObject<int>
{
    private ICollection<StateOrProvince> _statesOrProvinces;

    public string Name { get; set; }
    public string Abbreviation { get; set; }

    public virtual ICollection<StateOrProvince> StatesOrProvinces
    {
        get { return _statesOrProvinces ?? (_statesOrProvinces = new List<StateOrProvince>()); }
        protected set { _statesOrProvinces = value; }
    }
}

public class StateOrProvince : DomainObject<int>
{
    public int CountryId { get; set; }
    public virtual Country Country { get; set; }
    public string Name { get; set; }
    public string Abbreviation { get; set; }
}

我想为我的表示层投影到以下 View 模型:

public class CountryListModel
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }

    public string Abbreviation { get; set; }

    [Display(Name = "States/Provinces")]
    public int StatesOrProvincesCount { get; set; }
}

如您所见,我的模型上有一个 StateOrProvincesCount 属性,该属性应该表示我的国家/地区实体上 StatesOrProvinces 列表的聚合计数。

为了保持 MVC Controller 代码精简,我为域实体创建了静态映射扩展方法。用于映射到 CountryListModel 的模型如下所示:

public static CountryListModel MapToListModel(this Country country) 
{
    return new CountryListModel 
    { 
        Id = country.Id, 
        Name = country.Name, 
        Abbreviation = country.Abbreviation, 
        StatesOrProvincesCount = country.StatesOrProvinces.Count
    };
 }

然后我尝试按如下方式使用它:

var models = _countries.OrderBy(x => x.Name).Select(x => x.MapToListModel()).ToList();

但是,这引发了异常LINQ to Entities 无法识别“CountryListModel MapToListModel(Country)”方法,并且此方法无法转换为存储表达式。 我的假设是这是IQueryable 存在无法将代码转换为 SQL 的问题。

然后我尝试了:

var models = _countries.AsEnumerable().OrderBy(x => x.Name).Select(x => x.MapToListModel()).ToList();

这并没有引发错误,并给了我想要的结果。但是,当查看 SQL Express Profiler 时,我可以看到这会导致 (N+1) 查询被发送到数据库。首先,它查询以获取国家/地区列表,然后查询以选择每个国家/地区的所有州/省。

如果您直接在 .Select 方法中丢弃映射扩展和项目,则只有一个查询,并且它直接在 SQL 中执行 Count 而不是返回然后计算州/省:

var models = _countries.OrderBy(x => x.Name).Select(x => new CountryListModel 
{ 
    Id = x.Id, 
    Name = x.Name, 
    Abbreviation = x.Abbreviation, 
    StatesOrProvincesCount = x.StatesOrProvinces.Count 
}).ToList();

这太棒了,但这只是一个快速原型(prototype)。从长远来看,我想将事物分成不同的层(例如 - 核心、数据、业务、演示)。

从我在此过程中学到的知识来看,数据层必须知道我正在投影的 View 模型才能有效地查询。在这种情况下, View 模型是否与域实体一起属于核心/公共(public)项目?我应该创建额外的 DTO 对象并映射到这些对象上吗?您在项目中如何处理这个问题?

最佳答案

我不认为你的解决方案是错误的。我以前用过它,有时最终会编写自己的 SQL 并传递给 EF。有时,这比尝试调整 EF 来创建您想要的要容易得多。在这种情况下,EF 对您的扩展方法根本无能为力。对于评论中提到的解决方案,由于扩展方法中的 country.StatesOrProvinces.Count,您将遇到 N+1。如果您不能将其扁平化,那么编写 SQL 可能是一个不错的选择。

In the long run I want to separate things into layers (e.g. - Core, Data, Business, Presentation).

我在过去的项目中曾多次尝试实现这一目标,使用了各种存储库、UnitOfWorks、ViewModels等。结果不太令人满意。它常常导致数据访问层和服务层臃肿,到处违反单一责任原则,增加了更多的维护麻烦。这个post罗布·康纳利 (Rob Conery) 的著作让我有了不同的想法。保持简单比你不需要的教条优化更重要。两者都讨论了解决方案,命令/查询对象(不一定是完整的 CQRS)和在请求的生命周期中使用 DataContext 效果很好,并且随着项目的生命周期而良好地成长。

In cases such as this do the View Models belong in the Core/Common project along with the Domain entities? Should I create additional DTO objects and map onto those? How do you handle this in your projects?

我强烈建议您看看 Jimmy Bogard 对 Contoso University App 的看法。 。他正在使用MediatoR 。它解耦得很好。值得一试。

没有 Elixir 。在开始收紧代码之前,我要记住的事情是简单性、可测试性和性能。总是存在将“通用代码”放在可由多个事物访问的某个地方的诱惑。在创建此类实体之前,我会考虑更改的原因。

我希望这会有所帮助。

关于c# - 将域模型子计数投影到 View 模型上,无需额外查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29637516/

相关文章:

c# - 如何将默认命名空间添加到加载的 XmlDocument 以便验证 "works"

c# - 贝塞尔曲面中的自动内部边缘

c# - 如何从 C++ 后台任务(Windows 通用应用程序)调用 C# 函数?

asp.net-mvc - Market Place Paypal 整合

c# - MVC 5 应用程序设置

entity-framework - 更新 Entity Framework 对象

c# - Windows 后台服务可根据需要按名称终止进程

asp.net-mvc - N次登录失败后如何显示验证码?

c# - 无法使用 SetModelValue 修改 ModelState

c# - MySQL 和 C# Entity Framework "ProviderIncompatibleException"