WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

Understanding Reflection, Part 2

by Nick Harrison
11/17/2003

Reflecting on Custom Attributes

In the previous article, we explored some of the basic objects used to make reflection work. We saw how to use reflection to find out all of the properties of a type, all of the methods of a type, all of the parameters of a method, and all of the types in an assembly. .NET makes it easy to access all of this wonderful information, but it doesn't stop there. Using custom attributes, we can define and reference metadata that the original .NET designers never imagined.

Custom attributes are classes that we derive from System.Attribute that will allow us to define additional information about types, methods, properties, etc. Essentially, these new classes allow us to set the values for properties when we add the attribute to our code, and then programmatically reference the values at run time using reflection.

Here we will define some custom attributes to help with commenting our code. This will allow us to retrieve our comments directly from the executable without having to have the actual source code. We will start by reviewing the basic mechanics of declaring custom attributes, and then we will declare some attributes that will be useful in meeting our documentation goals. Once we have created these new attributes, we will explore using them to add useful metadata to our code. Finally, we will explore how we might access and use this information.

Mechanics of Defining Custom Attributes

To define a custom attribute, we simply define a new class, derived from System.Attribute. In our new class, we will define any properties that we may want to reference at run time.

Before defining our new attribute, we need to use a built-in attribute provided just for defining attributes. The AttributeUsageAttribute is used to define when an attribute can be used, whether or not the attribute can be defined multiple times, and whether or not the attribute can be inherited. We will use this attribute to specify where our newly defined attributes can be used, that the attribute can be added multiple times to a single target, and that the attribute is not inherited. If we try using an attribute differently than defined by the AttributeUsageAttribute, we will get a compile-time error.

Use code similar to this to define an attribute:

[System.AttributeUsage(AttributeTargets.All, 
                       AllowMultiple=true, Inherited=false)]
public class SampleAttribute  : System.Attribute 
{
  public SampleAttribute() : base()
  {
      
  }
}

Or, in VB:

<AttributeUsage(AttributeTargets.All, _
                   AllowMultiple:=True, Inherited:=False)> _
Public Class SampleAttribute
  Inherits System.Attribute
  
  Public Sub New()
    myBase.New()
  End Sub
End Class

In both cases, we are defining a new attribute that can be used anywhere, takes no parameters, and exposes no properties. Not a very interesting attribute, but it illustrates the basic principles that every attribute we define will follow. Convention suggests that we end our attribute names with "Attribute." There is nothing to enforce this convention, but I recommend sticking with convention and keeping the word Attribute on the end of the names of attributes we define.

Defining Our Custom Attributes

We will be defining several custom attributes. We will define a FlowerBoxAttribute that can be used to provide the data that would commonly be included in a "flowerbox" comment at the start of a method. We will then define a similar ChangeLogAttribute that can be used to structure ChangeLog type entries. Finally, we will define a RequirementsTrackingAttribute that can be used to tie individual code features to specific functional requirements. We will follow the same basic steps used earlier, with just a couple of twists.

Custom attributes can be derived either directly or indirectly from System.Attribute. Looking at the attributes that we intend to define, we find that they potentially have a great deal in common. We can group all of these common features into a common base class, and then define the features that are different in the derived attribute classes as well as change the nature of the AttributeUsageAttribute.

Our base class will expose several properties that will be initialized through the constructor. It is possible to use "named" parameters to the constructor that will, in essence, invoke the set method for the property identified in the "name." This is handy in cases where a property's value is not required. For this example, we want to make sure that all of the elements of our comments are properly filled out, so we will require that they be set in the constructor.

The CommentBaseAttribute will look similar to this:

public abstract class CommentBaseAttribute : System.Attribute
{
  string _Programmer;
  string _MadeDate;
  string _Description;

  public CommentBaseAttribute (string Programmer, 
			   string MadeDate,
			   string Description)
  {
    _Programmer = Programmer;
    _MadeCreationDate = MadeCreationDate;
    _Description = Description;
  }
  public string Programmer {get { return _Programmer;}}
  public string MadeDate { get {return _MadeDate;}}
  public string Description {get {return _Description;}}
}

Or, in VB:

