Exploring the Practical Applications of Reflection in C#

Written by igorlopushko | Published 2023/05/25
Tech Story Tags: c-sharp | reflection | dotnet-core | internal-tools | c-programming | dotnet | dot-net-framework | programming-tips

TLDRReflection is a C# language feature that lets you create custom attributes and access them in your program's metadata. It can be used to create an instance of a type, bind the type to an existing object, or get the type from anexisting object and invoke its methods or access its fields and properties. Using your custom attributes can open you to a new world of how can you design your applications.via the TL;DR App

I have been on a long journey with C# for over 15 years. And for the last two years, I’ve been teaching people C# in a local IT school. There are topics that cause a lot of confusion in C#. Reflection is one of those.

According to Microsoft, reflection in C# provides objects (of type Type) that describe assemblies, modules, and types. It allows you to perform various dynamic operations, such as creating instances of types, binding types to existing objects, invoking methods, and accessing fields and properties. Reflection leverages metadata present in assemblies to enable these dynamic capabilities.

In real-life scenarios, reflection can be applied in the following cases:

  • Create custom attributes and access them in your program's metadata.
  • Serialization/deserialization.
  • Dependency Injection.

While there are dedicated libraries available for serialization, deserialization, and dependency injection in C# and the .NET platform, understanding how these libraries utilize reflection can provide valuable insights into the inner workings of the language and platform. Additionally, leveraging custom attributes through reflection can offer new possibilities for designing flexible and extensible applications.

I hope this overview provides a good starting point to dive into the world of reflection and its applications. If you have any specific questions or if there's anything else I can assist you with, please let me know!

Create custom attributes

When it comes to creating custom attributes, it's important to understand their purpose and why you might choose to use them instead of alternative approaches. Custom attributes are metadata extensions that provide additional information to the compiler about elements in the program code at runtime. By marking types, methods, properties, or other code elements with custom attributes, you can convey specific information or instructions to other code that interacts with them.

Let's consider a simple example where we want to create a custom attribute to represent a minimum value for a property. To create a custom attribute, you need to create a new class that inherits from the System.Attribute class. It is the convention to suffix the attribute class name with "Attribute."

public class MinValueAttribute : Attribute
{
    public int Value { get;}
    public MinValueAttribute(int value) => Value = value;
}

It is possible to apply the newly created attribute to a certain property of the class. Let’s create a User class with Age property which is marked with MinValueAttribute.

public class User
{
    public string Name { get; set; }

    [MinValue(18)]
    public int Age { get; set; }
}

Remember that attributes are useless until somebody uses them. It means that some piece of code has to verify if a specific attribute is applied to a particular entity and build some logic based on that. Let’s create a validator class that verifies if MinValueAttribute is applied and if the current state of the object satisfies minimum value logic.

public static class MinValueValidator
{
    public static bool Validate(User person)
    {
        var type = typeof(User);
        var properties = type.GetProperties();

        foreach (var property in properties)
        {
            var attributes = property.GetCustomAttributes(false);

            foreach (var attr in attributes)
            {
                if (attr is MinValueAttribute minValueAttribute)
                {
                    var propValue = property.GetValue(person, null);
                    if (propValue == null || 
                        int.Parse(propValue.ToString()) < minValueAttribute.Value)
                    {
                        return false;
                    }
                }
            }
        }
    
        return true;
    }
}

To get Type object of the particular class we can use 3 different approaches: with the help of typeof operator, using GetType() method of the Object class, or using the static method Type.GetType(). After that, we need to get all the properties of the type under inspection by calling the GetProperties() method. For each property, we need to call GetCustomAttributes() method and for each attribute check if it is a required one by using is operator. If it is we can call GetValue() method for the property object to get actually value of the property. Then it is possible to compare the actual property value and the Value for the MinValueAttribute.

To run the validator we can use the following code:

var user = new User() { Age = 45, Name = "John" };
Console.WriteLine("User is under 18: " + MinValueValidator.Validate(user));

It has to return User is under 18: True string.

Serialization/deserialization

Serialization converts object state into a transportable or persistent format. Deserialization reverses this process. Popular serialization formats include XML, JSON, and binary..

How can we utilize reflection for this purpose?

Remember that reflection provides Type object that contains all the information of class members and their states. That is what we need to make serialization work. Let’s create a simple XmlConvertor class and the Serialize() method inside.

public static class XmlConverter
{
    public static string Serialize(object obj)
    {
        var sb = new StringBuilder();
        var type = obj.GetType();

        sb.AppendLine("<" + type.Name + ">");

        var props = new List<PropertyInfo>(type.GetProperties());
        foreach (var prop in props)
        {
            var propValue = prop.GetValue(obj, null);
            sb.AppendLine("\t<" + prop.Name + ">" + propValue + "</" + prop.Name + ">");
        }

        sb.AppendLine("</" + type.Name + ">");

        return sb.ToString();
    }
}

All we need to do is to call GetProperties() method for the Type object and iterate through existing PropertyInfo collection to retrieve their values using GetValue() method. What is left is just to create a proper XML string. Let’s serialize the User object we described earlier.

var obj = new User { Name = "John Doe", Age = 30 };
var s = XmlConverter.Serialize(obj);
Console.WriteLine(s);

Output:

<User>
  <Name>John Doe</Name>
  <Age>30</Age>
</User>

Let’s write Deserialize() method for the opposite operation to convert XML into a C# object. It can be made generic to simplify the way we determine the type to deserialize.

public static class XmlConverter
{
    // Serialize() method is somewhere here

