Validation and synchronous commands in CQRS

1.5k Views Asked by At

I like the idea of trying CQRS without event sourcing. But I'm not sure how to tackle the fact that I need to give user an instant feedback.

This is how my current signup (simplified to understand) I use Dapper for reads and nHibernate for writes.

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 
}

Which of the appraoches would be the most CQRS friendly?

Method 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));
}

What if I need to do the same validation somewhere else in the system (I would have duplicated code)?

I thought about creating ValidationService.

What if command "explodes" for some reason and user will get false feedback?

Method 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 
            } 
}

Basically inside the handler I cheat and validate (adding extra methods to nHibernate repo).

Method 3

Similar to the first approach, but encapsulating validation and dispatch within 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));
}

I like this approach but I feel it will become a Baklava code.

2

There are 2 best solutions below

0
On

Generally when using CQRS, you want to use an optimistic approach.

The idea is to validate your input (may it be simple validation against a string format or the uniqueness of an email) before to issue the command.

Obviously, you will double check your data when actually building your Command to make sure your Command is in a valid and safe state (the same applies to every other objects).

Based on this, you can assume your Command is going to dispatch properly and use an optimistic approach to give a positive feedback to your user.

If your CommandHandler fails to handle your Command it is likely to throw an Exception you can catch and notify your user accordingly.

Take the Messenger / Facebook example, when you type and send, the UI let you think everything is going well and your message has been sent, though, if something happens, your UI will be rolled-back.

0
On

Like you have seen, there are multiple possible approaches to dealing with the dispatch of a command. Sometimes you will see people use a strict adherence to returning a void from the command handler, but there is also the approach of an ACK/NACK (Acknowledged / Not Acknowledged) response.

The way that I have done this, is that the Dispatch() method of my command handler implementations will always return one of two possible statuses, being either an ACK or a NACK. This tells me whether or not the command that I attempted to dispatch was deemed to be in a state that it could be applied to the system. However, as opposed to a simple enumeration for ACK/NACK, every command is capable of returning an Acknowledgement class. That class contains a state (the ACK/NACK), along with one or more Command Failures. This way, if I have an ACK, I know that the command was received, and I can assume that it will be processed. On the other hand, if I get back a NACK, I have a failure that I can then format and present to the user. At no time, though, am I returning any information related to the post-dispatch state. The failures that I report (and there can be multiple failures for the same command) are entirely based on the data in the command and not related to it being applied to the system.

This all most closely relates to your third example. Where the above approach varies would mainly be that encapsulating the failures and status will allow you to present/track all issues with a command, as opposed to the first failure.

Here are the simple classes/enumeration that I use to accomplish this.

Acknowledgement

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