Attributes and Reflection in C#

Dated Apr 8, 2025; last modified on Tue, 08 Apr 2025

When you compile code for the runtime, it is converted into common intermediate language (CIL) and placed inside a portable executable (PE) file along with metadata generated by the compiler. Attributes allow you to place extra descriptive information into metadata that can be extracted using runtime reflection services.

Example of reflection in C#: All types descended from the System.Object base class (the root of the type hierarchy in .NET) inherit the GetType() method.

int i = 42;
Type type = i.GetType();
Console.WriteLine(type); // System.Int32

Another example, obtaining the full name of the loaded assembly:

Assembly info = typeof(int).Assembly;
Console.WriteLine(info); // System.Private.CoreLib, Version=7.0.0.0, ...

Common Attributes

A couple notable attributes are built into .NET Core:

  • [Obsolete] is useful for providing declarative documentation, and supports a boolean parameter to escalate it from a compiler warning to a compiler error.
  • [Conditional] is useful for stripping out calls to the target method if the input string doesn’t match a #define directive; useful in debugging.
  • [CallerMemberName] is useful for injecting the name of the method that is calling another method; useful in eliminating magic strings.

Defining an Attribute

Snippets runnable at https://godbolt.org/z/xYKqeqhMa.

using System;
using System.Reflection;

// By convention, all attribute names end with "Attribute".
// System.AttributeUsageAttribute is used to define key characteristics of the
// attribute, e.g., targets, inheritance, multiplicity, etc.
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = false)]
public class DeveloperAttribute : Attribute
{
  private string name;
  private string level;
  private bool reviewed;

  // Define required parameters like name and level as positional params. Params
  // to the ctor are limited to simple types/literals, e.g., bool, int, double,
  // string, Type, enums, etc., and arrays of those types.
  public DeveloperAttribute(string name, string level)
  {
    this.name = name;
    this.level = level;
    this.reviewed = false;
  }

  // Define Name property, a read-only attribute.
  public virtual string Name
  {
    get { return name; }
  }

  // Define Level property, a read-only attribute.
  public virtual string Level
  {
    get { return level; }
  }

  // Define Reviewed property, a read/write attribute. This can be set using
  // optional named parameters.
  public virtual bool Reviewed
  {
    get { return reviewed; }
    set { reviewed = value; }
  }
}

Mental Model for Attribute Syntax

Code like:

// Although the attribute is called "DeveloperAttribute", you can drop the
// "Attribute" suffix when using the attribute.
[Developer("Musa", "63", Reviewed = true)]
class SampleClass {}

… is conceptually equivalent to:

var anonymousAuthorsObject = new Developer("Musa", "63")
{
  Reviewed = true
};

… with the caveat that the code is not executed until SampleClass is queried for attributes (e.g., via Attribute.GetCustomAttribute).

In addition to lazy instantiation, Attribute objects are instantiated each time. Calling GetCustomAttribute twice in a row returns two different instances of the Attribute.

AttributeTargets

AttributeTargets controls the program elements to which the attribute can be applied, e.g., class, method, entire assembly, etc.

// Applied to a method
[ValidatedContract]
int Method1() { return 0; }

// Applied to a method parameter
int Method2([ValidatedContract] string contract) { return 0; }

// Applied to a return value
[return: ValidatedContract]
int Method3() { return 0; }

Trying to use an attribute on a non-supported target is a compiler error, e.g.,

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class FooAttribute : Attribute {}

public class Bar
{
  [Foo] // Compiler error: Attribute 'Foo' is only valid on 'class, struct'...
  public Baz() {}
}

AttributeUsageAttribute.Inherited

AttributeUsageAttribute.Inherited defines how the attribute propagates to classes derived from a base class to which the attribute is applied.

// Defaults to Inherited = true
public class FooAttribute : Attribute {}

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class BarAttribute : Attribute {}

public class Base
{
  [Foo]
  [Bar]
  public virtual void Qux() {}
}

public class Derived
{
  // Qux will have the Foo attribute, but not the Bar attribute.
  public override void Qux() {}
}

AttributeUsageAttribute.AllowMultiple

In a similar vein, AttributeUsageAttribute.AllowMultiple indicates whether multiple instances of the attribute can exist on an element. It defaults to false.

public class Foo
{
  // Compiler error if DeveloperAttribute set AllowMultiple = false.
  [Developer("Chege", "1")]
  [Developer("Atieno", "1")]
  public void Bar() {}
}

If both AllowMultiple is set to false and Inherited is set to true (the default behavior in both cases), then values of any attributes in the parent class will be overwritten by new instances of the same attribute in the child class.

Sample Client of an Attribute

using System;
using System.Reflection;

[Developer("Joan Smith", "1")]
public class Foo
{
    [Developer("Joan Smith", "1", Reviewed = true)]
    static void Bar() {}
}

class Program
{
  static void Main()
  {
    // DeveloperAttribute for Foo: Name=Joan Smith, Level=1, Reviewed=False
    GetAttribute(typeof(Foo));
  }

  public static void GetAttribute(Type t)
  {
    DeveloperAttribute attr =
      (DeveloperAttribute) Attribute.GetCustomAttribute(t, typeof (DeveloperAttribute));
    
    if (attr is null)
    {
      Console.WriteLine("DeveloperAttribute not found.");
    }
    else
    {
      Console.WriteLine(
        "DeveloperAttribute for {0}: Name={1}, Level={2}, Reviewed={3}",
        t, attr.Name, attr.Level, attr.Reviewed);
    }
  }
}

To retrieve all instances of the same attribute applied to the same scope, use Attribute.GetCustomAttributes instead of Attribute.GetCustomAttribute. To retrieve attribute instances across different scopes, e.g., for all methods in class, you’d need to supply every scope, e.g.,

public static void PrintMethodAttributes(Type t)
{
  MemberInfo[] MyMemberInfo = t.GetMethods();
  for (int i = 0; i < MyMemberInfo.length; i++) {
    DeveloperAttribute attr =
      (DeveloperAttribute) Attribute.GetCustomAttribute(
          MyMemberInfo[i], typeof (DeveloperAttribute));
    // Print the attribute information to the console.
  }
}

… where methods like Type.GetMethods, Type.GetProperties, and Type.GetConstructors come in handy.

References

  1. Extending Metadata Using Attributes - .NET. learn.microsoft.com . Accessed Apr 9, 2025.
  2. Writing Custom Attributes - .NET. learn.microsoft.com . Accessed Apr 9, 2025.
  3. Retrieving Information Stored in Attributes - .NET. learn.microsoft.com . Accessed Apr 9, 2025.
  4. Attributes and reflection. learn.microsoft.com . Accessed Apr 10, 2025.
  5. Object Class (System) | Microsoft Learn. learn.microsoft.com . Accessed Apr 10, 2025.
  6. Access attributes using reflection - C# | Microsoft Learn. learn.microsoft.com . Accessed Apr 10, 2025.
  7. Tutorial: Define and read custom attributes. - C# | Microsoft Learn. learn.microsoft.com . Accessed Apr 10, 2025.