Attributes Programming in VB.NET

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.


Similar Articles