Custom validation in LightSpeed

LightSpeed has a bunch of handy validations built in, to check things like ranges, string lengths, email and URL validity, and so on. In addition, the validation system is extensible, allowing you to build your own custom validations. In this article, I’ll show how to build a simple custom validation and apply it to a domain model.

The validation requirement

For my example, I’m going to build an “is in list” validation. When you apply the validation to a property, you’ll provide a list of permitted values, and LightSpeed will check that the property value is in the list. (In practice, you might prefer to model such a property as an enum, but work with me here.) For simplicity, I’m going to restrict the validation to strings.

LightSpeed validation classes

A LightSpeed validation is split into two classes: a validation attribute, which is what gets applied to the domain model, and a validation rule, which is where the actual logic lives. The reason these are separate is because it is possible for several different attributes to use the same rule: for example, you could build specific formatting validation attributes, such as an OrderNumberFormatAttribute, over a formatting rule class that was parameterised by a regex.

In our case, however, there’s no existing rule that would be used to implement the “is in list” validation. So I will need to create both classes from scratch.

Implementing the validation rule

Validation rule classes always derive from the snappily-named ValidationRule, and override the Validate method.

In my case, I need the rule to store a list of permitted values (because different instances could have different sets of permitted values), so I have to do some plumbing before I can implement Validate:

public class IsInListValidationRule : ValidationRule
  private readonly ReadOnlyCollection<string> _permittedValues;
  public IsInListValidationRule(IEnumerable<string> permittedValues)
    _permittedValues = permittedValues.ToList().AsReadOnly();

With the permitted list set up, implementing Validate is a snap:

  public override void Validate(ValidationContext validationContext)
    string value = (string)(validationContext.TargetValue);  // Error handling omitted
    bool isPermitted = _permittedValues.Contains(value, StringComparer.OrdinalIgnoreCase);
    if (!isPermitted)
      validationContext.AddError("Value is not in list of permitted values", this);

Let’s take this apart quickly. ValidationContext.TargetValue contains the value of the field being validated. You can also get hold of other information such as the property name (ValidationContext.TargetName) for use in an error message and other property values (ValidationContext.GetAttributeValue) if validity depends on the state of the object as a whole. So the first two lines just check if the value being validated is part of the permitted value list.

ValidationContext.AddError registers an error on the property. This will appear in the Entity.Errors collection, the Entity.Error string, in Entity’s IDataErrorInfo implementation and — if you try to save an invalid entity — in the ever-popular ValidationException. So the last two lines register an error if the value was not part of the permitted list, with a blatantly unhelpful message. It’s not mandatory for validation messages to be unhelpful, but we’re programmers, right? If we wanted to write helpful messages we would have become user interface designers.

One little wrinkle: I’ve used an AddError overload that was added in a recent nightly build. The advantage of this overload is that it allows the UI layer to determine the type of validation failure, in case it wants to display different UI for different kinds of problem. If you’re using LightSpeed 3.11 RTM or an older nightly, just remove the this argument.

That’s it for the validation logic. Now we need an attribute so we can apply it to a field.

Implementing the validation attribute

Validation attributes always derived from the, you guessed it, ValidationAttribute class, and override the ValidationRule property. You can totally guess what I’m going to override it with.

public class ValidateIsInListAttribute : ValidationAttribute
  private readonly ValidationRule _rule;
  public ValidateIsInListAttribute(params string[] permittedValues)
    _rule = new IsInListValidationRule(permittedValues);
  public override ValidationRule ValidationRule
    get { return _rule; }

Needs moar CQRS, obviously, but apart from that it’s pretty self-explanatory.

Applying the validation attribute

Now we’ve built the validation attribute, we can start applying it to things. If you’re writing entities by hand, remember validation attributes go on fields, not properties:

public class Penguin : Entity<int>
  // numerous other interesting facts about penguins elided
  [ValidateIsInList("adelie", "king", "emperor", "yellow-eyed")]
  private string _species;
  public string Species
    get { return _species; }
    set { Set(ref _species, value, "Species"); }

If you’re using the designer, you’ll need to go into the LightSpeed Model Explorer, right-click the property and choose Add Custom Validation, then spell out the validation attribute as you want to see it appear in code (minus the attribute brackets):

SUPER SECRET NINJA TIP: If you’re using custom validations in the designer, get a recent nightly build. This makes it much easier to see in the explorer what each validation is — you can even enter a handy description. (Thanks to customer BradL for suggesting this feature.)

Try it out

Want to give it a try? Got custom validation requirements of your own? Concerned that your program might be harbouring an illegal penguin? Here’s the sample code, just ready and waiting for you to run. (Remember you’ll need to make a small change if you don’t have a recent nightly.)

Not got LightSpeed? The Express Edition is fully functional except for a limit of 8 entity types, and it’s FREE to use, even in commercial applications. Download it here!

Tagged as LightSpeed

Leave a Reply


Join our mailer

You should join our newsletter! Sent monthly:

Back to Top