Attributed Programming in .NET Using C#
Summary
An attribute is a new code level languageconstruct in all major .NET languages. It provides integration ofdeclarative information to assemblies, classes, interfaces, members,etc. at the code level. The information can then be used to change theruntime behavior or collect organizational information. In thisarticle, I illustrate the power of attributed programming by examplesthat show a modular approach to issues that can crosscut many classes.Attributes will provide exciting software development abstractions inthe future. It is a major contribution to programming language elementsand it opens up new ways to solve many software development problemsthat do not have elegant solutions.
Introduction
An attribute is a powerful .NET languagefeature that is attached to a target programming element (e.g., aclass, method, assembly, interface, etc.) to customize behaviors orextract organizational information of the target at design, compile, orruntime. The paradigm of attributed programming first appeared in theInterface Definition Language(IDL) of COM interfaces. Microsoft extended the concept to TransactionServer (MTS) and used it heavily in COM+.It is a clean approach to associate metadata with program elements andlater use the metadata at design, compile orrun time to accomplish some common objectives. In .NET, Microsoft wenta step further by allowing theimplementation of attributes in the source code, unlike theimplementation in MTS and COM+ where attributes weredefined in a separate repository. To understand the power ofattributes, consider the serialization of an object. In.NET, you just need to mark a class Serializable to make its membervariables as Serializable. For example:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
[Serializable]
public class User{
public string userID;
public string password;
public string email;
public string city;
public void Save(string fileName){
FileStream s=new FileStream(fileName,FileMode.Create);
SoapFormatter sf=new SoapFormatter();
sf.Serialize(s,this);
}
static void Main(string[] args){
User u=new User();
u.userID="firstName";
u.password="Zxfd12Qs";
u.email="asdf@qwer.com";
u.city="TheCity";
u.Save("user.txt");
}
}
Note: You may have to Add a reference to assembly System.Runtime.Serialization.Formatters.Soap.dll
The above example illustrates the power of attributes. We do not have to tell what to serialize; we just need to markthe class as serializable by annotating the class with Serializable attribute. Of course, we need to tell the serializationformat (as in the Save method).
Intrinsic and Custom Attributes
.NET framework is littered with attributes and CLR (common language runtime) provides a set of intrinsic attributesthat are integrated into the framework. Serializable is an example of intrinsic attribute. Besides the frameworksupplied attributes, you can also define your own custom attributes to accomplish your goal. When do you defineyour custom attributes? Attributes are suitable when you have crosscutting concerns. Object Oriented (OO)methodology lacks a modular approach to address crosscutting concerns in objects. Serialization is an example ofcrosscutting concern. Any object can be either serializable or non-serializable. If, for example, halfway in thedevelopment phase you realize that you need to make a few classes serializable, how do you do that? In .NET, youonly need to mark them as serializable and provide methods to implement a serialization format. Other crosscuttingconcerns can be security, program monitoring and recording during debugging, data validation, etc. If you have aconcern that affects a number of unrelated classes, you have a crosscutting concern and attributes are excellentcandidates to address crosscutting concerns.
Attribute Targets and Specifications
All .NET programming elements (assemblies, classes, interfaces, delegates, events, methods, members, enum, struct,and so forth) can be targets of attributes. When they are specified at global scope for an assembly or module, theyshould be placed immediately after all using statements and before any code. Attributes are placed in squarebrackets by immediately placing them before their targets, as in
[WebMethod] In the absence of any target-specifier, the target of the above attribute defaults to the method it is applied to(CapitalCity). However, for global scoped attributes, the target-specifier must be explicitly specifed, as in [assembly:CLSCompliant(true)] Multiple attributes can be applied to a target by stacking one on top of another, or by placing them inside a squarebracket and then separating adjacent attributes by commas. Attributes are classes (we will discuss that later) and as such are able to accept parameters in their specifications (likeclass constructor). There are two types of parameters, positional and named, that attributes accept in their usage.Positional parameters are like constructor arguments and their signature should match one of the constructors of theattribute class. For example, [assembly:CLSCompliant(true)] In the above example, CLSCompliant attribute accepts a boolean parameter in one of its constructors and it shouldbe used with a boolean parameter. Positional parameters are always set through constructors of the attribute. Named parameters are defined as non-static property in the attribute class declaration. They are optional and, whenused, their names should exactly match the name of the property defined in the attribute class declaration. Forexample, [WebMethod(EnableSession=true)] In the above attribute usage, EnableSession is a named parameter and it is optional. It also tells us that WebMethodattribute has a property called EnableSession. Implementing a Custom Attribute The power of attributes can be further extended by implementing your own custom attributes. In this section, we willdiscuss the implementation of a custom attribute to restrict the length of the member fields in the User class declaredabove. This will illustrate how to define a custom attribute and then use reflection on the attribute target toaccomplish our goal. Before defining the custom attribute, let usdiscuss how do we accomplish our goal without using any attribute. Wewant to restrict the userID and password fields between four and eightcharacters and e-mail to a minimum of fourcharacters and a maximum of 60 characters. There is no restriction oncity; it can even be null. Also, we want tovalidate the fields before they are serialized, and if one or morefields are invalid, according to our validation criteria, we want toabandon serialization and display a message to the user informinghim/her th e field( Let us say that we have defined an attribute, ValidLength. The attribute accepts two positional parameters forminimum and maximum length, and an optional named parameter for the message that will be displayed to the user. Ifno value for the optional parameter is supplied, we will display a generic message to the user. Now, let us apply theattribute to User class as: using System; [Serializable] [ValidLength(4,8,Message="Password should be between 4 and 6 characters long")] [ValidLength(4,60)] public void Save(string fileName){ static void Main(string[] args){ As you can see above, in the redefined User class, userID, password, and email fields are annotated withValidLength attribute. To validate a User object, we pass the object to IsValid method of a Validator object. TheValidator class can now be used to validate an object of any class by calling the IsValid method. If the string typefields of that object are targets of ValidLength attribute, IsValid will return true or false depending on the parametersof ValidLength attributes. We have completely decoupled our validation codes from the class that requiresvalidation and the class where the validation is performed. A custom attribute class should be derived from the base class Attribute defined in System namespace and housedin mscorlib.dll assembly. By convention, name of an attribute class is postfixed with "Attribute" and the suffix"Attribute" can be dropped when the custom attribute is applied to a target. Using this convention, we define ourcustom attribute as: [AttributeUsage(AttributeTargets.Property|AttributeTargets.Field)] public ValidLengthAttribute(int min,int max){ public string Message{ public string Min{ public string Max{ public bool IsValid(string theValue){ The custom attribute definition is mostly self-explanatory, however, we will discuss a few things before we proceedto define our Validator class. Like any other class, a custom attribute class can be a target of other attributes as wehave in the definition above. The attribute AttributeUsage specifies the type of targets the attribute can be appliedto. A custom attribute class should be public. By default, the custom attribute defined above can only be used onceper target. The Validator class to validate an object is defined as: public class Validator{ public bool IsValid(object anObject){ private bool isValidField(FieldInfo aField,object anObject){ private bool isValidField(FieldInfo aField, object anObject,ValidLengthAttribute anAttr){ private void addMessages(FieldInfo aField,ValidLengthAttribute anAttr){
public string CapitalCity(string country){
//code to return capital city of a country
}
s) that is/are invalid. To accomplish this goal, weneed a class, Validator, with a method, IsValid, and we need to callthis method, before running the serialization code, for each field thatrequires validation. Each time we add a field, requiring validation, tothe class, we have to add codes for its validation. Also, if we declareother classes with fields that require similarvalidation, we have to duplicate codes to validate each field of everyclass. So, field validation is our crosscuttingconcern and the use of a simple Validator class does not provide aclean, modular approach to address this concern.We will see that an attribute, along with the Validator class, willprovide a cleaner approach to our validationconcern.
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Reflection;
using System.Collections;
public class User{
[ValidLength(4,8,Message="UserID should be between 4 and 8 characters long")]
public string userID;
public string password;
public string email;
public string city;
FileStream s=new FileStream(fileName,FileMode.Create);
SoapFormatter sf=new SoapFormatter();
sf.Serialize(s,this);
}
User u=new User();
u.userID="first";
u.password="Zxfd12Qs";
u.email=".com";
u.city="";
Validator v=new Validator();
if(!v.IsValid(u)){
foreach(string message in v.Messages)
Console.WriteLine(message);
}
else {u.Save("user.txt");}
}
}
public class ValidLengthAttribute : Attribute{
private int _min;
private int _max;
private string _message;
_min=min;
_max=max;
}
get {return(_message);}
set {_message=value;}
}
get{return _min.ToString();}
}
get{return _max.ToString();}
}
int length=theValue.Length;
if(length >= _min && length <= _max) return true;
return false;
}
}
public ArrayList Messages=new ArrayList();
bool isValid=true;
FieldInfo[] fields = anObject.GetType().GetFields(BindingFlags.Public|BindingFlags.Instance);
foreach (FieldInfo field in fields)
if(!isValidField(field,anObject)) isValid=false;
return isValid;
}
object[] attributes=aField.GetCustomAttributes(typeof(ValidLengthAttribute),true);
if(attributes.GetLength(0) ==0) return true;
return isValidField(aField,anObject,(ValidLengthAttribute)attributes[0]);
}
string theValue=(string)aField.GetValue(anObject);
if (anAttr.IsValid(theValue)) return true;
addMessages(aField,anAttr);
return false;
}
if(anAttr.Message !=null){
Messages.Add(anAttr.Message);
return;
}
Messages.Add("Invalid range for "+aField.Name+". Valid range is between "+anAttr.Min+" and "+anAttr.Max);
}
}
The Validator class uses reflection classes to validate the object passed as a parameter to its IsValid method. First, itextracts all the public fields in the object using GetType().GetFields(BindingFlags.Public|BindingFlags.Instance)method. For each field, it extracts the custom attribute of type ValidLengthAttribute usingGetCustomAttributes(typeof(ValidLengthAttribute),true). If it does not find our custom attribute for a field, itassumes the field to be valid. If it finds our custom attribute for a field, it calls the IsValid method ofValidLengthAttribute to validate the value of the field.
Under the Hood
What does exactly happen to our custom attribute when the compiler compiles the class User? The simpleexplanation goes like this: when the compiler encounters the ValidLength specification in class User, it looks for aclass ValidLength but it can find one. It then searches for a class ValidLengthAttribute and it finds one. Next, thecompiler verifies if the target of ValidLengthAttribute is valid. It then verifies if there is a constructor whosesignature matches the parameters used in the attribute specification. If a named parameter is used, it also verifies theexistence of field or property by that name. The compiler also verifies if it is able to create an object ofValidLengthAttribute class. If no error is encountered, the attribute parameter values are stored along with othermetadata information of the class.












No comments yet... Be the first to leave a reply!