В данной статье хочу немного рассказать о классе Reflector, который был написан для облегчения работы с объектами и методами предоставляемыми.NET Reflection API из пространства имен System.Reflection. Отражение позволяет выполнять задачи по исследованию сборок, типов, интерфейсов, методов с их параметрами, полей, свойств и событий путем получения информации, описывающей их структуру. Эта информация хранится в метаданных сборки и получить ее можно, используя предназначенные для этого объекты и методы API отражения. Она может потребоваться как просто для получения метаданных об интересуемых объектах, так и для генерации используемого их кода в момент работы приложения либо через сам Reflection API из пространства имен System.Reflection.Emit, либо через API LINQ Expressions из пространства имен System.Linq.Expressions. В данной статье мы не будем касаться генерации кода, а рассмотрим другую возможность, которую предоставляет отражение — это доступ к членам типов или к членам их экземпляров, вызов их методов и активацию событий.

Здесь надо отметить, что обращение к членам класса и вызов методов через механизм отражения имеет как недостатки, так и преимущества. Одним из преимуществ является возможность изучения внутренней структуры типа и получение доступа как к публичным, так и к закрытым его членам. Можно модифицировать закрытые поля, устанавливать свойства и вызывать методы. Но здесь также таится и недостаток в плане безопасности — без точного понимания процессов, происходящих при изменении внутреннего состояния объекта, можно привести его в неработоспособное состояние. Основным же недостатком механизма отражения является скорость его работы, поэтому его используют там, где скорость не является критическим фактором, например, тестирование или там, где используется позднее связывание и/или требуется информация о структуре типа (плагины, сериализаторы, интерфейсы с «утиной» типизацией и пр.).

Особенно хочу коснуться модульного тестирования, именно оно сподвигло меня доделать некогда сделанный в академических целях класс. Требовалось написать unit‑тесты на не один десяток методов, каждый из которых должен был запускаться с несколькими наборами параметров. Поэтому был использован подход, называемый DDT (Data Driven Testing) — управляемое данными тестирование и заключающийся в том, что на вход тестового метода подаются значения входных параметров и эталонный (ожидаемый) результат, с которым производится сравнение полученного (фактического) значения в результате выполнения тестируемого действия над входными параметрами.

Большинство методов в тестовых фреймворках оперируют значениями, приведенными к типу object. API отражения также оперирует значениями, приведенными к типу object, поэтому все преобразования и проверки типов ложатся на соответствующие фреймворки. Нам же фактически необходимо найти нужный метод, соответствующий входным параметрам. Например, сейчас можно в массив данных для тестирования записать тип или его экземпляр, наименование определенного в нем метода и некоторые его атрибуты, массив значений параметров и ожидаемый результат или массив результатов (поддерживаются также out параметры). Внутри тестового метода в самом простом случае нужно будет вызвать метод Reflector.CallMethod с переданными параметрами, получить фактический результат и сравнить его с ожидаемым. Т.е. никаких дескрипторов членов типа получать не нужно. Ниже приведен фрагмент тестов методов валидации строковых аргументов класса валидатора Argument из этой же библиотеки.

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);
}

Простой сценарий доступа к членам типов через стандартный API заключается в получении метаинформации о требуемом члене типа или его экземпляра, для чего надо вызвать метод, возвращающий необходимые метаданные. Обычно в качестве параметров мы должны знать тип, в котором определен интересуемый нас член типа, его наименование, тип доступа (публичный или непубличный), область определения (статический или экземплярный) и тип его значения (для полей и простых свойств) или сигнатуру (для методов, свойств-индексаторов и конструкторов). Эти параметры запроса передаются функциям API отражения, которые возвращают найденный единственный член типа или коллекцию найденных согласно указанному условию членов типа. Возвращаемые члены типов могут быть определены как в самом исследуемом типе, так и в его базовых классах, что опять же зависит от задаваемых условий.