Public Class CommentAttribute
  Inherits System.Attribute
  
  Dim _Programmer As String
  Dim _MadeDate As String
  Dim _Description As String
  
  Public Sub New(ByVal Programmer As String, _
                 ByVal MadeDate As String, _
                 ByVal Description As String)
     
    _Programmer = Programmer
    _MadeDate = MadeDate
    _Description = Description
  End Sub
  
  Public ReadOnly Property Programmer() As String
    Get
      Return _Programmer
    End Get
  End Property
  
  Public ReadOnly Property MadeDate() As String
    Get
      Return _MadeDate
    End Get
  End Property
  
  Public ReadOnly Property Description() As String
    Get
      Return _Description
    End Get
  End Property
End Class 

Defining our FlowerBoxAttribute will be a simple matter of setting the AttributeUsageAttribute to specify when the attribute can be used.

The FlowerBoxAttribue will look similar to this:

[System.AttributeUsage (AttributeTargets.Class |
			AttributeTargets.Method, 
			AllowMultiple=false, 
			Inherited=false)]
public class FlowerBoxAttribute : CommentBaseAttribute
{
  
  public FlowerBoxAttribute (string Programmer, 
			     string MadeDate, 
			     string Description) : 
  base (Programmer, MadeDate,Description )
  {
    // Let the base class handle everything	
  }
}

Or, in VB:

<System.AttributeUsage(AttributeTargets.Class Or _
                          AttributeTargets.Method , _
                          AllowMultiple:=false, _ 
                          Inherited:=False)> _
Public Class FlowerBoxAttribute
  Inherits CommentBaseAttribute
  Public Sub New(ByVal Programmer As String, _
                 ByVal MadeDate As String, _
                 ByVal Description As String)

    MyBase.New(Programmer, MadeDate, Description)
    'Let the base class handle everything

  End Sub
End Class

In both cases, our derived class is leaving the entire implementation to the base class. Our derived class is setting the AttributeUsageAttribute to indicate that this attribute is valid only for classes and methods. We are also specifying that the attribute is not passed on to subclasses, and the attribute can be added only once.

For our ChangeLogAttribute, we will be specifying that it is valid only for classes, methods, and properties. This time, it will also make sense to allow the attribute to be added multiple times.

The ChangeLogAttribute will be similar to this:

[System.AttributeUsage (AttributeTargets.Class |
                        AttributeTargets.Method|
                        AttributeTargets.Property,
                        AllowMultiple=true, 
                        Inherited=false)]
public class ChangeLogAttribute : CommentBaseAttribute
{
  public ChangeLogAttribute (string Programmer, 
                  			     string MadeDate, 
			                       string Description) 
    : base (Programmer, MadeDate,Description )
  {
    // Let the base class handle everything	
  }
}

Or, in VB:


<System.AttributeUsage(AttributeTargets.Class Or _
  AttributeTargets.Method Or _
  AttributeTargets.Property, _
  AllowMultiple:=true, Inherited:=False)> _
Public Class ChangeLogAttribute
    Inherits CommentBaseAttribute
    Public Sub New(ByVal Programmer As String, _
       ByVal MadeDate As String, _
       ByVal Description As String)
        MyBase.New(Programmer, MadeDate, Description)
	'Let the base class handle everything
    End Sub
End Class

For our RequirementsTrackingAttribute, we will again use the CommentBaseAttribute. This time we will be adding two new properties that will need to be initialized in the constructor (the RequirementID and a Comment). We will also specify in the AttributeUsageAttribute that this attribute can be applied anywhere, multiple times, and will not be inherited.

The RequirementsTrackingAttribute will be similar to this:

[System.AttributeUsage (AttributeTargets.All,       
         AllowMultiple=true, Inherited=false)]
public class RequirementTrackingAttribute : CommentBaseAttribute 
{
  private string _RequirementID ;
  private string _Comment;
  public RequirementTrackingAttribute (string Programmer, 
                                       string MadeDate, 
                                       string Description, 
                                       bool RequirementsTracked,
                                       string RequirementID,
                                       string Comment) 
    : base (Programmer, MadeDate,Description )
  {
    _RequirementID = RequirementID;
    _Comment = Comment;
  }
  public string RequirementID {get {return _RequirementID;}}
  public string Comment {get {return _Comment;}}
}

Or, in VB:


<System.AttributeUsage(AttributeTargets.All, _
   AllowMultiple:=True, Inherited:=False)> _
