Skip to main content

Attributes and Reflection

Definition

Attributes are declarative metadata tags applied to program entities (assemblies, types, methods, properties, etc.) to convey information to the compiler, runtime, or tools. They don't change program logic directly but can be queried and acted upon.

Reflection is the ability to inspect and manipulate type metadata at runtime. Through System.Reflection, code can discover types, invoke methods, access properties, and examine attributes dynamically.

Attributes and reflection are tightly coupled: attributes embed metadata, and reflection reads it.

// Attributes add metadata
[Obsolete("Use ProcessAsync instead.")]
public void Process(int id) { }

// Reflection reads that metadata at runtime
var method = typeof(MyService).GetMethod(nameof(Process));
var attr = method?.GetCustomAttribute<ObsoleteAttribute>();
Console.WriteLine(attr?.Message); // "Use ProcessAsync instead."

Core Concepts

Built-in Attributes

AttributePurposeExample
[Obsolete]Marks members as deprecated[Obsolete("Use NewMethod")]
[Serializable]Marks a type for binary serialization[Serializable] class Config { }
[NonSerialized]Excludes a field from serialization[NonSerialized] private string _temp;
[Flags]Marks an enum as a bit field[Flags] enum Perms { Read=1, Write=2 }
[Conditional]Conditionally compiles method calls[Conditional("DEBUG")]
[DllImport]Imports an unmanaged DLL function[DllImport("user32.dll")]
// [Obsolete] — warns or errors on usage
[Obsolete("Use ProcessOrderAsync instead.", error: false)]
public void ProcessOrder(int orderId) { }

// [Flags] — enables bitwise operations and correct ToString()
[Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
All = Read | Write | Execute
}
var perms = Permissions.Read | Permissions.Write;
Console.WriteLine(perms); // "Read, Write"

// [Conditional] — calls compiled only when symbol is defined
[Conditional("DEBUG")]
public void LogDebug(string message) => Console.WriteLine(message);
// In Release builds, all calls to LogDebug are removed

// [DllImport] — P/Invoke
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);

Custom Attributes

Create custom attributes by deriving from System.Attribute. Use [AttributeUsage] to control where it can be applied:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class ValidateRangeAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public string? ErrorMessage { get; set; }

public ValidateRangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
}

// Applying the custom attribute
public class Product
{
[ValidateRange(0, 9999, ErrorMessage = "Price must be 0-9999")]
public int Price { get; set; }

[ValidateRange(1, 10000)]
public int Quantity { get; set; }
}

[AttributeUsage] parameters:

ParameterTypeDefaultDescription
ValidOnAttributeTargetsAllWhich elements the attribute can target
AllowMultipleboolfalseWhether the attribute can be applied multiple times
InheritedbooltrueWhether derived classes inherit the attribute
Naming convention

By convention, attribute class names end with Attribute. When applying the attribute, the suffix can be omitted: [ValidateRange] is shorthand for [ValidateRangeAttribute].

Querying Attributes with Reflection

Use reflection to read attributes at runtime:

using System.Reflection;

// Get a specific attribute
var prop = typeof(Product).GetProperty(nameof(Product.Price))!;
var attr = prop.GetCustomAttribute<ValidateRangeAttribute>();
if (attr is not null)
Console.WriteLine($"Range: {attr.Min} to {attr.Max}, Error: {attr.ErrorMessage}");

// Check if an attribute is defined
bool hasRange = prop.IsDefined(typeof(ValidateRangeAttribute));

// Get all attributes on a type
var typeAttrs = typeof(Product).GetCustomAttributes();

// Get all properties with a specific attribute
var validatedProps = typeof(Product)
.GetProperties()
.Where(p => p.IsDefined(typeof(ValidateRangeAttribute)))
.ToList();

Simple validation engine using attributes + reflection:

public static List<string> Validate(object obj)
{
var errors = new List<string>();
foreach (var prop in obj.GetType().GetProperties())
{
var attr = prop.GetCustomAttribute<ValidateRangeAttribute>();
if (attr is null) continue;

int value = (int)prop.GetValue(obj)!;
if (value < attr.Min || value > attr.Max)
errors.Add($"{prop.Name}: {attr.ErrorMessage ?? $"Must be {attr.Min}-{attr.Max}"}");
}
return errors;
}

var product = new Product { Price = -5, Quantity = 0 };
var errors = Validate(product);
// ["Price: Price must be 0-9999"]

Reflection Basics

The System.Reflection namespace provides types for inspecting assemblies, types, and members at runtime.

using System.Reflection;

// --- Getting a Type ---
Type type = typeof(Product); // From type name
Type type2 = product.GetType(); // From instance
Type? type3 = Type.GetType("MyApp.Product"); // From string (needs assembly-qualified name for external)

// --- Exploring a Type ---
PropertyInfo[] props = type.GetProperties();
MethodInfo[] methods = type.GetMethods();
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
ConstructorInfo[] ctors = type.GetConstructors();

// --- Getting and setting values ---
object? value = prop.GetValue(product); // Read
prop.SetValue(product, 42); // Write

// --- Invoking methods ---
MethodInfo method = type.GetMethod("ToString")!;
string? result = (string?)method.Invoke(product, null);

// --- Creating instances ---
object? instance = Activator.CreateInstance(type); // Parameterless
object? instance2 = Activator.CreateInstance(type, args); // With parameters

Binding flags control what members reflection finds:

FlagWhat it includes
BindingFlags.PublicPublic members
BindingFlags.NonPublicPrivate, protected, internal members
BindingFlags.InstanceInstance members
BindingFlags.StaticStatic members
BindingFlags.FlattenHierarchyStatic members up the inheritance chain

You must combine Public/NonPublic with Instance/Static — they are not optional filters.

Late Binding with Reflection

Invoke methods and create instances when the type is not known at compile time:

// Plugin-style: load type by name
Type? type = Type.GetType("MyApp.Services.EmailService, MyApp");
if (type is null) throw new TypeLoadException("EmailService not found");

object? instance = Activator.CreateInstance(type);
MethodInfo? method = type.GetMethod("Send");
method?.Invoke(instance, new object[] { "Hello", "recipient@example.com" });

// Generic method invocation
MethodInfo? generic = type.GetMethod("Process")?.MakeGenericMethod(typeof(string));
generic?.Invoke(instance, new object[] { "data" });

// Loading an external assembly
Assembly assembly = Assembly.LoadFrom("plugins/MyPlugin.dll");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
{
if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract)
{
IPlugin plugin = (IPlugin)Activator.CreateInstance(t)!;
plugin.Initialize();
}
}

Reflection Performance

Reflection is significantly slower than direct calls. Typical overhead:

OperationRelative Cost
Direct method call1x (baseline)
Cached MethodInfo.Invoke10-50x
Activator.CreateInstance10-20x
GetProperty + GetValue20-100x

Mitigation strategies:

// 1. Cache reflection objects (never re-lookup in a loop)
private static readonly MethodInfo _method = typeof(MyClass).GetMethod("Process")!;

// 2. Compile expression trees for repeated invocation
var param = Expression.Parameter(typeof(MyClass), "obj");
var call = Expression.Call(param, _method);
var func = Expression.Lambda<Func<MyClass, string>>(call, param).Compile();

// Now func(instance) is nearly as fast as a direct call
string result = func(myInstance);

// 3. Use Delegate.CreateDelegate for method invocation
var del = Delegate.CreateDelegate(typeof(Func<MyClass, string>), null, _method);
var typedDel = (Func<MyClass, string>)del;
string result2 = typedDel(myInstance);
Avoid reflection in hot paths

Never use reflection in tight loops or per-request in high-throughput APIs. Cache lookups, compile expression trees, or use source generators instead.

Source Generators as Modern Alternative

