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:
Imports
System
Imports
System.IO
Imports
System.Runtime.Serialization.Formatters.Soap
<Serializable()
Public
Class User
Public userID
As String
Public password As
String
Public email As
String
Public city As
String
Public
Sub Save(ByVal
fileName As
String)
Dim s As
New FileStream(fileName, FileMode.Create)
Dim sf As
New SoapFormatter
sf.Serialize(s, Me)
End Sub
'Save
'Entry point which
delegates to C-style main Private Function
Public
Overloads
Shared Sub Main()
Main(System.Environment.GetCommandLineArgs())
End
Sub
Overloads
Shared Sub
Main(ByVal args()
As String)
Dim u As
New User
u.userID = "firstName"
u.password = "Zxfd12Qs"
u.email = [email protected]
u.city = "TheCity"
u.Save("user.txt")
End Sub
'Main
End
Class
'User
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
Public<WebMethod()>
Function CapitalCity(country As String) As String
End Function 'CapitalCity
'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:
Imports
System
Imports
System.IO
Imports
System.Runtime.Serialization.Formatters.Soap
Imports
System.Reflection
Imports
System.Collections
<Serializable()>
Public
Class
User<ValidLength(4, 8, Message := "UserID should be between 4 and 8 characters
long")>
Public
userID As
String<ValidLength(4, 8,
Message := "Password should be between 4 and 8 characters long")>
Public
password As
String<ValidLength(4,
60)>
Public
email As
String
Public city As
String
Public
Sub Save(ByVal
fileName As
String)
Dim
s As
New
FileStream(fileName, FileMode.Create)
Dim
sf As
New
SoapFormatter
sf.Serialize(s, Me)
End
Sub
'Save
'Entry point which
delegates to C-style main Private Function
Public
Overloads
Shared
Sub
Main()
Main(System.Environment.GetCommandLineArgs())
End
Sub
Overloads
Shared
Sub Main(ByVal
args() As
String)
Dim
u As
New
User
u.userID = "first"
u.password = "Zxfd12Qs"
u.email = ".com"
u.city = ""
Dim
v As
New
Validator
If
Not v.IsValid(u)
Then
Dim message As
String
For Each
message In
v.Messages
Console.WriteLine(message)
Next
message
Else
u.Save("user.txt")
End
If
End Sub
'Main
End
Class
'User
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
Or
AttributeTargets.Field)>
Public
Class
ValidLengthAttribute
Inherits
Attribute
Private
_min As
Integer
Private _max As
Integer
Private _message As
String
Public
Sub
New(ByVal
min As
Integer,
ByVal max
As
Integer)
_min = min
_max = max
End
Sub
'New
Public
Property Message()
As
String
Get
Return _message
End
Get
Set(ByVal
Value As
String)
_message = value
End
Set
End Property
Public ReadOnly
Property Min()
As
String
Get
Return _min.ToString()
End
Get
End Property
Public ReadOnly
Property Max()
As
String
Get
Return _max.ToString()
End
Get
End Property
Public Function
IsValid(ByVal
theValue As
String)
As
Boolean
Dim length As
Integer
= theValue.Length
If
length >= _min And
length <= _max Then
Return True
End If
Return False
End Function
'IsValid
End
Class
'ValidLengthAttribute
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
Messages As
New
ArrayList
Public
Function IsValid(ByVal
anObject As
Object)
As
Boolean
Dim isValid As
Boolean =
True
Dim fields As
FieldInfo() = anObject.GetType().GetFields((BindingFlags.Public
Or
BindingFlags.Instance))
Dim
field As
FieldInfo
For
Each field
In
fields
If
Not
isValidField(field, anObject) Then
isValid =
False
End
If
Next
field
Return
isValid
End
Function
'IsValid
Private
Overloads
Function isValidField(ByVal
aField As FieldInfo,
ByVal anObject
As
Object)
As
Boolean
Dim attributes As
Object() =
aField.GetCustomAttributes(GetType(ValidLengthAttribute),
True)
If
attributes.GetLength(0) = 0 Then
Return True
End If
Return isValidField(aField, anObject,
CType(attributes(0),
ValidLengthAttribute))
End
Function
'isValidField
Private
Overloads
Function isValidField(ByVal
aField As FieldInfo,
ByVal anObject
As
Object,
ByVal anAttr
As ValidLengthAttribute)
As
Boolean
Dim theValue As
String =
CStr(aField.GetValue(anObject))
If
anAttr.IsValid(theValue) Then
Return True
End If
addMessages(aField, anAttr)
Return
False
End Function
'isValidField
Private
Sub addMessages(ByVal
aField As FieldInfo,
ByVal anAttr
As
ValidLengthAttribute)
If
Not (anAttr.Message
Is
Nothing)
Then
Messages.Add(anAttr.Message)
Return
End
If
Messages.Add(("Invalid range for "
+ aField.Name + ". Valid range is between " + anAttr.Min + " and " + anAttr.Max))
End
Sub
'addMessages
End
Class
'Validator
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.