    public static T Deserialize<T>(string s)
    {
        // instantiate the object to be deserialized
        var assembly = typeof(T).Assembly;
        var obj = assembly.CreateInstance(typeof(T).FullName);
        if (obj == null)
        {
            throw new Exception($"Can't instantiate a new object of type '{typeof(T).FullName}'");
        }
        
        // parse input string int XML document
        var doc = XDocument.Parse(s);
        if (doc.Root == null)
        {
            throw new ArgumentException("Can't parse specified xml string");
        }

        // deserialize class members states
        foreach (var node in doc.Root.Elements())
        {
            var property = 
                obj.GetType()
                   .GetProperty(node.Name.LocalName, BindingFlags.Public | BindingFlags.Instance);
            if (null != property && property.CanWrite)
            {
                if (typeof(int) == property.PropertyType)
                {
                    property.SetValue(obj, int.Parse(node.Value));
                } 
                else if (typeof(string) == property.PropertyType)
                {
                    property.SetValue(obj, node.Value);    
                }
                // TODO: add other types conversion
            }
        }
        
        return (T)obj;
    }
}

First of all, we need to get the Assembly object from the type we want to deserialize. It provides CreateInstance() method to create a new object from the specified type. Then we have to parse XML and iterate over the all elements. Since we have a very simple XML structure we just need to iterate over the internal nodes and use GetProperty() method to find a corresponding property of the deserialized object. After that, we can call SetValue() method to set the actual value. The section with setting properties value could be quite bulky so I just wrote an example for the int and string types.

Let’s verify the Deserialize() method.

var stringToDecode = "<User><Name>John Doe</Name><Age>30</Age></User>";
var decodedUser = XmlConverter.Deserialize<User>(stringToDecode);
Console.WriteLine("User name: " + decodedUser.Name);
Console.WriteLine("User age: " + decodedUser.Age);

It will bring the following output:

User name: John Doe
User age: 30

Dependency Injection

Dependency Injection (DI) is a design pattern that encourages loose coupling between classes. It enables the passing of dependencies into a class from external sources, rather than having the class handle its dependencies internally. This approach enhances class modularity, testability, and maintainability.

To understand the Dependency Injection pattern we need to see the participants it comprises of.

  • Client Class: The client class (dependent class) is a class that depends on the service class
  • Service Class: The service class (dependency) is a class that provides service to the client class.
  • Injector Class: The injector class injects the service class object into the client class.

The following figure illustrates the relationship between these classes:

The IoC container creates an object of the specified class and also injects all the dependency objects through a constructor, a property, or a method at run time and disposes it at the appropriate time. There is a number of IoC container libraries implemented for the C#. But let’s create our one DI container to see how it utilizes reflection for its needs.

We are going to introduce Client and Service classes, and the dependency between them through the interface. It is a good practice to use interfaces as a contract to interact between two objects to follow the Dependency Inversion principle.

There is an interface for the service with the Process() method.

public interface IService
{
    void Process();
}

The implementation of the IService interface is very simple just to do some action inside the method.

public class Service : IService
{
    public void Process()
    {
        System.Console.WriteLine("Call Process() method");
    }
}

The client code uses IService as an abstraction between the Service and the Client.

public class Client
{
    private IService _service;
    
    public Client(IService service)
    {
        _service = service;
    }

    public void RunProcess()
    {
        _service.Process();
    }
}

Let’s create a simple Container class and implement Dependency Injection mechanisms.

public class Container
{
    private Dictionary<Type, Type> _containerData = new Dictionary<Type, Type>();

    public void Register<TType, TImplementation>()
    {
        _containerData.Add(typeof(TType), typeof(TImplementation));
    }
}

Inside the container, we need a dictionary _containerData that contains the type we want to register as a key and the type that represents the implementation as a value. Register<TType, TImplementation>() is a generic method that accepts two types, the type to be registered and the implementation for it.

public class Container
{
    // ...
        
    public TType GetInstance<TType>()
    {
        var type = typeof(TType);
        if (type.GetConstructors().Length > 1)
            throw new Exception($"The type '{type.FullName}' should implement only one constructor");
        
        ConstructorInfo ctor = type.GetConstructors()[0];
        ParameterInfo[] parameters = ctor.GetParameters();
        object[] constructorArgs = new object[parameters.Length];
        
        for (int i = 0; i < parameters.Length; i++)
        {
            var parameterType = parameters[i].ParameterType;

            if (!_containerData.ContainsKey(parameterType))
                throw new Exception($"Can't find implementation for the type '{parameterType.FullName}'");

            var obj = Activator.CreateInstance(_containerData[parameterType]);
            if (obj != null)
                constructorArgs[i] = obj;
        }
        
        return (TType)ctor.Invoke(constructorArgs);
    }
}

To provide instances of the particular type we can write a new GetInstance<TType>() method. Inside this method, we need to find the ConstructorInfo object in the type we want to build and get an array of ParameterInfo objects to know which parameters path into the constructor. After that, we could iterate over the array of parameters and instantiate each of them adding them to an array of objects constructorArgs variable. When all the parameters are instantiated we can call Invoke() method of the constructor and path just created parameters.

To verify the functionality of the DI container we can utilize the following code:

var container = new Container();
container.Register<IService, Service>();

var client = container.GetInstance<Client>();
client.RunProcess();

You should get the following output:

Call Process() method

Summary

We have only touched the surface of its power, but it can take you a long way in comprehending how widely used libraries operate and how reflection is utilized in everyday tasks. Many C# developers either do not utilize reflection at all or lack understanding of its potential applications. Feel free to delve into the documentation to familiarize yourself with the full range of capabilities offered by reflection.


Written by igorlopushko | Programmer, Architect, Teacher
Published by HackerNoon on 2023/05/25