This past week I started to work on the EViL project over at Codeplex. I figured that if I commented about all of its weaknesses, I should get involved and offer some solutions to the problems that I pointed out.
Today I finished up the first bit of code that I was working on. What I was looking to do was extend the capabilities of the attributes in the EViL framework so that comparisons could be made to properties that were attached to embedded objects. For example, I want to be able to ensure that date of an invoice is not before the opening date of the customer associated with that invoice.
1 |
|
In the end the solution was quite slick. Because you can’t get the source from Codeplex, I’ve included it here. What I managed to do was write a recursive function that would parse through the “.” delimited property name and navigate the tree of properties until it either couldn’t find the requested property or it reached the end of the passed in property name. I ran into two interesting situations while working on this.
First, I had to figure out how to handle lists of objects. I couldn’t figure out a reason that I’d want to compare one property value to one property from multiple different objects. Until I hear of or run into a situation with the need for this, I decided to leave it as a gap in the functionality. To do this I had to treat all IEnumerable objects that were the value in the property string as if they weren’t found. This allows the code to navigate over an IEnumerable to make comparisons to properties such as Count and Length. Here’s how you can compare the Count property of the Items IList<> type.
1 |
|
The second thing I had to deal with was as a result of limiting the comparisons to IEnumerable types. As many developers forget (this one included…thank goodness for unit tests), strings are IEnumerable. As soon as I implemented the code to treat IEnumerable types as not existing, all comparisons to strings would fail. All I had to do was add a quick check for string types and everything was peachy again.
Here’s the function that I created to do all of this work. As you can see, it’s nothing special and it could probably do with a little refactoring to get the logic depth under control.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// <summary>
/// Gets the value of property that is being compared to. This recursively searches
/// through the object tree based on the propertyName passed in. If the name is
/// in a Class.Class.Class.Property format it will traverse all the way to the Property
/// and use it for comparison.
/// </summary>
/// <param name="propertyName">String representation of the property that is being compared to.
/// Should be in a Class.Class.Class.Property format if the property is in embedded objects.</param>
/// <param name="entity">The class that the property is being searched for in.</param>
/// <returns>The property value if it is found. If the property is not found, null will be returned.</returns>
protected object GetPropertyValue(string propertyName, object entity)
{
string[] propertyNames = propertyName.Split('.');
if (propertyNames.Length == 0 || entity == null)
{
return null;
}
string firstPropertyName = propertyNames[0];
PropertyInfo[] properties = entity.GetType().GetProperties();
// Loop thru to find the one we want
foreach (PropertyInfo pi in properties)
{
if (pi.Name == firstPropertyName)
{
//this is to prevent comparisons against lists of objects, but allows comparison against properties of those lists
if (pi.PropertyType.GetInterface("IEnumerable") != null && propertyNames.Length == 1)
{
//this is needed because strings are IEnumerable and without this we can't compare string to string easily
if (typeof(string) == pi.PropertyType)
{
return pi.GetValue(entity, null);
}
else
{
return null;
}
}
else if (propertyNames.Length > 1)
{
return GetPropertyValue(MakeTrimmedPropertyName(propertyNames), pi.GetValue(entity, null));
}
else
{
return pi.GetValue(entity, null);
}
}
}
return null;
}