从Controller访问数据库实体

Ben*_*aum 18 c# asp.net-mvc entity-framework controller separation-of-concerns

TL;博士

在一个很好的设计.是否应该在单独的业务逻辑层(在asp.net MVC模型中)处理访问数据库,或者将IQueryables或DbContext对象传递给控制器​​是否可以?

为什么?各自的优点和缺点是什么?


我正在用C#构建一个ASP.NET MVC应用程序.它使用EntityFramework作为ORM.

让我们稍微简化一下这个场景.

我有一个可爱的蓬松小猫数据库表.每只小猫都有小猫图像链接,小猫蓬松指数,小猫名称和小猫ID.这些映射到EF生成的POCO调用Kitten.我可能会在其他项目中使用此类,而不仅仅是asp.net MVC项目.

我有一个KittenController应该去取最新的蓬松小猫/Kittens.它可能包含选择小猫的一些逻辑,但不是太多的逻辑.我一直在和朋友争论如何实现这一点,我不会透露双方:)

选项1:控制器中的db:

public ActionResult Kittens() // some parameters might be here
{
   using(var db = new KittenEntities()){ // db can also be injected,
       var result = db.Kittens // this explicit query is here
                      .Where(kitten=>kitten.fluffiness > 10) 
                      .Select(kitten=>new {
                            Name=kitten.name,
                            Url=kitten.imageUrl
                      }).Take(10); 
       return Json(result,JsonRequestBehavior.AllowGet);
   }
}
Run Code Online (Sandbox Code Playgroud)

选项2:单独的模型

public class Kitten{
   public string Name {get; set; }
   public string Url {get; set; }
   private Kitten(){
        _fluffiness = fluffinessIndex;
   }

   public static IEnumerable<Kitten> GetLatestKittens(int fluffinessIndex=10){ 
        using(var db = new KittenEntities()){ //connection can also be injected
            return db.Kittens.Where(kitten=>kitten.fluffiness > 10)
                     .Select(entity=>new Kitten(entity.name,entity.imageUrl))
                     .Take(10).ToList();
        }
    } // it's static for simplicity here, in fact it's probably also an object method
      // Also, in practice it might be a service in a services directory creating the
      // Objects and fetching them from the DB, and just the kitten MVC _type_ here

}
Run Code Online (Sandbox Code Playgroud)

//----Then the controller:
public ActionResult Kittens() // some parameters might be here
{
    return Json(Kittens.GetLatestKittens(10),JsonRequestBehavior.AllowGet);
}
Run Code Online (Sandbox Code Playgroud)

注意:GetLatestKittens不太可能在代码的其他地方使用,但它可能.可以使用构造函数Kitten而不是静态构建方法并更改Kittens的类.基本上它应该是数据库实体之上的一层,因此控制器不必知道实际的数据库,映射器或实体框架.

  • 每种设计有哪些优缺点?
  • 有明显的赢家吗?为什么?

注意:当然,替代方法也非常有价值.

澄清1:这在实践中不是一个微不足道的应用.这是一个具有数十个控制器和数千行代码的应用程序,这些实体不仅在这里使用,而且在数十个其他C#项目中使用.这里的例子是一个简化的测试用例.

Zir*_*rak 25

第二种方法是优越的.让我们尝试一个蹩脚的比喻:

你进入一家披萨店,走到柜台."欢迎来到McPizza Maestro Double Deluxe,我可以接受您的订单吗?" 那个疙瘩的收银员问你,他眼中的虚空有可能引诱你."是的,我会买一个带橄榄的大披萨"."好的",收银员回答,他的声音在"o"声中间嘶叫.他向厨房大喊"One Jimmy Carter!"

然后,等了一会儿,你会得到一个带橄榄的大披萨.你注意到什么特别的东西吗?收银员没有说"拿一些面团,像圣诞节那样旋转它,倒一些奶酪和番茄酱,撒上橄榄,放入烤箱约8分钟!" 想一想,这根本不是特别的.收银员只是两个世界之间的门户:想要披萨的顾客和制作披萨的厨师.对于所有收银员都知道,厨师从外星人那里得到他的披萨,或者把他们从吉米卡特那里切下来(他是一个人口减少的资源).

那是你的情况.你的收银员不是傻瓜.他知道如何制作披萨.这并不意味着他应该制作披萨,或者告诉别人如何制作披萨.这是厨师的工作.正如其他答案(特别是Florian Margaine和Madara Uchiha)所说明的那样,责任分离.该模型可能不会做太多,它可能只是一个函数调用,它甚至可能是一行 - 但这并不重要,因为控制器并不关心.

现在,让我们说主人认为比萨饼只是一种时尚(亵渎神灵!)而你又转向更现代的东西,一种奇特的汉堡包.让我们回顾一下会发生什么:

