Skip to content

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

Dext Fluent Validation

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.


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:

  1. 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”).
  2. Dependency on Magic Strings: Custom validators often require property names as string literals, introducing risks during refactoring.
  3. Coupling with the Entity: Validations are statically bound to the data class, making it difficult to use different rules for different business contexts.

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.

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;

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 RuleFor for the Prop<string>, Prop<Integer>, etc. records. This avoids erroneous implicit conversions to empty basic types and ensures that fields are validated at compile time.

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 expressions
RuleFor(Model.Active = True).WithMessage('The user must be active');
// Fluent conditional based on strongly-typed expressions
RuleFor(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 method
RuleFor('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 age
RuleFor('Email').Required.When(function(Model: TUser): Boolean
begin
Result := Model.Age >= 18;
end);

Instead of repeating complex regular expressions across multiple validators, Dext provides a centralized, localized registry:

// Registration in Startup
TValidationPatterns.Register('PostalCode', '^\d{5}$', 'fr-FR');
// In the Validator
RuleFor(m.PostalCode).MatchesPattern('PostalCode', 'fr-FR');

The main differentiator of Fluent Validation in Dext is its automatic coupling with the Web server’s Model Binding:

  1. DI Container Registration: Register your validator class in services during startup:
    Services.AddSingleton<IValidator<TUser>, TUserValidator>;
  2. Auto-Validation: When an HTTP request hits an endpoint mapped to the TUser parameter, Dext automatically locates the corresponding validator in the DI container and executes validation before your method is even invoked.
  3. 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):

  1. Same DI Registration: The same dependency injection registered in the container (Services.AddSingleton<IValidator<TUser>, TUserValidator>) is leveraged by the ORM.
  2. SaveChanges Interception: During the execution of SaveChanges or SaveChangesAsync, TDbContext automatically locates the registered validator for each modified or inserted entity class.
  3. Prevention of Invalid Data: Validation runs before generating SQL insert or update commands in the database. If it fails, an EValidationException containing all error messages is thrown, immediately aborting the transaction and preventing invalid data from reaching the database.

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 (var declared 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.


To dive deeper into the implementation details and check out other practical validation examples in the framework, consult the official documentation: