是否可以自动从 ValueTuple<> 映射到 class 属性?

Is it possible to automatically map from ValueTuple<> to class properties?

使用一些现有的 Mapper,是否可以:

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

其中 sourceIEnumerable<(string Foo, int Bar)>Dto 是 class,具有属性 FooBar?


示例代码:

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 在运行时访问值元组元素,特别是它的名称。

这里的根本困难在于 namedTuple 只是一个语法糖,你无法在 运行 时间内使用 Tuple Name。

来自Document

These synonyms are handled by the compiler and the language so that you can use named tuples effectively. IDEs and editors can read these semantic names using the Roslyn APIs. You can reference the elements of a named tuple by those semantic names anywhere in the same assembly. The compiler replaces the names you've defined with Item* equivalents when generating the compiled output. The compiled Microsoft Intermediate Language (MSIL) does not include the names you've given these elements.

这迫使您在 运行 次使用物品*。

有两种方法可以做到这一点,我知道我的解决方案不优雅、不灵活或可扩展(我知道很多问题),但我只是想指出一个方向。您可以稍后完善解决方案。

1、反思:

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, 字典

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 用户遵守规则,我不认为type safety reflection give you will do much.

元组类型名称由返回它们的方法定义,而不是实际的元组类型。元组名称是 100% 的语法糖,因此任何映射代码都需要知道使用元组的上下文。与可以在运行时获取对象的 属性 名称的普通对象相比,这使得通过反射进行映射变得困难。

这是一种使用 linq 表达式来捕获 returns 元组的方法:

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

这是一个 ValueTuple 映射器,它使用提供的方法中的项目名称。虽然这可行,但我建议使用匿名对象并从中映射比使用 ValueTuple 更好,除非你有性能问题,如果你有匿名对象的性能问题,那么使用 ValueTuple 有帮助,您将通过反射来进行自动映射而失去任何收益。另请注意,任何带有名称的嵌套元组类型可能无法正常工作。

Utility class 中,我创建了一些用于处理 MemberInfo 的辅助方法,因此您可以同样对待字段和属性,然后使用您的方法获取 ValueTuple 来自方法的成员名称。然后我使用中间 class(并下拉到 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 自动映射到 Dto

var target = source.TupleMapper(typeof(DataAccessLayer), nameof(DataAccessLayer.Method)).To<Dto>().ToList();