We briefly touched on the nameof operator in Chapter 11, where it was used to provide the name of a parameter in an argument exception:
throw new ArgumentException(
"The argument did not represent a digit", nameof(textDigit));
This contextual keyword3 produces a constant string containing the unqualified name of whatever program element is specified as an argument. In this case, textDigit is a parameter to the method, so nameof(textDigit) returns “textDigit.” (Given that this activity happens at compile time, nameof is not technically reflection. We include it here because ultimately it receives data about the assembly and its structure.)
You might ask what advantage is gained by using nameof(textDigit) over simply "textDigit" (especially given that the latter might even seem easier to use to some programmers). The advantages are twofold:
In the snippet given earlier, nameof(textDigit) produces the name of a parameter. In reality, the nameof operator works with any program element that is in scope. In fact, starting in C# 11, you can even use nameof with parameter names of the method an attribute is decorating. For example, Listing 18.7 uses nameof to pass the property name to INotifyPropertyChanged.PropertyChanged.
No matter whether only the unqualified “Name” is provided (because it’s in scope) or the fully (or partially) qualified name such as Person.Name is used, the result is only the final identifier (the last element in a dotted name).
You can still use the CallerMemberName parameter attribute4 to obtain a property’s name; see http://itl.tc/CallerMemberName for an example.
Before delving into the details of how to program attributes, we should consider a use case that demonstrates their utility. In the CommandLineHandler example in Listing 18.3, you dynamically set a class’s properties based on the command-line option matching the property name. This approach is insufficient, however, when the command-line option is an invalid property name. The command-line option /?, for example, cannot be supported. Furthermore, this mechanism doesn’t provide any way of identifying which options are required versus which are truly optional.
Instead of relying on an exact match between the option name and the property name, you can use attributes to identify additional metadata about the decorated construct—in this case, the option that the attribute decorates. With attributes, you can decorate a property as Required and provide a /? option alias. In other words, attributes are a means of associating additional data with a property (and other constructs).
Attributes appear within square brackets preceding the construct they decorate. For example, you can modify the CommandLineInfo class to include attributes, as shown in Listing 18.8.
In Listing 18.8, the Help and Out properties are decorated with attributes. These attributes have two purposes: They allow an alias of /? for /Help, and they indicate that /Out is a required parameter. The idea is that from within the CommandLineHandler.TryParse() method, you enable support for option aliases and, assuming the parsing was successful, you check that all required switches were specified.
Attributes may be associated with the same construct in two ways. First, you can separate the attributes with commas within the same square brackets. Alternatively, you can place each attribute within its own square brackets. Listing 18.9 provides examples.
In addition to decorating properties, developers can use attributes to decorate assemblies, classes, constructors, delegates, enums, events, fields, generic parameters, interfaces, methods, modules, parameters, properties, return values, and structs. For the majority of these cases, applying an attribute involves the same square brackets syntax shown in Listing 18.9. However, this syntax doesn’t work for return values, assemblies, and modules.
Assembly attributes are used to add metadata about the assembly. Visual Studio’s Project Wizard for .NET Framework projects, for example, generates an AssemblyInfo.cs file that includes numerous attributes about the assembly. Listing 18.10 is an example of such a file.
The assembly attributes define things such as the company, product, and assembly version number. Similar to using assembly, identifying an attribute usage as module requires prefixing it with module:. The restriction on assembly and module attributes is that they must appear after the using directive but before any namespace or class declarations. The attributes in Listing 18.10 are generated by the Visual Studio Project Wizard and should be included in all projects to mark the resultant binaries with information about the contents of the executable or dynamic link library (DLL).
Return attributes, such as the one shown in Listing 18.11, appear before a method declaration but use the same type of syntax structure.
In addition to assembly: and return:, C# allows for explicit target identifications of module:, class:, and method:, corresponding to attributes that decorate the module, class, and method, respectively. class: and method:, however, are optional, as demonstrated earlier.
One of the conveniences of using attributes is that the language takes into consideration the attribute naming convention, which calls for Attribute to appear at the end of the name. However, in all the attribute uses in the preceding listings, no such suffix appears, even though each attribute used follows the naming convention. This is because although the full name (DescriptionAttribute, AssemblyVersionAttribute, and so on) is allowed when applying an attribute, C# makes the suffix optional. Generally, no such suffix appears when applying an attribute; rather, it appears only when defining one or using the attribute inline (such as typeof(DescriptionAttribute)).
Note that instead of generating an AssemblyInfo.cs file, .NET Core–based projects allow specification of the assembly information within the *.CSPROJ file. Listing 18.12, for example, injects corresponding assembly attributes into the assembly at compile time. The results are shown in Output 18.5.
Defining a custom attribute is relatively trivial. Attributes are objects; therefore, to define an attribute, you need to define a class. The characteristic that turns a general class into an attribute is that it derives from System.Attribute. Consequently, you can create a CommandLineSwitchRequiredAttribute class, as shown in Listing 18.13.
With that simple definition, you now can use the attribute as demonstrated in Listing 18.8. So far, no code responds to the attribute; therefore, the Out property that includes the attribute will have no effect on command-line parsing.
In addition to providing properties for reflecting on a type’s members, Type includes methods to retrieve the Attributes decorating that type. Similarly, all the reflection types (e.g., PropertyInfo and MethodInfo) include members for retrieving a list of attributes that decorate a type. Listing 18.14 defines a method to return a list of required switches that are missing from the command line.
The code that checks for an attribute is relatively simple. Given a PropertyInfo object (obtained via reflection), you call GetCustomAttributes() and specify the attribute sought, then indicate whether to check any overloaded methods. (Alternatively, you can call the GetCustomAttributes() method without the attribute type to return all of the attributes.)
Although it is possible to place code for finding the CommandLineSwitchRequiredAttribute attribute within the CommandLineHandler’s code directly, it makes for better object encapsulation to place the code within the CommandLineSwitchRequiredAttribute class itself. This is frequently the pattern for custom attributes. What better location to place code for finding an attribute than in a static method on the attribute class?
The call to GetCustomAttributes() returns an array of objects that can be cast to an Attribute array. Because the attribute in our example didn’t have any instance members, the only metadata information that it provided in the returned attribute was whether it appeared. Attributes can also encapsulate data, however. Listing 18.15 defines a CommandLineSwitchAliasAttribute attribute—a custom attribute that provides alias command-line options. For example, you can provide command-line support for /Help or /? as an abbreviation. Similarly, /S could provide an alias to /Subfolders that indicates the command should traverse all the subdirectories.
To support this functionality, you need to provide a constructor for the attribute. Specifically, for the alias, you need a constructor that takes a string argument. (Similarly, if you want to allow multiple aliases, you need to define an attribute that has a params string array for a parameter.)
When applying an attribute to a construct, only constant values and typeof() expressions are allowed as arguments. This constraint is required to enable their serialization into the resultant CIL. It implies that an attribute constructor should require parameters of the appropriate types; creating a constructor that takes arguments of type System.DateTime would be of little value, as there are no System.DateTime constants in C#.
The objects returned from PropertyInfo.GetCustomAttributes() will be initialized with the specified constructor arguments, as demonstrated in Listing 18.16.
Furthermore, as Listing 18.17 and Listing 18.18 demonstrate, you can use similar code in a GetSwitches() method on CommandLineSwitchAliasAttribute that returns a dictionary collection of all the switches, including those from the property names, and associate each name with the corresponding attribute on the command-line object.
Most attributes are intended to decorate only particular constructs. For example, it makes no sense to allow CommandLineOptionAttribute to decorate a class or an assembly, as the attribute would be meaningless in those contexts. To avoid inappropriate use of an attribute, custom attributes can be decorated with System.AttributeUsageAttribute (yes, an attribute is decorating a custom attribute declaration). Listing 18.19 (for CommandLineOptionAttribute) demonstrates how to do this.
If the attribute is used inappropriately, as it is in Listing 18.20, it will cause a compile-time error, as Output 18.6 demonstrates.
AttributeUsageAttribute’s constructor takes an AttributeTargets flag. This enum provides a list of all possible targets that the runtime allows an attribute to decorate. For example, if you also allowed CommandLineSwitchAliasAttribute on a field, you would update the AttributeUsageAttribute class, as shown in Listing 18.21.
In addition to restricting what an attribute can decorate, AttributeUsageAttribute provides a mechanism for allowing duplicates of the same attribute on a single construct. The syntax appears in Listing 18.22.
This syntax is different from the constructor initialization syntax discussed earlier. The AllowMultiple parameter is a named parameter, similar to the named parameter syntax used for optional method parameters.5 Named parameters provide a mechanism for setting specific public properties and fields within the attribute constructor call, even though the constructor includes no corresponding parameters. The named attributes are optional designations, but they provide a means of setting additional instance data on the attribute without providing a constructor parameter for the purpose. In this case, AttributeUsageAttribute includes a public member called AllowMultiple; you can set this member using a named parameter assignment when you use the attribute. Assigning named parameters must occur as the last portion of a constructor, following any explicitly declared constructor parameters.
Named parameters allow for assigning attribute data without providing constructors for every conceivable combination of which attribute properties are specified and which are not. Given that many of an attribute’s properties may be optional, this is a useful construct in many cases.
Chapter 9 introduced enums and included an Advanced Topic covering FlagsAttribute. This framework-defined attribute targets enums that represent flag type values. The Beginner Topic here also addresses FlagsAttribute, starting with the sample code shown in Listing 18.23 with Output 18.7.
The flag documents that the enumeration values can be combined. Furthermore, it changes the behavior of the ToString() and Parse() methods. For example, calling ToString() on an enumeration that is decorated with FlagsAttribute writes out the strings for each enumeration flag that is set. In Listing 18.23, file.Attributes.ToString() returns "ReadOnly, Hidden" rather than the 3 it would have returned without the FlagsAttribute flag. If two enumeration values are the same, the ToString() call would return the first one. As mentioned earlier, however, you should use caution when relying on this outcome because it is not localizable.
Parsing a value from a string to the enumeration also works, provided that each enumeration value identifier is separated by a comma.
Note that FlagsAttribute does not automatically assign the unique flag values or check that flags have unique values. The values of each enumeration item still must be assigned explicitly.
The AttributeUsageAttribute attribute has a special characteristic that you haven’t seen yet in the custom attributes you have created in this book. This attribute affects the behavior of the compiler, causing it to sometimes report an error. Unlike the reflection code that you wrote earlier for retrieving CommandLineSwitchRequiredAttribute and CommandLineSwitchAliasAttribute, AttributeUsageAttribute has no runtime code; instead, it has built-in compiler support.
AttributeUsageAttribute is a predefined attribute. Not only do such attributes provide additional metadata about the constructs they decorate, but the runtime and compiler also behave differently to facilitate these attributes’ functionality. Attributes such as AttributeUsageAttribute, FlagsAttribute, ObsoleteAttribute, and ConditionalAttribute are examples of predefined attributes. They implement special behavior that only the CLI provider or compiler can offer because there are no extension points for additional noncustom attributes. In contrast, custom attributes are entirely passive. Listing 18.23 includes a couple of predefined attributes; Chapter 19 includes a few more.
Within a single assembly, the System.Diagnostics.ConditionalAttribute attribute behaves a little like the #if/#endif preprocessor identifier. However, instead of eliminating the CIL code from the assembly, System.Diagnostics.ConditionalAttribute will optionally cause the call to behave like a no-op, an instruction that does nothing. Listing 18.24 demonstrates the concept, and Output 18.8 shows the results.
This example defined CONDITION_A, so MethodA() executed normally. CONDITION_B, however, was not defined either through #define or by using the csc.exe /Define option. As a result, all calls to Program.MethodB() from within this assembly will do nothing.
Functionally, ConditionalAttribute is similar to placing an #if/#endif around the method invocation. The syntax is cleaner, however, because developers create the effect by adding the ConditionalAttribute attribute to the target method without making any changes to the caller itself.
The C# compiler recognizes the attribute on a called method during compilation; if the preprocessor identifier is not defined, it then eliminates any calls to the method. ConditionalAttribute, however, does not affect the compiled CIL code on the target method itself (besides the addition of the attribute metadata). Instead, it affects the call site during compilation by removing the calls. This further distinguishes ConditionalAttribute from #if/#endif when calling across assemblies. Because the decorated method is still compiled and included in the target assembly, the determination of whether to call a method is based not on the preprocessor identifier in the callee’s assembly, but rather on the caller’s assembly. In other words, if you create a second assembly that defines CONDITION_B, any calls to Program.MethodB() from the second assembly will execute. This is a useful characteristic in many tracing and testing scenarios. In fact, calls to System.Diagnostics.Trace and System.Diagnostics.Debug use this trait with ConditionalAttributes on TRACE and DEBUG preprocessor identifiers.
Because methods don’t execute whenever the preprocessor identifier is not defined, ConditionalAttribute may not be used on methods that include an out parameter or specify a return other than void. Doing so causes a compile-time error. This makes sense because potentially none of the code within the decorated method will execute, so it is unknown what to return to the caller. Similarly, properties cannot be decorated with ConditionalAttribute. The AttributeUsage (see the section titled “System.AttributeUsageAttribute” earlier in this chapter) for ConditionalAttribute6 is decorated with the AttributeTargets.Class and AttributeTargets.Method, which allow the attribute to be used on either a method or a class, respectively. However, the class usage is special because ConditionalAttribute is allowed only on System.Attribute-derived classes.
When ConditionalAttribute decorates a custom attribute, the latter can be retrieved via reflection only if the conditional string is defined in the calling assembly. Without such a conditional string, reflection that looks for the custom attribute will fail to find it.
As mentioned earlier, predefined attributes affect the compiler’s and/or the runtime’s behavior. ObsoleteAttribute provides another example of attributes affecting the compiler’s behavior. Its purpose is to help with versioning of code by providing a means of indicating to callers that a member or type is no longer current. Listing 18.25 provides an example of ObsoleteAttribute’s use. As Output 18.9 shows, any callers that compile code that invokes a member marked with ObsoleteAttribute will cause a compile-time warning and, optionally, an error.
In this case, ObsoleteAttribute simply displays a warning. However, the attribute has two additional constructors. The first constructor, ObsoleteAttribute(string message), appends the additional message argument to the compiler’s obsolete message. The best practice for this message is to provide direction on what replaces the obsolete code. The second constructor is a bool error parameter that forces the warning to be recorded as an error instead.
ObsoleteAttribute allows third parties to notify developers of deprecated APIs. The warning (not an error) allows the original API to continue to work until the developer is able to update the calling code.
Starting with C# 11.0, C# supports generic attributes. For example, consider the custom attribute ExpectedException<TException> shown in Listing 18.26.
Here we have a generic attribute that is used to decorate a test method. For this test method to pass, it must throw an exception of type TException. In Listing 18.26, ThrowsDivideByZeroExceptionTest() is expected to throw a DivideByZeroException and is identified by the ExpectedException<DivideByZeroException> that decorates it.
In C# 10.0, Caller* Attributes (frequently referred to as CallerArgumentExpression attributes) were added. With these attributes, the compiler injects argument values of the caller for the invoked method. Consider, for example, the invocation of the AssertExceptionThrown() method:
ExpectedException<DivideByZeroException>.AssertExceptionThrown(
() => throw new Exception(),
"() => throw new Exception()",
nameof(Method),
"./FileName.cs");
Notice that in addition to the type argument for the expected exception type, there is also an expression and a repeat of the expression as a string, along with the method name and file name of the caller. Without any additional language support, all these arguments are required if the AssertExceptionThrown() method is to include complete diagnostics when the expected exception it not thrown. Even though all the values other than the initial expression are obviously trivial to determine automatically. (In this case, the duplication of the expression as a string is especially annoying.) Nonetheless, they all still need to be specified for the called method to have all the necessary information to provide a detailed error message (see Listing 18.27).
Notice that all the parameters, other than testAction, have 1) an attribute and 2) a default value. The attributes provide metadata to the compiler that the parameter values, which should not be specified by the caller, should be provided by the compiler. As a result, the invocation for the method is simplified significantly as shown in Listing 18.28.
Now, even though no argument is specified in the source code for the testActionMemberName parameter (for example), the string value “Method” still is injected and available in the AssertExceptionThrown() method. Obviously, there is a limited set of such Caller* attributes as shown in Table 18.1.
Attribute |
Description |
Type |
CallerFilePathAttribute |
Full path of the source file that contains the caller. The full path is the path at compile time. |
string |
CallerLineNumberAttribute |
Line number in the source file from which the method is called. |
System.Int32 |
CallerMemberNameAttribute |
Method name or property name of the caller. |
string |
CallerArgumentExpressionAttribute |
String representation of the argument expression. |
string |
All the Caller* attributes are limited to use on parameter constructs and mostly self-explanatory from Listing 18.27. The one exception is the CallerArgumentExpressionAttrribute since it requires its own parameter. The CallerArgumentExpressionAttrribute identifies (as text) the expression used for another parameter within the method. For example, in Listing 18.28 the expression () => throw new DivideByZeroException() is for the testAction parameter. And, since the CallerArgumentExpressionAttrribute decorating the testExpression parameter has nameof(testAction) as a parameter for the attribute (thus identifying the testAction parameter), the compiler injects the string “() => throw new Exception()” as the value for the testExpression parameter. (Allowing nameof as an attribute parameter to refer to a parameter in the same method signature was added in C# 11.) In summary, the CallerArgumentExpressionAttrribute decorates parameter testExpression and takes a parameter of its own—a string that identifies testAction as the parameter for which it should extract the expression and provide it as text for the testExpression argument. (If the parameter that CallerArgumentExpressionAttrribute is pointing to is an extension method, this is an allowable value.)
Note that for the compiler to allow Caller* attribute decorated parameters to not require arguments, it is necessary to provide a default value. Even though the data types on the parameters should be non-nullable – deterring a caller from explicitly providing a null value and usurping the compiler injecting a value – the only reasonable value to specify for the default is null. In a somewhat contradiction, therefore, the string parameters decorated by the Caller* attributes are non-nullable but assigned null values with a null forgiveness operator. This is done because the compiler is expected to appropriately provide values for all of these arguments.
________________________________________