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.
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 yourCommand
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 yourCommand
it is likely to throw anException
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.