我喜欢在没有事件采购的情况下尝试CQRS的想法。 但我不确定如何解决我需要给用户提供即时反馈的事实。
这是我目前的注册方式(简化为了解) 我使用Dapper进行读取,使用nHibernate进行写入。
signupcontroller.cs
public ActionResult signup(UserCreateModel model)
{
// model validation (email, password strength etc)
if (!ModelState.IsValid)
{
// get back to form and show errors
}
// use service layer to validate against database and create user
var registermodel = _userService.create_user_account(model.username, model.email, model.password);
// service returns and object with several states
if (registermodel.Status == UserRegistrationStatus.Ok)
{
// user registered ok, display thank you or whatever
}
if (registermodel.Status == UserRegistrationStatus.DuplicatedUsername)
{
// duplicated username found, back to form and show errors
}
if (registermodel.Status == UserRegistrationStatus.DuplicatedEmail)
{
// duplicated email found, back to form and show errors
}
// get back to form and show errors
}
signupcontroller.cs
public ActionResult signup(UserCreateModel model)
{
// model validation (email, password strength etc)
if (!ModelState.IsValid)
{
// get back to form and show errors
}
// validate duplicated email
bool is_email_duplicated = _read.user_email_exists(model.email);
// duplicated email found, back to form and show errors
// validate duplicated username
bool is_username_duplicated = _read.user_username_exists(model.username);
// duplicated username found, back to form and show errors
// assume all is perfect and dispatch
_commandDispatcher.Dispatch(new CreateUserCommand(model.username, model.email, model.password));
}
如果我需要在系统中的其他位置进行相同的验证(我会有重复的代码)怎么办?
我考虑过创建ValidationService。
如果命令由于某种原因“爆炸”并且用户会得到错误的反馈怎么办?
signupcontroller.cs
public ActionResult signup(UserCreateModel model)
{
// model validation (email, password strength etc)
if (!ModelState.IsValid)
{
// get back to form and show errors
}
// dispatch and validate inside the handler, abort execution if validation failed
var command = new CreateUserCommand(model.username, model.email, model.password)
// attached common feedback object to the command and deal with errors
if(command.status == UserRegistrationStatus.DuplicatedUsername)
{
// get back to form and show errors
}
}
基本上在处理程序中我作弊和验证(为nHibernate repo添加额外的方法)。
与第一种方法类似,但在UserService中封装验证和分发
signupcontroller.cs
public ActionResult signup(UserCreateModel model)
{
// model validation (email, password strength etc)
if (!ModelState.IsValid)
{
// get back to form and show errors
}
var feedback = _userService.create_user(model.username, model.email, model.password);
// check for status and return feedback to the user
}
userservice.cs
public Feedback create_user(string username, string email, string password)
{
// validate duplicated email
bool is_email_duplicated = _read.user_email_exists(email);
// duplicated email found, back to form and show errors
// validate duplicated username
bool is_username_duplicated = _read.user_username_exists(username);
// duplicated username found, back to form and show errors
// dispatch command
_commandDispatcher.Dispatch(new CreateUserCommand(username, email, password));
}
我喜欢这种方法,但我觉得它会成为一种果仁蜜饼的代码。
答案 0 :(得分:1)
通常在使用CQRS时,您希望使用乐观的方法。
我们的想法是在发出命令之前验证您的输入(可能是对字符串格式的简单验证或电子邮件的唯一性)。
显然,在实际构建Command
时,您会仔细检查数据,以确保Command
处于有效且安全的状态(同样适用于所有其他对象)。
基于此,您可以假设您的Command
将正确发送并使用乐观的方法向您的用户提供积极的反馈。
如果CommandHandler
无法处理您的Command
,则可能会抛出Exception
,您可以抓住并相应地通知您的用户。
使用Messenger / Facebook示例,当您键入并发送时,UI会让您认为一切正常并且您的消息已被发送,但是,如果发生某些事情,您的UI将被回滚。
答案 1 :(得分:0)
就像你所看到的,有多种可能的方法来处理命令的发送。有时您会看到人们严格遵守命令处理程序返回的void,但也有ACK / NACK(已确认/未确认)响应的方法。
我这样做的方式是,我的命令处理程序实现的Dispatch()方法将始终返回两种可能的状态之一,即ACK或NACK。这告诉我,我试图发送的命令是否被认为处于可以应用于系统的状态。但是,与ACK / NACK的简单枚举相反,每个命令都能够返回Acknowledgment类。该类包含一个状态(ACK / NACK),以及一个或多个命令失败。这样,如果我有一个ACK,我知道收到了命令,我可以假设它将被处理。另一方面,如果我回到NACK,我会失败,然后我可以格式化并呈现给用户。但是,在任何时候,我都不会返回与调度后状态相关的任何信息。我报告的故障(同一命令可能存在多个故障)完全基于命令中的数据,与应用于系统的数据无关。
这与你的第三个例子密切相关。上述方法的变化主要是封装故障和状态将允许您使用命令来呈现/跟踪所有问题,而不是第一次失败。
以下是我用来完成此操作的简单类/枚举。
<强>确认强>
namespace Commands
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Provides an ACK/NACK for an issued <see cref="Command" />. Can represent one of two states,
/// being either Acknowledged or Not Acknowledged, along with the ability to track unhandled faults.
/// </summary>
/// <remarks>
/// ACK/NACK implies a synchronous command execution. Asynchronous commands, while more rarely used,
/// should represent the concept of command acknowledgement through events.
/// </remarks>
public sealed class Acknowledgement
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Acknowledgement"/> class.
/// </summary>
/// <remarks>
/// This is representative of an <see cref="AcknowledgementState.Acknowledged" /> state, with
/// no command failures nor faults.
/// </remarks>
public Acknowledgement()
{
this.State = AcknowledgementState.Acknowledged;
this.CommandFailures = new List<CommandValidationFailure>();
}
/// <summary>
/// Initializes a new instance of the <see cref="Acknowledgement"/> class.
/// </summary>
/// <param name="failures">The command validation failures that led to NACK.</param>
/// <remarks>
/// This is representative of a <see cref="AcknowledgementState.NotAcknowledged" /> state, with
/// at least one command validation failure and no fault.
/// </remarks>
public Acknowledgement(IEnumerable<CommandValidationFailure> failures)
{
this.State = AcknowledgementState.NotAcknowledged;
this.CommandFailures = failures;
}
/// <summary>
/// Initializes a new instance of the <see cref="Acknowledgement"/> class.
/// </summary>
/// <param name="fault">The fault that led to the NACK.</param>
/// <remarks>
/// This is representative of a <see cref="AcknowledgementState.NotAcknowledged" /> state, with
/// a fault and no command validation failures.
/// </remarks>
public Acknowledgement(Exception fault)
{
this.State = AcknowledgementState.NotAcknowledged;
this.Fault = fault;
}
#endregion
#region Public Properties
/// <summary>
/// Gets the command failures that led to a NACK, if any.
/// </summary>
/// <value>
/// The command failures, if present.
/// </value>
public IEnumerable<CommandValidationFailure> CommandFailures { get; }
/// <summary>
/// Gets the fault that led to a NACK, if present.
/// </summary>
/// <value>
/// The fault.
/// </value>
public Exception Fault { get; }
/// <summary>
/// Gets a value indicating whether this <see cref="Acknowledgement" /> is backed by a fault.
/// </summary>
/// <value>
/// <c>true</c> if this instance is reflective of a fault; otherwise, <c>false</c>.
/// </value>
public bool IsFaulted => this.Fault != null;
/// <summary>
/// Gets a value indicating whether this <see cref="Acknowledgement" /> is backed by command validation failures.
/// </summary>
/// <value>
/// <c>true</c> if this instance is reflective of command failures; otherwise, <c>false</c>.
/// </value>
public bool IsInvalid => this.CommandFailures != null && this.CommandFailures.Any();
/// <summary>
/// Gets the state of this instance, in terms of an ACK or NACK.
/// </summary>
/// <value>
/// The state representation.
/// </value>
public AcknowledgementState State { get; }
#endregion
}
}
<强> AcknowledgementState 强>
namespace Commands
{
/// <summary>
/// Provides a simple expression of acknowledgement state (ACK/NACK).
/// </summary>
public enum AcknowledgementState
{
/// <summary>
/// Indicates an ACK that contains no command failures nor a fault.
/// </summary>
Acknowledged,
/// <summary>
/// Indicates a NACK that contains either command failures or a fault.
/// </summary>
NotAcknowledged
}
}
<强> CommandValidationFailure 强>
namespace Commands
{
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
/// <summary>
/// Thrown when on or more violations are found during the attempted execution of a command.
/// </summary>
/// <remarks>
/// In general, this exception is thrown as a guard against non-validation of a command ahead
/// of application. The most feasible scenario is a command handler which attempts to skip
/// validation, prior to execution.
/// </remarks>
[Serializable]
public class CommandValidationException : Exception
{
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CommandValidationException"/> class.
/// </summary>
/// <param name="violations">The violations leading to this exception being thrown.</param>
public CommandValidationException(List<DomainValidationFailure> violations)
{
this.Violations = violations;
}
/// <summary>
/// Initializes a new instance of the <see cref="CommandValidationException"/> class.
/// </summary>
/// <param name="violations">The violations leading to this exception being thrown.</param>
/// <param name="message">The message to associate with the exception.</param>
public CommandValidationException(List<DomainValidationFailure> violations, string message) : base(message)
{
this.Violations = violations;
}
/// <summary>
/// Initializes a new instance of the <see cref="CommandValidationException"/> class.
/// </summary>
/// <param name="violations">The violations leading to this exception being thrown.</param>
/// <param name="message">The message to associate with the exception.</param>
/// <param name="innerException">The inner exception to associate with this exception.</param>
public CommandValidationException(List<DomainValidationFailure> violations, string message, Exception innerException) : base(message, innerException)
{
this.Violations = violations;
}
/// <summary>
/// Initializes a new instance of the <see cref="CommandValidationException"/> class.
/// </summary>
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that contains contextual information about the source or destination.</param>
public CommandValidationException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
#endregion
#region Public Properties
/// <summary>
/// Gets the violations associated to this exception.
/// </summary>
/// <value>
/// The violations associated with this exception.
/// </value>
public List<DomainValidationFailure> Violations { get; }
#endregion
}
}