Skip to content

Instantly share code, notes, and snippets.

@PaulUpson
Created April 4, 2012 15:13
Show Gist options
  • Save PaulUpson/2302490 to your computer and use it in GitHub Desktop.
Save PaulUpson/2302490 to your computer and use it in GitHub Desktop.
Validation within Aggregate Roots - Part 2
public interface IValidationHandler<in T> where T : Command {
ValidationResult Validate(T cmd);
}
public interface ICommandValidator {
ValidationResult Validate<T>(T command) where T : Command;
}
public class CommandValidator : ICommandValidator {
private readonly IDictionary<Type, Func<object, ValidationResult>> _validationHandlers
= new Dictionary<Type, Func<object, ValidationResult>>();
public void RegisterValidationHandler<T>(Func<T,ValidationResult> handler) where T : Command {
_validationHandlers.Add(typeof(T), o => handler((T) o));
}
public ValidationResult Validate<T>(T command) where T : Command {
Func<object, ValidationResult> handler;
if (!_validationHandlers.TryGetValue(command.GetType(), out handler))
return new ValidationResult();
try {
return handler(command);
}
catch(TargetInvocationException ex) {
throw ex.InnerException;
}
}
}
public class ValidationResult {
private readonly List<ValidationFailure> errors = new List<ValidationFailure>();
public bool IsValid {
get { return Errors.Count == 0; }
}
public IList<ValidationFailure> Errors {
get { return errors; }
}
public ValidationResult() { }
public ValidationResult(IEnumerable<ValidationFailure> failures) {
errors.AddRange(failures.Where(failure => failure != null));
}
}
public class ValidationFailure {
public ValidationFailure(string propertyName, string error)
: this(propertyName, error, null) {}
public ValidationFailure(string propertyName, string error, object attemptedValue) {
PropertyName = propertyName;
ErrorMessage = error;
AttemptedValue = attemptedValue;
}
public string PropertyName { get; private set; }
public string ErrorMessage { get; private set; }
public object AttemptedValue { get; private set; }
public override string ToString() {
return ErrorMessage;
}
}
// Class to encapsulate all command validation rules for an Aggregate Root
public class PatientValidationHandler : IValidationHandler<AddReferredPatient>, IValidationHandler<ChangePatientAddress> {
public bool Validate(AddReferredPatient command) {
var validationErrors = new List<ValidationFailure>();
if (string.IsNullOrWhitespace(command.Firstname))
validationErrors.Add("firstname", "First Name is required");
return new ValidationResult(validationErrors);
}
// Register your command validators as you might your command handlers (N.B. this could be done with an IoC or reflection to auto-wireup)
var patientValidator = new PatientValidationHandler();
validator.RegisterValidator<AddReferredPatient>(patientValidator.Validate);
validator.RegisterValidator<ChangePatientAddress>(patientValidator.Validate);
// Then on all command sending
protected void Send<T>(T cmd) where T : Command {
// Validate the command
var result = Validator.Validate(cmd);
// if invalid set Model Error warnings and be done
if (!result.IsValid) {
SetModelErrors(result.Errors);
}
else { // if valid try a send
try {
Bus.Send(cmd);
}
catch (DomainValidationException ex) { //only catch business rule failures that can only be validated from within the AR
SetModelErrors(ex.Errors);
}
}
}
// since both command and domain validation return the same error collection use the same process to strip them out
private void SetModelErrors(IEnumerable<ValidationFailure> errors) {
foreach (var error in errors) {
ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
@nomad3k
Copy link

nomad3k commented Apr 7, 2012

Would you mind if I draw us back to the actual story, and see what real code that produces? Working to complete the story will force the completion of all the code from textbox to db. Seeing the whole thing, we can then decide which bits have value. I'd submit some code myself, but I don't have a windows machine.

So, what is the original story relating to referring patients?

Is it

  1. As a GP I want to refer a Patient, so that we can give treatment
  2. As a Patient I want to be referred by a GP, so that I can receive treatment

Difference being that #1 has a GP sat at a PC and #2 is a patient showing up at a surgery with details of a referral from a GP.

Feels like a Referral is the actual AR as it has some kind of approval/acceptance process, KPIs and record of Treatment and eventual Completion (which may include another referral). So it also feels like the main story is an epic, which needs breaking down.

As a GP I want to refer a Patient, so that we can give treatment

  1. As a GP I want to create a Referral, so that a patient can receive treatment and we can record performance
  2. As a Specialist I want to browse open referrals to my TreatmentCentre, so that I can accept/reject them
  3. As a Specialist I want to accept/reject a referral
  4. As a Specialist I want to record Treatment against a Referral, so that we can measure performance against KPIs
  5. As a Specialist I want to complete a Referral, so that we can finalise the KPIs
  6. As a GP I want to browse my Referrals, so that I can see their progress
  7. As a GP I want to browse my Patient's Referrals, so that I can review treatment

So what ARs do we have? Patient, GP, Specialist, Referral & TreatmentCentre?

So, if we're looking at story 1.1 then it appears to me that we've got a story more complex than just validating primitive types, and it should highlight for us how the system will deal with things like referential integrity, and answer the question about where is Treatment recorded?

@PaulUpson
Copy link
Author

Thanks Craig, that was what I was getting at. Chris, I thought the original focus was on validation strategies so I'd extended my examples in that direction. I agree that getting a stable set of stories agreed is necessary for taking the discussion regarding the specifics of the aggregate roots forward.

Can I just caveat my responses with the fact that I'm currently working in a completely separate and unrelated domain and am transposing my examples over. Therefore loosing the true business context and fitting the above solutions to contrived examples in the healthcare domain. I appreciate that aggregate roots (once defined) in our shared healthcare context may not behave the same as mine in my current context, but I felt that the discussions regarding validation transcended these domain specifics.

Can I suggest that we start a new gist focused on clarifying the ARs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment