Self Aware Objects – Performance Testing

The code for the self-aware objects uses reflection to check the values of properties, and see if they meet the validation requirements declared by the custom attributes.

If you’ve ever worked with reflection, you know it isn’t fast.  So I ran some performance tests this morning, to compare the validation routines using my SAO code and the exact same validation rules in a normal controller.

There’s good news and bad news.

The bad news is, yes, SAO validation is slower.  It takes about 31 times longer to validate an object – as the code currently is written.

However, the good news is that it’s still fast enough for almost every business application I’ve ever been involved with.

The exact results, to perform the object validation routine ten thousand times, are:

Normal validation: 0.0042 seconds

SAO validation: 0.131421 seconds

I don’t know what your volume of data input is, but I’ve personally only ever worked on one business application with requirements to potentially validate tens of thousands of objects per second.  Even in that situation, you will still get sub-second throughput.

The thing to keep in mind is the big picture.

Will using a method like this cause a noticeable degradation in your application’s performance?  It’s very unlikely.

Will using a method like this decrease your development time, produce more reliable code, and make maintenance easier (due to the clarity from easily-found business logic)?  I think it will.

Here’s the code I used for the performance tests – both the standard code and the SAO code.

STANDARD VALIDATION CODE

BaseObject.cs

using System.Collections.Generic;
using System.Xml.Serialization;

namespace Engine.Standard.Model
{
    public abstract class BaseObject
    {
        [XmlIgnore]
        public bool IsValid { get; private set; }

        [XmlIgnore]
        public List<string> ErrorMessages { get; private set; }

        protected BaseObject()
        {
            ErrorMessages = new List<string>();
        }

        internal void ClearErrorStatus()
        {
            IsValid = true;
            ErrorMessages.Clear();
        }

        internal void Invalidate(string errorMessage)
        {
            IsValid = false;
            ErrorMessages.Add(errorMessage);
        }
    }
}

Customer.cs

using System;

namespace Engine.Standard.Model
{
    public class Customer : BaseObject
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string City { get; set; }
        public string StateCode { get; set; }
        public string ZIPCode { get; set; }

        public Customer()
        {
            ID = Guid.NewGuid();
        }
    }
}

CustomerController.cs

using System;
using System.Linq;
using System.Text.RegularExpressions;

using Engine.Standard.Model;

namespace Engine.Standard.Controllers
{
    public static class CustomerController
    {
        public static void Validate(Customer customer)
        {
            customer.ClearErrorStatus();

            if(string.IsNullOrWhiteSpace(customer.Name))
            {
                customer.Invalidate("Name cannot be empty");
            }

            if(customer.Address == null || customer.Address.Length < 5)
            {
                customer.Invalidate("Address cannot be less than 5 characters long");
            }
            else if(customer.Address.Length > 100)
            {
                customer.Invalidate("Address cannot be more than 100 characters long");
            }

            if(customer.City == null || customer.City.Length < 2)
            {
                customer.Invalidate("City cannot be less than 2 characters long");
            }
            else if(customer.City.Length > 100)
            {
                customer.Invalidate("City cannot be more than 100 characters long");
            }

            if(customer.StateCode == null || customer.StateCode.Length != 2)
            {
                customer.Invalidate("State must be two characters");
            }

            if(customer.StateCode != null && !customer.StateCode.All(Char.IsLetter))
            {
                customer.Invalidate("State can only contain letters");
            }

            if(customer.ZIPCode == null || !new Regex(@"^(\d{5}-\d{4}|\d{5})$").IsMatch(customer.ZIPCode))
            {
                customer.Invalidate("ZIP Code must be formatted like '99999' or '99999-9999'");
            }
        }
    }
}

SAO VALIDATION CODE

SAObject.cs

using System.Collections.Generic;
using System.Xml.Serialization;

namespace SAO.ADTs
{
    public abstract class SAObject
    {
        [XmlIgnore]
        public bool IsValid { get; private set; }

        [XmlIgnore]
        public List<string> ErrorMessages { get; private set; }

        protected SAObject()
        {
            ErrorMessages = new List<string>();
        }

        internal void ClearErrorStatus()
        {
            IsValid = true;
            ErrorMessages.Clear();
        }

