Danger ... Danger Dr. Smith... Philosophical post ahead
The purpose of this post is to determine if placing the validation logic outside of my domain entiti
I cannot say what I did is the perfect thing to do for I am still struggling with this problem myself and fighting one fight at a time. But I have been doing so far the following thing :
I have basic classes for encapsulating validation :
public interface ISpecification where TEntity : class, IAggregate
{
bool IsSatisfiedBy(TEntity entity);
}
internal class AndSpecification : ISpecification where TEntity: class, IAggregate
{
private ISpecification Spec1;
private ISpecification Spec2;
internal AndSpecification(ISpecification s1, ISpecification s2)
{
Spec1 = s1;
Spec2 = s2;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate);
}
}
internal class OrSpecification : ISpecification where TEntity : class, IAggregate
{
private ISpecification Spec1;
private ISpecification Spec2;
internal OrSpecification(ISpecification s1, ISpecification s2)
{
Spec1 = s1;
Spec2 = s2;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate);
}
}
internal class NotSpecification : ISpecification where TEntity : class, IAggregate
{
private ISpecification Wrapped;
internal NotSpecification(ISpecification x)
{
Wrapped = x;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return !Wrapped.IsSatisfiedBy(candidate);
}
}
public static class SpecsExtensionMethods
{
public static ISpecification And(this ISpecification s1, ISpecification s2) where TEntity : class, IAggregate
{
return new AndSpecification(s1, s2);
}
public static ISpecification Or(this ISpecification s1, ISpecification s2) where TEntity : class, IAggregate
{
return new OrSpecification(s1, s2);
}
public static ISpecification Not(this ISpecification s) where TEntity : class, IAggregate
{
return new NotSpecification(s);
}
}
and to use it, I do the following :
command handler :
public class MyCommandHandler : CommandHandler
{
public override CommandValidation Execute(MyCommand cmd)
{
Contract.Requires(cmd != null);
var existingAR= Repository.GetById(cmd.Id);
if (existingIntervento.IsNull())
throw new HandlerForDomainEventNotFoundException();
existingIntervento.DoStuff(cmd.Id
, cmd.Date
...
);
Repository.Save(existingIntervento, cmd.GetCommitId());
return existingIntervento.CommandValidationMessages;
}
the aggregate :
public void DoStuff(Guid id, DateTime dateX,DateTime start, DateTime end, ...)
{
var is_date_valid = new Is_dateX_valid(dateX);
var has_start_date_greater_than_end_date = new Has_start_date_greater_than_end_date(start, end);
ISpecification specs = is_date_valid .And(has_start_date_greater_than_end_date );
if (specs.IsSatisfiedBy(this))
{
var evt = new AgregateStuffed()
{
Id = id
, DateX = dateX
, End = end
, Start = start
, ...
};
RaiseEvent(evt);
}
}
the specification is now embedded in these two classes :
public class Is_dateX_valid : ISpecification
{
private readonly DateTime _dateX;
public Is_data_consuntivazione_valid(DateTime dateX)
{
Contract.Requires(dateX== DateTime.MinValue);
_dateX= dateX;
}
public bool IsSatisfiedBy(MyAggregate i)
{
if (_dateX> DateTime.Now)
{
i.CommandValidationMessages.Add(new ValidationMessage("datex greater than now"));
return false;
}
return true;
}
}
public class Has_start_date_greater_than_end_date : ISpecification
{
private readonly DateTime _start;
private readonly DateTime _end;
public Has_start_date_greater_than_end_date(DateTime start, DateTime end)
{
Contract.Requires(start == DateTime.MinValue);
Contract.Requires(start == DateTime.MinValue);
_start = start;
_end = end;
}
public bool IsSatisfiedBy(MyAggregate i)
{
if (_start > _end)
{
i.CommandValidationMessages.Add(new ValidationMessage(start date greater then end date"));
return false;
}
return true;
}
}
This allows me to reuse some validations for different aggregate and it is easy to test. If you see any flows in it. I would be real happy to discuss it.
yours,