DEV Community

Evgeny Vashchenko
Evgeny Vashchenko

Posted on • Updated on

.NET Reflection. How to simplify work and use in DDT testing.

In this post, I want to talk about the Reflector class, which was written to make it easier to work with objects and methods provided by the .NET Reflection API from the System.Reflection namespace. Reflection allows you to perform tasks of examining assemblies, types, interfaces, methods with their parameters, fields, properties, and events by obtaining information that describes their structure. This information is stored in the assembly's metadata and can be retrieved using reflection API objects and methods. It can be required either simply to obtain metadata about the objects of interest, or to generate the code used by them at the time of application operation, either through the Reflection API itself from the System.Reflection.Emit namespace, or through the LINQ Expressions API from the System.Linq.Expressions namespace. In this article, we will not touch on code generation, but will consider another possibility that reflection provides - this is access to members of types or members of their instances, calling their methods and raising events. It should be noted here that accessing class members and calling methods through the reflection mechanism has both disadvantages and advantages. One advantage is the ability to examine the internal structure of a type and gain access to both its public and nonpublic members. You can modify private fields, set properties, and call methods. But there is also a security flaw here - without an accurate understanding of the processes that occur when the internal state of an object changes, you can bring it into an inoperable state. The main disadvantage of the reflection mechanism is its low speed, so it is used where speed is not a critical factor, for example, testing or where late binding is used and/or information about the structure of type is required (plugins, serializers, duck typing interfaces and others). I especially want to touch on unit testing, it was it that inspired me to finish the class once made for academic purposes. I needed to write unit tests for many methods, each of which had to be run with several sets of parameters. Therefore, a methodology called DDT (Data Driven Testing) was used and which consists in the fact that the input of the test method is given the values of the input parameters and the reference (expected) result, with which they obtained (actual) value is compared as a result of the execution of the test method. actions on input parameters. Most methods in test frameworks operate on values cast to type object. The reflection API also operates on values cast to the object type, so all conversions and type checking fall on the appropriate frameworks. We actually need to find the desired method corresponding to the input parameters. For example, now it is possible to write a type or its instance, the name of the method defined in it and some of its attributes, an array of parameter values and the expected result or an array of results (out parameters are also supported) into the data array for testing. Inside the test method, in the simplest case, you will need to call the Reflector.CallMethod method with the passed parameters, get the actual result and compare it with the expected one. Those no type member descriptors need to be obtained. Below is a fragment of test methods for validating string arguments of the Argument validator class from the same library.

public static IEnumerable<object?[]> StringSuccessfulData
  => new object?[][]
  {
    new object?[] { nameof(Argument.NotNullOrEmpty), new object?[] { "X" } },
    new object?[] { nameof(Argument.NotNullOrEmpty), new object?[] { "ABC" } },
    new object?[] { nameof(Argument.NotNullOrWhitespace), new object?[] { "X" } },
    new object?[] { nameof(Argument.NotNullOrWhitespace), new object?[] { " Y " } },
    new object?[] { nameof(Argument.Empty), new object?[] { "" } },
    new object?[] { nameof(Argument.NotEmpty), new object?[] { " " } },
    new object?[] { nameof(Argument.NotEmpty), new object?[] { "A" } },
    new object?[] { nameof(Argument.Whitespace), new object?[] { "" } },
    new object?[] { nameof(Argument.Whitespace), new object?[] { "  " } },
    new object?[] { nameof(Argument.NotWhitespace), new object?[] { "  0" } },
    new object?[] { nameof(Argument.Contains), new object?[] { AlphaNumericSymbols, "456", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.Contains), new object?[] { AlphaNumericSymbols, "FgHiJ", CultureInfo.CurrentCulture, CompareOptions.IgnoreCase } },
    new object?[] { nameof(Argument.NotContains), new object?[] { AlphaNumericSymbols, "465", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.NotContains), new object?[] { AlphaNumericSymbols, "FgHiJ", CultureInfo.CurrentCulture, CompareOptions.None } },
    new object?[] { nameof(Argument.StartsWith), new object?[] { AlphaNumericSymbols, " 0123456789ABC", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.StartsWith), new object?[] { AlphaNumericSymbols, " 0123456789abc", CultureInfo.CurrentCulture, CompareOptions.IgnoreCase } },
    new object?[] { nameof(Argument.NotStartsWith), new object?[] { AlphaNumericSymbols, "0123456789", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.NotStartsWith), new object?[] { AlphaNumericSymbols, " 0123456789abc", CultureInfo.CurrentCulture, CompareOptions.None } },
    new object?[] { nameof(Argument.EndsWith), new object?[] { AlphaNumericSymbols, "MNOPQRSTUVWXYZ", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.EndsWith), new object?[] { AlphaNumericSymbols, "PQRStUVwXyZ", CultureInfo.CurrentCulture, CompareOptions.IgnoreCase } },
    new object?[] { nameof(Argument.NotEndsWith), new object?[] { AlphaNumericSymbols, "STUVXYZ", TypedValue.DefaultOf<CultureInfo>() } },
    new object?[] { nameof(Argument.NotEndsWith), new object?[] { AlphaNumericSymbols, "PQRStUVwXyZ", CultureInfo.CurrentCulture, CompareOptions.None } },
    new object?[] { nameof(Argument.Match), new object?[] { RegexSymbols, @"^\(\w+[:](?:\s+\w+)+\)$", RegexOptions.None } },
    new object?[] { nameof(Argument.NotMatch), new object?[] { RegexSymbols, @"^\(\w+[-](?:\s+\w+)+\)$", RegexOptions.None } },
  };

