ASP.MVC: 反映IQueryable而不是Linq to SQL的存储库,DDD如何问题

12
我想创建一个DDD仓储库,返回与Linq to SQL底层类匹配的IQueryable实体,减去任何关联。我可以轻松地使用Linq选择新的{field,field,...}投影返回没有关系的实体。如何编写仓库实体类?如果要从仓库返回一个对象,而不是Linq to SQL类,如何填充它并从Linq选择中返回多个实体?在我的ViewModel中如何引用这个返回的类?
我很新手,所以可能会有明显的错误。我是否错过了什么,应该只从仓库返回完整的实体,而不是投影?我仍然需要在发送它们之前从仓库中剥离Linq to SQL的关系。我完全偏离了正确方向吗?我真的想保持IQueryable数据类型。
例如,在我的仓库中,我的Linq to SQL代码:
public class MiniProduct
{
    public MiniProduct( string ProductNo, string Name, string Description, double Price)
    {    this.ProductNo = ProductNo;
         this.Name = Name;
         this.Description = Description;
         this.Price = Price;
    }
}

public IQueryable<MiniProduct> GetProductsByCategory( string productCategory)
{
    return ( from p in db.Products
             from c in db.Categories
             from pc in db.ProductCategories
             where c.CategoryName == productCategory &&
                   c.CatID == pc.CatID &&
                   pc.ProductID == p.ProductID
             select new { p.ProductNo, p.Name, p.Description, p.Price } );
    // how to return IQueryable<MiniProduct> instead of IQueryable<AnonymousType>???
}

在视图中(尝试强类型化ViewModel),我的模型数据类型是什么,如何从视图中引用?

<% Page Inherits="System.Web.Mvc.ViewPage<MyStore.Models.MiniProduct>" %>

编辑:

Cottsak对代码进行了优化,使其正常运行,因此他获得了复选框。但是,Mark Seemann指出这种技术会引起副作用,他的意见是,将POCO项目投影或子集化是不好的。在让代码正常运行后,我最终创建了更多的一次性实体对象,这导致了不必要的复杂性。最终,我根据Mark的建议修改了代码。

除了Cottsak的建议外:我的存储库返回值为IQueryable。页面指令模型参考类型为

Inherits="System.Web.Mvc.ViewPage<IQueryable<MyStore.Models.MiniProduct>>"

模型字段的访问方式:

Model.SingleOrDefault().ProductNo
Model.SingleOrDefault().Name
...

这导致了一个

foreach (MyStore.Models.MiniProduct myproduct in Model) {}

感谢两位回答者。


你对强类型视图的理解是正确的。你的例子是正确的。 - Matt Kocaj
2个回答

29
假设您的LINQ to SQL(L2S)类是自动生成的,并反映了您的底层数据库,简短的答案是:不要公开任何L2S类的IQueryable - 这将是一个泄漏的抽象。
稍微详细一点的答案:
仓储的整个重点是隐藏数据访问在一个抽象后面,以便您可以独立于域模型替换或变化您的数据访问代码。当您基于特定实现(您的基于L2S的数据访问组件(DAC))中定义的类型来构建存储库接口时,这是不可能的 - 即使您可以提供新的存储库接口实现,您也需要引用L2S DAC。如果您突然决定切换到LINQ to Entities或Azure表存储服务,那将不会很顺畅。
域对象应以技术中立的方式定义。最好使用普通的C#对象(POCO)进行此操作。
此外,公开IQueryable为您提供执行投影的机会。这听起来很有吸引力,但在DDD上下文中实际上相当危险。
在DDD中,我们应该设计域对象,以便它们封装领域逻辑并确保所有不变量。
例如,考虑实体的概念(不是LINQ to Entitities实体,而是DDD实体)。实体几乎总是由持久ID标识,因此一个常见的不变量是ID必须定义。我们可以编写如下基本实体类:
public abstract class Entity
{
    private readonly int id;

    protected Entity(int id)
    {
        if(id <= 0)
        {
            throw new ArgumentOutOfRangeException();
        }
        this.id = id;
    }

    public int Id
    {
        get { return this.id; }
    }
}

这样的类很好地执行了它的不变量(在本例中,ID必须是正数)。可能有许多其他更具领域特定性的不变量在特定的类中实现,但其中许多很可能与投影的概念相矛盾:您可能能够定义一个省略某些属性(如ID)的投影,但它将在运行时崩溃,因为类型的默认值(如int的0)将抛出异常。
换句话说,即使您只需要领域对象的某些属性,也只有完全填充它才有意义,因为这是您满足其不变量的唯一方式。
在任何情况下,如果您经常发现需要仅选择Domain Objects的某些部分,则可能是因为它们违反了单一责任原则。将Domain Class拆分为两个或多个类可能是一个更好的主意。
总之,暴露IQueryable听起来像是一种非常有吸引力的策略,但在当前的L2S实现中,它将导致泄漏的抽象和无血统的领域模型。
随着下一个版本的Entity Framework的推出,我们应该获得POCO支持,因此它可能会使我们更接近目标。然而,在我看来,关于这一点还没有定论。

