-
-
Save PaulUpson/2302490 to your computer and use it in GitHub Desktop.
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); | |
} | |
} |
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?
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
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
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?