[Theory]
[MemberData(nameof(StringSuccessfulData))]
public void StringValidationSuccessful(object name, object parameters)
{
  var methodName = Argument.That.InstanceOf<string>(name);
  var positionalParams = Argument.That.InstanceOf<IList<object?>>(parameters);
  var result = Reflector.CallMethod(Argument.That, methodName, MemberAccessibility.Public, null, positionalParams, null, null);
  Assert.Equal(positionalParams[0], result);
}
Enter fullscreen mode Exit fullscreen mode

A simple scenario for accessing type members through the standard API is to obtain meta information about the desired type member or instance by calling a method that returns the required metadata. Usually, as parameters, we need to know the type in which the type member we are interested in is defined, its name, access type (public or non-public), scope (static or instance) and its value type (for fields and simple properties) or signature (for methods, indexer properties, and constructors). These query parameters are passed to the reflection API functions, which return the single type member found, or the collection of type members found according to the specified condition. Returned members of types can be defined both in the type itself and in its base classes, which again depends on the specified conditions.

Let's make a small digression and agree on the following terminology. We will call generic types with types passed as parameters, for example, GenType<TA, TB>, where TA and TB will be arguments of the generic type GenType. By the same principle, we will call methods universal, for example, GenMethod<TX, TY>(TX x, TY y). Generic types and methods with unspecified type arguments are called generic type and method definition, respectively. Generic types and methods are called opened if not all type arguments are specified. Otherwise, they are called closed.

Let's consider a simple example of setting a new value for a private static class field and returning its previous value. Unlike the usual code, here we get the handle of this static field declared in class A in the first line, and then we call the appropriate methods for getting and setting the value of this field. The second method shows that the functionality of the Reflector class can do all these things for you. It should be noted that here, for simplicity, checks for the null value of the parameter are omitted, which is not allowed, because unable to retrieve object type. To do this, in the above class, an empty value of a specific type can be passed through an instance of the TypedValue class, or you can use a method with a generic value type parameter. In the case of passing to the method a value with a mismatched field type int, an exception will be generated about the absence of the specified field in the type.

public class A
{
  private static int value = 100;
}

internal static class Example_1
{
  internal static object ReplaceValue(object newValue)
  {
    var fieldInfo = typeof(A).GetField("value", BindingFlags.Static | BindingFlags.NonPublic);
    var oldValue = fieldInfo.GetValue(null);
    fieldInfo.SetValue(null, newValue);
    return oldValue;
  }

  internal static object ReplaceValue2(object newValue)
  {
    return Reflector.ReplaceFieldValue<A>("value", MemberAccessibility.Private, newValue);
  }
}
Enter fullscreen mode Exit fullscreen mode

Consider another example with a generic type, where the type of the field is the argument of the class in which it is declared. Each closed generic type with a unique set of its arguments will have its own instance of a static member of the type. Let the value cast to the object type be passed to the method that sets the value of this field. Therefore, we must determine the type of this value, obtain a generic type definition, from which, using the type of the passed field value as a parameter, construct a closed generic type. Here is also a second method that performs the same action, but using the Reflector class.

