图 2 域模型中的业务逻辑
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 并确保其有效。为清楚起见,我在操作方法中执行此工作,但在大型应用程序中,我会通过使用操作筛选器清除每个操作中的重复代码:
  [HttpPost]
  public IActionResult AddRsvp([FromBody]RsvpRequest rsvpRequest)
  {
  if (!ModelState.IsValid)
  {
  return HttpBadRequest(ModelState);
  }
  假定 ModelState 有效,操作下一步必须使用请求中提供的标识符来提取适当的 Dinner 实例。如果操作找不到匹配该 ID 的 Dinner 实例,它应返回“未找到”结果:
  var dinner = _dinnerRepository.GetById(rsvpRequest.DinnerId);
  if (dinner == null)
  {
  return HttpNotFound("Dinner not found.");
  }
  在完成这些检查后,操作即可将由请求表示的业务操作委托给域模型,调用你之前看到的 Dinner 类上的 AddRsvp 方法,并在返回 OK 响应前保存域模型的更新状态(在这种情况下,为 dinner 实例及其 RSVP 集合)。
  var result = dinner.AddRsvp(rsvpRequest.Name,
  rsvpRequest.Email,
  _systemClock.Now);
  _dinnerRepository.Update(dinner);
  return Ok(result);
  }
  请记住,我决定 Dinner 类不应对系统时钟具有依赖关系,而选择将当前时间传入此方法。在控制器中,我为 currentDateTime 参数传入 _systemClock.Now。这是通过 DI 填充的本地字段,这还会防止控制器紧密耦合到系统时钟。
  适当的做法是使用控制器上的 DI 而非域实体,因为控制器始终由 ASP.NET 服务容器创建,这将实现控制器在其构造函数中声明的任何依赖关系。_systemClock 是类型 IDateTime 的字段,只需几行代码即可定义和实现此字段。
  public interface IDateTime
  {
  DateTime Now { get; }
  }
  public class MachineClockDateTime : IDateTime
  {
  public DateTime Now { get { return System.DateTime.Now; } }
  }
  当然,我还需要确保将 ASP.NET 容器配置为在类需要 IDateTime 实例时使用 MachineClockDateTime。此操作可以在 Startup 类的 ConfigureServices 中完成,在这种情况下,尽管任何对象生存期都有效,但我选择单一生存期,因为一个 MachineClockDateTime 实例将适用于整个应用程序:
  services.AddSingleton<IDateTime, MachineClockDateTime>();
  在准备好这个简单抽象后,我能基于 RSVP 是否过期来测试控制器的行为,并确保返回正确的结果。因为我已经对 Dinner.AddRsvp 方法进行了测试,验证其按预期方式工作,我不需要通过控制器对相同行为进行多次测试来使我确信这一点,在协同工作时,控制器和域模型都能正常工作。
  后续步骤
  下载关联的示例项目,查看 Dinner 和 DinnersController 的单元测试。请记住,相比充斥着依赖于基础结构问题的“新的”或静态方法调用的紧密耦合代码,松散耦合代码通常更容易进行单元测试。应该在你的应用程序中有意而不是意外使用“新关键字是粘附”和新关键字。在 docs.asp.net 上了解有关 ASP.NET Core 及其对依赖关系注入的支持。