Skip to main content

Reflection

In many cases, it's not enough to use types only at compile-time; we need to extract information at runtime about data types, functions, and other code-related details. Languages with runtime capabilities, such as C# and Java, have gained popularity due to exposing this information at runtime through reflection mechanisms. Reflection essentially means that a program can introspect and acquire information about the executing code.

The simplest example of why C# and Java have become popular in cloud systems is the fact that, with information about how a class is structured, objects can be automatically serialized and deserialized into various formats like JSON or XML without explicitly writing corresponding methods for each class.

In C#, there are many methods for extracting information about a type, primarily through the Type structure, which can be obtained using the GetType method of any instance or the typeof keyword on a type. Through the Type, you can extract:

  • Information about methods/fields/properties/constructors
  • Inheritance hierarchy of the type
  • Whether it's a class, struct, abstract class, or interface
  • If it's a generic type
  • Generic type parameters, if applicable
  • Attributes attached to the class

Several examples of how we can use reflection can be seen below.

var interfaceType = typeof(TInterface);
var implementationType = typeof(TImplementation);

// We can check if a type is an interface.
if (!interfaceType.IsInterface)
{
throw new ArgumentException($"{interfaceType.Name} is not an interface!");
}

// Or it's a class.
if (!implementationType.IsClass)
{
throw new ArgumentException($"{implementationType.Name} is not a concrete class!");
}
public class TestClass
{
public TestClass(int intArg)
{
// ...
}

public TestClass(string stringArg)
{
// ...
}
}

// For each implementation type, extract constructors to invoke one.
var constructors = typeof(TestClass).GetConstructors();

foreach (var constructor in constructors)
{
Console.Write("Printing constructor arguments: ");

// Then, extract information about constructor parameters.
foreach (var parameter in constructor.GetParameters())
{
Console.Write("{0} {1}, ", parameter.ParameterType.Name, parameter.Name);
}

Console.WriteLine();

if (constructor.GetParameters().Length == 1 && constructor.GetParameters()[0].ParameterType == typeof(string))
{
// We can invoke a specific constructor and get an instance.
var instance = constructor.Invoke(new object[] { "Test" });

Console.WriteLine("Created an instance of {0}: {1}", typeof(TestClass).Name, instance);
}
}

In addition to the general information we have for a class, we can further assign additional information to it through attributes if what the class and inheritance chain offer is insufficient. An attribute in C# is a class that inherits from Attribute and has a name ending with "Attribute". It can be attached within [] to a class, method, property, field, or method parameter.

/* To use attributes, we need to inherit from Attribute
* and add an attribute to indicate where this attribute can be used.
* In this case, it's used on a class.
*/
[AttributeUsage(AttributeTargets.Class)]
public class InjectableAttribute : Attribute
{
public LifetimeEnum Lifetime { get; }
public InjectableAttribute(LifetimeEnum lifetime) => Lifetime = lifetime;
}

// The attribute is attached to the class, and data can be passed to its constructor.
[Injectable(LifetimeEnum.Transient)]
public class InjectedService
{
// ...
}

// Here, we extract all existing types from the executing assembly.
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
{
// For custom non-standard attributes, we can extract a list of attributes of that type.
var attribute = type.GetCustomAttributes(typeof(InjectableAttribute)).FirstOrDefault();

if (attribute is InjectableAttribute injectableAttribute)
{
Console.WriteLine("Type {0} has attribute {1} with lifetime {2}.", type.Name, typeof(InjectableAttribute).Name, injectableAttribute.Lifetime);
}
}

Attributes are necessary in many applications to serve as markers for various purposes, containing useful information that can be attached via the attribute's constructor. Furthermore, attributes, or their equivalents in other languages such as annotations in Java, are used in attribute-oriented programming (@OP). The role of @OP is to facilitate adding logic over code without modifying it directly. For instance, if we want to automatically log method calls for certain classes, we can use logic where we simply decorate the class with an attribute to log each call without modifying the methods themselves.

caution

Reflection imposes runtime overhead, and due to performance reasons, excessive use of these mechanisms should be avoided. Additionally, many commonly used mechanisms in larger applications, such as dependency injection, are already implemented and optimized. It's advisable to use those built-in solutions instead of reinventing the wheel.

📄️ Dependency Injection

For modern applications, various methods have been created to simplify the implementation of an application from scratch. Even though there are many libraries that facilitate writing applications for different use cases, the challenge lies in application construction and control. In procedural programming, when writing code, it calls other code that can reside in libraries or be exposed by a framework, dictating both the program's control flow and its construction. This approach is problematic as it imposes on the program the responsibility of determining the control flow of different components based on their implementation details, rather than using an abstraction to make the code more reusable and easier to follow.