Implementing Custom Attributes in .NET
Visual Studio .Net creates assemblies. An assembly is a combination of metadata and a portable executable. Unlike applications built in earlier versions of Visual Studio, Visual Studio .Net combines extra information necessary to describe the executable. This extra information is the metadata, and is the technical feature that supports xcopy deployment of Visual Basic .Net assemblies. It is the Attribute class that allows you to add metadata to your assemblies.
The Attribute class is defined in the System namespace. Because Attribute is a class, you can implement new attributes by subclassing the System.Attribute class. Implementing new attribute classes is a way for you to effectively extend Visual Studio .Net without needing or having access to the Visual Studio .Net or Visual Basic .Net source code. This is an important concept, so I will repeat it. Attributes allows you to add metadata to your assemblies, and implementing custom attributes allows you to extended .Net. The best part is that everything you know about object-oriented programming in Visual Basic .Net can be leveraged to create new attributes. If you know how to use inheritance, define classes, and use attributes then you can create custom attributes.
A custom attribute is a class that inherits from the System.Attribute class or a subclass of System.Attribute. Custom attributes are created when you want to extend the metadata that is added to your applications. To this end, the example in this article implements the AuthorAttribute class step-by-step.
Using the AttributeUsageAttribute
When you create custom attributes you need to use the AttributeUsageAttribute on your attribute. The AttributeUsageAttribute specifies the entities that an attribute is valid on. (When you specify the AttributeUsageAttribute you are providing a value for the AttributeUsageAttribute.ValidOn field.)
The AttributeUsageAttribute Sub New constructor procedure header is defined as follows:
Public Sub New(ByVal ValidOn As AttributeTargets)
AttributeTargets is an enumeration. For example, you can use AttributeTargets.All to indicate that your attribute can be applied to all entities. If you initialize the AttributeUsageAttribute with AttributeTargets.Class then the attribute may only be applied to class entities. Because our custom attribute will be applied to all entities-that is, we want to be able to indicate that all elements of a program can be marked with author information-we will use the AttributeTargets.All value to initialize the AttributeUsageAttribute that will be applied to our custom attribute. The AttributeUsageAttribute statement, as described thus far, would be added as follows:
(By convention we drop the Attribute suffix when we apply an attribute; hence AttributeUsageAttribute is typed as AttributeUsage.)
As a design choice we will elect to allow users of our attribute to apply the attribute multiple times. This reflects the case where code has multiple authors. AllowMultiple is a property in the AttributeUsageAttribute class. We must initialize properties in the attribute statement. To initialize properties we used the named argument syntax to specify the value of properties. Indicating that our as yet defined custom attribute, AuthorAttribute, can be applied more than once to a single entity we incorporate the AllowMultiple named argument into our AttributeUSageAttribute statement.
Generalize the System.Attribute
The cat is out of the bag (a little Halloween humor). Our custom attribute annotates code with author information; consequently we will name the class AuthorAttribute. To keep the attribute functional without too much overhead, I chose to store the author's email address and indicate whether the code has been refactored. (We will come back to the implementation of the class in a few minutes.)
Based on the information assembled this far we can add the class shell to our custom attribute.
<Attribute(AttributeTargets.All, AllowMultiple:=True)> _ Public Class AuthorAttribute End Class
Classes can be added to any module. If we want to reuse the AuthorAttribute in other applications then it is best implemented in a Class Library project, but this is not an enforced requirement.
Attributes must inherit from the System.Inherit class, which is the base class for all attributes. You can incorporate the generalization statement to the class now, too.
<Attribute(AttributeTargets.All, AllowMultiple:=True)> _ Public Class AuthorAttribute Inherits System.Attribute End Class
Implement the Constructor
The constructor supports initializing an instance of a class. Attributes classes are no different in this regard. If you need to initialize an attribute class then you will want to implement a constructor. Our attribute class initializes the AuthorEmail information, as a result we will implement our custom attribute with the author's email information. Here is the class with the new constructor.
<Attribute(AttributeTargets.All, AllowMultiple:=True)> _ Public Class AuthorAttribute Inherits System.Attribute Public Sub New(ByVal AuthorEmail As String) MyBase.New() FAuthorEmail AS AuthorEmail End Sub End Class
Earlier we elected to indicate if a specific class had been Refactored or not. Combined with the obvious FAuthorEmail field introduced in the constructor we can modify our custom attribute further by adding the supporting fields and properties for marking a class as Refactored.
AuthorEmail is initialized and cannot change. However we do want to be able to query the author's email address using Reflection. We will need to add a ReadOnly property and a field to support the AuthorEmail information. The Refactored information is implemented as a modifiable property that we can initialize using the named argument syntax or at a later time using Reflection. Updating the AuthorAttribute class to support these requirements yields the final implementation of the AuthroAttribute class (in listing 1).
Listing 1: A custom attribute that stores code-author email and Refactored-state information.
<AttributeUsageAttribute( _ AttributeTargets.All, AllowMultiple:=True)> _ Public Class AuthorAttribute Inherits System.Attribute Private FAuthorEmail As String Public ReadOnly Property AuthorEmail() As String Get Return FAuthorEmail End Get End Property Public Sub New(ByVal AuthorEMail As String) MyBase.New() FAuthorEmail = AuthorEMail FRefactored = False End Sub Private FRefactored As Boolean Public Property Refactored() As Boolean Get Return FRefactored End Get Set(ByVal Value As Boolean) FRefactored = Value End Set End Property End Class
The field FRefactored is initialized to False. If you use the named argument syntax, you can change this property when the attribute is applied. As you can quickly determine from the code the author information can not be changed unless the attribute is reapplied because the AuthorEmail property is ReadOnly.
The Refactored property is readable and writable. Refactoring is an outgrowth of Extreme Programming, and is a study and process that describes how to improve the design of code. We will not digress into Refactoring, but is clear that this property can be modified such as might be done by a tool.
Test Custom Attribute
The last step is to test the new attribute. Assuming the AuthorAttribute is included in a test application or a class library you can test the AuthorAttribute class by applying the attribute to a class and by creating an instance of the attribute class using Reflection. The following fragment demonstrates an application of the AuthorAttribute to a Form class.
<Author("email@example.com", Refactored:=False)> _ Public Class Form1 Inherits System.Windows.Forms.Form ...
The second example demonstrates how to manipulate the attribute programmatically using Reflection. (The following code is implemented in the same form as the one that the attribute is applied to. You can add a breakpoint to the attribute's constructor to debug the AuthorAttribute.)
Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Dim Attributes As Object() = _ Me.GetType().GetCustomAttributes(GetType(AuthorAttribute), False) If (Attributes.Length = 0) Then Exit Sub MsgBox(CType(Attributes(0), AuthorAttribute).AuthorEmail) End Sub
The first statement requests an array of custom attributes from the Type information object of the form this code resides in. GetCustomAttribute(GetType(AuthorAttribute), False) returns an array of AuthorAttribute objects. The if...then check exits the sub if the number of objects returned is 0, and the generic array of objects named Attributes is typecast to the AuthorAttribute class. Finally the AuthorEmail property is displayed in a MsgBox.
Implementing custom attributes to extend the kind of metadata you may incorporate into your applications, and using Reflection to discover that additional metadata, is a powerful way to extend .Net and add new capabilities to your solutions.
About the Author
Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for cool Visual Basic .Net topics in his upcoming book Visual Basic .Net Unleashed.
Paul founded Software Conceptions, Inc. in 1990. Contact Paul Kimmel at firstname.lastname@example.org for help building VB.NET applications or migrating VB6 applications to .NET.