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;
}
}
}
}