65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MVC 和 Document DB 实现基于 CQRS 的架构

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (9投票s)

2013年6月22日

CPOL

8分钟阅读

viewsIcon

40482

downloadIcon

968

ASP.NET MVC 4、Ninject 和 MongoDB 的典型 CQRS 模式实现。我将演示一个充当基于 CQRS 的架构的基础构建块的示例应用程序。

MVC MongoDB Ninject CQRS

这是 ASP.NET MVC4、MongoDB 和 Ninject 的一个示例实现,可作为您要在项目中使用这些技术以及 CQRS 模式的起始模板。

目标读者

如果这些术语大部分都显得陌生,那么这篇文章可能不适合您:CQRS、Repository、Aggregate (DDD)、NuGet、单元测试、依赖注入、文档数据库。由于本文的范围,我无法详细介绍这些主题,因为这是直接实现。

Greg Young 在这里对 CQRS 进行了几段介绍:CQRS、基于任务的 UI、事件溯源 agh,如果您想要一个简单的介绍,我建议至少阅读 Greg 文章的前半部分(在事件溯源之前)。

范围和定义

在本文中,我指的是纯 CQRS 模式,而不是与 CQRS 相关的所有模式。纯 CQRS 为事件溯源等其他模式打开了大门,而事件溯源通常与 CQRS 相关。这是纯 CQRS,不包含任何其他相关模式。

 

技术选择

我选择 MongoDB 是因为它开放的许可政策和简洁性,选择 Ninject 是因为它具有许可、简洁性以及 Ninject.Extensions.Conventions 扩展,使我的代码更加 DRY(不要重复自己)。

我本可以选择 SQL Server,但出于简洁的考虑和为了传达概念,文档数据库更容易使用。您也可以调整此示例实现以在 RDBMS 上运行,只要您正确实现存储库即可。

这是 ASP.NET MVC4、MongoDB 和 Ninject 的一个示例实现,可作为起始模板,以防您要在项目中使用这些技术以及 CQRS 模式。

任务应用程序

Task App

这 meant to be a trivial application,以便将焦点从功能转移到 MVC 和 CQRS 模式。该应用程序

  • 允许查询任务。当用户单击“查询”时,如果选择了“已完成”,它将显示已完成的任务,如果没有选择,它将显示未完成的任务。这 meant to represent the start of a CQRS’s Query。
  • 在“已完成?”列中选中或取消选中任何复选框将通过 Ajax 更新任务。这 meant to represent the start of a CQRS’s Command。
  • 无法添加或删除任务。要将示例任务加载到数据库中,我在下载文件中包含了一个 MongoDB 脚本,该脚本将填充数据库。

为什么在 MVC 中使用 CQRS?

我最近写了您应该单元测试您的控制器,而不是!,其中我提供了指南,说明 MVC 控制器应该几乎没有代码,因此甚至不需要单元测试,但很难在本文的范围内展示如何实现这一点。

使用 CQRS 可以强制更好地分离关注点,并产生一个几乎为空的控制器,其中 ViewModel 在获取数据时变成查询,在发布数据时变成命令。

ASP.NET MVC with CQRS

上图是该解决方案实现的草图,当您阅读本文档和源代码时,它会变得更加清晰。

解决方案结构

VS MVC CQRS Solution

首先,当您注意到命名空间中使用“AT”时,您可以将其替换为您公司的名称,正如您已经得出的结论,呃,AT 是我的首字母。

该解决方案使用 VS 2012,包含 4 个项目

  • AT.Core:这是基础设施类所在的位置,例如 CQRS 的基础设施类。
  • AT.SampleApp.Cqrs:命令、命令处理程序、查询、查询处理程序、查询结果和域类。这是您的业务逻辑所在之处。此项目引用 AT.Core。在此示例项目中,它有一个示例查询 TasksByStatusQuery 和一个命令 ChangeTaskStatusCommand
  • AT.SampleApp.Cqrs.Test.UnitAT.SampleApp.Cqrs 项目的单元测试。它们遵循《.NET 单元测试艺术:含示例》作者:Roy Osherove 中指定的约定,我推荐您阅读,虽然它有点过时,但在撰写本文时,有一个新版本正在制作中。
  • AT.Web.Website:ASP.NET MVC4 项目,引用 AT.CoreAT.SampleApp.Cqrs

