Skip to content

Expo Custom Rules

SteffenBlake edited this page Sep 28, 2026 · 1 revision

Custom rules

Use a reusable attribute when several models share a rule. Use a model-specific validation method when a rule needs other properties on the same model or should return several messages.

Create a reusable attribute

Implement IExpoValidationAttribute with a static bool IsValid(object? value) method:

// IsEvenAttribute.cs
[AttributeUsage(AttributeTargets.Property)]
public sealed class IsEvenAttribute(string? message = null) : Attribute, IExpoValidationAttribute
{
    public string? Message { get; } = message;

    public static bool IsValid(object? value)
    {
        return value is null || value is int number && number % 2 == 0;
    }
}
// PairedCourseCapacity.cs
[Expo]
public sealed partial class PairedCourseCapacity
{
    [IsGreaterThan(0)]
    [IsEven("Capacity must allow students to work in pairs.")]
    public int Capacity { get; init; }
}

This custom rule allows null, accepts even integers, and rejects other values. The positive comparison remains a separate rule, so zero fails even though it is even.

The static method receives the property value, rather than an attribute instance or the model. Constructor arguments cannot configure that static method's behavior. Use a distinct attribute for a distinct reusable condition; the message argument configures the failure text.

Expo uses the optional constructor message or named Message argument when available. Without one, a reusable custom rule reports a default message such as Capacity is invalid.

Validate collection items

A reusable attribute on an enumerable property checks each item. The attribute controls how null items behave. To require non-null values, return false for null in IsValid.

An empty or null outer collection does not run an item rule. Add [IsRequired] and a collection size rule when the outer collection must exist and contain entries. See Collections.

Add model-specific validation

Mark a property with [CustomValidation], then implement the generated private partial method named Validate followed by the property name:

// CourseBooking.cs
[Expo]
public sealed partial class CourseBooking
{
    public bool RequiresLab { get; init; }

    [CustomValidation]
    public string? Room { get; init; }

    private partial IEnumerable<string> ValidateRoom()
    {
        if (RequiresLab && string.IsNullOrWhiteSpace(Room))
        {
            return ["A lab course requires a room."];
        }

        return [];
    }
}

The method must return IEnumerable<string>. Each returned string becomes an error at /Room; an empty sequence means the rule passed. Return several strings when you need several messages for the same property.

This method can read the whole model. It runs even when Room is null, so write the null checks your rule needs. On a collection property, this hook receives no individual item argument: inspect the collection through the model and return messages at the property pointer.

Custom hooks are synchronous. Keep external lookups in your application flow rather than trying to await them inside the generated validation contract. Exceptions from a custom rule propagate to the caller of TryValidate.

See Property comparisons for declarative relationships between two fields and Errors and metadata for reporting custom failures.

Clone this wiki locally