Validation
Every code snippet on this page is in the validationTests module of DocCode. The tests are yours to explore and modify. Please send us your feedback and contributions.
A good application strives to ensure the integrity of data before saving them to permanent storage in part by screening new and changed with validation rules.
Validation in a web application must be performed on the server … always. Client-side validation is no substitute for server-side validation.
But client-side validation can be essential to the user experience. Users want to know immediately when input is bad. They despise filling in a long form only to have it rejected by the backend. We expect the app to catch bone head mistakes before we submit them. Tell us now that the date is required, the name is too long, daffodils don’t come in black, and we can’t receive a product until we’ve ordered it.
Support for validation is built into Breeze entities and the Breeze processing pipelines. Breeze maintains lists of validation rules and applies them automatically at specific moments in the entity life-cycle. But you remain always in control. Breeze discovers some validation rules and infers others; but you can add your own rules and remove any rule that you don’t like. You choose when Breeze validates. You can validate any set of entities or entity properties at any time. You decide what the error messages say and how they’re presented.
This topic covers the most important aspects of the Breeze validation system.
Validator: the Breeze validation rule
Validation is a process of judging the current state of an entity with validation rules. Each rule assesses a fact about the validity of an entity or one of its properties. A rule judges but it does not change; the entity values are the same, before and after evaluation.
Concretely, a validation rule is an instance of the Breeze Validator class.
Please read the Breeze API documentation for the Validator class. It has a lot of good information about how validations work and how to write them.
Most Breeze validators evaluate property data. A length validator can detect if the CompanyName is too long; a required validator determines that the CompanyName has a value; a string data type validator ensures that the CompanyName is a string, not a number or a date. These rules combine to determine the overall validity of the CompanyName property.
During validation, Breeze calls each validator’s validate method, passing in the value to evaluate and such context as the rule requires. If the value passes validation, the validate method returns null; if the value fails validation, the method returns a ValidationError.
Breeze accumulates ValidationErrors in the entity’s validation errors collection which you can access by calling entity.entityAspect.getValidationErrors(). When a rule fails, Breeze adds the rules ValidationError to the collection. When a rule passes, Breeze removes the previous ValidationError associated with that rule.
After a full entity validation, a “clean” entity will have an empty validation errors collection. The entity is invalid if there are any errors left in the collection.
Breeze does not object to entities having errors. It’s the developer who decides what to do about invalid entities. Breeze won’t interfere except on one occasion: Breeze won’t save an entity with validation errors. In fact, it won’t save any entities in a change-set if even one of them has errors.
An application typically tells the user when an entity has errors and tries to guide the user toward correcting them. Breeze has no prescription for doing this; it’s the developer’s job to present errors. Breeze does raise an event when errors are added and removed from the error collection; the developer can listen by subscribing to an entity’s entityAspect.validationErrorsChanged event and adjust application behavior accordingly.
Automatic validation
The Breeze EntityManager can validate an entity in cache at four predetermined times:
- the entity enters cache as a result of a query
- an entity is added or attached to the EntityManager
- an entity property value is changed
- an entity is about to be saved
The manager’s ValidationOptions determines whether the manager will or will not validate at those times. The default options are:
|
Option |
Default |
|
validateOnQuery |
false |
|
validateOnAttach |
true |
|
validateOnPropertyChange |
true |
|
validateOnSave |
true |
You can change those settings by updating the manager’s options. For example, let's stop validating when we attach an entity to a manager:
// copy options, changing only "validateOnAttach"
var valOpts = em.validationOptions.using({ validateOnAttach: false });
// reset manager's options
manager.setProperties({ validationOptions: valOpts });
We can make this the default for all future managers:
valOpts.setAsDefault();
Breeze automatically validates entities in cache. It won’t do so for detached entities. For example, a newly created Customer is technically invalid because its CompanyName is null and that property is required. Breeze does not validate the customer until you add it to the cache. This gives you time to set the values of a new entity before it enters the cache and triggers validation.
Manual validation
You can validate an individual entity at any time, whether it is attached or not:
if (!newCustomer.entityAspect.validateEntity()) {/* do something about errors */}
You can also validate a specific property:
if (!newCustomer.entityAspect.validateProperty("CompanyName")) {
/* do something about errors */}
Add a Breeze validator
Of course validation works only because there are validation rules (AKA, "validators") associated with the EntityType. It's up to you to ensure that the right validators are in place.
The Breeze Validator class has a few stock property validators.
- several "dataType" validators that ensure new values conform to the target data type
- maxLength
- required
Breeze adds these property validators for you when it processes the metadata information generated by Breeze.NET from your server-side model classes.
What if the server-side class didn't adequately constrain the property? Suppose the MyThing.Name string property lacks the [Required] attribute.
You can add the required validation to the client-side definition of that property yourself:
var myThingType = manager.metadataStore.getEntityType("MyThing");
myThingType
.getProperty("Name").validators.push(Validator.required);
Write a custom validator
You can write your own validators too. A validator is essentially the same whether stock or custom.
You create a custom validator by calling the Validator’s constructor with three parameters:
- The name of your validator
- The validation function that actually performs the assessment; Validator.validate delegates to this function.
- An optional, arbitrary context object which is your chance to supply the function with any external information it needs.
To illustrate, we’ll add a jingoistic validator to our application that only approves of the United States. We start with the validation function for that rule:
function countryIsUSValidationFn(value, context) {
if (value == null) return true; // '== null' matches null and empty string
return value.toUpperCase().startsWith("US");
};
Next we construct a new validator to apply this function:
var countryIsUSValidator = new breeze.Validator(
"countryIsUS", // validator name
countryIsUSValidationFn, // validation function
{ // validator context
messageTemplate: "'%displayName%' must start with 'US'"
});
The validator context object defines a messageTemplate that Breeze will recognize and use to construct an error message, substituting the property name for “%displayName%”.
We might write some unit tests that call validate with a variety of values, something you can do to stock validators as well.
Apply the validator
Now that we've crafted a new validator, it's time to put it to work. Let's add this validator to the property of an Employe. We get the EntityType for the Employee from a MetadataStore and add the rule to the validators for the Employee type’s “Country” DataProperty:
var employeeType = manager.metadataStore.getEntityType("Employee");
employeeType
.getProperty("Country")
.validators.push(countryIsUSValidator);
The “countryIsUS” rule is now active for the Employee.Country property.
It’s a general purpose rule that we could apply to any other string property. For example, we might only do business with US companies:
var customerType = manager.metadataStore.getEntityType("Customer");
customerType
.getProperty("Country") // Customer has "Country" property too
.validators.push(countryIsUSValidator);
The EntityType must be present in metadata before you can apply the rule. You can define the rule before getting the metadata, perhaps immediately after launching the application. But you can't add the validator to the type or any of its properties until the type is defined. For most applications, you must wait untl the client has retrieved metadata from the server. You can fetch that metadata explicitly or wait until Breeze gets it implicitly during the first query.
Parameterized validator
The “countryIsUS” property now excludes non-US customers. What if a Canadian company wants to use this validator and wants only Canadian customers?
Rather than mint a completely new rule, we can generalize the one we have:
function countryValidationFn(value, context) {
if (value == null) return true; // '== null' matches null and empty string
return value.toUpperCase().startsWith(context.country.toUpperCase());
};
Notice that we’re actually using the context object this time. It holds the value (the country name) to compare with user input.
Then we create a validator factory (a function returning a validator) instead of a fixed validator object:
// returns a countryValidator with its parameters filled
function countryValidatorFactory(context) {
return new breeze.Validator(
"countryValidator", // validator name
countryValidationFn, // validation function
{ // validator context
messageTemplate: "'%displayName%' must start with '%country%'",
country: context.country
});
}
Notice that the message template incorporates the parameter.
Finally, our Canadian friends use the factory to add their version of the rule to the model:
// create a Canada-only validator
var canadaOnly = countryValidatorFactory({ country: "Canada" });
// add the Canada-only validator
customerType
.getProperty("Country")
.validators.push(canadaOnly);
Entity-level validator
Some business rules evaluate the entity as a whole. They might confirm that the values of several properties are collectively consistent or that a parent entity (e.g., Order) has proper child entities (e.g., at least one OrderDetail).
To illustrate, we’ll add a rule to test if a customer postal code is a valid US zip code. We only want to perform that test if the customer is in the USA.
First, the validation function:
// The value to assess will be an entity
// with Country and PostalCode properties
function zipCodeValidationFn(entity, context) {
// This validator only validates US Zip Codes.
if (entity.getProperty("Country") === "USA") {
var postalCode = entity.getProperty("PostalCode");
context.postalCode = postalCode;
return isValidZipCode(postalCode);
}
return true;
};
function isValidZipCode(value) {
var re = /^\d{5}([\-]\d{4})?$/;
return (re.test(value));
}
Take note of this line:
context.postalCode = postalCode;
We added the actual entity property value as a new property of the context. Breeze can pick up that value later and plug it into the error message if the template has a “%postalCode% token.
Now the validator:
var zipCodeValidator = new breeze.Validator(
"zipCodeValidator",
zipCodeValidationFn,
{ messageTemplate: "'%postalCode%' is not a valid US zip code" });
Finally, add this validation rule to the model.
// add US zip code validator to the entity (not to a property)
customerType
.validators.push(zipCodeValidator);
Register custom validators with Breeze
Typically, you define a custom validator, get the metadata from the server (implicitly or explictily), and then add your custom validator to the validator collections of the appropriate entity types and properties as discussed above That's usually all you have to do.
But there is a special case. When you get your metadata from a local serialized source rather than the server, you must take one more step: you must register that validator with Breeze.
For example, suppose you designed your application to load entities from local storage when it starts. In the previous session you exported the entity cache and stowed the serialized cache data to browser local storage (see "Export/Import"). Now when you start a new session, your app restores the cache and you pick up where you left off ... without having to hit the server. Pretty cool!
Unfortunately, the app throws an exception when it loads the locally stored data. The exception complains about an unknown, unregistered validation rule ... perhaps that custom zipCodeValidator we just created. What happened?
When you exported the cache of entities, you also exported the metadata; they're part of the serialized cache. Inside that metadata is a reference to a validator named zipCodeValidator. The name of the function is in the serialized metadata; the function definition is not. The metadata definition of the Customer includes the fact that a customer entity must be validated with something called "zipCodeValidator". Unfortunately, Breeze doesn't know what a "zipCodeValidator" is. When Breeze restores the metadata (and the entities) it needs your help in connecting the name of the validator to the validator function itself. You provide that help by registering your custom validator with the Breeze Validator class:
Validator.register(zipCodeValidator);
If you created a validator factory, you register that with a related method:
Validator.registerFactory(countryValidatorFactory, "countryValidator");
Notice the second parameter. That's the registration name and it must be the name of the validator that your factory creates; scroll up and you'll find that we named it "countryValidator". No, Breeze won't figure that out for you; it would have to execute your factory with a context object to produce a validator with the right name ... but has no way to create a valid context object
Make sure you register your custom validators and validator factories before retrieving metadata from local storage.
There is no harm in registering your custom validators. You only have to register them if you'll be getting metadata from a serialized source other than the server.
Remove a validator from the EntityType
You can remove a rule from the model by removing the validator from its collection.
var custValidators = customerType.validators; custValidators.splice(custValidators.indexOf(unwantedRule), 1);
Add and remove ValidationErrors
We removed the rule but not any ValidationErrors that were already produced by that rule. Those you must remove manually from the affected entities.
// Clear out any "unwantedRule" errors // Must do manually because that rule is now gone // and, therefore, can't cleanup after itself cust.entityAspect.removeValidationError(unwantedRule);
And of course you can insert a ValidationError of your own - one that you made up yourself without the aid of a validator - by doing this:
// create a fake ValidationError for a someValidator
var fakeError = new breeze.ValidationError(
someValidator, // a marker validator, perhaps faked
{}, // validation context
"You were wrong this time!" // error message
);
// add the fake error
cust.entityAspect.addValidationError(fakeError);
Customize the message templates
Don't like the messages produced by the stock validators? Need to translate them to another language? No problem. The messages are based on templates that you can access and change. The Validator.messageTemplates class property returns the template collection, keyed by the validator name. Configure it to suit your needs. For example, we could make the Validator.required message a bit more emphatic:
Validator.messageTemplates["required", "Dude! The '%displayName%' is really required ... seriously ... as in mandatory");
Your custom validators can keep their templates in this collection too.
Learn more
For more details, please consult the API documentation for the following related classes:
Please consult the API documentation for the following related classes:
