正文转自:http://blog.jobbole.com/101270/

初稿出处: Steve
Smith
   

ASP.NET Core 1.0 是 ASP.NET
的一点一滴重复编辑,那些新框架的重大目标之一就是更多的模块化设计。即,应用应该可以仅使用其所需的框架部分,方法是框架在它们呼吁时提供倚重关系。另外,使用
ASP.NET Core
构建利用的开发人士应该力所能及接纳这一相同效用保障其使用松散耦合和模块化。借助
ASP.NET MVC,ASP.NET
团队极大地进步了框架的协助以便编写松散耦合代码,但仍至极容易落入紧密耦合的骗局,尤其是在支配器类中。

紧密耦合

紧凑耦合适用于演示软件。倘使你看一下认证什么构建 ASP.NET MVC(版本 3 到
5)站点的金榜题名示范应用程序,你很可能会找到如下所示代码(从 NerdDinner MVC
4 示例的 DinnersController 类):

 

 

 

 

 

 

C#

 

private NerdDinnerContext db = new NerdDinnerContext(); private const
int PageSize = 25; public ActionResult Index(int? page) { int pageIndex
= page ?? 1; var dinners = db.Dinners .Where(d => d.EventDate >=
DateTime.Now).OrderBy(d => d.EventDate); return
View(dinners.ToPagedList(pageIndex, PageSize)); }

1
2
3
4
5
6
7
8
9
private NerdDinnerContext db = new NerdDinnerContext();
private const int PageSize = 25;
public ActionResult Index(int? page)
{
  int pageIndex = page ?? 1;
  var dinners = db.Dinners
  .Where(d => d.EventDate >= DateTime.Now).OrderBy(d => d.EventDate);
  return View(dinners.ToPagedList(pageIndex, PageSize));
}

这类代码难以举办单元测试,因为 NerdDinnerContext
作为类的协会的一局部而创办,并索要一个要连续的数据库。毫无疑问,这种演示应用程序平常不包括其他单元测试。不过,你的应用程序可能会从局部单元测试获益,尽管你不是测试驱动开发,但极致是编辑代码以便举行测试。

除此以外,此代码违反了切勿重复 (DRY)
原则,因为各样执行其它数据访问的支配器类都在其中所有同等的代码以创建Entity Framework (EF)
数据库上下文。这使以后变动的工本更高且更易于失误,尤其是随着岁月的推移应用程序不断增强。

在查看代码以评估其耦合度时,请记住这句话“新重点字就是粘附”。
也就是说,在收看“新”关键字实例化类的其他地方,应意识到您正在将您的落实粘贴到该特定实现代码。依赖关系注入原则
(bit.ly/DI-Principle) 提出:
“抽象不应依赖于详细消息,详细信息应借助于于肤浅。”
在本示例中,控制器怎么样将数据整合在一块儿以扩散视图的详细音讯倚重于如何获取此数额(即
EF)的详细音讯。

除却新重点字外,“墨守成规”是造成紧密耦合的另一个缘故,使得应用程序更加难以开展测试和护卫。在上述示范中,执行总结机的连串时钟上设有一个依靠关系,其模式为对
Date提姆(Tim)e.Now 的调用。此耦合度可能造成难以创造一组用于某些单元测试的测试
Dinners,因为其 伊芙ntDate
属性需要相对于当下时钟的装置开展安装。有多种主意可以将耦合度从此方法中除去,其中最简便的法子就是让再次回到Dinners 的其它新抽象来处理这一问题,因而,那不再是此方法的一有些。

另外,我赋予此值一个参数,由此方法恐怕会在提供的 Date提姆(Tim)e 参数后回来所有
Dinners,而不是始终使用
Date提姆(Tim)e.Now。最终,我创造当前光阴的悬空,并透过该抽象引用当前时光。假若应用程序日常引用
Date提姆e.Now,这将是一个科学的法门。(另外还应该注意,由于这一个 Dinners
可能会产出在不同时区中,所以在实际上利用中 Date提姆(Tim)eOffset
类型可能是一个更好的挑选)。

诚实