你进入一个花式汉堡关节,走到柜台."欢迎来到Le Burger Maestro Double Deluxe,我可以接受您的订单吗?" "是的,我会吃一个带橄榄的大汉堡"."好的",他转向厨房,"一个吉米卡特!"

然后,你会得到一个带橄榄的大汉堡包(ew).

  • +1优秀的答案除了让我在阅读后感到饥饿. (2认同)

Sla*_*uma 12

选项1和2有点极端,就像魔鬼和深蓝色海洋之间的选择,但如果我不得不在两者之间做出选择,我宁愿选择1.

首先,选项2将抛出运行时异常,因为实体框架不支持投影到实体中(Select(e => new Kitten(...))并且它不允许在投影中使用带参数的构造函数.现在,这个注释在这种情况下似乎有点迂腐,但是通过投射到实体并返回Kitten(或枚举Kittens),你隐藏了这种方法的真正问题.

显然,您的方法返回您要在视图中使用的实体的两个属性 - 小猫nameimageUrl.因为这些只是所有Kitten属性的选择,返回(半填充)Kitten实体是不合适的.那么,从这个方法实际返回什么类型?

  • 您可以返回object(或IEnumerable<object>)(这就是我理解您对" 对象方法 "的评论),如果您将结果传递Json(...)给稍后在Javascript中处理,那么这很好.但是你会丢失所有编译时类型信息,我怀疑object结果类型对其他任何东西都有用.
  • 您可以返回一些只包含两个属性的命名类型 - 可能称为"KittensListDto".

现在,这只是一个视图的一种方法 - 列出小猫的视图.然后你有一个详细信息视图来显示一只小猫,然后是编辑视图,然后是删除确认视图.现有Kitten实体的四个视图,每个视图可能需要不同的属性,并且每个视图都需要单独的方法和投影以及不同的DTO类型.对于Dog实体和项目中的100个实体来说,它们相同,您可能获得400种方法和400种返回类型.

并且很可能不会在这个特定视图之外的任何其他地方重复使用任何一个.你为什么要Take10周的小猫刚nameimageUrl任何地方第二次?你有第二只小猫列表视图吗?如果是这样,它将有一个原因,并且查询只是偶然和现在相同,如果一个更改另一个不一定,否则列表视图没有被正确地"重用"并且不应该存在两次.或者Excel导出使用的列表可能相同吗?但也许Excel用户希望明天有1000只小猫,而视图应该仍然只显示10.或者视图应该显示Age明天的小猫,但Excel用户不希望这样,因为他们的Excel宏将不再正确运行随着这种变化.仅仅因为两段代码是相同的,如果它们处于不同的上下文或具有不同的语义,则不必将它们分解为公共可重用组件.你最好离开它GetLatestKittensForListViewGetLatestKittensForExcelExport.或者你最好在服务层中没有这样的方法.


根据这些考虑因素,比萨店的游览作为类比,为什么第一种方法是优越的:)

"欢迎来到BigPizza,定制比萨店,我可以接受您的订单吗?" "好吧,我想要一个披萨加橄榄,但是上面是番茄酱,底部是奶酪,然后在烤箱里烘烤90分钟,直到它变成黑色,像硬花岗岩一样坚硬." "好的,先生,定制的比萨饼是我们的职业,我们会做到的."

收银员去了厨房."柜台里有一个心理学家,他想要一个比萨饼......它是一块花岗岩的岩石......等等......我们首先需要一个名字",他告诉厨师.

"不!",厨师尖叫道,"不会再说了!你知道我们已经尝过了." 他拿了一叠400页的纸,"这里我们有2005年的花岗岩岩石,但是...它没有橄榄,而是青椒......或者这里是顶级番茄 ......但客户想要它烤了半分钟." "也许我们应该把它称为TopTomatoGraniteRockSpecial?" "但它并没有把底部的奶酪考虑在内......"收银员:"这就是特别应该表达的东西." "但披萨形成的金字塔形状也很特别",厨师回答道."嗯......这很难......",绝望的收银员说.

"我的比萨已经在烤箱里了吗?",突然它从厨房门口喊道."让我们停止讨论,告诉我如何制作这种比萨饼,我们不会第二次吃这种比萨饼",厨师决定."好吧,这是一个披着橄榄的披萨,但是顶部是番茄酱,底部是奶酪,然后在烤箱里烘烤90分钟,直到它变成黑色,像硬花岗岩一样坚硬."


如果选项1通过在视图层中使用数据库上下文违反了关注点分离原则,则选项2通过在服务或业务层中使用以表示为中心的查询逻辑来违反相同的原则.从技术角度来看,它不会,但最终会得到一个服务层,除了表示层之外的"可重用".它具有更高的开发和维护成本,因为对于控制器操作中的每个必需数据,您必须创建服务,方法和返回类型.

