Можно ли автоматически сопоставить ValueTuple <> со свойствами класса? - PullRequest
0 голосов
/ 25 октября 2018

Используя некоторые существующие Mapper, можно:

var target = Mapper.Map(source).To<Dto>();

, где source - IEnumerable<(string Foo, int Bar)>, а Dto - класс со свойствами Foo и Bar?


Пример кода:

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace MapFromDynamicsToComplex
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var source = DataAccessLayer.Method();
            //var target = Mapper.Map(source).To<Dto>();
            var parameterNames = string.Join(", ", Utilities.GetValueTupleNames(typeof(DataAccessLayer), nameof(DataAccessLayer.Method)));

            Console.WriteLine(parameterNames);
            Console.ReadKey();
        }
    }

    public class DataAccessLayer
    {
        public static IEnumerable<(string Foo, int bar)> Method()
        {
            return new List<(string Foo, int bar)>
            {
                ValueTuple.Create("A", 1)
            };
        }
    }

    public class Dto
    {
        public string Foo { get; set; }
        public int Bar { get; set; }
        public object Baz { get; set; }
    }

    public static class Utilities
    {
        public static IEnumerable<string> GetValueTupleNames(Type source, string action)
        {
            var method = source.GetMethod(action);
            var attr = method.ReturnParameter.GetCustomAttribute<TupleElementNamesAttribute>();

            return attr.TransformNames;
        }
    }
}

Используя TupleElementNamesAttribute , можно получить доступ к элементу кортежа значения во время выполнения, в частности к его имени.

Ответы [ 3 ]

0 голосов
/ 25 октября 2018

Имена типов кортежей определяются методом, возвращающим их, а не фактическим типом кортежа.Имена кортежей являются на 100% синтаксическим сахаром, поэтому любой код отображения должен знать контекст, в котором используется кортеж.Это затрудняет отображение через отражение по сравнению с обычным объектом, где вы можете просто получить имена свойств объекта во время выполнения.

Вот один из подходов, использующий выражение linq для захвата метода, возвращающего кортеж:

public static class Mapper
{
  public static TupleMapper<TTuple> FromTuple<TTuple>(Expression<Func<TTuple>> tupleSource) where TTuple : struct, ITuple
  {
    if (!(tupleSource.Body is MethodCallExpression call))
    {
      throw new ArgumentException("Argument must be method call returning tuple type", nameof(tupleSource));
    }

    var tupleNamesAttribute = call.Method.ReturnParameter.GetCustomAttribute<TupleElementNamesAttribute>();

    var compiledTupleSource = tupleSource.Compile();

    return new TupleMapper<TTuple>(compiledTupleSource(), tupleNamesAttribute.TransformNames);
  }
}

public struct TupleMapper<TTuple> where TTuple : struct, ITuple
{
  private readonly IList<string> _names;
  private readonly TTuple _tuple;

  public TupleMapper(TTuple tuple, IList<string> names)
  {
    _tuple = tuple;
    _names = names;
  }

  public T Map<T>() where T : new()
  {
    var instance = new T();
    var instanceType = typeof(T);

    for (var i = 0; i < _names.Count; i++)
    {
      var instanceProp = instanceType.GetProperty(_names[i]);
      instanceProp.SetValue(instance, _tuple[i]);
    }

    return instance;
  }
}

Чтобы использовать это, синтаксис будет:

static void Main(string[] args)
{
  var dto = Mapper.FromTuple(() => ReturnsATuple()).Map<Dto>();

  Console.WriteLine($"Foo: {dto.Foo}, Bar: {dto.Bar}");

  Console.Read();
}

public static (string Foo, int Bar) ReturnsATuple()
{
  return ("A", 1);
}

class Dto
{
  public string Foo { get; set; }
  public int Bar { get; set; }
}
0 голосов
/ 26 октября 2018

Вот преобразователь ValueTuple, который использует имена элементов из предоставленного метода.Хотя это работает, я бы предложил использовать анонимный объект и сопоставление с ним было бы лучше, чем использовать ValueTuple, если у вас нет проблем с производительностью, и если у вас есть проблемы с производительностью с анонимными объектами, такие как использование ValueTuple, вы собираетесьпотерять любую прибыль, делая Reflection, чтобы сделать autopping.Также обратите внимание, что любые вложенные типы кортежей с именами могут работать некорректно.

Внутри класса Utility я создаю несколько вспомогательных методов для работы с MemberInfo, чтобы вы могли обрабатывать поля и свойства одинаково, а затемиспользуйте ваш метод для получения ValueTuple имен членов из метода.Затем я использую промежуточный класс (и опускаюсь до IEnumerable), чтобы я мог вывести тип источника и затем указать тип назначения во втором универсальном методе.

