Страницы

Поиск по вопросам

пятница, 5 октября 2018 г.

C#, Как скопировать поля объекта в другой объект, создав для этого expression

Хочу написать функцию, которая будет копировать одинаковые свойства из одного объекта в другой, но при этом чтобы она работала быстро. Вот что есть:
static class Copy { private static readonly ConcurrentDictionary> PropertiesDictionaries = new ConcurrentDictionary>();
public static void Сopyfields(object source, object target) { var sourceType = source.GetType(); var targetType = target.GetType(); var sourceProperties = GetProperties(sourceType); var targetPropertyes = GetProperties(targetType);
foreach (var targetProperty in targetPropertyes) { if (sourceProperties.TryGetValue(targetProperty.Key, out var sourceProperty)) { targetProperty.Value.SetValue(target, sourceProperty.GetValue(source)); } } }
private static ConcurrentDictionary GetProperties(Type objType) { if (!PropertiesDictionaries.TryGetValue(objType, out var propertiesInfoDictionary)) { var infos = objType.GetProperties(); propertiesInfoDictionary = new ConcurrentDictionary(); foreach (var propertyInfo in infos) { propertiesInfoDictionary.GetOrAdd(propertyInfo.Name, propertyInfo); } PropertiesDictionaries.GetOrAdd(objType, propertiesInfoDictionary); } return propertiesInfoDictionary; } }
Как мне переписать эту функцию, чтобы создавать expression и использовать рефлексию только 1 раз для каждой комбинации типов?


Ответ

Вот вам простейший вариант:
class MapperFactory { public static Action CreateMapper() { var sourceType = typeof(P); var targetType = typeof(Q);
var sourceProperties = GetVisibleProperties(sourceType); var targetProperties = GetVisibleProperties(targetType);
var fromVar = Expression.Variable(sourceType, "from"); var toVar = Expression.Variable(targetType, "to");
Expression CreateCopyProperty(string name) { try { return Expression.Assign( Expression.Property(toVar, targetProperties[name]), Expression.Property(fromVar, sourceProperties[name])); } catch (Exception ex) { throw new ArgumentException($"Невозможно скопировать свойство {name}", ex); } }
var commonProperties = sourceProperties.Keys.Intersect(targetProperties.Keys) .OrderBy(n => n); var assignExpressions = commonProperties.Select(CreateCopyProperty); var assignment = Expression.Block(assignExpressions); var lambda = Expression.Lambda>(assignment, fromVar, toVar); return lambda.Compile(); }
static Dictionary GetVisibleProperties(Type type) { IEnumerable GetTypeChain(Type t) => t == null ? Enumerable.Empty() : GetTypeChain(t.BaseType).Prepend(t);
Dictionary result = new Dictionary(); foreach (var t in GetTypeChain(type)) { var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (var prop in properties) { if (!result.ContainsKey(prop.Name)) result.Add(prop.Name, prop); } } return result; } }
Он не проверяет доступность, не заглядывает в private/protected, не проверяет совместимость типов. Если свойства с одинаковыми именами не могут быть скопированы, произойдёт исключение при создании маппера. Если целевое свойство недоступно для записи, произойдёт исключение при создании маппера. Не имеющие пары свойства молча игнорируются. Если свойство базового класса перекрыто по имени свойством производного класса, базовое свойство игнорируется (а что ещё можно делать?). Статические свойства игнорируются (это кажется правильным). Свойства, являющиеся явными реализациями интерфейса, игнорируются (потому что я не придумал, что можно с ними делать).
Пользоваться так:
class From { public int A { get; set; } public string B { get; set; } }
class To { public int A { get; set; } public string C { get; set; } }
class Program { static void Main(string[] args) { Action map = MapperFactory.CreateMapper(); var from = new From() { A = 15, B = "hello world" }; var to = new To(); map(from, to); Console.WriteLine(to.A); // 15 } }

Варианты, которые правильно работают и не падают:
interface I { int X { get; set; } }
class FromBase { public string A { get; set; } public int B { get; set; } }
class From : FromBase, I { public new int A { get; set; } static public string C { get; set; } int I.X { get; set; } }
class To { public int A { get; set; } public string C { get; set; } }

Объяснение.
Давайте для начала разберём функцию GetVisibleProperties. Нам нужно получить список всех свойств. Обычно это делается при помощи простого type.GetProperties(), но что делать в случае, когда у нас есть несколько свойств с одним именем? Такое может быть, если свойство из базового класса перекрыть при помощи new, как в последнем примере. Как получить только самое последнее свойство?
Для этого давайте сделаем такой трюк: пройдём по цепочке базовых типов от нашего типа до object, и будет добавлять на каждом шаге только свойства, которые определяются в этом типе, и только те, имя которых ещё не встречалось у нас. Для этого нам нужно получить цепочку базовых типов. Это проще всего сделать рекурсивно: к цепочке для базового класса добавить в начало наш собственный тип. Получается просто:
IEnumerable GetTypeChain(Type t) => t == null ? Enumerable.Empty() : GetTypeChain(t.BaseType).Prepend(t);
Окей, дальше мы походим по этой цепочке, и для каждого типа получаем только открытые свойства (BindingFlags.Public), только нестатические (BindingFlags.Instance), и только определённые прямо в этом типе, а не унаследованные (BindingFlags.DeclaredOnly). Для каждого из них мы проверяем, есть ли уже свойство с таким именем, и добавляем новое свойство в результат только если свойства с таким же именем ещё не было.
С GetVisibleProperties всё. Теперь, главная функция: CreateMapper
Мы передаём типы как обобщённые параметры, чтобы можно было возвращать типизированный делегат Action. Для начала, мы получаем свойства с помощью уже разобранной функции GetVisibleProperties. Далее, мы берём пересечение множества имён: это те имена свойств, которые есть обоих типах (commonProperties). Я ещё сортирую их по алфивиту, чтобы порядок был одинаковым.
Мы идём через System.Linq.Expressions, которые позволяют конструировать выражения (типа Expression) в коде. Это стандартный приём для подобной кодогенерации, у нас много ответов на сайте, использующих эту технику.
Итак, вспомогательная функция CreateCopyProperty. Она создаёт аналог выражения to.Prop1 = from.Prop1;. Мы предварительно определяем Expression-переменные fromVar и toVar, соответствующие «реальным» переменным типов P и Q с именами from и to. Expression.Property(toVar, targetProperties[name]) — это полный аналог to.Prop1, где свойство Prop1 определено через PropertyInfo (это необходимо для случая, когда у нас могут быть перекрытые имена). Expression.Assign создаёт, понятно, код для присвоения. Создание выражения может бросить исключение если, например, свойства несовместимых типов, или у целевого свойства нету сеттера. Можно было бы и не ловить это исключение, но тогда непонятно было бы, какое именно свойство «виновато», поэтому я добавил блок try/catch, и в тексте нового исключения упоминаю имя проблемного свойства.
Окей, дальше. У нас есть набор имён свойств (commonProperties), при помощи Select из него создаётся набор Expression'ов. Этот набор нужно упаковать в одно при помощи Expression.Block, который создаёт аналог блока из фигурных скобок:
{ to.Prop1 = from.Prop1; to.Prop2 = from.Prop2; ... }
Теперь из этого блока делаем лямбду:
(from, to) => { to.Prop1 = from.Prop1; to.Prop2 = from.Prop2; ... }
при помощи Expression.Lambda
Финальный трюк состоит в вызове Compile, который Expression компилирует в реальную функцию, которую мы и возвращаем пользователю.

Комментариев нет:

Отправить комментарий