A class invariants helper library.
Note that this isn't a test assertion library. This is for enforcing class invariants via assertions, which is generally going to be a much smaller set of comparison types that you would need in a test assertions library such as Shouldly.
Use NuGet:
PM> Install-Package check
Check.That(() => firstName).IsNotNull();
Check.That(() => lastName).IsNotNullOrEmpty();
Check.That(() => dateOfBirth.Year < 1989).IsTrue();
Failing checks throw an exception inheriting from InvariantException
:
var i1 = 10;
Check.That(() => i1 > 20).IsTrue(); // throws InvariantShouldBeTrueException: "(i1 > 20) should be true"
Check can be extended with custom invariant checkers. Create a checker class that inherits from CheckGenericInvariant<T>
and pass it to Check.That
as a generic parameter for instantiation:
Check.That<IValidatableViewModel, CheckValidatableViewModel>(() => loginViewModel);
The easiest way to demonstrate why class invariants is something we want to use is the creation of a domain model - an instance of a Person
for example. In our domain, when a person is created they may require a first name, a last name and an email address. We won't do any validation of the email address at this level, instead we'll assume that the address has already been validated. These three requirements will be our business rules for the use case of creating a person within the domain.
The requirements should be enforced within the piece of code that constructs the person - in this case the Person
class's constructor. If the requirements were only enforced outside of the constructor, then for a start you're violating Single Responsibility Principle as every piece of code that can construct a person needs to implement these requirements. Most of the person creation might be via a single form for someone doing data entry, so it appears to be fine to do this in the form handler. What happens when a data import tool gets added? If those requirements aren't implemented correctly in the import tool it could be a vector for bad (corrupt) data entering the system, which could have unpredicable effects.
So the requirements should really be implemented in the Person
class constructor. This is pretty easy to implement:
class Person
{
public Person(
string firstName,
string lastName,
string emailAddress)
{
if (string.IsNullOrEmpty(firstName))
{
throw new ArgumentException("First name is required", "firstName");
}
if (string.IsNullOrEmpty(lastName))
{
throw new ArgumentException("Last name is required", "lastName");
}
if (string.IsNullOrEmpty(emailAddress))
{
throw new ArgumentException("Email address is required", "emailAddress");
}
// continue creating the person
}
}
The selling point of Check (and other assertion helper libraries) is that this can be expressed a lot more concisely:
class Person
{
public Person(
string firstName,
string lastName,
string emailAddress)
{
Check.That(() => firstName).IsNotNullOrEmpty("First name is required");
Check.That(() => lastName).IsNotNullOrEmpty("Last name is required");
Check.That(() => emailAddress).IsNotNullOrEmpty("Email address is required");
// continue creating the person
}
}
This habit of asserting preconditions can be used outside of a domain model. Consider a simple function that, given natural numbers a0
and ak
calculates a0 / a1 / a2 ... / ak
:
double f(int a0, int ak)
{
var range = a0 < ak
? Enumerable.Range(a0, ak - a0 + 1)
: Enumerable.Range(ak, a0 - ak + 1).Reverse();
return range.Skip(1).Aggregate(
(double)range.First(),
(r, an) => r / an);
}
Ok so it sounded simple at first. This will work for any input (until it overflows double
's precision which happens pretty quickly) but if the range crosses zero (eg f(-2, 4)
or f(3, -3)`) the result is negative infinity. This is technically correct but may result in other errors, so we have a business rule where all inputs have to be greater than zero.
double f(int a0, int ak)
{
Check.That(() => a0 > 0).IsTrue();
Check.That(() => ak > 0).IsTrue();
// ...
In practice, the domain requirements will probably end up getting copied over several layers instead of staying isolated in the Person
constructor. This is because, pragmatically, a larger system will be spread over several layers and to actually ship quality, responsive code, you end up with domain validation in the UI. There's nothing inherently wrong with this as long as the system with the greatest responsibity is performing all of the checks required to enforce the business rules. In other words, the Person
constructor is the last line of defence when the system is constructing a model of a person, and it should be responsible for implementing the requirements by executing the set of class invariants.
Further reading:
- Invariant (Computer science)
- Class invariant
- Design Smell: Default Constructor
- Design by contract
- Assertion (software development)
Each .Is
method takes an optional message
parameter which overrides the automatically generated message.
Check.That(() => true).IsTrue("Bending the laws of space and time, are we?");
The generic invariant checker is the base type of all invariant checkers and contains the shared logic. If the type of the expression passed into Check.That
isn't handled by a checker, a generic invariant checker will be returned.
Checks that the target expression result is not null.
Checks that the target expression result is equal to the provided value (using .Equals()
to perform the comparison).
Checks that the target expression result is not equal to the provided value (using .Equals()
to perform the comparison).
Checks that the target expression result is or is not one of the provided values (using .Contains()
to perform the comparison).
The boolean invariant checker checks that an expression that results in a boolean is true or false. The resulting exception message displays the entire compiled expression for easier debugging:
var i1 = 10;
Check.That(() => i1 > 20).IsTrue(); // throws InvariantShouldBeTrueException: "(i1 > 20) should be true"
Check.That(() => 20 > i1).IsFalse() // throws InvariantShouldBeFalseException
The string invariant checker contains check methods for strings.
Checks that the target expression result is not null or empty.
var occupation = "Unicorn Wrangler";
Check.That(() => occupation).IsNotNullOrEmpty();
Checks that the target expression matches the supplied regular expression.
var phone = "(07) 1234 5678";
Check.That(() => phone).IsMatchForRegex(@"\([0]\d\) \d\d\d\d \d\d\d\d");
Checks that the target expression is a valid email address, according to the .NET regex found at http://emailregex.com/.
var email = "[email protected]";
Check.That(() => email).IsValidEmailAddress();
The type invariant checker contains check methods for System.Type
.
Checks that the type contains a public constructor that takes the provided parameter types (in order).
Check.That(typeof(TTaxCalulator)).HasPublicConstructorWithParameters(
typeof(ITaxConfiguration),
typeof(TaxStatus));
The Guid
invariant checker contains check methods for Guid
s.
Checks that the target is not null or equal to Guid.Empty
.
public Entity(Guid id)
{
Check.That(() => id).IsNotNullOrEmpty();
}
Extend Check with a custom invariant checker. Create a checker that implements CheckGenericInvariant<T>
:
public interface IValidatableViewModel
{
bool IsValid { get; }
}
public class CheckValidatableViewModel : CheckGenericInvariant<IValidatableViewModel>
{
public CheckValidatableViewModel(Expression<Func<IValidatableViewModel>> target)
: base(target)
{
}
public void IsValid()
{
if (!TargetValue.IsValid)
{
throw new ViewModelMustBeValidException(FieldName);
}
}
}
The checker type and the target type both have to be passed to Check.That
for the checker to resolve correctly.
Check.That<IValidatableViewModel, CheckValidatableViewModel>(() => viewModel).IsValid()
Note that this is a trivial example that could be implemented without a custom checker by simply executing
Check.That(() => viewModel.IsValid)
, but custom checkers with complex logic could be reused making the effort worthwhile.
The checker type is instantiated using a public constructor that takes Expression<Func<T>>
only, so make sure the constructor is correct for the checker.