Domain Validation in a CQRS architecture

前端 未结 11 1499
南旧
南旧 2020-12-12 11:25

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

相关标签:
11条回答
  • 2020-12-12 11:51

    I am at the beginning of a project and I am going to implement my validation outside my domain entities. My domain entities will contain logic to protect any invariants (such as missing arguments, null values, empty strings, collections, etc). But the actual business rules will live in validator classes. I am of the mindset of @SonOfPirate...

    I am using FluentValidation that will essentially give me bunch of validators that act on my domain entities: aka, the specification pattern. Also, in accordance with the patterns described in Eric's blue book, I can construct the validators with any data they may need to perform the validations (be it from the database or another repository or service). I would also have the option to inject any dependencies here too. I can also compose and reuse these validators (e.g. an address validator can be reused in both an Employee validator and Company validator). I have a Validator factory that acts as a "service locator":

    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
                }
        }
    
    }
    

    And the validator would contain code, something like this:

       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();
                ...
    }
    
        }
    

    We have to support more than one type of presentation: websites, winforms and bulk loading of data via services. Under pinning all these are a set of services that expose the functionality of the system in a single and consistent way. We do not use Entity Framework or ORM for reasons that I will not bore you with.

    Here is why I like this approach:

    • The business rules that are contained in the validators are totally unit testable.
    • I can compose more complex rules from simpler rules
    • I can use the validators in more than one location in my system (we support websites and Winforms, and services that expose functionality), so if there is a slightly different rule required for a use case in a service that differs from the websites, then I can handle that.
    • All the vaildation is expressed in one location and I can choose how / where to inject and compose this.
    0 讨论(0)
  • 2020-12-12 11:51

    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

    0 讨论(0)
  • 2020-12-12 11:53

    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.

    0 讨论(0)
  • 2020-12-12 11:54

    I would not call a class which inherits from EntityBase my domain model since it couples it to your persistence layer. But that's just my opinion.

    I would not move the email validation logic from the Customer to anything else to follow the Open/Closed principle. To me, following open/closed would mean that you have the following hierarchy:

    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);
    }
    

    You suggestions moves the control from the domain model to an arbitrary class, hence breaking the encapsulation. I would rather refactor my class (Customer) to comply to the new business rules than doing that.

    Use domain events to trigger other parts of the system to get a more loosely coupled architecture, but don't use commands/events to violate the encapsulation.

    Exceptions

    I just noticed that you throw DomainException. That's a way to generic exception. Why don't you use the argument exceptions or the FormatException? They describe the error much better. And don't forget to include context information helping you to prevent the exception in the future.

    Update

    Placing the logic outside the class is asking for trouble imho. How do you control which validation rule is used? One part of the code might use SomeVeryOldRule when validating while another using NewAndVeryStrictRule. It might not be on purpose, but it can and will happen when the code base grows.

    It sounds like you have already decided to ignore one of the OOP fundamentals (encapsulation). Go ahead and use a generic / external validation framework, but don't say that I didn't warn you ;)

    Update2

    Thanks for your patience and your answers, and that's the reason why I posted this question, I feel the same an entity should be responsible to guarantee it's in a valid state (and I have done it in previous projects) but the benefits of placing it in individual objects is huge and like I posted there's even a way to use individual objects and keep the encapsulation but personally I am not so happy with design but on the other hand it is not out of the table, consider this ChangeEmail(IEnumerable> validators, string email) I have not thought in detail the imple. though

    That allows the programmer to specify any rules, it may or may not be the currently correct business rules. The developer could just write

    customer.ChangeEmail(new IValidator<Customer>[] { new NonValidatingRule<Customer>() }, "notAnEmail")
    

    which accepts everything. And the rules have to be specified in every single place where ChangeEmail is being called.

    If you want to use a rule engine, create a singleton proxy:

    public class Validator
    {
        IValidatorEngine _engine;
    
        public static void Assign(IValidatorEngine engine)
        {
            _engine = engine;
        }
    
        public static IValidatorEngine Current { get { return _engine; } }
    }
    

    .. and use it from within the domain model methods like

    public class Customer
    {
        public void ChangeEmail(string email)
        {
            var rules = Validator.GetRulesFor<Customer>("ChangeEmail");
            rules.Validate(email);
    
            // valid
        }
    
    }
    

    The problem with that solution is that it will become a maintenance nightmare since the rule dependencies are hidden. You can never tell if all rules have been specified and working unless you test every domain model method and each rule scenario for every method.

    The solution is more flexible but will imho take a lot more time to implement than to refactor the method who's business rules got changed.

    0 讨论(0)
  • 2020-12-12 11:55

    I wouldn't suggest trowing big pieces of code into your domain for validation. We eliminated most of our awkward placed validations by seeing them as a smell of missing concepts in our domain. In your sample code you write I see validation for an e-mail address. A Customer doesn't have anything to do with email validation.

    Why not make an ValueObject called Email that does this validation at construct?

    My experience is that awkward placed validations are hints to missed concepts in your domain. You can catch them in Validator objects, but I prefer value object because you make the related concept part of your domain.

    0 讨论(0)
提交回复
热议问题