PropertyInfo and MethodInfo in C#: The Practical Developer's Guide
If you've ever needed to read or write a property at runtime without knowing its name at compile time, you've encountered the need for C# PropertyInfo and MethodInfo. These two types -- both living under System.Reflection -- are the workhorses of runtime type inspection in .NET. They power ORMs, serializers, DI containers, test frameworks, and plugin systems. Understanding them well gives you the foundation to build genuinely dynamic systems in .NET 10, while avoiding the performance traps that catch developers off guard.
This guide goes beyond the basics. We'll cover PropertyInfo and MethodInfo in depth, look at practical code patterns, touch on related types like FieldInfo and ConstructorInfo, and make sure you understand where performance matters.
If you haven't read the foundational overview yet, Reflection in C#: 4 Simple But Powerful Code Examples is a great starting point.
Where PropertyInfo and MethodInfo Fit
Before working with PropertyInfo or MethodInfo, it helps to understand where they sit within the System.Reflection namespace. Every piece of type metadata in .NET is accessible through a hierarchy of objects rooted at Assembly. The System.Reflection namespace provides a hierarchy of types that represent the metadata of a .NET assembly:
Assembly-- the compiled.dllor.exeModule-- a partition within an assemblyType-- a class, struct, interface, enum, or delegateMemberInfo-- the abstract base for all membersPropertyInfo-- a property (get/setaccessors)MethodInfo-- a methodFieldInfo-- a fieldEventInfo-- an eventConstructorInfo-- a constructor
You get to PropertyInfo and MethodInfo through a Type instance, which you obtain from typeof(MyClass), instance.GetType(), or Type.GetType("Namespace.ClassName").
PropertyInfo Deep Dive
PropertyInfo is the type that represents a single property on a class or struct at runtime. You can use it to read the property's value with GetValue(), write it with SetValue(), inspect its type via PropertyType, and check whether get/set accessors are available. It also exposes the underlying MethodInfo for each accessor, which matters when you need to check accessibility at the method level rather than the property level.
Getting a PropertyInfo
Type type = typeof(Person);
// Get a specific named property
PropertyInfo? nameProp = type.GetProperty("Name");
// Get all public instance properties
PropertyInfo[] allProps = type.GetProperties(
BindingFlags.Public | BindingFlags.Instance);
The overload without BindingFlags returns public instance and static properties. Always prefer the explicit BindingFlags overload -- it narrows the search and communicates intent.
Reading and Writing Values
Once you have a PropertyInfo reference, GetValue(object? obj) retrieves the property's current value from a given instance, and SetValue(object? obj, object? value) assigns a new value. Both return and accept object?, so value types are boxed on the way out and unboxed on the way in. If the setter is private or protected, SetValue may throw MethodAccessException or behave unexpectedly depending on runtime context and access level -- always test your specific scenario. To reliably reach a private setter, explicitly retrieve it with GetSetMethod(nonPublic: true) and invoke it through that. Here's a practical illustration:
public class Person
{
public string Name { get; set; } = "";
public int Age { get; private set; }
public Person(string name, int age) { Name = name; Age = age; }
}
var person = new Person("Alice", 30);
Type type = typeof(Person);
// Read
PropertyInfo nameProp = type.GetProperty("Name")!;
string? value = (string?)nameProp.GetValue(person);
Console.WriteLine(value); // "Alice"
// Write (only works if the setter is accessible)
nameProp.SetValue(person, "Bob");
Console.WriteLine(person.Name); // "Bob"
// Attempting to set a property with a private setter may throw MethodAccessException
PropertyInfo ageProp = type.GetProperty("Age")!;
// ageProp.SetValue(person, 31); // may throw MethodAccessException depending on runtime context
// With NonPublic binding, you can reach the private setter
PropertyInfo agePropNonPublic = type.GetProperty(
"Age",
BindingFlags.Public | BindingFlags.Instance)!;
MethodInfo? privateSetter = agePropNonPublic.GetSetMethod(nonPublic: true);
privateSetter?.Invoke(person, new object[] { 31 });
CanRead and CanWrite
CanRead and CanWrite indicate whether a property has a getter or setter at all, but they don't tell you about accessibility -- a property with a private setter still returns true for CanWrite. To distinguish public from non-public accessors, you must call GetGetMethod() or GetSetMethod() with the nonPublic parameter. The default overloads without that parameter return only public accessors.
PropertyInfo prop = typeof(Person).GetProperty("Age")!;
Console.WriteLine(prop.CanRead); // true
Console.WriteLine(prop.CanWrite); // true -- the setter exists, just private
// GetSetMethod(false) only returns public setter; GetSetMethod(true) returns any
MethodInfo? publicSetter = prop.GetSetMethod(nonPublic: false); // null
MethodInfo? anySetter = prop.GetSetMethod(nonPublic: true); // not null
PropertyType
PropertyType tells you what type the property holds at runtime. This is especially useful when building generic mappers, validators, or serializers that need to branch on the property's type -- for example, to apply different formatting to DateTime vs string properties, or to detect nullable reference and value types.
PropertyInfo prop = typeof(Person).GetProperty("Age")!;
Console.WriteLine(prop.PropertyType.Name); // "Int32"
bool isNullable = Nullable.GetUnderlyingType(prop.PropertyType) != null;
Practical Example: Generic Property Copier
Here's a real-world use case -- copying matching properties from one object to another, regardless of their specific type:
public static class PropertyCopier
{
private static readonly ConcurrentDictionary<(Type, Type), (PropertyInfo Source, PropertyInfo Target)[]>
_cache = new();
public static void CopyProperties<TSource, TTarget>(TSource source, TTarget target)
where TSource : class
where TTarget : class
{
var key = (typeof(TSource), typeof(TTarget));
var pairs = _cache.GetOrAdd(key, k => BuildPairs(k.Item1, k.Item2));
foreach (var (srcProp, tgtProp) in pairs)
{
var value = srcProp.GetValue(source);
tgtProp.SetValue(target, value);
}
}
private static (PropertyInfo, PropertyInfo)[] BuildPairs(Type src, Type tgt)
{
var srcProps = src.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead)
.ToDictionary(p => p.Name);
return tgt.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanWrite && srcProps.ContainsKey(p.Name))
.Select(p => (srcProps[p.Name], p))
.ToArray();
}
}
The pairs are built once per type-pair combination and cached. Subsequent calls do only the GetValue/SetValue work -- no metadata lookups.
MethodInfo Deep Dive
MethodInfo is the reflection type that represents a callable method. Like PropertyInfo, you get it through a Type object. Once you have a MethodInfo, you can inspect its parameters, return type, generic arguments, and calling conventions -- and you can invoke it dynamically at runtime by calling Invoke(). Understanding the subtleties of how MethodInfo resolves overloaded methods and handles generic definitions is key to using it correctly.
Getting a MethodInfo
Type type = typeof(Calculator);
// Specific method by name (fails if overloaded)
MethodInfo? addMethod = type.GetMethod("Add");
// Specific overload by parameter types
MethodInfo? addInts = type.GetMethod("Add", new[] { typeof(int), typeof(int) });
// All public instance methods
MethodInfo[] allMethods = type.GetMethods(
BindingFlags.Public | BindingFlags.Instance);
When a method is overloaded, GetMethod(string name) will throw AmbiguousMatchException. Always pass the parameter types array when the method might be overloaded.
Invoking a Method
Calling MethodInfo.Invoke(object? obj, object?[]? parameters) executes the method. For instance methods, obj is the target object. For static methods, pass null. The return value is always object? and must be cast. If the method throws, the exception is wrapped in a TargetInvocationException -- you should unwrap it before propagating. Here's a clear example covering both instance and static invocation:
public class Calculator
{
public int Add(int a, int b) => a + b;
public static double Sqrt(double value) => Math.Sqrt(value);
}
var calc = new Calculator();
MethodInfo addMethod = typeof(Calculator).GetMethod("Add", new[] { typeof(int), typeof(int) })!;
// Instance method: pass the target as first arg to Invoke
object? result = addMethod.Invoke(calc, new object[] { 3, 4 });
Console.WriteLine(result); // 7
// Static method: pass null as the target
MethodInfo sqrtMethod = typeof(Calculator).GetMethod("Sqrt")!;
object? sqrtResult = sqrtMethod.Invoke(null, new object[] { 16.0 });
Console.WriteLine(sqrtResult); // 4
GetParameters and ReturnType
Two of the most commonly used MethodInfo properties are GetParameters() and ReturnType. GetParameters() returns an array of ParameterInfo objects, each carrying the parameter name, type (ParameterType), position, and any default value. ReturnType is a Type representing what the method returns -- for void methods, that's typeof(void). Inspecting these allows you to build type-safe dynamic wrappers and validate argument compatibility at runtime before invoking.
MethodInfo method = typeof(Calculator).GetMethod("Add", new[] { typeof(int), typeof(int) })!;
Console.WriteLine(method.ReturnType.Name); // "Int32"
foreach (ParameterInfo param in method.GetParameters())
{
Console.WriteLine($"{param.Name}: {param.ParameterType.Name}");
// a: Int32
// b: Int32
}
ParameterInfo carries the name, type, position, and any default value. This is how frameworks build dynamic method dispatchers.
IsGenericMethod
public class Box
{
public T Wrap<T>(T value) => value;
}
MethodInfo wrapMethod = typeof(Box).GetMethod("Wrap")!;
Console.WriteLine(wrapMethod.IsGenericMethod); // true
Console.WriteLine(wrapMethod.IsGenericMethodDefinition); // true
// Make a concrete version for int
MethodInfo wrapInt = wrapMethod.MakeGenericMethod(typeof(int));
var box = new Box();
int wrapped = (int)wrapInt.Invoke(box, new object[] { 42 })!;
MakeGenericMethod constructs a closed generic method from an open generic definition. Cache the result -- each call to MakeGenericMethod involves JIT work.
Practical Example: Simple Method Dispatcher
The following dispatcher ties together the caching and invocation patterns described above into a reusable component. It resolves a method by name from any object, caches the MethodInfo in a ConcurrentDictionary keyed by (Type, string), and correctly unwraps TargetInvocationException so callers see the real error. This is a common pattern in command-routing systems and dynamic plugin dispatchers:
public sealed class MethodDispatcher
{
private readonly ConcurrentDictionary<(Type, string), MethodInfo?> _cache = new();
public object? Dispatch(object target, string methodName, params object[] args)
{
var type = target.GetType();
var key = (type, methodName);
var method = _cache.GetOrAdd(key, k =>
k.Item1.GetMethod(k.Item2, BindingFlags.Public | BindingFlags.Instance));
if (method is null)
throw new InvalidOperationException(
$"Method '{methodName}' not found on '{type.Name}'.");
try
{
return method.Invoke(target, args);
}
catch (TargetInvocationException tie)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo
.Capture(tie.InnerException!)
.Throw();
return null; // unreachable
}
}
}
Notice the TargetInvocationException unwrapping -- that's the correct way to propagate exceptions from reflected method calls.
FieldInfo: A Brief Look
FieldInfo is the reflection equivalent of a field (not a property). Fields don't have getters/setters -- they're direct memory slots. The API is similar to PropertyInfo:
public class Config
{
public string ConnectionString = ""; // field, not property
}
FieldInfo field = typeof(Config).GetField("ConnectionString")!;
var config = new Config();
field.SetValue(config, "Server=localhost;Database=dev");
string? value = (string?)field.GetValue(config);
The key difference: FieldInfo has no CanRead/CanWrite because fields are always both readable and writable (unless readonly). Use field.IsInitOnly to check for readonly fields.
EventInfo and ConstructorInfo Overview
Beyond PropertyInfo, MethodInfo, and FieldInfo, the System.Reflection namespace includes EventInfo and ConstructorInfo. These are less frequently used in day-to-day code, but understanding them rounds out your mental model of the reflection API and comes in handy in plugin systems, dynamic event wiring, and factory patterns.
EventInfo
EventInfo represents a .NET event. You can add or remove handlers at runtime -- useful for building dynamic event wiring in plugin systems or UI frameworks that need to attach behavior without knowing the event name at compile time:
EventInfo? clickEvent = typeof(Button).GetEvent("Click");
// clickEvent.AddEventHandler(button, handler);
// clickEvent.RemoveEventHandler(button, handler);
Useful for building dynamic event wiring in plugin systems or UI frameworks.
ConstructorInfo
ConstructorInfo represents a constructor. It's what powers Activator.CreateInstance under the hood. Caching and calling ConstructorInfo.Invoke directly can be faster in repeated-use scenarios when the cached reference is reused many times.
ConstructorInfo ctor = typeof(Person)
.GetConstructor(new[] { typeof(string), typeof(int) })!;
Person person = (Person)ctor.Invoke(new object[] { "Charlie", 25 });
For a deeper dive on this, see Activator.CreateInstance in C# - A Quick Rundown and the comparison in Activator.CreateInstance vs Type.InvokeMember - A Clear Winner?.
Performance Gotchas: Why Caching PropertyInfo and MethodInfo Matters
Every call to GetProperty(), GetMethod(), GetProperties(), or GetMethods() involves:
- Looking up the
Typeobject in the CLR's internal metadata tables - Allocating the result object(s)
- Potentially filtering by
BindingFlags
This is not free. The first call is meaningfully slower than subsequent calls even with CLR internal caching, and methods like GetProperties() allocate a new array every time.
The pattern to internalize: look up once, store forever, access many times.
// ✅ Good: static cache, lookup happens once per type
private static readonly ConcurrentDictionary<Type, PropertyInfo[]> _cache = new();
PropertyInfo[] GetCachedProperties(Type t) =>
_cache.GetOrAdd(t, type =>
type.GetProperties(BindingFlags.Public | BindingFlags.Instance));
// ❌ Bad: lookup happens on every call
PropertyInfo[] GetProperties(Type t) =>
t.GetProperties(BindingFlags.Public | BindingFlags.Instance);
When building caches that are written at startup and only read after that, FrozenDictionary<Type, PropertyInfo[]> from System.Collections.Frozen (available in .NET 8+ and fully supported in .NET 10) reduces lookup overhead compared to ConcurrentDictionary for the steady-state read case.
BindingFlags Explained
BindingFlags controls which members are returned. The most important flags:
| Flag | Effect |
|---|---|
BindingFlags.Public |
Include public members |
BindingFlags.NonPublic |
Include private/protected/internal members |
BindingFlags.Instance |
Include instance members |
BindingFlags.Static |
Include static members |
BindingFlags.DeclaredOnly |
Exclude inherited members |
BindingFlags.FlattenHierarchy |
Include inherited static members (rare) |
Flags are combined with bitwise OR. Missing the Instance or Static flag when it's needed is a common bug -- GetProperty("Name") without flags uses defaults that include both, but GetProperty("Name", BindingFlags.Public) without also specifying BindingFlags.Instance will return null for an instance property.
// ✅ Explicit and correct
var prop = type.GetProperty("Name",
BindingFlags.Public | BindingFlags.Instance);
// ⚠️ Might return null even if the property exists
var propBug = type.GetProperty("Name", BindingFlags.Public); // missing Instance flag
For an exploration of how proper BindingFlags usage feeds into plugin-style architectures, see Plugin Contracts and Interfaces in C#: Designing Extensible Plugin Systems.
FAQ
Reflection questions come up often when developers first start building dynamic systems in .NET. Here are answers to the most common questions about PropertyInfo, MethodInfo, and the surrounding reflection API.
What is PropertyInfo in C# used for?
PropertyInfo represents a property on a type at runtime. It's used to read values with GetValue(), write values with SetValue(), and inspect metadata like PropertyType, CanRead, and CanWrite. Common use cases include generic mappers, serializers, validation frameworks, and dynamic DTO converters.
What is the difference between PropertyInfo and FieldInfo in C#?
PropertyInfo represents a C# property (which has get/set accessor methods under the hood). FieldInfo represents a field -- a raw memory slot on the class. Properties are typically backed by fields but expose validation and encapsulation logic through their accessors. FieldInfo is faster to access via reflection since there's no method dispatch, but fields are usually private in well-designed classes.
How do I invoke a method using MethodInfo in C#?
Call MethodInfo.Invoke(target, args) where target is the object instance (or null for static methods) and args is an object[] of arguments in declaration order. The return value is object? and must be cast to the expected type. Wrap the call in a try/catch for TargetInvocationException to access the real exception if the method throws.
Why does GetProperty return null even though the property exists?
The most common reason is a missing or incorrect BindingFlags. If you pass BindingFlags.Public without also including BindingFlags.Instance, instance properties won't be found. Similarly, if you're looking for a property declared in a base class but pass BindingFlags.DeclaredOnly, it won't appear. Omitting BindingFlags.NonPublic won't find private properties.
Is it safe to cache PropertyInfo and MethodInfo across threads?
Yes. PropertyInfo and MethodInfo instances are immutable metadata descriptors. They don't hold instance state. It's safe to share them across threads via a static readonly field or a thread-safe dictionary like ConcurrentDictionary. The caching pattern is in fact encouraged for performance.
How does MethodInfo handle generic methods?
For generic method definitions, IsGenericMethodDefinition is true. You cannot invoke a generic method definition directly -- you first call MakeGenericMethod(typeof(T1), typeof(T2), ...) to get a concrete MethodInfo for the closed type arguments you want. That concrete MethodInfo can then be invoked normally. Cache the result of MakeGenericMethod per type argument combination.
Can I use PropertyInfo and MethodInfo in .NET Native AOT?
Yes, with caveats. Native AOT trims away code that appears unreachable at publish time. Reflection-accessed members may be removed unless you annotate code with [DynamicallyAccessedMembers], use [RequiresUnreferencedCode], or configure rd.xml / trimmer roots. In .NET 10, the publish toolchain emits warnings for unsafe reflection patterns. For high-performance AOT scenarios, compiled delegates or source generators are typically better alternatives.
Conclusion
PropertyInfo and MethodInfo are the two reflection types you'll encounter most frequently in dynamic .NET code. PropertyInfo gives you runtime access to property values, types, and accessors. MethodInfo lets you invoke methods dynamically, inspect signatures, and work with generic methods. Both are cache-friendly -- look them up once and store them for reuse.
Understanding FieldInfo, ConstructorInfo, and EventInfo fills out the picture: each represents a different kind of type member, all reachable through the Type object. Master BindingFlags to control exactly which members you get back, and always handle TargetInvocationException when invoking through reflection.
With these tools in hand, you're equipped to build dynamic systems -- from simple property copiers to full plugin dispatchers -- while keeping performance under control.

