简体   繁体   中英

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

Using some existing Mapper, is it possible to:

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

where source is IEnumerable<(string Foo, int Bar)> and Dto is class with properties Foo and Bar ?


Example code:

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

By using TupleElementNamesAttribute it is possible to access value tuple element at runtime specifically it's name.

Tuple type names are defined by the method returning them, not the actual tuple type. The tuple names are 100% syntactic sugar, so any mapping code needs to be aware of the context in which the tuple is used. This makes mapping through reflection difficult compared to the a normal object where you can just grab an object's property names at runtime.

Here's one approach using a linq expression to capture the method which returns the tuple:

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

To use this, the syntax would be:

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

Here is a ValueTuple mapper that uses the item names from a provided method. While this works, I would suggest using an anonymous object and mapping from that would be better than using a ValueTuple unless you have performance issues, and if you have performance issues with anonymous objects such that using ValueTuple helps, you are going to lose any gains by doing Reflection to do automapping. Note also that any nested tuple types with names may not work properly.

Inside the Utility class, I create some helper methods for working with MemberInfo so you can treat fields and properties the same, and then use your method for getting ValueTuple member names from a method. Then I use an intermediate class (and drop down to IEnumerable ) so I can infer the source type and then specify the destination type in a second generic method.

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

With these methods, you can now map the ValueTuple s to Dto automatically:

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

The fundamental difficulty here is the namedTuple just a syntactic sugar, you don't have a way to use Tuple Name during run time.

From 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.

This forced you to use Item* at run time.

There are 2 ways to do this, I know my solution is not elegant and flexible or expendable (many issue I know), but I just want to point out a direction. You can refine the solution late.

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

Personally , I like the second solution.

Reflection has some level of type safety, but it is slow, when you have a lot of data, the performance is an issue, with dictionary , the type safety is worse, but the performance will be better(theory , not tested), With your problem, type safety is a fundamental issue which you just need to either use defensive coding and have better error handling, or train the API user to play by the rule, I don't think the type safety reflection give you will do much.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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