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:

The C# compiler ensures that the argument to the nameof operator is, in fact, a valid program element. This helps prevent errors when a program element name is changed, helps prevent misspellings, and so on.
IDE tools work better with the nameof operator than with literal strings. For example, the “find all references” tool will find program elements mentioned in a nameof expression but not in a literal string. The automatic refactoring also works better, and so on.

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.

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, 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.Reflection;
using System.Collections.Generic;
 
public class CommandLineSwitchRequiredAttribute : Attribute
{
    public static string[] GetMissingRequiredOptions(
        object commandLine)
    {
        List<string> missingOptions = new();
        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, nullis 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 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.

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

Listing 18.17: Retrieving Custom Attribute Instances
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();
 
        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.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 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.

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 enum FileAttributes
{
    ReadOnly = 0x0001,
    Hidden   = 0x0002,
    // ...
}
*/
 
public class Program
{
    public static void Main()
    {
        // ...
        string fileName = @"enumtest.txt";
        FileInfo file = new(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 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.

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

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.

Generic Attributes

Starting with C# 11.0, C# supports generic attributes. For example, consider the custom attribute ExpectedException<TException> shown in Listing 18.26.

Listing 18.26: Generic Attributes
public class SampleTests
{
    [ExpectedException<DivideByZeroException>]
    public static void ThrowDivideByZeroExceptionTest()
    {
        var result = 1/"".Length;
    }
}
 
[AttributeUsage(AttributeTargets.Method)]
public class ExpectedException<TException> : 
    Attribute where TException : Exception
{
    public static TException AssertExceptionThrown(Action testMethod)
    {
        try
        {
            testMethod();
            throw new InvalidOperationException(
                $"The expected exception, {
                    typeof(TException).FullName }, was not thrown.");
        }
        catch (TException exception) 
        {
            return exception;
        }
    }
 
    // Attribute detection
    // ...
}

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.

Caller* Attributes

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

Listing 18.27: CallerArgumentExpression Attributes Example
public class SampleTests
{
    [ExpectedException<DivideByZeroException>]
    public static void ThrowArgumentNullExceptionTest()
    {
        var result = 1/"".Length;
    }
}
 
[AttributeUsage(AttributeTargets.Method)]
public class ExpectedException<TException> : 
    Attribute where TException : Exception
{
    public static TException AssertExceptionThrown(
        Action testAction,
        [CallerArgumentExpression(nameof(testAction))]
            string testExpression = null!,
        [CallerMemberName]string testActionMemberName = null!,
        [CallerFilePath]string testActionFileName = null!
        )
    {
        try
        {
            testAction();
            throw new InvalidOperationException(
                $"The expected exception, {
                    typeof(TException).FullName }, was not thrown" +
                    $" by the expression '{
                        testExpression }' in the method '{
                        testActionMemberName }' and file '{
                        testActionFileName }'.");
        }
        catch (TException exception) 
        {
            return exception;
        }
    }
 
    // Attribute detection
    // ...
}

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.

Listing 18.28: Invoking a Caller* Attributes Method
ExpectedException<DivideByZeroException>.AssertExceptionThrown(
    () => throw new DivideByZeroException());

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.

Table 18.1: Caller* Attributes7

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.

Guidelines
DO NOT explicitly pass arguments for Caller* attribute decorated parameters.
DO use non-nullable string for the data type of Caller* attribute decorated string parameters.
DO assign null! for the default value of Caller* attribute decorated string parameters.

________________________________________

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.
7. https://learn.microsoft.com /dotnet/csharp/language-reference/attributes/caller-information
{{ snackbarMessage }}
;