2016-12-21 41 views
2

我喜歡在沒有事件源的情況下嘗試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回購)的內部。

方法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)); 
} 

我喜歡這種方法,但我覺得它會成爲一個Baklava代碼。

+0

可以完美把你的「閱讀」添加到你的CommandHandler中。畢竟,在您決定創建新的AR或修改某些內容之前,CommandHandler中沒有任何事情發生。你可以很好的從你的CommandHandler中拋出一個異常,讓控件處理它。 –

回答

1

通常在使用CQRS時,您希望使用樂觀的方法。

這個想法是在發出命令之前驗證您的輸入(可能是對字符串格式或電子郵件唯一性的簡單驗證)。

顯然,在實際構建您的Command以確保您的Command處於有效和安全狀態時(相同情況適用於所有其他對象),您將仔細檢查數據。

基於此,您可以假設您的Command將正確調度並使用樂觀的方法向用戶提供積極的反饋。

如果您的CommandHandler未能處理您的Command它很可能會拋出一個Exception您可以捕獲並相應地通知您的用戶。

以Messenger/Facebook爲例,當您鍵入併發送時,UI會讓您認爲一切進展順利,並且您的消息已發送,但如果發生什麼情況,您的UI將會回滾。

0

就像你所看到的,有多種可能的方法來處理一個命令的發送。有時你會看到人們嚴格遵守從命令處理程序返回一個void,但也有一個ACK/NACK(已確認/未確認)響應的方法。

我這樣做的方式是,我的命令處理程序實現的Dispatch()方法將始終返回兩種可能的狀態之一,可能是ACK或NACK。這告訴我我試圖發送的命令是否被認爲處於可以應用於系統的狀態。然而,與簡單的ACK/NACK枚舉相反,每個命令都能夠返回一個確認類。該類包含一個狀態(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 
    } 
}