1

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.

Tomasito
  • 1,864
  • 1
  • 20
  • 43
  • It is easy enough to write your own, but you would need to tell the Mapper the method name so it can use `GetValueTupleNames`. – NetMage Oct 25 '18 at 21:41

3 Answers3

1

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; }
}
Eric Damtoft
  • 1,353
  • 7
  • 13
  • Interestingly, this is a lot easier to do in the other direction because you could call `MethodBase.GetCurrentMethod()` and use it's return parameter to get the names – Eric Damtoft Oct 25 '18 at 20:00
  • I can not figure out how to use it with method returns IEnumerable<(..., ...)> – Tomasito Oct 25 '18 at 21:20
1

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 ValueTuples to Dto automatically:

var target = source.TupleMapper(typeof(DataAccessLayer), nameof(DataAccessLayer.Method)).To<Dto>().ToList();
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • I'll try with anonymous object. I'm trying to merge Dapper, (Some)Mapper to reduce hand code. It looks like this: many SP in DB->Dapper->Anonymous or Tuples->Magic->Dto where SP/Dto ratio is ~100/10 – Tomasito Oct 26 '18 at 12:21
0

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.

LeY
  • 659
  • 7
  • 21
  • The original question has a link pointing out how it is possible to get the runtime names and shows code that does it. – NetMage Oct 25 '18 at 21:41
  • @NetMage Yes, you can see the names, but you can't call ValueTuple."Name", not only syntactically impossible, even it is , the run time will not understand it , because there is no "Name" Property at all on a ValueTupe. That's why you have to use a nameMaping with correct order with ValueTuple.Item* – LeY Oct 25 '18 at 22:38
  • All it takes is a little Reflection and a mapping to `Item*`. See my answer. – NetMage Oct 25 '18 at 22:43
  • I think you misunderstand - you don't have to create a mapping, you can use reflection and build the mapping automatically. – NetMage Oct 25 '18 at 23:02
  • @NetMage i am not creating the map manually.I skipped the Utilities.GetValueTupleNames(typeof(DataAccessLayer), nameof(DataAccessLayer.Method))), which is the reflected map the user already know. – LeY Oct 26 '18 at 01:22
  • How is `nameMapping` created? Your ``ToDto` and `ToDto2` only handle one property? – NetMage Oct 26 '18 at 17:49