Сделаем небольшое отступление и договоримся о последующей терминологии. Будем называть универсальными, типы с типами передаваемыми в качестве параметров, например, GenType<TA, TB>, где TA и TB будут аргументами универсального типа GenType. По такому же принципу будем называть методы универсальными, например, GenMethod<TX, TY>(TX x, TY y). Универсальные типы и методы с неозначенными аргументами типами называются определениями универсальных типов и методов соответственно. Универсальные типы и методы называются открытыми, если не все аргументы типы означены. В противном случае они называются закрытыми.

Рассмотрим простой пример установки нового значения закрытого статического поля класса с возвратом его предыдущего значения. В отличие от обычного кода здесь мы первой строкой получаем дескриптор этого статического поля, объявленного в классе A, а затем вызываем соответствующие методы получения и установки значения этого поля. Во втором методе показано, что функционал класса Reflector может выполнить все эти действия за вас. Надо отметить, что здесь для упрощения опущены проверки на null-значение параметра, которое не допускается, т.к. невозможно извлечь тип объекта. Для этого в вышеупомянутом классе пустое значение конкретного типа можно передать через экземпляр класса TypedValue, либо использовать метод с универсальным параметром типа значения. В случае же передачи методу значения с несовпадающим типом поля int будет сгенерировано исключение об отсутствии указанного поля в типе.

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);
  }
}

Рассмотрим уже другой пример с универсальным типом, где типом поля является аргумент класса, в котором оно объявлено. Для каждого закрытого универсального типа с уникальным набором его аргументов будет создан свой экземпляр статического члена типа. Пусть в метод, устанавливающий значение этого поля, передается приведенное к типу object значение. Поэтому мы должны определить тип данного значения, получить определение универсального типа, из которого применив в качестве параметра тип переданного значения поля сконструировать закрытый универсальный тип. Здесь также приведен второй метод, выполняющий то же действие, но с использованием класса Reflector.

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);
  }
}

Далее рассмотрим вызов универсального метода универсального класса. Этот код уже более сложен, хотя и имеет всего два обобщающих аргумента – один для типа, другой для метода. В листинге ниже приведены два метода вызывающие один и тот же метод Replace класса B<T>. В первом методе приведен код, который надо написать в обычном порядке для выполнения вышеописанного действия. Второй метод использует функционал класса Reflector.

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);
  }
}

А если таких методов будет множество, и они будут иметь большее число параметров?! Поэтому и возникла мысль упростить такие вызовы и автоматизировать выбор подходящих членов типа на основе значений входных параметров и ожидаемых типов возвращаемых значений.

Класс Reflector позволяет осуществлять получение дескрипторов полей, свойств, методов, конструкторов и событий с означенными аргументами универсальных типов и методов на основе типов параметров и ожидаемых типов возвращаемых значений. Также он позволяет осуществлять доступ к значениям таких членов типа как поля и свойства на чтение и запись, осуществлять вызов как синхронных, так и асинхронных методов, конструкторов и событий.

Во всех методах работы с объектами отражения класса Reflector в качестве первого параметра может быть указан либо экземпляр объекта требуемого типа для работы с его экземплярными членами, либо сам тип для работы с его статическими членами. Также тип может быть указан в обобщающих параметрах методов и опущен в обычных параметрах. Другим параметром является наименование члена, которое не указывается только для членов конструкторов. Еще одним общим параметром является значение флагов перечислимого типа MemberAccessibility:
None – отсутствие флагов;
IgnoreCase – флаг игнорирования регистра при сравнении наименований членов типа и параметров метода;
DeclaredOnly – флаг, допускающий только экземплярные члены, определенные в указанном типе. В отсутствии данного флага допускаются экземплярные члены всей иерархии наследования;
FlattenHierarchy – флаг, допускающий статические члены всей иерархии типов. В отсутствии данного флага допускаются статические члены, определенные только в указанном типе;
TopHierarchy – указывает на то, что в случае обнаружения нескольких подходящих под условие членов типа на разных уровнях наследования, будет взят тот, что находится выше других в списке наследования. В случае отсутствия данного флага и обнаружения нескольких подходящих методов на разных уровнях наследования или на одном уровне наследования независимо от наличия данного флага, будет сгенерировано исключение о неоднозначности выбора члена типа.
Family – указывает что будут допускаться члены типа с уровнем доступа Family (protected в C#);
Assembly – указывает что будут допускаться члены типа с уровнем доступа Assembly (internal в C#);
FamilyOrAssembly – указывает что будут допускаться члены типа с уровнем доступа Family или Assembly (internal protected в C#);
FamilyAndAssembly – указывает что будут допускаться члены типа с уровнем доступа Family и Assembly (private protected в C#);
Private – указывает что будут допускаться члены типа с уровнем доступа Private (private в C#);
Public – указывает что будут допускаться члены типа с уровнем доступа Public (public в C#);
NonPublic – является комбинацией всех флагов с непубличным уровнем доступа;
AnyAccess – является комбинацией всех флагов уровня доступа;
MemberAccessMask – является маской для всех флагов уровня доступа.

Далее опционально могут следовать два массива аргументов универсального типа и метода. Первый массив аргументов универсального типа передается в параметре typeArguments, второй массив аргументов универсального метода передается в параметре methodArguments. Применяются следующие правила интерпретации этих параметров.
Если указан закрытый универсальный тип для обнаружения искомого члена, то данный параметр указывать не требуется. Если же данный параметр будет указан, то будет осуществлена проверка на равенство размерности количества аргументов универсального типа и указанного массива. Далее будут сравниваться каждый аргумент универсального типа с элементом массива в соответствующей позиции, но только в том случае если элемент массива указан, т.е. не содержит нулевого значения. Таким образом длинной массива аргументов мы можем вводить ограничение арности аргументов универсального типа или метода, хотя сами аргументы можем не указывать.

Также мы можем указать определение универсального типа для обнаружения искомого члена. При указании массива аргументов в данном случае функционал отражения класса Reflector будет пытаться означить определение универсального типа указанными аргументами. В случае если какой-то аргумент будет не задан или сам массив аргументов, то функционал попытается вывести его, основываясь на использовании, например, из типа значения поля, свойства или параметров метода. В случае успешного означивания аргументов определения универсального типа будет создан закрытый универсальный тип, в котором уже будет производится обнаружение искомого члена.

Вышеописанные правила также могут быть применены и к аргументам универсальных методов. Если массив типов аргументов будет указан, то параметры с типами этих аргументам буду проверяться на соответствие, если же аргументы будут не указаны или указаны частично, то они будут означиваться по использованию. Подходящие универсальные методы должны иметь количество аргументов, равное длине массива передаваемых аргументов в случае его задания, хотя сами элементы массива могут быть не заданы, возлагая ответственность за их определение на функционал класса Reflector.

Здесь надо отметить, что передача пустого массива аргументов явно говорит о том, что тип или метод не должны быть универсальными.

Для параметризированных членов, а это, свойства с индексаторами, методы и конструкторы указываются типы параметров (для функций получения дескрипторов членов) или значения параметров (для функций доступа к членам). Параметры могут передаваться как позиционные в списке, так и поименованные в словаре (допускается указание обеих).

Позиционные параметры начинаются с нулевого индекса и каждое значение будет соответствовать параметру члена в соответствующей позиции. Необязательные параметры имеющие значения по умолчанию могут быть не указаны, в этом случае они инициализируются значением по умолчанию, указанным в параметре. Сопоставление поименованных параметров происходит по их именам в соответствии с флагом IgnoreCase из вышеописанного перечислимого типа MemberAccessibility, т.е. с учетом чувствительности к регистру или без него. После вызова методов, имеющих параметры со значением, передающимся по ссылке или возвращающимся по ней, в соответствующих этим параметрам позициях списка или словаря будут содержаться измененные или возвращенные вызванным методом значения. При сопоставлении типов передаваемых параметров применяется правило вариантности, т.е. тип значения, передаваемый в метод, должен быть равен или быть наследован от типа параметра, возвращаемый тип должен быть равен типу параметра или находится в его иерархии наследования. В случае параметра, передаваемого по ссылке, тип его значения должен быть инвариантен, т.е. равен типу значения параметра метода. В случае если в параметре передается значение null, то его тип мы получить не можем и этот параметр будет игнорирован при сопоставлении. Поэтому существует класс-заглушка TypedValue который можно указывать в качестве передаваемого значения. Используя его, можно передавать значения любых типов, даже интерфейсов. Default-значения типов желательно передавать так:

TypedValue.DefaultOf<T>()
TypedValue.DefaultOf(Type type)

Строго типизированное значение можно передать в методе с универсальным параметром типа значения или так:

TypedValue.ValueOf<T>(T? value)
TypedValue.ValueOf(object? value, Type type)

Для методов возвращающих значения полей, свойств или методов, можно указать ковариантный тип типу его значения или не указывать. Если значение типа не указано, то выбираются члены с любым типом возвращаемого значения и другими удовлетворяющими условиям параметрами.

Методы класса Reflector могут возвращать три ожидаемых вида ошибки в исключении InvalidOperationException: "член не обнаружен в типе", "неоднозначное обнаружение члена в типе" и "не все аргументы означены" для универсального типа или метода.

Для работы с каждым видом членов типа существует две категории методов: прямые методы, которые генерируют исключение при отсутствии искомого члена и пробные методы с префиксом Try которые возвращают в этой ситуации значение false. Количество вариантов методов просто огромно, поэтому здесь мы будем приводить по одному виду из каждой категории. Интересующиеся могут посмотреть весь список методов класса Reflector на Wiki или через браузер объектов в Visual Studio.

Давайте перейдем к рассмотрению самого API.

  1. Методы для работы с полями.

Существует несколько видов методов для работы с полями: получение дескриптора поля и получение, установка, замена и обмен значения поля.

// Получение дескриптора экземплярного поля объекта
public static FieldInfo GetField(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Получение дескриптора статического поля типа
public static FieldInfo GetField(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Получение значения экземплярного поля объекта
public static object? GetFieldValue(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Получение значения статического поля типа
public static object? GetFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Установка значения экземплярного поля объекта
public static void SetFieldValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Установка значения статического поля типа
public static void SetFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Замена значения экземплярного поля объекта
public static object? ReplaceFieldValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Замена значения статического поля типа
public static object? ReplaceFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Обмен значения экземплярного поля объекта
public static void ExchangeFieldValue(object source, string name, MemberAccessibility memberAccessibility, ref object? value);
// Обмен значения статического поля типа
public static void ExchangeFieldValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, ref object? value);
  1. Методы для работы со свойствами.

Данные методы по параметрам идентичны методам работы с полями.

// Получение дескриптора экземплярного свойства объекта
public static PropertyInfo GetProperty(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Получение дескриптора статического свойства типа
public static PropertyInfo GetProperty(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Получение значения экземплярного свойства объекта
public static object? GetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, Type? valueType);
// Получение значения статического свойства типа
public static object? GetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, Type? valueType);

// Установка значения экземплярного свойства объекта
public static void SetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Установка значения статического свойства типа
public static void SetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Замена значения экземплярного свойства объекта
public static object? ReplacePropertyValue(object source, string name, MemberAccessibility memberAccessibility, object? value);
// Замена значения статического свойства типа
public static object? ReplacePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, object? value);

// Обмен значения экземплярного свойства объекта
public static void ExchangePropertyValue(object source, string name, MemberAccessibility memberAccessibility, ref object? value);
// Обмен значения статического свойства типа
public static void ExchangePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, ref object? value);
  1. Методы для работы с индексаторами (индексируемыми свойствами).

В данных методах по сравнению с обычными свойствами добавлены параметры значений индексаторов. Параметры могут задаваться как позиционные, так и поименованные.

// Получение дескриптора экземплярного индексатора объекта
public static PropertyInfo GetProperty(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);
// Получение дескриптора статического индексатора типа
public static PropertyInfo GetProperty(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);

// Получение значения экземплярного индексатора объекта
public static object? GetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);
// Получение значения статического индексатора типа
public static object? GetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, Type? valueType);

// Установка значения экземплярного индексатора объекта
public static void SetPropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);
// Установка значения статического индексатора типа
public static void SetPropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);

// Замена значения экземплярного индексатора объекта
public static object? ReplacePropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);
// Замена значения статического индексатора типа
public static object? ReplacePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, object? value);

// Обмен значения экземплярного индексатора объекта
public static void ExchangePropertyValue(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, ref object? value);
// Обмен значения статического индексатора типа
public static void ExchangePropertyValue(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues, ref object? value);
  1. Методы для работы с методами.

Данные методы позволяют вызывать экземплярные или статические методы типов. Поддерживаются как синхронные, так и асинхронные методы. Асинхронные методы поддерживают вызов методов возвращающих любые awaitable типы. Для асинхронных типов в качестве фильтра должен указываться тип значения возвращаемый awaiter-ом, т.е. не Task<TResult>, а именно тип TResult.Для всех методов где отсутствует параметр типа возвращаемого значения подразумевается что вызываемый метод возвращает тип void.

// Получение дескриптора экземплярного метода объекта
public static MethodInfo GetMethod(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Type? returnType);
// Получение дескриптора статического метода типа
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);

// Вызов экземплярного метода объекта
public static object? CallMethod(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);
// Вызов статического метода типа
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);

// Вызов экземплярного асинхронного метода объекта
public static Task<object?> CallMethodAsync(object source, string name, MemberAccessibility memberAccessibility, IList<Type?>? methodArguments, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Type? returnType);
// Вызов статического асинхронного метода типа
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);
  1. Методы для работы с конструкторами.

Данные методы вызывают конструкторы указанного типа и возвращают его сконструированный экземпляр.

// Получение дескриптора конструктора
public static ConstructorInfo GetConstructor(Type sourceType, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues);
// Создание экземпляра типа с использованием конструктора удовлетворяющего параметрам
public static object Construct(Type sourceType, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<object?>? positionalParameterValues, IReadOnlyDictionary<string, object?>? namedParameterValues);
  1. Методы для работы с событиями.

Данные методы позволяют получать информацию о дескрипторе события и выполнять с ним следующие действия: добавлять обработчик, удалять обработчик, делать очистку всех обработчиков и вызывать обработчики события как синхронно, так и асинхронно. Несмотря на то, что возвращаемые значения делать в обработчиках событий не рекомендуется, т.к. это лишено смысла, тем не менее, здесь сделана поддержка работы с такими обработчиками. Также сделана поддержка асинхронных awaitable-обработчиков, реализовать работу с которыми в обычном коде разработчик должен сам, тем более сделать это не так и сложно. Надо упомянуть, что сделана еще и поддержка асинхронного вызова стандартных синхронных обработчиков - каждый обработчик вызывается в отдельной задаче. Методы осуществляющие такие вызовы содержат параметр TaskFactory, с помощью которого создается задача для каждого обработчика. Ниже будут приведены методы для работы со стандартными обработчиками. Методы очистки и вызова обработчиков события содержат дополнительный параметр-делегат eventHandlerResolver. Он требуется в том случае, если хранение обработчиков события реализовано нестандартно, т.е. делегат не хранится в защищенном поле с именем идентичным наименованию события. В этом случае методу нужно иметь доступ к делегатам для их очистки или вызова, что и должен предоставлять вышеупомянутый резолвер. Либо вызывать событие стандартно, через метод, предоставляемый реализацией события.

// Получение дескриптора экземплярного события объекта
public static EventInfo GetEvent(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes);
// Получение дескриптора статического события типа
public static EventInfo GetEvent(Type sourceType, string name, MemberAccessibility memberAccessibility, IList<Type?>? typeArguments, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes)

// Добавление экземплярного метода-обработчика в экземплярное событие объекта
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);
// Добавление статического метода-обработчика в экземплярное событие объекта
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);
// Добавление экземплярного метода-обработчика в статическое событие типа
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);
// Добавление статического метода-обработчика в статическое событие типа
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);

// Удаление экземплярного метода-обработчика из экземплярного события объекта
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);
// Удаление статического метода-обработчика из экземплярного события объекта
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);
// Удаление экземплярного метода-обработчика из статического события типа
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);
// Удаление статического метода-обработчика из статического события типа
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);

// Очистка всех методов-обработчиков экземплярного события объекта
public static void ClearEventHandlers(object source, string name, MemberAccessibility memberAccessibility, IReadOnlyList<Type?>? positionalParameterTypes, IReadOnlyDictionary<string, Type?>? namedParameterTypes, Func<EventInfo, object, Delegate?>? eventHandlerResolver = null);
// Очистка всех методов-обработчиков статического события типа
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);

// Вызов методов-обработчиков экземплярного события объекта
public static void RaiseEvent(object source, string name, MemberAccessibility memberAccessibility, IList<object?>? positionalParameterValues, IDictionary<string, object?>? namedParameterValues, Func<EventInfo, object, Delegate?>? eventHandlerResolver = null);
// Вызов методов-обработчиков статического события типа
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);

// Асинхронный вызов синхронных методов-обработчиков экземплярного события объекта
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);
// Асинхронный вызов синхронных методов-обработчиков статического события типа
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);

Далее, для наглядности, приведу небольшой пример работы с экземпляром класса ObservableCollection<string> с использованием методов класса Reflector.

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()
  {
    //  Создание экземпляра класса ObservableCollection
    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);
    
    //  Получение свойства счетчика элементов списка
    var count = Reflector.GetPropertyValue(observableList, "Count", MemberAccessibility.Public, null);
    
    Console.WriteLine("List count: {0}", count);
    
    //  Добавление обработичика события PropertyChanged
    Reflector.AddEventHandler(
      observableList, "CollectionChanged", MemberAccessibility.Public,
      typeof(Sample), "OnCollectionChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(NotifyCollectionChangedEventArgs) }, null);
    
    //  Добавление обработичика события PropertyChanged
    Reflector.AddEventHandler(
      observableList, "PropertyChanged", MemberAccessibility.Family,
      typeof(Sample), "OnPropertyChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(PropertyChangedEventArgs) }, null);

    //  Добавление элемента в список
    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));
    
    //  Замена элемента списка по индексу
    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));
    
    //  Вызов метода Move
    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));

    //  Вызов события PropertyChanged из стороннего кода
    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);

    //  Удаление конкретного обработчика из события
    Reflector.RemoveEventHandler(
      observableList, "PropertyChanged", MemberAccessibility.Family,
      typeof(Sample), "OnPropertyChanged", MemberAccessibility.Private, null, null,
      new[] { typeof(object), typeof(PropertyChangedEventArgs) }, null);

    //  Удаление всех обработчиков из события
    Reflector.ClearEventHandlers(
      observableList, "CollectionChanged", MemberAccessibility.Public,
      new[] { typeof(object), typeof(NotifyCollectionChangedEventArgs) }, null);
  }
}

После вызова метода Test() класса Example в консоли мы должны увидеть следущее:

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'

В итоге, отмечу, что nuget-пакет в котором содержится класс Reflector называется VasEug.PowerLib.System и имеет MIT лицензию.

