In this article, we are going to talk about Reflection in C#.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

What is Reflection in C#?

In the OOP world, we define the characteristics and behaviors of objects using models i.e. class, struct, etc. The data that can describe these models, definitions, and similar application components are commonly known as metadata. In C#, we can deal with such metadata at runtime through a powerful built-in mechanism called Reflection.

C# Reflection ecosystem primarily consists of System.Type class and the classes/interfaces from System.Reflection namespace. We can use this ecosystem for looking into objects and their types for supported properties, methods, events, interfaces,  attributes, and so on. This ability opens up enormous opportunities for runtime extensibility such as:

  • Look for certain characteristics in target objects and behave accordingly
  • Dynamic instantiation of objects from type information
  • Late binding of fields, properties, and methods
  • Copy data from one object to another
  • Collection of application insights and take pre-emptive measurements like Dependency Injection for example
  • Design of complex routines which need access to a part of code that is rather inaccessible in compile time
  • Creating new types at runtime on demand

That’s a lot of possibilities. So, let’s dive into the deep.

Reflection in C# and System.Type

Type class is the heart of the reflection ecosystem. An instance of Type contains all the metadata of a certain class or similar constructs.

Extract Type Information

To see how this works, let’s start with a single interface:

public interface IMotionSensor 
{ 
    void Observe(); 
    void Observe(string direction); 
}

And a typical .NET class that implements our interface:

[Description("Detects movements in the vicinity")]
public class MotionSensor : IMotionSensor
{
    private int _detections;

    public string? FocalPoint;

    public MotionSensor() : this("*") { }

    public MotionSensor(string focalPoint) => FocalPoint = focalPoint;

    public event EventHandler<string>? MotionDetected;

    [Description("Turn On/Off")]
    public bool Enabled { get; set; }

    public string Status => _detections > 0 ? "Red" : "Green"; 

    public bool IsCritical(int threshold) => _detections >= threshold;

    public void Observe() => RaiseMotionDetected("*");

    public void Observe(string direction) => RaiseMotionDetected(direction);

    private void RaiseMotionDetected(string direction)
    {
        _detections++;
        FocalPoint = direction;
        MotionDetected?.Invoke(this, direction);
    }
}

We define a MotionSensor class with two constructors, an event, a field, and two properties. This class supports the IMotionSensor interface. So, we implement a parameter-less Observe() method and its overload that takes a direction parameter. There is another IsCritical() method that returns a boolean value. Apart from these public members, we have a private field and a private method as well. In addition, we decorate the class and the Enabled property with the Description attribute.

We can programmatically explore all these components by extracting the type information:

var type1 = typeof(MotionSensor);

var sensor = new MotionSensor();
var type2 = sensor.GetType();

var qualifiedName = "ReflectionInCSharp.MotionSensor, ReflectionInCSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
var type3 = Type.GetType(qualifiedName);

As we can see, there are several ways to retrieve a type. The quickest way is to use the typeof operator with the class name. If we have an object instance, we call the GetType() method instead.

The third form uses the static Type.GetType() method to resolve the type from a plain string. This variant is handy to dynamically resolve a type from a variable. However, the argument here is not the usual class name but a special name called AssemblyQualifiedName. It consists of the class name, namespace, and assembly information.

System.Type at a Glance

Now that we get a type instance, let’s take a look at the key information it provides:

PropertyValue
NameMotionSensor
NamespaceReflectionInCSharp
FullNameReflectionInCSharp.MotionSensor
AssemblyQualifiedNameReflectionInCSharp.MotionSensor, ReflectionInCSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
IsClassTrue
IsValueTypeFalse

Here, we see a bunch of metadata about MotionSensor such as its name, full name, namespace, assembly-qualified name, etc. Also, we can check whether it’s a class or value-type. 

Type of a generic class like List<T> also holds the generic type arguments:

var type = typeof(List<string>);

Assert.True(type.IsGenericType);
Assert.Single(type.GenericTypeArguments);
Assert.Equal(typeof(string), type.GenericTypeArguments[0]);

This is just a glimpse of the information available. Check this link for a complete list.

Dynamic Object Creation by Reflection in C#

A type instance is not only an information container but also a tool for instantiating objects dynamically:

var type = typeof(MotionSensor);

var sensor1 = Activator.CreateInstance(type)!;
var sensor2 = Activator.CreateInstance(type, new[] { "left" })!;

Assert.True(sensor1 is MotionSensor);
Assert.Equal("*", ((MotionSensor)sensor1).FocalPoint);

Assert.True(sensor2 is MotionSensor);
Assert.Equal("left", ((MotionSensor)sensor2).FocalPoint);