public static class Utilities {
    // ***
    // *** MemberInfo Extensions
    // ***
    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }

    public static object GetValue(this MemberInfo member, object srcObject) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.GetValue(srcObject);
            case PropertyInfo mpi:
                return mpi.GetValue(srcObject);
            default:
                throw new ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", nameof(member));
        }
    }
    public static T GetValue<T>(this MemberInfo member, object srcObject) => (T)member.GetValue(srcObject);

    public static void SetValue<T>(this MemberInfo member, object destObject, T value) {
        switch (member) {
            case FieldInfo mfi:
                mfi.SetValue(destObject, value);
                break;
            case PropertyInfo mpi:
                mpi.SetValue(destObject, value);
                break;
            default:
                throw new ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", nameof(member));
        }
    }

    public static IEnumerable<string> GetValueTupleNames(Type source, string action) {
        var method = source.GetMethod(action);
        var attr = method.ReturnParameter.GetCustomAttribute<TupleElementNamesAttribute>();

        return attr.TransformNames;
    }

    public class MapSource {
        public IEnumerable src { get; }
        public Type srcType { get; }
        public Type methodClass { get; }
        public string methodReturnsTupleName { get; }

        public MapSource(IEnumerable src, Type srcType, Type methodClass, string methodReturnsTupleName) {
            this.src = src;
            this.srcType = srcType;
            this.methodClass = methodClass;
            this.methodReturnsTupleName = methodReturnsTupleName;
        }
    }

    public static MapSource TupleMapper<VT>(this IEnumerable<VT> src, Type sourceClass, string methodReturnsTupleName) =>
        new MapSource(src, typeof(VT), sourceClass, methodReturnsTupleName);

    public static IEnumerable<T> To<T>(this MapSource ms) where T : new() {
        var srcNames = GetValueTupleNames(ms.methodClass, ms.methodReturnsTupleName).Take(ms.srcType.GetFields().Length).ToList();
        var srcMIs = srcNames.Select((Name, i) => new { ItemMI = ms.srcType.GetMember($"Item{i + 1}")[0], i, Name })
                             .ToDictionary(min => min.Name, min => min.ItemMI);
        var destMIs = srcNames.Select(n => new { members = typeof(T).GetMember(n), Name = n })
                              .Where(mn => mn.members.Length == 1 && srcMIs[mn.Name].GetMemberType() == mn.members[0].GetMemberType())
                              .Select(mn => new { DestMI = mn.members[0], mn.Name })
                              .ToList();

        foreach (var s in ms.src) {
            var ans = new T();
            foreach (var MIn in destMIs)
                MIn.DestMI.SetValue(ans, srcMIs[MIn.Name].GetValue(s));
            yield return ans;
        }
    }
}

С этими методами вы можете теперьсопоставить ValueTuple s с Dto автоматически:

var target = source.TupleMapper(typeof(DataAccessLayer), nameof(DataAccessLayer.Method)).To<Dto>().ToList();
0 голосов
/ 25 октября 2018

Фундаментальная трудность здесь в том, что namedTuple - просто синтаксический сахар, у вас нет способа использовать имя кортежа во время выполнения.

С Документ

Эти синонимы обрабатываются компилятором и языком, чтобы вы могли эффективно использовать именованные кортежи.Среды разработки и редакторы могут читать эти семантические имена с помощью API Roslyn.Вы можете ссылаться на элементы именованного кортежа по этим семантическим именам в любом месте одной сборки. Компилятор заменяет имена, которые вы определили, эквивалентами Item * при создании скомпилированного вывода. Скомпилированный Microsoft Intermediate Language (MSIL) не включает имена, которые вы дали этим элементам.

Это вынудило вас использовать Item * во время выполнения.

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

1, Reflection:

public static Dto ToDto((string, int) source , string[] nameMapping)
    {
        var dto = new Dto();
        var propertyInfo1 = typeof(Dto).GetProperty(nameMapping[0]);
        propertyInfo1?.SetValue(dto, source.Item1);
        var propertyInfo2 = typeof(Dto).GetProperty(nameMapping[1]);
        propertyInfo2?.SetValue(dto, source.Item2);
        return dto;
    }

2, Dictinary

public static Dto ToDto2((string, int) source, string[] nameMapping)
        {
            var dic = new Dictionary<string, object> {{nameMapping[0], source.Item1}, {nameMapping[1], source.Item2}};
            return new Dto {Foo = (string)dic[nameMapping[0]], Bar = (int)dic[nameMapping[1]]};
        }

Лично мне нравится второе решение.

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

...