For my domain entities currently, all validation errors cause exceptions to be thrown. I don't like this as it doesn't allow me to compile all validation errors before exiting a method. I'd like to pursue something similar to what's described here: https://enterprisecraftsmanship.com/2016/09/13/validation-and-ddd/ ...in particular, the idea of returning IReadOnlyList from all entity methods. The problem I'm running into is regarding entity creation... how do I utilize this technique? I see no other way than to throw exceptions from the constructor when validation rules aren't met with the given parameter values.
2 Answers
This is a question I have been asking myself and doing research for quite some time now.
Below are a couple of ways you could be telling the caller of your constructors/methods what went wrong during the process they requested, in order of how idiomatic they are (but in reverse order of how effective I consider them):
Throw a composite (domain) exception
The gist of this approach is that you are still going to use exceptions, it is just that you are going to collect each validation exception before actually throwing.public Person(int age, string fullName) { var validationExceptions = new List<string>(); if (age < 18) validationExceptions.Add("Must be at least 18!"); if (string.IsNullOrWhiteSpace(fullName)) validationExceptions.Add("Name field must not be empty!"); if (validationExceptions.Any()) { throw new CompositeExpectedException(validationExceptions); } this.age = age; this.fullName = fullName; }
Factory methods
Constructors are basically a set of glorified methods with the signature of
{private/internal/public} {ClassName} {ClassName}(/* parameters */)
so the only thing you can do with them is complete construction successfully or throw an exception - there is no way around it.
Fortunately, there are other ways of returning information from a method:/// Prevent instantiation of this class through any other means besides the /// TryCreate method below. private Person(int age, string fullName) { this.age = age; this.fullName = fullName; } public static IEnumerable<string> TryCreate(int age, string fullName, out Person person) { var validationExceptions = new List<string>(); if (age < 18) validationExceptions.Add("Must be at least 18!"); if (string.IsNullOrWhiteSpace(fullName)) validationExceptions.Add("Name field must not be empty!"); if (validationExceptions.Any()) person = null; else person = new Person(age, fullName); return validationExceptions; }
A variation of this approach would be to swap the return type of the method and the out parameter.
(public nested or internal) factories
A variation of the solution above - it transforms the factory method into a fully-fledged factory object. This is particularly useful if you need some dependencies in order to figure out whether the parameters obey some business rule (say, for example, if the supplied username exists in your persistence layer).public class Person { private Person(int age, string fullName) { ... } public class Factory { public Factory(ISomeDependency someDependency) { ... } public static IEnumerable<string> TryCreate(int age, string fullName, out Person person) { ... } } }
If you are not a fan of nested classes, you can mark the Person constructor as internal and move the Factory class to its own file and keep it public. Any class outside the Person's assembly will have to resort to the Factory class if they want a Person instance.
Either way, any consumer of the Person class will only be able to instantiate a Person class through the Factory class and will be notified of any validation rule they might be breaking.
Here is some further reading on replacing exceptions with notifications if you would like to go down this path.Either
So far, we have used exceptions and out parameters to return 2 entirely different data types: the Person instance and the list of validation exceptions that have been encountered.
Instead of these, why not put both the Person instance and validation exceptions inside the same class? Either seems like a good candidate for this - a concept borrowed from the functional world, but that fits inside the OOP world of C# just nicely.public class Person { private Person(int age, string fullName) { ... } public static IEither<Person, IReadOnlyList<string>> TryCreate(int age, string fullName) { var validationExceptions = new List<string>(); if (age < 18) validationExceptions.Add("Must be at least 18!"); if (string.IsNullOrWhiteSpace(fullName)) validationExceptions.Add("Name field must not be empty!"); if (validationExceptions.Any()) return validationExceptions; else return new Person(age, fullName); } }
I personally prefer this approach the most because it forces the caller of the TryCreate method to explicitly handle both situations - all the data was good and the Person was successfully instantiated OR something was bad and a list of validation messages was returned. That IEither interface looks like:
/// <summary> /// Defines the contract for data types that will act as an exclusive or (XOR) discriminated unions. /// </summary> /// <typeparam name="T1"> Option 1. </typeparam> /// <typeparam name="T2"> Option 2. </typeparam> public interface IEither<out T1, out T2> { /// <summary>/// Does pattern matching based on the real underlying type. /// </summary> /// <param name="func1"> The func to be executed in case the type is option 1. </param> /// <param name="func2"> The func to be executed in case the type is option 2. </param> /// <typeparam name="TResult"> The type of result. </typeparam> /// <returns> The <see cref="TResult"/>. </returns> TResult Match<TResult>(Func<T1, TResult> func1, Func<T2, TResult> func2); }
As you can see, this method requires that the caller supplies 2 functions: one that will transform the result into the desired type if it is actually a T1, and the other function that will do the same if the actual return type is T2. Usage below:
Person.TryCreate(12, "Peter").Match( person => this.Ok() as IHttpActionResult, validationErrors => this.BadRequest(string.Join(Environment.NewLine, validationErrors)));
Now, regardless of what happens inside the TryCreate, the caller will know for sure that no surprise exception will be thrown and that they have handled each scenario that this method can yield.
Word of warning: although I consider this last approach as being the strongest and most comprehensive way of dealing with possible expected (be them validation or otherwise) exceptions (it is also a solution to the checked vs uncheked exceptions debate, but that is a whole different topic), I strongly suggest that before introducing this sort of code into production applications, you first apply this pattern in your own personal toy applications and get a thorough understanding of how it works!
This would be it. These are all the approaches I know of through which you can tell the caller not only that something went wrong during their call, but also what did!
Object creation is a technical concern that has nothing to do with enforcing business invariants. I would go as far as to say that absolutely zero business validation should occur in constructors. Validation is best left for during the processes within which invariants need to be enforced (where the data is used). Following this rule helps keep your system much more declarative and, therefore, easier to understand. Remember DDD is about modeling rules about behavior, not modeling rules about data.
What that means is that, given that a Customer
has to be over 25 years old to register
, the enforcement of that invariant occurs in the register
method, not the Customer
constructor. This allows the possibility of another rule that requires a Customer
to be under 25 years old for some other process.
The only exception to the above is when the values passed to the object are, themselves, what the object intends to abstract (it's identity) e.g. EmailAddress
. In this case the string passed into an EmailAddress
constructor must conform to a certain specification in order to be considered a valid address. This means that the above exception only applies to ValueObjects
.
Here is a link to an answer I posted to a similar question that may be of interest to you: Zero argument constructors and Always Valid entities
All that said, you don't want to go down the road of compiling all validation errors before exiting. Not only does it make your methods much more complicated, it only works for the simplest of processes where all validations can occur before any real logic is executed. It seems like a good idea when the only invariants are the existence of address
and dayOfWeek
strings, but falls apart for more complicated operations. Furthermore, many domain errors aren't meant to be viewed directly by clients (so what are you going to do with them? Parse a DomainErrorList
into a ClientErrorList
? Overkill!).
Leave simple validation out of your domain altogether, and raise it "upwards" into your view. This is precisely why ViewModels
exist in the first place. What I mean is, if the purpose of collecting error messages for some input is to then expose them to your client, then keep all of that validation in the view itself (and do it the way MS does it in their ViewModels
-> procedurally). I may feel redundant, but it's also decoupled.
-
3
I would go as far as to say that absolutely zero business validation should occur in constructors.
While I see how this can arguably adhere to the letter of SRP, I strongly disagree that it should be applied as such. The beauty of having validation in a constructor is that you can guarantee that no invalid object will ever be created; and it's virtually impossible for any developer to forget to use the validation (unless they create a different constructor altogether). The only real drawback here is serialization; as it relies on creating an (invalid) object and then later populating it.– FlaterCommented Oct 29, 2018 at 8:30 -
@Flater Not only is there no such guarantee (i.e. reflection), the drawback you point out is an incredibly common use case. As I point out above, object construction is not a business concern, and should therefore not be a process where business rules are enforced. Furthermore, as I attempt to explain above (maybe poorly), validation on construction either becomes impossible, incredibly convoluted, or leads down a rabbit hole as use cases develop as it cannot be known on construction what the "next use case" is going to be. See the link I posted for more on that. Commented Oct 29, 2018 at 15:19
-
1@Flater That said, there are certain kinds of data validation that I would consider appropriate for a constructor. For example, ensuring
startDate
is less thanendDate
. The line on these kinds of validations is not always clear, so it's important to be wary. Because the OP is really asking about collecting and displaying errors, I decided to leave this bit out of my post. Commented Oct 29, 2018 at 15:23 -
@Flater Think about it like this: It doesn't matter if an invalid object can be created if you can't actually do anything with it. In this way, we can keep our focus on the behavior of our system and let data become an implementation detail. This is the perspective DDD intends to bring. The only case I can think of where an object needs to "always be valid" is when I intend to create a dependency (ask it for data). Importantly, this practice should be avoided as much as possible and considered very carefully when such a situation arises. Commented Oct 29, 2018 at 15:36
-
@Flater Your comment makes perfect sense in general, but "business validation" has a very specific meaning in the context of DDD. A constructor should always protect its class invariant, but in DDD, entities often have a fairly narrow responsibility and "business rules" are modeled externally. Commented Dec 28, 2018 at 4:58