代码

我认为展示该模式的最佳方式是像调试一样逐步进行代码,并展示每个阶段发生的事情。我发现这是最快的学习方式,我们花费了我们生命的一半在调试上!

命令

用户单击“已完成?”复选框以将任务标记为已完成,这将执行以下 JavaScript/jQuery 代码

$.post("/Task/ChangeTaskStatus",
    {
    "TaskId": $(this).data("id"),
    "IsCompleted": this.checked,
    "UpdatedOn": fomattedNow
    }
);

命令和控制器

POST 请求到达 ChangeTaskStatus 操作方法,并成为一个命令

public class TaskController : Controller
{
    private readonly IQueryDispatcher _queryDispatcher;
    private readonly ICommandDispatcher _commandDispatcher;

    public TaskController(IQueryDispatcher queryDispatcher, 
                          ICommandDispatcher commandDispatcher)
    {
        _queryDispatcher = queryDispatcher;
        _commandDispatcher = commandDispatcher;
    }

    [HttpPost]
    public ActionResult ChangeTaskStatus(ChangeTaskStatusCommand command)
    {
        _commandDispatcher.Dispatch(command);

        return new HttpStatusCodeResult(HttpStatusCode.Accepted);
    }

    // other action methods
}

ChangeTaskStatus 方法中较容易的部分开始,操作方法向客户端返回一个 HTTP 状态码,表示一切正常。

命令分发器

_commandDispatcher.Dispatch 接收一个命令,并依赖依赖注入来查找该命令的处理程序。在这种情况下,它会将 ChangeTaskStatusCommandChangeTaskStatusCommandHanlder 匹配。

为什么要依赖注入而不是直接调用命令处理程序?简单的答案:解耦。控制器不了解太多正在发生的事情,它只是分发(有些人称之为发布,但发布可能意味着异步调用或总线调用)一个命令“给所有相关方”,而适当的处理程序将接收并处理它。

_commandDispatcher 也被注入到控制器中,我们将在后面更详细地看到它的作用,但在那之前,让我们先看看依赖注入器的代码,为此,我使用了 Ninject 和来自 NuGet 的 Ninject.Extensions.Convention,这是我使用的内核代码

kernel.Bind(x => x.FromAssembliesMatching("AT.Core.dll", "AT.Web.Website.dll")
      .SelectAllClasses().BindDefaultInterface());

BindDefaultInterface 简单地说就是按约定将 ISomething 绑定到 Something,因此,第一行将分发器注入控制器。

现在,这个分发方法是如何工作的?它如何将命令与其处理程序匹配?

public void Dispatch<TParameter>(TParameter command) where TParameter : ICommand
{
    var handler = _kernel.Get<ICommandHandler<TParameter>>();
    handler.Execute(command);
}

_kernel 是 Ninject,它在运行时查询传入命令的处理程序的句柄,然后调用其 Execute 方法将命令传递给它。

您一定喜欢依赖注入器促进解耦的事实,但您的模块会与之耦合!也许我们需要一个二级依赖注入器来解耦第一个。但是,Ninject 有一个很酷的标志,所以我不在乎我的模块依赖于它 :)

命令处理程序

command handler

业务逻辑就生活在这里。处理程序可能会更改状态,保存到数据库(更准确地说,通过存储库持久化),调用另一个处理程序等……

这是 ChangeTaskStatusCommandHanlder 的 Execute 方法

