Menu

C# Reflection and Attributes: typeof, GetType and Custom Attributes

How reflection works in C#: Type objects from typeof and GetType, reading and setting properties, calling methods by name, creating instances, and attributes: the built-in ones such as Obsolete, declaring your own, and reading them at run time. Plus what reflection costs.

This page includes runnable editors - edit, run, and see output instantly.

Reflection lets a program examine types while it runs: which properties a class has, what their values are, which methods exist, which attributes are attached. Attributes are the other half: declarative tags in square brackets, like [Obsolete] or [JsonPropertyName("id")], that mean nothing by themselves until the compiler or some code reads them with reflection. Serializers, ORMs, validation libraries, test frameworks and ASP.NET routing all work this way.

Most application code rarely needs reflection directly, but knowing how it works explains a lot of framework behavior.

Type objects: typeof and GetType

Everything starts from a System.Type. There are two ways to get one:

Output:

Employee
Manager
Employee
False
True
True
System.Int32
False
Name

The difference to remember: typeof(X) names a type you know when writing the code, and obj.GetType() asks an object what it really is. Comparing GetType() == typeof(Employee) is an exact match that fails for derived classes, which is usually not what you want; is and IsAssignableFrom respect inheritance. nameof looks similar but is not reflection at all: the compiler replaces it with a string constant.

GetType() on a null reference throws NullReferenceException, since there is no object to ask. On a boxed nullable value type it returns the underlying type: ((int?)5).GetType() is System.Int32.

Reading and setting properties

GetProperties() lists the public properties of a type as PropertyInfo objects, each of which can read and write the value on a given instance:

Output:

Name   String   = Mug
Price  Decimal  = 8.50
Stock  Int32    = 12
7.90
True
SUP-77

Three things this shows:

  • GetValue returns object, so value types come back boxed and you cast them to use them.
  • GetProperty with a name that does not exist returns null, and the next call on it throws NullReferenceException. Check before use.
  • BindingFlags.NonPublic | BindingFlags.Instance reaches private members. That is legitimate in tooling and tests, but it bypasses encapsulation and breaks silently when the class is refactored.

This loop is essentially how a CSV exporter or an object-to-JSON serializer works: walk the properties, read each value, format it.

Calling methods and creating objects by name

GetMethod finds a method, and Invoke calls it with an array of arguments. Activator.CreateInstance creates an object from a Type, which is how plugin systems and DI containers build types chosen at run time:

Output:

60.00
Decimal WithTax(1 parameters)
String Describe(0 parameters)
True

DeclaredOnly limits the list to members declared in the class itself; without it, GetMethods also returns ToString, Equals, GetHashCode and GetType from object. Type.GetType("Name") needs the namespace-qualified name, and for types in other assemblies the assembly name too ("MyApp.Plugins.Csv, MyApp.Plugins").

If the invoked method throws, Invoke wraps the exception in a TargetInvocationException; the original is in its InnerException.

Attributes: tags the compiler and frameworks read

An attribute is written in square brackets before the thing it describes. The framework defines many; a few that change what the compiler does:

public class OrderService
{
    [Obsolete("Use PlaceOrderAsync instead.")]
    public void PlaceOrder(Order order) { }
    // Every call site: warning CS0618: 'OrderService.PlaceOrder(Order)' is obsolete: 'Use PlaceOrderAsync instead.'
    // [Obsolete("...", true)] makes it error CS0619 instead.

    [Conditional("DEBUG")]
    public void Trace(string message) => Console.WriteLine(message);
    // Calls to Trace are removed entirely from builds without the DEBUG symbol.
}

[Flags] enum Channels { None = 0, Email = 1, Sms = 2 }   // changes how ToString formats combinations
[Serializable] class Snapshot { }                     // marks a type for legacy binary serialization

Others are read by libraries at run time: [JsonPropertyName] and [JsonIgnore] by System.Text.Json, [Required] and [MaxLength] by ASP.NET Core model validation and Entity Framework, [HttpGet("orders/{id}")] by ASP.NET routing, [Fact] and [Test] by test runners. The attribute itself does nothing; the code that looks for it does.

