1

I have two classes:

[HasSelfValidation]
class Country : DomainObject
{
    [NotNullValidator]
    [StringLengthValidator(2, 10)]
    public string Name { get; set; }

    [ObjectCollectionValidator(typeof(Region))]
    public List<Region> Regions { get; set; }
}

and

[HasSelfValidation]
class Region : DomainObject
{
    [NotNullValidator]
    [ObjectValidator]
    public Country Country { get; set; }

    [NotNullValidator]
    [StringLengthValidator(2, 20)]
    public string Name { get; set; }
}

Where DomainObject has method:

public virtual ValidationResults Validate()
{
    Validator validator = 
        ValidationFactory.CreateValidator(this.GetType());
    ValidationResults results = new ValidationResults();
    validator.Validate(this, results);
    return results;
}

I'm using Microsoft Enterprise Library 4.1 - October 2008/.NET 3.5 SP1/Vista.

If I call Validate for a newly created Country object withn null as list of regions, I get a StackOverflow exception. If I remove the [ObjectCollectionValidator(typeof(Region))] for the Country.Regions property, everything works fine. I guess the link Country - Region - Country is the cause of failure. However, I do not want to remove the validation of the Regions collection; neither removal of the [ObjectValidator] from region is an option for me. Is there anything I can do to maintain all these validations attributes, w/o StackOverflow exception?

Thanks,

Lucian

Steven
  • 166,672
  • 24
  • 332
  • 435
lmsasu
  • 7,459
  • 18
  • 79
  • 113

3 Answers3

2

I ran into this same issue just the other day, and was able to get by it by implementing a custom validator class that functions much in the same way as ObjectValidator, except that it delays the evaluation of the property being evaluated until the actual DoValidate method, and doesn't keep building up validators if the property is null.

using System;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Validation.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Validation.Properties;

namespace Microsoft.Practices.EnterpriseLibrary.Validation.Validators
{
    /// <summary>
    /// Performs validation on objects by applying the validation rules specified for a supplied type at RUNTIME.
    /// This validator can be used to get past StackOverflowExceptions that can be thrown as a result of the design
    /// of the ObjectValidator attribute
    /// </summary>
    /// <seealso cref="ValidationFactory"/>
    public class RuntimeObjectValidator : Validator
    {
        private Type targetType;
        private string targetRuleset;

        /// <summary>
        /// <para>Initializes a new instance of the <see cref="RuntimeObjectValidator"/> for a target type.</para>
        /// </summary>
        /// <param name="targetType">The target type</param>
        /// <remarks>
        /// The default ruleset for <paramref name="targetType"/> will be used.
        /// </remarks>
        /// <exception cref="ArgumentNullException">when <paramref name="targetType"/> is <see langword="null"/>.</exception>
        public RuntimeObjectValidator(Type targetType)
            : this(targetType, string.Empty)
        { }

        /// <summary>
        /// <para>Initializes a new instance of the <see cref="RuntimeObjectValidator"/> for a target type
        /// using the supplied ruleset.</para>
        /// </summary>
        /// <param name="targetType">The target type</param>
        /// <param name="targetRuleset">The ruleset to use.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="targetType"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentNullException">when <paramref name="targetRuleset"/> is <see langword="null"/>.</exception>
        public RuntimeObjectValidator(Type targetType, string targetRuleset)
            : base(null, null)
        {
            if (targetType == null)
            {
                throw new ArgumentNullException("targetType");
            }
            if (targetRuleset == null)
            {
                throw new ArgumentNullException("targetRuleset");
            }

            this.targetType = targetType;
            this.targetRuleset = targetRuleset;
        }