如果您想更深入地讨论一个非常相似的主题,请参阅IQueryable is Tight Coupling


我加上“实际目的”这个警告,因为我知道DDD纯粹主义者可能不会同意我的某些设计,但我相信它包含足够的DDD来从DDD中受益,并且可以利用现有技术。特别是针对LinqToSql,我必须承认,“IQueryable”存在将POCO模型与LinqToSql紧密耦合的风险,但我也有信心,我的“Lazy*”类适当地消除了这种耦合,使得我的模型能够自行运行。 - Matt Kocaj
1
我通常不同意“IQueryable” Repo会创建一个泄漏的抽象。了解LinqToSql的一些复杂性使我同意,“IQueryable” Repo实现LinqToSql可能会创建一个泄漏的抽象,但我认为有方法可以避免它。 - Matt Kocaj
2
澄清一下:理论上,我认为暴露IQueryable的想法非常有吸引力,与DDD并不矛盾。但是,在实际实现细节方面,它(目前)失败了,因为很难将实现细节与接口解耦。也许有一天我们会到达那里,但现在我们还没有到达那里。造成泄漏抽象的不是IQueryable本身,而是IQueryable中的“T”。 - Mark Seemann
2
上次我检查时,EF4中的“POCO支持”仍然不允许非默认构造函数,所以我还没有改变我的立场。 - Mark Seemann
2
我的当前观点是不使用关系型数据库才是解决方案。 - Mark Seemann
显示剩余6条评论

8

试试这样做:

public class AnnouncementCategory : //...
{
    public int ID { get; set; }
    public string Name { get; set; }
}

在你的代码库中:

public IQueryable<AnnouncementCategory> GetAnnouncementCategories()
{
    return from ac in this._dc.AnnouncementCategories
           let announcements = this.GetAnnouncementsByCategory(ac.ID)
           select new AnnouncementCategory
           {
               ID = ac.ID,
               Name = ac.Text,
               Announcements = new LazyList<Announcement>(announcements)
           };
}

private IQueryable<Announcement> GetAnnouncementsByCategory(int categoryID)
{
    return GetAnnouncements().Where(a => a.Category.ID == categoryID);
}

这样做,我不是将其投射到匿名类型,而是投射到我的AnnouncementCategory类的一个新实例中。如果您愿意,可以忽略GetAnnouncementsByCategory函数,该函数用于链接每个类别关联的Announcement对象集合,但以一种方式使它们能够使用IQueryable进行延迟加载(即,当我最终调用此属性时,我不必调用整个集合,我可以在此基础上进行更多的LINQ筛选)。


如果您需要有关使用延迟加载类链接对象图的更多信息,请告诉我。这很有趣。 - Matt Kocaj
哇,我要把它打印出来放在我见过的最酷的代码墙上。使用let语句定义一个集合,然后在同一查询中延迟加载该集合,这真是非常了不起。你是从哪里发现这样的模式的? - Zachary Scott
哇,我没想到它是那么棒。但是我确实费了很大的力气才找到"let" 部分 - 在我使用 let 之前(和单独的函数 - 顺便说一下,这对于这个工作是绝对必要的),一切都能够运行,但除了惰性查询(延迟执行)外。在不使用 let 和函数的情况下,图形中所有链接的对象都会立即查询(就像枚举触发 LinqToSql 一样)。这种方法由 Microsoft 提供文档支持,据我所知是将延迟执行映射到另一种懒加载模式的唯一方式。我使用的懒加载类:http://bit.ly/1Aaxx5 - Matt Kocaj
但是如果您的上下文在惰性调用之前被处理,它不会崩溃吗? - Maslow
我还没有测试过那个Maslow,但这确实是一个有效的问题。我的假设是上下文的基础架构就像连接或池被重置/清除/回收时,上下文有能力再次启动连接/线程。我认为委托内部的引用足以经受住这些类型的“过期问题”。此外,在短生命周期的环境中(Web应用程序,HTTP上下文等),我不认为这应该成为数据上下文的问题。 - Matt Kocaj
需要注意的是,L2S中的DataContext特别设计了Unit Of Work模式,因此非常轻量级,适用于短生命周期和快速构建/销毁。在这种情况下,我认为这应该是架构师的关注点,而不是上下文的内部。也就是说,如果您将此Lazy委托保留太长时间,则存在更大的问题。 - Matt Kocaj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接