DEV Community

Cover image for 🏷️ C# Attributes and Reflection: The Metadata System
RecurPixel
RecurPixel

Posted on • Edited on

🏷️ C# Attributes and Reflection: The Metadata System

C# Attributes allow developers to add descriptive metadata (tags) to code elements (classes, methods, properties) without changing their logic.

They function like "sticky notes" that provide instructions or context to frameworks. Reflection is the process used by your code or a framework (like ASP.NET, Entity Framework) to read and inspect this metadata at runtime, enabling advanced features like automatic validation, routing, ORM mapping, and dependency injection.
The most common mistake is forgetting to use the generic method GetCustomAttributes<T>() or casting the result when reading multiple attributes, which is essential for most complex filtering and framework development.

🎯 The Big Picture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              METADATA SYSTEM                        β”‚
β”‚                                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚  β”‚  ATTRIBUTES  │────────>β”‚  REFLECTION  β”‚          β”‚
β”‚  β”‚  (Add data)  β”‚         β”‚  (Read data) β”‚          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚         β”‚                        β”‚                  β”‚
β”‚         β”‚                        β”‚                  β”‚
β”‚         v                        v                  β”‚
β”‚    [Validation]            GetCustomAttribute()     β”‚
β”‚    [Route("api")]          GetProperties()          β”‚
β”‚    [Obsolete]              GetMethods()             β”‚
β”‚    [Serializable]          Activator.CreateInstance β”‚
β”‚                                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Common Use Cases:
β€’ Validation (ASP.NET, Entity Framework)
β€’ Serialization (JSON, XML)
β€’ Routing (ASP.NET MVC/Web API)
β€’ Testing (NUnit, xUnit)
β€’ Dependency Injection
β€’ ORM Mapping (Entity Framework)
Enter fullscreen mode Exit fullscreen mode

πŸ” Attribute Filtering Patterns (Common Scenarios)

Pattern 1: Find All Classes with Specific Attribute

// Find all classes decorated with [Developer] attribute
Assembly assembly = Assembly.GetExecutingAssembly();

var classesWithAttr = assembly.GetTypes()
    .Where(t => t.IsClass && 
                !t.IsAbstract && 
                t.IsDefined(typeof(Developer), false));

