nameof Operator

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. For example, Listing 18.7 uses nameof to pass the property name to INotifyPropertyChanged.PropertyChanged.

Listing 18.7: Dynamically Invoking a Member
using System.ComponentModel;
 
public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    public Person(string name)
    {
        Name = name;
    }
    private string _Name = string.Empty;
    public string Name
    {
        get { return _Name; }
        set
        {
            if (_Name != value)
            {
                _Name = value;
                PropertyChanged?.Invoke(
                    this,
                    new PropertyChangedEventArgs(
                        nameof(Name)));
            }
        }
    }
    // ...
}

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.

Attributes

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.

Listing 18.8: Decorating a Property with an Attribute
public class CommandLineInfo
{
    [CommandLineSwitchAlias("?")]
    public bool Help { getset; }
 
    [CommandLineSwitchRequired]
    public string? Out { getset; }
 
    public System.Diagnostics.ProcessPriorityClass Priority
    { getset; } = 
        System.Diagnostics.ProcessPriorityClass.Normal;
}

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.

Listing 18.9: Decorating a Property with Multiple Attributes
[CommandLineSwitchRequired,
CommandLineSwitchAlias("FileName")]
public string? Out { getset; }
 
public System.Diagnostics.ProcessPriorityClass Priority
    { getset; } = 
        System.Diagnostics.ProcessPriorityClass.Normal;
}

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 (though not for .NET Core–generated 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.

Listing 18.10: Assembly Attributes within AssemblyInfo.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
// General information about an assembly is controlled
// through the following set of attributes. Change these
// attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("CompressionLibrary")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("IntelliTect")]
[assembly: AssemblyProduct("Compression Library")]
[assembly: AssemblyCopyright("Copyright© IntelliTect 2006-2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
 
// Setting ComVisible to false makes the types in this
// assembly not visible to COM components. If you need to
// access a type in this assembly from COM, set the ComVisible
// attribute to true on that type.
[assembly: ComVisible(false)]
 
// The following GUID is for the ID of the typelib
// if this project is exposed to COM
[assembly: Guid("417a9609-24ae-4323-b1d6-cef0f87a42c3")]
 
// Version information for an assembly consists
// of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can
// default the Revision and Build Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

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.

Listing 18.11: Specifying a Return Attribute
[return: Description(
   "Returns true if the object is in a valid state.")]
public bool IsValid()
{
    // ...
    return true;
}

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.

Listing 18.12: Defining a Custom Attribute
<Project>
  <PropertyGroup>
    <Company>Addison Wesley</Company>
    <Copyright>Copyright © Addison Wesley 2020</Copyright>
    <Product>Essential C# 8.0</Product>
    <Version>8.0</Version>
  </PropertyGroup>
</Project>
Output 18.5
[assembly: AssemblyCompany("Addison Wesley")]
[assembly: AssemblyCopyright("Copyright © Addison Wesley 2020")]
[assembly: AssemblyFileVersion("8.0.0.0")]
[assembly: AssemblyInformationalVersion("8.0")]
[assembly: AssemblyProduct("Essential C# 8.0")]
[assembly: AssemblyVersion("8.0.0.0")]
Guidelines
DO apply AssemblyVersionAttribute to assemblies with public types.
CONSIDER applying AssemblyFileVersionAttribute and AssemblyCopyrightAttribute to provide additional information about the assembly.
DO apply the following information assembly attributes: System.Reflection.AssemblyCompanyAttribute, System.Reflection.AssemblyCopyrightAttribute, System.Reflection.AssemblyDescriptionAttribute, and System.Reflection.AssemblyProductAttribute.
Custom Attributes

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.

Listing 18.13: Defining a Custom Attribute
public class CommandLineSwitchRequiredAttribute : Attribute
{
}

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.

Guidelines
DO name custom attribute classes with the suffix Attribute.
Looking for Attributes

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.

Listing 18.14: Retrieving a Custom Attribute
using System;
using System.Reflection;
using System.Collections.Generic;
 
public class CommandLineSwitchRequiredAttribute : Attribute
{
    public static string[] GetMissingRequiredOptions(
        object commandLine)
    {
        List<string> missingOptions = new List<string>();
        PropertyInfo[] properties =
            commandLine.GetType().GetProperties();
 
        foreach(PropertyInfo property in properties)
        {
            Attribute[] attributes =
                 (Attribute[])property.GetCustomAttributes(
                    typeof(CommandLineSwitchRequiredAttribute),
                    false);
            if (attributes.Length > 0 &&
                property.GetValue(commandLine, null) == null)
            {
                missingOptions.Add(property.Name);
            }
        }
        return missingOptions.ToArray();
    }
}

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?

Initializing an Attribute through a Constructor

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

Listing 18.15: Providing an Attribute Constructor
public class CommandLineSwitchAliasAttribute : Attribute
{
    public CommandLineSwitchAliasAttribute(string alias)
    {
        Alias = alias;
    }
    public string Alias { get; }
}
public class CommandLineInfo
{
    [CommandLineSwitchAlias("?")]
    public bool Help { getset; }
 
    // ...
}

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.

Listing 18.16: Retrieving a Specific Attribute and Checking Its Initialization
PropertyInfo property =
    typeof(CommandLineInfo).GetProperty("Help")!;
CommandLineSwitchAliasAttribute? attribute =
    (CommandLineSwitchAliasAttribute?)
        property.GetCustomAttribute(
        typeof(CommandLineSwitchAliasAttribute), false);
if(attribute?.Alias == "?")
{
    Console.WriteLine("Help(?)");
};

Furthermore, as Listing 18.17 and Listing 18.18 demonstrate, you can use similar code in a GetSwitches() method on CommandLineAliasAttribute 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.

Listing 18.17: Retrieving Custom Attribute Instances
using System;
using System.Reflection;
using System.Collections.Generic;
 
public class CommandLineSwitchAliasAttribute : Attribute
{
    public CommandLineSwitchAliasAttribute(string alias)
    {
        Alias = alias;
    }
 
    public string Alias { getset; }
 
    public static Dictionary<string, PropertyInfo> GetSwitches(
        object commandLine)
    {
        PropertyInfo[] properties;
        Dictionary<string, PropertyInfo> options =
            new Dictionary<string, PropertyInfo>();
 
        properties = commandLine.GetType().GetProperties(
            BindingFlags.Public | BindingFlags.Instance);
        foreach(PropertyInfo property in properties)
        {
            options.Add(property.Name, property);
            foreach (CommandLineSwitchAliasAttribute attribute in
                property.GetCustomAttributes(
                typeof(CommandLineSwitchAliasAttribute), false))
            {
                options.Add(attribute.Alias.ToLower(), property);
            }
        }
        return options;
    }
}
Listing 18.18: Updating CommandLineHandler.TryParse() to Handle Aliases
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Diagnostics;
 
public class CommandLineHandler
{
    // ...
    public static bool TryParse(
        string[] argsobject commandLine,
        out string? errorMessage)
    {
        bool success = false;
        errorMessage = null;
 
        Dictionary<string, PropertyInfo> options =
            CommandLineSwitchAliasAttribute.GetSwitches(
                commandLine);
 
        foreach (string arg in args)
        {
            string option;
            if(arg[0] == '/' || arg[0] == '-')
            {
                string[] optionParts = arg.Split(
                    new char[] { ':' }, 2);
                option = optionParts[0].Remove(0, 1).ToLower();
 
                if (options.TryGetValue(option, out PropertyInfo? property))
                {
                    success = SetOption(
                        commandLine, property,
                        optionParts, ref errorMessage);
                }
                else
                {
                    success = false;
                    errorMessage = 
                        $"Option '{option}' is not supported.";
                }
            }
        }
        return success;
    }
 
    private static bool SetOption(
        object commandLine, PropertyInfo property,
        string[] optionParts, ref string? errorMessage)
    {
        bool success;
 
        if(property.PropertyType == typeof(bool))
        {
            // Last parameters for handling indexers
            property.SetValue(
                commandLine, truenull);
            success = true;
        }
        else
        {
 
            if (optionParts.Length < 2
                || optionParts[1] == "")
            {
                // No setting was provided for the switch.
                success = false;
                errorMessage =
                     $"You must specify the value for the { property.Name } option.";
            }
            else if(
                property.PropertyType == typeof(string))
            {
                property.SetValue(
                    commandLine, optionParts[1], null);
                success = true;
            }
            else if(
                // property.PropertyType.IsEnum also available
                property.PropertyType ==
                    typeof(ProcessPriorityClass))
            {
                success = TryParseEnumSwitch(
                    commandLine, optionParts,
                    property, ref errorMessage);
            }
            else
            {
                success = false;
                errorMessage = 
                    $@"Data type '{ property.PropertyType.ToString() }' on {
                    commandLine.GetType().ToString() } is not supported.";
            }
        }
        return success;
    }
Guidelines
DO provide get-only properties (without public setters) on attributes with required property values.
DO provide constructor parameters to initialize properties on attributes with required properties. Each parameter should have the same name (albeit with different casing) as the corresponding property.
AVOID providing constructor parameters to initialize attribute properties corresponding to the optional arguments (and, therefore, avoid overloading custom attribute constructors).
System.AttributeUsageAttribute

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.

Listing 18.19: Restricting the Constructs an Attribute Can Decorate
[AttributeUsage(AttributeTargets.Property)]
public class CommandLineSwitchAliasAttribute : Attribute
{
    // ...
}

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.

Listing 18.20:AttributeUsageAttribute Restricting Where to Apply an Attribute
// ERROR: The attribute usage is restricted to properties
[CommandLineSwitchAlias("?")] 
class CommandLineInfo
{
}
Output 18.6
...Program+CommandLineInfo.cs(24,17): error CS0592: Attribute
'CommandLineSwitchAlias' is not valid on this declaration type. It is
valid on 'property, indexer' declarations only.

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.

Listing 18.21: Limiting an Attribute’s Usage with AttributeUsageAttribute
// Restrict the attribute to properties and methods
[AttributeUsage(
  AttributeTargets.Field | AttributeTargets.Property)]
public class CommandLineSwitchAliasAttribute : Attribute
{
    // ...
}
Guidelines
DO apply the AttributeUsageAttribute class to custom attributes.
Named Parameters

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.

Listing 18.22: Using a Named Parameter
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CommandLineSwitchAliasAttribute : Attribute
{
    // ...
}

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

Beginner Topic
FlagsAttribute

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

Listing 18.23: Using FlagsAttribute
/*
[Flags]
public enun FileAttributes
{
    ReadOnly = 0x0001,
    Hidden   = 0x0002,
    // ...
}
*/
 
using System;
using System.IO;
 
public class Program
{
    public static void Main()
    {
        // ...
        string fileName = @"enumtest.txt";
        FileInfo file = new FileInfo(fileName);
 
        file.Attributes = FileAttributes.Hidden |
            FileAttributes.ReadOnly;
 
        Console.WriteLine("\"{0}\" outputs as \"{1}\"",
            file.Attributes.ToString().Replace(","" |"),
            file.Attributes);
 
        FileAttributes attributes =
            (FileAttributes)Enum.Parse(typeof(FileAttributes),
            file.Attributes.ToString());
 
        Console.WriteLine(attributes);
 
        // ...
    }
}
Output 18.7
"ReadOnly | Hidden" outputs as "ReadOnly, Hidden"

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.

Predefined Attributes

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

System.ConditionalAttribute

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.

Listing 18.24: Using ConditionalAttribute to Eliminate a Call
#define CONDITION_A
// ...
using System;
using System.Diagnostics;
 
public class Program
{
    public static void Main()
    {
        Console.WriteLine("Begin...");
        MethodA();
        MethodB();
        Console.WriteLine("End...");
    }
 
    [Conditional("CONDITION_A")]
    public static void MethodA()
    {
        Console.WriteLine("MethodA() executing...");
    }
 
    [Conditional("CONDITION_B")]
    public static void MethodB()
    {
        Console.WriteLine("MethodB() executing...");
    }
}
Output 18.8
Begin...
MethodA() executing...
End...

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 notices the attribute on a called method during compilation; assuming the preprocessor identifier exists, it then eliminates any calls to the method. ConditionalAttibute, 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.

System.ObsoleteAttribute

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.

Listing 18.25: Using ObsoleteAttribute
public class Program
{
    public static void Main()
    {
        ObsoleteMethod();
    }
 
    [Obsolete]
    public static void ObsoleteMethod()
    {
    }
}
Output 18.9
c:\SampleCode\ObsoleteAttributeTest.cs(24,17): warning CS0612:
Program.ObsoleteMethod()' is obsolete

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.

________________________________________

3. Introduced in C# 6.0.
4. Starting in C# 5.0.
5. Starting in C# 4.0.
6. . A feature introduced in Microsoft .NET Framework 2.0.
{{ snackbarMessage }}