With the help of System.Activator class, we can create instances of MotionSensor from its type. This works by invoking the class’s constructor, usually a public one, under the hood.

For a parameter-less constructor, we achieve this by calling the CreateInstance method with the type. A parameterized constructor, on the other hand, needs appropriate arguments as an array. For example, in the case of sensor2, we pass a string argument to ensure that instantiation happens through the second constructor which takes a string parameter.

In some advanced scenarios, we may need an instance of a class that exists for some internal mechanism and doesn’t have a public constructor:

public class InternalTracker
{
    private static int _instanceCount = 0;

    private InternalTracker() => Sequence = ++_instanceCount;

    public int Sequence { get; }
}

As it looks, we can never instantiate an InternalTracker object outside of the class in compile time. But, reflection allows us to do it in runtime:

var type = typeof(InternalTracker);

var tracker = Activator.CreateInstance(type, nonPublic: true)!;

Assert.True(tracker is InternalTracker);
Assert.Equal(1, ((InternalTracker)tracker).Sequence);

This time we use an overload of CreateInstance method that accepts a nonPublic flag. That does the trick!

However, this ability exists not to serve general-purpose solutions but for unlikely advanced manipulations.

Inspect Members of a Class

System.Type also offers plenty of methods to dig out more useful metadata. Among them, the Get* methods are the most important ones.

One such method is GetMembers which supplies general information about members of a class:

var type = typeof(MotionSensor);

var members = type.GetMembers();

This provides us with an array of public members, each one represented by a MemberInfo object:

MemberMember TypeRemarks
.ctorConstructor
.ctorConstructorConstructor that takes a string focalPoint parameter
MotionDetectedEvent
FocalPointField
IsCriticalMethod
ObserveMethod
ObserveMethodObserve method that takes a string direction parameter
EnabledProperty
StatusProperty
add_MotionDetectedMethodSubscriber method of MotionDetected event
remove_MotionDetectedMethodUnsubscriber method of MotionDetected event
get_EnabledMethodGetter method of Enabled property
set_EnabledMethodSetter method of Enabled property
get_StatusMethodGetter method of Status property
GetTypeMethodInherited from Object
ToStringMethodInherited from Object
EqualsMethodInherited from Object
GetHashCodeMethodInherited from Object

The result set includes all public constructors, properties, fields, methods, and events of MotionSensor.

Interestingly, it also includes some alien entries. The reason is twofold. Firstly, the getter/setter of each auto-property (Enabled, Status) is interpreted by an equivalent method under the hood. The same goes for the MotionDetected event for its subscribe/unsubscribe ability. Secondly, GetMembers() includes the members of the base class by default. That’s why the last four members in the list come from the mother of all .NET objects i.e. the Object class.

We can also request information for a specific member by name:

var type = typeof(MotionSensor);

var members = type.GetMember(nameof(MotionSensor.Observe))!;

Contrary to its singular name, GetMember method actually returns an array of members:

Observe : Method
Observe : Method

BindingFlags

Many of the Get* methods have overloads that work with a BindingFlags parameter. This flag decides the searching scope of the target member. For example, BindingFlags.Static instructs to look for a static member, BindingFlags.Instance demands for an instance member and so on.

We can even look for a private member:

var type = typeof(MotionSensor);

var members = type.GetMember("RaiseMotionDetected", BindingFlags.NonPublic | BindingFlags.Instance)!;

Assert.Single(members);
Assert.True(((MethodInfo)members[0]).IsPrivate);

We get access to a private RaiseMotionDetected method using a combination of NonPublic and Instance flags!

Explore Class Components by Reflection in C#

MemberInfo is the base of all other variants specific to a certain category such as PropertyInfo, MethodInfo, ConstructorInfo, etc. We also have specific Get* methods for each of these categories.

Properties and Fields

Extracting metadata for properties is probably the most frequent reflection task:

var type = typeof(MotionSensor);

var properties = type.GetProperties(); 
var statusProperty = type.GetProperty(nameof(MotionSensor.Status))!;

Assert.Equal(2, properties.Length);
Assert.Equal(typeof(string), statusProperty.PropertyType);
Assert.False(statusProperty.CanWrite);
Assert.True(statusProperty.CanRead);

By calling GetProperties() method, we get an array of PropertyInfo containing all public properties. The singular GetProperty() form allows us to pick each property individually by name.  

A PropertyInfo contains vital information about a property like its data type (PropertyType), writability (CanWrite), readability (CanRead), etc. Most importantly, it provides us with the ability to dynamically get/set the value of a property:

object? GetPropertyValue(object obj, string propertyName)
{
    var propertyInfo = obj.GetType().GetProperty(propertyName);

    return propertyInfo?.GetValue(obj);
}

void SetPropertyValue(object obj, string propertyName, object value)
{
    var propertyInfo = obj.GetType().GetProperty(propertyName);

    propertyInfo?.SetValue(obj, value);
}

We come up with two helper methods that aim to get/set the value of an object’s property by name. Our first step is to retrieve the target PropertyInfo from the object’s type. Then, to get the value, we call propertyInfo.GetValue() method with the target object. And for the set action, calling the propertyInfo.SetValue() method with the supplied value does the job.

With these helper methods, getting or setting a property value is just a one-liner code:

var sensor = new MotionSensor();

var enabled = GetPropertyValue(sensor, nameof(sensor.Enabled))!;
Assert.Equal(false, enabled);

var status = GetPropertyValue(sensor, nameof(sensor.Status))!;
Assert.Equal("Green", status);

SetPropertyValue(sensor, nameof(sensor.Enabled), true);
Assert.True(sensor.Enabled);

Assert.ThrowsAny<ArgumentException>(() => SetPropertyValue(sensor, nameof(sensor.Status), "Yellow"));

As expected, calling GetPropertyValue() or SetPropertyValue() rightly reflects the target property. Nevertheless, trying to set a getter-only property (e.g. Status) fails. This confirms one key fact: GetValue/SetValue methods are not forgiving and throw exceptions against non-permitted attempts.

Similar to properties, we can work with fields with the help of GetFields/GetField methods. 

Methods and Parameters

If we want to collect information about methods only, we can do that too:

var type = typeof(MotionSensor);

var methods = type.GetMethods();

var isCritical = type.GetMethod(nameof(MotionSensor.IsCritical));
Assert.Throws<AmbiguousMatchException>(() => type.GetMethod(nameof(MotionSensor.Observe)));

var observe1 = type.GetMethod(nameof(MotionSensor.Observe), Type.EmptyTypes)!;
Assert.Empty(observe1.GetParameters());

var observe2 = type.GetMethod(nameof(MotionSensor.Observe), new Type[] { typeof(string) })!;
Assert.Single(observe2.GetParameters());

Like properties, we can retrieve metadata of all methods at once using GetMethods() or can pick a specific method by GetMethod().

There is one crucial point though. A single method like IsCritical just needs a name to retrieve. But for overloaded methods, we also have to supply proper parameter types to uniquely identify a specific overload. For example, passing an empty type-array returns the parameter-less Observe method. Similarly, we get the second Observe method by supplying the string type.

Once we get a MethodInfo, we can examine its parameters and return type:

var method = typeof(MotionSensor).GetMethod(nameof(MotionSensor.IsCritical))!;

var parameters = method.GetParameters();

Assert.Single(parameters);
Assert.Equal("threshold", parameters[0].Name);
Assert.Equal(typeof(int), parameters[0].ParameterType);

Assert.Equal(typeof(bool), method.ReturnType);

As we inspect the IsCritical method, we see it takes an int parameter and returns a bool result. 

The best thing is we can invoke an object’s method dynamically by its MethodInfo:

var method = typeof(MotionSensor).GetMethod(nameof(MotionSensor.IsCritical))!;

var sensor1 = new MotionSensor();
var sensor2 = new MotionSensor();
sensor2.Observe();

var result1 = method.Invoke(sensor1, new object[] { 1 });
var result2 = method.Invoke(sensor2, new object[] { 1 });

Assert.Equal(false, result1);
Assert.Equal(true, result2);

We start with a MethodInfo that refers to the IsCiritical method. To invoke this, we have to pass necessary arguments (as the original method expects) as an array of objects. In our case, this has to be an int value for the threshold parameter. By invoking this MethodInfo we can eventually execute the IsCritical method on as many MotionSensor objects as we want. And yes, in return we get a bool result as we would with the direct method. 

Constructors

We also have dedicated methods for constructors:

var type = typeof(MotionSensor);

var constructors = type.GetConstructors();
var constructor1 = type.GetConstructor(Type.EmptyTypes)!;
var constructor2 = type.GetConstructor(new Type[] { typeof(string) })!;

Assert.Single(constructor2.GetParameters());

This is much similar to “methods” except that we don’t need to specify the name but just the types of parameters. In other words, when we have a ConstructorInfo we can find out the necessary parameters to instantiate the object instance.

Events

GetEvents/GetEvent methods help us find events’ information in the form of EventInfo:

var type = typeof(MotionSensor);

var events = type.GetEvents();
var @event = type.GetEvent(nameof(MotionSensor.MotionDetected))!;

