Attributed Programming in .NET using C#


Summary

An attribute is a new code level language construct in all major .NET languages. It provides integration of declarative information to assemblies, classes, interfaces, members, etc. at the code level. The information can then be used to change the runtime behavior or collect organizational information. In this article, I illustrate the power of attributed programming by examples that show a modular approach to issues that can crosscut many classes. Attributes will provide exciting software development abstractions in the future. It is a major contribution to programming language elements and it opens up new ways to solve many software development problems that do not have elegant solutions.

Introduction

An attribute is a powerful .NET language feature that is attached to a target programming element (e.g., a class, method, assembly, interface, etc.) to customize behaviors or extract organizational information of the target at design, compile, or runtime. The paradigm of attributed programming first appeared in the Interface Definition Language (IDL) of COM interfaces. Microsoft extended the concept to Transaction Server (MTS) and used it heavily in COM+. It is a clean approach to associate metadata with program elements and later use the metadata at design, compile or run time to accomplish some common objectives. In .NET, Microsoft went a step further by allowing the implementation of attributes in the source code, unlike the implementation in MTS and COM+ where attributes were defined in a separate repository. To understand the power of attributes, consider the serialization of an object. In .NET, you just need to mark a class Serializable to make its member variables 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";
[email protected];
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 mark the class as serializable by annotating the class with Serializable attribute. Of course, we need to tell the serialization format (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 attributes that are integrated into the framework. Serializable is an example of intrinsic attribute. Besides the framework supplied attributes, you can also define your own custom attributes to accomplish your goal. When do you define your 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 of crosscutting concern. Any object can be either serializable or non-serializable. If, for example, halfway in the development phase you realize that you need to make a few classes serializable, how do you do that? In .NET, you only need to mark them as serializable and provide methods to implement a serialization format. Other crosscutting concerns can be security, program monitoring and recording during debugging, data validation, etc. If you have a concern that affects a number of unrelated classes, you have a crosscutting concern and attributes are excellent candidates 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, they should be placed immediately after all using statements and before any code. Attributes are placed in square brackets by immediately placing them before their targets, as in

[WebMethod]

public string CapitalCity(string country)
{
//code to return capital city of a country
}

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 square bracket 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 (like class 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 the attribute class. For example,

[assembly:CLSCompliant(true)]

In the above example, CLSCompliant attribute accepts a boolean parameter in one of its constructors and it should be 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, when used, their names should exactly match the name of the property defined in the attribute class declaration. For example,

[WebMethod(EnableSession=true)]

In the above attribute usage, EnableSession is a named parameter and it is optional. It also tells us that WebMethod attribute 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 will discuss the implementation of a custom attribute to restrict the length of the member fields in the User class declared above. This will illustrate how to define a custom attribute and then use reflection on the attribute target to accomplish our goal.

Before defining the custom attribute, let us discuss how do we accomplish our goal without using any attribute. We want to restrict the userID and password fields between four and eight characters and e-mail to a minimum of four characters and a maximum of 60 characters. There is no restriction on city; it can even be null. Also, we want to validate the fields before they are serialized, and if one or more fields are invalid, according to our validation criteria, we want to abandon serialization and display a message to the user informing him/her the field(s) that is/are invalid. To accomplish this goal, we need a class, Validator, with a method, IsValid, and we need to call this method, before running the serialization code, for each field that requires validation. Each time we add a field, requiring validation, to the class, we have to add codes for its validation. Also, if we declare other classes with fields that require similar validation, we have to duplicate codes to validate each field of every class. So, field validation is our crosscutting concern and the use of a simple Validator class does not provide a clean, modular approach to address this concern. We will see that an attribute, along with the Validator class, will provide a cleaner approach to our validation concern.

Let us say that we have defined an attribute, ValidLength. The attribute accepts two positional parameters for minimum and maximum length, and an optional named parameter for the message that will be displayed to the user. If no value for the optional parameter is supplied, we will display a generic message to the user. Now, let us apply the attribute to User class as:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;
using System.Reflection;
using System.Collections;
[Serializable]
public class User
{
[ValidLength(4,8,Message="UserID should be between 4 and 8 characters long")]
public string userID;
[ValidLength(4,8,Message="Password should be between 4 and 8 characters long")]
public string password;
[ValidLength(4,60)]
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="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");}
}
}

As you can see above, in the redefined User class, userID, password, and email fields are annotated with ValidLength attribute. To validate a User object, we pass the object to IsValid method of a Validator object. The Validator class can now be used to validate an object of any class by calling the IsValid method. If the string type fields of that object are targets of ValidLength attribute, IsValid will return true or false depending on the parameters of ValidLength attributes. We have completely decoupled our validation codes from the class that requires validation 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 housed in 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 our custom attribute as:

[AttributeUsage(AttributeTargets.Property|AttributeTargets.Field)]

public class ValidLengthAttribute : Attribute
{
private int _min;
private int _max;
private string _message;
public ValidLengthAttribute(int min,int max)
{
_min=min;
_max=max;
}
public string Message
{
get {return(_message);}
set {_message=value;}
}
public string Min
{
get{return _min.ToString();}
}
public string Max
{
get{return _max.ToString();}
}
public bool IsValid(string theValue)
{
int length=theValue.Length;
if(length >= _min && length <= _max) return true;
return false;
}
}

The custom attribute definition is mostly self-explanatory, however, we will discuss a few things before we proceed to define our Validator class. Like any other class, a custom attribute class can be a target of other attributes as we have in the definition above. The attribute AttributeUsage specifies the type of targets the attribute can be applied to. A custom attribute class should be public. By default, the custom attribute defined above can only be used once per target.

The Validator class to validate an object is defined as:

public class Validator
{
public ArrayList Messages=new ArrayList();
public bool IsValid(object anObject)
{
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;
}
private bool isValidField(FieldInfo aField,object anObject)
{
object[] attributes=aField.GetCustomAttributes(typeof(ValidLengthAttribute),true);
if(attributes.GetLength(0) ==0) return true;
return isValidField(aField,anObject,(ValidLengthAttribute)attributes[0]);
}
private bool isValidField(FieldInfo aField, object anObject,ValidLengthAttribute anAttr)
{
string theValue=(string)aField.GetValue(anObject);
if (anAttr.IsValid(theValue)) return true;
addMessages(aField,anAttr);
return false;
}
private void addMessages(FieldInfo aField,ValidLengthAttribute anAttr)
{
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, it extracts 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 using GetCustomAttributes(typeof(ValidLengthAttribute),true). If it does not find our custom attribute for a field, it assumes the field to be valid. If it finds our custom attribute for a field, it calls the IsValid method of ValidLengthAttribute 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 simple explanation goes like this: when the compiler encounters the ValidLength specification in class User, it looks for a class ValidLength but it can find one. It then searches for a class ValidLengthAttribute and it finds one. Next, the compiler verifies if the target of ValidLengthAttribute is valid. It then verifies if there is a constructor whose signature matches the parameters used in the attribute specification. If a named parameter is used, it also verifies the existence of field or property by that name. The compiler also verifies if it is able to create an object of ValidLengthAttribute class. If no error is encountered, the attribute parameter values are stored along with other metadata information of the class.

Up Next
    Ebook Download
    View all
    Learn
    View all