Basic Instincts
Designing With Custom Attributes
Ted Pattison
Contents
The Evolution of Attribute-Based Programming
Using Attributes in Software Design
Creating a Custom Attribute Class
Designing Attributes with Parameters
Summing It Up
One of the most powerful aspects of the Microsoft® .NET Framework is its support for attributes. Attribute-based programming is extremely powerful because it adds a declarative dimension to designing and writing software. Declarative programming provides an elegant new way for developers to leverage services and functionality from reusable libraries and frameworks as well as from developer tools, such as the Visual Basic® .NET compiler.
As a user of Visual Basic .NET, it's essential that you understand what attributes are and how to use the syntax necessary to apply them. I will cover these topics in this month's column and will then discuss why and how to create your own custom attribute class.
The Evolution of Attribute-Based Programming
Microsoft support for attribute-based programming can be traced back to the days before the .NET Framework, back to Microsoft Transaction Services (MTS) and COM+. These older runtime environments allowed you to declaratively assign attributes to classes and methods to express your intent to leverage system services. For example, you can declare that a class "requires a transaction" simply by setting a COM+ component attribute. This results in developers being able to take advantage of COM+ services without having to write much code. Reducing the coding burden is certainly a major benefit of attribute-based programming.
The .NET Framework raises the bar with attribute-based programming, taking it much further than either MTS or COM+. I say this because MTS and COM+ offer only a handful of system-defined attributes. In contrast, there are scores of attributes defined in the Framework Class Library. What's more, the .NET Framework introduces extensibility mechanisms that lets you or your development team create your own custom attributes.
Many teams at Microsoft working on the Framework Class Library have created their own attributes as a means to expose their services and functionality. You will likely be applying these system-defined attributes in your code on a regular basis. The .NET Framework support for attributes is especially elegant because you can apply them directly in your source code. Figure 1 is a simple example of applying attributes to an assembly, a class, and a method in Visual Basic .NET.
Figure 1 Applying Attributes
Imports System
Imports System.Reflection
Imports System.Diagnostics
'*** apply AssemblyDescriptionAttribute to an assembly
<Assembly: AssemblyDescription("Bob's component library")>
'*** apply SerializableAttribute to Class1
<Serializable()> _
Public Class Class1
'*** apply DebuggerStepThroughAttribute to Method1
<DebuggerStepThrough()> _
Sub Method1()
'*** method implementation
End Sub
End Class
To apply an attribute, you simply supply the name of the attribute, surrounded by the < and > characters, immediately before the definition of the target entity to which you'd like to apply it. You can see in Figure 1 that a line-continuation character has been placed after the attribute to introduce a line break between the attribute and the class it has been applied to. This is commonly used in Visual Basic .NET to improve readability when applying attributes.
If the attribute defines parameters, the parameters are passed inside parentheses. Note that applying an attribute at the assembly level is a special case in which you must prefix the attribute name with the Assembly keyword and a colon. Assembly-level attributes must be defined in a source file after any Imports statements and before any other code. Also keep in mind that it is a common convention to maintain all your assembly-level attributes for a Visual Studio® .NET project in the file named AssemblyInfo.vb.
By convention, the names of attributes end with the suffix "Attribute" as in the case of AssemblyDescriptionAttribute and SerializableAttribute. However, the compilers for both Visual Basic .NET and C# allow you to optionally omit the suffix when applying an attribute. For example, the compiler will accept an attribute name of Serializable or SerializableAttribute. Notice that the IntelliSense® feature of Visual Studio .NET prefers the shorter version, stripping the suffix off the end of an attribute name automatically.
When you compile your code, the attributes you've applied and their parameterized settings are recorded in the component metadata of your assembly (see Figure 2). These attributes can then be read from the assembly image later. The act of inspecting an assembly at run time to read type information and to determine how attributes have been applied is known as reflection.
Figure 2** Attribute Settings in the Assembly **
When you apply an attribute that is defined with parameters, there are two different styles for parameter passing. Attribute parameters can be passed either by position or by name. The attribute definition itself dictates which style is allowed or required. That means that whoever authors an attribute gets to decide how parameters are passed.
First, let's look at a simple example of an attribute parameter passed by position:
Imports System.Reflection
<Assembly: AssemblyDescription("Bob's component library")>
As you can see, passing parameters by position uses the standard Visual Basic .NET syntax for passing parameters to methods and properties. Now compare that to the next example in which attribute parameters are passed by name:
Imports System.Web.Services
<WebService(Name:="BobSVC", Description:="Bob's web service")> _
Class BobsWebService : Inherits WebService
'*** web service implementation
End Class
Here named parameters are passed using the special := syntax. You may recall using this syntax in method calls when passing parameters by name in earlier versions of Visual Basic. It is interesting to note that Visual Basic .NET no longer supports passing parameters by name in method calls. Named parameters are only supported if you're passing parameters when applying attributes. However, the support for named parameters with attributes is valuable because, unlike methods and properties, attributes do not support the concept of overloading the parameter list.
Using Attributes in Software Design
Now that I have walked through the basic syntax for applying attributes, let's take a step back and think about attributes at a higher level. This will make it easier to see how attributes are actually used in designing systems and applications.
It all starts when a programmer or development team creates an attribute. Creating an attribute involves defining its semantic meaning and writing an attribute class definition. After the attribute has been created, other programmers can then apply it. Finally, someone (often the creator of the attribute) typically uses reflection to determine how the attribute has been applied and takes a course of action. For example, the common language runtime (CLR) and managed frameworks such as ASP.NET will change the behavior of your code depending on how you have applied their attributes.
Let's look at the SerializableAttribute created by the CLR team. The semantics of this attribute imply that objects created from classes that carry the SerializableAttribute can be serialized by the CLR. The CLR provides a convenient built-in service that can automatically serialize your objects. This service is valuable when you need to persist the contents of an object to a file or write this information into the body of a message to transmit across the network. Applying the SerializableAttribute is the key to taking advantage of the CLR's automatic serialization service:
<Serializable()> _
Public Class Customer
Protected Name As String
Protected Phone As String
End Class
Remember that applying an attribute doesn't do any good until the CLR or some other component uses reflection to inspect it. Imagine you attempt to pass a Customer object created from the class definition you just saw across the wire as a parameter in a method call using .NET Remoting. The .NET Remoting layer cannot pass an object across the network unless it is able to serialize its type information and contents into the body of a message.
The .NET Remoting layer and the CLR use reflection to inspect the object's underlying class definition. They do this to see if it has been defined with the SerializableAttribute. If the object is not serializable, the attempt to pass it in a .NET Remoting call fails. In this case the Customer object is serializable. Therefore, .NET Remoting calls upon the CLR's built-in serialization service to construct a network message holding the type information and contents of the object being passed.
This example shows one of the most common usage patterns of attribute-based programming—a development team defines attributes intended for other programmers to apply. This development team then also uses reflection to see how other programmers have applied their attributes. In this usage pattern, attributes provide an elegant means for a development team to expose their services and functionality to other programmers.
One determination you should make when you apply an attribute is when the attribute will be inspected. The answer varies because many different attributes are inspected at different times. For example, the SerializableAttribute from the last example is inspected by the CLR at run time. The AssemblyDescription attribute is inspected by the Visual Basic compiler when building an assembly at compile time.
A second piece of information you need when you apply an attribute is who is inspecting the attribute. The answer might be the CLR, ASP.NET, the Visual Basic .NET compiler, or Visual Studio .NET. The table in Figure 3 provides a few more examples to give you an idea of when attributes are inspected and by whom.
Figure 3 Attributes Defined in the Framework Class Library
Attribute Name | Applied to | Inspected by | Inspected at |
---|---|---|---|
AssemblyVersionAttribute | Assemblies | CLR | Run time |
SerializableAttribute | Classes | CLR | Run time |
WebServiceAttribute | Classes | ASP.NET | Run time |
WebMethodAttribute | Methods | ASP.NET | Run time |
ObsoleteAttribute | Types/members | Language compilers | Compile time |
DefaultValueAttribute | Anything | Visual Studio .NET | Design time |
DesignerCategoryAttribute | Classes | Visual Studio .NET | Design time |
Creating a Custom Attribute Class
Now that you have a better understanding of what an attribute really is, it's time to create one. The first thing to do when creating a custom attribute is to decide what semantic meaning it should have. In the example, you will create an attribute named TestedAttribute. The semantics will be defined such that any class or assembly to which the attribute is applied has been tested by a specific individual and given a test grade.
Before creating the attribute class, let's create an enumeration named TestGrade with four discrete test grade values to assist you in parameterizing your custom attribute class:
Public Enum TestGrade
Excellent
Good
Poor
Unacceptable
End Enum
When you want to create a custom attribute, you should follow these steps. First, create a class that inherits from System.Attribute. Next, add fields to store the attribute's parameterized values. Third, add one or more constructors to initialize the attribute's parameterized values. The following code snippet shows a starting point for the attribute class that I've created:
Public Class TestedAttribute
Inherits System.Attribute
Public Tester As String
Public Grade As TestGrade
Sub New(ByVal Tester As String, _
ByVal Grade As TestGrade)
Me.Tester = Tester
Me.Grade = Grade
End Sub
End Class
Once you have created your attribute class, you can apply it to a target such as an assembly or a class definition:
'*** apply attribute to assembly
<Assembly: Tested("Fred", TestGrade.Good)
'*** apply attribute to a class
<Tested("Bob", TestGrade.Excellent)> _
Class Customer
'*** class definition goes here
End Class
When you create an attribute class you must consider which targets are applicable. For example, it doesn't make sense for every attribute to apply to assemblies, classes, methods, and parameters. One attribute might be designed so that it only makes sense to apply it at the assembly level. Another attribute should only apply to classes and structures.
You can specify the possible targets by applying the AttributeUsageAttribute to your attribute class definition. When you apply this attribute, you use a parameter typed to the AttributeTargets enumeration to list the target type. For example, if you wanted to create TestedAttribute so that it could only be applied to classes, you might write its definition like this:
<AttributeUsage(AttributeTargets.Class)> _
Class TestedAttribute : Inherits System.Attribute
'*** attribute definition goes here
End Class
Once you specify your target in this manner, the compiler will carry out your wishes. For example, another programmer will now experience a compile-time error when trying to apply the TestedAttribute to anything other than a class definition:
'*** compile-time error
'*** attribute not supported at assembly level
<Assembly: Tested("Fred", TestGrade.Good)>
If you want to allow for multiple types of targets, you can Or together values of the AttributeTargets enumeration:
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Struct Or _
AttributeTargets.Enum Or AttributeTargets.Delegate)> _
Class TestedAttribute : Inherits System.Attribute
'*** attribute definition goes here
End Class
You can also use the AttributeTargets enumeration value of All to explicitly specify that your attribute class can be applied to any type of target supported by the .NET programming model:
<AttributeUsage(AttributeTargets.All)> _
Class TestedAttribute : Inherits System.Attribute
'*** attribute definition goes here
End Class
Note that a parameter value of AttributeTargets.All provides the same result as the default behavior. In other words, an attribute class defined without the AttributeUsageAttribute can be applied to any type of target. Therefore, you must use the AttributeUsageAttribute whenever you want to restrict the possible target types.
Another important issue to consider when creating a custom attribute is whether it can be applied multiple times to the same target. By default, an attribute can only be applied once. However, the attribute creator can override this and allow the attribute to be applied two or more times to a single target. You can do this by defining your attribute class with the AttributeUsageAttribute and initializing the AllowMultiple parameter with a value of true.
For example, let's say you want to create an attribute class to track the author of a class. However, you would like to apply the attribute more than once when a class has been written by multiple authors. See the following definition for an AuthorAttribute class:
<AttributeUsage(AttributeTargets.Class, AllowMultiple:=True)> _
Public Class AuthorAttribute : Inherits Attribute
Public Name As String
Sub New(ByVal Name As String)
Me.Name = Name
End Sub
End Class
Now that the attribute class has been defined with the AllowMultiple parameter, the compiler will allow you and other programmers to apply it several times to a class definition:
<Author("Jose"), Author("Doug")> _
Public Class SuperTransactionProcessor
'*** class definition goes here
End Class
Another consideration is how an attribute class is affected by inheritance. Ask yourself the following question: "If you apply the attribute to a base class, should all derived classes automatically have the attribute applied as part of their definition?"
<Tested("Bob", TestGrade.Excellent)> _
Class Customer
' class definition goes here
End Class
'*** Should this class definition include Customer's TestedAttribute?
Class RetailCustomer : Inherits Customer
' class definition goes here
End Class
In some designs, it makes sense to have an attribute that is implicitly applied to all derived classes when it is applied to a base class. However, it does not make sense in other cases, including the one here with the TestedAttribute class. Applying the TestedAttribute to the Customer class should not implicitly cause it to be applied to its derived classes, since the derived classes have not been tested yet.
You can control the inheritance behavior when applying the AttributeUsageAttribute to the attribute class definition. In the case of the TestedAttribute class, you definitely do not want inheritance to affect the attribute. No class should be defined with the TestedAttribute unless it is explicitly applied to its class definition. Therefore, the attribute class should be defined like this:
<AttributeUsage(AttributeTargets.All, Inherited:=False)> _
Public Class TestedAttribute
'*** attribute definition goes here
End Class
Designing Attributes with Parameters
When designing an attribute class, you should consider how you would like other programmers to pass parameters. You can define your parameters so that they can be passed by name or by position. You can also design your attribute class with the flexibility so that other programmers can choose to pass parameters by name or by position. Let's look at an example—the final version of the TestedAttribute class definition in Figure 4.
Figure 4 TestedAttribute Class
Defining an Attribute
Public Enum TestGrade
Excellent
Good
Poor
Unacceptable
End Enum
<AttributeUsage(AttributeTargets.All, Inherited:=False)> _
Public Class TestedAttribute : Inherits System.Attribute
Public Tester As String
Public Grade As TestGrade
'*** parameterized constructor for passing parameters by position
Sub New(ByVal Tester As String, ByVal Grade As TestGrade)
Me.Tester = Tester
Me.Grade = Grade
End Sub
'*** default constructor for passing parameters by name
Sub New()
End Sub
End Class
Using the Attribute
'*** applying attribute and passing parameters by position
<Tested("Bob", TestGrade.Excellent)> Class Customer
' class definition goes here
End Class
'*** applying attribute and passing parameters by name
<Tested(Tester:="Alice", Grade:=TestGrade.Excellent)> Class Employee
' class definition goes here
End Class
Note that there are now two constructors in the TestedAttribute class. The first constructor is parameterized, allowing for the following usage in which parameters are passed by position:
'*** applying attribute and passing parameters by position
<Tested("Bob", TestGrade.Excellent)> _
Class Customer
' class definition goes here
End Class
The arguments in a parameterized constructor define how parameters are to be passed by position. However, a default constructor (a no-argument constructor) doesn't define any parameters to be passed by position. You should note that your attribute class will require a default constructor if you want to allow all parameters to be passed by name:
<Tested(Tester:="Alice", Grade:=TestGrade.Excellent)> _
Class Employee
' class definition goes here
End Class
Note that you need more than just a default constructor to allow for parameter passing by name. Each named parameter must also be defined in the attribute class as a public field or a public writeable property. The TestedAttribute class definition in Figure 4 exposes public fields named Tester and Grade. These public fields can be passed as named parameters.
As a final note, you should see that constructors dictate which parameters must be passed by position. For example, if you remove the default constructor, you cannot apply the attribute class with only named parameters. You would be forced to pass both parameters by position.
Summing It Up
This month's column has discussed the power and flexibility that attributes bring to the programming model of the .NET Framework. It's critical that you know how to apply attributes because that's how many of the .NET libraries expose their services and functionalities to you.
You also saw how to create your own custom attribute class. Such an undertaking will make it possible for you to incorporate the elegance of attribute-based programming in your own designs. However, this column has not told the complete story. If you really want to embrace designs that make use of attribute-based programming, you are going to have to learn to write code that uses reflection. In particular, you must be able to inspect compiled assemblies and determine if and how your attributes have been applied. Stay tuned, because that will be the subject of the next installment of Basic Instincts.
Send your questions and comments for Ted to instinct@microsoft.com.
Ted Pattison is a cofounder of Barracuda .NET, an education company that assists companies building collaborative applications using Microsoft technologies. Ted is the author of several books including Building Applications and Components with Visual Basic .NET (Addison-Wesley, 2003).