Introduction
I was playing around with custom attributes this week, and decided I would abuse them to within an inch of their lives, and then make people's heads blow up with something that you really probably don't have to do, or necessarily should do. I say, what's life without a little whimsy?
The purpose of an Attribute
is to attach some form of metadata to a property, object, or method, and by using reflection, gain access to said metdata at runtime. I've extended that paradigm to include functionality that checks whether or not the value of the property is "valid".
Why I Did It
I recently posted an article regarding the parsing of CSV files. I write that code because I have to import data from CSV files, and due to the nature of my environment, I have to go to retarded levels to ensure that a human somewhere in the data handling chain didn't somehow screw up the data. To that end, I have established values for each expected column of data that indicates a problem was encountered while importing the file. These values are usually "ERROR" for strings, -1 for numeric values, etc, etc. So, I figured, "Hey! I'll take this opportunity to play with custom attributes."
In my defense, I was on a bacon high, so this just reinforces the idea that you shouldn't code when you're in a state of bliss, because things that normally bother the hell out of you don't really seem to matter much. I started a Lounge topic about this code and someone suggested I mention why I resorted to doing this. Well, that's the funny part - I didn't have to resort to it (in point of fact, I already had an alternative solution in place that contained less code). All the code that I wrote followed my (loudly) vocalized derision (which might be mistaken for sudden onset Tourette Syndrome) when I read that "attributes aren't supposed to provide functionality". RUBBISH! That's right! RUBBISH!
The Code
The InvalidValueAttribute Class
The class declaration is important because that's where you tell the attribute where it can be used, and whether or not to allow multiple instances. Ironically enough, that's done with an attribute, as seen below.
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple=true)]
public class InvalidValueAttribute : System.Attribute
In order to control how the comparison is performed, I defined an enum inside the class.
public enum TriggerType
{
Valid,
Equal,
NotEqual,
Over,
Under
};
Then I implement the configuration properties.
public TriggerType Trigger { get; protected set; }
public object TriggerValue { get; protected set; }
public Type ExpectedType { get; protected set; }
public object PropertyValue { get; protected set; }
This property allows the object that contains the decorated property to display the validity status.
public string TriggerMsg
{
get
{
string format = string.Empty;
switch (this.Trigger)
{
case TriggerType.Valid :
case TriggerType.Equal : format = "equal to"; break;
case TriggerType.NotEqual : format = "not equal to"; break;
case TriggerType.Over : format = "greater than"; break;
case TriggerType.Under : format = "less than"; break;
}
if (!string.IsNullOrEmpty(format))
{
format = string.Concat("Cannot be ", format, " '{0}'. \r\n Current value is '{1}'.\r\n");
}
return (!string.IsNullOrEmpty(format)) ? string.Format(format, this.TriggerValue, this.PropertyValue) : string.Empty;
}
}
As the class evolved, the constructor became somewhat complex. This occurred mostly due to handling of DateTime
objects. There should probably be more code added to verify that the triggerValue
can be cast to the expectedType (if it is specified), but I'll leave that to the descretion of, and as an exorcise for the programmer (that's you).
public InvalidValueAttribute(object triggerValue, TriggerType trigger=TriggerType.Valid, Type expectedType=null )
{
if (this.IsIntrinsic(triggerValue.GetType()))
{
this.Trigger = trigger;
if (expectedType != null)
{
if (this.IsDateTime(expectedType))
{
long ticks = Math.Min(Math.Max(0, Convert.ToInt64(triggerValue)), Int64.MaxValue);
this.TriggerValue = new DateTime(ticks);
}
else
{
this.TriggerValue = triggerValue;
}
this.ExpectedType = expectedType;
}
else
{
this.TriggerValue = triggerValue;
this.ExpectedType = triggerValue.GetType();
}
}
else
{
throw new ArgumentException("The triggerValue parameter must be a primitive, string, or DateTime, and must match the type of the attributed property.");
}
}
The last method in the class that I'll be illustrating is the IsValid
method (since that's what all of this is about). This method performs the appropriate comparison (as specified by the Trigger
property) of the property value against the TriggerValue
specified in the constructor.
public bool IsValid(object value)
{
bool result = false;
this.PropertyValue = value;
Type valueType = value.GetType();
if (this.IsDateTime(valueType))
{
this.TriggerValue = this.MakeNormalizedDateTime();
this.ExpectedType = typeof(DateTime);
}
if (valueType == this.ExpectedType)
{
switch (this.Trigger)
{
case TriggerType.Equal : result = this.IsEqual (value, this.TriggerValue); break;
case TriggerType.Valid :
case TriggerType.NotEqual : result = this.IsNotEqual (value, this.TriggerValue); break;
case TriggerType.Over : result = !this.GreaterThan(value, this.TriggerValue); break;
case TriggerType.Under : result = !this.LessThan (value, this.TriggerValue); break;
}
}
else
{
throw new InvalidOperationException("The property value and trigger value are not of compatible types.");
}
return result;
}
The remainder of the methods in the class are helper methods, type checkers and the actual comparison methods, and really aren't that interesting. They are only included here in the interest of completeness. Feel free to minimize the following code block to make the article appear shorter. Because their intended purpose is obvious (to me, at least), I'm not going to explain any of them. Besides, I'm sure by now that you're anxious to tell me why I shouldn't have written this code in the first place.
private DateTime MakeNormalizedDateTime()
{
DateTime date = new DateTime(0);
if (this.IsInteger(this.TriggerValue.GetType()))
{
long ticks = Math.Min(Math.Max(0, Convert.ToInt64(this.TriggerValue)), Int64.MaxValue);
date = new DateTime(ticks);
}
else if (this.IsDateTime(this.TriggerValue.GetType()))
{
date = Convert.ToDateTime(this.TriggerValue);
}
return date;
}
#region type detector methods
protected bool IsUnsignedInteger(Type type)
{
return ((type != null) &&
(type == typeof(uint) ||
type == typeof(ushort) ||
type == typeof(ulong)));
}
protected bool IsInteger(Type type)
{
return ((type != null) &&
(this.IsUnsignedInteger(type) ||
type == typeof(byte) ||
type == typeof(sbyte) ||
type == typeof(int) ||
type == typeof(short) ||
type == typeof(long)));
}
protected bool IsDecimal(Type type)
{
return (type != null && type == typeof(decimal));
}
protected bool IsString(Type type)
{
return (type != null && type == typeof(string));
}
protected bool IsDateTime(Type type)
{
return ((type != null) && (type == typeof(DateTime)));
}
protected bool IsFloatingPoint(Type type)
{
return ((type != null) && (type == typeof(double) || type == typeof(float)));
}
protected bool IsIntrinsic(Type type)
{
return (this.IsInteger(type) ||
this.IsDecimal(type) ||
this.IsFloatingPoint(type) ||
this.IsString(type) ||
this.IsDateTime(type));
}
protected bool LessThan(object obj1, object obj2)
{
bool result = false;
Type objType = obj1.GetType();
if (this.IsInteger(objType))
{
result = (this.IsUnsignedInteger(objType) && this.IsUnsignedInteger(obj2.GetType())) ?
(Convert.ToUInt64(obj1) < Convert.ToUInt64(obj2)) :
(Convert.ToInt64(obj1) < Convert.ToInt64(obj2));
}
else if (this.IsFloatingPoint(objType))
{
result = (Convert.ToDouble(obj1) < Convert.ToDouble(obj2));
}
else if (this.IsDecimal(objType))
{
result = (Convert.ToDecimal(obj1) < Convert.ToDecimal(obj1));
}
else if (this.IsDateTime(objType))
{
result = (Convert.ToDateTime(obj1) < Convert.ToDateTime(obj2));
}
else if (this.IsString(objType))
{
result = (Convert.ToString(obj1).CompareTo(Convert.ToString(obj2)) < 0);
}
return result;
}
protected bool GreaterThan(object obj1, object obj2)
{
bool result = false;
Type objType = obj1.GetType();
if (this.IsInteger(objType))
{
result = (this.IsUnsignedInteger(objType) && this.IsUnsignedInteger(obj2.GetType())) ?
(Convert.ToUInt64(obj1) > Convert.ToUInt64(obj2)) :
(Convert.ToInt64(obj1) > Convert.ToInt64(obj2));
}
else if (this.IsFloatingPoint(objType))
{
result = (Convert.ToDouble(obj1) > Convert.ToDouble(obj2));
}
else if (this.IsDecimal(objType))
{
result = (Convert.ToDecimal(obj1) > Convert.ToDecimal(obj1));
}
else if (this.IsDateTime(objType))
{
result = (Convert.ToDateTime(obj1) > Convert.ToDateTime(obj2));
}
else if (this.IsString(objType))
{
result = (Convert.ToString(obj1).CompareTo(Convert.ToString(obj2)) > 0);
}
return result;
}
protected bool IsEqual(object obj1, object obj2)
{
bool result = false;
Type objType = obj1.GetType();
if (this.IsInteger(objType))
{
result = (this.IsUnsignedInteger(objType) && this.IsUnsignedInteger(obj2.GetType())) ?
(Convert.ToUInt64(obj1) == Convert.ToUInt64(obj2)) :
(Convert.ToInt64(obj1) == Convert.ToInt64(obj2));
}
else if (this.IsFloatingPoint(objType))
{
result = (Convert.ToDouble(obj1) == Convert.ToDouble(obj2));
}
else if (this.IsDecimal(objType))
{
result = (Convert.ToDecimal(obj1) == Convert.ToDecimal(obj1));
}
else if (this.IsDateTime(objType))
{
result = (Convert.ToDateTime(obj1) == Convert.ToDateTime(obj2));
}
else if (this.IsString(objType))
{
result = (Convert.ToString(obj1).CompareTo(Convert.ToString(obj2)) == 0);
}
return result;
}
protected bool IsNotEqual(object obj1, object obj2)
{
return (!this.IsEqual(obj1, obj2));
}
#endregion type detector methods
Example Usage
In order to exercise my attribute, I wrote the following class. The idea is that after the properties are set in my model, I can check the IsValid
property to make sure I can save the imported object to the database. For purposes of example, I only set four attributed properties. Anything NOT decorated with atttribute is not included in the IsValid
code.
The first thing you see are the properties that are actually part of the model, and that are decorated with our attribute (refer to the comments for a description of what's going on).
public const long TRIGGER_DATE = 630822816000000000;
public const string TRIGGER_STRING = "ERROR";
[InvalidValue(-1, InvalidValueAttribute.TriggerType.Valid)]
public int Prop1 { get; set; }
[InvalidValue(5d, InvalidValueAttribute.TriggerType.Under)]
[InvalidValue(10d, InvalidValueAttribute.TriggerType.Over)]
public double Prop2 { get; set; }
[InvalidValue(TRIGGER_STRING, InvalidValueAttribute.TriggerType.Valid)]
public string Prop3 { get; set; }
[InvalidValue(TRIGGER_DATE, InvalidValueAttribute.TriggerType.Over, typeof(DateTime))]
public DateTime Prop4 { get; set; }
Next, we see the properties that aren't part of the actual model, but that are used to assist the sample application display the results.
public DateTime TriggerDate { get { return new DateTime(TRIGGER_DATE); } }
public bool ShortCircuitOnInvalid { get; set; }
public string InvalidPropertyMessage { get; private set; }
Finally, we get to the reason we're all here - the IsValid
property. This property retrieves all properties decorated with the InvalidValueAttribute
attribute, and processes each attribute instance for each property. Of course, if you don't allow your own custom attribute to have multiple instances, then you can skip the foreach code, but for my purposes, I needed to do this. I also have the requirement that each instance use a unique Trigger
enumerator, because having multiples of any given Trigger
doesn't make sense for me.
public bool IsValid
{
get
{
this.InvalidPropertyMessage = string.Empty;
bool isValid = true;
PropertyInfo[] infos = this.GetType().GetProperties();
foreach(PropertyInfo info in infos)
{
var attribs = info.GetCustomAttributes(typeof(InvalidValueAttribute), true);
if (attribs.Count() > 1)
{
var distinct = attribs.Select(x=>((InvalidValueAttribute)(x)).Trigger).Distinct();
if (attribs.Count() != distinct.Count())
{
throw new Exception(string.Format("{0} has at least one duplicate InvalidValueAttribute specified.", info.Name));
}
}
foreach(InvalidValueAttribute attrib in attribs)
{
object value = info.GetValue(this, null);
bool propertyValid = attrib.IsValid(value);
if (!propertyValid)
{
isValid = false;
this.InvalidPropertyMessage = string.Format("{0}\r\n{1}", this.InvalidPropertyMessage,
string.Format("{0} is invalid. {1}", info.Name, attrib.TriggerMsg));
if (this.ShortCircuitOnInvalid)
{
break;
}
}
}
}
return isValid;
}
Finally, the application is nothing more than a console application that instantiates the sample class, and feeds the console with status messages as the invalid properties are fixed. There's really no point in posting the code, as it would just make the article that much longer, serving no real purpose.
What I Learned Along the Way
As it is with most programming efforts, I learn little things along the way. At my age, I'll probably forget them by the following day's breakfast, but hey, thats one of the things what makes the onset of Alzheimer's/dimentia so much fun. On the bright side, I still remember bacon, so silver linings...
You Can't Get There From Here
As much as I'd like it to be the case, you cannot reference the property to which your attribute is attached. This was especially inconvenient because I couldn't get get ahead of the unique Trigger
aspect until I enumerated the properties (in their parent class) and retrieved the instances of the attribute object.
Doctor, It Hurts When I Raise My Arm...
When you're passing parameters to an attribute, the parameter must be a constant expression, a typeof
expression or array creation expression. This precludes you from specifying anything but a primitive type that doesn't have to be instantiated with new
(such as DateTime
, StringBuilder
, etc). This restriction forced me to rethink the constructor as I was writing this article.
I'm Not Sure It Was Worth The Effort
This exercise resulted in a LOT of code that is probably only useful in a data loading situation where you need to be double-damn sure that the loaded/imported data complies with certain value restrictions. According to everything I've read, attributes aren't supposed to be used this way, but my nature is to balk and rebel when I'm told I "can't do this", or I "shouldn't do that". Accept this as an admission that I probably broke some seemingly arbitrary rule or violated an equally arbitrary best practice, so don't feel like you need to tell me about it.
Article History
- 25 Feb 2018 - Fixed some spelling errors, mostly because I don't have anything better to do with my time time now.
- 27 SEP 2016 - Initial publication (probably immediate followed by a dozen or so micro-edits to fix spelling/grammar errors that I didn't catch before I pushed the big orange button).