那类代码在可维护性方面的另一个问题在于它对它的协作者并不诚实。你应制止编制可在低效状态中实例化的类,因为这个类平时会化为错误的起点。因而,类为了举行其职责所需的全部都应透过其构造函数提供。如显式倚重关系原则
(bit.ly/ED-Principle)
所述:“方法和类应显式要求正常干活所需的任何合作对象。”

DinnersController
类唯有一个默认构造函数,这意味它应当不需要此外协作者就能健康干活。但是假若您测试那多少个类会发生咋样情状?
假若您从引用 MVC
项目标新控制台应用程序运行此类,那一个代码会执行什么样操作?

 

 

 

 

 

 

C#

 

var controller = new DinnersController(); var result =
controller.Index(1);

1
2
var controller = new DinnersController();
var result = controller.Index(1);

在这种情状下,代码执行的首先个操作就是尝试实例化 EF 上下文。代码引发
InvalidOperationException:
“应用程序配置文件中找不到名为‘NerdDinnerContext’的连天字符串。”
我被骗了! 该类需要比其构造函数所阐明的更多内容才能健康办事。
如若类需要一种访问 Dinner
实例集合的模式,则应通过其构造函数举行呼吁(或者,作为其艺术上的参数)。

依靠关系注入

依靠关系注入 (DI)
引用将某个类或措施的倚重关系用作参数传递的技巧,而不是透过新的或静态调用对那一个涉嫌举办硬编码。这是
.NET
开发中一种更加广泛的技能,因为该技能向利用此技能的应用程序提供分离。ASP.NET
的早期版本没有动用 DI,固然 ASP.NET MVC 和 Web API 在支撑 DI
的问题上拿到了一些拓展,但都并未变化对成品的通通匡助,包括用于管理看重关系及其对象生命周期的容器。借助
ASP.NET Core 1.0,DI 不仅收获现成援助,还被产品自己广泛应用。

ASP.NET Core 不仅协理 DI,它还包括一个 DI 容器—又叫做控制反转 (IoC)
或劳动容器。每个 ASP.NET Core 应用使用 Startup 类的 Configure瑟维斯(Service)(Service)s
方法中的此容器配置其借助关系。此容器提供所需的骨干帮助,但万一需要,可将其替换为自定义实现。而且,EF
Core 还提供对 DI 的放置辅助,因而,在 ASP.NET Core 应用程序中配备 DI
就像调用扩充方法一致简单。我为本文创制了 NerdDinner 衍生,称为
GeekDinner。配置 EF Core,如此处所示:

 

 

 

 

 

 

C#

 

public void ConfigureServices(IServiceCollection services) {
services.AddEntityFramework() .AddSqlServer()
.AddDbContext<GeekDinnerDbContext>(options =>
options.UseSqlServer(ConnectionString)); services.AddMvc(); }

1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{
  services.AddEntityFramework()
  .AddSqlServer()
  .AddDbContext<GeekDinnerDbContext>(options =>
  options.UseSqlServer(ConnectionString));
  services.AddMvc();
}

布置好后,即可非凡轻松地应用 DI 从诸如 DinnersController 的操纵器类请求
GeekDinnerDbContext 的实例。

 

 

 

 

 

 

C#

 

public class DinnersController : Controller { private readonly
GeekDinnerDbContext _dbContext; public
DinnersController(GeekDinnerDbContext dbContext) { _dbContext =
dbContext; } public IActionResult Index() { return
View(_dbContext.Dinners.ToList()); } }

1
2
3
4
5
6
7
8
9
10
11
12
public class DinnersController : Controller
{
  private readonly GeekDinnerDbContext _dbContext;
  public DinnersController(GeekDinnerDbContext dbContext)
  {
  _dbContext = dbContext;
  }
  public IActionResult Index()
  {
  return View(_dbContext.Dinners.ToList());
  }
}

请留心,没有新重点字的单个实例,控制器所急需的依靠关系总体通过其构造函数传入,并且
ASP.NET DI
容器会代我负责处理此过程。在专注于编写应用程序时,我不必担心经过其构造函数完成自身的类请求的依赖性关系所涉及的探测。

