【问题标题】:Is it possible to automatically map from ValueTuple<> to class properties?是否可以自动从 ValueTuple<> 映射到类属性?
【发布时间】:2018-10-25 16:51:51
【问题描述】:

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

var target = Mapper.Map(source).To&lt;Dto&gt;();

其中sourceIEnumerable&lt;(string Foo, int Bar)&gt;Dto 是具有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 it is possible 在运行时访问值元组元素,特别是它的名称。

【问题讨论】:

  • 自己编写很容易,但您需要告诉 Mapper 方法名称,以便它可以使用 GetValueTupleNames

标签: c# mapping valuetuple


【解决方案1】:

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

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

使用这些方法,您现在可以自动将ValueTuples 映射到Dto

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

【讨论】:

  • 我会尝试使用匿名对象。我正在尝试合并 Dapper、(Some)Mapper 以减少手动代码。它看起来像这样:DB->Dapper->Anonymous 或 Tuples->Magic->Dto 中的许多 SP,其中 SP/Dto 比率约为 100/10
【解决方案2】:

元组类型名称由返回它们的方法定义,而不是实际的元组类型。元组名称是 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; }
}

【讨论】:

  • 有趣的是,这在另一个方向上更容易做到,因为您可以调用 MethodBase.GetCurrentMethod() 并使用它的返回参数来获取名称
  • 我不知道如何将它与方法返回 IEnumerable 一起使用
【解决方案3】:

这里的根本困难是 namedTuple 只是一个语法糖,你没有办法在运行时使用 Tuple Name。

来自Document

这些同义词由编译器和语言处理,因此 您可以有效地使用命名元组。 IDE 和编辑器可以阅读这些 使用 Roslyn API 的语义名称。您可以参考元素 由这些语义名称的命名元组在同一位置的任何位置 部件。 编译器会将您定义的名称替换为 Item* 生成编译输出时的等价物。 Microsoft 中间语言 (MSIL) 不包括名称 你已经给出了这些元素。

这迫使您在运行时使用 Item*。

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

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

就个人而言,我喜欢第二种解决方案。

Reflection 有一定程度的类型安全,但速度很慢,当你有很多数据时,性能是个问题,使用字典,类型安全性更差,但性能会更好(理论,未测试),对于您的问题,类型安全是一个基本问题,您只需要使用防御性编码并具有更好的错误处理能力,或者训练 API 用户按规则行事,我认为类型安全反射不会给您做很多事情。

【讨论】:

  • 原始问题有一个链接,指出如何获取运行时名称并显示执行此操作的代码。
  • @NetMage 是的,可以看到名字,但是不能调用ValueTuple。"Name",不仅语法上不可能,即使是,运行时也不会理解,因为有ValueTupe 上根本没有“名称”属性。这就是为什么您必须使用具有正确顺序的 nameMaping 和 ValueTuple.Item*
  • 只需要一点反射和到Item* 的映射。看我的回答。
  • 我想你误解了——你不必创建映射,你可以使用反射并自动构建映射。
  • @NetMage 我没有手动创建地图。我跳过了 Utilities.GetValueTupleNames(typeof(DataAccessLayer), nameof(DataAccessLayer.Method))),这是用户已经知道的反射地图。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-23
  • 2021-04-30
  • 2012-06-23
  • 1970-01-01
  • 2013-10-24
相关资源
最近更新 更多