DEV Community

Reflection in C#: What It Is, Why It Exists

What Is Reflection?

Reflection lets code inspect and interact with types, methods, properties, and assemblies at runtime - even ones it didn't know about at compile time. It lives in the System.Reflection namespace and has been part of .NET since .NET Framework 1.0 in 2002.

Type type = someObject.GetType();
PropertyInfo[] properties = type.GetProperties();
object instance = Activator.CreateInstance(type);

That's the mechanism. The value of reflection only becomes clear once we see the alternative.

The Problem: Life Without Reflection

Imagine someone building a system that sends different kinds of emails - order confirmations, password resets, shipping updates - each with its own data merged into a template. Without reflection, developers need to write a separate merge method for every email type, manually pulling out each field:

public class OrderConfirmationData
{
    public string CustomerName { get; set; }
    public string OrderNumber { get; set; }
    public decimal Total { get; set; }
}

public static string BuildOrderConfirmationEmail(string template, OrderConfirmationData data)
{
    return template
        .Replace("{{CustomerName}}", data.CustomerName)
        .Replace("{{OrderNumber}}", data.OrderNumber)
        .Replace("{{Total}}", data.Total.ToString("C"));
}

public static string BuildPasswordResetEmail(string template, PasswordResetData data)
{
    return template
        .Replace("{{UserName}}", data.UserName)
        .Replace("{{ResetLink}}", data.ResetLink);
}

// ...and one more method for every new email type, forever

This has three predictable failure modes:

  • Duplication - the same merge pattern gets copy-pasted for every model.
  • It doesn't scale - add ShippingUpdateData, InvoiceData, WelcomeEmailData, and write three more methods.
  • Silent bugs - add a new field like EstimatedDelivery to a model and forget to add its .Replace() call. Nothing fails at compile time; the placeholder just never gets filled in.

The Fix: A Reflection-Based Template Engine

Instead of hardcoding which fields exist, the engine asks each object what properties it has and merges them generically:

public static class TemplateEngine
{
    public static string Render(string template, object data)
    {
        Type type = data.GetType();
        foreach (PropertyInfo prop in type.GetProperties())
        {
            string placeholder = "{{" + prop.Name + "}}";
            object value = prop.GetValue(data);
            template = template.Replace(placeholder, FormatValue(value));
        }
        return template;
    }

    private static string FormatValue(object value) => value switch
    {
        null => "",
        decimal d => d.ToString("C"),
        DateTime dt => dt.ToString("MMMM dd, yyyy"),
        _ => value.ToString()
    };
}

Usage is now completely generic - one method handles every email type ever created:

var order = new OrderConfirmationData
{
    CustomerName = "Sarah",
    OrderNumber = "ORD-4471",
    Total = 89.99m
};

string template = "Hi {{CustomerName}}, your order {{OrderNumber}} totaling {{Total}} has shipped!";
string email = TemplateEngine.Render(template, order);
// "Hi Sarah, your order ORD-4471 totaling $89.99 has shipped!"

var reset = new PasswordResetData
{
    UserName = "sarah_k",
    ResetLink = "https://..."
};

string resetEmail = TemplateEngine.Render("Hi {{UserName}}, click {{ResetLink}} to reset.", reset);

What this actually fixed:

  • One merge engine instead of N methods. Render() never changes as new email types are added.
  • Adding a field to an email becomes a one-line change: add the property to the model, add the placeholder to the template. No merge code to remember.

This is essentially how real templating libraries like RazorLight and Scriban work under the hood: bind a plain object's properties to placeholders without the library ever needing to know specific classes in advance.

Advanced Feature: Attribute-Driven Formatting

Once the basic engine works, attributes push formatting rules into the model instead of hardcoding them in the engine - keeping Render() generic while still allowing per-field customization:

[AttributeUsage(AttributeTargets.Property)]
public class DateFormatAttribute : Attribute
{
    public string Format { get; }
    public DateFormatAttribute(string format) => Format = format;
}

public class ShippingUpdateData
{
    public string CustomerName { get; set; }

    [DateFormat("dddd, MMMM d")]
    public DateTime EstimatedDelivery { get; set; }
}

Inside Render(), a quick check against the attribute decides how to format that specific field:

var dateFormat = prop.GetCustomAttribute<DateFormatAttribute>();
string formatted = dateFormat != null && value is DateTime dt
    ? dt.ToString(dateFormat.Format)
    : FormatValue(value);

This is the same pattern used by [Required] in ASP.NET Core's DataAnnotations validation, or [JsonPropertyName] in System.Text.Json - metadata lives next to the data it describes, and generic engines read it at runtime instead of needing per-type special-casing.

Other Places Reflection Does Real Work

The template engine isn't a one-off case - it's an instance of a general pattern: code that would otherwise need to know about every type in advance gets replaced by code that discovers what it needs at runtime. A few other production-grade examples:

Dependency Injection Containers - Instead of manually writing new OrderController(new OrderService(new OrderRepository(new DbConnection(...)))), a DI container reads each class's constructor via GetConstructors() and GetParameters(), then recursively resolves and builds the whole graph. This is exactly what builder.Services.AddScoped<IOrderRepository, OrderRepository>() does under the hood in ASP.NET Core.

Validation Frameworks - DataAnnotations and FluentValidation scan a model's properties for attributes like [Required] or [Range] and apply the corresponding rule - one generic validator instead of a hand-written method per class.

Serialization - System.Text.Json and Newtonsoft.Json walk an object's properties via reflection to produce JSON, and walk a JSON document's structure to populate an object - without ever needing hardcoded knowledge of the classes.

Test Runners - xUnit and NUnit scan the compiled assembly for methods tagged [Fact] or [Test] rather than maintaining a manual list of test methods.

ORMs - Entity Framework maps class properties to database columns by reflecting over the model classes, matching property names (or [Column] attributes) to schema columns.

Reflection Is Slow

Reflection is slower than direct method calls, but the difference only matters when executed thousands or millions of times. Frameworks like ASP.NET Core, Entity Framework, and System.Text.Json all use reflection where appropriate, then cache metadata so the cost is paid once rather than on every operation.

Takeaway

Reflection is powerful, but it isn't a replacement for normal object-oriented code. If types are known at compile time, direct property access is simpler, safer, and faster. Reflection shines while building reusable infrastructure that must work with types it doesn't know in advance.

Comments

No comments yet. Start the discussion.