In this article, we are going to talk about Reflection in C#.
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:
Property | Value |
---|---|
Name | MotionSensor |
Namespace | ReflectionInCSharp |
FullName | ReflectionInCSharp.MotionSensor |
AssemblyQualifiedName | ReflectionInCSharp.MotionSensor, ReflectionInCSharp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null |
IsClass | True |
IsValueType | False |
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:
Member | Member Type | Remarks |
---|---|---|
.ctor | Constructor | |
.ctor | Constructor | Constructor that takes a string focalPoint parameter |
MotionDetected | Event | |
FocalPoint | Field | |
IsCritical | Method | |
Observe | Method | |
Observe | Method | Observe method that takes a string direction parameter |
Enabled | Property | |
Status | Property | |
add_MotionDetected | Method | Subscriber method of MotionDetected event |
remove_MotionDetected | Method | Unsubscriber method of MotionDetected event |
get_Enabled | Method | Getter method of Enabled property |
set_Enabled | Method | Setter method of Enabled property |
get_Status | Method | Getter method of Status property |
GetType | Method | Inherited from Object |
ToString | Method | Inherited from Object |
Equals | Method | Inherited from Object |
GetHashCode | Method | Inherited 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.