public class A<T>
{
  private static T value;
}

internal static class Example_2
{
  internal static object ReplaceValue(object newValue)
  {
    var argType = newValue.GetType();
    var classType = typeof(A<>).MakeGenericType(argType);
    var fieldInfo = classType.GetField("value", BindingFlags.Static | BindingFlags.NonPublic);
    var oldValue = fieldInfo.GetValue(null);
    fieldInfo.SetValue(null, newValue);
    return oldValue;
  }

  internal static object ReplaceValue2(object newValue)
  {
    return Reflector.ReplaceFieldValue(typeof(A<>), "value", MemberAccessibility.Private, null, newValue);
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, consider calling a generic method of a generic class. This code is already more complex, although it has only two generic arguments - one for the type, the other for the method. The listing below shows two methods that call the same Replace method of the B<T> class. The first method contains the code that must be written in the usual way to perform the above action. The second method uses the functionality of the Reflector class.

public class B<T>
{
  internal static T Replace<L>(L list, int index, T newValue)
    where L : IList<T>
  {
    var oldValue = list[index];
    list[index] = newValue;
    return oldValue;
  }  
}

internal static class Example_3
{
  internal static object ReplaceListItem(object list, int index, object newItem)
  {
    var argType = newItem.GetType();
    var listType = list.GetType();
    var listIFace = typeof(IList<>).MakeGenericType(argType);
    var classType = typeof(B<>).MakeGenericType(argType);
    var methodDef = classType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
      .Single(methodInfo =>
      {
        if (methodInfo.Name != "Replace")
          return false;
        var arguments = methodInfo.GetGenericArguments();
        var parameters = methodInfo.GetParameters();
        return arguments.Length == 1 && parameters.Length == 3
          && parameters[0].ParameterType == arguments[0]
          && parameters[1].ParameterType == typeof(int)
          && parameters[2].ParameterType == argType;
      });
    var methodRes = methodDef.MakeGenericMethod(listType);
    return methodRes.Invoke(null, new object[] { list, index, newItem });
  }

  internal static object ReplaceListItem2(object list, int index, object newItem)
  {
    return Reflector.CallMethod(typeof(B<>), "Replace", MemberAccessibility.Assembly, null, null, new[] { list, index, newItem }, null, null);
  }
}
Enter fullscreen mode Exit fullscreen mode

And if there are many such methods, and they will have a larger number of parameters?! Therefore, the idea arose to simplify such calls and automate the selection of appropriate type members based on the values of the input parameters and the expected types of return values.

The Reflector class allows you to retrieve handles to fields, properties, methods, constructors, and events with valued generic type and method arguments based on the parameter types and expected return types. It also allows you to access the values of such members of the type as fields and properties for reading and writing, to call both synchronous and asynchronous methods, constructors and events.

In all methods of working with reflection objects of the Reflector class, either an instance of an object of the required type for working with its instance members, or the type itself for working with its static members can be specified as the first parameter. Also, the type can be specified in the generic parameters of methods and omitted in the normal parameters. The other parameter is the name of the member, which is omitted only for constructor members. Another common parameter is the value of the MemberAccessibility enum flags:

  • None - no flags;
  • IgnoreCase – flag to ignore case when comparing names of type members and method parameters;
  • DeclaredOnly - a flag that allows only instance members defined on the specified type. In the absence of this flag, instance members of the entire inheritance hierarchy are allowed;
  • FlattenHierarchy is a flag that allows static members of the entire type hierarchy. In the absence of this flag, static members defined only in the specified type are allowed;
  • TopHierarchy - indicates that if several matching type members are found at different inheritance levels, the one that is higher than others in the inheritance list will be taken. If this flag is not present and multiple matching methods are found at different inheritance levels or at the same inheritance level, regardless of the presence of this flag, a type member ambiguity exception will be thrown.
  • Family - indicates that members of the type with access level Family (protected in C#) will be allowed;
  • Assembly - indicates that members of the type with the Assembly access level (internal in C #) will be allowed;
  • FamilyOrAssembly - indicates that members of the type with access level Family or Assembly (internal protected in C #) will be allowed;
  • FamilyAndAssembly - indicates that members of the type with access level Family and Assembly (private protected in C #) will be allowed;
  • Private - specifies that members of the type with access level Private (private in C#) will be allowed;
  • Public - indicates that members of the type with access level Public (public in C #) will be allowed;
  • NonPublic - a combination of all flags with a non-public access level;
  • AnyAccess - a combination of all access level flags;
  • MemberAccessMask - the mask for all access level flags.

Two arrays of generic type and method arguments can optionally follow. The first array of generic type arguments is passed in the typeArguments parameter, the second array of generic method arguments is passed in the methodArguments parameter. The following rules for interpreting these parameters apply. If a private generic type is specified to locate the member to look up, then this parameter does not need to be specified. If this parameter is specified, then a check will be made for equality of the dimensions of the number of arguments of the generic type and the specified array. Further, each generic type argument will be compared with the array element in the corresponding position, but only if the array element is specified, i.e. does not contain a null value. Thus, with a long array of arguments, we can impose an arity constraint on the arguments of a generic type or method, although the arguments themselves may not be specified. We can also specify a generic type definition to locate the member we are looking for. When specifying an array of arguments in this case, the reflection functionality of the Reflector class will try to signify the definition of a generic type with the specified arguments. If some argument is not set or the array of arguments itself, then the functionality will try to display it based on the use, for example, from the type of the field value, property or method parameters. If the arguments of the generic type definition are successfully evaluated, a closed generic type will be created, in which the required member will already be discovered.

The above rules can also be applied to generic method arguments. If an array of argument types is specified, then parameters with the types of these arguments will be checked for compliance, but if the arguments are not specified or partially specified, then they will be valued by use. Appropriate generic methods must have a number of arguments equal to the length of the array of arguments passed in if one is specified, although the array elements themselves may not be specified, leaving the responsibility for their definition to the functionality of the Reflector class.

It should be noted here that passing an empty array of arguments clearly indicates that the type or method should not be generic.

For parameterized members, and these are properties with indexers, methods, and constructors, parameter types (for member descriptor retrieval functions) or parameter values (for member accessor functions) are specified. Parameters can be passed both positional in the list and named in the dictionary (both are allowed). Positional parameters start at index zero and each value will match the member parameter at the corresponding position. Optional parameters with default values may not be specified, in which case they are initialized with the default value specified in the parameter. Named parameters are matched by their names according to the IgnoreCase flag from the MemberAccessibility enum type described above, i.e. with or without case sensitivity. After calling methods that have parameters with a value passed by or returning by reference, the positions of the list or dictionary corresponding to these parameters will contain the values changed or returned by the called method. When comparing the types of passed parameters, the variance rule is applied, i.e. the value type passed to the method must be equal to or inherited from the parameter type, the return type must be equal to the parameter type or be in its inheritance hierarchy. In the case of a parameter passed by reference, the type of its value must be invariant, i.e. is equal to the value type of the method parameter. If null is passed in the parameter, then we cannot get its type and this parameter will be ignored when matching. Therefore, there is a TypedValue stub class that can be specified as the passed value. Using it, you can pass values of any type, even interfaces. Default-values of types should be passed like this:

object TypedValue.DefaultOf<T>();
object TypedValue.DefaultOf(Type type);
Enter fullscreen mode Exit fullscreen mode

A strongly typed value can be passed in a method with a generic value type parameter, or like this:

object TypedValue.ValueOf<T>(T? value);
object TypedValue.ValueOf(object? value, Type type);
Enter fullscreen mode Exit fullscreen mode

For methods that return values of fields, properties, or methods, you can specify a covariant type to its value type or not specify it. If no type value is specified, then members with any return type and other matching parameters are selected.

Methods of the Reflector class can return the three expected types of error in an InvalidOperationException: "member not found in type", "member ambiguous in type", and "not all arguments defined" for a generic type or method.

There are two categories of methods for dealing with each kind of type member: direct methods, which throw an exception if the member is missing, and try methods prefixed with Try, which return false in this situation. The number of variants of methods is very large, so here I will give one type from each category. Those who are interested can view the entire list of Reflector class methods on the Wiki or through the Object Browser in Visual Studio.

Let's move on to the API.

Methods for working with fields.

There are several types of methods for working with fields:
getting a field descriptor and getting, setting, replacing, and
exchanging a field value.

// Getting the info of an instance field of an object
public static FieldInfo GetField(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Getting the info of a static field of a type
public static FieldInfo GetField(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Getting the value of an instance field of an object
public static object? GetFieldValue(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Getting the value of a static field of a type
public static object? GetFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Setting the value of an instance field of an object
public static void SetFieldValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Setting the value of a static field of a type
public static void SetFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Replacing the value of an instance field of an object
public static object? ReplaceFieldValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Replacing the value of a static field of a type
public static object? ReplaceFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Exchanging the value of an instance field of an object
public static void ExchangeFieldValue(object source, string name, MemberAccessibility memberAccessibility, ref object? value);
// Exchanging the value of a static field of a type
public static void ExchangeFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, ref object? value);
Enter fullscreen mode Exit fullscreen mode

Methods for working with properties.

These methods are identical in parameters to the methods of working with fields.

// Getting the info of an instance property of an object
public static PropertyInfo GetProperty(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Getting the info of a static property of a type
public static PropertyInfo GetProperty(Type sourceType, string name, MemberAccessibility memberAccessibility, IList? typeArguments, Type? valueType);

// Getting the value of an instance property of an object
public static object? GetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Getting the value of a static property of a type
public static object? GetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList? typeArguments, Type? valueType);

// Setting the value of an instance property of an object
public static void SetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Setting the value of a static property of a type
public static void SetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList? typeArguments, object? value);

// Replacing the value of an instance property of an object
public static object? ReplacePropertyValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Replacing the value of a static property of a type
public static object? ReplacePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList? typeArguments, object? value);

// Exchanging the value of an instance property of an object
public static void ExchangePropertyValue(object source, string name, MemberAccessibility memberAccessibility, ref object? value);
// Exchanging the value of a static property of a type
public static void ExchangePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList? typeArguments, ref object? value);
Enter fullscreen mode Exit fullscreen mode

Methods for working with indexers (indexed properties).

In these methods, compared to regular properties, parameters of indexer values have been added. Parameters can be set both positional and named.

// Getting the info of an instance indexer of an object
public static PropertyInfo GetProperty(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);
// Getting the info of a static indexer of a type
public static PropertyInfo GetProperty(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);

// Getting the value of an instance indexer of an object
public static object? GetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);
// Getting the value of a static indexer of a type
public static object? GetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);

// Setting the value of an instance indexer of an object
public static void SetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);
// Setting the value of a static indexer of a type
public static void SetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);

// Replacing the value of an instance indexer of an object
public static object? ReplacePropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);
// Replacing the value of a static property of a type
public static object? ReplacePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);

// Exchanging the value of an instance indexer of an object
public static void ExchangePropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, ref object? value);
// Exchanging the value of a static indexer of a type
public static void ExchangePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, ref object? value);
Enter fullscreen mode Exit fullscreen mode