        internal void Invalidate(string errorMessage)
        {
            IsValid = false;
            ErrorMessages.Add(errorMessage);
        }
    }
}

Customer.cs

using System;

using SAO.ADTs;
using SAO.Attributes.Property;

namespace Engine.Model
{
    public class Customer : SAObject
    {
        public Guid ID { get; set; }

        [CannotBeEmptyString("Name cannot be empty")]
        public string Name { get; set; }

        [MinimumLength(5, "Address cannot be less than 5 characters long")]
        [MaximumLength(100, "Address cannot be more than 100 characters long")]
        public string Address { get; set; }

        [MinimumLength(2, "City cannot be less than 2 characters long")]
        [MaximumLength(100, "City cannot be more than 100 characters long")]
        public string City { get; set; }

        [ExactLengthString(2, "State must be two characters")]
        [ContainsOnlyLettersAttribute("State can only contain letters")]
        public string StateCode { get; set; }

        [MatchesRegEx(@"^(\d{5}-\d{4}|\d{5})$", "ZIP Code must be formatted like '99999' or '99999-9999'")]
        public string ZIPCode { get; set; }

        public Customer()
        {
            ID = Guid.NewGuid();
        }
    }
}

SAObjectValidator.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

using SAO.ADTs;
using SAO.Attributes;
using SAO.Attributes.Property;

namespace SAO
{
    public static class SAObjectValidator
    {
        private static readonly List<SAOAttributeInfo> _saoAttributes = new List<SAOAttributeInfo>();

        public static void Validate(SAObject obj)
        {
            string className = obj.GetType().ToString();

            // Cache SAO validation rules, if we haven't already done so, for this class. 
            // This saves time by not repeating the same reflection code each time we valiadate an object of a type of class we've already seen.
            if(!_saoAttributes.Exists(x => x.ClassName == className))
            {
                PopulateSAOAttributesListForClass(className, obj);
            }

            obj.ClearErrorStatus();

            foreach(SAOAttributeInfo saoRule in _saoAttributes.Where(x => x.ClassName == className))
            {
                var value = obj.GetType().GetProperty(saoRule.PropertyName).GetValue(obj, null);

                switch(saoRule.ValidationAttributeName)
                {
                    case "CannotBeNullAttribute":
                        EvaluateCannotBeNullAttribute(obj, value, (CannotBeNullAttribute)saoRule.ValidationAttribute);
                        break;
                    case "CannotBeEmptyStringAttribute":
                        EvaluateCannotBeEmptyStringAttribute(obj, value, (CannotBeEmptyStringAttribute)saoRule.ValidationAttribute);
                        break;
                    case "ContainsOnlyLettersAttribute":
                        EvaluateContainsOnlyLettersAttribute(obj, value, (ContainsOnlyLettersAttribute)saoRule.ValidationAttribute);
                        break;
                    case "ExactLengthStringAttribute":
                        EvaluateExactLengthStringAttribute(obj, value, (ExactLengthStringAttribute)saoRule.ValidationAttribute);
                        break;
                    case "MatchesRegExAttribute":
                        EvaluateMatchesRegExAttribute(obj, value, (MatchesRegExAttribute)saoRule.ValidationAttribute);
                        break;
                    case "MinimumLengthAttribute":
                        EvaluateMinimumLengthAttribute(obj, value, (MinimumLengthAttribute)saoRule.ValidationAttribute);
                        break;
                    case "MaximumLengthAttribute":
                        EvaluateMaximumLengthAttribute(obj, value, (MaximumLengthAttribute)saoRule.ValidationAttribute);
                        break;
                }
            }
        }

        #region SAOAttribute validation methods

        private static void EvaluateCannotBeNullAttribute(SAObject obj, object propertyValue, CannotBeNullAttribute attribute)
        {
            if(propertyValue == null)
            {
                obj.Invalidate(attribute.ErrorMessage);
                return;
            }
        }

