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
| Attribute | Purpose | Example |
|---|---|---|
[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:
| Parameter | Type | Default | Description |
|---|---|---|---|
ValidOn | AttributeTargets | All | Which elements the attribute can target |
AllowMultiple | bool | false | Whether the attribute can be applied multiple times |
Inherited | bool | true | Whether derived classes inherit the attribute |
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:
| Flag | What it includes |
|---|---|
BindingFlags.Public | Public members |
BindingFlags.NonPublic | Private, protected, internal members |
BindingFlags.Instance | Instance members |
BindingFlags.Static | Static members |
BindingFlags.FlattenHierarchy | Static 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:
| Operation | Relative Cost |
|---|---|
| Direct method call | 1x (baseline) |
Cached MethodInfo.Invoke | 10-50x |
Activator.CreateInstance | 10-20x |
GetProperty + GetValue | 20-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);
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:
| Aspect | Reflection | Source Generators |
|---|---|---|
| When code is generated | Runtime | Compile time |
| Performance | Slow (late binding) | Fast (direct calls) |
| AOT compatibility | Often incompatible | Fully compatible |
| Type safety | Runtime errors | Compile-time errors |
| IDE support | None (strings) | Full IntelliSense |
Common source generator use cases:
System.Text.Jsonserialization ([JsonSerializable])Microsoft.Extensions.Loggingcompile-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
| Scenario | Use Attributes + Reflection? |
|---|---|
| Declarative validation rules | Yes (custom attributes + validation engine) |
| Plugin discovery and loading | Yes (Assembly.LoadFrom, reflection) |
| ORM column/table mapping | Yes (or source generators) |
| Serialization configuration | Yes (or source generators) |
| Test framework method discovery | Yes (test runners use reflection) |
| Performance-critical hot paths | No — use source generators or compiled expressions |
| Simple configuration | No — use appsettings.json or plain constants |
Common Pitfalls
-
Performance overhead — Reflection is slow. Always cache
Type,MethodInfo, andPropertyInfoobjects. Consider compiled expression trees for repeated access. -
Breaking refactoring silently — Renaming a method referenced by string in
GetMethod("Name")causes a runtime failure with no compile-time warning. Usenameof()where possible. -
Security risks — Reflection can access private members. Be cautious in sandboxed or security-sensitive environments.
-
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.
-
Missing null checks —
GetMethod,GetProperty, andType.GetTypereturnnullif the member is not found. Always null-check before using the result.
Key Takeaways
- Attributes add metadata to code elements; reflection reads that metadata at runtime.
- Use
[AttributeUsage]to control where custom attributes can appear. Type,MethodInfo,PropertyInfo, andAssemblyare the core reflection types.- Reflection is powerful but slow — cache lookups or pre-compile with expression trees.
- Source generators are the modern compile-time alternative that eliminates many runtime reflection use cases.
- Always validate reflection results for
null—GetMethod,GetProperty, andType.GetTypereturnnullon 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 withAttribute, 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/MethodInfoobjects, compiling expression trees, usingDelegate.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.