The name ObsoleteAttribute is shortened to [Obsolete] when applied: by convention every attribute class name ends in Attribute, and C# lets you drop the suffix.

Declaring and reading a custom attribute

A custom attribute is a class that derives from Attribute. [AttributeUsage] says what it may be applied to. Constructor parameters become positional arguments, and public settable properties become named arguments:

Output:

Username must be at most 20 characters
Email is required
Keep the city code short
0

That is a miniature version of what ASP.NET Core model validation does with System.ComponentModel.DataAnnotations. Attribute arguments must be compile-time constants (numbers, strings, typeof(...), enum values, or arrays of those), because they are stored in the assembly's metadata. GetCustomAttribute<T>() is an extension method in System.Reflection; there is also IsDefined(typeof(T)) when you only need to know whether an attribute is present.

The cost of reflection

Reflection trades speed and safety for flexibility:

  • Speed. Looking up a member by name and calling it through Invoke or GetValue is far slower than a direct call, and boxes value types. For repeated use, look up the PropertyInfo or MethodInfo once and keep it, or turn it into a delegate with Delegate.CreateDelegate or MethodInfo.CreateDelegate and call that.
  • Safety. A misspelled name or a changed signature compiles fine and fails at run time. Prefer nameof(Product.Price) over the string "Price" wherever you can, so renames are caught.
  • Trimming and AOT. Trimmed and Native AOT apps remove members nothing appears to use, and reflection hides usage from that analysis. Modern libraries (System.Text.Json, GeneratedRegex, logging) are moving to source generators, which do the same work at compile time.

Use reflection for the parts of a program that genuinely do not know their types in advance: plugins, generic tooling, serializers, test helpers. When the types are known, ordinary code, generics or interfaces are faster and checked by the compiler.

Common mistakes

  • GetType() == typeof(Base) to test for a base class. It fails for derived types. Use is or IsAssignableFrom.
  • Not checking for null. GetProperty, GetMethod and Type.GetType return null when nothing matches.
  • Reflection in a hot loop without caching. Cache the MemberInfo or compile a delegate.
  • Catching the wrong exception from Invoke. The real exception is the InnerException of TargetInvocationException.
  • Magic strings for member names. Use nameof.

Frequently Asked Questions

What is reflection in C#?

Reflection is the ability of a program to inspect types at run time: list a class's properties and methods, read and set values by name, call methods, create instances, and read attributes. It lives in System.Reflection and starts from a Type object. Serializers, ORMs, dependency injection containers and test frameworks are built on it.

What is the difference between typeof and GetType in C#?

typeof(Customer) is resolved at compile time from a type name and needs no object. obj.GetType() is called on an instance at run time and returns the object's actual type, which can be more derived than the variable's declared type: for Animal a = new Dog();, a.GetType() is Dog. GetType() on a null reference throws NullReferenceException.

How do I get a property value by name in C#?

obj.GetType().GetProperty("Price") returns a PropertyInfo (or null if there is no such public property), and .GetValue(obj) reads it as an object. .SetValue(obj, value) writes it. Cache the PropertyInfo if you do this in a loop, because the lookup is the expensive part.

How do I create a custom attribute in C#?

Declare a class that derives from System.Attribute, name it with the Attribute suffix, and mark where it may be used with [AttributeUsage]: [AttributeUsage(AttributeTargets.Property)] class MaxLengthAttribute : Attribute { public int Length { get; } public MaxLengthAttribute(int length) { Length = length; } }. Apply it as [MaxLength(50)] and read it with property.GetCustomAttribute<MaxLengthAttribute>().

What does the Obsolete attribute do in C#?

[Obsolete("Use PlaceOrderAsync instead")] on a member makes the compiler emit warning CS0618, with your message, at every call site. [Obsolete("...", true)] turns the warning into error CS0619. It is how libraries retire an API without breaking callers overnight.

Is reflection slow in C#?

Compared with a direct call, yes: finding a member by name and invoking it through MethodInfo.Invoke or PropertyInfo.GetValue is typically tens to hundreds of times slower, and it boxes value types. It is fine for start-up, configuration and occasional use. For hot paths, cache the MemberInfo, build a delegate once, or use generics or a source generator instead.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED