C# is a procedural programming language that also includes some declarative features.(I know it’s OO—I mean "procedural" in terms of its approach to computations.)For example, you can declare access to classes and members by attaching modifiers such as public or private. C# lets you define and attach your own declarative information through attributes. The attributes attached to an entity are stored along with the other metadata generated by the compiler and can be retrieved at run time through reflection. Let’s take a look at how you define, attach, and retrieve your own custom attributes.
Don't miss these previous articles in our C# series
- · "Give your C# programs a boost: Tips for using constants, methods, and constructors"
- · "Exploring C# constants, fields, and methods"
- · "Anatomy of a C# class"
Defining an attribute
An attribute is simply a class that derives, either directly or indirectly, from the .NET framework class System.Attribute. It defines positional and/or named parameters that a user may specify to attach the attribute to an entity. Positional parameters are the formal parameters to public constructors of the class. Named parameters are the public nonstatic read-write properties of the class. An attribute class that doesn’t define a constructor has only named parameters. Positional and named parameters are limited to the bool, byte, char, double, float, int, long, short, string, object, System.Type, and enum types. The code in Listing A defines the attribute FooAttribute.
FooAttribute defines a constructor that takes a single integer argument. This argument is the positional parameter. FooAttribute also defines the properties Id and Name. These are the named parameters.
An attribute class may have attributes attached to it. The .NET framework attribute System.AttributeUsage enables the attribute author to specify how the attribute may be applied. The AttributeUsage constructor takes a single argument that specifies the types of targets to which the attribute may be attached. The targets are specified as a set of System.AttributeTargets enumeration values combined through bitwise OR. The AllowMultiple Boolean property specifies whether the attribute may be attached more than once to the same target. The Inherited Boolean property specifies whether the attribute is inherited by derived classes or overridden members. Both AllowMultiple and Inherited default to false.
The AttributeUsage for FooAttribute specifies that FooAttribute may be attached only to classes and methods. Because neither AllowMultiple nor Inherited is specified as a named parameter, a FooAttribute may be attached to the same target only once and is not inherited.
As a class, an attribute may contain other members besides constructors and properties. For example, FooAttribute overrides the ToString method. I’ll use ToString later.
An attribute is attached to a target by enclosing the attribute name and parameters in square brackets immediately before the target. Positional parameters are specified first, followed by the named parameters, which are optional. The code in Listing B attaches FooAttribute to the class Target and to its DoubleIt and TripleIt methods.
The FooAttribute for Target and for DoubleIt specify both the positional parameter and the named parameter Name. No named parameters are specified in the attribute for TripleIt. Looking back to the FooAttribute constructor, you see that the positional parameter is used to set the private field id. The public property Id also manages the id field. It is valid to attach FooAttribute with the declaration [FooAttribute( 1, Id = 42 )]. However, what value would be stored in the id field, 1 or 42? The answer is 42. Positional parameters are applied first, during construction of the attribute instance, followed by named parameters.
Notice that the attribute declarations for DoubleIt and TripleIt specify the attribute name Foo instead of FooAttribute. By convention, attribute class names end with the suffix Attribute. You can omit the suffix when attaching the attribute to a target. This is simply a shortcut provided by the C# compiler.
An attribute may live within a namespace, just like any other class. FooAttribute is in the namespace Articles.TechRepublic.Attributes. Because the Target class is in the same namespace, it isn’t necessary to fully qualify the FooAttribute declarations. The fully qualified attribute declaration for Target.TripleIt is [Articles.TechRepublic.Attributes.Foo( 3 )].
.NET assemblies contain types and types contain members. The .NET framework provides classes that describe these entities under the System and System.Reflection namespaces. Using an instance of one of these classes to “look within” an entity is known as reflection.
To examine the attributes of class Target and its methods, I’ll use the reflection classes shown below. Derived classes are shown indented underneath their base class, like this.
The System.Attribute class provides the static method GetCustomAttributes to retrieve the attributes attached to an entity, given a reflection class that describes the entity. This is the same System.Attribute from which attribute classes are derived. The program in Listing C retrieves the FooAttributes attached to the class Target and to its DoubleIt and TripleIt methods.
Main uses the typeof operator to create a System.Type object that describes the Target class. The targetType.GetMethod is called to create MethodInfo objects that describe the methods DoubleIt and TripleIt. PrintFooAttribues is called once with each of these reflection objects.
Within PrintFooAttributes, a System.Type object is created to describe the FooAttribute class itself. The shorthand name Foo cannot be used here as it can when attaching the attribute. The static method Attribute.GetCustomAttributes returns all of the FooAttributes for the element described by member. A different overloaded form of GetCustomAttributes could have been called to return all of the entity attributes without regard to type. However, retrieving only the FooAttributes allows casting without fear of failure in the Console.WriteLine statement inside the foreach loop. Casting each attribute to FooAttribute allows Console.WriteLine to call the ToString implementation in FooAttribute.
Simplify your programming
FooAttribute doesn’t provide lots of functionality, but it is useful to illustrate attribute definition, attachment, and retrieval. Many classes in the .NET framework make creative use of attributes to simplify programming tasks. In my next article, I’ll show you how attributes are used with the XML serializer to significantly simplify XML processing.