Introduction
This article tries to build a Property based validator for .NET objects. We specify object test conditions and validation rules in the configuration
and based on these rules identify which object satisfies the given criteria.
Background
We came across this problem when our operational team wanted us to quickly filter a list of object based on a very specific criteria (a property value in this case).
Even though request was very specific for a single property, we realized that filter it in the code will not be a solution fit for our application as there could be new
similar requests coming in future at a short notice. Hence we came with a solution to use XML Rule Based validator which could be used to fulfill such request
with a neat configurable option without changing the code.
Using the code
This is a sample piece of code and not intended to be used in a production system. It should be carefully extended and organized before using it for a real software.
Here is my XML file where I specify what are the entry criteria for the objects and how should they be validated.
Note: We are only doing property validation here.
Rules.Xml
<Rules>
<Rule>
<Condition>
<Property Name="Property1" Value="Condition1" />
<Property Name="Property2" Value="Condition1" />
</Condition>
<Validation>
<Property Name="ValidateProperty" Operator="StartsWith" Value="TestProperty" />
</Validation>
</Rule>
<Rule>
<Condition>
<Property Name="Property1" Value="Condition2" />
<Property Name="Property2" Value="Condition1" />
</Condition>
<Validation>
<Property Name="ValidateProperty" Operator="StartsWith" Value="test" />
</Validation>
</Rule>
</Rules>
Main.cs
public class Property
{
public string PropertyName;
public string Operator;
public string Value;
}
interface IRule
{
bool ShouldValidate(object o);
bool Validate(object o);
}
public class Rule: IRule
{
public List<Property> Conditions = new List<Property>();
public List<Property> Validators = new List<Property>();
private XmlNode xmlNode;
public Rule()
{
}
public bool Validate(object p)
{
bool shouldValidate = ShouldValidate(p);
if (shouldValidate)
{
return Validators.All(v =>
{
var pi = p.GetType().GetProperty(v.PropertyName);
if (pi != null)
{
var pValue = pi.GetValue(p) as string;
if (pValue != null)
{
switch (v.Operator)
{
case "StartsWith":
return pValue.StartsWith(v.Value);
default:
return false;
}
}
}
return false;
});
}
return true;
}
public bool ShouldValidate(object p)
{
int propertyMatch = 0;
foreach (Property prop in Conditions)
{
var pi = p.GetType().GetProperty(prop.PropertyName);
if (pi != null)
{
var pValue = pi.GetValue(p) as string;
if (pValue != null && pValue == prop.Value)
{
propertyMatch++;
}
}
}
return propertyMatch == Conditions.Count;
}
}
public class RuleFactory
{
static IRule CreateRule(XmlNode xmlNode)
{
if (xmlNode != null)
{
return new Rule()
{
Conditions = xmlNode.SelectSingleNode("Condition").SelectNodes("Property").Cast<XmlNode>().Select(
property => new Property
{
PropertyName = property.Attributes["Name"].Value,
Value = property.Attributes["Value"].Value
}).ToList(),
Validators = xmlNode.SelectSingleNode("Validation").SelectNodes("Property").Cast<XmlNode>().Select(
property => new Property
{
PropertyName = property.Attributes["Name"].Value,
Value = property.Attributes["Value"].Value,
Operator = property.Attributes["Operator"].Value
}).ToList()
};
}
return null;
}
}
Code above is the basic framework needed to get us going. Note that I'm only looking for hard-coded starts with operator here but we can extend this solution to support other operators
as well. We'll of course need to refactor this class to add strongly type validator and support OR based conditions as well. This class only checks for object that satisfies
all the conditions and that confirms to all validation rules (in summary only AND operation is supported).
Now rest of the code to test this logic is posted below. Here we'll create some rules and test our objects based on the rules in the XML file.
static void Main(string[] args)
{
var document = new XmlDocument();
document.Load(@"C:\Code\test.xml");
var rules = document.SelectSingleNode("Rules").SelectNodes("Rule").Cast<XmlNode>().
Select(xmlNode => RuleFactory.CreateRule(xmlNode));
List<TestObject> testObjects = new List<TestObject>()
{
new TestObject(){Property1 = "Condition1", Property2="Condition2"},
new TestObject(){Property1 = "Condition1", Property2="Condition3", ValidateProperty="Hello"},
new TestObject(){Property1 = "Condition1", Property2="Condition2", ValidateProperty="TestProperty"},
new TestObject(){Property1 = "Condition1", Property2="Condition2", ValidateProperty="Test123" },
new TestObject(){Property1 = "Condition1", Property2="Condition1", ValidateProperty="TestProperty" },
new TestObject(){Property1 = "Condition2", Property2="Condition1", ValidateProperty="test" },
new TestObject(){Property1 = "Condition2", Property2="Condition2"}
};
var success = testObjects.Where(pr =>
{
if (rules == null)
return false;
var ru = rules.FirstOrDefault(r => r.ShouldValidate(pr));
return ru != null && ru.Validate(pr);
});
var result = success.Count();
}
Here in the main method we are creating rules based on the XML file and also creating a list of objects to validate. Based on the conditions specified in the configuration
file only two objects satisfies the criteria and if you run the application you will see that the value of result variable indeed is two.
Points of Interest
TBD: Good exercise with lambda and reflection.
History
First draft of the article.