CQRS中的验证和同步命令

时间:2016-12-21 15:42:41

标签: c# validation nhibernate cqrs

我喜欢在没有事件采购的情况下尝试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 
}

哪个appraoches最适合CQRS?

方法1

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。

如果命令由于某种原因“爆炸”并且用户会得到错误的反馈怎么办?

方法2

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添加额外的方法)。

方法3

与第一种方法类似,但在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));
}

我喜欢这种方法,但我觉得它会成为一种果仁蜜饼的代码。

2 个答案:

答案 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
    }
}