繁体   English   中英

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

[英]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是具有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 ,可以在运行时访问值元组元素,特别是它的名称。

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

这是一个ValueTuple映射器,它使用提供的方法中的项目名称。 尽管这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;
        }
    }
}

有了这些方法,你现在可以映射ValueTuple s到Dto自动:

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

这里的基本困难是namedTuple只是一个语法糖,您无法在运行时使用Tuple Name。

来自文件

这些同义词由编译器和语言处理,因此您可以有效地使用命名的元组。 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]]};
        }

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

反射具有某种程度的类型安全性,但是它很慢,当您有大量数据时,性能是一个问题,对于dictionary来说,类型安全性较差,但性能会更好(理论上未经测试),您的问题,类型安全是一个基本问题,您只需要使用防御性编码并具有更好的错误处理能力,或者训练API用户按规则进行操作,我认为类型安全反射不会给您带来太大的帮助。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM