Attributes & Metadata
Apply and create custom attributes for declarative programming
Attributes & Metadata in C#
Master the power of attributes and metadata with free flashcards and spaced repetition practice. This lesson covers custom attribute creation, reflection-based metadata querying, and practical attribute applications—essential concepts for building extensible C# applications that leverage compile-time annotations and runtime inspection.
Welcome to the World of Declarative Programming 💻
Attributes in C# are a powerful metaprogramming feature that allows you to add declarative information to your code. Think of attributes as sticky notes you attach to classes, methods, properties, or other code elements—these notes can be read at runtime (or even compile-time) to change behavior, enforce rules, or provide additional context.
Metadata is the underlying data that describes your program's structure—classes, methods, parameters, and their relationships. Attributes are one way to extend this metadata with your own custom information.
🤔 Did you know? Attributes are used extensively throughout .NET—from marking methods for serialization ([Serializable]) to defining API routes ([Route("/api/users")]) in ASP.NET Core. Understanding attributes unlocks the ability to create frameworks and libraries that feel "magical" to users.
What Are Attributes? 🏷️
An attribute is a class that inherits from System.Attribute. You apply attributes to code elements using square bracket syntax:
[Obsolete("Use NewMethod instead")]
public void OldMethod()
{
// Method implementation
}
In this example:
Obsoleteis the attribute- The string parameter provides additional information
- The compiler will warn developers who call this method
Key characteristics:
- Attributes are metadata attached to code elements
- They don't execute on their own—something must read them
- They're typically read via reflection at runtime
- They can have parameters (positional and named)
Built-in Attributes You Should Know 📚
C# comes with many built-in attributes:
| Attribute | Purpose | Example Usage |
|---|---|---|
[Obsolete] | Marks code as deprecated | Warn developers about old APIs |
[Serializable] | Marks classes for serialization | Enable binary serialization |
[DllImport] | Imports native methods | Call Windows API functions |
[Conditional] | Conditional compilation | Debug-only method calls |
[CallerMemberName] | Captures caller information | Logging and INotifyPropertyChanged |
💡 Tip: The [Obsolete] attribute has two parameters: a message string and a boolean indicating whether usage should cause a compiler error (not just a warning).
Creating Custom Attributes ⚒️
To create your own attribute:
- Create a class inheriting from
Attribute - Name it with the
Attributesuffix (by convention) - Add properties or constructor parameters for data
- Apply
[AttributeUsage]to control where it can be used
Here's a complete example:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = false,
Inherited = true)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Version { get; set; }
public AuthorAttribute(string name)
{
Name = name;
}
}
Breaking it down:
AttributeUsagecontrols where[Author]can be appliedAttributeTargets.Class | AttributeTargets.Methodmeans classes and methods onlyAllowMultiple = falsemeans you can't apply it twice to the same elementInherited = truemeans derived classes inherit the attributeNameis a positional parameter (required, set via constructor)Versionis a named parameter (optional, set via property)
Usage:
[Author("Jane Smith", Version = "2.1")]
public class DataProcessor
{
[Author("John Doe")]
public void ProcessData() { }
}
🧠 Memory device: Think of AttributeUsage as the "rules of where the sticky note can stick"—you wouldn't put a "Refrigerate After Opening" sticker on a book!
AttributeTargets: Where Can Attributes Go? 🎯
The AttributeTargets enum specifies valid locations:
| Target | Applied To | Example |
|---|---|---|
Assembly | Entire assembly | [assembly: InternalsVisibleTo("Tests")] |
Class | Classes | [Serializable] class Data { } |
Method | Methods | [Obsolete] void Old() { } |
Property | Properties | [JsonIgnore] string Secret { get; } |
Field | Fields | [NonSerialized] private int _temp; |
Parameter | Method parameters | void Method([NotNull] string s) |
ReturnValue | Return values | [return: MarshalAs(...)] |
All | Any code element | Most permissive option |
You can combine targets using the bitwise OR operator (|):
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
public class DocumentationAttribute : Attribute { }
Reading Attributes with Reflection 🔍
Reflection is the mechanism that reads metadata at runtime. Here's how to query attributes:
// Get attributes from a type
Type type = typeof(DataProcessor);
var attrs = type.GetCustomAttributes(typeof(AuthorAttribute), true);
foreach (AuthorAttribute attr in attrs)
{
Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}");
}
// Modern generic approach
var authorAttr = type.GetCustomAttribute<AuthorAttribute>();
if (authorAttr != null)
{
Console.WriteLine($"Created by {authorAttr.Name}");
}
Common reflection methods:
| Method | Purpose | Returns |
|---|---|---|
GetCustomAttributes() | Get all attributes of a type | Array of attributes |
GetCustomAttribute<T>() | Get single attribute (generic) | Single attribute or null |
IsDefined() | Check if attribute exists | Boolean |
GetCustomAttributesData() | Get raw attribute data | Metadata without instantiation |
💡 Tip: The second parameter in GetCustomAttributes(type, inherit) controls whether to search base classes for inherited attributes.
Practical Example: Validation Framework 🛡️
Let's build a simple validation system using attributes:
Step 1: Create validation attributes
[AttributeUsage(AttributeTargets.Property)]
public abstract class ValidationAttribute : Attribute
{
public abstract bool IsValid(object value);
public string ErrorMessage { get; set; }
}
public class RequiredAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
return value != null && !string.IsNullOrWhiteSpace(value.ToString());
}
}
public class RangeAttribute : ValidationAttribute
{
public int Min { get; }
public int Max { get; }
public RangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
public override bool IsValid(object value)
{
if (value is int intValue)
return intValue >= Min && intValue <= Max;
return false;
}
}
Step 2: Apply attributes to a model
public class User
{
[Required(ErrorMessage = "Name is required")]
public string Name { get; set; }
[Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
public int Age { get; set; }
}
Step 3: Create a validator using reflection
public static class Validator
{
public static List<string> Validate(object obj)
{
var errors = new List<string>();
Type type = obj.GetType();
foreach (var property in type.GetProperties())
{
var validationAttrs = property.GetCustomAttributes<ValidationAttribute>();
foreach (var attr in validationAttrs)
{
object value = property.GetValue(obj);
if (!attr.IsValid(value))
{
errors.Add(attr.ErrorMessage ?? $"{property.Name} is invalid");
}
}
}
return errors;
}
}
Step 4: Use the validator
var user = new User { Name = "", Age = 15 };
var errors = Validator.Validate(user);
foreach (var error in errors)
{
Console.WriteLine(error);
}
// Output:
// Name is required
// Age must be between 18 and 120
🌍 Real-world analogy: This is exactly how ASP.NET Core's model validation works! When you use [Required] or [Range] on model properties, the framework uses reflection to read these attributes and validate incoming data automatically.
Advanced: Attribute Parameters 🎛️
Attributes can have different parameter types:
Positional Parameters (Constructor):
public class TableAttribute : Attribute
{
public string Name { get; }
public TableAttribute(string name) // Positional
{
Name = name;
}
}
[Table("Users")] // Must provide name
public class User { }
Named Parameters (Properties):
public class ColumnAttribute : Attribute
{
public string Name { get; set; }
public bool IsPrimaryKey { get; set; }
public int MaxLength { get; set; } = 255;
}
[Column(Name = "user_id", IsPrimaryKey = true)]
public int Id { get; set; }
Mixing Both:
public class CacheAttribute : Attribute
{
public int Duration { get; } // Positional (required)
public string Key { get; set; } // Named (optional)
public bool SlidingExpiration { get; set; } // Named (optional)
public CacheAttribute(int duration)
{
Duration = duration;
}
}
[Cache(300, Key = "user-data", SlidingExpiration = true)]
public UserData GetUserData() { }
⚠️ Important: Attribute parameter values must be compile-time constants. You can use:
- Primitive types (
int,string,bool, etc.) typeof()expressions- Enums
- Arrays of the above
You cannot use:
- Variables
- Method calls
- Object initializers (except arrays)
Performance Considerations ⚡
Reflection has performance costs:
Cost breakdown:
- Type discovery: Getting
Typeobjects (moderate) - Member enumeration: Iterating properties/methods (moderate)
- Attribute querying: Reading attributes (expensive)
- Invocation: Calling methods via reflection (very expensive)
Optimization strategies:
// ❌ BAD: Query attributes repeatedly
for (int i = 0; i < 1000; i++)
{
var attr = type.GetCustomAttribute<AuthorAttribute>(); // Slow!
}
// ✅ GOOD: Cache attribute data
var attr = type.GetCustomAttribute<AuthorAttribute>();
for (int i = 0; i < 1000; i++)
{
// Use cached attr
}
// ✅ BETTER: Use a caching strategy
private static readonly Dictionary<Type, AuthorAttribute> _cache = new();
public static AuthorAttribute GetCachedAttribute(Type type)
{
if (!_cache.TryGetValue(type, out var attr))
{
attr = type.GetCustomAttribute<AuthorAttribute>();
_cache[type] = attr;
}
return attr;
}
💡 Tip: Many frameworks (like ASP.NET Core) build attribute caches during startup to avoid repeated reflection at runtime.
Common Use Cases 🎯
1. Object-Relational Mapping (ORM):
[Table("customers")]
public class Customer
{
[Column("customer_id"), PrimaryKey]
public int Id { get; set; }
[Column("full_name"), MaxLength(100)]
public string Name { get; set; }
[Ignore] // Don't map to database
public string TempData { get; set; }
}
2. Serialization Control:
public class ApiResponse
{
[JsonProperty("user_name")]
public string UserName { get; set; }
[JsonIgnore]
public string Password { get; set; }
}
3. Dependency Injection:
public class OrderService
{
[Inject]
public ILogger Logger { get; set; }
[Inject]
public IDatabase Database { get; set; }
}
4. Testing:
[TestClass]
public class CalculatorTests
{
[TestMethod]
[ExpectedException(typeof(DivideByZeroException))]
public void Divide_ByZero_ThrowsException()
{
var calc = new Calculator();
calc.Divide(10, 0);
}
}
5. API Routing:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult GetUser(int id) { }
}
Assembly-Level Attributes 🏛️
You can apply attributes to an entire assembly:
// Typically in AssemblyInfo.cs or at top of any .cs file
[assembly: AssemblyTitle("MyApplication")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("MyApplication.Tests")]
These attributes:
- Apply to the whole compiled assembly
- Are placed outside any namespace
- Use the
assembly:target specifier - Affect assembly metadata visible in file properties
🤔 Did you know? The [InternalsVisibleTo] attribute is commonly used to expose internal members to test projects, allowing you to test internal implementation details without making them public.
Conditional Attributes & Compilation ⚙️
The [Conditional] attribute makes method calls conditional based on compilation symbols:
using System.Diagnostics;
public class Logger
{
[Conditional("DEBUG")]
public static void DebugLog(string message)
{
Console.WriteLine($"DEBUG: {message}");
}
[Conditional("TRACE")]
public static void TraceLog(string message)
{
Console.WriteLine($"TRACE: {message}");
}
}
// Usage
Logger.DebugLog("This only runs in DEBUG builds");
Logger.TraceLog("This only runs when TRACE is defined");
How it works:
- If the symbol (
DEBUG,TRACE) is not defined, the compiler removes the call entirely - The method still exists in the assembly, but calls to it vanish
- No runtime overhead—it's a compile-time optimization
- Multiple
[Conditional]attributes use OR logic (any match executes)
⚠️ Important: [Conditional] only works on methods that return void. This makes sense—if the call is removed, what would you do with a return value?
Caller Information Attributes 📞
These attributes automatically capture information about the calling code:
using System.Runtime.CompilerServices;
public class Logger
{
public static void Log(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine($"[{memberName}] {message}");
Console.WriteLine($" at {filePath}:{lineNumber}");
}
}
// Usage
public void ProcessData()
{
Logger.Log("Processing started"); // Don't pass extra parameters
}
// Output:
// [ProcessData] Processing started
// at C:\Projects\MyApp\DataProcessor.cs:42
Available caller attributes:
| Attribute | Captures | Common Use |
|---|---|---|
[CallerMemberName] | Method/property name | INotifyPropertyChanged, logging |
[CallerFilePath] | Source file path | Debugging, diagnostics |
[CallerLineNumber] | Line number in source | Error reporting |
[CallerArgumentExpression] | Argument expression (C# 10+) | Assertion messages |
INotifyPropertyChanged pattern:
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(); // No need to pass "Name"!
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
💡 Tip: Caller information attributes must be applied to optional parameters with default values. The compiler fills in the actual values at compile time.
Common Mistakes to Avoid ⚠️
1. Forgetting the Attribute suffix:
// ❌ BAD: Confusing class name
public class Author : Attribute { }
// ✅ GOOD: Clear it's an attribute
public class AuthorAttribute : Attribute { }
While both work, the convention makes code more readable. When you write [Author], C# automatically looks for AuthorAttribute.
2. Using non-constant values in attributes:
// ❌ ERROR: Won't compile
string GetTableName() => "Users";
[Table(GetTableName())] // ERROR: Attribute argument must be constant
public class User { }
// ✅ GOOD: Use constant
const string TableName = "Users";
[Table(TableName)]
public class User { }
3. Not caching reflection results:
// ❌ BAD: Queries attributes on every access
public string GetTableName<T>()
{
var attr = typeof(T).GetCustomAttribute<TableAttribute>();
return attr?.Name; // Slow if called repeatedly
}
// ✅ GOOD: Cache results
private static readonly ConcurrentDictionary<Type, string> _tableCache = new();
public string GetTableName<T>()
{
return _tableCache.GetOrAdd(typeof(T), type =>
{
var attr = type.GetCustomAttribute<TableAttribute>();
return attr?.Name ?? type.Name;
});
}
4. Forgetting AttributeUsage:
// ❌ BAD: Can be applied anywhere, multiple times
public class PrimaryKeyAttribute : Attribute { }
// ✅ GOOD: Explicit control
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class PrimaryKeyAttribute : Attribute { }
5. Wrong return type with [Conditional]:
// ❌ ERROR: Conditional methods must return void
[Conditional("DEBUG")]
public int GetDebugValue() => 42; // Won't compile
// ✅ GOOD: Returns void
[Conditional("DEBUG")]
public void LogDebugValue(int value) { }
6. Incorrect attribute parameter types:
// ❌ ERROR: Can't use complex objects
public class ConfigAttribute : Attribute
{
public ConfigAttribute(Dictionary<string, string> settings) { } // ERROR
}
// ✅ GOOD: Use simple types
public class ConfigAttribute : Attribute
{
public string[] Keys { get; set; } // Arrays are OK
}
Key Takeaways 🎯
✅ Attributes are metadata that you attach to code elements using [AttributeName] syntax
✅ Custom attributes must inherit from System.Attribute and conventionally end with "Attribute"
✅ [AttributeUsage] controls where attributes can be applied and whether they can be used multiple times
✅ Reflection is how you read attributes at runtime using methods like GetCustomAttribute<T>()
✅ Performance matters—cache reflection results instead of querying repeatedly
✅ Attribute parameters must be compile-time constants (primitives, strings, typeof, enums, arrays)
✅ Built-in attributes like [Obsolete], [Conditional], and [CallerMemberName] provide powerful functionality
✅ Common use cases include validation, serialization, ORM mapping, dependency injection, and API routing
✅ Assembly-level attributes apply to the entire compiled output and use the assembly: target
✅ Caller information attributes automatically capture method names, file paths, and line numbers for diagnostics
📋 Quick Reference Card
| Create Attribute | class MyAttribute : Attribute { } |
| Apply Attribute | [My(param)] or [MyAttribute(param)] |
| Control Usage | [AttributeUsage(AttributeTargets.Class)] |
| Read Single | type.GetCustomAttribute<MyAttribute>() |
| Read Multiple | type.GetCustomAttributes<MyAttribute>() |
| Check Exists | type.IsDefined(typeof(MyAttribute)) |
| Assembly Level | [assembly: MyAttribute] |
| Positional Param | Constructor parameter (required) |
| Named Param | Public property (optional) |
| Conditional | [Conditional("DEBUG")] void Method() |
📚 Further Study
- Microsoft Docs - Attributes: https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/
- Reflection in C#: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/reflection
- AttributeUsage Reference: https://learn.microsoft.com/en-us/dotnet/api/system.attributeusageattribute
You now have the knowledge to create powerful, extensible C# applications using attributes and metadata! Practice by building your own validation framework, creating custom ORM attributes, or implementing an attribute-based plugin system. The metaprogramming possibilities are endless! 🚀