Saturday, October 2, 2010

Annotations and Attribute based programming

Annotations are the best way to write a concise code if you want to see dynamic behavior of your code. Here are some simple scenarios that this will come handy.
  1. Custom serialization (XML, CSV, custom format)
  2. Command line handlers
  3. Compiler options - DEBUG mode and others
  4. Assembly information
  5. Module information
.....and the list goes on.


I have seen a lot of people embrace Annotation based development and then later they revert back to normal style....reason, code gets complicated and messy. I had the same issues too. I took a step back in one such scenario and did a bit of introspection to why this complicates my code. This is what I found....

Here's a sample command line operation based on annotation (put in the code from TR).

If you notice the class command line handler does handle a lot of things in itself - name, alias name, required attribute, usage information, description, error message. This gets even more complicated when yu want to add more functionality into this.

Best way to handle this situation is to do something like this (insert Commandline utility code here)
If you notice the code is separated by multiple annotations (like the AssemblyInfo.cs handles). Each annotation serves different purpose and the code to handle this is much more easier. Simple way to make this happen is to separate commandline handler behavior from business requirements. Here is a simple way of handling it

[Required, multipleUsage]
[Aliasname]
[LocalizationSupport]
[description, errorMessage]
variable-type property_name

This mechanism will result in having multiple annotations to a property/method, but this will certainly make things more readable and easy to handle.

My sample code to handle this CommandlineUtility is as below. I made this code pretty fast so there are certainly areas for improvement, so when you pick it up, make sure you correct them. My focus was primarily on the annotations part alone.

There are 3 parts to this development
  1. Create a class with annotations. This is the class that will eventually be loaded with values entered by the user in command prompt (command line arguments)
  2. Parse command line arguments for validity
  3. Load command line arguments into the annotated class.

Annotated class gives out the following information
  1. Valid parameters to be passed
  2. Valid values
  3. Aliases and error handling

What we will do
  1. Once the user enters command line arguments, parse the arguments to see if all required parameters are passed
  2. Check to is if all arguments have values
  3. Validate values (if needed)
  4. Load the values (through reflection) into annotated class
  5. Pass the annotated class to you business layer for all further processing

I have provided sample code to do the first 4 operations.
Annotated class definition

CommandLineArguments.cs
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FileCrypt
{
[CommandlineExclusiveAttribute("operation", "Encrypt", "Decrypt")]
public class CommandLineArguments
{
public CommandLineArguments()
{
Log = false;
Verbose = false;
Help = false;
}

[CommandLineAttribue("encrypt")]
public bool Encrypt { get; set; }

[CommandLineAttribue("dencrypt")]
public bool Decrypt { get; set; }

[CommandLineAttribue("file")]
[CommandlineRequired(true)]
public string FileName { get; set; }

[CommandLineAttribue("key")]
[CommandlineRequired(true)]
public string KeyPhrase { get; set; }

[CommandLineAttribue("log")]
public bool Log { get; set; }

[CommandLineAttribue("verbose")]
public bool Verbose { get; set; }

[CommandLineAttribue("help")]
public bool Help {get;set;}
}
}


Make sure all the properties in the class with annotation have both getter and setter. This class can also contain other properties that are not annotated for business purposes. But my take will be to avoid such usage on this class.

Having defined annotated class we now need to define each of these attributes
CommandlineExclusiveAttribute - Defines mutually exclusive properties in the class
CommandLineAttribute - argument name that will be used in the command line mode
CommandLineRequired - properties that are required and must be provided

You can add more as per your requirement

CommandLineExclusiveAttribute.cs
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FileCrypt
{
[AttributeUsage(AttributeTargets.Class)]
public class CommandlineExclusiveAttribute : Attribute
{
public Dictionary<string, string[]> ExclusiveAttributes
{
get;
private set;
}

public CommandlineExclusiveAttribute(string type, params string[] properties)
{
ExclusiveAttributes = new Dictionary<string, string[]>();
ExclusiveAttributes.Add(type, properties);
}

public string[] GetExclusiveProperties()
{
return ExclusiveAttributes.Keys.ToArray();
}

public string[] GetValues(string property)
{
return ExclusiveAttributes[property];
}
}
}

CommandLineAttribute.cs
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FileCrypt
{
[AttributeUsage(AttributeTargets.Property)]
public class CommandLineAttribue : Attribute
{
public string UsageName
{
get;
private set;
}
public CommandLineAttribue(string usageName)
{
this.UsageName = usageName;
}
}
}

CommandlIneRequired.cs
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FileCrypt
{
[AttributeUsage(AttributeTargets.Property)]
class CommandlineRequiredAttribute : Attribute
{
public bool Property
{
get;
private set;
}
public CommandlineRequiredAttribute(bool property)
{
Property = property;
}
}
}

This is the main program that will invoke CommandLineHandler and handle commandline arguments
using System;

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FileCrypt
{
/*
* This is a simple utility to check command line utility
* Encrypt file
* /encrypt - encrypt
* /decrypt - decrypt file
* /key - key phrase for encryption/decrypting
* /file - file to encrypt
* /log - logger
* /verbose - verbose
* /help - usage information
*
* sample usage
* FileCrypt /encrypt /key:test /file:"/path/to/file.txt" /log /verbose
* Decrypt file
* FileCrypt /decrypt /file:/path/of/encrypted/file /log /key:test /verbose
* Usage information
* FileCrypt /help
*/

class Program
{
static void Main(string[] args)
{
CommandLineArguments commandLineArguments = new CommandLineArguments();
CommandlineHandler commandlineHandler = new CommandlineHandler(args, commandLineArguments);
commandlineHandler.Parse();
if (commandlineHandler.errors.Count > 0)
{
foreach (string error in commandlineHandler.errors)
{
Console.WriteLine(error);
}
Console.ReadKey(true);
return;
}
// load values into commandlinearguments object
commandlineHandler.Load();
}
}
}

CommandlineHandler is the parser class with will look for arguments, values
parse them and load it into CommandlineArguments class

