π― 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)
π 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);
}
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));
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);
}
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}");
}
}
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));
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));
π 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);
β 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
β 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"); // β
β 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"));
β 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);
}
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
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() { }
2. [Serializable] - Allow Serialization
[Serializable]
public class Person
{
public string Name { get; set; }
}
3. [DllImport] - Call Native Code
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
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
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)
6. [Conditional] - Conditional Compilation
[Conditional("DEBUG")]
public void DebugLog(string message)
{
Console.WriteLine(message); // Only runs in DEBUG mode
}
Creating Custom Attributes
Step 1: Define Attribute Class
Rules:
- Must inherit from
System.Attribute - Name should end with "Attribute" (optional but convention)
- 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; }
}
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;
}
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
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 { }
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
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
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);
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!
π― 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");
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
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}");
}
}
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);
}
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");
}
}
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
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
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
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)
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
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);
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);
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
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);
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
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
}
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>());
}
}
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
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) { }
Framework reads HttpGet attribute to register route.
Entity Framework
[Table("users")]
public class User
{
[Key]
public int Id { get; set; }
}
EF reads attributes to generate SQL.
Data Annotations
[Required]
[StringLength(50)]
public string Name { get; set; }
ASP.NET reads these to validate form data.
Dependency Injection
services.AddTransient<IUserService, UserService>();
DI container uses reflection to create instances and inject dependencies.
Final Tips
- Attributes are metadata - They describe code, they don't execute
- Reflection reads metadata - At runtime, using Type, PropertyInfo, etc.
- Cache reflection results - Reflection is slow, cache what you find
- Convention: End attribute names with "Attribute" - But you can omit it in code
- Use AttributeUsage - Control where attributes can be applied
- GetCustomAttribute vs IsDefined - Use IsDefined when you only need to check existence
- Performance matters - Avoid reflection in hot paths
- 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)