Understanding Reflection, Part 2
by Nick Harrison11/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:
|
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
- Trackback from http://weblogs.asp.net/javery/posts/0.aspx
.NET Nightly 54
2003-11-18 17:17:38 [View]
-
.NET Nightly 54
2004-01-03 16:28:22 anonymous2 [View]
-
.NET Nightly 54
2004-02-10 16:50:53 nancy_sandoval [View]
- Trackback from http://weblogs.asp.net/tatochip/posts/0.aspx
Awesome OReilly Reflection Article
2003-11-18 06:21:12 [View]