        /// <summary>
        /// Validates by applying the validation rules for the target type specified for the receiver.
        /// </summary>
        /// <param name="objectToValidate">The object to validate.</param>
        /// <param name="currentTarget">The object on the behalf of which the validation is performed.</param>
        /// <param name="key">The key that identifies the source of <paramref name="objectToValidate"/>.</param>
        /// <param name="validationResults">The validation results to which the outcome of the validation should be stored.</param>
        /// <remarks>
        /// If <paramref name="objectToValidate"/> is <see langword="null"/> validation is ignored.
        /// <para/>
        /// A referece to an instance of a type not compatible with the configured target type
        /// causes a validation failure.
        /// </remarks>
        protected internal override void DoValidate(object objectToValidate,
            object currentTarget,
            string key,
            ValidationResults validationResults)
        {
            if (objectToValidate != null)
            {
                if (this.targetType.IsAssignableFrom(objectToValidate.GetType()))
                {
                    validationResults.AddAllResults(
                        ValidationFactory.CreateValidator(objectToValidate.GetType()).Validate(objectToValidate));
                }
                else
                {
                    // unlikely
                    this.LogValidationResult(validationResults, Resources.ObjectValidatorInvalidTargetType, currentTarget, key);
                }
            }
        }

        /// <summary>
        /// Gets the message template to use when logging results no message is supplied.
        /// </summary>
        protected override string DefaultMessageTemplate
        {
            get { return null; }
        }

        #region test only properties

        internal Type TargetType
        {
            get { return this.targetType; }
        }

        internal string TargetRuleset
        {
            get { return this.targetRuleset; }
        }

        #endregion
    }
}

And of course, you need to also create a RuntimeObjectValidatorAttribute class, so that you can do this:

public class AClassThatReferencesItself
    {
        private AClassThatReferencesItself _other;

        private string myString;

        [NotNullValidator]
        public string MyString
        {
            get { return myString; }
            set { myString = value; }
        }


        [RuntimeObjectValidator]
        [NotNullValidator]
        public AClassThatReferencesItself Other
        {
            get { return _other; }
            set { _other = value; }
        }

    }
Craig Vermeer
  • 1,987
  • 19
  • 29
1

The trick is actually to not to use any ObjectValidator and ObjectCollectionValidator attributes. You can validate all object by their self. This is not always workable, but works especially well in the context of O/RM scenario's, where the O/RM framework knowns which entities are new or have changed.

Look for instance at this example, where the validation is triggered just before the changes are submitted to the database using Entity Framework:

public partial class NorthwindEntities
{
    partial void OnContextCreated()
    {
        // Adding validation support when saving.
        this.SavingChanges += (sender, e) =>
        {
            // Throws an exception when invalid.
            EntityValidator.Validate(
                this.GetChangedEntities());
        }
    }

    private IEnumerable<object> GetChangedEntities()
    {
        const EntityState AddedAndModified =
            EntityState.Added | EntityState.Modified;

        var entries = this.ObjectStateManager
            .GetObjectStateEntries(AddedAndModified);

        return
            from entry in entries
            where entry.Entity != null
            select entry.Entity;
    }
}

The code hooks onto the ObjectContext's SavingChanges event.

The EntityValidator is a custom class that allows validating a group of objects using Validation Application Block. When the validation fails, it throws a custom ValidationException that wraps the ValidationResults collection.

static class EntityValidator
{
    public static void Validate(IEnumerable<object> entities)
    {
        ValidationResults[] invalidResults = (
            from entity in entities
            let type = entity.GetType()
            let validator = ValidationFactory.CreateValidator(type)
            let results = validator.Validate(entity)
            where !results.IsValid
            select results).ToArray();

        // Throw an exception when there are invalid results.
        if (invalidResults.Length > 0)
        {
            throw new ValidationException(invalidResults);
        }
    }
}

More info here.

I hope this helps.

Steven
  • 166,672
  • 24
  • 332
  • 435
1

This bug is in issue tracker of the EntLib on codeplex. And I'm not sure that it can be easily fixed. And I didn't find any good workaround for it :(.

zihotki
  • 5,201
  • 22
  • 26