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.