Source generators (C# 9 / .NET 5+) are compile-time code generators that inspect your code via Roslyn and produce additional source files. They eliminate many runtime reflection use cases.

// System.Text.Json source generator
[JsonSerializable(typeof(Product))]
public partial class ProductContext : JsonSerializerContext { }

// Compile-time generated serialization — no reflection at runtime
string json = JsonSerializer.Serialize(product, ProductContext.Default.Product);

Advantages over reflection:

AspectReflectionSource Generators
When code is generatedRuntimeCompile time
PerformanceSlow (late binding)Fast (direct calls)
AOT compatibilityOften incompatibleFully compatible
Type safetyRuntime errorsCompile-time errors
IDE supportNone (strings)Full IntelliSense

Common source generator use cases:

  • System.Text.Json serialization ([JsonSerializable])
  • Microsoft.Extensions.Logging compile-time logging
  • ORM mapping (EF Core compiled models)
  • Dependency injection registration
  • Protocol buffer serialization

See Modern C# for partial properties and source generator patterns.

When to Use

ScenarioUse Attributes + Reflection?
Declarative validation rulesYes (custom attributes + validation engine)
Plugin discovery and loadingYes (Assembly.LoadFrom, reflection)
ORM column/table mappingYes (or source generators)
Serialization configurationYes (or source generators)
Test framework method discoveryYes (test runners use reflection)
Performance-critical hot pathsNo — use source generators or compiled expressions
Simple configurationNo — use appsettings.json or plain constants

Common Pitfalls

  1. Performance overhead — Reflection is slow. Always cache Type, MethodInfo, and PropertyInfo objects. Consider compiled expression trees for repeated access.

  2. Breaking refactoring silently — Renaming a method referenced by string in GetMethod("Name") causes a runtime failure with no compile-time warning. Use nameof() where possible.

  3. Security risks — Reflection can access private members. Be cautious in sandboxed or security-sensitive environments.

  4. Attribute misuse — Attributes should encode metadata, not logic. Logic that depends on attributes should live in the code that queries them, not inside the attribute class.

  5. Missing null checksGetMethod, GetProperty, and Type.GetType return null if the member is not found. Always null-check before using the result.

Key Takeaways

  1. Attributes add metadata to code elements; reflection reads that metadata at runtime.
  2. Use [AttributeUsage] to control where custom attributes can appear.
  3. Type, MethodInfo, PropertyInfo, and Assembly are the core reflection types.
  4. Reflection is powerful but slow — cache lookups or pre-compile with expression trees.
  5. Source generators are the modern compile-time alternative that eliminates many runtime reflection use cases.
  6. Always validate reflection results for nullGetMethod, GetProperty, and Type.GetType return null on failure, not exceptions.

Interview Questions

Q: What are attributes in C#?

Attributes are declarative metadata tags applied to program elements using square brackets (e.g., [Obsolete]). They don't change program logic directly but can be queried at runtime via reflection or acted upon by compilers and tools.

Q: What is reflection and when would you use it?

Reflection is the ability to inspect type metadata at runtime using System.Reflection. It allows you to discover types, invoke methods, read properties, and query attributes dynamically. Common uses include plugin systems, serialization, validation frameworks, and ORM mapping.

Q: How do you create a custom attribute?

Derive a class from System.Attribute, apply [AttributeUsage] to specify valid targets and behavior, and define constructor parameters and properties. By convention, the class name ends with Attribute, but the suffix is omitted when applying it.

Q: What is the performance impact of reflection?

Reflection is 10-100x slower than direct calls. Mitigate by caching Type/MethodInfo objects, compiling expression trees, using Delegate.CreateDelegate, or replacing runtime reflection with source generators.

Q: What are source generators and how do they relate to reflection?

Source generators are compile-time code generators that inspect your program using the Roslyn API and produce additional source files. They eliminate many runtime reflection use cases (serialization, mapping, registration) by generating code at compile time, providing better performance and AOT compatibility.

References