O'Reilly Book Excerpts: VB .NET Language in a Nutshell
Attributesby Steven Roman, Ron Petrusha, Paul Lomax
Using a Custom Attribute
This is the third and final part of this excerpt from O'Reilly's VB .NET Language in a Nutshell, Chapter 8, "Attributes." This article focuses on custom attributes.
The Visual Basic compiler and .NET platform automatically recognize the meaning of the attributes based on attribute classes in the .NET Framework Class Library. This recognition isn't true, however, for custom attributes. Thus, not only must you define them, you must also develop a set of routines that will identify the presence of an attribute so your code can handle them.
NET assemblies are self-describing; when the compiler creates the .NET assembly, it writes metadata describing the assembly and its classes and methods to the assembly manifest. This metadata is then accessed programmatically at runtime by using the .NET Framework's reflection classes.
TIP: An assembly's metadata is similar to a COM type library. In addition to their greater accessibility through .NET Framework APIs, assembly metadata is always stored along with the assembly. In contrast, although a type library can be stored in the EXE or DLL containing the COM object (as did previous versions of Visual Basic), it is most commonly stored in a file different from the file containing the COM objects it describes.
Previously in the Series
The .NET Framework provides support for reflection in the Type class (in the System namespace) and in the types found in the System.Reflection namespace. The following code creates a console mode application that uses the reflection classes to extract information about the
<DeveloperNote> custom attribute and the program elements to which it is applied:
Option Strict On Imports Microsoft.VisualBasic Imports System Imports System.Reflection Imports System.Text Imports Extensions.CustomAttributes Module modComments Public Sub Main( ) Dim strFile As String = Command( ) Dim sOutput As String If strFile = "" Then Console.WriteLine("Syntax is: " & vbCrLf & _ " DevNotes <filename>") Exit Sub End If ' Load assembly Dim oAssem As System.Reflection.Assembly = _ System.Reflection.Assembly.LoadFrom(strFile) ' Get any assembly-level attributes Dim oAttribs( ) As Attribute = Attribute.GetCustomAttributes(oAssem) if UBound(oAttribs) >= 0 Then sOutput = DisplayDeveloperNotes(oAttribs) if sOutput <> "" Then Console.WriteLine(oAssem.GetName.Name & _ " Assembly Developer Notes:" & vbCrLf) Console.WriteLine(sOutput) End If End If ' Get any module-level attributes Dim oMod As System.Reflection.Module Dim oMods() As System.Reflection.Module = oAssem.GetModules( ) For Each oMod in oMods oAttribs = Attribute.GetCustomAttributes(oMod) If UBound(oAttribs) >= 0 Then sOutput = DisplayDeveloperNotes(oAttribs) If sOutput <> "" Then Console.WriteLine(oMod.Name & " Module Developer Notes: " _ & vbCrLf) Console.WriteLine(sOutput) End If End If Next ' Enumerate types EnumerateTypes(oAssem) End Sub ' Show information about each attribute Public Function DisplayDeveloperNotes(oAttribs( ) As Object) As String Dim sMsg As New StringBuilder Dim oAttrib As Attribute Dim oNote As DeveloperNoteAttribute For Each oAttrib in oAttribs Try oNote = CType(oAttrib, DeveloperNoteAttribute) sMsg.Append(" Developer: " & oNote.Name & vbCrLf) sMsg.Append(" Comment: " & oNote.Comment & vbCrLf) sMsg.Append(" Date: " & oNote.DateRecorded & vbCrLf) sMsg.Append(" Bug: " & oNote.Bug & vbCrLf) Catch ' No need to do anything End Try Next Return sMsg.ToString End Function Private Sub EnumerateTypes(oObj As Object) Dim sOutput As String Dim oType, oTypes( ) As Type If oObj.GetType.ToString = "System.Reflection.Assembly" Then Dim oAssem As System.Reflection.Assembly = CType(oObj, _ System.Reflection.Assembly) oTypes = oAssem.GetTypes( ) Else oTypes.SetValue(oObj, 0) End If For each oType in oTypes Dim strType, strTypeAttr, strMeth As String If oType.IsClass Then strType = "Class" ElseIf oType.IsValueType Then strType = "Structure" ElseIf oType.IsInterface Then strType = "Interface" ElseIf oType.IsEnum Then strType = "Enum" End If sOutput = strType & " " & oType.Name & ":" & vbCrLf ' Get any type-level attributes Dim oCustAttribs( ) As Object = oType.GetCustomAttributes(False) If oCustAttribs.Length > 0 Then strTypeAttr = DisplayDeveloperNotes(oCustAttribs) End If strMeth = EnumerateTypeMembers(oType) ' Display Type and Member Info If strMeth <> "" Or strTypeAttr <> "" Then Console.WriteLine(sOutput) If strTypeAttr <> "" Then Console.WriteLine(strTypeAttr) End If If strMeth <> "" Then Console.WriteLine(strMeth & vbCrLf) End If End If Next End Sub Private Function EnumerateTypeMembers(oType As Type) As String Dim strMeth, strRetVal As String Dim oAttribs( ) As Object ' Get members of type Dim oMembersInfo( ), oMemberInfo As MemberInfo oMembersInfo = oType.GetMembers For Each oMemberInfo in oMembersInfo ' Determine if attribute is present oAttribs = oMemberInfo.GetCustomAttributes(False) If oAttribs.Length > 0 Then ' determine member type Select Case oMemberInfo.MemberType Case MemberTypes.All strMeth = " All " Case MemberTypes.Constructor strMeth = " Constructor " Case MemberTypes.Custom strMeth = " Custom method " Case MemberTypes.Event strMeth = " Event " Case MemberTypes.Field strMeth = " Field " Case MemberTypes.Method strMeth = " Method " Case MemberTypes.NestedType strMeth = " Nested type " Case MemberTypes.Property strMeth = " Property" Case MemberTypes.TypeInfo strMeth = " TypeInfo" End Select If oMemberInfo.Name = ".ctor" Then strMeth = "New " & strMeth Else strMeth = oMemberInfo.Name & strMeth End If strMeth = strMeth & vbCrLf & DisplayDeveloperNotes(oAttribs) _ & vbCrLf strRetVal = strRetVal & strMeth End If Next Return strRetVal End Function End Module
The program's entry point, the Main routine, first instantiates an Assembly object (in the System.Reflection namespace) representing the assembly by calling the LoadFrom method and passing it the filename containing the assembly. It then calls the Attribute class' shared GetCustomAttributes method, passing it a reference to an Assembly object, which returns an array of Attribute objects representing each custom attribute, if any exist. These attributes are then displayed by calling the DisplayDeveloperNotes method.
The shared GetCustomAttributes method of the Attribute class has several overloads that allow you to retrieve custom attributes belonging to assemblies, modules, class members, and parameters. (Unfortunately, the method does not retrieve the custom attributes belonging to types.) Since derived classes call the base class implementation, you can also retrieve attributes of a specific custom type with the following code:
Dim oAttribs( ) As Attribute = _ DeveloperNoteAttribute.GetCustomAttributes(oAssem)
After listing any DeveloperNoteAttributes applied to the assembly, the code retrieves the modules in the assembly by calling the Assembly object's GetModules method, which returns an array of Module objects. The code then iterates these modules and again calls the Attribute class' shared GetCustomAttributes method, this time passing it a Module object (to retrieve an array of custom Attribute objects belonging to that module). These objects are also displayed by calling the DisplayDeveloperNotes method.
Finally, Main calls the EnumerateTypes method, a generic routine that it uses to iterate the types in the Assembly object. (The routine could also be called from a type to extract information about custom attributes in its nested types.) This iteration casts the generic object passed as a parameter to an Assembly object, and then calls the Assembly object's GetTypes method to return an array of Type objects (defined in the System namespace) containing information about each type (such as a class, interface, delegate, structure, or num) in the assembly. Each Type object's GetCustomAttributes method is then called and its custom attributes are displayed.
While iterating the type objects, the EnumerateTypes method also calls the EnumerateTypeMembers method, which is responsible for iterating the members of each type and extracting their custom DeveloperNoteAttribute attributes. The EnumerateTypeMembers method first extracts an array of MemberInfo objects corresponding to each member by calling the GetMembers method of oType, the Type object passed to it as a parameter. GetMembers returns an array of MemberInfo objects, each element of which corresponds to a member of the type. The method then calls the MemberInfo object's GetCustomAttributes method to extract information about any custom types. Instead, it could also have called the Attribute object's GetCustomAttributes method, passing it a MemberInfo object representing the member whose custom attribute information was to be retrieved.
The program can be easily extended by adding recursion (allowing it to retrieve information about custom attributes in a nested class and its members), as well as by retrieving information about custom attributes applied to parameters belonging to individual methods.
For more information on VB.NET in a Nutshell, visit the catalog page.