How to find the source property based on the name of a flattened property with AutoMapper

谁都会走 提交于 2019-12-04 02:50:41

For the time being, I wrote a helper class that can determine the originating property chain from a concatenated property chain. Ofcourse, this will become obsolete when AutoMapper gets a feature to do this kind of thing.

using System.Globalization;
using System.Reflection;

/// <summary>
///     Resolves concatenated property names back to their originating properties.
/// </summary>
/// <remarks>
///     An example of a concatenated property name is "ProductNameLength" where the originating
///     property would be "Product.Name.Length".
/// </remarks>
public static class ConcatenatedPropertyNameResolver
{
    private static readonly object mappingCacheLock = new object();
    private static readonly Dictionary<MappingCacheKey, string> mappingCache = new Dictionary<MappingCacheKey, string>();

    /// <summary>
    ///     Returns the nested name of the property the specified concatenated property
    ///     originates from.
    /// </summary>
    /// <param name="concatenatedPropertyName">The concatenated property name.</param>
    /// <typeparam name="TSource">The mapping source type.</typeparam>
    /// <typeparam name="TDestination">The mapping destination type.</typeparam>
    /// <returns>
    ///     The nested name of the originating property where each level is separated by a dot.
    /// </returns>
    public static string GetOriginatingPropertyName<TSource, TDestination>(string concatenatedPropertyName)
    {
        if (concatenatedPropertyName == null)
        {
            throw new ArgumentNullException("concatenatedPropertyName");
        }
        else if (concatenatedPropertyName.Length == 0)
        {
            throw new ArgumentException("Cannot be empty.", "concatenatedPropertyName");
        }

        lock (mappingCacheLock)
        {
            MappingCacheKey key = new MappingCacheKey(typeof(TSource), typeof(TDestination), concatenatedPropertyName);

            if (!mappingCache.ContainsKey(key))
            {
                BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;

                List<string> result = new List<string>();
                Type type = typeof(TSource);

                while (concatenatedPropertyName.Length > 0)
                {
                    IEnumerable<PropertyInfo> properties = type.GetProperties(bindingFlags).Where(
                        n => concatenatedPropertyName.StartsWith(n.Name)).ToList();

                    if (properties.Count() == 1)
                    {
                        string match = properties.First().Name;
                        result.Add(match);
                        concatenatedPropertyName = concatenatedPropertyName.Substring(match.Length);
                        type = type.GetProperty(match, bindingFlags).PropertyType;
                    }
                    else if (properties.Any())
                    {
                        throw new InvalidOperationException(
                            string.Format(
                                CultureInfo.InvariantCulture,
                                "Ambiguous properties found for {0} on type {1}: {2}.",
                                concatenatedPropertyName,
                                typeof(TSource).FullName,
                                string.Join(", ", properties.Select(n => n.Name))));
                    }
                    else
                    {
                        throw new InvalidOperationException(
                            string.Format(
                                CultureInfo.InvariantCulture,
                                "No matching property found for {0} on type {1}.",
                                concatenatedPropertyName,
                                typeof(TSource).FullName));
                    }
                }

                mappingCache.Add(key, string.Join(".", result));
            }

            return mappingCache[key];
        }
    }

    /// <summary>
    ///     A mapping cache key.
    /// </summary>
    private struct MappingCacheKey
    {
        /// <summary>
        ///     The source type.
        /// </summary>
        public Type SourceType;

        /// <summary>
        ///     The destination type the source type maps to. 
        /// </summary>
        public Type DestinationType;

        /// <summary>
        ///     The name of the mapped property.
        /// </summary>
        public string MappedPropertyName;

        /// <summary>
        ///     Initializes a new instance of the <see cref="MappingCacheKey"/> class.
        /// </summary>
        /// <param name="sourceType">The source type.</param>
        /// <param name="destinationType">The destination type the source type maps to.</param>
        /// <param name="mappedPropertyName">The name of the mapped property.</param>
        public MappingCacheKey(Type sourceType, Type destinationType, string mappedPropertyName)
        {
            SourceType = sourceType;
            DestinationType = destinationType;
            MappedPropertyName = mappedPropertyName;
        }
    }
}

Here's a usage example:

class TestEntity
{
    public Node Root {get; set;}
}

class Node
{
    public string Leaf {get; set;}
}

class TestFlattenedEntity
{
    public string RootLeaf {get; set;}
}

string result = ConcatenatedPropertyNameResolver.GetOriginatingPropertyName<TestEntity, TestFlattenedEntity>("RootLeaf");

Assert.AreEqual("Root.Leaf", result);

I ran into a similar need with AutoMapper. Here is the solution that I can up with. I have only tested this against very simple mappings. Mainly one class to another with only properties being used (basically the default behavior of Mapper.CreateMap. I am assuming that there is only one mapping, so I use First instead of iterating over the collections.

    private MemberInfo getSource(Type destinationType, string destinationPropertyname)
    {
        TypeMap map = Mapper.GetAllTypeMaps().Where(m => m.DestinationType.Equals(destinationType)).First();

        IEnumerable<PropertyMap> properties = map.GetPropertyMaps().Where(p => p.DestinationProperty.Name.Equals(destinationPropertyname, StringComparison.CurrentCultureIgnoreCase));

        PropertyMap sourceProperty = properties.First();

        IMemberGetter mg = sourceProperty.GetSourceValueResolvers().Cast<IMemberGetter>().First();

        return mg.MemberInfo;
    }

I've been looking into the same problem, and came up with the following code snippet. It comes up with the unflattened property chain from AutoMapper. I got some inspiration from sgriffinusa's solution.

using System.Linq;
using System.Reflection;
using AutoMapper;

public static class TypeMapExtensions
{
    public static MemberInfo[] TryGetSourceProperties(this TypeMap @this, string propertyName)
    {
        if (@this != null)
        {
            var propertyMap = @this.GetPropertyMaps()
                .Where(p => p.DestinationProperty.Name == propertyName).FirstOrDefault();

            if (propertyMap != null)
            {
                var sourceProperties = propertyMap.GetSourceValueResolvers().OfType<IMemberGetter>();
                if (sourceProperties.Any())
                    return sourceProperties.Select(x => x.MemberInfo).ToArray();
            }
        }
        return null;
    }

    /// <summary>
    /// Trys to retrieve a source property name, given a destination property name. Only handles simple property mappings, and flattened properties.
    /// </summary>
    public static string TryGetSourcePropertyName(this TypeMap @this, string propertyName)
    {
        var members = TryGetSourceProperties(@this, propertyName);
        return (members == null) ? null : string.Join(".", members.Select(x => x.Name).ToArray());
    }
}

And you get the desired TypeMap using:

Mapper.FindTypeMapFor<TSource, TDestination>();

with ValueInjecter you could do:

var trails = TrailFinder.GetTrails(flatPropertyName, target.GetType().GetInfos(), t => true);

target is the unflat object (we need to pass the collection of PropertyInfo)

trails will be a list of strings so

var result = string.join(".",trails.ToArray());
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!