Methods for working with methods.

These methods allow you to call instance or static methods of types. Both synchronous and asynchronous methods are supported. Asynchronous methods support calling methods that return any awaitable types. For asynchronous types, the value type returned by the awaiter must be specified, i.e. not Task<TResult>, but TResult type. For all methods where there is no return value type parameter, it is assumed that the called method returns the void type.

// Getting the info of an instance method of an object
public static MethodInfo GetMethod(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Type? returnType);
// Getting the info of a static method of a type
public static MethodInfo GetMethod(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IList<Type?>? methodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Type? returnType);

// Calling an instance method of an object
public static object? CallMethod(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);
// Calling a static method of a type
public static object? CallMethod(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);

// Calling an instance asynchronous method of an object
public static Task<object?> CallMethodAsync(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);
// Calling a static asynchronous method of a type
public static Task<object?> CallMethodAsync(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);
Enter fullscreen mode Exit fullscreen mode

Methods for working with constructors.

These methods call the constructors of the specified type and return its constructed instance.

// Getting the info of a constructor
public static ConstructorInfo GetConstructor(Type sourceType, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues);
// Creating an instance of a type using a constructor
public static object Construct(Type sourceType, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues);
Enter fullscreen mode Exit fullscreen mode

Methods for working with events.

These methods allow you to get information about the event handle and perform the following actions with it: add a handler, remove a handler, clear all handlers, and call event handlers both synchronously and asynchronously. Despite the fact that it is not recommended to do return values in event handlers, because it makes no sense, however, support for working with such handlers is made here. Support has also been made for asynchronous awaitable handlers, which the developer must implement in the usual code, especially since it is not so difficult to do so. It should be mentioned that support for asynchronous calling of standard synchronous handlers has also been made - each handler is called in a separate task. Methods that make such calls contain a TaskFactory parameter, which creates a task for each handler. Below are methods for working with standard handlers. The event handler cleanup and raising methods contain an additional eventHandlerResolver delegate parameter. It is required if the storage of event handlers is implemented non-standard, i.e. the delegate is not stored in a protected field with the same name as the event name. In this case, the method needs to have access to the delegates to clear or invoke them, which is what the above resolver should provide. Or call the event in a standard way, through a method provided by the event implementation.