        private static void EvaluateCannotBeEmptyStringAttribute(SAObject obj, object propertyValue, CannotBeEmptyStringAttribute attribute)
        {
            if(propertyValue == null)
            {
                obj.Invalidate(attribute.ErrorMessage);
                return;
            }

            if(propertyValue is String)
            {
                if(string.IsNullOrWhiteSpace(propertyValue.ToString()))
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        private static void EvaluateContainsOnlyLettersAttribute(SAObject obj, object propertyValue, ContainsOnlyLettersAttribute attribute)
        {
            if(propertyValue == null)
            {
                return;
            }

            if(propertyValue is String)
            {
                if(!propertyValue.ToString().All(Char.IsLetter))
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        private static void EvaluateExactLengthStringAttribute(SAObject obj, object propertyValue, ExactLengthStringAttribute attribute)
        {
            if(propertyValue == null)
            {
                obj.Invalidate(attribute.ErrorMessage);
                return;
            }

            if(propertyValue is String)
            {
                if(propertyValue.ToString().Length != attribute.Length)
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        private static void EvaluateMatchesRegExAttribute(SAObject obj, object propertyValue, MatchesRegExAttribute attribute)
        {
            if(propertyValue == null)
            {
                obj.Invalidate(attribute.ErrorMessage);
                return;
            }

            if(propertyValue is String)
            {
                if(!new Regex(attribute.RegEx).IsMatch(propertyValue.ToString()))
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        private static void EvaluateMinimumLengthAttribute(SAObject obj, object propertyValue, MinimumLengthAttribute attribute)
        {
            if(propertyValue == null)
            {
                if(attribute.MinimumLength > 0)
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }

                return;
            }

            if(propertyValue is String)
            {
                if(propertyValue.ToString().Length < attribute.MinimumLength)
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        private static void EvaluateMaximumLengthAttribute(SAObject obj, object propertyValue, MaximumLengthAttribute attribute)
        {
            if(propertyValue == null)
            {
                return;
            }

            if(propertyValue is String)
            {
                if(propertyValue.ToString().Length > attribute.MaximumLength)
                {
                    obj.Invalidate(attribute.ErrorMessage);
                }
            }
        }

        #endregion

        # region Internal methods

        private static void PopulateSAOAttributesListForClass(string className, SAObject obj)
        {
            foreach(PropertyInfo property in obj.GetType().GetProperties())
            {
                foreach(SAOAttribute attribute in property.GetCustomAttributes(typeof(SAOAttribute), true))
                {
                    if(!_saoAttributes.Exists(x => (x.ClassName == className) && (x.PropertyName == property.Name) && (x.ValidationAttribute == attribute)))
                    {
                        SAOAttributeInfo rule = new SAOAttributeInfo();

                        rule.ClassName = className;
                        rule.PropertyName = property.Name;
                        rule.ValidationAttribute = attribute;
                        rule.ValidationAttributeName = attribute.GetType().Name;

                        _saoAttributes.Add(rule);
                    }
                }
            }
        }

        #endregion
    }
}

SAOAttributeInfo.cs (used to cache SAO custom attribute information in the validator)

namespace SAO.Attributes
{
    internal class SAOAttributeInfo
    {
        public string ClassName { get; set; }
        public string PropertyName { get; set; }
        public SAOAttribute ValidationAttribute { get; set; }
        public string ValidationAttributeName { get; set; }
    }
}

Next Step

I have a couple other ideas I want to try out with building self-aware objects.  Before I go much further with these custom attributes, I want to see how the other ideas look.

I’m searching for programming techniques to improve code quality, shorten development time, and make maintenance work easier.

After all, the ultimate goal of programming is to solve business problems.  The faster we can provide a reliable, working solution to the people who hire us, and the less time we spend maintaining those solutions, the happier they’ll be.

One thought on “Self Aware Objects – Performance Testing

  1. I just made a change to the SAObjectValidator.cs Validate() method.

    Instead of having a switch statement that calls a private method to handle the logic for each custom attribute, I moved the validation logic into a Validate() method of each custom attribute classes. Now, SAObjectValidator.Validate() just passes the object and property into the custom attribute’s Validate() method and lets it do its magic.

    That made the code much cleaner, put the validation logic where you’d most expect to find it, and improved the performance by around eight percent.

Leave a Reply

Your email address will not be published. Required fields are marked *