2016-12-21 7 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)); 
} 

Мне нравится этот подход, но я чувствую, что он станет кодом Баклавы.

+0

Вы можете отлично добавьте свои «чтения» в CommandHandler. В конце концов, ничего не происходит в CommandHandler, пока вы не решите создать новое AR или не модифицируете что-либо. Вы можете отлично отбросить исключение из своего CommandHandler и позволить управлению справиться с этим. –

ответ

1

Как правило, при использовании CQRS вы хотите использовать оптимистический подход.

Идея состоит в том, чтобы подтвердить свой ввод (возможно, это простая проверка в отношении формата строки или уникальности электронной почты), прежде чем выпустить команду.

Очевидно, что вы будете проверять свои данные при фактическом построении Command, чтобы убедиться, что ваш Command находится в допустимом и безопасном состоянии (то же самое относится ко всем другим объектам).

Исходя из этого, вы можете предположить, что ваш Command собирается правильно отправить и использовать оптимистический подход, чтобы дать положительный отзыв вашему пользователю.

Если ваш CommandHandler не сможет обработать ваш Command, он, скорее всего, бросит Exception, вы можете поймать и уведомить об этом пользователя.

Примите пример Messenger/Facebook, когда вы вводите и отправляете, пользовательский интерфейс позволяет вам думать, что все идет хорошо, и ваше сообщение отправлено, хотя, если что-то произойдет, ваш пользовательский интерфейс будет отменен.

0

Как вы видели, существует несколько возможных подходов к обработке отправки команды. Иногда вы увидите, что люди используют строгую приверженность возврату пустоты из обработчика команд, но есть также подход 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 
    } 
}