Exploring .NET reflection - Part 1

Introduction, reading signatures

Exploring .NET reflection - Part 1

Introduction

In the process of planning development for my DocFX alternative tool (MarkDoc.Core), which generates documentation for .NET projects, I decided to look for a better way of extracting types and their member signatures. The initial approach was to use the source code as input, which would require writing a bunch of regular expressions, hoping they would correctly process even the worst formatted codebase. However, why do that when .NET has a beautiful feature called reflection?

What is reflection?

Reflection is the ability to examine or modify loaded assemblies during runtime. [1]

In layman's terms, when your .NET application is running, it can examine or modify itself, its referenced assemblies, or load new assemblies (like plugins).

How do you use it?

Every .NET object provides a public GetType() method, which allows you to reflect on itself. This method returns a Type object (msdocs), that allows you to access all sorts of stuff.


public class TestClass
{
    public void TestMethod()
    {
        var type = this.GetType(); // "this" keyword can be omitted
    }
}

You can access all the given type members (such as methods, properties, fields...), attributes, and additional information describing the type itself.

Let's extend our example below, and print all the method names of the TestClass class:

public class TestClass
{
    public void TestMethod()
    {
        // Reflect on self
        var type = GetType();
        // Get all method names
        var methodNames = type
            // Extract type method
            .GetMethods()
            // Extract method names
           .Select(method => method.Name);

        // Output to user
        Console.WriteLine(string.Join(Environment.NewLine, methodNames));
    }
}

Output:

> TestMethod
> Equals
> GetHashCode
> GetType
> ToString

As you can see, the generated output includes the methods we've defined, and the ones implicitly inherited from the base object class.

When extending our example, you must have noticed that every reflected method object (MethodInfo), also has a ton of information that can be useful for your work.

Why is it useful?

So far, I've praised how cool and useful reflection is, without giving any concrete use case. Time to fix that.

UC1: Plugins

Using reflection, you can load an external assembly, which is not referenced by any of your solution projects. This means that some third party can create an assembly, based on your specifications, and distribute it separately as an extension, a.k.a. plugin, or DLC.

I'll write about this sometime later.

UC2: Calling (invoking) hidden members

You might find yourself in a situation where some method you need from some library is not marked as public. Using reflection magic, you can still access it! This also applies to hidden constructors and events.

Let's say, you need to invoke a method of some type:

public class ClassWithSecret
{
     protected void SecretMethod(int secret)
          => Console.WriteLine($"The secret message is {secret * 2}.");
}

public class TestClass
{
     public void TestMethod(ClassWithSecret input)
     {
          // Find the method
          var method = input
              // Reflect inputs type
              .GetType()
              // Retrieve type method of a given name and accessibility
              .GetMethod("SecretMethod", BindingFlags.Instance | BindingFlags.NonPublic);

         // If no method was found..
         if (method is null)
             return;

         // Convert reflected method to an invokable delegate
         var delegateMethod = method.CreateDelegate<Action<int>>(input);
         // Invoke
         delegateMethod(21);
     }
}

Output:

> The secret message is 42.

You might see similar examples using method.Invoke(...); however, this is not recommended because doing it through a delegate is

  • simpler
  • faster (about 50x) [2]
  • it just works

UC3: Assembly modification

Reflection can be used to modify .NET assemblies for the purposes of code obfuscation or injecting new code [3].

UC4: Assembly inspection

With reflection, you can inspect the types and their members of a given assembly. This allows you to create code analyzers, or projects like my documentation generator, MarkDoc.Core.

Summary

To summarize this small piece of the reflection world, it is crucial to quote Uncle Ben: "With great power, comes great responsibility". Reflection is cool and powerful, but at the same time, it can be used to break conventions and, in general, mess up your code. Additionally, reflection is slow, so think of ways to use it sparingly. However, this doesn't mean that you should avoid this tool at any cost because having it opens new possibilities for your projects.

Thank you for reading, and happy coding!

References

  1. SOBEL, J. M. a Daniel P. FRIEDMAN, 1996. An Introduction to Reflection-Oriented Programming [online]. 1996. Available at: citeseerx.ist.psu.edu/viewdoc/download?doi=..

  2. BONDI FABIEN, 2015. Is the use of dynamic considered a bad practice? stackoverflow [online]. [rel. 2022-07-18]. Available at: stackoverflow.com/a/32817143/7281492

  3. Anon., 2019. Inject a class with a method using dnlib [online]. [rel. 2022-07-18]. Available at: stackoverflow.com/questions/54441057/inject..

P.S.

Hey, thank you for reading my first article. Please let me know what I can improve, or what you'd like to read next!

Did you find this article valuable?

Support Denis Akopyan by becoming a sponsor. Any amount is appreciated!