CQRS体系结构中的域validation
危险…史密斯博士危险…前面的哲学职位
这篇文章的目的是为了确定是否将validation逻辑放在我的域实体之外(实际上是聚合根)实际上给了我更多的灵活性,或者它是kamikaze代码
基本上我想知道是否有更好的方法来validation我的域实体。 这是我打算这样做,但我希望你的意见
我考虑的第一个方法是:
class Customer : EntityBase<Customer> { public void ChangeEmail(string email) { if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”); if(!email.IsEmail()) throw new DomainException(); if(email.Contains(“@mailinator.com”)) throw new DomainException(); } }
我实际上不喜欢这个validation,因为即使当我将validation逻辑封装在正确的实体中时,这违反了打开/closures原则(打开以进行扩展,但是closures以进行修改),并且我发现违反这个原则,代码维护变成当应用程序复杂化时,真正的痛苦。 为什么? 由于领域规则的变化比我们想要承认的要多,而且如果规则隐藏在这样一个实体中,他们很难testing,很难阅读,很难维护,但是为什么我不喜欢这个方法是:如果validation规则改变,我必须来编辑我的域实体。 这是一个非常简单的例子,但在RL中validation可能会更复杂
所以遵循Udi Dahan的哲学, 明确的angular色以及Eric Evans在蓝皮书中的build议,接下来的尝试是实现规范模式,像这样
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer> { private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver; public bool IsSatisfiedBy(Customer customer) { return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email); } }
但是后来我意识到,为了遵循这种方法,我必须首先对实体进行变异,以便传递被赋值的值 ,在这种情况下是电子邮件,但是变异会导致我的域事件被激发,而我不希望直到新的电子邮件有效
所以在考虑了这些方法之后,我拿出了这个方法,因为我要实现一个CQRS架构:
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand> { public void IsValid(Customer entity, ChangeEmailCommand command) { if(!command.Email.HasValidDomain()) throw new DomainException(“...”); } }
那么这就是主要思想,实体被传递给validation器,以防我们需要实体的某些值来执行validation,命令包含来自用户的数据,由于validation器被认为是可注入的对象,因此它们可能会注入外部依赖关系如果validation需要它。
现在困境 ,我对这样的devise感到高兴,因为我的validation被封装在单个对象中,带来很多好处:易于unit testing,易于维护,域不variables使用泛在语言明确expression,易于扩展,validation逻辑是集中和validation器可以一起使用来执行复杂的域规则。 甚至当我知道我把我的实体validation在他们之外(你可以争辩一个代码味道 – 贫血的域名),但我认为这种权衡是可以接受的
但是有一件事我没有想到如何以一种干净的方式来实现它。 我应该如何使用这个组件…
由于他们将被注入,他们不会自然适合我的域名实体,所以基本上我看到两个select:
-
将validation器传递给我的实体的每个方法
-
从外部validation我的对象(从命令处理程序)
我对选项1不满意,所以我会解释如何用选项2来做
class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand> { // here I would get the validators required for this command injected private IEnumerable<IDomainInvariantValidator> validators; public void Execute(ChangeEmailCommand command) { using (var t = this.unitOfWork.BeginTransaction()) { var customer = this.unitOfWork.Get<Customer>(command.CustomerId); // here I would validate them, something like this this.validators.ForEach(x =. x.IsValid(customer, command)); // here I know the command is valid // the call to ChangeEmail will fire domain events as needed customer.ChangeEmail(command.Email); t.Commit(); } } }
那么这就是它。 你可以给我你的想法或分享你的经验与域实体validation
编辑
我认为我的问题并不清楚,但真正的问题是:隐藏域规则对应用程序的未来可维护性有着严重的影响,并且在应用程序的生命周期中,域规则也经常发生变化。 因此,在实现这一点的时候,我们可以很容易地扩展它们。 现在想象一下,在将来实现规则引擎的时候,如果规则被封装在域实体之外,这个改变将更容易实现
我知道把我的实体外部的validation放在他的答案中提到的@jgauffin打破了封装,但是我认为将validation放在单个对象中的好处要比保持一个实体的封装要重要得多。 现在我认为封装在传统的n层体系结构中更有意义,因为实体在域层的几个地方使用,但在CQRS体系结构中,当命令到达时,会有一个命令处理程序访问聚合根对聚合根进行操作只会创build一个完美的窗口来进行validation。
我想在实体内放置validation的优点与放置在单个对象中进行比较
-
在单个对象中validation
- 临。 易于编写
- 临。 易于testing
- 临。 这是明确表示
- 临。 它成为领域devise的一部分,用当前的泛在语言来expression
- 临。 由于它现在是devise的一部分,所以可以使用UML图来build模
- 临。 非常容易维护
- 临。 使我的实体和validation逻辑松散耦合
- 临。 易于扩展
- 临。 在SRP之后
- 临。 遵循打开/closures原则
- 临。 不违反德米特(嗯)的法律?
- 临。 我是集中的
- 临。 它可以是可重用的
- 临。 如果需要,可以轻松注入外部依赖关系
- 临。 如果使用插件模型,只需删除新的程序集,就可以添加新的validation程序,而无需重新编译整个应用程序
- 临。 实现规则引擎会更容易
- CON。 打破封装
- CON。 如果封装是强制性的,我们将不得不将各个validation器传递给实体(聚合)方法
-
validation封装在实体内部
- 临。 封装?
- 临。 可重复使用的?
我很乐意阅读你对此的看法
我同意其他答复中提出的一些概念,但我把它们放在我的代码中。
首先,我同意使用值对象来包含行为是封装常用业务规则的好方法,而电子邮件地址是一个完美的select。 但是,我倾向于将这个限制为不变的规则,不会经常改变。 我相信你正在寻找一个更一般的方法,电子邮件只是一个例子,所以我不会把重点放在这个用例上。
我的方法的关键是认识到validation在应用程序的不同位置有不同的用途。 简而言之,只validation需要什么来确保当前操作可以执行而没有意外的/意外的结果。 那导致什么validation应该发生在什么地方?
在你的例子中,我会问自己,如果域名实体真的关心电子邮件地址是否符合某种模式和其他规则,或者我们只关心在调用ChangeEmail时“email”不能为空或空白? 如果后者比ChangeEmail方法中所需的简单检查来确保存在值。
在CQRS中,修改应用程序状态的所有更改都以命令处理程序中的实现的命令出现(如您所示)。 我通常会把任何'钩子'放入业务规则等等,validation操作是否可以在命令处理程序中执行。 我实际上按照你的方法注入validation程序到命令处理程序,它允许我扩展/replace规则集,而无需更改处理程序。 这些“dynamic”规则允许我在更改实体状态之前定义业务规则(如构成有效电子邮件地址的内容),从而进一步确保它不会进入无效状态。 但是,这种情况下的“无效”是由业务逻辑来定义的,正如你所指出的那样,它是高度dynamic的。
通过CSLA的排名,我发现这个变化很难采用,因为它似乎破坏了封装。 但是,如果退后一步,询问validation在模型中真正起到了什么作用,那么封装就不会中断。
我发现这些细微差别对于保持我的头脑清晰是非常重要的。 validation可以防止属于方法本身的错误数据(例如缺less参数,空值,空string等),并且有确认来确保业务规则的执行。 在前者的情况下,如果客户必须有一个电子邮件地址,那么我唯一需要关心的防止我的域名对象失效的规则是确保一个电子邮件地址已经提供给ChangeEmail方法。 其他规则是关于价值本身有效性的更高层次的关注,并且对域实体本身的有效性确实没有影响。
这已经成为与开发人员进行大量“讨论”的来源,但是当大多数人从更广泛的angular度来考察validation的真正作用时,他们往往会看到光明。
最后,还有一个用于UIvalidation的地方(通过UI,我指的是无论是作为应用程序的界面,无论是屏幕,服务端点还是其他)。 我发现完全合理地复制UI中的一些逻辑以为用户提供更好的交互性。 但是这是因为这个validation为我提供了这样一个单一的目的,为什么我允许这样的重复 然而,使用注入的validation器/规范对象以这种方式促进重用,而没有在多个位置定义这些规则的负面影响。
不知道这是否有帮助…
我不会build议大量的代码到您的域进行validation。 我们通过将我们的领域中缺less的概念视为一种嗅觉,消除了我们大多数尴尬的validation。 在你写的示例代码中,我看到了一个电子邮件地址的validation。 客户与电子邮件validation没有任何关系。
为什么不build立一个名为Email
的ValueObject
来构造这个validation呢?
我的经验是,尴尬的地方validation暗示你遗漏了你的域名的概念。 您可以在Validator对象中捕获它们,但是我更喜欢值对象,因为您将相关概念作为域的一部分。
你把validation放在错误的地方。
你应该使用ValueObjects这样的事情。 观看这个演示http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson它也会教你关于数据的重心。;
还有一个如何重用数据validation的示例,例如使用静态validation方法ala Email.IsValid(string)
我在一个项目的开始,我要在我的域实体之外实现我的validation。 我的域实体将包含逻辑来保护任何不variables(如缺less参数,空值,空string,集合等)。 但是实际的业务规则将存在于validation者类中。 我是@SonOfPirate的心态…
我正在使用FluentValidation ,它将基本上为我提供了一堆对我的域实体起作用的validation器:即规范模式。 另外,按照Eric的蓝皮书中描述的模式,我可以使用任何可能需要执行validation的数据(不论是从数据库还是另一个存储库或服务)构buildvalidation器。 我也可以select在这里注入任何依赖项。 我也可以编写和重用这些validation器(例如,地址validation器可以在“validation器”和“validation器”中重复使用)。 我有一个作为“服务定位器”的Validator工厂:
public class ParticipantService : IParticipantService { public void Save(Participant participant) { IValidator<Participant> validator = _validatorFactory.GetValidator<Participant>(); var results = validator.Validate(participant); //if the participant is valid, register the participant with the unit of work if (results.IsValid) { if (participant.IsNew) { _unitOfWork.RegisterNew<Participant>(participant); } else if (participant.HasChanged) { _unitOfWork.RegisterDirty<Participant>(participant); } } else { _unitOfWork.RollBack(); //do some thing here to indicate the errors:generate an exception (or fault) that contains the validation errors. Or return the results } } }
validation器将包含代码,如下所示:
public class ParticipantValidator : AbstractValidator<Participant> { public ParticipantValidator(DateTime today, int ageLimit, List<string> validCompanyCodes, /*any other stuff you need*/) {...} public void BuildRules() { RuleFor(participant => participant.DateOfBirth) .NotNull() .LessThan(m_today.AddYears(m_ageLimit*-1)) .WithMessage(string.Format("Participant must be older than {0} years of age.", m_ageLimit)); RuleFor(participant => participant.Address) .NotNull() .SetValidator(new AddressValidator()); RuleFor(participant => participant.Email) .NotEmpty() .EmailAddress(); ... } }
我们必须支持多种types的演示:网站,winforms和通过服务批量加载数据。 在固定的情况下,所有这些都是以一种统一的方式公开系统function的一组服务。 我们不使用entity framework或ORM的原因,我不会让你。
这就是为什么我喜欢这种方法:
- validation器中包含的业务规则完全是unit testing的。
- 我可以从简单的规则中编写更复杂的规则
- 我可以在我的系统中的多个位置使用validation器(我们支持网站和Winforms以及公开function的服务),因此,如果服务中的用例与网站有所不同,则需要稍微不同的规则我可以处理的。
- 所有的performance在一个位置,我可以select如何/在哪里注入和组成。
我不能说我做了什么是完美的事情,我自己还在为这个问题而挣扎,一次又一次地打架。 但是我一直在做以下事情:
我有封装validation的基本类:
public interface ISpecification<TEntity> where TEntity : class, IAggregate { bool IsSatisfiedBy(TEntity entity); } internal class AndSpecification<TEntity> : ISpecification<TEntity> where TEntity: class, IAggregate { private ISpecification<TEntity> Spec1; private ISpecification<TEntity> Spec2; internal AndSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2) { Spec1 = s1; Spec2 = s2; } public bool IsSatisfiedBy(TEntity candidate) { return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate); } } internal class OrSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate { private ISpecification<TEntity> Spec1; private ISpecification<TEntity> Spec2; internal OrSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2) { Spec1 = s1; Spec2 = s2; } public bool IsSatisfiedBy(TEntity candidate) { return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate); } } internal class NotSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate { private ISpecification<TEntity> Wrapped; internal NotSpecification(ISpecification<TEntity> x) { Wrapped = x; } public bool IsSatisfiedBy(TEntity candidate) { return !Wrapped.IsSatisfiedBy(candidate); } } public static class SpecsExtensionMethods { public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate { return new AndSpecification<TEntity>(s1, s2); } public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate { return new OrSpecification<TEntity>(s1, s2); } public static ISpecification<TEntity> Not<TEntity>(this ISpecification<TEntity> s) where TEntity : class, IAggregate { return new NotSpecification<TEntity>(s); } }
并使用它,我做了以下几点:
命令处理器:
public class MyCommandHandler : CommandHandler<MyCommand> { public override CommandValidation Execute(MyCommand cmd) { Contract.Requires<ArgumentNullException>(cmd != null); var existingAR= Repository.GetById<MyAggregate>(cmd.Id); if (existingIntervento.IsNull()) throw new HandlerForDomainEventNotFoundException(); existingIntervento.DoStuff(cmd.Id , cmd.Date ... ); Repository.Save(existingIntervento, cmd.GetCommitId()); return existingIntervento.CommandValidationMessages; }
总计:
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<MyAggregate> 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); } }
该规范现在embedded在这两个类中:
public class Is_dateX_valid : ISpecification<MyAggregate> { private readonly DateTime _dateX; public Is_data_consuntivazione_valid(DateTime dateX) { Contract.Requires<ArgumentNullException>(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<MyAggregate> { private readonly DateTime _start; private readonly DateTime _end; public Has_start_date_greater_than_end_date(DateTime start, DateTime end) { Contract.Requires<ArgumentNullException>(start == DateTime.MinValue); Contract.Requires<ArgumentNullException>(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; } }
这使我可以重用一些validation不同的聚合,这是很容易testing。 如果你看到任何stream量。 我会很乐意讨论这个问题。
你的,
我不会调用一个从EntityBase
我的域模型inheritance的类,因为它将它耦合到您的持久层。 但那只是我的个人意见。
我不会将电子邮件validation逻辑从Customer
移到其他任何地方,以遵循“打开/closures”原则。 对我来说,打开/closures意味着你有以下层次:
public class User { // some basic validation public virtual void ChangeEmail(string email); } public class Employee : User { // validates internal email public override void ChangeEmail(string email); } public class Customer : User { // validate external email addresses. public override void ChangeEmail(string email); }
您的build议将控制从域模型转移到任意类,从而打破封装。 我宁愿重构我的class级( Customer
)遵守新的业务规则,而不是这样做。
使用域事件来触发系统的其他部分以获得更松散耦合的体系结构,但不要使用命令/事件来违反封装。
例外
我只注意到你抛出DomainException
。 这是通用exception的一种方法。 为什么不使用参数exception或FormatException
? 他们更好地描述错误。 不要忘记包含上下文信息,以帮助您防止将来出现exception。
更新
把课堂上的逻辑放在课堂上是要求麻烦的。 你如何控制使用哪个validation规则? 代码的一部分可能在validation时使用SomeVeryOldRule
,而另一部分使用NewAndVeryStrictRule
。 这可能并不是故意的,但是当代码库增长的时候,它可能会发生。
这听起来像你已经决定忽略OOP的基本原理之一(封装)。 继续使用通用/外部validation框架,但不要说我没有提醒你;)
UPDATE2
感谢您的耐心和您的回答,这就是为什么我发布这个问题的原因,我觉得一个实体应该负责保证它处于有效状态(我已经在以前的项目中做过),但是放置它的好处在个人对象是巨大的,就像我张贴有甚至一个方法来使用单个对象,并保持封装,但个人,我不是很喜欢devise,但另一方面它不是摆脱了表,考虑这个ChangeEmail(IEnumerable>validation,string电子邮件)我没有想到详细的实施。 虽然
这允许程序员指定任何规则,它可能或可能不是当前正确的业务规则。 开发人员可以写
customer.ChangeEmail(new IValidator<Customer>[] { new NonValidatingRule<Customer>() }, "notAnEmail")
它接受一切。 规则必须在ChangeEmail
被调用的每个地方指定。
如果您想使用规则引擎,请创build一个单例代理:
public class Validator { IValidatorEngine _engine; public static void Assign(IValidatorEngine engine) { _engine = engine; } public static IValidatorEngine Current { get { return _engine; } } }
..并使用它从域模型方法内
public class Customer { public void ChangeEmail(string email) { var rules = Validator.GetRulesFor<Customer>("ChangeEmail"); rules.Validate(email); // valid } }
该解决scheme的问题是,由于隐藏了规则依赖关系,它将成为维护的噩梦。 除非您为每种方法testing每个领域模型方法和每个规则scheme,否则您永远无法确定是否已经指定了所有规则并正常工作。
这个解决scheme更加灵活,但是我们会花费更多的时间来实现,而不是重构业务规则发生变化的方法。
从我的OO经验(我不是DDD专家)将代码从实体移动到更高的抽象级别(到命令处理程序)将导致代码重复。 这是因为每当一个命令处理程序获得一个电子邮件地址时,就必须实例化电子邮件validation规则。 这种代码在一段时间后会腐烂,而且会闻到非常糟糕的味道。 在目前的例子中,它可能不会,如果你没有另外的命令来改变电子邮件地址,但在其他情况下肯定会…
如果您不想将规则移回到较低的抽象级别(如实体或电子邮件值对象),那么我强烈build议您通过对规则进行分组来减轻痛苦。 所以在你的电子邮件例子中有以下三条规则:
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”); if(!email.IsEmail()) throw new DomainException(); if(email.Contains(“@mailinator.com”)) throw new DomainException();
可以成为EmailValidationRule
组的一部分,您可以更轻松地重用。
从我的angular度来看,对于把validation逻辑放在哪里这个问题没有明确的答案。 它可以是取决于抽象层次的每个对象的一部分。 在您当前的情况下,对电子邮件地址的正式检查可以是EmailValueObject
一部分,并且mailinator
规则可以是更高抽象级别概念的一部分,其中您声明用户不能在该域上指定电子邮件地址。 例如,如果有人想不经注册就联系您的用户,那么您可以检查她的电子邮件与正式的validation,但你不必根据邮件规则检查她的电子邮件。 等等…
所以我完全同意@ pjvds声称这种尴尬的放置validation是一个坏devise的标志。 I don't think you will have any gain by breaking encapsulation, but it's your choice and it will be your pain.
The validation in your example is validation of a value object, not an entity (or aggregate root).
I would separate the validation into distinct areas.
- Validate internal characteristics of the
Email
value object internally.
I adhere to the rule that aggregates should never be in an invalid state. I extend this principal to value objects where practical.
Use createNew()
to instantiate an email from user input. This forces it to be valid according to your current rules (the "user@email.com" format, for example).
Use createExisting()
to instantiate an email from persistent storage. This performs no validation, which is important – you don't want an exception to be thrown for a stored email that was valid yesterday but invalid today.
class Email { private String value_; // Error codes const Error E_LENGTH = "An email address must be at least 3 characters long."; const Error E_FORMAT = "An email address must be in the 'user@email.com' format."; // Private constructor, forcing the use of factory functions private Email(String value) { this.value_ = value; } // Factory functions static public Email createNew(String value) { validateLength(value, E_LENGTH); validateFormat(value, E_FORMAT); } static public Email createExisting(String value) { return new Email(value); } // Static validation methods static public void validateLength(String value, Error error = E_LENGTH) { if (value.length() < 3) { throw new DomainException(error); } } static public void validateFormat(String value, Error error = E_FORMAT) { if (/* regular expression fails */) { throw new DomainException(error); } } }
-
Validate "external" characteristics of the
Email
value object externally, eg, in a service.class EmailDnsValidator implements IEmailValidator { const E_MX_MISSING = "The domain of your email address does not have an MX record."; private DnsProvider dnsProvider_; EmailDnsValidator(DnsProvider dnsProvider) { dnsProvider_ = dnsProvider; } public void validate(String value, Error error = E_MX_MISSING) { if (!dnsProvider_.hasMxRecord(/* domain part of email address */)) { throw new DomainException(error); } } } class EmailDomainBlacklistValidator implements IEmailValidator { const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted."; public void validate(String value, Error error = E_DOMAIN_FORBIDDEN) { if (/* domain of value is on the blacklist */)) { throw new DomainException(error); } } }
优点:
-
Use of the
createNew()
andcreateExisting()
factory functions allow control over internal validation. -
It is possible to "opt out" of certain validation routines, eg, skip the length check, using the validation methods directly.
-
It is also possible to "opt out" of external validation (DNS MX records and domain blacklisting). Eg, a project I worked on initially validated the existance of MX records for a domain, but eventually removed this because of the number of customers using "dynamic IP" type solutions.
-
It is easy to query your persistent store for email addresses that do not fit the current validation rules, but running a simple query and treating each email as "new" rather than "existing" – if an exception is thrown, there's a problem. From there you can issue, for example, a
FlagCustomerAsHavingABadEmail
command, using the exception error message as guidance for the user when they see the message. -
Allowing the programmer to supply the error code provides flexibility. For example, when sending a
UpdateEmailAddress
command, the error of "Your email address must be at least 3 characters long" is self explanatory. However, when updating multiple email addresses (home and work), the above error message does not indicate WHICH email was wrong. Supplying the error code/message allows you to provide richer feedback to the end user.
I wrote a blog post on this topic a while back. The premise of the post was that there are different types of validation. I called them Superficial Validation and Domain Based Command Validation.
This simple version is this. Validating things like 'is it a number' or 'email address' are more often than not just superficial. These can be done before the command reaches the domain entities.
However, where the validation is more tied to the domain then it's right place is in the domain. For example, maybe you have some rules about the weight and type of cargo a certain lorry can take. This sounds much more like domain logic.
Then you have the hybrid types. Things like set based validation. These need to happen before the command is issued or injected into the domain (try to avoid that if at all possible – limiting dependencies is a good thing).
Anyway, you can read the full post here: How To Validate Commands in a CQRS Application
I'm still experimenting with this concept but you can try Decorators. If you use SimpleInjector you can easily inject your own validation classes that run ahead of your command handler. Then the command can assume it is valid if it got that far. However, This means all validation should be done on the command and not the entities. The entities won't go into an invalid state. But each command must implement its own validation fully so similar commands may have duplication of rules but you could either abstract common rules to share or treat different commands as truly separate.
You can use a message based solution with Domain Events as explained here .
Exceptions are not the right method for all validation errors, is not said that a not valid entity is an exceptional case.
If the validation is not trivial, the logic to validate the aggregate can be executed directly on the server and while you are trying to set new input you can raise a Domain Event to tell to the user (or the application that is using your domain) why the input is not correct.