现在,实际上可能存在经常重复使用的查询或查询部分,这就是为什么我认为选项1几乎与选项2一样极端 - 例如Where键的子句(可能会在详细信息中使用,编辑和删除确认视图) ),过滤掉"软删除"实体,由多租户架构中的租户过滤或禁用变更跟踪等.对于这种真正的重复查询逻辑,我可以想象将其提取到服务或存储库层(但可能只能重用)扩展方法)可能有意义,比如

public IQueryable<Kitten> GetKittens()
{
    return context.Kittens.AsNoTracking().Where(k => !k.IsDeleted);
}
Run Code Online (Sandbox Code Playgroud)

之后的任何其他内容(如投影属性)都是特定于视图的,我不希望在此层中拥有它.为了使这种方法成为可能,IQueryable<T>必须从服务/存储库中公开.这并不意味着select必须直接在控制器动作中.特别是胖和复杂的投影(可能通过导航属性加入其他实体,执行分组等)可以移动到IQueryable<T>在其他文件,目录甚至另一个项目中收集的扩展方法,但仍然是一个项目,作为附录表示层,它比服务层更接近它.然后一个动作可能如下所示:

public ActionResult Kittens()
{
    var result = kittenService.GetKittens()
        .Where(kitten => kitten.fluffiness > 10) 
        .OrderBy(kitten => kitten.name)
        .Select(kitten => new {
            Name=kitten.name,
            Url=kitten.imageUrl
        })
        .Take(10);
    return Json(result,JsonRequestBehavior.AllowGet);
}
Run Code Online (Sandbox Code Playgroud)

或者像这样:

public ActionResult Kittens()
{
    var result = kittenService.GetKittens()
        .ToKittenListViewModel(10, 10);
    return Json(result,JsonRequestBehavior.AllowGet);
}
Run Code Online (Sandbox Code Playgroud)

随着ToKittenListViewModel():

public static IEnumerable<object> ToKittenListViewModel(
    this IQueryable<Kitten> kittens, int minFluffiness, int pageItems)
{
    return kittens
        .Where(kitten => kitten.fluffiness > minFluffiness)
        .OrderBy(kitten => kitten.name)
        .Select(kitten => new {
            Name = kitten.name,
            Url = kitten.imageUrl
        })
        .Take(pageItems)
        .AsEnumerable()
        .Cast<object>();
}
Run Code Online (Sandbox Code Playgroud)

这只是一个基本思想和草图,另一个解决方案可能在选项1和2之间.

嗯,这一切都取决于整体架构和要求,我上面写的所有内容可能都是无用的和错误的.您是否必须考虑将来可以更改ORM或数据访问技术?控制器和数据库之间是否存在物理边界,控制器是否与上下文断开连接,是否需要通过Web服务获取数据,例如将来?这将需要一种非常不同的方法,这种方法更倾向于选项2.

这样的架构是如此不同 - 在我看来 - 你根本不能说"可能"或"不是现在,但可能在未来可能是一个要求,或者可能不会".这是项目利益相关者在进行架构决策之前必须定义的内容,因为它会大大增加开发成本,如果"可能"结果永远不会成为现实,我们就会浪费资金进行开发和维护.

我只讨论在Web应用程序中的查询或GET请求,这些应用程序很少有我称之为"业务逻辑"的东西.POST请求和修改数据是一个完全不同的故事.如果禁止在开票后可以更改订单,例如这是一般的"业务规则",无论是哪个视图或Web服务或后台进程或任何尝试更改订单的内容,通常都适用.我肯定会将订单状态检查到业务服务或任何常见组件中,而不会进入控制器.

可能存在反对IQueryable<T>在控制器操作中使用的争论,因为它与LINQ-to-Entities耦合,并且它将使单元测试变得困难.但是什么是单元测试将在不包含任何业务逻辑的控制器操作中进行测试,该控制器操作传递的参数通常来自视图,通过模型绑定或路由 - 未被单元测试覆盖 - 使用模拟存储库/服务返回IEnumerable<T>- 未测试数据库查询和访问 - 并且返回View- 未测试视图的正确呈现?


Flo*_*ine 9

这是关键词:

我可能会在其他项目中使用此类,而不仅仅是asp.net MVC项目.

控制器以HTTP为中心.它只在那里处理HTTP请求.如果要在任何其他项目(即业务逻辑)中使用模型,则控制器中不能包含任何逻辑.您必须能够脱掉模型,将其放在其他地方,并且所有业务逻辑仍然有效.

所以,不,不要从控制器访问您的数据库.它会杀死你可能获得的任何可能的重用.

当您可以使用可重用的简单方法时,是否真的想要在所有项目中重写所有db/linq请求?

另一件事:选项1中的函数有两个职责:它从mapper对象中获取结果显示它.这是太多的责任.责任清单中有一个"和".您的选项2只有一个责任:作为模型和视图之间的链接.