Assert.Single(events);
Assert.Equal(typeof(EventHandler<string>), @event.EventHandlerType);
Assert.True(@event.IsMulticast);

An EventInfo exposes an event’s handler type, multi-casting ability, an insight of internal subscribe/unsubscribe methods, etc.

Interfaces

System.Type also exposes metadata about supported interfaces:

var type = typeof(MotionSensor);

var interfaces = type.GetInterfaces();
var supported = type.GetInterface(nameof(IMotionSensor))!;
var notSupported = type.GetInterface(nameof(IDisposable));

Assert.Single(interfaces);
Assert.Equal(typeof(IMotionSensor), supported);
Assert.Null(notSupported);

When we need a list of all supported interfaces, we can use the GetInterfaces method. And, GetInterface() is handy to check whether specific interface support exists or not.

Reflection on Attribute

One of the key sources of metadata is the custom attributes. They’re useful tools of carrying extra data to facilitate features like data validation, database modeling, serialization/deserialization, etc. Reflection is our way to extract data from attributes at runtime:

var type = typeof(MotionSensor);

var classAttribute = type.GetCustomAttribute<DescriptionAttribute>()!;
var propertyAttribute = type.GetProperty(nameof(MotionSensor.Enabled))!
    .GetCustomAttribute<DescriptionAttribute>()!;

Assert.Equal("Detects movements in the vicinity", classAttribute.Description);
Assert.Equal("Turn On/Off", propertyAttribute.Description);

Our MotionSensor class has two Description attributes: one on class-level and the other on the Enabled property. We retrieve both of them with the help of the generic GetCustomAttribute() extension method. This method works with any MemberInfo instance e.g. Type, PropertyInfo, MethodInfo, etc. 

To learn more about custom attributes, you can check out our Custom Attributes in .NET article.

Reflection on Assembly

Assembly class is another important part of C# Reflection. It supplies information about a certain assembly, its types, and resources.

We can collect assembly information from various contexts:

var type = typeof(MotionSensor);

var assembly1 = type.Assembly;
var assembly2 = Assembly.GetExecutingAssembly();
var assembly3 = Assembly.GetCallingAssembly();

Assert.Equal("ReflectionInCSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", assembly1.FullName);
Assert.Equal("ReflectionInCSharp.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", assembly2.FullName);
Assert.Equal("xunit.execution.dotnet, Version=2.4.1.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c", assembly3.FullName);

For example, a type’s Assembly property tells us about the assembly it belongs to. Assembly class itself provides some static methods that tell us about the currently executing assembly, the caller assembly, the entry assembly, etc.

An Assembly instance allows us to enumerate the types that belong there:

var assembly = typeof(MotionSensor).Assembly;

var allTypes = assembly.GetTypes();
var exportedTypes = assembly.GetExportedTypes();

Here, we aim to examine the types of MotionSensor’s container assembly. GetTypes method gives us the array of all types including the internal ones. On the other hand, GetExportedTypes() provides only the types that are visible outside of that assembly:

IMotionSensor : TypeInfo
InternalTracker : TypeInfo
MotionSensor : TypeInfo

As we expect, the exported types contain only the public classes/interfaces.

Assembly also provides access to manifest resources at runtime:

var assembly = typeof(MotionSensor).Assembly;

using var stream = assembly.GetManifestResourceStream("ReflectionInCSharp.SampleManifest.txt")!;
using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();

Assert.Equal("sample resource content", content);

For example, we have an embedded “SampleManifest.txt” file in the library project. A call to assembly.GetManifestResourceStream() gives us the stream of this resource. From there, we can easily extract the content.

Dynamic Assemblies and Types

C# supports a whole new level of reflection capabilities. With the power of  System.Reflection.Emit classes we can do low-level interaction with MSIL. This allows us to create new types, new methods, and dynamic assemblies on the fly. Script engines and compilers are primary candidates for this feature.

Drawbacks of Reflection

Undoubtedly, reflection is a powerful tool for runtime customizations. But it has its price. Most of its practical usage comes with the ability to bind objects, properties, and methods lately. The late binding (or runtime binding) is generally slower than early binding (or compile-time binding) because of its extra lookup overhead. And that’s not the only drawback:

  • It is prone to side effects and maintenance overhead
  • It makes refactoring risky and prone to hidden breakages and runtime errors
  • Routines that heavily rely on reflections are generally difficult to test
  • The ability to access normally inaccessible members is a potentially dangerous thing and often causes a security risk

Conclusion  

In this article, we have learned about Reflection in C#. We have also learned how reflection can be handy at runtime as well as some alarming factors about it.