Комментарии (7)


  1. ValeraBgg
    00.00.0000 00:00
    +10

    Какой ужас... Даже не в плане чистоты кода, а именно подхода.

    Если вам надо сделать обобщенный тест, который бы перебирал методы тестируемого класса, то это лучше делать через передачу функций как параметров, например так:

    public class Example
    {
        public class Subject
        {
            public int Sum(int x, int y) => x + y;
            public double Multiply(double x, double y) => x * y;
        }
    
        public static IEnumerable<object?[]> TestData
            => new[]
            {
                new object[] {(Subject s) => s.Sum(1, 2), 3},
                new object[] {(Subject s) => s.Sum(0, 8), 8},
                new object[] {(Subject s) => s.Multiply(1, 2), 2},
                new object[] {(Subject s) => s.Multiply(8, 0), 0},
            };
    
        [Theory]
        [MemberData(nameof(TestData))]
        public void TestTheory<T>(Func<Subject, T> act, T expected)
        {
            var subject = new Subject();
            var actual = act(subject);
            Assert.Equal(expected, actual);
        }
    }

    А когда вам для тестов нужно внедряться внутрь тестируемого класса, например тестировать приватные методы, то лучше отказаться от этой затеи вообще и переписать тестируемый класс так, чтобы его тестировать было удобно. Возможно, для этого потребуется разбить его на несколько классов и тестировать их отдельно.

    По опыту знаю, что попытки пролезть тестами там, где неудобно, через рефлексию заканчиваются в лучшем случае хрупкими тестами, на поддержку которых уходит непростительно много времени.

    Рефлексия же нужна в очень исключительных случаях, например в АОП, но никак не в тестах.


    1. VasEug Автор
      00.00.0000 00:00

      Если у вас данные приходят десериализованные из внешнего источника а тестируемые методы и типы являются универсальными, то ваш код уже не подходит, и вам также придется использовать рефлексию. Идея именно в этом заключалась. Смысл этой надстройки как раз в том чтобы упростить работу с универсальными типами и методами когда на входе "нетипизированые" данные.

      По поводу тестирования закрытых членов я полностью с вами согласен, но...приходилось писать специальный тест на стороннюю сборку. Да и использование этой надстройки гораздо шире, чем тесты.


      1. ValeraBgg
        00.00.0000 00:00
        +3

        Смысл этой надстройки как раз в том чтобы упростить работу с универсальными типами и методами когда на входе "нетипизированые" данные

        Не надо работать с нетипизированными данными, типизируйте их :) У типизации много преимуществ, надо ими пользоваться. Главное из них - нахождение ошибок еще до запуска тестов, на стадии компиляции.

        Даже для данных из внешнего источника рефлексия не нужна, смотрите:

        public class Example
        {
            public class Subject
            {
                public int Sum(int x, int y) => x + y;
                public double Multiply(double x, double y) => x * y;
            }
            
            private abstract class Case {}
        
            private class Case<T> : Case
            {
                public T Expected { get; init; }
            }
        
            private class SumCase : Case<int>
            {
                public int A { get; init; }
                public int B { get; init; }
            }
        
            private class MultiplyCase : Case<double>
            {
                public double A { get; init; }
                public double B { get; init; }
            }
        
            private static List<Case> ParseFile(string fileName)
            {
                ...
            }
        
            public static IEnumerable<object?[]> TestData
                => ParseFile("test-cases.txt")
                  .Select(x => x switch { 
                      SumCase sc => new object[] {(Subject s) => s.Sum(sc.A, sc.B), sc.Expected},
                      MultiplyCase mc => new object[] {(Subject s) => s.Multiply(mc.A, mc.B), mc.Expected}
                  });
        
            [Xunit.Theory]
            [MemberData(nameof(TestData))]
            public void TestTheory<T>(Func<Subject, T> act, T expected)
            {
                var subject = new Subject();
                var actual = act(subject);
                Assert.Equal(expected, actual);
            }
        }


        1. VasEug Автор
          00.00.0000 00:00

          Так давайте уже придем к взаимопониманию...

          Никто здесь не спорит что типизация это плохо и т.п. Тем более я и сам в своем случае начинал писать методы пытаясь типизировать объекты, но потом меня это занятие сильно утомило, т.к. писать тесты на несколько сотен универсальных функций да еще и с различными наборами параметров на каждую. Притом данные приходят из внешнего источника, да еще и в некоторых случаях могут ожидаться объекты любых типов и они гораздо сложнее чем те которые приводите вы. И вот вы сами же показали выше написанные адаптеры. А чем объемнее и сложнее код, тем выше вероятность сделать ошибку в тестах и отлаживать уже их. И загружались у меня из внешннего источника куда более сложные типы данных чем int и long, И сколько это бы заняло времени? Вот и был найден способ ускорить данную процедуру. Тем более что написав с десяток адаптеров, пришлось отлаживать уже их. Поэтому и было принято данное решение. Вам не требуется тратить время на написание различных типизирующих адаптеров. Здесь вам нужно лишь передать десериализованный объект сразу в функцию без каких-либо накладных расходов.

          Еще раз скажу, что фактически здесь приводится инструмент позволяющий очень сильно упростить работу с унивирсальными типами и методами и "нетипизированными" данными которые надо обработать этими методами. И писалась данная надстройка конкретно не для использования в тестировании, но использование в конкретном кейсе тестирования приводится в качестве ПРИМЕРА. Здесь статья не про методологию тестирования, а про применение надстройки работы с отражением в конкретном примере тестирования. Вы видимо не так поняли посыл статьи, или я неправильно его раскрыл.

          P.S. Да, вспомнил еще в чем была трудность. Типы объектов которые приходили на тестирование также являлись закрытыми универсальными типами с несколькми параметрами.


  1. VasEug Автор
    00.00.0000 00:00

    Появилось немного времени и дабы исключить последующие комментарии вышенаписанной тематики, дам более развернутое объяснение для чего это было сделано в тестировании. Скажу сразу, что я ни в коей мере не намерен оспаривать то что написал автор вышенаписанных комментариев, но это действительно касается именно типизированных данных. Здесь приведу пример, который должен удовлетворить подобных комментаторов.

    Например, есть у нас такой код:

    public class Subject<T1, T2>
    {
    }
    
    public class Processor<T1>
    {
      public static R Method<M1, M2, R>(M1 m1, M2 m2)
        where M1 : Subject<T1, M2>
      {
      }
    }
    
    public class B1 { }
    public class B2 { }
    public class C1 { }
    public class C2 { }
    

    И на вход тестовой функции поступают из внешенго источника "нетипизированные" приведенные к типу object экземпляры классов Subject<B1, C2>, Subject<B2, C1>, Subject<B2,C2>..., вернее массив параметров тестируемого метода Method класса Processor. Таким источником может быть как файл, база данных и в моем случае это также отлаживаемый генератор объектов, который может генерировать данные универсальных типов в runtime. Если использовать предлагаемую надстройку, то необходимо написать лишь один вызов.

    [Xunit.Theory]
    [MemberData(nameof(TestData))]
    public void TestTheory(object[] inputs, object expected)
    {
      var actual = Reflector.CallMethod(typeof(Processor<>), MemberAccessibility.Public, null, null, inputs, null, null);
      Assert.Equal(expected, actual);
    }


    1. VasEug Автор
      00.00.0000 00:00

      Опечатка в коде вызова метода:

      var actual = Reflector.CallMethod(typeof(Processor<>), "Method", MemberAccessibility.Public, null, null, inputs, null, null);


      1. VasEug Автор
        00.00.0000 00:00

        Здесь я опять допустил ошибку :). Вызов должен быть такой:

        var actual = Reflector.CallMethod(typeof(Processor<>), "Method", MemberAccessibility.Public, null, null, inputs, null, expected.GetType());

        Если мы допускаем null-объекты в типе R, то необходимо ожидаемый тип передавать так же в параметрах, иначе можно в ограничении аргументов метода для типа R поставить notnull. А так все должно работать, проверил.