当我没有 ID 时执行 CQRS 命令

Execute a CQRS command when i don't have the ID

我目前正在开发一个具有登录页面的 CQRS 应用程序。在 LoginCommand 中,从数据库中获取一个用户,如果它可以登录(加密密码与给定的加密密码相匹配),则会为该用户分配一个令牌。

在 CQRS 中,命令通常接收命令所针对的元素的 ID,以便获取它标识的域聚合并在其上执行逻辑。但是,在那种情况下,我从用户那里得到的是电子邮件。尽管这是一个唯一字段,但我不确定使用该字段获取聚合是否错误,尽管它是一个唯一字段。

我还可以想到具有相同问题的其他情况,例如尝试通过不包含 post 的 ID 的给定语义 URL 来识别 Post

由于禁止在命令内执行查询,并且不太可能从登录表单附加用户 ID,在这种情况下,我有什么选择可以获取用户?我应该在命令外查询读取模型(例如控制器)吗?

从域的角度来看,与 User 记录关联的 ID 只是 surrogate key。它在现实世界中没有相应的表示,只是为了帮助你持久化和检索数据。

因此,如果 email 是您的 User 记录的唯一字段,请务必像在命令中使用标识符一样使用它。

这并不一定意味着您可以摆脱 id 字段。

您仍然希望在您的 User 记录中有一个代理键,例如 id 字段,因为您可能希望让您的用户可以选择更改他们的电子邮件地址。即使更改了电子邮件地址,您也需要能够在整个系统中唯一地标识用户,这就是代理键派上用场的地方。出于性能原因,您还需要一个代理键;使用 IntegerUUID 字段而不是 String 电子邮件地址作为主键或在参考字段中几乎总是更好。

您还应该区分 Command 及其对应的 Command HandlerCommand 只是一个 DTO,它封装了外部世界发生的变化,或者需要提交给数据库的变化。从这个意义上说,它们是不可变的,不应以任何方式执行查询或更新自身。

A Command Handler(本质上类似于应用程序服务,但有后台)使用命令中的数据。在那里,您可以查询您的存储库并检索记录。事实上,这将是进行任何类型的重复或引用密钥验证的必要条件。

在这种特殊情况下,我认为 CQRS 不会对您的设计产生任何影响。也许事件溯源可能。

如果您有一些自然键,那么您有几个选择。一方面,它必须是独一无二的。您的 UserRepository 可能有一种方法使用电子邮件地址来获取用户以及 ID:

public interface IUserRepository
{
    User Get(Guid id);
    User Find(string email);
}

当我可能 return null 时,我倾向于使用 Find 方法,因为 Get 表明该实体应该存在,并且在找不到时抛出异常.

如果您只想通过 id 查找,那么您需要使用某些商店的 email 查找 id。根据您的一致性要求,一个最终一致的 query/read 存储可能就足够了,但是没有什么可以阻止您访问具有电子邮件到 id 映射的 100% 一致的存储。

我强烈建议这不是 'command' 而是读取模型的读取。原因如下:

您只是在检查提供的凭据是否与读取模型中的相匹配。此操作不会更改域的状态,因此与命令的使用不一致。

但是这里还有更严重的潜在问题。我不确定您是否在使用事件源,但如果您在使用,我会非常担心将密码放入其中。即使加密。使用当前和历史密码的事件存储的数据泄露可能是一个真正的问题。

还有更多...

我想尽可能地限制通过网络传输密码。与对成员数据库进行传统凭证检查相比,将其添加到命令中(取决于您的基础架构)会增加额外的传输时间。

我知道,但是您可能想要记录某人登录或登录失败的事实。为此考虑发出像 'RecordSuccessfullLoggin' 或 'RecordFailedLoginAttempt' 这样的命令,如果这是您的域需要的。

首先,我假设当您说您为用户分配令牌时,这意味着您在数据库中写入该分配。否则您的登录命令将不是命令。

也就是说,我认为在您的用户存储库中有一个方法可以检索知道该电子邮件的用户没有问题。

这是一个非常有趣的问题,我在 CQRS/DDD/Event 采购上下文中也遇到过这个难题。在已经提到的其他答案之上,还有一些额外的答案。

选项 A: 如果您认为通过将用户设置为已登录或类似状态来修改用户状态,则需要一个 ID。

  1. 使用电子邮件作为您的 ID。您可以将电子邮件视为 aggregate/user ID。尽管就像有人提到的那样有限制,因为电子邮件永远无法修改。不过,您始终可以使用所需的电子邮件地址(即 ID)创建一个克隆用户,所以这更多是一个技术问题,如果业务需要则可以轻松实现。

  2. 进行两步 UI 登录并在第一步 上检索 ID。或者您可以以标准方式为您的用户使用 int/guid/uuid,并预先通过电子邮件查询阅读方以检索此 ID。 有些登录系统(Google 或 Yahoo)也分两步工作。参见 https://ux.stackexchange.com/questions/91763/google-and-yahoo-require-you-to-enter-your-username-first-then-password-why-is 虽然值得商榷,但它有一些优点,例如根据用户的偏好允许不同的登录机制,在 UI 上显示自定义登录,烦人的自动机器人,因为它会减少网络钓鱼附件,因为第二个屏幕看起来“不同” "针对不同的用户。此外,如果您从已注册的设备(例如:您的徽标等)访问与从新设备访问,它允许阅读方 return 不同的东西。

选项B: 另一个选项是不要将此视为命令,而是将其视为查询。基本上,您通过传递电子邮件和密码来查询系统,以获得与该请求匹配的令牌。您可以稍后使用该令牌来实际更改系统,例如作为有效负载的一部分附加到命令,或命令元数据或 http header 或类似内容。如果您愿意,负责发行令牌的系统可以引发事件,以便您的域能够对这些“外部”事件(例如:userLoggedIn、userLoggedOut、loginAttemptFailed)做出反应,并对特定用户的聚合做任何需要做的事情.

我个人喜欢 选项 A.2 有 2 个步骤的选项,所以流程是这样的:

  1. 最终用户向阅读端发送查询,通过特定电子邮件进行搜索
  2. 查询方return的信息或多或少取决于设备是否已注册。但无论如何它return是一个ID
  3. 如果电子邮件存在(或者即使不存在,如果您想避免共享有关哪些电子邮件存在的信息,最终用户也不需要知道这一点......)显示第二个 UI 步骤,用户在此处输入密码 and/or 任何其他安全凭证(PIN?phone 号码?通过电子邮件发送的代码?)。 此外,请注意使用此机制用户可以轻松拥有多个电子邮件、别名等。
  4. 最终用户通过登录尝试向写入端提交命令,提供凭据并使用正确的 ID。
  5. 写入方(命令 handler/domain)验证凭据并执行其需要执行的任何操作,引发事件 userLoggedIn 或 failedLoginAttempted 或您认为与系统相关的任何内容以捕获或更改为。

总而言之,我的建议是,如果您想将用户视为更改状态(已登录、已登录、已锁定)的聚合,您仍然可以使用“标准”CQRS 流程,但您需要一种机制前端预先知道ID以便发送命令。 这还不错,因为这个 ID 可以缓存在浏览器中并自动检索,甚至无需访问系统的查询端,用户不需要关心它。