foreach (var type in classesWithAttr)
{
    Console.WriteLine(type.Name);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Find Classes Where Attribute Matches Condition

// Find classes developed by specific person
string targetDeveloper = "Arthor";

var matchingClasses = assembly.GetTypes()
    .Where(t => t.IsClass && !t.IsAbstract)
    .Where(t => t.GetCustomAttributes<Developer>()
                 .Any(d => d.Name == targetDeveloper));
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Get All Attribute Values Across All Classes

// Get all unique developer names
var allDevelopers = assembly.GetTypes()
    .Where(t => t.IsClass && !t.IsAbstract)
    .SelectMany(t => t.GetCustomAttributes<Developer>())
    .Select(d => d.Name)
    .Distinct();

foreach (var name in allDevelopers)
{
    Console.WriteLine(name);
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Group Classes by Attribute Value

// Group classes by developer
var grouped = assembly.GetTypes()
    .Where(t => t.IsClass && !t.IsAbstract)
    .SelectMany(t => t.GetCustomAttributes<Developer>()
                      .Select(d => new { Type = t, Developer = d }))
    .GroupBy(x => x.Developer.Name);

foreach (var group in grouped)
{
    Console.WriteLine($"\nDeveloper: {group.Key}");
    foreach (var item in group)
    {
        Console.WriteLine($"  - {item.Type.Name} v{item.Developer.Version}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Find Classes with Multiple Specific Attributes

// Find classes that have BOTH [Obsolete] AND [Serializable]
var classesWithBoth = assembly.GetTypes()
    .Where(t => t.IsClass &&
                t.IsDefined(typeof(ObsoleteAttribute), false) &&
                t.IsDefined(typeof(SerializableAttribute), false));
Enter fullscreen mode Exit fullscreen mode

Pattern 6: Complex Filtering (Version Range)

// Find classes with version between 2.0 and 4.0
var versionRange = assembly.GetTypes()
    .Where(t => t.IsClass && !t.IsAbstract)
    .Where(t => t.GetCustomAttributes<Developer>()
                 .Any(d => d.Version >= 2.0 && d.Version <= 4.0));
Enter fullscreen mode Exit fullscreen mode

πŸŽ“ Key Takeaways: Avoiding Common Mistakes

❌ Mistake 1: Forgetting to Cast (Legacy Method)

// WRONG
object[] attrs = type.GetCustomAttributes(typeof(Developer), false);
Console.WriteLine(attrs[0].Name);  // ❌ Error! object doesn't have Name

// RIGHT
Developer dev = (Developer)attrs[0];  // βœ… Cast first
Console.WriteLine(dev.Name);
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 2: Using Singular Method for Multiple Attributes

// WRONG (only gets first attribute)
var attr = type.GetCustomAttribute<Developer>();  // ❌ Loses other attributes

// RIGHT
var attrs = type.GetCustomAttributes<Developer>();  // βœ… Gets all
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 3: Not Checking for Null/Empty

// WRONG
var attr = type.GetCustomAttribute<Developer>();
Console.WriteLine(attr.Name);  // ❌ NullReferenceException if not found

// RIGHT
var attr = type.GetCustomAttribute<Developer>();
if (attr != null)  // βœ… Check first
{
    Console.WriteLine(attr.Name);
}

// OR use null-conditional operator
Console.WriteLine(attr?.Name ?? "No developer");  // βœ…
Enter fullscreen mode Exit fullscreen mode

❌ Mistake 4: Using GetCustomAttributes in LINQ Without Cast

// WRONG
var result = types.Where(t => 
    t.GetCustomAttributes(typeof(Developer), false).Any(a => a.Name == "Arthor"));
    // ❌ object doesn't have Name property

// RIGHT - Option 1: Cast
var result = types.Where(t => 
    t.GetCustomAttributes(typeof(Developer), false)
     .Cast<Developer>()  // βœ… Cast to Developer
     .Any(d => d.Name == "Arthor"));

// RIGHT - Option 2: Use generic (better)
var result = types.Where(t => 
    t.GetCustomAttributes<Developer>()  // βœ… Already typed
     .Any(d => d.Name == "Arthor"));
Enter fullscreen mode Exit fullscreen mode

βœ… Best Practice: Use Generic Methods

// ❌ Old way (verbose, requires casting)
object[] attrs = type.GetCustomAttributes(typeof(Developer), false);
foreach (object attr in attrs)
{
    Developer dev = (Developer)attr;
    Console.WriteLine(dev.Name);
}

// βœ… Modern way (clean, type-safe)
foreach (Developer dev in type.GetCustomAttributes<Developer>())
{
    Console.WriteLine(dev.Name);
}
Enter fullscreen mode Exit fullscreen mode

What Are Attributes?

Simple Definition: Attributes are tags you put on code (classes, methods, properties) to add metadata.

Think of it like: Sticky notes on code that frameworks can read later.

[Obsolete("Use NewMethod instead")]  // ← Attribute (sticky note)
public void OldMethod() { }           // ← Your code

[Required]                            // ← Attribute
public string Name { get; set; }      // ← Your property
Enter fullscreen mode Exit fullscreen mode

Key Point: Attributes do NOTHING by themselves. Something else (framework or your code) must READ them using Reflection.


Built-in Attributes (Most Common)

1. [Obsolete] - Mark Old Code

[Obsolete]  // Warning
public void OldMethod() { }

[Obsolete("Use NewMethod instead")]  // Warning with message
public void OldMethod() { }

[Obsolete("Don't use this!", true)]  // Compiler ERROR
public void OldMethod() { }
Enter fullscreen mode Exit fullscreen mode

2. [Serializable] - Allow Serialization

[Serializable]
public class Person
{
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

3. [DllImport] - Call Native Code

[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
Enter fullscreen mode Exit fullscreen mode

4. [CallerMemberName] - Get Caller Info (Debugging)

public void Log(string message, 
    [CallerMemberName] string caller = "")
{
    Console.WriteLine($"{caller}: {message}");
}

// Usage: Log("Something happened");
// Output: MyMethod: Something happened
Enter fullscreen mode Exit fullscreen mode

5. [Flags] - Enum as Bit Flags

[Flags]
public enum FileAccess
{
    Read = 1,    // 001
    Write = 2,   // 010
    Execute = 4  // 100
}

var access = FileAccess.Read | FileAccess.Write; // 011 (3)
Enter fullscreen mode Exit fullscreen mode

6. [Conditional] - Conditional Compilation

[Conditional("DEBUG")]
public void DebugLog(string message)
{
    Console.WriteLine(message);  // Only runs in DEBUG mode
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Attributes

Step 1: Define Attribute Class

Rules:

  1. Must inherit from System.Attribute
  2. Name should end with "Attribute" (optional but convention)
  3. Add [AttributeUsage] to specify where it can be used
// Simple attribute (no parameters)
public class MyAttribute : Attribute
{
}

// Attribute with parameters
public class DescriptionAttribute : Attribute
{
    public string Text { get; }

    public DescriptionAttribute(string text)
    {
        Text = text;
    }
}

// Attribute with named parameters
public class ValidationAttribute : Attribute
{
    public string ErrorMessage { get; set; }
    public int MinLength { get; set; }
    public int MaxLength { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Control Where Attribute Can Be Used

[AttributeUsage(AttributeTargets.Class)]  // Only on classes
public class TableAttribute : Attribute
{
    public string Name { get; }
    public TableAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Property)]  // Only on properties
public class ColumnAttribute : Attribute
{
    public string Name { get; }
    public ColumnAttribute(string name) => Name = name;
}

// Multiple targets
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LogAttribute : Attribute { }

// Allow multiple instances
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RouteAttribute : Attribute
{
    public string Path { get; }
    public RouteAttribute(string path) => Path = path;
}
Enter fullscreen mode Exit fullscreen mode

AttributeTargets Options

Assembly        // [assembly: AssemblyVersion("1.0")]
Module          // Rarely used
Class           // [Table("Users")]
Struct          // [Serializable]
Enum            // [Flags]
Constructor     // [Obsolete]
Method          // [Route("api/users")]
Property        // [Required]
Field           // [NonSerialized]
Event           // [Description("Fired when...")]
Interface       // [ServiceContract]
Parameter       // void Method([FromBody] string data)
Delegate        // Rarely used
ReturnValue     // [return: MarshalAs(...)]
GenericParameter// class MyClass<[SomeAttribute] T>
All             // Any of the above
Enter fullscreen mode Exit fullscreen mode

Using Custom Attributes

// Usage (Note: "Attribute" suffix is optional)
[Description("This is a user")]
public class User { }

// Same as:
[DescriptionAttribute("This is a user")]
public class User { }

// With named parameters
[Validation(ErrorMessage = "Invalid", MinLength = 5, MaxLength = 50)]
public string Username { get; set; }

// Multiple attributes
[Table("Users")]
[Description("User entity")]
[Serializable]
public class User { }

// Multiple instances (if AllowMultiple = true)
[Route("api/users")]
[Route("api/v1/users")]
public class UserController { }
Enter fullscreen mode Exit fullscreen mode

Reflection Basics

What is Reflection?: Examining and manipulating code at runtime.

Common Uses:

  • Read attributes
  • Get type information
  • Create instances dynamically
  • Invoke methods dynamically
  • Access private members (testing)

Key Reflection Classes

Type            // Represents a type (class, interface, etc.)
Assembly        // Represents a .dll or .exe
MemberInfo      // Base for all members
PropertyInfo    // Property information
MethodInfo      // Method information
FieldInfo       // Field information
ConstructorInfo // Constructor information
Enter fullscreen mode Exit fullscreen mode

Getting Type Information

// Three ways to get Type
Type type1 = typeof(User);                    // Compile-time
Type type2 = user.GetType();                  // Runtime (from instance)
Type type3 = Type.GetType("MyNamespace.User"); // From string

// Basic type info
string name = type.Name;              // "User"
string fullName = type.FullName;      // "MyNamespace.User"
string namespaceName = type.Namespace; // "MyNamespace"
bool isClass = type.IsClass;          // true
bool isInterface = type.IsInterface;  // false
bool isAbstract = type.IsAbstract;    // false
bool isSealed = type.IsSealed;        // false
bool isPublic = type.IsPublic;        // true
Enter fullscreen mode Exit fullscreen mode

Reading Attributes (The Important Part!)

⚠️ Understanding Attribute Retrieval Methods

There are THREE main ways to get attributes, and choosing the wrong one causes confusion:

Method Returns Use When
GetCustomAttribute<T>() Single T or null You expect ONE attribute
GetCustomAttributes<T>() IEnumerable<T> You expect MULTIPLE attributes
GetCustomAttributes(type, inherit) object[] Legacy, need casting

Single Attribute on Class

// Define
[Table("Users")]
public class User { }

// Read - Modern way (Generic)
Type type = typeof(User);
TableAttribute attr = type.GetCustomAttribute<TableAttribute>();

if (attr != null)
{
    Console.WriteLine(attr.Name);  // "Users"
}

// Read - Legacy way (Non-generic, requires cast)
object[] attrs = type.GetCustomAttributes(typeof(TableAttribute), false);
if (attrs.Length > 0)
{
    TableAttribute attr = (TableAttribute)attrs[0];  // Must cast!
    Console.WriteLine(attr.Name);
}

// Or just check if exists
bool hasTable = type.IsDefined(typeof(TableAttribute), false);
Enter fullscreen mode Exit fullscreen mode

Multiple Attributes on Class

// Define
[Route("api/users")]
[Route("api/v1/users")]
public class UserController { }

// βœ… Method 1: Modern Generic (PREFERRED)
Type type = typeof(UserController);
IEnumerable<RouteAttribute> routes = type.GetCustomAttributes<RouteAttribute>();

foreach (var route in routes)
{
    Console.WriteLine(route.Path);
}

// βœ… Method 2: Legacy Non-Generic (Requires casting)
object[] routesArray = type.GetCustomAttributes(typeof(RouteAttribute), false);

foreach (object attr in routesArray)
{
    RouteAttribute route = (RouteAttribute)attr;  // Must cast each one!
    Console.WriteLine(route.Path);
}

// ❌ WRONG: Using GetCustomAttribute (singular) for multiple
// This only returns the FIRST attribute, you'll miss others!
var route = type.GetCustomAttribute<RouteAttribute>();  // Only gets first one!
Enter fullscreen mode Exit fullscreen mode

🎯 Common Confusion: Accessing Properties After GetCustomAttributes

This is the #1 confusion point when working with multiple attributes:

[Developer("Arthor", 1.0)]
[Developer("Alice", 1.1)]
class MyClass { }

// ❌ WRONG: Trying to access properties directly on object[]
object[] attrs = type.GetCustomAttributes(typeof(Developer), false);
// Can't do: attrs[0].Name ❌ (object doesn't have Name property)

// βœ… CORRECT: Must cast first
foreach (object attr in attrs)
{
    Developer dev = (Developer)attr;  // Cast first!
    Console.WriteLine(dev.Name);       // Now can access properties
}

// βœ… BETTER: Use generic version (no casting needed)
foreach (Developer dev in type.GetCustomAttributes<Developer>())
{
    Console.WriteLine(dev.Name);  // Direct access, no cast!
}

// βœ… BEST: Use LINQ for filtering
var arthorClasses = type.GetCustomAttributes<Developer>()
    .Where(d => d.Name == "Arthor");
Enter fullscreen mode Exit fullscreen mode

Real Example: Filtering Classes by Developer Name

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class Developer : Attribute
{
    public string Name { get; init; }
    public double Version { get; init; }
    public Developer(string name, double version)
    {
        Name = name;
        Version = version;
    }
}

[Developer("Arthor", 1.0)]
[Developer("Alice", 1.1)]
class Class1 { }

[Developer("Alex", 2.0)]
class Class2 { }

[Developer("Arthor", 4.0)]
class Class3 { }

// Get all classes with Developer attribute
Assembly assembly = Assembly.GetExecutingAssembly();
Type[] allTypes = assembly.GetTypes();

// Filter 1: Classes that have the attribute
var classesWithDevAttr = allTypes.Where(t =>
    t.IsClass &&
    !t.IsAbstract &&
    Attribute.IsDefined(t, typeof(Developer))
);

// Filter 2: Classes by specific developer
string targetDev = "Arthor";

// ❌ WRONG: Trying to filter without proper casting
// var wrong = allTypes.Where(t => 
//     t.GetCustomAttributes(typeof(Developer), false)[0].Name == targetDev);
//     ↑ This fails! object[] doesn't have Name property

// βœ… CORRECT Method 1: Legacy way with casting
var classesByDev1 = allTypes.Where(t =>
    t.IsClass &&
    !t.IsAbstract &&
    t.GetCustomAttributes(typeof(Developer), false)  // Returns object[]
        .Cast<Developer>()                            // Convert to Developer
        .Any(d => d.Name == targetDev)                // Now can access Name
);

// βœ… CORRECT Method 2: Modern generic way (PREFERRED)
var classesByDev2 = allTypes.Where(t =>
    t.IsClass &&
    !t.IsAbstract &&
    t.GetCustomAttributes<Developer>()     // Already typed!
        .Any(d => d.Name == targetDev)     // Direct property access
);

// Display results
Console.WriteLine($"Classes by {targetDev}:");
foreach (var type in classesByDev2)
{
    Console.WriteLine($"\nClass: {type.Name}");

    // Get all Developer attributes for this class
    var devAttrs = type.GetCustomAttributes<Developer>();

    foreach (var dev in devAttrs)
    {
        Console.WriteLine($"  -> Developer: {dev.Name}, Version: {dev.Version:F1}");
    }
}

// Output:
// Classes by Arthor:
// 
// Class: Class1
//   -> Developer: Arthor, Version: 1.0
//   -> Developer: Alice, Version: 1.1
// 
// Class: Class3
//   -> Developer: Arthor, Version: 4.0
Enter fullscreen mode Exit fullscreen mode

Attributes on Properties

// Define
public class User
{
    [Required]
    [StringLength(50)]
    public string Name { get; set; }

    [Column("email_address")]
    public string Email { get; set; }
}

// Read from single property
PropertyInfo prop = typeof(User).GetProperty("Name");
var required = prop.GetCustomAttribute<RequiredAttribute>();
var stringLength = prop.GetCustomAttribute<StringLengthAttribute>();

// Read from all properties
Type type = typeof(User);
foreach (PropertyInfo prop in type.GetProperties())
{
    var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
    if (columnAttr != null)
    {
        Console.WriteLine($"{prop.Name} -> {columnAttr.Name}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Attributes on Methods

// Define
public class UserService
{
    [Obsolete("Use GetUserByIdAsync instead")]
    public User GetUser(int id) => null;

    [HttpGet("api/users/{id}")]
    public async Task<User> GetUserByIdAsync(int id) => null;
}

// Read
MethodInfo method = typeof(UserService).GetMethod("GetUser");
var obsolete = method.GetCustomAttribute<ObsoleteAttribute>();

if (obsolete != null)
{
    Console.WriteLine(obsolete.Message);
}
Enter fullscreen mode Exit fullscreen mode

Attributes on Parameters

// Define
public void UpdateUser([FromBody] User user, [FromQuery] int id) { }

// Read
MethodInfo method = typeof(MyController).GetMethod("UpdateUser");
ParameterInfo[] parameters = method.GetParameters();

foreach (var param in parameters)
{
    var fromBody = param.GetCustomAttribute<FromBodyAttribute>();
    if (fromBody != null)
    {
        Console.WriteLine($"{param.Name} is from body");
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern #1: Validation Framework

// 1. Define validation attributes
public class RequiredAttribute : Attribute { }

public class RangeAttribute : Attribute
{
    public int Min { get; }
    public int Max { get; }
    public RangeAttribute(int min, int max)
    {
        Min = min;
        Max = max;
    }
}

public class EmailAttribute : Attribute { }

// 2. Use on model
public class User
{
    [Required]
    public string Name { get; set; }

    [Required]
    [Email]
    public string Email { get; set; }

    [Range(18, 100)]
    public int Age { get; set; }
}

// 3. Create validator
public static class Validator
{
    public static List<string> Validate(object obj)
    {
        var errors = new List<string>();
        Type type = obj.GetType();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            object value = prop.GetValue(obj);

            // Check Required
            if (prop.IsDefined(typeof(RequiredAttribute)))
            {
                if (value == null || string.IsNullOrWhiteSpace(value.ToString()))
                {
                    errors.Add($"{prop.Name} is required");
                }
            }

            // Check Range
            var rangeAttr = prop.GetCustomAttribute<RangeAttribute>();
            if (rangeAttr != null && value is int intValue)
            {
                if (intValue < rangeAttr.Min || intValue > rangeAttr.Max)
                {
                    errors.Add($"{prop.Name} must be between {rangeAttr.Min} and {rangeAttr.Max}");
                }
            }

            // Check Email
            if (prop.IsDefined(typeof(EmailAttribute)))
            {
                if (value != null && !value.ToString().Contains("@"))
                {
                    errors.Add($"{prop.Name} must be a valid email");
                }
            }
        }

        return errors;
    }
}

// 4. Usage
var user = new User { Name = "", Email = "invalid", Age = 15 };
var errors = Validator.Validate(user);

foreach (var error in errors)
{
    Console.WriteLine(error);
}
// Output:
// Name is required
// Email must be a valid email
// Age must be between 18 and 100
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern #2: Simple ORM (Object-Relational Mapping)

// 1. Define attributes
[AttributeUsage(AttributeTargets.Class)]
public class TableAttribute : Attribute
{
    public string Name { get; }
    public TableAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Property)]
public class ColumnAttribute : Attribute
{
    public string Name { get; }
    public ColumnAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Property)]
public class PrimaryKeyAttribute : Attribute { }

// 2. Use on model
[Table("users")]
public class User
{
    [PrimaryKey]
    [Column("user_id")]
    public int Id { get; set; }

    [Column("user_name")]
    public string Name { get; set; }

    [Column("email_address")]
    public string Email { get; set; }

    public int Age { get; set; }  // No attribute = ignored
}

// 3. Create SQL generator
public static class SqlGenerator
{
    public static string GenerateInsert<T>(T obj)
    {
        Type type = typeof(T);

        // Get table name
        var tableAttr = type.GetCustomAttribute<TableAttribute>();
        string tableName = tableAttr?.Name ?? type.Name;

        var columns = new List<string>();
        var values = new List<string>();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            // Skip if no Column attribute
            var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
            if (columnAttr == null) continue;

            // Skip primary key (usually auto-increment)
            if (prop.IsDefined(typeof(PrimaryKeyAttribute))) continue;

            columns.Add(columnAttr.Name);

            object value = prop.GetValue(obj);
            string valueStr = value is string ? $"'{value}'" : value.ToString();
            values.Add(valueStr);
        }

        return $"INSERT INTO {tableName} ({string.Join(", ", columns)}) " +
               $"VALUES ({string.Join(", ", values)})";
    }

    public static string GenerateSelect<T>()
    {
        Type type = typeof(T);
        var tableAttr = type.GetCustomAttribute<TableAttribute>();
        string tableName = tableAttr?.Name ?? type.Name;

        var columns = new List<string>();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
            if (columnAttr != null)
            {
                columns.Add(columnAttr.Name);
            }
        }

        return $"SELECT {string.Join(", ", columns)} FROM {tableName}";
    }
}

// 4. Usage
var user = new User 
{ 
    Name = "John", 
    Email = "john@example.com", 
    Age = 30 
};

string insertSql = SqlGenerator.GenerateInsert(user);
Console.WriteLine(insertSql);
// INSERT INTO users (user_name, email_address) VALUES ('John', 'john@example.com')

string selectSql = SqlGenerator.GenerateSelect<User>();
Console.WriteLine(selectSql);
// SELECT user_id, user_name, email_address FROM users
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern #3: API Route Registration

// 1. Define route attribute
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class RouteAttribute : Attribute
{
    public string Method { get; }  // GET, POST, etc.
    public string Path { get; }

    public RouteAttribute(string method, string path)
    {
        Method = method;
        Path = path;
    }
}

// 2. Use on controller
public class UserController
{
    [Route("GET", "/api/users")]
    public List<User> GetAllUsers()
    {
        return new List<User>();
    }

    [Route("GET", "/api/users/{id}")]
    public User GetUser(int id)
    {
        return null;
    }

    [Route("POST", "/api/users")]
    public void CreateUser(User user)
    {
    }
}

// 3. Create route scanner
public class RouteScanner
{
    public static void RegisterRoutes(Type controllerType)
    {
        foreach (MethodInfo method in controllerType.GetMethods())
        {
            var routes = method.GetCustomAttributes<RouteAttribute>();

            foreach (var route in routes)
            {
                Console.WriteLine($"{route.Method} {route.Path} -> {method.Name}");
            }
        }
    }
}

// 4. Usage
RouteScanner.RegisterRoutes(typeof(UserController));
// Output:
// GET /api/users -> GetAllUsers
// GET /api/users/{id} -> GetUser
// POST /api/users -> CreateUser
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern #4: Dependency Injection Container

// 1. Define attributes
[AttributeUsage(AttributeTargets.Class)]
public class ServiceAttribute : Attribute
{
    public ServiceLifetime Lifetime { get; }
    public ServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Transient)
    {
        Lifetime = lifetime;
    }
}

public enum ServiceLifetime { Transient, Singleton }

// 2. Use on services
[Service(ServiceLifetime.Singleton)]
public class Logger
{
    public void Log(string message) => Console.WriteLine(message);
}

[Service(ServiceLifetime.Transient)]
public class UserService
{
    private readonly Logger _logger;

    public UserService(Logger logger)
    {
        _logger = logger;
    }
}

// 3. Create simple container
public class SimpleContainer
{
    private readonly Dictionary<Type, object> _singletons = new();
    private readonly Dictionary<Type, Type> _registrations = new();

    public void AutoRegister(Assembly assembly)
    {
        var types = assembly.GetTypes()
            .Where(t => t.IsDefined(typeof(ServiceAttribute)));

        foreach (var type in types)
        {
            _registrations[type] = type;
        }
    }

    public T Resolve<T>()
    {
        Type type = typeof(T);
        var serviceAttr = type.GetCustomAttribute<ServiceAttribute>();

        // Singleton: return cached instance
        if (serviceAttr?.Lifetime == ServiceLifetime.Singleton)
        {
            if (_singletons.TryGetValue(type, out var singleton))
                return (T)singleton;

            var instance = CreateInstance(type);
            _singletons[type] = instance;
            return (T)instance;
        }

        // Transient: create new instance
        return (T)CreateInstance(type);
    }

    private object CreateInstance(Type type)
    {
        // Get constructor
        var constructor = type.GetConstructors().First();

        // Resolve dependencies
        var parameters = constructor.GetParameters()
            .Select(p => Resolve(p.ParameterType))
            .ToArray();

        // Create instance
        return Activator.CreateInstance(type, parameters);
    }

    private object Resolve(Type type)
    {
        var method = GetType().GetMethod("Resolve").MakeGenericMethod(type);
        return method.Invoke(this, null);
    }
}

// 4. Usage
var container = new SimpleContainer();
container.AutoRegister(Assembly.GetExecutingAssembly());

var service1 = container.Resolve<UserService>();
var service2 = container.Resolve<UserService>();

var logger1 = container.Resolve<Logger>();
var logger2 = container.Resolve<Logger>();

// logger1 == logger2 (Singleton)
// service1 != service2 (Transient)
Enter fullscreen mode Exit fullscreen mode

Real-World Pattern #5: Test Data Builder

// 1. Define attribute
[AttributeUsage(AttributeTargets.Property)]
public class TestDataAttribute : Attribute
{
    public object DefaultValue { get; }
    public TestDataAttribute(object defaultValue)
    {
        DefaultValue = defaultValue;
    }
}

// 2. Use on model
public class User
{
    [TestData("John Doe")]
    public string Name { get; set; }

    [TestData("john@example.com")]
    public string Email { get; set; }

    [TestData(25)]
    public int Age { get; set; }
}

// 3. Create builder
public static class TestDataBuilder
{
    public static T Build<T>() where T : new()
    {
        T obj = new T();
        Type type = typeof(T);

        foreach (PropertyInfo prop in type.GetProperties())
        {
            var attr = prop.GetCustomAttribute<TestDataAttribute>();
            if (attr != null)
            {
                prop.SetValue(obj, attr.DefaultValue);
            }
        }

        return obj;
    }
}

// 4. Usage (in tests)
var testUser = TestDataBuilder.Build<User>();
Console.WriteLine(testUser.Name);   // John Doe
Console.WriteLine(testUser.Email);  // john@example.com
Console.WriteLine(testUser.Age);    // 25
Enter fullscreen mode Exit fullscreen mode

Working with Generic Types

// Define generic class
public class Repository<T> where T : class
{
    public void Save(T entity) { }
}

// Get generic type info
Type genericType = typeof(Repository<>);  // Open generic
Type closedType = typeof(Repository<User>);  // Closed generic

Console.WriteLine(genericType.IsGenericType);           // true
Console.WriteLine(genericType.IsGenericTypeDefinition); // true
Console.WriteLine(closedType.IsGenericType);            // true
Console.WriteLine(closedType.IsGenericTypeDefinition);  // false

// Get generic arguments
Type[] typeArgs = closedType.GetGenericArguments();
Console.WriteLine(typeArgs[0].Name);  // "User"

// Create instance of generic type
Type repoType = typeof(Repository<>).MakeGenericType(typeof(User));
object repo = Activator.CreateInstance(repoType);
Enter fullscreen mode Exit fullscreen mode

Working with Interfaces

// Check if type implements interface
Type type = typeof(User);
bool implementsIComparable = type.GetInterfaces().Contains(typeof(IComparable));

// Or
bool implements = typeof(IComparable).IsAssignableFrom(type);

// Get all types implementing interface
var implementations = Assembly.GetExecutingAssembly()
    .GetTypes()
    .Where(t => typeof(IMyInterface).IsAssignableFrom(t) && !t.IsInterface);
Enter fullscreen mode Exit fullscreen mode

Creating Instances Dynamically

// Method 1: Activator.CreateInstance (simple)
User user1 = (User)Activator.CreateInstance(typeof(User));

// Method 2: Activator.CreateInstance with parameters
var user2 = (User)Activator.CreateInstance(
    typeof(User), 
    new object[] { "John", 25 }  // Constructor params
);

// Method 3: Using ConstructorInfo
Type type = typeof(User);
ConstructorInfo ctor = type.GetConstructor(new[] { typeof(string), typeof(int) });
var user3 = (User)ctor.Invoke(new object[] { "John", 25 });

// Method 4: Generic
T CreateInstance<T>() where T : new()
{
    return new T();  // Requires parameterless constructor
}

// Method 5: Using expression trees (fastest for repeated use)
var ctor = typeof(User).GetConstructor(Type.EmptyTypes);
var newExp = Expression.New(ctor);
var lambda = Expression.Lambda<Func<User>>(newExp);
var factory = lambda.Compile();
var user4 = factory();  // Very fast after compilation
Enter fullscreen mode Exit fullscreen mode

Invoking Methods Dynamically

public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Multiply(int a, int b) => a * b;
}

// Get method
Type type = typeof(Calculator);
MethodInfo method = type.GetMethod("Add");

// Invoke
Calculator calc = new Calculator();
object result = method.Invoke(calc, new object[] { 5, 3 });
Console.WriteLine(result);  // 8

// Invoke static method
// MethodInfo staticMethod = type.GetMethod("StaticMethod");
// object result = staticMethod.Invoke(null, parameters);
Enter fullscreen mode Exit fullscreen mode

Getting/Setting Properties Dynamically

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

User user = new User();
PropertyInfo nameProp = typeof(User).GetProperty("Name");

// Set value
nameProp.SetValue(user, "John");

// Get value
object value = nameProp.GetValue(user);
Console.WriteLine(value);  // John
Enter fullscreen mode Exit fullscreen mode

Performance Tips

⚠️ Reflection is SLOW

// Slow: Reflection in loop
for (int i = 0; i < 1000000; i++)
{
    var method = typeof(MyClass).GetMethod("MyMethod");
    method.Invoke(instance, null);
}

// Fast: Cache reflection results
var method = typeof(MyClass).GetMethod("MyMethod");
for (int i = 0; i < 1000000; i++)
{
    method.Invoke(instance, null);
}

// Faster: Compile to delegate
var method = typeof(MyClass).GetMethod("MyMethod");
var action = (Action)Delegate.CreateDelegate(typeof(Action), instance, method);
for (int i = 0; i < 1000000; i++)
{
    action();  // Much faster
}
Enter fullscreen mode Exit fullscreen mode

Caching Pattern

public static class AttributeCache
{
    private static readonly ConcurrentDictionary<Type, object> _cache = new();

    public static T GetAttribute<T>(Type type) where T : Attribute
    {
        return (T)_cache.GetOrAdd(type, t => t.GetCustomAttribute<T>());
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Reflection Methods Cheat Sheet

// TYPE INFO
Type type = typeof(User);
type.Name                    // "User"
type.FullName                // "MyNamespace.User"
type.IsClass                 // true
type.IsInterface             // false
type.IsAbstract              // false
type.IsSealed                // false
type.BaseType                // typeof(object)
type.GetInterfaces()         // All interfaces

// ATTRIBUTES
type.GetCustomAttribute<T>()           // Single attribute
type.GetCustomAttributes<T>()          // Multiple attributes
type.IsDefined(typeof(T))              // Check if has attribute

// CONSTRUCTORS
type.GetConstructor(Type[])            // Specific constructor
type.GetConstructors()                 // All public constructors

// PROPERTIES
type.GetProperty("Name")               // Specific property
type.GetProperties()                   // All public properties
prop.GetValue(obj)                     // Get property value
prop.SetValue(obj, value)              // Set property value
prop.GetCustomAttribute<T>()           // Property attribute

// METHODS
type.GetMethod("MethodName")           // Specific method
type.GetMethods()                      // All public methods
method.Invoke(instance, parameters)    // Invoke method
method.GetCustomAttribute<T>()         // Method attribute

// FIELDS
type.GetField("fieldName")             // Specific field
type.GetFields()                       // All public fields

// CREATING INSTANCES
Activator.CreateInstance(type)         // Create instance
Activator.CreateInstance(type, args)   // With constructor params

// ASSEMBLIES
Assembly.GetExecutingAssembly()        // Current assembly
Assembly.GetTypes()                    // All types in assembly
Enter fullscreen mode Exit fullscreen mode

Quick Decision Guide

When to Use Attributes?

  • βœ… Validation rules
  • βœ… API routing
  • βœ… Database mapping
  • βœ… Serialization control
  • βœ… Testing metadata
  • βœ… Configuration
  • ❌ Business logic (use methods)
  • ❌ Performance-critical paths

When to Use Reflection?

  • βœ… Framework/library code
  • βœ… Plugin systems
  • βœ… ORM implementation
  • βœ… Dependency injection
  • βœ… Test frameworks
  • βœ… Code generation tools
  • ❌ Application code (usually)
  • ❌ Tight loops (cache results)

Summary: How Frameworks Use This

ASP.NET Core

[HttpGet("api/users/{id}")]  // Routing
public User GetUser(int id) { }
Enter fullscreen mode Exit fullscreen mode

Framework reads HttpGet attribute to register route.

Entity Framework

[Table("users")]
public class User
{
    [Key]
    public int Id { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

EF reads attributes to generate SQL.

Data Annotations

[Required]
[StringLength(50)]
public string Name { get; set; }
Enter fullscreen mode Exit fullscreen mode

ASP.NET reads these to validate form data.

Dependency Injection

services.AddTransient<IUserService, UserService>();
Enter fullscreen mode Exit fullscreen mode

DI container uses reflection to create instances and inject dependencies.


Final Tips

  1. Attributes are metadata - They describe code, they don't execute
  2. Reflection reads metadata - At runtime, using Type, PropertyInfo, etc.
  3. Cache reflection results - Reflection is slow, cache what you find
  4. Convention: End attribute names with "Attribute" - But you can omit it in code
  5. Use AttributeUsage - Control where attributes can be applied
  6. GetCustomAttribute vs IsDefined - Use IsDefined when you only need to check existence
  7. Performance matters - Avoid reflection in hot paths
  8. Most developers use - Existing framework attributes, rarely create custom ones

Remember: You'll use existing attributes (from frameworks) 90% of the time. Creating custom attributes is mainly for building your own frameworks or tools.

Top comments (0)