To accomplish parsing, we need to get typ[e information on each property in
CommandlineArgument class. For e.g.
1. How do we know what properties are REQUIRED
2. How do we know mutually exclusive properties

We query CommnadLineArgument class in RuntimeCommandlineInfo.cs
and get required information. RunTimeCommandlineInfo uses reflection to find
annotation information on CommandLineArgument class and passes it back to
CommandLineHandler. To site a few, CommandLineHandler will ask for the following
  1. Get me all mutually exclusive properties - When this information is made available, CommandLineHandler will scan the arguments passed to make
  2. sure no 2 exclusive properties are passed in
  3. Get all required properties
  4. Get usage name for property - e.g. usage name for Encrypt is "encrypt", Verbose is "verbose"
CommandlineHandler.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace FileCrypt
{
public class CommandlineHandler
{
private string[] arguments;
Type _type;

public List<string> errors
{
get;
private set;
}

public CommandlineHandler(string[] args, object commandlineInfo)
{
errors = new List<string>();
this.arguments = args;
_type = commandlineInfo.GetType();
}

public void Parse()
{
// made sure all is fine. No mixup of arguments
if (CheckExclusiveArguments() == false)
{
return;
}
// Made sure all required attributes are available
if (CheckRequiredAttributes() == false)
{
return;
}

if (CheckAttributesForValues() == false)
{
return;
}
}

public void Load()
{
// seperate each of these arguments into a dictionary may be and then
// load it in to the object
// each argument is seperated by a space and values for the arguments are seperated by ':'
// Arguments that have more then one value will be loaded into the dictionary as
// CSV
CommandLineArguments cmdLineArgument = new CommandLineArguments();
LoadeArgumentsAndValuesIntoDictionary(cmdLineArgument);
}

private void LoadeArgumentsAndValuesIntoDictionary(object commandlineArgument)
{
string[] commandlineAttributes = RuntimeCommandlineInfo.GetAllCommandLineAttributeProperties(_type);

foreach (string attribute in commandlineAttributes)
{
string value = GetArgumentValues(attribute);
if (ArgumentExists(attribute) == false)
{
continue;
}
PropertyInfo propertyInfo = RuntimeCommandlineInfo.GetPropertyInfoFromUsageName(attribute, _type);
if (propertyInfo == null)
{
continue;
}

if (propertyInfo.PropertyType == typeof(bool))
{
propertyInfo.SetValue(commandlineArgument, true, null);
}
else if ((propertyInfo.PropertyType == typeof(string)))
{
propertyInfo.SetValue(commandlineArgument, value, null);
}
}
}

private bool ArgumentExists(string argument)
{
// should be completly revamped by Regex
// split aruments by / and then by :
foreach (string arg in arguments)
{
string argumentName = arg.Replace("/", "");
argumentName = argumentName.Split(':')[0];
if (argumentName.Equals(argument))
{
return true;
}
}
return false;
}

private string GetArgumentValues(string argument)
{
// should be completly revamped by Regex
foreach (string arg in arguments)
{
string argumentName = arg.Replace("/", "");
string[] argumentNameAndValue = argumentName.Split(':');
if (argumentNameAndValue[0].Equals(argument) && argumentNameAndValue.Length > 1)
{
return argumentNameAndValue[1];
}
}
return string.Empty;
}

private bool CheckAttributesForValues()
{
// use Regex here
// go over each attribute to see if they have values in them. If the underlying data type of
// the attribute is boolen then value may not be defined, else, you should see value for it
bool allValuesFound = true;
string[] allNonBooleanArguments = RuntimeCommandlineInfo.GetAllNonBooleanProperties(_type);
foreach (string argument in arguments)
{
foreach (string nonBooleanArgument in allNonBooleanArguments)
{
if (argument.Contains("/" + nonBooleanArgument))
{
string[] argValue = argument.Split(':');
if (argValue.Length <= 1)
{
errors.Add("Value not provided for argument : " + nonBooleanArgument);
allValuesFound = false;
}
}
}
}
return allValuesFound;
}

private bool CheckRequiredAttributes()
{
// use Regex here
string[] requiredClassAttributes = RuntimeCommandlineInfo.GetRequiredClassAttributes(_type);
string[] requiredAttributes = RuntimeCommandlineInfo.GetCommandLineAttributes(requiredClassAttributes, _type);
bool requiredAttributeFound = false;
bool executionSuccess = true;
foreach (string requiredAttribute in requiredAttributes)
{
requiredAttributeFound = false;
foreach (string argument in arguments)
{
if (argument.Contains("/" + requiredAttribute)
|| argument.Equals(requiredAttribute, StringComparison.CurrentCultureIgnoreCase))
{
requiredAttributeFound = true;
}
}
if (requiredAttributeFound == false)
{
errors.Add("Missing Required attribute : " + requiredAttribute);
executionSuccess = false;
}
}
return executionSuccess;
}

private bool CheckExclusiveArguments()
{
Dictionary<string, string[]> allExclusiveArguments = RuntimeCommandlineInfo.GetExclusiveProperties(_type);
foreach (string key in allExclusiveArguments.Keys)
{
string[] exclusiveArguments = allExclusiveArguments[key];
if (CheckForMoreThanOneArgument(exclusiveArguments) == false)
{
errors.Add("Invalid argument passed - " + GetCommaSeperatedValues(exclusiveArguments));
return false;
}
}
return true;
}

private string GetCommaSeperatedValues(string[] exclusiveArguments)
{
string returnValue = exclusiveArguments[0];
foreach (string str in exclusiveArguments)
{
if (returnValue.Equals(str))
continue;
returnValue += ", " + str;
}
return returnValue;
}

private bool CheckForMoreThanOneArgument(string[] exclusiveArguments)
{
// use Regex here
bool argumentDefined = false;
foreach (string argument in arguments)
{
foreach (string exclusive in exclusiveArguments)
{
if (argument.Equals(exclusive, StringComparison.CurrentCultureIgnoreCase) ||
argument.Equals("/" + exclusive, StringComparison.CurrentCultureIgnoreCase))
{
if(argumentDefined == true)
{
return false;
}
else
{
argumentDefined = true;
}
}

}
}
return argumentDefined;
}
}
}

Reflection information on CommandLineArguments class is provided through RunTimeCommandLineInfo class

RunTimeCommandLineInfo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.Reflection;

namespace FileCrypt
{
public class RuntimeCommandlineInfo
{

public static Dictionary<string, string[]> GetExclusiveProperties(Type _type)
{
Dictionary<string, string[]> exclusiveProperties = new Dictionary<string, string[]>();
foreach (Attribute attribute in _type.GetCustomAttributes(false))
{
if (attribute is CommandlineExclusiveAttribute)
{
CommandlineExclusiveAttribute exclusiveAttribute = attribute as CommandlineExclusiveAttribute;
if (exclusiveAttribute != null)
{
return exclusiveAttribute.ExclusiveAttributes;
}
}
}
return null;
}

public static string[] GetRequiredClassAttributes(Type _type)
{
List<string> requiredAttributes = new List<string>();

foreach (PropertyInfo propertyInfo in _type.GetProperties())
{
foreach (Attribute attribute in propertyInfo.GetCustomAttributes(false))
{
if (attribute is CommandlineRequiredAttribute)
{
CommandlineRequiredAttribute requiredAttribute = attribute as
CommandlineRequiredAttribute;
if (requiredAttribute != null)
{
if (requiredAttribute.Property == true)
{
requiredAttributes.Add(propertyInfo.Name);
}

}
}
}
}
return requiredAttributes.ToArray();
}

public static string[] GetAllNonBooleanProperties(Type _type)
{
List<string> nonBooleanProperties = new List<string>();
foreach (PropertyInfo propertyInfo in _type.GetProperties())
{
if (propertyInfo.PropertyType.Equals(typeof(bool)))
{
continue;
}
string attributeusageName = GetCommandlineAttributeUsageName(propertyInfo);
nonBooleanProperties.Add(attributeusageName);

}
return nonBooleanProperties.ToArray();
}

public static string[] GetCommandLineAttributes(string[] propertyNames, Type _type)
{
List<string> commandlineAttributes = new List<string>();
if (propertyNames == null)
{
return GetAllCommandLineAttributeProperties(_type);
}
else
{
foreach (string propertyName in propertyNames)
{
PropertyInfo propertyInfo = GetPropertyInfo(propertyName, _type);
if (propertyInfo != null)
{
string usageName = GetCommandlineAttributeUsageName(propertyInfo);
if (!string.IsNullOrEmpty(usageName))
{
commandlineAttributes.Add(usageName);
}
else
{

}
}
}
}
return commandlineAttributes.ToArray();
}

public static string[] GetAllCommandLineAttributeProperties(Type _type)
{
List<string> allcommandlineAttributes = new List<string>();

foreach (PropertyInfo propertyInfo in _type.GetProperties())
{
foreach (Attribute attribute in propertyInfo.GetCustomAttributes(false))
{
if (attribute is CommandLineAttribue)
{
CommandLineAttribue commandlineAttribute = attribute as
CommandLineAttribue;
if (commandlineAttribute != null)
{
allcommandlineAttributes.Add(commandlineAttribute.UsageName);

}
}
}
}
return allcommandlineAttributes.ToArray();
}

public static PropertyInfo GetPropertyInfo(string propertyName, Type _type)
{
foreach (PropertyInfo propertyInfo in _type.GetProperties())
{
if (propertyInfo.Name.Equals(propertyName))
{
return propertyInfo;
}
}
return null;
}

public static PropertyInfo[] GetAllProperties(Type _type)
{
return _type.GetProperties();
}

public static PropertyInfo GetPropertyInfoFromUsageName(string usageName, Type _type)
{
foreach (PropertyInfo propertyInfo in GetAllProperties(_type))
{
foreach (Attribute attribute in propertyInfo.GetCustomAttributes(false))
{
if (attribute is CommandLineAttribue)
{
CommandLineAttribue cmdLineAttribute = attribute as CommandLineAttribue;
if (cmdLineAttribute != null)
{
if (cmdLineAttribute.UsageName.Equals(usageName))
{
return propertyInfo;
}
}
}
}
}
return null;
}

private static string GetCommandlineAttributeUsageName(PropertyInfo propertyInfo)
{
foreach (Attribute attribute in propertyInfo.GetCustomAttributes(false))
{
if (attribute is CommandLineAttribue)
{
CommandLineAttribue commandlineAttribute = attribute as CommandLineAttribue;
if (commandlineAttribute != null)
{
return commandlineAttribute.UsageName;
}
}
}
return string.Empty;
}
}
}

Hope this helps...feel free to share your thoughts....

Have fun !!!!!