Validate Event (Property)

Summary

The Validate event defines validation rules for individual property values. When a property value changes, the validation logic runs and rejects invalid values by adding an error message. The property retains its last valid value until a valid value is provided.

When does it fire?

Validation fires during the set pipeline, after AutoCorrect:

Set property → ReadOnly check → AutoCorrect → Validate → (if valid) Dirty dependents → Changed

If validation rejects the value, the property keeps its previous valid value and the error message is surfaced to the caller.

Syntax

productUom.Validate(u => u.Factor)
    .RejectIf(v => v <= 0)
    .WithMessage("Factor must be greater than zero");

Fluent API

Step Method Description
Required .RejectIf(condition) Condition that, when true, rejects the value
Required .WithMessage(message) Error message shown when validation fails

RejectIf overloads

Signature Description
.RejectIf(value => bool) Validate against the proposed value only
.RejectIf((value, args) => bool) Validate with access to the entity and services via args

WithMessage overloads

Signature Description
.WithMessage("string") Static error message
.WithMessage(entity => "string") Dynamic message with access to the entity

Scenarios

1. Rejecting out-of-range values

A product accessory quantity must be at least 1. If someone enters 0 or a negative number, it is rejected.

[Logic]
public class ProductAccessoryBL(ProductAccessory.Logic productAccessory)
{
    [RegisterLogic]
    public void ValidateQuantity()
    {
        productAccessory.Validate(a => a.Quantity)
            .RejectIf(qty => qty < 1.0m)
            .WithMessage("Quantity must be at least 1");
    }
}

2. Conditional validation based on entity state

A product UOM's factor must always be 1 when it is the main unit. The validation uses the args overload to access the parent entity.

[Logic]
public class ProductUomBL(ProductUom.Logic productUom)
{
    [RegisterLogic]
    public void ValidateMainUomFactor()
    {
        productUom.Validate(u => u.Factor)
            .RejectIf((value, args) => args.Entity.Operation == UomOperation.Main && value != 1)
            .WithMessage(e => $"{e.Name} is a main unit and its factor should always be one.");
    }
}

3. Validating a required text field is not empty

A product UOM name must be specified unless it is the main unit (which gets its name automatically).

[Logic]
public class ProductUomBL(ProductUom.Logic productUom)
{
    [RegisterLogic]
    public void ValidateName()
    {
        productUom.Validate(u => u.Name)
            .RejectIf((name, args) => string.IsNullOrEmpty(name)
                                   && args.Entity.Operation != UomOperation.Main)
            .WithMessage("Name must be specified unless the operation type is Main");
    }
}

4. Uniqueness validation using data access

A sales order document number must be unique. The validation queries existing orders through args.GetEntities<T>().

[Logic]
public class SalesOrderBL(SalesOrder.Logic salesOrder)
{
    [RegisterLogic]
    public void ValidateDocNumber()
    {
        salesOrder.Validate(o => o.DocNumber)
            .RejectIf((docNumber, args) =>
            {
                return args.GetEntities<SalesOrder>()
                    .Any(o => o.DocNumber == docNumber && o.Guid != args.Entity.Guid);
            })
            .WithMessage(o => $"Duplicate document number {o.DocNumber}");
    }
}

5. Cross-field date range validation

An end date must not come before a start date on an entity.

[Logic]
public class ContractBL(Contract.Logic contract)
{
    [RegisterLogic]
    public void ValidateEndDate()
    {
        contract.Validate(c => c.EndDate)
            .RejectIf((endDate, args) => endDate < args.Entity.StartDate)
            .WithMessage("End date must be on or after the start date");
    }
}

Notes

  • Validation fires immediately when the property is set — it does not wait until save.
  • When validation fails, the property value reverts to its last valid value.
  • Multiple validators can exist on the same property; the first one failing validation will stop the execution of the remaining validators.
  • For whole-entity validation that runs at save time, see ENTITY_VALIDATE.md.