4 real-life examples of using reflection in C#

As C# developers, we sometimes must interact dynamically with objects, fields, properties, methods, or types. Use cases such as inter-mapping between types, traversing model properties, developing an extensible system, injecting dependencies, etc., demand runtime handling. You may wonder how to tackle such scenarios. In this post, I will show you how reflection can allow you to inspect and control object metadata at runtime. We'll look closer at what reflection is, how it can be implemented, how it operates under the hood, real-world use cases for automating design patterns, and tips to use it efficiently.

4 real-life examples of using reflection in C#

What is Reflection in C#?

Reflection in C# is a runtime feature that dynamically enables the program to inspect and interact with the metadata of assemblies and types like classes, interfaces, value types, methods, fields, and properties. The System.Reflection namespace facilitates Reflection functionalities and is often used with System.Type for metaprogramming. With its introspective capability, you can invoke methods, create instances of types, and load assemblies, knowing them at compile time. 

Why use Reflection in C#?

After the introduction, the main question arises. Why is reflection required in my project, and where do I use it? 

Reflection is ideal in scenarios where static typing or compile-time constructs fall short, and you need to rely on the runtime inputs. The following are a few scenarios where you could reduce complexity with this feature.

  • Your application needs to load assemblies or types at runtime based on external input.
  • When JSON or XML serializers don't know the structure of objects and are meant to deal with them on the fly,
  • Automatic type mappings to copy data between properties with matching names or attributes.
  • Developing a dependency injection (DI) container to resolve constructors, inject parameters, and activate services.
  • Exporting data into CSV with so many types and their hard-coded property mapping is impractical.
  • Testing tools like NUnit or xUnit use Reflection to discover test methods and attributes without manually registering each test case.
  • Avoid boilerplate code for writing repetitive type methods and properties. Reflection can help automate property access or method invocation.
  • Accessing and validating model properties dynamically.
  • Mapping class properties to database columns and configuring models dynamically in ORMs (Object-Relational Mappers), where the model can be customized and contains arbitrary properties.

Many built-in features of the System namespace and other libraries utilize reflection under the hood. .NET dependency injection and Entity Framework Core are prominent examples.

While reflection powers many advanced features like DI containers and serializers, avoid reimplementing these unless you have a strong reason. Prefer well-tested frameworks like System.Text.Json, AutoMapper, or built-in .NET DI. The examples shown later in this post are for illustration purposes only.

Scenario 1: Basic method invocation

Let's start simple and use reflection to create a new object and call a method using reflection:

using System;
using System.Reflection;

class ClassA
{
    public void Say()
    {
        Console.WriteLine("Hello from Reflection!");
    }
}

class Program
{
    static void Main()
    {
        Type type = typeof(ClassA);
        Console.WriteLine("Class Name: " + type.Name);
        object instance = Activator.CreateInstance(type);
        MethodInfo method = type.GetMethod("Say");
        method.Invoke(instance, null);
    }
}
Output

We created a simple class with a method. Reflection does the following here.

  • Get the type information of a class (ClassA) using the typeof keyword.
  • Create an instance of that class at runtime using: Activator.CreateInstance(type).
  • Find a specific method (Say) as MethodInfo with GetMethod.
  • Invoke that method without directly calling it in code.

Scenario 2: Dynamic CSV exporter

For the second example, we create a method for exporting a list of objects to CSV, where the type is unknown at compile time:

public static class CsvExporter
{
    public static string ExportToCsv<T>(List<T> data)
    {
        if (data == null || data.Count == 0) return string.Empty;
        var type = typeof(T);
        var properties = type.GetProperties();
        var sb = new StringBuilder();

        // Header
        sb.AppendLine(string.Join(",", properties.Select(p => p.Name)));

        // Rows
        foreach (var item in data)
        {
            var values = properties.Select(p =>
            {
                var val = p.GetValue(item, null);
                return val?.ToString()?.Replace(",", ";") ?? "";
            });
            
            sb.AppendLine(string.Join(",", values));
        }

        return sb.ToString();
    }
}

