Generated code - Validation per field or per entity
Preface
Business logic can be implemented in a variety of ways. Roughly you can divide the business rules in three groups:
- business logic rules which deal with just a single field of one entity, for example a value for an ID field may not be less than 0. In this section,
this is called Field validation.
- business logic rules which work with multiple fields of a single entity, for example the shipping date of an order can't be earlier in time
than the orderdate itself. In this section, this is called Entity validation.
- business logic rules which deal with multiple entities, for example a customer is a gold customer if the customer has more than n orders in
the last m months
For the first two groups, LLBLGen Pro offers a rich validation mechanism which you can fill in using two different ways: implement the business rules inside the
entity class or implement the business rules inside a
Validator class. This section describes both ways, starting by describing how to add validation
logic directly to your entity classes.
LLBLGen Pro's entity classes also have built-in validation to check whether a value possibly matches the field's characteristics. For example if an entity field is of type string and has a length of 10, and the field is set to a string with length 20, the built-in validation logic will throw an exception. This is further discussed in detail in the paragraph below.
Note:
|
For people migrating from LLBLGen Pro v1.0.2005.1 or earlier, it's key to understand that in V3
(which was introduced in v2.0) there's
just 1 interface for both field and entity validation: IValidator, there are no longer two interfaces used. The reason for this is that by using 1
interface it simplifies writing validation logic and also limits the amount of code to write. Also be sure to read the Migrating your code section
to learn more about breaking changes related to validator classes and how to migrate your validation code to the new system. |
Built-in field validation logic
LLBLGen Pro's entities have validation logic built-in for field values. This logic is executed every time an entity field's value is set, either through the properties or through
entity.SetNewFieldValue(). The validation logic is meant to prevent database errors when the entity is persisted so it rejects every value which doesn't match the field's aspects: for string based fields and byte-array based fields, the length of the value is checked compared to the length set for the field. For numeric fields the precision is checked and for fields with a fraction like Decimal, Single/float and Double based fields the scale is also checked.
The built-in validation logic also checks if a field which isn't nullable is set to a null value. When an overflow is detected, an exception is thrown, the IDataErrorInfo error message for the field is set and the value is rejected.
Bypassing built-in validation logic
It might be that you don't want to use the built-in validation logic and want to use your own validation logic instead. You can make LLBLGen Pro bypass the built-in validation logic using the static (Shared) property on EntityBase (if you're using SeflServicing) or EntityBase2 (if you're using Adapter) called
BuildInValidationBypassMode. Set that property to any of the
BuildInValidationBypass values, which are
NoBypass (value 0, default),
AlwaysBypass (value 1) or
BypassWhenValidatorPresent (value 2). If you choose to use AlwaysBypass and there's no validator present no validation on the field will be performed. You can also set this property through the application's config file by adding this line to the
appSettings section of your application's config file:
<add key="buildInValidationBypassMode" value="0"/>
Where value is one of the values 0, 1 or 2 for resp. NoBypass, AlwaysBypass or BypassWhenValidatorPresent.
Defining the Scale overflow correction action to use
If you opt for the built-in validation logic and LLBLGen Pro detects a scale overflow for a field, it will truncate the fractional part of the value specified to the Scale size set for the field, e.g. if the scale is set to 2 for a given field and the field is set to the value 10.1234, the value the field is set to will be 10.12. It might be this is not what you want: perhaps you want to round the value or throw an exception. You can define the scale overflow correction action by setting the
ScaleOverflowCorrectionActionToUse property on EntityBase (if you're using SelfServicing) or EntityBase2 (if you're using Adapter) to any of the
ScaleOverflowCorrectionAction values, which are
None (value 0, an exception will be thrown),
Truncate (value 1, the fraction will be truncated to match the scale value) or
Round (value 2, rounds the fraction to a value which fits the scale and rounds values using Math.Round(value, scale). You can also set this value using the config file of your application by adding the following line to the
appSettings section of your application's config file:
<add key="scaleOverflowCorrectionActionToUse" value="0"/>
Where value is one of the values 0, 1 or 2 for resp. None, Truncate or Round.
Validation logic inside Entity classes
The first of the two ways to add validation logic to the entity framework is by adding the validation logic directly to the entity classes.
The recommended way to do this is by defining partial classes of the generated entity classes.
Validation logic is added to the entity classes by overriding pre-defined methods of the entity base class. The LLBLGen Pro runtime will call during
the entity life-cycle at pre-defined places a variety of methods on the entity, namely the method matching the current state of the entity and the action
which is about to be taken on the entity. By overriding these pre-defined methods, the code you add to the override will then be executed when the method is
called by the runtime. By default the pre-defined methods are virtual / Overridable methods which call into an existing
validator object, if the entity has a
validator object set, otherwise they're no-ops.
Field validation
Every entity has a protected virtual (Overridable in VB.NET) method which is called by the routine which checks if a field should be set to a new value:
OnValidateFieldValue. This method is called by the entity's SetValue routine after it has performed preliminairy checks on the new value, for example
to see if a provided string value isn't too long for the set length of the field and also after
Authorization has been granted by authorization logic. If the routine returns true, the value is an acceptable value, otherwise
the value should be rejected. OnValidateFieldValue() accepts the fieldindex of the field which value is about to be set and the new value. As this method is
implemented on the entity, you can perform whatever check you need, as long as you return true if the value should be accepted or false if not. If false is
returned, the value is simply rejected and the field isn't set to a new value. The default implementation checks if there's a Validator object set in the entity and if so, it simply calls
ValidateFieldValue on that Validator object to perform the validation. If no Validator is set, the default implementation simply returns true.
An override of OnValidateFieldValue is a good candidate for the situation where you want to set an error message for IDataErrorInfo if the value isn't correct. In
that case see the
IDataErrorInfo section below.
Note: |
Be aware that the value passed to OnValidateFieldValue could be null / Nothing if the field to validate is a nullable field. |
Example
// C# in partial class or user code region of OrderEntity
protected override bool OnValidateFieldValue( int fieldIndex, object value )
{
bool toReturn = true;
switch((OrderFieldIndex)fieldIndex)
{
case OrderFieldIndex.OrderId:
// id is valid if the value is > 0
toReturn = ((int)value > 0);
break;
default:
toReturn = true;
break;
}
return toReturn;
}
' VB.NET in partial class or user code region of OrderEntity
Protected Overrides Function OnValidateFieldValue(fieldIndex As Integer, value As Object) As Boolean
Dim toReturn As Boolean = True
Select Case CType(fieldIndex, OrderFieldIndex)
Case OrderFieldIndex.OrderId
' id is valid if the value is > 0
toReturn = (CInt(value) > 0)
Case Else
toReturn = True
End Select
Return toReturn
End Function
This field validation routine added to the OrderEntity class through either a partial class or by adding this code to the user code region for custom code
of the OrderEntity class (at the bottom of the generated class), checks if the value specified for OrderId is > 0. All values for other fields are ignored
by this validation logic and will simply bypass this routine by returning true, which means the value set is always accepted.
Entity validation
Entities can be validated in various contexts, for example when they're loaded from the database, before or after they're saved, before they're deleted etc.
For each context a special method is created in the entity base class as protected virtual (Overridable). The methods defined are below with the description
when they're called. It's up to you to override one or more of these methods in a partial class of an entity or in a user code region (or include template). You
don't have to override a method if you don't need validation for the entity for that context.
If validation fails, but it's not major, don't throw an exception but simply set the IDataErrorInfo error text on the entity. If the entity isn't
valid, throw an
ORMEntityValidationException. The LLBLGen Pro code simply calls OnValidateEntity
contextdescription on the entity and moves on
if no exception is thrown. There's also a general method:
ValidateEntity(), which isn't used internally by the LLBLGen Pro runtime, but can
be used by your own code to validate an entity in any context. The method
Validate has been kept, though is now marked Obsolete. People who use
Validate should call ValidateEntity() from now on. Validate was the entity validate method in v1.0.2005.1 and earlier of LLBLGen Pro.
- OnValidateEntityAfterLoad. This method is called right after the entity has been filled with data and is ready to be used. Use this
method to be sure a loaded entity contains the data you expect it to have. If the fetch method specifies a prefetch path, the entity is validated
before the prefetch path has been loaded as well. All AfterLoad validation is performed right after the entity's own fetch routine has been
completed and the state has been set. For bulk fetches of entities into
collections, the entity is validated right before the entity will be added to the collection.
- OnValidateEntityBeforeSave. This method is called right before the save sequence for the entity will start.
- OnValidateEntityAfterSave. This method is called when the entity's save action has been completed and a refetch (if applicable) has taken place.
If a refetch action takes place (Adapter) the AfterLoad validation is performed before this event. In SelfServicing, refetching of an entity occurs
when a property is accessed after a save, so in SelfServicing AfterLoad will take place after AfterSave validation.
- OnValidateEntityBeforeDelete. This method is called when the entity's delete action is about to be executed.
Validation logic inside validator classes
It can be that you don't want to add validation logic to the generated classes but want to store this validation logic in a separate class, a
Validator
class and of which you can plug an instance of into an entity object. LLBLGen Pro supports this through
Validator classes. Validator classes implements the interface
IValidator. To make life easier for you, we provide a basic implementation of this interface in the form of the base class
ValidatorBase. Please consult the LLBLGen Pro reference manual for details about
this base class and the interface.
In v1.0.2005.1 and earlier, LLBLGen Pro generated for each entity a validator class, called
entitynameValidator. This isn't the case anymore. You can still
generate validator classes per entity however, you just have to enable the SD.Tasks.Generic.ValidatorClassesGenerator
task in the preset of your choice at Tab 3 of the generator configuration
dialog.
Setting an entity's Validator
If you've decided to use Validator classes to place your validation logic in, you'll be confronted with the question: how to set the right Validator in every entity object? You've three options:
- Setting the Validator property of an entity object manually. This is straight forward, but error prone: if you forget to set a validator, validation using this validator object isn't performed. Also, entities fetched in bulk into a collection are created using the factories so you have to alter these as well. You could opt for overriding OnInitializing in an entity to add the creation of the Validator class.
- By overriding the Entity method CreateValidator. This is a protected virtual (Protected Overridable) method which by default returns null / Nothing. You can override this method in a partial class or user code region of the Entity class to create the Validator to use for the entity. The LLBLGen Pro runtime framework will take care of calling the method. One way to create an override for most entities is by using a template. Please see the LLBLGen Pro SDK documentation for details about how to write templates to generate additional code into entities and in separate files. Also please see Adding Adding your own code to the generated classes for details.
- By using Dependency Injection. Using the Dependency Injection mechanism build into LLBLGen Pro, the defined Validators are injected into the entity objects for you. This option is the least amount of work.
Entity classes don't get a validator instance by default, even if you generate them through LLBLGen Pro.
As mentioned above, the methods implemented on the entity, like OnValidateEntityAfterLoad, call into an existing validator object. The following list informs
you which entity method is calling which validator method and thus which method you should override in your own validator class to validate an entity in a
given context.
- ValidateFieldValue. Called by entity.OnValidateFieldValue.
- ValidateEntityAfterLoad. Called by entity.OnValidateEntityAfterLoad.
- ValidateEntityBeforeSave. Called by entity.OnValidateEntityBeforeSave.
- ValidateEntityAfterSave. Called by entity.OnValidateEntityAfterSave.
- ValidateEntityBeforeDelete. Called by entity.OnValidateEntityBeforeDelete.
- ValidateEntity. Called by entity.ValidateEntity
Field validation
The example above in the field validation section for code inside entity classes can also be implemented by adding the code to a validator class instead.
This lead to the following code.
Note: |
Be aware that the value passed to OnValidateFieldValue could be null / Nothing if the field to validate is a nullable field. |
Example
// C# in validator class deriving from ValidatorBase.
protected override bool ValidateFieldValue( IEntityCore involvedEntity, int fieldIndex, object value )
{
bool toReturn = true;
switch((OrderFieldIndex)fieldIndex)
{
case OrderFieldIndex.OrderId:
// id is valid if the value is > 0
toReturn = ((int)value > 0);
break;
default:
toReturn = true;
break;
}
return toReturn;
}
' VB.NET in validator class deriving from ValidatorBase.
Protected Overrides Function ValidateFieldValue( involvedEntity As IEntityCore, fieldIndex As Integer, value As Object) As Boolean
Dim toReturn As Boolean = True
Select Case CType(fieldIndex, OrderFieldIndex)
Case OrderFieldIndex.OrderId
' id is valid if the value is > 0
toReturn = (CInt(value) > 0)
Case Else
toReturn = True
End Select
Return toReturn
End Function
Entity validation
The following example illustrates a CustomerValidator class which checks if the BillingAddress or VisitingAddress references to an AddressEntity are indeed
set to a value and throws an ORMEntityValidationException if this isn't the case. The exception makes sure any transaction currently in progress will be terminated. The validator is injected into all customer entities using
Dependency Injection.
// C#
[DependencyInjectionInfo(typeof(CustomerEntity), "Validator",
ContextType = DependencyInjectionContextType.Singleton)]
public class CustomerValidator : ValidatorBase
{
/// <summary>
/// Method to validate the containing entity right before the save sequence for the entity will start. LLBLGen Pro will call this method right after the
/// containing entity is selected from the save queue to be saved.
/// </summary>
/// <param name="involvedEntity">The involved entity.</param>
public override void ValidateEntityBeforeSave( IEntityCore involvedEntity )
{
CustomerEntity toValidate = (CustomerEntity)involvedEntity;
if( toValidate.BillingAddress == null )
{
throw new ORMEntityValidationException( "BillingAddress can't be null", toValidate );
}
if( toValidate.VisitingAddress == null )
{
throw new ORMEntityValidationException( "VisitingAddress can't be null", toValidate );
}
base.ValidateEntityBeforeSave( involvedEntity );
}
}
' VB.NET
<DependencyInjectionInfo(GetType(CustomerEntity), "Validator", _
ContextType := DependencyInjectionContextType.Singleton)> _
Public Class CustomerValidator
Inherits ValidatorBase
''' <summary>
''' Method to validate the containing entity right before the save sequence for the entity will start. LLBLGen Pro will call this method right after the
''' containing entity is selected from the save queue to be saved.
''' </summary>
''' <param name="involvedEntity">The involved entity.</param>
Public Overrides Sub ValidateEntityBeforeSave(involvedEntity As IEntityCore)
Dim toValidate As CustomerEntity = CType(involvedEntity, CustomerEntity)
If toValidate.BillingAddress Is Nothing Then
Throw New ORMEntityValidationException( "BillingAddress can't be null", toValidate )
End If
If toValidate.VisitingAddress Is Nothing Then
Throw New ORMEntityValidationException( "VisitingAddress can't be null", toValidate )
End If
MyBase.ValidateEntityBeforeSave( involvedEntity )
End Sub
End Class
IDataErrorInfo implementation
The .NET interface IDataErrorInfo is now implemented on EntityBase and EntityBase2. Two methods have been added to the entities:
SetEntityError and
SetEntityFieldError, which allows external code to set the error of a field and/or entity. If
append is set to true when calling SetEntityFieldError,
the error message is appended to an existing message for that field using a semi-colon as separator.
Entity field validation, which is triggered by SetNewFieldValue(), sets the field error if an exception occurs or when the custom field validator fails.
The error message is appended to an existing message.