Programming with Attributes - Diving Deeper - Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Part V: Diving Deeper

Hour 21. Programming with Attributes


What You’ll Learn in This Hour

Understanding attributes

Using the caller info attributes

Working with the common attributes

Using custom 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.

Understanding Attributes

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.


Tip: Attribute Names

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.

Listing 21.1. Using the FlagsAttribute


[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.

Listing 21.2. Additional Attribute Usage


[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.

Listing 21.3. Attribute Parameters


[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.

Table 21.1. Attribute Targets

Image

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.

Listing 21.4. Attribute Parameters


[CustomAttribute]
string Method()
{
return String.Empty;
}

[method: CustomAttribute]
string Method()
{
return String.Empty;
}

[return: CustomAttribute]
string Method()
{
return String.Empty;
}


Using the Caller Info Attributes

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.

Table 21.2. Caller Info Attributes

Image

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.

Listing 21.5. Using the Caller Info Attributes


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.

Listing 21.6. INotifyPropertyChanged Using CallerMemberNameAttribute


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.

Table 21.3. CallerMemberNameAttribute Results

Image

Working with the Common Attributes

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

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.

Listing 21.7. 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

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.

Listing 21.8. 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 theConditional method.


The Global Attributes

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).


Note: Assembly Manifest

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.

Table 21.4. Global Attributes

Image

Using Custom Attributes

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.

Listing 21.9. Creating a Custom Attribute


[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.

Listing 21.10. Applying a Custom Attribute


[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;
}
}


Accessing Attributes at Runtime

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.

Image

Figure 21.1. Output of retrieving a single attribute at runtime.

Listing 21.11. Accessing a Single Attribute at Runtime


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.

Listing 21.12. Accessing Multiple Attributes at Runtime


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);
}
}
}


Summary

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&A

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.

Workshop

Quiz

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?

Answers

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.

Exercises

There are no exercises for this hour.