The CSV exporter exports data of any type by reading properties using reflection at type.GetProperties(). The example creates a column per property automatically.

Scenario 3: Mapping models with a custom mapper

Code often needs to map properties from one type to another. Almost like an Automapper-like feature, we can implement this using reflection:

public static void MapProperties<TSource, TTarget>(TSource source, TTarget target)
{
    var sourceProps = typeof(TSource).GetProperties();
    var targetProps = typeof(TTarget).GetProperties();
    foreach (var srcProp in sourceProps)
    {
        var targetProp = targetProps.FirstOrDefault(p =>
            p.Name == srcProp.Name
            && p.PropertyType == srcProp.PropertyType);
        if (targetProp != null && targetProp.CanWrite)
        {
            var value = srcProp.GetValue(source);
            targetProp.SetValue(target, value);
        }
    }
}

The code can be used to map domain models to a DTO or vice versa without writing separate mapping logic for each type.

Scenario 4: Dynamic Plugin/assembly loading

In some systems, it makes sense to implement an extension system or in other ways be able to enrich an application at runtime by including additional assemblies. This can be done using reflection too:

var asm = Assembly.LoadFrom("MyPlugin.dll");
var type = asm.GetType("MyPlugin.MyClass");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("Execute");
method.Invoke(instance, null);

The snippet loads an assembly at runtime using the Assembly.LoadFrom method, creates an instance of a type by its name, and calls a method on the new object.

Performance considerations and best practices

Reflection is a powerful feature that automates many functionalities. However, it brings performance overhead to the code. You should follow best practices to use it properly.

  • Avoid using reflection in tight loops. It may slow down the execution since methods like GetType can potentially be called thousands of times.
// BAD!

var items = Enumerable.Range(0, 10000)
    .Select(i => new Sample { Name = "Item" + i })
    .ToList();

foreach (var item in items)
{
    var prop = item.GetType().GetProperty("Name");
    var value = prop.GetValue(item);
}

// BETTER!

var items = Enumerable.Range(0, 10000)
    .Select(i => new Sample { Name = "Item" + i })
    .ToList();

var type = typeof(Sample);
var prop = type.GetProperty("Name");

foreach (var item in items)
{
    var value = prop.GetValue(item);
}
  • Do not use reflection in performance-critical code. Instead, rely on built-in methods and pre-compiled delegates. For instance, DI with reflection offers you more control and access, but the built-in DI resolution of .NET is well-optimized and tested.
  • Calling GetMethod() and GetProperty() repeatedly can be expensive. Use a cache for reflection results:
Dictionary<string, MethodInfo> methodCache = new();
MethodInfo GetCachedMethod(Type type, string methodName)
{
    if (!methodCache.ContainsKey(methodName))
        methodCache[methodName] = type.GetMethod(methodName);

    return methodCache[methodName];
}
  • Use compiled delegates or expression trees (Expression<Func<T, object>>) to avoid repeated reflection. This can significantly speed up property access or method invocation in performance-critical paths:
public static Func<T, object> CreateGetter<T>(PropertyInfo prop)
{
    var instance = Expression.Parameter(typeof(T), "instance");
    var propertyAccess = Expression.Property(Expression.Convert(instance, prop.DeclaringType), prop);
    var convert = Expression.Convert(propertyAccess, typeof(object));
    return Expression.Lambda<Func<T, object>>(convert, instance).Compile();
}

Conclusion

In C# development, we often face scenarios requiring runtime decisions. Either reading the metadata of types or executing methods at runtime, traditional ways may not always be practical or possible. C# Reflection is a powerful tool that offers a way to cope with such scenarios. Reflection provides access to type and assembly metadata and enables the invocation of their methods without hardcoding them. We discussed several use cases where it is handy and eliminates boilerplate code. Besides its usefulness, reflection costs in performance if not handled properly.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website