Master runtime type inspection, dynamic programming, and attribute-driven frameworks using reflection, dynamic types, ExpandoObject, and the Dynamic Language Runtime (DLR).
Reflection enables runtime inspection and invocation of types, methods, and properties by reading metadata from assemblies. Combined with attributes and dynamic types, it powers serializers, DI containers, ORMs, and test frameworks.
Reflection allows runtime inspection and invocation of types, methods, and properties by reading metadata from assemblies. It powers serializers, DI containers, ORMs, and test frameworks.
Reflection reads IL metadata at runtime — type names, method signatures, attributes, and generics. It enables late binding (calling methods by name string), plugin loading, and attribute-driven frameworks.
// Reflection: inspect type membersvar type = typeof(Order);var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);foreach (var p in props) Console.WriteLine($"{p.Name}: {p.PropertyType.Name}");// Late binding: invoke method by namevar method = type.GetMethod("Validate");var order = new Order();method?.Invoke(order, null); // Slow, allocates// Discover attributesvar attrs = type.GetCustomAttributes<ValidationAttribute>();foreach (var attr in attrs) Console.WriteLine($"Validation: {attr.ErrorMessage}");
Unoptimized reflection is 100-1000× slower than direct calls. The cost includes: metadata lookup, security checks, parameter boxing, and lack of JIT optimization.
For performance-critical reflection code, compile expression trees to cached delegates.
// Compiled delegate: cache for fast repeated callsvar type = typeof(Order);var method = type.GetMethod("Validate")!;var param = Expression.Parameter(typeof(Order));var call = Expression.Call(param, method);var fn = Expression.Lambda<Action<Order>>(call, param).Compile();// fn(order) — now near-direct-call speed after JITfor (int i = 0; i < 1000000; i++){ fn(order); // Fast: compiled delegate}// vs slow reflectionfor (int i = 0; i < 1000000; i++){ method.Invoke(order, null); // Slow: 100-1000× slower}
Cache compiled expression trees as static delegates — the compilation cost is paid once, and subsequent invocations run at near-direct-call speed compared to MethodInfo.Invoke() on every call.
// Custom attribute[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = true)]public class ValidateAttribute : Attribute{ public string ErrorMessage { get; set; } public int MinLength { get; set; } public ValidateAttribute(string errorMessage) { ErrorMessage = errorMessage; }}// Usage[Validate("Order must be valid")]public class Order{ [Validate("Name is required", MinLength = 3)] public string Name { get; set; } [Validate("Price must be positive")] public decimal Price { get; set; }}
// Discover and process attributespublic class Validator{ public static List<string> Validate(object obj) { var errors = new List<string>(); var type = obj.GetType(); // Check type-level attributes var typeAttrs = type.GetCustomAttributes<ValidateAttribute>(); // Check property-level attributes foreach (var prop in type.GetProperties()) { var attrs = prop.GetCustomAttributes<ValidateAttribute>(); foreach (var attr in attrs) { var value = prop.GetValue(obj); if (value is string str && attr.MinLength > 0) { if (str.Length < attr.MinLength) errors.Add(attr.ErrorMessage); } } } return errors; }}// Usagevar order = new Order { Name = "AB", Price = -10 };var errors = Validator.Validate(order);// errors: ["Name is required", "Price must be positive"]
[Serializable][DataContract]public class Order{ [DataMember] [JsonPropertyName("order_id")] public int Id { get; set; } [JsonIgnore] public string Internal { get; set; }}
Dependency Injection
[Service(Lifetime = ServiceLifetime.Scoped)]public class OrderService{ [Inject] public ILogger Logger { get; set; } public OrderService( [FromServices] IRepository repo) { }}
Validation
public class CreateOrderDto{ [Required] [StringLength(100, MinimumLength = 3)] public string Name { get; set; } [Range(0.01, 10000)] public decimal Price { get; set; }}
Testing
[TestClass]public class OrderTests{ [TestMethod] [DataRow(1, "Order1")] [DataRow(2, "Order2")] public void TestOrder(int id, string name) { // Test logic }}
The dynamic keyword defers member resolution from compile time to runtime via the Dynamic Language Runtime (DLR). ExpandoObject allows adding members at runtime like a property bag.
dynamic bypasses the C# type system — the compiler generates DLR call-site code that resolves members at runtime. It is useful for COM interop, Python/JavaScript interop, and consuming weakly-typed data (JSON).
// Without dynamic: cast-heavy COM codeExcel.Application excel = new Excel.Application();excel.Visible = true;Excel.Workbooks workbooks = (Excel.Workbooks)excel.Workbooks;Excel.Workbook workbook = (Excel.Workbook)workbooks.Add();Excel.Sheets sheets = (Excel.Sheets)workbook.Sheets;Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];Excel.Range range = (Excel.Range)sheet.Cells[1, 1];range.Value = "Hello";// With dynamic: readable COM codedynamic excel = Activator.CreateInstance( Type.GetTypeFromProgID("Excel.Application")!);excel.Visible = true;excel.Workbooks.Add();excel.Sheets[1].Cells[1, 1].Value = "Hello";
Avoid dynamic in performance-sensitive code paths — DLR call-site resolution is 100-1000× slower than compiled code. Prefer source generators (C# 10+) or compiled expression trees for metadata-driven APIs.