Custom Attributes

Types, members, modules, and assemblies all have associated metadata that is used by all the major CLR services, is considered an indivisible part of an application, and can be accessed via reflection (see the earlier Section 3.10).

A key characteristic of metadata is that it can be extended. You extend the metadata with custom attributes, which allow you to “decorate” a code element with additional information stored in the metadata associated with the element.

This additional information can then be retrieved at runtime and used to build services that work declaratively, which is the way that the CLR implements core features such as serialization and interception.

Language Support for Custom Attributes

Decorating an element with a custom attribute is known as specifying the custom attribute and is done by writing the name of the attribute enclosed in brackets ([]) immediately before the element declaration as follows:

[Serializable] public class Foo {...}

In this example, the Foo class is specified as serializable. This information is saved in the metadata for Foo, and affects the way the CLR treats an instance of this class.

A useful way to think about custom attributes is that they expand the built-in set of declarative constructs in the C# language such as public, private, and sealed.

Compiler Support for Custom Attributes

In reality, custom attributes are simply types derived from System.Attribute with language constructs for specifying them on an element (see Section 2.16 in Chapter 2).

These language constructs are recognized by the compiler, which emits a small chunk of data into the metadata. This custom data includes a serialized call to the constructor of the custom attribute type (containing the values for the positional parameters), and a collection of property set operations (containing the values for the named parameters).

The compiler also recognizes a small number of pseudo-custom attributes. These are special attributes that have direct representation in metadata and are stored natively (i.e., not as chunks of custom data). This is primarily a runtime performance optimization, although it has some implications for retrieving attributes via reflection, as discussed later.

To understand this, consider the following class with two specified attributes:

[Serializable, Obsolete]
class Foo {...}

When compiled, the metadata for the class Foo looks like this in MSIL:

.class private auto ansi serializable Foo {
  .custom instance void 
    System.ObsoleteAttribute::.ctor( ) = ( 01 00 00 00 ) 
    ...
  }
}

Compare the different treatment by the compiler of the Obsolete attribute, which is a custom attribute and is stored as a serialized constructor call to the System.ObsoleteAttribute type, to the treatment of the Serializable attribute, which is a pseudo-custom attribute represented directly in the metadata with the serializable token.

Runtime Support for Custom Attributes

At runtime the core CLR services such as serialization and remoting inspect the custom and pseudo-custom attributes to determine how to handle an instance of a type.

In the case of custom attributes, this is done by creating an instance of the attribute (invoking the relevant constructor call and property-set operations), and then performing whatever steps are needed to determine how to handle an instance of the type.

In the case of pseudo-custom attributes, this is done by simply inspecting the metadata directly and determining how to handle an instance of the type. Consequently, handling pseudo-custom attributes is more efficient than handling custom attributes.

Note that none of these steps is initiated until a service or user program actually tries to access the attributes, so there is little runtime overhead unless required.

Predefined Attributes

The .NET Framework makes extensive use of attributes for purposes ranging from simple documentation to advanced support for threading, remoting, serialization, and COM interop. These attributes are all defined in the BCL, and can be used, extended, and retrieved by your own code.

However, certain attributes are treated specially by the compiler and the runtime. Three attributes considered general enough to be defined in the C# specification are AttributeUsage, Conditional, and Obsolete. Other attributes such as CLSCompliant, Serializable, and NonSerialized are also treated specially.

AttributeUsage attribute

