9.3. Using a Custom AttributeThe attribute classes included with .NET already have specific uses. Visual Studio and the various .NET compilers take specific actions based on these attributes, such as making a method available on the Internet through the <WebMethod> attribute. But no code yet exists to use the <DeveloperNote> attribute designed above. Not only must you define the custom attribute, you must also develop routines that will identify the attribute and take action as needed. All attribute properties are stored in .NET assemblies as metadata. This metadata can be accessed programmatically at runtime by using the .NET Framework's reflection classes.
The .NET Framework provides support for reflection through the System.Type class and through the features found in the System.Reflection namespace. The following code creates a console mode application that uses reflection to extract <DeveloperNote> attribute details from an assembly that uses this custom attribute. Option Strict On Imports Microsoft.VisualBasic Imports System Imports System.Reflection Imports System.Text ' ----- We placed the <DeveloperNote> attribute in ' the Extensions.CustomAttributes namespace. Imports Extensions.CustomAttributes Module modComments Public Sub Main( ) ' ----- Report on <DeveloperNote> use in an assembly. Dim fileToExamine As String Dim outputText As String Dim assemblyView As System.Reflection.Assembly Dim attributeSet( ) As Attribute Dim moduleScan As System.Reflection.Module Dim moduleSet( ) As System.Reflection.Module ' ----- The assembly to examine comes through the command line. fileToExamine = Command( ) If (fileToExamine = "") Then Console.WriteLine("Syntax is:" & vbCrLf & _ " DevNotes <filename>") Exit Sub End If ' ----- Load the assembly through reflection. assemblyView = Reflection.Assembly.LoadFrom(fileToExamine) ' ----- Output information on assembly-level attributes. attributeSet = Attribute.GetCustomAttributes(assemblyView) If (attributeSet.Length > 0) Then outputText = PrepareDeveloperNotes(attributeSet) If (outputText <> "") Then Console.WriteLine(assemblyView.GetName.Name & _ " Assembly Developer Notes:") Console.WriteLine(outputText) End If End If ' ----- Output information on module-level attributes. moduleSet = assemblyView.GetModules( ) For Each moduleScan In moduleSet attributeSet = Attribute.GetCustomAttributes(moduleScan) If (attributeSet.Length > 0) Then outputText = PrepareDeveloperNotes(attributeSet) If (outputText <> "") Then Console.WriteLine(moduleScan.Name & _ " Module Developer Notes:) Console.WriteLine(outputText) End If End If Next moduleScan ' ----- Output information on type-level attributes. EnumerateTypes(assemblyView) End Sub Public Function PrepareDeveloperNotes(attributeSet( ) As Object) _ As String ' ----- Format information about each attribute. Dim msg As New StringBuilder Dim attributeScan As Attribute Dim noteEntry As DeveloperNoteAttribute On Error Resume Next ' ----- Build the notes. For Each attributeScan In attributeSet If (TypeOf (attributeScan) Is DeveloperNoteAttribute) Then noteEntry = CType(attributeScan, DeveloperNoteAttribute) msg.Append(" Developer: " & noteEntry.Name & vbCrLf) msg.Append(" Comment: " & noteEntry.Comment & vbCrLf) msg.Append(" Date: " & noteEntry.DateRecorded & vbCrLf) msg.Append(" Bug: " & noteEntry.Bug & vbCrLf) End If Next attributeScan ' ----- Return the results as an ordinary string. Return msg.ToString End Function Private Sub EnumerateTypes(assemblyView As Reflection.Assembly) ' ----- Process each type in the entire assembly. Dim typeScan As Type Dim typeSet( ) As Type Dim typeCategory As String Dim attributeSet( ) As Object Dim attributeMsg As String Dim methodMsg As String ' ----- Retrieve the types for this assembly. typeSet = assemblyView.GetTypes( ) ' ----- Get a friendly name for the type category. For Each typeScan In typeSet If typeScan.IsClass Then typeCategory = "Class" ElseIf typeScan.IsValueType Then typeCategory = "Structure" ElseIf typeScan.IsInterface Then typeCategory = "Interface" ElseIf typeScan.IsEnum Then typeCategory = "Enum" Else typeCategory = ".NET Type" End If ' ----- Get any type-level attributes. attributeSet = typeScan.GetCustomAttributes(False) If (attributeSet.Length > 0) Then attributeMsg = PrepareDeveloperNotes(attributeSet) Else attributeMsg = "" End If ' ----- Get the details for this type's members. methodMsg = EnumerateTypeMembers(typeScan) ' ----- Display any collected information, if available. If (methodMsg <> "") Or (attributeMsg <> "") Then Console.WriteLine(typeCategory & " " & typeScan.Name & ":") If (attributeMsg <> "") Then _ Console.WriteLine(attributeMsg) If (methodMsg <> "") Then _ Console.WriteLine(methodMsg) End If Next typeScan End Sub Private Function EnumerateTypeMembers(typeEntry As Type) As String Dim memberInfo As String Dim fullInfo As String = "" Dim noteDetails As String Dim attributeSet( ) As Object Dim memberScan As MemberInfo Dim memberSet( ) As MemberInfo ' ----- Get members of the type. memberSet = typeEntry.GetMembers For Each memberScan In memberSet ' ----- Determine if any attributes are present. attributeSet = memberScan.GetCustomAttributes(False) If (attributeSet.Length > 0) Then ' ----- Determine the member type. Select Case memberScan.MemberType Case MemberTypes.All memberInfo = " All" Case MemberTypes.Constructor memberInfo = " Constructor" Case MemberTypes.Custom memberInfo = " Custom method" Case MemberTypes.Event memberInfo = " Event" Case MemberTypes.Field memberInfo = " Field" Case MemberTypes.Method memberInfo = " Method" Case MemberTypes.NestedType memberInfo = " Nested type" Case MemberTypes.Property memberInfo = " Property" Case MemberTypes.TypeInfo memberInfo = " TypeInfo" Case Else memberInfo = " Member" End Select ' ----- Add in the name of the member. If (memberScan.Name = ".ctor") Then ' ----- Constructor. memberInfo = "New" & memberInfo Else memberInfo = memberScan.Name & memberInfo End If ' ----- Get the note details. noteDetails = PrepareDeveloperNotes(attributeSet) If (noteDetails <> "") Then _ fullInfo &= memberInfo & vbCrLf & noteDetails & vbCrLf End If Next memberScan ' ----- Fully formatted and ready to use. Return fullInfo End Function End Module This program scans an assembly, examining almost everything that can have attributes attached. If it finds attributes attached to an item, it loops through them looking for any that use the <DeveloperNote> attribute. If a match is found, it prints the name of the item and the related developer note details to the console. The program's entry point, Main, first instantiates an Assembly object (from the System.Reflection namespace) representing the assembly identified on the command line. It then calls the System.Attribute's shared GetCustomAttributes method to obtain any attributes associated with the assembly itself. If any exist, they are passed to the PrepareDeveloperNotes method, which looks specifically for DeveloperNoteAttribute entries and formats them for printing. Back in Main, the same process is done for each module contained within the assembly by calling the assembly's GetModules method. The final task displays the developer notes for each type in the assembly. Since the logic is somewhat different, it's all done in the EnumerateTypes routine. This routine gets all the types for the entire assembly through the assembly's GetTypes method. Then it scans each type, checking for associated attributes. If it finds them, it documents any developer notes (once again through the PrepareDeveloperNotes function). Since each type contains members that, in turn, can have <DeveloperNote> attributes, those are displayed as well through the EnumerateTypeMembers routine. This routine is not that different in its overall structure from the EnumerateTypes routine, but there is some interesting code used in the formatting of the member name. If the memberScan object represents a constructor, ".ctor" is used for the member name. The routine converts this to the more user-friendly "New." The program provides a good overview of attribute analysis, although it could be enhanced even more. The EnumerateTypes routine could be made a little more generic, and recursion added, allowing it to display attribute information found in nested classes. Another reasonable enhancement would display developer notes associated with parameters belonging to individual methods. |