Чтобы не ходить далеко, для примера напишем простой класс.
public class SimpleClass { private readonly string theValue; public SimpleClass(string value, int ratio) { theValue = value; } public string Value { get { return theValue; } } }
Добавим в этот класс свойства, предоставляющие доступ к его метаданным через механизм интроспекции:
internal static PropertyInfo ValueProperty { get { return typeof(SimpleClass).GetProperty("Value"); } } internal static FieldInfo ValueField { get { return typeof(SimpleClass).GetField("theValue", BindingFlags.NonPublic | BindingFlags.Instance); } } internal static ConstructorInfo Constructor { get { return typeof(SimpleClass).GetConstructor(new[] { typeof(string), typeof( }); } }
Если выполнить вывод значения этих свойств в консоль, то получим строковые представления этих программных элементов. Однако, если выполнить перестановку параметров конструктора при помощи встроенного в IDE движка рефакторинга(например, в Visual Studio это меню Refactoring, пункт Reorder parameters), то свойство, отражающее метаданные конструктора, будет возращать пустую ссылку(т.е. null). А если ещё и применить обфускацию к сборке, то отразить подобным образом поле и свойство станет вообще невозможным(за исключением случая применения атрибута ObfuscationAttribute).
Для того, чтобы выйти из ситуации, вооружимся деревьями выражений, лямбда-выражениями и обобщениями. Из всего это склеим вспомогательный метод:
static TMember Reflect<TDelegate, TMember>(Expression<TDelegate> memberAccess) where TDelegate : class where TMember : MemberInfo { if (memberAccess.Body is MemberExpression) return ((MemberExpression)memberAccess.Body).Member as TMember; else if (memberAccess.Body is NewExpression) return ((NewExpression)memberAccess.Body).Constructor as TMember; else if (memberAccess.Body is MethodCallExpression) return ((MethodCallExpression)memberAccess.Body).Method as TMember; else return null; }
Этот метод принимает два параметра-типа: первый принимает делегат, используемый для разрешения сигнатуры лямбда-выражения, который будет передаваться в качестве аргумента, второй — тип отражаемого члена класса(например, FieldInfo для поля). Передавамое лямбда-выражения в качестве аргумента описывает доступ к интересующему нас члену класса. Внутри метода происходит обращение к телу лямбда-выражения, представленного в виде дерева выражения. Поскольку тело представляет собой доступ к члену класса, то анализируем тип тела на возможные варианты:
- Доступ к свойству или полю;
- Вызов оператора new(данное выражение содержит ссылку на конструктор);
- Вызов метода.
Основная задача данного решения — уход от классических методов отражения с использованием флагов привязки, имён программных элементов и массивов, описывающих сигнатуры методов и конструкторов, поскольку эти методы не доступны для анализа движком рефакторинга и обфускаторами.
Теперь всё готово для написания кода отражения, изменяющегося автоматически в процессе рефакторинга и безопасного к алгоритмам обфускации.
internal static PropertyInfo ValueProperty { get { return Reflect<Func<SimpleClass, string>, PropertyInfo>(v => v.Value); } } internal static FieldInfo ValueField { get { return Reflect<Func<SimpleClass, string>, FieldInfo>(v => v.theValue); } } internal static ConstructorInfo Constructor { get { return Reflect<Func<string, int, SimpleClass>, ConstructorInfo>((a1, a2) => new SimpleClass(a1, a2)); } }
При таком подходе не используются строковые литералы для представления имён членов класса и массивы типов для описания сигнатуры конструктора.
Почему данный код остаётся рабочим после того, как по нему отработает обфускатор? Ответ кроется в том, как компилятор C# генерирует код для деревьев выражений. Если открыть сборку в ILDASM, то можно убедиться, что для загрузки метаданных члена класса используется инструкция LDTOKEN, которая оперирует числовым токеном, обозначающем расположение метаданных члена в соответствующей таблице внутри PE-файла.
Всегда есть НО
Данный способ подходит только для отражения программных элементов, доступных в текущей лексической области видимости.