自然,假若自身情愿,我可以自定义此表现,甚至可以用任何实现完全替换默认容器。因为自身的控制器类现在按照显式依赖关系原则,我精通要想使控制器类正常工作,我需要为其提供一个
GeekDinnerDbContext 实例。通过对 DbContext
举行局部装置,我得以独立实例化控制器,如此控制台应用程序所示:

 

 

 

 

 

 

C#

 

var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(Startup.ConnectionString); var dbContext =
new GeekDinnerDbContext(optionsBuilder.Options); var controller = new
DinnersController(dbContext); var result = (ViewResult)
controller.Index();

1
2
3
4
5
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseSqlServer(Startup.ConnectionString);
var dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
var controller = new DinnersController(dbContext);
var result = (ViewResult) controller.Index();

社团 EF Core DbContext 所波及的操作要比协会 EF6 DbContext
稍微多一些,后者只需一个接连字符串。这是因为,就像 ASP.NET Core 一样,EF
Core 已规划得更其模块化。平日状态下,你无需直接处理
DbContextOptionsBuilder,因为当您通过扩展方法(如 AddEntityFramework 和
AddSqlServer)配置 EF 时会在后台使用它。

不过否对它举行测试?

手动测试你的应用程序分外首要—你期望可以运转应用程序,查看它是不是确实运行并爆发预想的输出。然则,每回进行更改都不可能不开展测试很浪费时间。相比较紧密耦合应用,松散耦合应用程序的最大好处之一是它们更契合举行单元测试。更妙的是,相比较其前身,ASP.NET
Core 和 EF Core 都更便于举行测试。

先是,我将通过传播已安排为利用内存存储的 DbContext
来从来指向控制器编写一个概括测试。我将应用 DbContextOptions 参数来安排
GeekDinnerDbContext,它经过其构造函数公开为自家的测试的设置代码的一局部:

 

 

 

 

 

 

C#

 

var optionsBuilder = new
DbContextOptionsBuilder<GeekDinnerDbContext>();
optionsBuilder.UseInMemoryDatabase(); _dbContext = new
GeekDinnerDbContext(optionsBuilder.Options); // Add sample data
_dbContext.Dinners.Add(new Dinner() { Title = “Title 1” });
_dbContext.Dinners.Add(new Dinner() { Title = “Title 2” });
_dbContext.Dinners.Add(new Dinner() { Title = “Title 3” });
_dbContext.SaveChanges();