public void Execute(ChangeTaskStatusCommand command)
{
    if (command == null) { throw new ArgumentNullException("command"); }
    if (string.IsNullOrWhiteSpace(command.TaskId)) { 
        throw new ArgumentException("Id is not specified", "command"); 
    }

    var task = _taskRepository.All().Single(x => x.Id == command.TaskId);
    task.IsCompleted = command.IsCompleted;
    task.LastUpdated = command.UpdatedOn;

    _taskRepository.Update(task);
}

请注意,我们在这里进行的业务逻辑非常简单,并未展示 CQRS 模式的真正强大之处,但它确实为您提供了一个想法。

通过 Ninject 进行绑定(这段代码将适用于我所有的处理程序,而不仅仅是这个)

kernel.Bind(x => x.FromAssembliesMatching("AT.SampleApp.Cqrs.dll")
    .SelectAllClasses().InheritedFrom(typeof(ICommandHandler<>)).BindAllInterfaces());

存储库

我使用 MongoDB 作为存储介质,您可以通过 NuGet 获取。此外,我没有自己实现流行的 Repository DDD 模式,而是有一个好心人实现了 MongoDB 的存储库并将其打包为 NuGet,请在 NuGet 中查找 MongoRepository。我只是按原样使用它。您可以看到上面代码中的用法。我使用以下 Ninject 代码进行绑定

kernel.Bind(x => x.FromAssembliesMatching("AT.SampleApp.Cqrs.dll", "MongoRepository.dll")
    .SelectAllClasses().InheritedFrom(typeof(IRepository<>)).BindAllInterfaces());

Aggregate

这是一个简单的聚合,我将其作为文档持久化到 MongoDB 中

public class Task : IEntity
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string Id { get; set; }
    public string Title { get; set; }
    public bool IsCompleted;
    public DateTime LastUpdated;
}

查询

查询的路径与命令类似,但有以下区别

  • 控制器显式调用 _queryDispatcher,而不是像 _commandDispatcher 那样隐式调用。
  • 查询是双向的,即查询及其结果,而命令是单向的。
  • 还有更多区别,但我是从这个项目的角度来看待这些区别的。

回顾

希望我能够说明上面的图表,如果您发现错误或有任何疑问,请随时在评论中提问。

让我们回顾一下 CQRS 命令的实现步骤。

  • 命令由视图触发。
  • 命令到达操作方法。
  • 操作方法将命令分派给处理程序。
  • 处理程序接收命令,执行一些业务逻辑,然后通过存储库进行持久化。

下一步是什么?

这只是您 CQRS 应用程序中的基础和第一个构建块。您可能会在此之上添加其他与 CQRS 相关的模式。以下是一些完善您架构的建议

基类控制器

您可能希望有一个基类控制器并将您的分发器注入其中。类似这样

public abstract class BaseController : Controller
{
    [Inject]
    public ICommandDispatcher CommandDispatcher { get; set; }
    [Inject]
    public IQueryDispatcher QueryDispatcher { get; set; }
}

事件溯源

现在,您已经看到了基本模式,何不进一步了解:这是另一篇优秀的关于与我讨论过的不同焦点的文章:CQRS 简介

增强基本架构

没有日志记录,也没有错误处理,当然,因为这不是为生产准备的,它 meant to be a sample,使其成为主流 :)

结论

结果?控制器几乎是空的,代码是 DRY 的,并且该模式开放进行单元测试。显然,对于如此小的应用程序来说,代码量是过度的,但这仅仅是为了触发您的软件设计想象力,以便您可以将此模式应用于真实应用程序(显然不是强制使用该模式,如果它不合适)。

希望我让某人开心了一天,如果您喜欢这个,请在评论中告诉我,这样我将更有动力写更多。虽然我尽量保持简短,但这比我想要的要长。请注意,下载区域中的源代码包含完整的代码。

我愿意接受改进的建议,请在评论中告诉我您的想法。

© . All rights reserved.