[AttributeUsage( target-enum
[, AllowMultiple=[true|false]]?
[, Inherited=[true|false]]?
] (for classes)

The AttributeUsage attribute is applied to a new attribute class declaration. It controls how the new attribute should be treated by the compiler, specifically, what set of targets (classes, interfaces, properties, methods, parameters, etc.) the new attribute can be specified on, whether multiple instances of this attribute may be applied to the same target, and whether this attribute propagates to subtypes of the target.

target-enum is a bitwise mask of values from the System. AttributeTargets enum, which looks like this:

namespace System {
  [Flags]
  public enum AttributeTargets {
    Assembly     = 0x0001,
    Module       = 0x0002,
    Class        = 0x0004,
    Struct       = 0x0008,
    Enum         = 0x0010,
    Constructor  = 0x0020,
    Method       = 0x0040,
    Property     = 0x0080,
    Field        = 0x0100,
    Event        = 0x0200,
    Interface    = 0x0400,
    Parameter    = 0x0800,
    Delegate     = 0x1000,
    ReturnValue  = 0x2000,
    All          = 0x3fff,
    ClassMembers = 0x17fc,
  }
}

Conditional attribute

[Conditional (symbol) ] (for methods)

The Conditional attribute can be applied to any method with a void return type. The presence of this attribute tells the compiler to conditionally omit calls to the method unless symbol is defined in the calling code. This is similar to wrapping every call to the method with #if and #endif preprocessor directives, but Conditional has the advantage of needing to be specified only in one place.

Obsolete attribute

[Obsolete([Message=]? message
IsError= [true|false]]
] (for all attribute targets)

Applied to any valid attribute target, the Obsolete attribute indicates that the target is obsolete. Obsolete can include a message that explains which alternative types or members to use and a flag that tells the compiler to treat the use of this type or member as either a warning or an error.

For example, referencing type Bar in the following example causes the compiler to display an error message and halts compilation:

[Obsolete("Don't try this at home", IsError=true)]
class Bar { ... }

CLSCompliant attribute

[CLSCompliant(true|false)]
(for all attribute targets)

Applied to an assembly, the CLSCompliant attribute tells the compiler whether to validate CLS compliance for all the exported types in the assembly. Applied to any other attribute target, this attribute allows the target to declare if it should be considered CLS-compliant. In order to mark a target as CLS-compliant, the entire assembly needs to be considered as such.

In the following example, the CLSCompliant attribute is used to specify an assembly as CLS-compliant and a class within it as not CLS-compliant:

[assembly:CLSCompliant(true)]

[CLSCompliant(false)]
public class Bar { 
  public ushort Answer { get {return 42;} } 
}

Serializable attribute

[Serializable]
(for classes, structs, enums, delegates)

Applied to a class, struct, enum, or delegate, the Serializable attribute marks it as being serializable. This attribute is a pseudo-custom attribute and is represented specially in the metadata.

NonSerialized attribute

[NonSerialized] (for fields)

Applied to a field, the NonSerialized attribute prevents it from being serialized along with its containing class or struct. This attribute is a pseudo-custom attribute and is represented specially in the metadata.

Defining a New Custom Attribute

In addition to using the predefined attributes supplied by the .NET Framework, you can also create your own.

To create a custom attribute:

  1. Derive a class from System.Attribute or from a descendent of System.Attribute. By convention the class name should end with the word “Attribute,” although this isn’t required.

  2. Provide the class with a public constructor. The parameters to the constructor define the positional parameters of the attribute and are mandatory when specifying the attribute on an element.

  3. Declare public-instance fields, public-instance read/write properties, or public-instance write-only properties to specify the named parameters of the attribute. Unlike positional parameters, these are optional when specifying the attribute on an element.

    The types that can be used for attribute constructor parameters and properties are bool, byte, char, double, float, int, long, short, string, object, the Type type, enum, or a one-dimensional array of the aforementioned types.

  4. Finally, define what the attribute, may be specified on using the AttributeUsage attribute, as described in the preceding section.

Consider the following example of a custom attribute, CrossRef-Attribute , which removes the limitation that the CLR metadata contains information about statically linked types but not dynamically linked ones.

using System;
[AttributeUsage(AttributeTargets.ClassMembers, AllowMultiple=true)]
class CrossRefAttribute : Attribute {
  Type   xref;
  string desc = "";
  public string Description { set { desc=value; } } 
  public CrossRefAttribute(Type xref) { this.xref=xref; }
  public override string ToString( ) {
    string tmp = (desc.Length>0) ? " ("+desc+")" : "";
    return "CrossRef to "+xref.ToString( )+tmp;
  }
}

From the attribute user’s perspective, this attribute can be applied to any class member multiple times (note the use of the AttributeUsage attribute to control this). CrossRefAttribute takes one mandatory positional parameter (namely the type to cross reference) and one optional named parameter (the description), and is used as follows:

[CrossRef(typeof(Bar), Description="Foos often hang around Bars")]
class Foo {...}

Essentially, this attribute embeds cross references to dynamically linked types (with optional descriptions) in the metadata. This information can then be retrieved at runtime by a class browser to present a more complete view of a type’s dependencies.

Retrieving a Custom Attribute at Runtime

Retrieving attributes at runtime is done using reflection via one of the GetCustomAttribute overloads on the object’s Type instance. This is one of the few circumstances where the difference between custom attributes and pseudo-custom attributes becomes apparent, since pseudo-custom attributes can’t be retrieved with GetCustomAttribute.

Here is an example that uses reflection to determine what attributes are on a specific type:

using System;
[Serializable, Obsolete]
class Test {
  static void Main( ) {
    Type t = typeof(Test);
    object[] caarr = t.GetCustomAttributes( );
    Console.WriteLine("{0} has {1} custom attribute(s)",
                      t, caarr.Length);
    foreach (object ca in caarr)
      Console.WriteLine(ca);
  }
}

Although the Test class of the preceding example has two attributes specified, the sample produces the following output:

Test has 1 custom attribute(s)
System.ObsoleteAttribute

This demonstrates how the Serializable attribute (a pseudo-custom attribute) isn’t accessible via reflection, while the Obsolete attribute (a custom attribute) still is.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.199.56