Public Class RequirementTrackingAttribute
    Inherits CommentBaseAttribute
    Dim _RequirementId As String
    Dim _Comment As String

    Public Sub New(ByVal Programmer As String, _
       ByVal MadeDate As String, _
       ByVal Description As String, _
       ByVal RequirementId As String, _
       ByVal Comment As String)
        MyBase.New(Programmer, MadeDate, Description)
        _RequirementId = RequirementId
        _Comment = Comment
    End Sub

    Public ReadOnly Property RequirementID() As String
        Get
            Return _RequirementId
        End Get
    End Property

    Public ReadOnly Property Comment() As String
        Get
            Return _Comment
        End Get
    End Property
End Class

Using Our Freshly Minted Attributes

We can use our custom attributes just as we used the AttributeUsage attribute earlier. Consider the following code:

[FlowerBoxAttribute ("Nick Harrison", "October 25, 2003", 
		     "A simple class to do some simple calcs")]
[ChangeLogAttribute ("Nick Harrison", "October 28, 2003", 
		     "Added the Area Method")]
public class SimpleCalcs
{
  public SimpleCalcs()
  {
  }
  
  [ChangeLogAttribute ("Nick Harrison", "October 28, 2003", 
                       "Corrected the Area calculation to " + 
                       "handle rectangles as well as squares")]
  public System.Double Area ( Double Length,  Double  Width)
  {
    return Length * Width;
  }
  
  [RequirementTrackingAttribute ("Nick Harrison", "October 25, 2003", 
    "Must be able to calculate the area of squars", "1.1", 
    "We will simply multiply the Length times the Length")]
  public System.Double Area (Double Length)
  {
    return Length * Length;
  }   
}

Or, in VB:

<FlowerBoxAttribute("Nick Harrison", "October 25, 2003", _
                    "A simple class to do some simple calcs"), _
                    ChangeLogAttribute("Nick Harrison", _
                                       "October 28, 2003", _
                    "Added the Area method")> _
Public Class SimpleCalcs

  Sub New()
  End Sub
  
  <ChangeLogAttribute ("Nick Harrison", "October 28, 2003", _
        "Corrected the Area calculation " + _
        "to handle rectangles as well as squares")>_
  Public Function Area(ByVal Length As System.Double, _
                       ByVal Width As System.Double) As System.Double
    Return Length * Width
  End Function
  
  <RequirementTrackingAttribute("Nick Harrison", _
   "October 25, 2003", _
  "Must be able to calculate the area of squares", "1.1", _
  "We will simply multiply the Length times the Length")> _
  Public Function Area(ByVal Length As System.Double) As System.Double
    Return Length * Length
  End Function

End Class

There are a couple of distinctions worth making between the C# implementation and the VB.NET implementation. Note that in C#, multiple attributes are added independently, while in VB.NET, multiple attributes are all enclosed in the same angle brackets but separated by commas. Also, in VB.NET, the attribute is considered part of the same line as the language structure to which it applies. This means that the line continuation character is required.

Accessing Our Custom Attributes

The Type class, the MethodInfo class, the ParameterInfo class, the EventInfo class, the PropertyInfo class, and the FieldInfo class all derive either directly or indirectly from the MemberInfo class. Among other things, this ensures that each of these classes will have a GetCustomAttributes method that expects a Type object and a Boolean to indicate whether or not to search the inheritance tree for the attributes. This method will return an array of objects. These objects will be of the same type as the type passed as a parameter to the GetCustomAttributes method, and will be the attributes (if there are any) associated with the object being explored. For instance, method.GetCustomAttributes(Type.GetType ("CustomAttributes.FlowerBoxAttribute"), false) will return an array of FlowerBoxAttribute objects associated with a specific method. If the method being explored has no such attributes, the array returned will be empty, but if the array returned has elements, then we can loop through these elements and access the properties of the attributes that we defined earlier.

We will take advantage of the fact that the Type object and the MethodInfo object have a common ancestor, and build a function that will expect a MemberInfo object and a TreeNodeCollection object as parameters and will report on any custom attributes that are found. Our DocumentMember method will look similar to this:

private void DocumentMember (MemberInfo Member, 
   TreeNodeCollection ParentNode)
{
  // Output the name of the Member being documented
  ParentNode .Add (new TreeNode (Member.Name));

  // Get a reference to the current node so that all
  // entries added will be nested under this entry
  TreeNodeCollection  CurrentNode = 
    ParentNode[ParentNode.Count-1].Nodes;

  // Get an array of the FlowerBowAttributes
  object [] FlowerBoxes = Member.GetCustomAttributes 
    (typeof (CustomAttributes.FlowerBoxAttribute ), false);

  // If there is a FlowerBox attribute.   Note that we can
  // safely assume that there be at most 1.
  if (FlowerBoxes.Length >0)
  {
    // Output some details from this FloweBoxAttribute
    CustomAttributes.FlowerBoxAttribute FlowerBox = 
       (CustomAttributes.FlowerBoxAttribute) FlowerBoxes[0];
    CurrentNode.Add ("********************************");
    CurrentNode.Add ("*"+FlowerBox.Programmer );
    CurrentNode.Add ("*"+FlowerBox.InitialCreationDate );
    CurrentNode.Add ("*"+FlowerBox.Description);
    CurrentNode.Add ("********************************");
  }

  // Find out if there are any ChangeLog attributes.
  object [] ChangeLogs = Member.GetCustomAttributes (
    typeof (CustomAttributes.ChangeLogAttribute), false);

  // If there is at least one Change Log
  if (ChangeLogs.Length >0)
  {
    // Output that there is atleast one change log
    CurrentNode.Add ("Change Log");

    // Update that the CurrentNode is now the line
    // that tells us to look for a Change Log.   This
    // will ensure that the Change Log details are under
    // the label for the Change Logs
    CurrentNode = CurrentNode[CurrentNode.Count -1].Nodes;

    // Loop through each of the ChangeLog attributes
    foreach (CustomAttributes.ChangeLogAttribute 
              ChangeLog in ChangeLogs )
    {
      // Display some details about each ChangeLog
      CurrentNode.Add (ChangeLog.Programmer );
      CurrentNode.Add (ChangeLog.InitialCreationDate );
      CurrentNode.Add (ChangeLog.Description );
    }
  }

  // Find out if there are any RequirementTrackingAttributes
  object [] Requirements = Member.GetCustomAttributes (
    typeof (CustomAttributes.RequirementTrackingAttribute ), false);

  // If there is atleast one attribute
  if (Requirements.Length >0)
  {
    // Output that there are some requirements being tracked
    CurrentNode.Add ("Requirements Tracking");

    // Get a reference to the Node collection that was just created.
    // This will ensure that the details that we are about to output
    // are in fact requirements being tracked.
    TreeNodeCollection  ChildNode = CurrentNode[CurrentNode.Count -1].Nodes;

    // Loop through each of these attributes.
    foreach (CustomAttributes.RequirementTrackingAttribute Requirement in Requirements )
    {
       // Display some details from the RequirementsTrackingAttribute
       ChildNode.Add (Requirement.Programmer);
       ChildNode.Add (Requirement.Comment );
       ChildNode.Add ("Requirement: " + Requirement.RequirementID);
       ChildNode.Add (Requirement.Description );
    }
  }   
}

This method will be called repeatedly, once for each class and once for each method in each class. Our driving DocumentClasses method will be similar to this:

private void DocumentClasses(Assembly TestingAssembly)
{
  // Keep track of the current top level index into 
  // the Tree View
  int TreeViewIndex= 0;

  // Find all of the Types in the Assembly
  Type [] TargetTypes = TestingAssembly.GetTypes ();

  // Loop through these Types
  foreach (Type CurrentType in TargetTypes)
  {
    // Document this Class
    DocumentMember (CurrentType, tvwComments.Nodes );

    // Get a list of all of the Methods in the Type
    MethodInfo [] methods = CurrentType.GetMethods();

    // Log that we are about to document methods
    tvwComments.Nodes[TreeViewIndex].Nodes.Add ("Methods");

    // Loop through these methods
    foreach (MethodInfo method in methods)
    {
      // Document this method, logging that the 
      // comments should be under the node for the 
      // current class
      DocumentMember(method, 
        tvwComments.Nodes[TreeViewIndex].Nodes[
        tvwComments.Nodes[TreeViewIndex].
        Nodes.Count -1].Nodes);
    }
    TreeViewIndex++;
  }
}

Running this code against our SimpleCalcs assembly will produce results similar to this:

Output

Conclusion

Using similar techniques, we could easily create attributes for issue tracking, test case status, code reviews, etc. With minimal effort, we can create comprehensive documentation that we know will be in sync with the executable in use. This eliminates any confusion about whether or not you are running the latest version. You can simply run the commenter on the .dll being used and look for your comments.

Next time, we will go beyond simply exploring and creating metadata, and delve into using dynamically discovered types.

Nick Harrison UNIX-programmer-turned-.NET-advocate currently working in Charlotte, North Carolina using .NET to solve interesting problems in the mortgage industry.


Return to ONDotnet.com