What You’ll Learn in This Hour
• Using the caller info attributes
• Working with the common attributes
• Accessing attributes at runtime
In Hour 1, “The .NET Framework and C#,” you learned that C# supports component-oriented programming by enabling you to create assemblies that are self-contained, self-documenting, redistributable units of code. Metadata specifies how the types, properties, events, methods, and other code elements relate to one another. Attributes enable you to add additional metadata to an assembly, providing a way of associating declarative information with your code. Attributes are compiled and stored in the resulting common intermediate language (CIL) and can be accessed at runtime.
The .NET Framework provides and uses attributes for many different purposes. You saw one example of attributes in Hour 6, “Creating Enumerated Types and Structures,” when you learned about flags enumerations. Attributes also describe security, describe how to serialize data, limit optimizations by the Just-In-Time (JIT) compiler, and control the visibility of properties and methods during application development, among many other things. To add your own custom metadata, you use custom attributes that you create.
In this hour, you learn more about attributes—how to use them, how to create your own custom attributes, and how to access them at runtime.
Although attributes can be added to almost any code declaration, including assemblies, classes, methods, properties, and fields, many are valid only on certain code declarations. For example, some attributes are valid only on methods, whereas others are valid only on type declarations.
Although it is considered a best practice for all attribute names to end with the word Attribute so that they can be easily distinguished from other types, you don’t need to specify the Attribute suffix when using them in code. For example, [Flags]
is equivalent to [FlagsAttribute]
. The actual class name for the attribute is FlagsAttribute
.
To place an attribute on a code declaration, you place the name of the attribute enclosed in square brackets ([ ]
) immediately before the declaration to which it is applied. For example, the System.IO.FileShare
enumeration shown in Listing 21.1 has the FlagsAttribute
applied.
[Flags]
public enum FileShare
{
None = 0,
Read = 0x001,
Write = 0x002,
ReadWrite = 0x003,
Delete = 0x004,
Inheritable = 0x010,
}
A code declaration can have multiple attributes, and some attributes can be specified more than once for the same declaration, as shown in Listing 21.2.
[Conditional("DEBUG"), Conditional("EXAMPLE")]
void Method() { }
void TestMethod([In][Out] string value) { }
void TestMethod2([In, Out] string value) { }
When you apply an attribute to a code declaration, you, in effect, are calling one of the constructors of the attribute class. This means that you can provide parameters to the attribute, as shown by the ConditionalAttribute
from Listing 21.2.
Note: Named Attribute Parameters
The named parameters used by attributes are not the same as the named parameters you learned about in Hour 4, “Understanding Classes and Objects the C# Way.” When used with attributes, they are really more like object initializers (which you also learned about in Hour 4); they actually correspond to public read-write properties of the attribute, easily allowing you to set the property value.
Parameters defined by a constructor are called positional parameters because they must be specified in a defined order and cannot be omitted. Attributes also make use of named parameters, which are optional and can be specified in any order. Positional parameters must always be specified first. For example, the attributes shown in Listing 21.3 are all equivalent.
[DllImport("kernel32.dll")]
[DllImport("kernel32.dll", SetLastError = false, ExactSpelling = true)]
[DllImport("kernel32.dll", ExactSpelling = true, SetLastError = false)]
The type of code declaration to which an attribute is applied is called the target. Table 21.1 shows the possible target values.
Although an attribute normally applies to the element it precedes, you can also explicitly identify the target to which it applies. For example, you can identify whether an attribute applies to a method, its parameter, or its return value, as shown in Listing 21.4.
[CustomAttribute]
string Method()
{
return String.Empty;
}
[method: CustomAttribute]
string Method()
{
return String.Empty;
}
[return: CustomAttribute]
string Method()
{
return String.Empty;
}
The Caller Info attributes allow you to obtain information about the caller to a method, such as the path to the source file, the line number where the method was called, and the name of the caller. The Caller Info attributes are shown in Table 21.2.
These attributes are applied to optional parameters and change the default value that’s passed in when the argument is omitted. Listing 21.5 shows an example of how to use the Caller Info attributes. Because these attributes are defined in the System.Runtime.CompilerServices
namespace, you should add a using
statement to include this namespace in your code file.
public void DoSomething()
{
Log("Performing some action.");
}
public void Log(string message,
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string filePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int lineNumber = 0)
{
System.Diagnostics.Debug.WriteLine("message: {0}", memberName);
System.Diagnostics.Debug.WriteLine("filePath: {0}", filePath);
System.Diagnostics.Debug.WriteLine("lineNumber: {0}", lineNumber);
}
The most common use for the Caller Info attributes is to specify the name of the caller for tracing and diagnostic methods or for implementing data-binding interfaces, such as INotifyPropertyChanged
. An example of how to use the CallerMemberNameAttribute
is shown in Listing 21.6.
public class Contact : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string firstName;
private string lastName;
public string FirstName
{
get
{
return this.firstName;
}
set
{
this.firstName = value;
NotifyPropertyChanged();
}
}
public string LastName
{
get
{
return this.lastName;
}
set
{
this.lastName = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Table 21.3 shows the member name values returned when using the CallerMemberNameAttribute
.
The .NET Framework defines many attributes that you can use in your own applications. The most common ones are the Obsolete
attribute, the Conditional
attribute, and the set of global attributes.
The Obsolete
attribute indicates that a code declaration should no longer be used and causes the compiler to generate a warning or error, depending on how the attribute is configured. This attribute can be used with no parameters, but it is recommended to supply an explanation and indicate if a compiler warning or error should be generated. Listing 21.7 shows an example of using the Obsolete
attribute.
public class Example
{
[Obsolete("Consider using OtherMethod instead.", false)]
public string Method()
{
return String.Empty;
}
public string OtherMethod()
{
return "Test";
}
}
The Conditional
attribute indicates that a code declaration is dependent on a preprocessor conditional compilation symbol, such as DEBUG
, and can be applied to a class or a method. The compiler uses this attribute to determine if the call is included or left out. If the conditional symbol is present during compilation, the call is included; otherwise, it is not. If the Conditional
attribute is applied to a method, the method must not have a return value. Listing 21.8 shows an example of using the Conditional
attribute.
public class Example
{
[Conditional("DEBUG")]
public void DisplayDiagnostics()
{
Console.WriteLine("Diagnostic information.");
}
public string Method()
{
return "Test";
}
}
The Conditional
attribute can also be applied to custom attributes, in which case the attribute adds metadata information only if the compilation symbol is defined.
Note: The #if and #endif Preprocessor Symbols
Using the Conditional
attribute is similar to using the #if
and #endif
preprocessor symbols but can provide a cleaner alternative that leads to fewer bugs. The class shown in Listing 21.8 using #if/#endif
instead of the Conditional
attribute is shown here.
public class Example
{
#if DEBUG
public void DisplayDiagnostics()
{
Console.WriteLine("Diagnostic information.");
}
#endif
public string Method()
{
return "Test";
}
}
Although you can mix the Conditional
attribute and the #if/#endif
preprocessor symbols, you need to be very careful if you do so. Removing code using #if/#endif
occurs earlier in the compilation process and may cause the compiler to be unable to compile the Conditional
method.
Although most attributes apply to specific code declarations, some apply to an entire assembly or module. These attributes appear in the source code after any using
directives but before any code declarations (such as class or namespace declarations).
The assembly manifest contains the data that describes how the elements in the assembly are related, including version and security information. The manifest is typically included with the compiled file.
Typically, global attributes are placed in an AssemblyInfo.cs
file, but they can appear in multiple files if those files are compiled in a single compilation pass. The common global attributes are shown in Table 21.4.
If you need to provide custom metadata for your own applications, you can create custom attributes by defining an attribute class that derives from Attribute
, either directly or indirectly.
For example, you can define a custom attribute that contains the Team Foundation Server Work Item number associated with a code change, as shown in Listing 21.9.
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public sealed class WorkItemAttribute : System.Attribute
{
private int workItemId;
public WorkItemAttribute(int workItemId)
{
this.workItemId = workItemId;
}
public int WorkItemId
{
get
{
return this.workItemId;
}
}
public string Comment
{
get;
set;
}
}
The custom attribute has a single public constructor whose parameters form the attribute’s positional parameters. Any public read-write fields or properties—in this case, the Comment
property—become named parameters. Finally, the AttributeUsage
attribute indicates that the WorkItem
attribute is valid on any declaration, that it can be applied more than once, and that it is not automatically applied to derived types.
You can then use this attribute as shown in Listing 21.10.
[WorkItem(1234, Comment = "Created class showing attributes being used.")]
public class Test
{
[WorkItem(5678, Comment = "Changed property to use auto-property syntax.")]
public int P
{
get;
set;
}
}
Adding metadata to your application through attributes doesn’t do much if you can’t access that information at runtime. The .NET Framework provides access to the runtime type information, including metadata provided by attributes, through a process called reflection. Because attributes provide metadata, the code associated with an attribute is not actually executed until the attributes are queried.
Retrieving custom attributes is easy using the Attribute.GetCustomAttribute
method. Listing 21.11 shows an example of accessing the custom attribute defined in Listing 21.9 and the simple class defined in Listing 21.10 at runtime. Figure 21.1 shows the output of Listing 21.11.
public class Program
{
public static void Main(string[] args)
{
WorkItemAttribute attribute =
Attribute.GetCustomAttribute(typeof(Test), typeof(WorkItemAttribute))
as WorkItemAttribute;
if (attribute != null)
{
Console.WriteLine("{0}: {1}", attribute.WorkItemId, attribute.Comment);
}
}
}
Attribute.GetCustomAttribute
returns a single attribute. If multiple attributes of the same type defined on the code element exist or you need to work with multiple attributes of different types, you can use the Attribute.GetCustomAttributes
method to return an array of custom attributes. You can then enumerate the resulting array, examining and extracting information from the array elements, as shown in Listing 21.12.
public class Program
{
public static void Main(string[] args)
{
var workItems = from attribute in
Attribute.GetCustomAttributes(typeof(Test)).
OfType<WorkItemAttribute>()
select attribute;
foreach (var attribute in workItems)
{
Console.WriteLine("{0}: {1}", attribute.WorkItemId, attribute.Comment);
}
}
}
Attributes provide a simple yet powerful way to add metadata to your applications. They are used throughout the .NET Framework for calling unmanaged code; describing component object model (COM) properties for classes, methods, and interfaces; describing which class members should be serialized for persistence; specifying security requirements; and controlling JIT compiler optimizations, just to name a few.
In this hour, you learned about attributes, including some of the common attributes provided by the .NET Framework. You then created your own custom attribute and learned how to retrieve that attribute and access its values at runtime.
Q. What is an attribute?
A. An attribute is a class that is used to add metadata to a code element.
Q. What are positional attribute parameters?
A. Positional attribute parameters are parameters required by the attribute and must be provided in a specific order. They are defined by the attribute’s constructor parameters.
Q. How do you define a custom attribute?
A. A custom attribute is defined by creating a class that derives from Attribute
.
1. Can the Obsolete
attribute generate compiler errors?
2. Does the Conditional
attribute affect compilation?
3. What are two methods that can retrieve custom attributes at runtime?
1. Yes, the Obsolete
attribute can generate a compiler error if the second positional parameter is set to true
.
2. Yes, the Conditional
attribute can affect compilation. If the conditional symbol specified in the attribute parameters is not present, the call to the method is not included.
3. Two methods to retrieve custom attributes at runtime are Attribute.GetCustomAttribute
and Attribute.GetCustomAttributes
.
There are no exercises for this hour.