Strongly-Typed Validation in Delphi: Introducing the Dext Fluent Validation API

Data validation is one of the most critical aspects of any software application. For years, Delphi developers have relied on extensive conditional blocks (if/then) scattered throughout their code or magic strings to validate data. Dext already offered a robust alternative with Attribute-Based Validation ([Required], [Range]), but complex enterprise applications demanded even greater flexibility.
Today, we are pleased to announce the completion of the Fluent Validation API in Dext — a validation engine based on fluent, strongly-typed code and fully integrated with ORM Smart Properties (Prop<T>) and the Web Model Binding pipeline.
The Limit of Declarative Attributes
Section titled “The Limit of Declarative Attributes”While decorating properties with attributes (such as [Required] or [StringLength(3, 50)]) works very well for simple scenarios, the declarative approach presents limitations when facing more complex business rules:
- Lack of Dynamic Conditionals: It is not simple to validate a field based on the state of another at runtime (e.g., “Email is required only if the notification type is Email”).
- Dependency on Magic Strings: Custom validators often require property names as string literals, introducing risks during refactoring.
- Coupling with the Entity: Validations are statically bound to the data class, making it difficult to use different rules for different business contexts.
The Solution: Fluent Validation API
Section titled “The Solution: Fluent Validation API”The Dext Fluent Validation API introduces a clean programmatic approach inspired by C#‘s FluentValidation. By inheriting from TAbstractValidator<T>, you declare rules fluently in the constructor of your validator class using the RuleFor method.
1. Basic Usage
Section titled “1. Basic Usage”type TUserValidator = class(TAbstractValidator<TUser>) public constructor Create; override; end;
constructor TUserValidator.Create;begin inherited Create;
RuleFor('Name').Required.Length(3, 50); RuleFor('Email').EmailAddress; RuleFor('Age').Range(18, 99);end;To execute validation programmatically in your service layer:
var Validator: TUserValidator; Result: TValidationResult;begin Validator := TUserValidator.Create; try Result := Validator.Validate(UserInstance); try if not Result.IsValid then raise Exception.Create(Result.ErrorMessage); finally Result.Free; end; finally Validator.Free; end;end;Advanced Features
Section titled “Advanced Features”1. Integration with Smart Properties (Strongly-Typed without Strings)
Section titled “1. Integration with Smart Properties (Strongly-Typed without Strings)”If your entities use Smart Properties (Prop<T>, StringType, IntType), you can completely eliminate string literals by mapping properties through a ghost object generated by Prototype:
constructor TOrderValidator.Create;begin inherited Create; var m := Prototype.Entity<TOrder>;
RuleFor(m.CustomerName).Required.Length(3, 100); RuleFor(m.Total).Range(1.0, 10000.0);end;[!NOTE] Internally, Dext exposes concrete overloads of
RuleForfor theProp<string>,Prop<Integer>, etc. records. This avoids erroneous implicit conversions to empty basic types and ensures that fields are validated at compile time.
2. Custom and Conditional Validations
Section titled “2. Custom and Conditional Validations”You can chain .When(...) conditionals and assertions based on expressions (Dext Style with Smart Properties) or traditional anonymous methods:
// --- Dext Style (Recommended - Using Smart Properties & Expressions) ---// The "Model" helper (or the short alias "M") is automatically exposed by TAbstractValidator<T>
// Custom assertion directly on model expressionsRuleFor(Model.Active = True).WithMessage('The user must be active');
// Fluent conditional based on strongly-typed expressionsRuleFor(Model.Email).Required.When(Model.Age >= 18);
// --- Traditional Style (Fallback - Using Strings and Anonymous Methods) ---// Useful for projects without the full Dext Smart Properties infrastructure
// Custom assertion with anonymous methodRuleFor('Active', function(Model: TUser): TValue begin Result := Model.Active; end).Must(function(Val: TValue): Boolean begin Result := Val.AsBoolean = True; end).WithMessage('The user must be active');
// Validate email only if the user is of legal ageRuleFor('Email').Required.When(function(Model: TUser): Boolean begin Result := Model.Age >= 18; end);3. Pattern Registry (TValidationPatterns)
Section titled “3. Pattern Registry (TValidationPatterns)”Instead of repeating complex regular expressions across multiple validators, Dext provides a centralized, localized registry:
// Registration in StartupTValidationPatterns.Register('PostalCode', '^\d{5}$', 'fr-FR');
// In the ValidatorRuleFor(m.PostalCode).MatchesPattern('PostalCode', 'fr-FR');Transparent Web Integration
Section titled “Transparent Web Integration”The main differentiator of Fluent Validation in Dext is its automatic coupling with the Web server’s Model Binding:
- DI Container Registration: Register your validator class in services during startup:
Services.AddSingleton<IValidator<TUser>, TUserValidator>;
- Auto-Validation: When an HTTP request hits an endpoint mapped to the
TUserparameter, Dext automatically locates the corresponding validator in the DI container and executes validation before your method is even invoked. - Standardized Response: If there are failures, execution is immediately aborted, raising a
TWebValidationException, which automatically returns an HTTP 400 Bad Request status containing a detailed JSON with the specific errors for each field.
Automatic Validation in the ORM (Dext Entity)
Section titled “Automatic Validation in the ORM (Dext Entity)”In addition to validating incoming HTTP requests, Dext integrates validation natively into the persistence lifecycle of your ORM (TDbContext):
- Same DI Registration: The same dependency injection registered in the container (
Services.AddSingleton<IValidator<TUser>, TUserValidator>) is leveraged by the ORM. - SaveChanges Interception: During the execution of
SaveChangesorSaveChangesAsync,TDbContextautomatically locates the registered validator for each modified or inserted entity class. - Prevention of Invalid Data: Validation runs before generating SQL insert or update commands in the database. If it fails, an
EValidationExceptioncontaining all error messages is thrown, immediately aborting the transaction and preventing invalid data from reaching the database.
Guaranteed Quality
Section titled “Guaranteed Quality”This implementation has been extensively validated:
- Unit Tests: Complete coverage testing fluent, conditional, custom rules, and auto-validation in REST web calls, with absolute success.
- Backward Compatibility: The framework internal code has been written without using inline variables (
vardeclared inside blocks), ensuring full compatibility of Dext with Delphi versions prior to 10.4.
Dext’s fluent validation is finalized, clean, and ready to shield your domain’s business rules.
Useful Links and Documentation
Section titled “Useful Links and Documentation”To dive deeper into the implementation details and check out other practical validation examples in the framework, consult the official documentation:
- Validation Documentation (Dext Book EN): Access the Technical Details
- Dext Official Repository: github.com/cesarliws/dext