In the previous article we covered entity mapping and types. But mapping alone does not guarantee data quality. What happens if someone inserts a product with a negative price, or a contact with an empty name?
Trysil provides a set of validation attributes that you place directly on entity fields. The ORM checks them automatically before every Insert and Update — if validation fails, no SQL is executed and you get a clear exception.
Available validation attributes
| Attribute | Applies to | Rule |
|---|---|---|
TRequired |
String, DateTime, TTNullable, TTLazy | Field cannot be empty, zero, or null |
TMaxLength(n) |
String | Length must be <= n |
TMinLength(n) |
String | Length must be >= n |
TMaxValue(n) |
Integer, Double | Value must be <= n |
TMinValue(n) |
Integer, Double | Value must be >= n |
TGreater(n) |
Integer, Double | Value must be > n |
TLess(n) |
Integer, Double | Value must be < n |
TRange(min, max) |
Integer, Double | Value must be in [min, max] |
TRegex(pattern) |
String | Value must match the regex |
TEmail |
String | Valid email format (built-in regex) |
All attributes live in Trysil.Validation.Attributes.
Using validation attributes
Here is a TTEmployee entity with comprehensive validation:
unit Employee.Model;
{$WARN UNKNOWN_CUSTOM_ATTRIBUTE ERROR}
interface
uses
Trysil.Types,
Trysil.Attributes,
Trysil.Validation.Attributes;
type
[TTable('Employees')]
[TSequence('EmployeesID')]
TTEmployee = class
strict private
[TPrimaryKey]
[TColumn('ID')]
FID: TTPrimaryKey;
[TRequired]
[TMinLength(2)]
[TMaxLength(50)]
[TColumn('Firstname')]
FFirstname: String;
[TRequired]
[TMinLength(2)]
[TMaxLength(50)]
[TColumn('Lastname')]
FLastname: String;
[TMaxLength(255)]
[TEmail]
[TColumn('Email')]
FEmail: String;
[TRange(18, 70)]
[TColumn('Age')]
FAge: Integer;
[TGreater(0.00)]
[TColumn('Salary')]
FSalary: Double;
[TColumn('VersionID')]
[TVersionColumn]
FVersionID: TTVersion;
public
property ID: TTPrimaryKey read FID;
property Firstname: String read FFirstname write FFirstname;
property Lastname: String read FLastname write FLastname;
property Email: String read FEmail write FEmail;
property Age: Integer read FAge write FAge;
property Salary: Double read FSalary write FSalary;
end;
With this definition:
-
FirstnameandLastnamemust be non-empty and between 2 and 50 characters -
Emailmust be a valid email address (if provided —TEmailskips empty strings) -
Agemust be between 18 and 70 -
Salarymust be greater than zero
Display names for error messages
By default, validation errors use the column name. You can override this with TDisplayName:
[TRequired]
[TDisplayName('First name')]
[TColumn('Firstname')]
FFirstname: String;
Now the error message will say "First name is required" instead of "Firstname is required".
Custom error messages
Every validation attribute accepts an optional error message:
[TRequired('Please enter the employee name')]
[TColumn('Firstname')]
FFirstname: String;
[TRange(18, 70, 'Age must be between 18 and 70')]
[TColumn('Age')]
FAge: Integer;
Handling validation errors
Validation runs automatically on Insert and Update. If any rule fails, Trysil raises an ETValidationException:
uses
Trysil.Exceptions;
var
LEmployee: TTEmployee;
begin
LEmployee := LContext.CreateEntity<TTEmployee>();
LEmployee.Firstname := ''; // will fail TRequired
LEmployee.Salary := -1000; // will fail TGreater(0.00)
try
LContext.Insert<TTEmployee>(LEmployee);
except
on E: ETValidationException do
ShowMessage(E.Message);
end;
end;
You can also validate explicitly without inserting:
try
LContext.Validate<TTEmployee>(LEmployee);
except
on E: ETValidationException do
// handle errors
end;
This is useful when you want to validate in a UI before the user clicks "Save".
Custom validators
For business rules that go beyond simple field checks, you can write a custom validator method and mark it with [TValidator]:
[TTable('Invoices')]
[TSequence('InvoicesID')]
TTInvoice = class
strict private
[TPrimaryKey]
[TColumn('ID')]
FID: TTPrimaryKey;
[TRequired]
[TColumn('IssueDate')]
FIssueDate: TDateTime;
[TRequired]
[TColumn('DueDate')]
FDueDate: TDateTime;
[TColumn('VersionID')]
[TVersionColumn]
FVersionID: TTVersion;
public
[TValidator]
procedure ValidateDates(const AErrors: TTValidationErrors);
property ID: TTPrimaryKey read FID;
property IssueDate: TDateTime read FIssueDate write FIssueDate;
property DueDate: TDateTime read FDueDate write FDueDate;
end;
procedure TTInvoice.ValidateDates(const AErrors: TTValidationErrors);
begin
if FDueDate < FIssueDate then
AErrors.Add('DueDate', 'Due date cannot be before issue date');
end;
The method is called automatically alongside the attribute-based validation. You can add multiple errors to AErrors — they are all collected before the exception is raised.
Validation and the ORM pipeline
Here is what happens when you call LContext.Insert<T>(LEntity):
-
Validation — all
[TRequired],[TMaxLength],[TRange], etc. attributes are checked, plus any[TValidator]methods -
Events —
[BeforeInsert]methods are called (more on events in a future article) - SQL execution — the INSERT statement is generated and executed
-
Post-events —
[AfterInsert]methods are called
If step 1 fails, steps 2-4 never happen. Your database stays clean.
What is next
We now have entities that are both correctly mapped and validated before they reach the database. In the next article we will explore filtering and querying — how to search, sort, and paginate data using Trysil's fluent filter builder.
Trysil is open-source and available on GitHub. If you find it useful, a star goes a long way!
Top comments (0)