// Getting the info of an instance event of an object
public static EventInfo GetEvent(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Getting the info of a static event of a type
public static EventInfo GetEvent(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes)

// Adding an instance handler method to an instance event
public static void AddEventHandler(object eventSource, string eventName, MemberAccessibility eventAccessibility, object methodSource, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Adding a static handler method to an instance event
public static void AddEventHandler(object eventSource, string eventName, MemberAccessibility eventAccessibility, Type methodType, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodTypeArguments, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Adding an instance handler method to a static event
public static void AddEventHandler(Type eventType, string eventName, MemberAccessibility eventAccessibility, IList<Type?>? eventTypeArguments, object methodSource, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Adding a static handler method to a static event
public static void AddEventHandler(Type eventType, string eventName, MemberAccessibility eventAccessibility, IList<Type?>? eventTypeArguments, Type methodType, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodTypeArguments, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);

// Removing an instance handler method from an instance event
public static void RemoveEventHandler(object eventSource, string eventName, MemberAccessibility eventAccessibility, object methodSource, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Removing a static handler method from an instance event
public static void RemoveEventHandler(object eventSource, string eventName, MemberAccessibility eventAccessibility, Type methodType, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodTypeArguments, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Removing an instance handler method from a static event
public static void RemoveEventHandler(Type eventType, string eventName, MemberAccessibility eventAccessibility, IList<Type?>? eventTypeArguments, object methodSource, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Removing a static handler method from a static event
public static void RemoveEventHandler(Type eventType, string eventName, MemberAccessibility eventAccessibility, IList<Type?>? eventTypeArguments, Type methodType, string methodName, MemberAccessibility methodAccessibility, IList<Type?>? methodTypeArguments, IList<Type?>? methodMethodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);

// Clearing all handler methods from an instance event
public static void ClearEventHandlers(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Func<EventInfo, object, Delegate?>? eventHandlerResolver = null);
// Clearing all handler methods from a static event
public static void ClearEventHandlers(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Func<EventInfo, Delegate?>? eventHandlerResolver = null);

// Raising handler methods of an instance event
public static void RaiseEvent(object source, string name, MemberAccessibility memberAccessibility, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Func<EventInfo, object, Delegate?>? eventHandlerResolver = null);
// Raising handler methods of a static event
public static void RaiseEvent(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Func<EventInfo, Delegate?>? eventHandlerResolver = null);

// Asynchronous raising synchronous handler methods of an instance event
public static Task RaiseEventAsync(object source, string name, MemberAccessibility memberAccessibility, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, TaskFactory? taskFactory, Func<EventInfo, object, Delegate?>? eventHandlerResolver = null);
// Asynchronous raising synchronous handler methods of a static event
public static Task RaiseEventAsync(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, TaskFactory? taskFactory, Func<EventInfo, Delegate?>? eventHandlerResolver = null);
Enter fullscreen mode Exit fullscreen mode

Final example

Next, I will give a small example to work with an instance of the ObservableCollection<string> class using methods of the Reflector class.

internal static class Example
{
  private static void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    => Console.WriteLine($"event: CollectionChanged, action: '{e.Action}'");

  private static void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    => Console.WriteLine($"event: PropertyChanged, property: '{e.PropertyName}'");

  public static void Test()
  {
    //  Creating an instance of the ObservableCollection class
    var listContent = new[] { "One", "Two", "Three", "Four", "Five" };
    var typeArguments = new Type[] { null };

    var observableList = Reflector.Construct(typeof(ObservableCollection<>), MemberAccessibility.Public, typeArguments, new[] { listContent }, null);
    //var observableList = Reflector.Construct(typeof(ObservableCollection<string>), MemberAccessibility.Public, null, new[] { listContent }, null);
    //var observableList = Reflector.Construct<ObservableCollection<string>>(MemberAccessibility.Public, new[] { listContent }, null);

    //  Getting the items count property of a list
    var count = Reflector.GetPropertyValue(observableList, "Count", MemberAccessibility.Public, null);

    Console.WriteLine("List count: {0}", count);

    //  Adding a CollectionChanged event handler
    Reflector.AddEventHandler(
      observableList, "CollectionChanged", MemberAccessibility.Public,
      typeof(Sample), "OnCollectionChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(NotifyCollectionChangedEventArgs) }, null);

    //  Adding a PropertyChanged event handler
    Reflector.AddEventHandler(
      observableList, "PropertyChanged", MemberAccessibility.Family,
      typeof(Sample), "OnPropertyChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(PropertyChangedEventArgs) }, null);

    //  Adding an item to a list
    var addItem = "Last";
    Console.WriteLine();
    Console.WriteLine("Add item: '{0}'", addItem);

    Reflector.CallMethod(observableList, "Add", MemberAccessibility.Public, null, new object[] { addItem }, null);

    Console.WriteLine("List content: '{0}'", string.Join(',', (IEnumerable<string>)observableList));

    //  Replacing a list item by index
    var updateItem = "ThreeNew";
    var updateIndex = 2;
    Console.WriteLine();
    Console.WriteLine("Replace item: '{0}' at position: '{1}'", updateItem, updateIndex);

    var oldValue = Reflector.ReplacePropertyValue(observableList, "Item", MemberAccessibility.Public, new object[] { updateIndex }, null, updateItem);

    Console.WriteLine("Previous item: '{0}'", oldValue);
    Console.WriteLine("List content: '{0}'", string.Join(',', (IEnumerable<string>)observableList));

    //  Calling the move method
    var moveFrom = 0;
    var moveTo = 3;
    Console.WriteLine();
    Console.WriteLine("Move item from position: '{0}' to position: '{1}'", moveFrom, moveTo);

    Reflector.CallMethod(observableList, "Move", MemberAccessibility.Public, null, new object[] { moveFrom, moveTo }, null);

    Console.WriteLine("List content: '{0}'", string.Join(',', (IEnumerable<string>)observableList));

    //  Raising the PropertyChanged event from other side code
    var propertyName = "MyProperty";
    Console.WriteLine();
    Console.WriteLine("Raise custom PropertyChanged event: '{0}'", propertyName);

    Reflector.RaiseEvent(observableList, "PropertyChanged", MemberAccessibility.Family,
      new object[] { null, new PropertyChangedEventArgs(propertyName) }, null);

    //  Removing a specific handler from an event
    Reflector.RemoveEventHandler(
      observableList, "PropertyChanged", MemberAccessibility.Family,
      typeof(Sample), "OnPropertyChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(PropertyChangedEventArgs) }, null);

    //  Removing all handlers from an event
    Reflector.ClearEventHandlers(
      observableList, "CollectionChanged", MemberAccessibility.Public,
      new[] { typeof(object), typeof(NotifyCollectionChangedEventArgs) }, null);
  }
}
Enter fullscreen mode Exit fullscreen mode

After calling the Test() method of the Example class in the console, we should see the following:

List count: 5

Add item: 'Last'
event: PropertyChanged, property: 'Count'
event: PropertyChanged, property: 'Item[]'
event: CollectionChanged, action: 'Add'
List content: 'One,Two,Three,Four,Five,Last'

Replace item: 'ThreeNew' at position: '2'
event: PropertyChanged, property: 'Item[]'
event: CollectionChanged, action: 'Replace'
Previous item: 'Three'
List content: 'One,Two,ThreeNew,Four,Five,Last'

Move item from position: '0' to position: '3'
event: PropertyChanged, property: 'Item[]'
event: CollectionChanged, action: 'Move'
List content: 'Two,ThreeNew,Four,One,Five,Last'

Raise custom PropertyChanged event: 'MyProperty'
event: PropertyChanged, property: 'MyProperty'
Enter fullscreen mode Exit fullscreen mode

As a result, I note that the nuget package that contains the Reflector class is called VasEug.PowerLib.System and has a MIT license.

Top comments (0)