1
2
3
4
5
6
7
8
var optionsBuilder = new DbContextOptionsBuilder<GeekDinnerDbContext>();
optionsBuilder.UseInMemoryDatabase();
_dbContext = new GeekDinnerDbContext(optionsBuilder.Options);
// Add sample data
_dbContext.Dinners.Add(new Dinner() { Title = "Title 1" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 2" });
_dbContext.Dinners.Add(new Dinner() { Title = "Title 3" });
_dbContext.SaveChanges();

在我的测试类中举行上述配置后,即可轻松编写一个测试,彰显正确的数目已回到到
ViewResult 的模型中:

 

 

 

 

 

 

C#

 

[Fact] public void ReturnsDinnersInViewModel() { var controller = new
OriginalDinnersController(_dbContext); var result = controller.Index();
var viewResult = Assert.IsType<ViewResult>(result); var viewModel
= Assert.IsType<IEnumerable<Dinner>>(
viewResult.ViewData.Model).ToList(); Assert.Equal(1, viewModel.Count(d
=> d.Title == “Title 1”)); Assert.Equal(3, viewModel.Count); }

1
2
3
4
5
6
7
8
9
10
11
[Fact]
public void ReturnsDinnersInViewModel()
{
  var controller = new OriginalDinnersController(_dbContext);
  var result = controller.Index();
  var viewResult = Assert.IsType<ViewResult>(result);
  var viewModel = Assert.IsType<IEnumerable<Dinner>>(
  viewResult.ViewData.Model).ToList();
  Assert.Equal(1, viewModel.Count(d => d.Title == "Title 1"));
  Assert.Equal(3, viewModel.Count);
}

当然,这里还没有大气的逻辑以供测试,由此,本测试不会真正举办那么多测试。批评家们会反驳这不是异常有价值的测试,我同意他们的见解。不过,这是装有更多逻辑时展开操作的起源,因为快速就会有更多逻辑。但首先,即使EF Core
可以经过其内存选项补助单元测试,我仍会防止直接耦合到自身的控制器中的
EF。没有理由经过数量访问基础结构问题来耦合 UI
问题—实际上,这违背了另一条规则,即关注点分离原则。

永不借助于你不行使的情节

接口分隔原则 (bit.ly/LS-Principle)
提议类应仅凭借于它们其实采纳的效用。对于启用 DI 的新 DinnersController
而言,它仍仰仗于漫天
DbContext。可以选取仅提供必需效用的空洞,而不将控制器实现整合到 EF 中。

此操作方法真正需要哪些才能健康工作? 当然不是全体DbContext。它竟然无需访问上下文的完全 Dinners
属性。它需要的只是可以体现合适页面的 Dinner 实例。表示此内容的最简便
.NET 抽象为
IEnumerable<Dinner>。因而,我将定义一个接口,该接口仅返回IEnumerable<Dinner>,并满意 Index 方法的(大多数)要求。

 

 

 

 

 

 

C#

 

public interface IDinnerRepository { IEnumerable<Dinner> List(); }

1
2
3
4
public interface IDinnerRepository
{
   IEnumerable<Dinner> List();
}

自我将此称呼存储库,因为它符合该形式:
它抽象出像样集合的接口后的数据访问。假诺是因为某些原因,你不爱好存储库情势或称谓,你可以将其称为
IGetDinners 或 IDinner瑟维斯(Service)或者其余你欣赏的名称(我的技术审阅者建议将其名叫
ICanHasDinner)。无论你哪些为此类型命名,它都能起到平等的法力。

一切就绪后,我现在就足以调整 DinnersController 以接受将
IDinnerRepository 作为构造函数参数,而不是 GeekDinnerDbContext,并调用
List 方法,而不间接访问 Dinners DbSet:

 

 

 

 

 

 

C#

 

private readonly IDinnerRepository _dinnerRepository; public
DinnersController(IDinnerRepository dinnerRepository) {
_dinnerRepository = dinnerRepository; } public IActionResult Index() {
return View(_dinnerRepository.List()); }

1
2
3
4
5
6
7
8
9
private readonly IDinnerRepository _dinnerRepository;
public DinnersController(IDinnerRepository dinnerRepository)
{
  _dinnerRepository = dinnerRepository;
}
public IActionResult Index()
{
  return View(_dinnerRepository.List());
}

此刻,你可以转移并运行你的 Web 应用程序,但假诺你导航到 /Dinners
则会碰到特别: Invalid­OperationException: 在品味激活
GeekDinner.Controllers.DinnersController
时,无法解析类型“Geek­Dinner.Core.Interfaces.IdinnerRepository”的劳务。

我从未落实此接口,并且在自己举行落实时,我还索要配备在 DI 满足IDinnerRepository 请求时要利用的兑现。实现接口并不复杂:

 

 

 

 

 

 

C#

 

public class DinnerRepository : IDinnerRepository { private readonly
GeekDinnerDbContext _dbContext; public
DinnerRepository(GeekDinnerDbContext dbContext) { _dbContext =
dbContext; } public IEnumerable<Dinner> List() { return
_dbContext.Dinners; } }

1
2
3
4
5
6
7
8
9
10
11
12
public class DinnerRepository : IDinnerRepository
{
private readonly GeekDinnerDbContext _dbContext;
  public DinnerRepository(GeekDinnerDbContext dbContext)
  {
  _dbContext = dbContext;
  }
  public IEnumerable<Dinner> List()
{
  return _dbContext.Dinners;
}
}

请留意,这可怜适用于直接将积存库实现耦合到 EF。如若我急需换出
EF,则只需创设此接口的新实现。此实现类是自我的应用程序的功底结构的一有些,这是应用程序中自我的类信赖于特定实现的一个地方。

若要在类请求 IDinnerRepository 时将 ASP.NET Core
配置为注入正确贯彻,我索要将以下代码行添加到以前所示的 ConfigureServices
方法的最后:

 

 

 

 

 

 

C#

 

services.AddScoped<IDinnerRepository, DinnerRepository>();

1
services.AddScoped<IDinnerRepository, DinnerRepository>();

此语句要求 ASP.NET Core DI 容器在容器解析倚重于 IDinnerRepository
实例的项目时采用 DinnerRepository 实例。功能域意味着一个实例将用于
ASP.NET 处理的各种 Web
请求。还足以行使临时或纯粹生存期添加服务。在这种气象下,成效域适用,因为自己的
DinnerRepository 依赖于一致运用成效域生存期的
DbContext。下边是可用对象生存期的摘要:

  • 暂时: 新类型实例在每一回请求类型时行使。
  • 功能域: 新类型实例在给定 HTTP
    请求中展开第一次呼吁时创造,然后重用于在该 HTTP
    请求期间解析的保有继续项目。
  • 单纯性: 单一门类实例会创制两遍,并由该品种的富有继续请求使用。

放到容器襄助多种艺术,来社团它将提供的序列。最登峰造极的境况是只提供容器和档次,容器将尝试实例化该类型,提供项目运行时索要的另外借助关系。你仍可以提供
lambda 表明式用来构造类型或纯粹生存期,你可以在登记时在
ConfigureServices 中提供完全构造的实例。

趁着依赖关系注入涉及,应用程序就足以像从前一样运行。现在,如图 1
所示,我得以因此准备妥当的新抽象,使用 IDinner­Repository
接口的假想或模拟实现对其进展测试,而不在我的测试代码中直接倚重于 EF。

图 1 使用 Mock 对象测试 DinnersController

 

 

 

 

 

 

C#

 

public class DinnersControllerIndex { private List<Dinner>
GetTestDinnerCollection() { return new List<Dinner>() { new
Dinner() {Title = “Test Dinner 1” }, new Dinner() {Title = “Test Dinner
2” }, }; } [Fact] public void ReturnsDinnersInViewModel() { var
mockRepository = new Mock<IDinnerRepository>();
mockRepository.Setup(r =>
r.List()).Returns(GetTestDinnerCollection()); var controller = new
DinnersController(mockRepository.Object, null); var result =
controller.Index(); var viewResult =
Assert.IsType<ViewResult>(result); var viewModel =
Assert.IsType<IEnumerable<Dinner>>(
viewResult.ViewData.Model).ToList(); Assert.Equal(“Test Dinner 1”,
viewModel.First().Title); Assert.Equal(2, viewModel.Count); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DinnersControllerIndex
{
  private List<Dinner> GetTestDinnerCollection()
  {
  return new List<Dinner>()
  {
  new Dinner() {Title = "Test Dinner 1" },
  new Dinner() {Title = "Test Dinner 2" },
  };
  }
  [Fact]
  public void ReturnsDinnersInViewModel()
  {
  var mockRepository = new Mock<IDinnerRepository>();
  mockRepository.Setup(r =>
  r.List()).Returns(GetTestDinnerCollection());
  var controller = new DinnersController(mockRepository.Object, null);
  var result = controller.Index();
  var viewResult = Assert.IsType<ViewResult>(result);
  var viewModel = Assert.IsType<IEnumerable<Dinner>>(
  viewResult.ViewData.Model).ToList();
  Assert.Equal("Test Dinner 1", viewModel.First().Title);
  Assert.Equal(2, viewModel.Count);
  }
}

任由 Dinner
实例的列表来自何处,此测试都能健康运转。你可以重写多少访问代码以利用任何数据库、Azure
表存储或 XML
文件,并且控制器也会同样正常运行。当然,在此情形中,并从未实施很多操作,那么,你也许想了解…

实在的逻辑会怎么着?

到近年来截止,我从没真的落实任何实际的政工逻辑—这只是再次回到简单多少集合的简约方法。测试的着实价值在于,在遇见逻辑和新鲜情状时,你需要对其会按部就班预期运行满怀信心。为了说明这点,我打算将部分需求添加到我的
GeekDinner 站点。此站点将公开一个 API,允许任谁访问 dinner 的 RSVP。

唯独,dinner 将兼具可选的最大容量,并且 RSVP
不应超越这一容量。请求超过最大容量的 RSVP
的用户不应被添加到候补名单中。最后,dinner
可以指定相对于其先导时间必须接受 RSVP
的末尾时限,在此期限后它们将终止接纳 RSVP。

自身得以将此逻辑全体编码到一个操作中,但自己以为这让一个主意承担了太多责任,尤其是
UI 方法应小心于 UI
问题,而不是业务逻辑。控制器应确认它接受的输入有效,并且应保证它回到的响应适合于客户端。在此之外的核定,尤其是业务逻辑,不属于控制器。

放置业务逻辑的特等地方位于应用程序的域模型中,这不应借助于基础结构方面的题材(如数据库或
UI)。Dinner 类在保管需求中所述的 RSVP 问题时最具价值,因为它会为 dinner
存储最大容量,并通晓最近已形成了稍稍 RSVP。可是,部分逻辑还凭借于 RSVP
爆发的年月以及是否超越最后期限,因而方法也急需拜访当前光阴。

自身得以只利用
Date提姆(Tim)e.Now,但这会招致逻辑难以测试,并将本人的域模型耦合到系统时钟。另一种方法是使用
IDate提姆e 抽象并将其注入到 Dinner 实体。不过,依据我的阅历,最好使
Dinner 等实体没有借助关系,尤其是一旦你计划采用类似 EF 的 O/RM
工具将这一个实体从持久性层中领到出来。我不期望将实体的倚重关系填充为该过程的一局部,EF
肯定不能在本人一贯不执行此外代码的情状下完了这或多或少。

此刻一个常用的艺术是将逻辑从 Dinner
实体中提取出来,并将其放在可轻松注入依赖关系的某类服务(如 DinnerService或 Rsvp瑟维斯(Service))中。这往往会导致缺乏域模型反格局(bit.ly/anemic-model),不过,其中实体具有很少行为或没有表现,只是状态包。不,在这种场所下,解决方案非凡简单—方法可以将眼前光阴作为参数,并让调用代码将其传播。

经过此措施,添加 RSVP 的逻辑分外简单(请参阅
2
)。有六个测试可申明此办法按预期运行,这一个主目的在于与本文关联的演示项目中提供。

图 2 域模型中的业务逻辑

 

 

 

 

 

 

C#

 

public RsvpResult AddRsvp(string name, string email, DateTime
currentDateTime) { if (currentDateTime > RsvpDeadlineDateTime()) {
return new RsvpResult(“Failed – Past deadline.”); } var rsvp = new
Rsvp() { DateCreated = currentDateTime, EmailAddress = email, Name =
name }; if (MaxAttendees.HasValue) { if (Rsvps.Count(r =>
!r.IsWaitlist) >= MaxAttendees.Value) { rsvp.IsWaitlist = true;
Rsvps.Add(rsvp); return new RsvpResult(“Waitlist”); } } Rsvps.Add(rsvp);
return new RsvpResult(“Success”); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public RsvpResult AddRsvp(string name, string email, DateTime currentDateTime)
{
  if (currentDateTime > RsvpDeadlineDateTime())
  {
    return new RsvpResult("Failed – Past deadline.");
  }
  var rsvp = new Rsvp()
  {
    DateCreated = currentDateTime,
    EmailAddress = email,
    Name = name
  };
   if (MaxAttendees.HasValue)
  {
  if (Rsvps.Count(r => !r.IsWaitlist) >= MaxAttendees.Value)
  {
  rsvp.IsWaitlist = true;
  Rsvps.Add(rsvp);
  return new RsvpResult("Waitlist");
  }
  }
  Rsvps.Add(rsvp);
  return new RsvpResult("Success");
}

通过将此逻辑转换为域模型,我保证我的控制器的 API
方法仍旧较小并留意于其自我的题目。由此,可以轻松测试控制器是否实施它应有举办的操作,因为经过此办法创制的门径相对较少。

控制器职责

控制器的一对任务是检查 ModelState
并保管其行之有效。为了解起见,我在操作方法中举行此工作,但在巨型应用程序中,我会通过应用操作筛选器清除每个操作中的重复代码:

 

 

 

 

 

 

C#

 

[HttpPost] public IActionResult AddRsvp([FromBody]RsvpRequest
rsvpRequest) { if (!ModelState.IsValid) { return
HttpBadRequest(ModelState); }

1
2
3
4
5
6
7
[HttpPost]
public IActionResult AddRsvp([FromBody]RsvpRequest rsvpRequest)
{
  if (!ModelState.IsValid)
  {
  return HttpBadRequest(ModelState);
  }

只要 ModelState 有效,操作下一步必须运用请求中提供的标识符来提取适当的
Dinner 实例。如若操作找不到卓殊该 ID 的 Dinner
实例,它应重返“未找到”结果:

 

 

 

 

 

 

C#

 

var dinner = _dinnerRepository.GetById(rsvpRequest.DinnerId); if
(dinner == null) { return HttpNotFound(“Dinner not found.”); }

1
2
3
4
5
var dinner = _dinnerRepository.GetById(rsvpRequest.DinnerId);
if (dinner == null)
{
  return HttpNotFound("Dinner not found.");
}

在形成这些检查后,操作即可将由请求表示的政工操作委托给域模型,调用你以前看到的
Dinner 类上的 AddRsvp 方法,并在回到 OK
响应前保存域模型的更新情形(在这种情形下,为 dinner 实例及其 RSVP
集合)。

 

 

 

 

 

 

C#

 

var result = dinner.AddRsvp(rsvpRequest.Name, rsvpRequest.Email,
_systemClock.Now); _dinnerRepository.Update(dinner); return
Ok(result); }

1
2
3
4
5
6
var result = dinner.AddRsvp(rsvpRequest.Name,
  rsvpRequest.Email,
  _systemClock.Now);
  _dinnerRepository.Update(dinner);
  return Ok(result);
}

请记住,我决定 Dinner
类不应对系统时钟具有倚重关系,而挑选将目前几日子传出此措施。在控制器中,我为
currentDate提姆e 参数传入 _systemClock.Now。这是经过 DI
填充的本地字段,这还会避免控制器紧密耦合到系统时钟。

确切的做法是利用控制器上的 DI 而非域实体,因为控制器始终由 ASP.NET
服务容器创立,这将实现控制器在其构造函数中声称的其余借助关系。_systemClock
是项目 IDate提姆(Tim)e 的字段,只需几行代码即可定义和促成此字段。

 

 

 

 

 

 

C#

 

public interface IDateTime { DateTime Now { get; } } public class
MachineClockDateTime : IDateTime { public DateTime Now { get { return
System.DateTime.Now; } } }

1
2
3
4
5
6
7
8
public interface IDateTime
{
  DateTime Now { get; }
}
public class MachineClockDateTime : IDateTime
{
  public DateTime Now { get { return System.DateTime.Now; } }
}

本来,我还索要保证将 ASP.NET 容器配置为在类需要 IDate提姆e 实例时使用
MachineClockDate提姆e。此操作可以在 Startup 类的 Configure瑟维斯(Service)(Service)s
中落成,在这种场合下,即便其余对象生存期都灵验,但自我拔取单一生存期,因为一个
MachineClockDate提姆(Tim)e 实例将适用于漫天应用程序:

 

 

 

 

 

 

C#

 

services.AddSingleton<IDateTime, MachineClockDateTime>();

1
services.AddSingleton<IDateTime, MachineClockDateTime>();

在准备好这么些大概抽象后,我能按照 RSVP
是否过期来测试控制器的一言一行,并确保再次来到正确的结果。因为自己早就对
Dinner.AddRsvp
方法举行了测试,验证其按预期模式行事,我不需要通过控制器对同一行为举办多次测试来使我坚信这点,在协同工作时,控制器和域模型都能正常干活。

后续手续

下载关联的以身作则项目,查看 Dinner 和 DinnersController
的单元测试。请牢记,相相比充斥着倚重于基础结构问题的“新的”或静态方法调用的紧密耦合代码,松散耦合代码平常更易于举办单元测试。应该在您的应用程序中有意识而不是出乎意外使用“新重点字就是粘附”和新重点字。在
docs.asp.net 上询问有关 ASP.NET Core
及其对倚重关系注入的支撑。

 

相关文章

网站地图xml地图