DEV Community

Cover image for C# Attributes & Reflection Practical Guide
RecurPixel
RecurPixel

Posted on

C# Attributes & Reflection Practical Guide

🎯 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)