Click here to Skip to main content
15,896,606 members
Articles / Programming Languages / C#

Validation Across Class Hierarchies and Interface Implementations

Rate me:
Please Sign up or sign in to vote.
4.00/5 (4 votes)
12 May 2008LGPL33 min read 29K   100   25  
Dependency injection of validation rules and their application across class hierarchies and interface implementations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Remoting;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using LinFu.DynamicProxy;
using ValidationRules;

namespace SampleTest {

    public class ProxyManager : IInterceptor, IBrokenRuleConsumer, IDataErrorInfo,
                                INotifyPropertyChanged, INotifyPropertyChanging {

        public event PropertyChangedEventHandler PropertyChanged;

        public event PropertyChangingEventHandler PropertyChanging;

        // The object been proxied
        private readonly object target;

        // The proxy to which calls to virtual methods defined in target.GetType() will be forwarded
        private readonly object proxy;

        // Maintains the collection of broken rules
        // Key := property name 
        // Value := the broken rule
        private Dictionary<string, IBrokenRule> brokenRules = new Dictionary<string, IBrokenRule>();

        private ProxyManager(object target, IProxy proxy) {

            this.target = target;

            this.proxy = proxy;

            // the proxy will forward virtual methods defined in target.GetType() to this instance
            // of ProxyManager which will route the messages accordingly
            proxy.Interceptor = this;
        }

        public static T  Create<T> () where T : new() {

            // create an instance of the requested type
            var target = Activator.CreateInstance<T>();

            // create the proxy and specify the interfaces it should implement
            var proxy = typeof(T).CreateProxy(new Type[]{typeof(IBrokenRuleConsumer), typeof(IDataErrorInfo),
                                                         typeof(INotifyPropertyChanged),
                                                         typeof(INotifyPropertyChanging)});

            var proxyManager = new ProxyManager(target, proxy);

            return (T)proxy;
        }

        IEnumerable<IBrokenRule> IBrokenRuleConsumer.BrokenRules {
            get {
                return brokenRules.Select(b => b.Value);
            }
        }

        void IBrokenRuleConsumer.EnforceConstraints() {

            EnforceConstraints();
        }

        string IDataErrorInfo.Error {
            get {

                if (brokenRules.Count == 0) {
                    return string.Empty;
                }

                // Concantenate and Delimit with a new line
                // all the error messages in all the broken rules
                return string.Join(Environment.NewLine,
                                    brokenRules.Select(b => b.Key.Substring(4) + " " + b.Value.ErrorMessage)
                                               .ToArray());
            }
        }

        string IDataErrorInfo.this[string property] {
            get {

                // Concantenate and Delimit with a new line
                // all the error messages in all the broken rules defined for the property
                var error = string.Join(Environment.NewLine,
                                        brokenRules.Where(b => b.Key.StartsWith("set_" + property))
                                                   .Select(c => c.Value.ErrorMessage).ToArray());


                return error;
            }
        }

        object IInterceptor.Intercept(InvocationInfo info) {

            var method = info.TargetMethod;

            var declaringType = method.DeclaringType;

            // route methods defined in the interfaces below to this insance of ProxyManager
            if (declaringType == typeof(IBrokenRuleConsumer) || declaringType == typeof(IDataErrorInfo) ||
                declaringType == typeof(INotifyPropertyChanged) ||
                declaringType == typeof(INotifyPropertyChanging)) {
                switch (method.Name) {
                    case "get_BrokenRules":
                        return (this as IBrokenRuleConsumer).BrokenRules;
                    case "get_Error":
                        return (this as IDataErrorInfo).Error;
                    case "get_Item":
                        return (this as IDataErrorInfo)[info.Arguments[0] as string];
                    case "add_PropertyChanged":
                        this.PropertyChanged += (PropertyChangedEventHandler)info.Arguments[0];
                        return null;
                    case "remove_PropertyChanged":
                        this.PropertyChanged -= (PropertyChangedEventHandler)info.Arguments[0];
                        return null;
                    case "add_PropertyChanging":
                        this.PropertyChanging += (PropertyChangingEventHandler)info.Arguments[0];
                        return null;
                    case "remove_PropertyChanging":
                        this.PropertyChanging -= (PropertyChangingEventHandler)info.Arguments[0];
                        return null;
                    case "EnforceConstraints":
                        EnforceConstraints();
                        return null;
                    default:
                        throw new InvalidOperationException();
                }
            }

            NotifyPropertyChanging(info);

            // route methods defined in target.GetType() to target
            var value = method.Invoke(target, info.Arguments);

            ApplyRules(info, method);

            NotifyPropertyChanged(info);

            return value;
        }

        private void EnforceConstraints() {

            foreach (var property in target.GetType().GetProperties().Where(p => p.CanRead && p.CanWrite)) {
                var method = property.GetSetMethod();
                ApplyRules(new InvocationInfo(proxy, property.GetSetMethod(), null, null,
                                                   new[] { property.GetValue(target, null) }), method);
            }
        }

        private void ApplyRules(InvocationInfo info, MethodInfo method) {

            foreach (var rule in RuleManager.ApplyRules(proxy, info)) {

                var key = method.Name + rule.Value.Name;

                if (rule.Key == true) {
                    brokenRules[key] = rule.Value;
                }
                else {
                    brokenRules.Remove(key);
                }
            }
        }

        private void NotifyPropertyChanged(InvocationInfo info) {

            var methodName = info.TargetMethod.Name;

            // If method name is not a call to set_{SomeProperty}
            // there's nothing to do here
            if (!methodName.StartsWith("set_")) {
                return;
            }

            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(methodName.Substring(4)));
            }
        }

        private void NotifyPropertyChanging(InvocationInfo info) {

            var methodName = info.TargetMethod.Name;

            // If method name is not a call to set_{SomeProperty}
            // there's nothing to do here
            if (!methodName.StartsWith("set_")) {
                return;
            }

            if (PropertyChanging != null) {
                PropertyChanging(this, new PropertyChangingEventArgs(methodName.Substring(4)));
            }
        }
    }

    public class RuleManager {

        private static readonly List<RuleEntry> rules = new List<RuleEntry>();

        private static readonly ReaderWriterLockSlim rulesLock = new ReaderWriterLockSlim();

        // rules will be loaded into a separate AppDomain that can be unloaded
        // if rule definitions or implementations change at runtime
        private static AppDomain ruleDomain = CreateRuleDomain();

        private static readonly bool registerRules = RegisterRules();

        private static string rulesFileName;

        private static DateTime lastWriteTime;

        private static bool RegisterRules() {

            rulesFileName = ConfigurationManager.AppSettings["ruleDefinition"];

            lastWriteTime = File.GetLastWriteTime(rulesFileName);

            var ruleElement = XElement.Load(rulesFileName);

            // should validate the file against a schema
            var definitions = from classes in ruleElement.Descendants("Class")
                              from property in classes.Descendants("Property")
                              select new {
                                  ClassName = classes.Attribute("name").Value,
                                  PropertyName = property.Attribute("name").Value,
                                  Rules = from rule in property.Descendants("Rule")
                                          select new {
                                              AssemblyName = rule.Attribute("assemblyName").Value,
                                              ClassName = rule.Attribute("class").Value,
                                              MethodName = rule.Attribute("method").Value,
                                              Arguments = rule.Attribute("arguments").Value.Split(',')
                                          }
                              };

            var loadedInstances = new Dictionary<string, object>();

            foreach (var definition in definitions) {

                var type = Type.GetType(definition.ClassName);

                foreach (var rule in definition.Rules) {

                    var instanceKey = rule.AssemblyName + "." + rule.ClassName;

                    if (!loadedInstances.ContainsKey(instanceKey)) {

                        var instance = ruleDomain.CreateInstance(rule.AssemblyName, rule.ClassName);

                        loadedInstances.Add(instanceKey, instance.Unwrap());
                    }

                    //var d = AppDomain.CurrentDomain.GetAssemblies();

                    // assuming that method exists, is an instance method and takes three parameters of types:
                    // object, object, object[]
                    // And returns:
                    // KeyValuePair<bool, IBrokenRule>
                    var method = loadedInstances[instanceKey].GetType().GetMethod(rule.MethodName);

                    Debug.Assert(method != null && !method.IsStatic);
                    Debug.Assert(method.GetParameters().Length == 3);
                    ValidateMethodParameters(method);

                    Func<object, object, KeyValuePair<bool, IBrokenRule>> handler =
                        delegate(object instance, object value) {
                            return (KeyValuePair<bool, IBrokenRule>)

                                      /*Debug.Assert(method.GetParameters()[0]
                                                   .ParameterType
                                                   .IsAssignableFrom(instance.GetType()));*/

                                      method.Invoke(loadedInstances[instanceKey],
                                                new object[] { instance, value, rule.Arguments });
                        };


                    rules.Add(new RuleEntry(type, definition.PropertyName, handler));
                }
            }

            return true;
        }

        private static void ReloadRulesIfFileChanged() {

            // If the file has not been written to since it was loaded
            // there's nothing to do here
            if (File.GetLastWriteTime(rulesFileName) == lastWriteTime) {
                return;
            }

            try {
                rulesLock.EnterWriteLock();

                // Potential race condition so check again
                if (File.GetLastWriteTime(rulesFileName) != lastWriteTime) {

                    lastWriteTime = File.GetLastWriteTime(rulesFileName);

                    // Clear the existing rules
                    rules.Clear();

                    // unload the rules
                    AppDomain.Unload(ruleDomain);

                    // recreate the AppDomain
                    ruleDomain = CreateRuleDomain();

                    // re-register the rules
                    RegisterRules();
                }

            }
            finally {
                if (rulesLock.IsWriteLockHeld) {
                    rulesLock.ExitWriteLock();
                }
            }
        }

        private static AppDomain CreateRuleDomain() {

            return AppDomain.CreateDomain("ruleDomain");
        }

        public static IEnumerable<KeyValuePair<bool, IBrokenRule>> ApplyRules(object instance,
                                                                              InvocationInfo info) {

            ReloadRulesIfFileChanged();

            List<KeyValuePair<bool, IBrokenRule>> applicableRules;

            try {

                rulesLock.EnterReadLock();

                var method = info.TargetMethod;

                if (!method.Name.StartsWith("set_")) {
                    yield break;
                }

                applicableRules = rules.Where(a => a.TargetType.IsInstanceOfType(instance) &&
                                                   a.MethodName == method.Name.Substring(4)
                                             )
                                       .Select(b => b.ValidationMethod)
                                       .Select(rule => rule(instance, info.Arguments[0]))
                                       .ToList();
            }
            finally {
                rulesLock.ExitReadLock();
            }

            foreach (var rule in applicableRules) {
                yield return rule;
            }
        }

        [Conditional("DEBUG")]
        private static void ValidateMethodParameters(MethodInfo method) {
            
            bool isValid = true;

            var parameters = method.GetParameters();

            isValid &= typeof(object).IsAssignableFrom(parameters[0].ParameterType);
            isValid &= typeof(object).IsAssignableFrom(parameters[1].ParameterType);
            isValid &= typeof(object[]).IsAssignableFrom(parameters[2].ParameterType);

            Debug.Assert(isValid);
        }

        private class RuleEntry {

            private readonly Type targetType;

            private readonly string methodName;

            private readonly Func<object, object, KeyValuePair<bool, IBrokenRule>> validationMethod;

            public Type TargetType {
                get {
                    return targetType;
                }
            }

            public string MethodName {
                get {
                    return methodName;
                }
            }

            public Func<object, object, KeyValuePair<bool, IBrokenRule>> ValidationMethod {
                get {
                    return validationMethod;
                }
            }

            public RuleEntry(Type targetType, string methodName,
                             Func<object, object, KeyValuePair<bool, IBrokenRule>> validationMethod) {

                this.targetType = targetType;

                this.methodName = methodName;

                this.validationMethod = validationMethod;
            }
        }

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Technical Lead Olivine Technology
Kenya Kenya
Technical Lead, Olivine Technology - Nairobi, Kenya.

"The bane of productivity: confusing the rituals of work (sitting at your desk by 8:00am, wearing a clean and well pressed business costume etc.) with actual work that produces results."

Watch me!

Comments and Discussions