Attribute TargetsSo far, you've seen how an attribute is declared in C# code to affect a class definition. However, attributes can be applied to nearly any part of a .NET assembly. When you declare an attribute as shown in Listing 1-5, the attribute is applied to the class, rather than to any of the class's methods or fields. If you want to make the target of an attribute explicit, you can prefix the attribute declaration with the target name by adding the type keyword, as Listing 1-6 shows.Listing 1-6. Explicitly Stating the Attribute Target[type: Obsolete("This class should no longer be used - switch to ImprovedCountry.",true)]public class BadCountry{ // ...} When an attribute is defined, it can also state the valid targets that it can be applied to in code. This is done via the AttributeTargets enumeration. Table 1-1 describes all of the values in the AttributeTargets enumeration.Table 1-1.Valid Attribute Targets
The values of AttributeTargets can be ORed together, so an attribute designer can make any combination of valid targets. The All value is supplied if an attribute can be applied to any target. C# defines nine keywords that can be used to apply the attribute to a specific target in code: assembly, event, field, method, module, param, property, return, and type. As you can guess, some of these keywords overlap with the values in AttributeTargets. For example, type can be used on a class, an interface, a structure, an enumeration, and a delegate definition; param can be used on only method arguments.Where an attribute can be legally applied is determined by the implementer of the attribute. An attribute designer uses the AttributeTargetsAttribute class on the attribute definition itself to control the attribute's valid destinations. (We'll cover AttributeTargetsAttribute in detail in Chapter 4.) For example, if you need to make only one method of BadCountry obsolete, you apply the attribute like this:[property: Obsolete("This is a bad property.",true)]public long Population{// ...}
However, you cannot make an entire assembly obsolete. The following C# code will cause a compilation error:[assembly: Obsolete("This assembly should no longer be used.",true)]
Furthermore, the location of the attribute when a target is explicitly given is also important. For example, an assembly-level attribute (like AssemblyTitleAttribute) cannot be declared inside a class or namespace; it must exist outside these scopes, as the following code snippet demonstrates:// This is OK.[assembly: AssemblyTitle("Country assembly.")]namespace Apress.NetAttributes{// This isn't.[assembly: AssemblyTitle("Country assembly.")]public class BadCountry{// Neither is this.[assembly: AssemblyTitle("Country assembly.")]}}You're not limited to applying only one attribute to a given target. For example,the following code is valid:[type: Obsolete("This class should no longer be used - switch to ImprovedCountry.",true)][type: Serializable]public class BadCountry{ // ...}You may, however, be limited in terms of how many times you can applyone specific attribute to a particular target. For example, the following code willnot compile:[type: Obsolete("This class should no longer be used - switch to ImprovedCountry.",true)][type: Obsolete("Really - don't use this class!",true)]public class BadCountry{ // ...}
As with the valid target locations, an attribute designer can control if an attribute can be applied to same target multiple times via the AllowMultiple property of the AttributeUsageAttributes class. The default behavior for an attribute is that it will not be a multiuse attribute, because it is unusual for an attribute to be applied to the same target multiple times, but this can be changed.Finally, attributes can control whether or not their information is inherited in subclasses or overridden methods. For example, let's say we tried to use BadCountry as a base class.[type: Obsolete("Really - don't use this class!",true)]public class BadCountry{ // ...}// This will not compile.public class BadInheritedCountry : BadCountry{ // ...}In this case, the code won't compile because BadInheritedCountry is trying touse an obsolete class. However, if we didn't set error to true in ObsoleteAttribute'sconstructor, not only would the code compile, but clients would be able to do thiswithout getting a warning.BadInheritedCountry bic = new BadInheritedCountry();
ObsoleteAttribute was designed so that its information does not flow to a subclass. However, this choice is up to the creator of the attribute. Again, AttributeUsageAttributes is the center of control to determine how an attribute's information flows in inheritance scenarios. The default behavior is for attributes to be inheritable,10 but an attribute can be limited to the target to which it is applied. We'll come back to these design issues in the "Inheritance and Custom Attributes" section in Chapter 4.You now know how attributes work in C#. But the story doesn't stop there. When you compile your attribute-laden code, the metadata ends up in the assembly in one form or another. In the next section, you'll see where the attribute is located in an assembly and how that information is stored.