Search Results for

    Show / Hide Table of Contents

    Data Validation

    Aurelius provides easy and straightforward ways to validate your entities before they are persisted. By adding specific attributes to your entity, you can easily ensure that the entity is always saved to the database in a valid state.

    In the following example, our mapping specifies that the FName field of the TCustomer class is required and its length must not exceed 20 characters:

    uses {...}, Aurelius.Validation.Attributes;
    
    type
      [Entity, Automapping]
      TCustomer = class
      strict private
        FId: Integer;
    
        [Required, MaxLength(20)]
        FName: string;
      public
        property Id: Integer read FId write FId;
        property Name: string read FName write FName;
      end;
    

    If we then try to save an customer with a name longer than 20 characters:

      var Customer := TCustomer.Create;
      Manager.AddOwnership(Customer);
      Customer.Name := 'Name too long for customer';
      Manager.Save(Customer);
    

    Aurelius will refuse to save it, raising an EEntityValidationException exception:

    EEntityValidationException: Validation failed for entity of type "Entities.Customer.TCustomer": Field FName must have no more than 20 character(s)
    
    Note

    The validations occur at application level. They are not related to database-level checks. For example, to set the column length at database level, and more properties like nullability, you still should use mapping attributes, like the Column attribute.

    Built-in Validators

    Aurelius provides several built-in validators that you can use by applying attributes to your mapped class members:

    Required

    Ensures that the class member has a valid value.

    [Required]
    FRate: Integer;
    
    [Required]
    FBirthday: Nullable<TDateTime>;
    

    Validation of FRate will never fail because even a 0 (zero) value is considered a valid value. FBirthday will fail if it's null only.

    Note

    The exception is the string value. An empty string is not considered a value and will fail the Required validation

    MaxLength

    Specifies the maximum length of a string value.

    [MaxLength(30)]
    FName: string;
    

    MinLength

    Specifies the minimum length of a string value.

    [MinLength(5)]
    FName: string;
    

    Range

    Specifies the range of valid values for numeric values.

    [Range(1, 10)]
    FRate: Integer;
    

    In the previous example, FRate value must be between 1 and 10.

    EmailAddress

    Ensures the string values contains a valid e-mail address.

    [EmailAddress]
    FEmail: string;
    

    RegularExpression

    Ensures the string value matches the provided regular expression.

    [RegularExpression('^[0-9]{5}$')]
    FZipCode: string;
    

    Entity validators

    In addition to class members, you can also apply validators at the entity-level. You should use it the same way: add a validation attribute to the entity class. Of course, the validation attribute must make sense for the whole entity. None of the current built-in validators can be applied to the entity, since they check class member values, but you can create custom validators and apply them to the class.

    But the more straightforward way to add entity validators is using the OnValidate attribute. Just add the attribute to a method you want to be executed when the entity should be validated:

      {$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])}
      TCustomer = class  
        [OnValidate]
        function CheckBirthday: IValidationResult;
        [OnValidate]
        function CheckName(Context: IValidationContext): IValidationResult;
    

    The method must always return an interface of type IValidationResult. The method can receive a single argument of type IValidationContext, or no parameter at all.

    Warning

    By default, Delphi doesn't generate RTTI for non-published methods. That's why you must add the directive {$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])} to your class. If you don't do that, Aurelius won't know about the mentioned methods and they will not be invoked when the validation is performed!

    This is a example of implementation:

    function TCustomer.CheckBirthday: IValidationResult;
    begin
      Result := TValidationResult.Create;
      if Birthday.IsNull then Exit;
    
      if YearOf(Birthday) < 1899 then
        Result.Errors.Add(TValidationError.Create('A person born in XIX century is not accepted'));
      if (MonthOf(Birthday) = 8) and (DayOf(Birthday) = 13) then
        Result.Errors.Add(TValidationError.Create('A person born on August, 13th is not accepted'));
    end;
    
    function TCustomer.CheckName(Context: IValidationContext): IValidationResult;
    begin
      Result := TValidationResult.Create;
      if Name = 'invalid name' then
        Result.Errors.Add(TValidationError.Create('Invalid name'));
    end;
    

    You can have multiple methods marked with the OnValidate attribute. All methods will be executed, in the order they are declared in the class.

    Note

    The class member validators are executed first. If and only if there are no errors in class member validations, the entity validators are executed.

    Validation Messages

    Each built-in validator has its own error message predefined. If the validation fails, the predefined error message will be displayed. For example, the following validation:

    [EmailAddress]
    FEmail: string;
    

    If failed, it will generate the error message:

    Field FEmail is not a valid e-mail address
    

    You can customize such messages in two ways.

    DisplayName

    This is not a validator, but it's an attribute you can add to any class member to modify its name when it's used in validation error messages.

    [EmailAddress, DisplayName('e-mail')]
    FEmail: string;
    

    When used with the DisplayName attribute as above, the EmailAddress validator will now generate the following error message:

    Field e-mail is not a valid e-mail address
    

    One advantage of using DisplayName attribute is that the modified name will be used in all existing validations set for the class member.

    Custom error message

    If changing DisplayName is not enough, you can simply provide a full custom error message. Every built-in validator attribute can receive an additional parameter with the custom error message to be used:

    [DisplayName('e-mail')]
    [EmailAddress('You must provide a valid e-mail address for field "%0:s"')]
    FEmail: string;
    

    In case of a failed validation, the following error message will be generated:

    You must provide a valid e-mail address for field "e-mail"
    

    Note how the custom error message can be combined with the DisplayName attribute. The %0:s parameter is valid for all built-in validators and contain the member name. Other validators can have additional parameters, for example the Range validator provides the minimum and maximum allowed values in parameters %1:s and %2:s respectively.

    Handling Failed Validation

    If one or more validation fails, an exception of type EEntityValidationException is raised and the persistence operation will not complete.

    In most cases, you won't have to do anything special to handle it - the exception will propagate as any other Delphi exception: if you are running a desktop application, the default exception handler will show a message to the user. If you are running an application server, you should be already catching and logging the exceptions, so it won't be different in this case.

    The EEntityValidationException has some specific properties you can inspect in a try..except..on block to gather more details about the validation. The Entity property contains the entity instance which failed to validate. The Results property contains a list of IManagerValidationResult interfaces with specific information for each validation.

    Consider the following mapping and validations:

    type
      [Entity, Automapping]
      TCustomer = class
      strict private
        FId: Integer;
    
        [Required, MaxLength(20)]
        FName: string;
    
        [EmailAddress]
        FEmail: string;
    
        [DisplayName('class rate')]
        [Range(1, 10, 'Values must be %1:d up to %2:d for field %0:s')]
        FRate: Integer;
      public
        property Id: Integer read FId write FId;
        property Name: string read FName write FName;
        property Email: string read FEmail write FEmail;
        property Rate: Integer read FRate write FRate;
      end;
    

    If we try to save such entity with many wrong properties, many validations will fail:

    procedure SaveWrongCustomer;
    var
      Customer: TCustomer;
    begin
      Customer := TCustomer.Create;
      Manager.AddOwnership(Customer);
      Customer.Name := 'Too long name for customer';
      Customer.Email := 'foo';
      Manager.Save(Customer);
    end;
    

    You can use the following code to catch the validation exception and log detailed information:

    var
      ValidationResult: IManagerValidationResult;
      Error: IValidationError;
    begin
      try
        SaveWrongCustomer;
      except
        on E: EEntityValidationException do
        begin
          WriteLn(Format('Validation failed for entity %s:', [E.Entity.ClassName]));
          for ValidationResult in E.Results do
            for Error in ValidationResult.Errors do
              WriteLn('  ' + Error.ErrorMessage);
        end;
      end;
    end;
    

    Which will generate the following output:

    Validation failed for entity TCustomer:
      Field FName must have no more than 20 character(s)
      Field FEmail is not a valid e-mail address
      Values must be 1 up to 10 for field class rate
    

    Disabling Validations

    Data validation is enabled by default. If you have added validators to your mapping, then they will already be enforced when you try to save an entity.

    In case you want to disable validations (for example, to improve performance), you can set TObjectManager.ValidationsEnabled property to False:

      Manager.ValidationsEnabled := False;
      Manager.Save(Customer); // no validation performed
    

    Custom Validators

    In addition to the built-in validators, you can create your own custom validators, including attributes, that you can apply to your entities.

    First, create a new class implementing the IValidator interface:

    uses {...}, Aurelius.Validation.Interfaces;
    
    type
      TMyDataValidator = class(TInterfacedObject, IValidator)
      public
        function Validate(const Value: TValue; Context: IValidationContext): IValidationResult;
      end;
    

    The interface has a single method Validate that you need to implement, which receives the Value to be validated and must return an IValidationResult interface:

    function TMyDataValidator.Validate(const Value: TValue;
      Context: IValidationContext): IValidationResult;
    begin
      // Add your own logic to check if Value is valid
      if IsValid(Value) then
        Result := TValidationResult.Success
      else
        Result := TValidationResult.Failed(Format(ErrorMessage,
          [Context.DisplayName]))
    end;
    

    Then just create your new attribute inheriting from ValidationAttribute and override the GetValidator method:

      MyDataAttribute = class(ValidationAttribute)
      strict private
        FValidator: IValidator;
      public
        constructor Create; 
        function GetValidator: IValidator; override;
      end;
    
    {...}
    
    constructor MyDataAttribute.Create;
    begin
      inherited Create;
      FValidator := TMyDataValidator.Create;
    end;
    
    function MyDataAttribute.GetValidator: IValidator;
    begin
      Result := FValidator;
    end;
    

    After this, all you need is to add the attribute to all cla members you want MyData validation to be applied:

      [MyData]
      FMyProp: string;
    

    Manual Validation

    Validations are performed automatically before an entity is about to be persisted (if ValidationsEnabled is false).

    But if for any reason you want to validate an entity without persisting it, just call Validate method of the object manager:

    Manager.Validate(MyEntity);
    

    If any validation fails, an exception will be raised and you can handle it as usual.

    In This Article
    Back to top TMS Aurelius v5.11
    © 2002 - 2022 tmssoftware.com