using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using Unosquare.Swan.Abstractions; namespace Unosquare.Swan.Components { /// /// Represents an AutoMapper-like object to map from one object type /// to another using defined properties map or using the default behaviour /// to copy same named properties from one object to another. /// /// The extension methods like CopyPropertiesTo use the default behaviour. /// /// /// The following code explains how to map an object's properties into an instance of type T. /// /// using Unosquare.Swan /// /// class Example /// { /// class Person /// { /// public string Name { get; set; } /// public int Age { get; set; } /// } /// /// static void Main() /// { /// var obj = new { Name = "Søren", Age = 42 }; /// /// var person = Runtime.ObjectMapper.Map<Person>(obj); /// } /// } /// /// The following code explains how to explicitly map certain properties. /// /// using Unosquare.Swan /// /// class Example /// { /// class User /// { /// public string Name { get; set; } /// public Role Role { get; set; } /// } /// /// public class Role /// { /// public string Name { get; set; } /// } /// /// class UserDto /// { /// public string Name { get; set; } /// public string Role { get; set; } /// } /// /// static void Main() /// { /// // create a User object /// var person = /// new User { Name = "Phillip", Role = new Role { Name = "Admin" } }; /// /// // create an Object Mapper /// var mapper = new ObjectMapper(); /// /// // map the User's Role.Name to UserDto's Role /// mapper.CreateMap<User, UserDto>() /// .MapProperty(d => d.Role, x => x.Role.Name); /// /// // apply the previous map and retrieve a UserDto object /// var destination = mapper.Map<UserDto>(person); /// } /// } /// /// public class ObjectMapper { private readonly List _maps = new List(); /// /// Copies the specified source. /// /// The source. /// The target. /// The properties to copy. /// The ignore properties. /// /// Copied properties count. /// /// /// source /// or /// target. /// public static Int32 Copy( Object source, Object target, String[] propertiesToCopy = null, String[] ignoreProperties = null) { if(source == null) { throw new ArgumentNullException(nameof(source)); } if(target == null) { throw new ArgumentNullException(nameof(target)); } return Copy( target, propertiesToCopy, ignoreProperties, GetSourceMap(source)); } /// /// Copies the specified source. /// /// The source. /// The target. /// The properties to copy. /// The ignore properties. /// /// Copied properties count. /// /// /// source /// or /// target. /// public static Int32 Copy( IDictionary source, Object target, String[] propertiesToCopy = null, String[] ignoreProperties = null) { if(source == null) { throw new ArgumentNullException(nameof(source)); } if(target == null) { throw new ArgumentNullException(nameof(target)); } return Copy( target, propertiesToCopy, ignoreProperties, source.ToDictionary( x => x.Key.ToLowerInvariant(), x => new TypeValuePair(typeof(Object), x.Value))); } /// /// Creates the map. /// /// The type of the source. /// The type of the destination. /// /// An object map representation of type of the destination property /// and type of the source property. /// /// /// You can't create an existing map /// or /// Types doesn't match. /// public ObjectMap CreateMap() { if(this._maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) { throw new InvalidOperationException("You can't create an existing map"); } IEnumerable sourceType = Runtime.PropertyTypeCache.RetrieveAllProperties(true); IEnumerable destinationType = Runtime.PropertyTypeCache.RetrieveAllProperties(true); PropertyInfo[] intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); if(intersect.Any() == false) { throw new InvalidOperationException("Types doesn't match"); } ObjectMap map = new ObjectMap(intersect); this._maps.Add(map); return map; } /// /// Maps the specified source. /// /// The type of the destination. /// The source. /// if set to true [automatic resolve]. /// /// A new instance of the map. /// /// source. /// You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}. public TDestination Map(Object source, Boolean autoResolve = true) { if(source == null) { throw new ArgumentNullException(nameof(source)); } TDestination destination = Activator.CreateInstance(); IObjectMap map = this._maps .FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination)); if(map != null) { foreach(KeyValuePair> property in map.Map) { Object finalSource = property.Value.Aggregate(source, (current, sourceProperty) => sourceProperty.GetValue(current)); property.Key.SetValue(destination, finalSource); } } else { if(!autoResolve) { throw new InvalidOperationException( $"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}"); } // Missing mapping, try to use default behavior _ = Copy(source, destination); } return destination; } private static Int32 Copy( Object target, IEnumerable propertiesToCopy, IEnumerable ignoreProperties, Dictionary sourceProperties) { // Filter properties IEnumerable requiredProperties = propertiesToCopy? .Where(p => !String.IsNullOrWhiteSpace(p)) .Select(p => p.ToLowerInvariant()); IEnumerable ignoredProperties = ignoreProperties? .Where(p => !String.IsNullOrWhiteSpace(p)) .Select(p => p.ToLowerInvariant()); IEnumerable properties = Runtime.PropertyTypeCache .RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite); return properties .Select(x => x.Name) .Distinct() .ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x)) .Where(x => sourceProperties.Keys.Contains(x.Key)) .When(() => requiredProperties != null, q => q.Where(y => requiredProperties.Contains(y.Key))) .When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties.Contains(y.Key))) .ToDictionary(x => x.Value, x => sourceProperties[x.Key]) .Sum(x => TrySetValue(x, target) ? 1 : 0); } private static Boolean TrySetValue(KeyValuePair property, Object target) { try { SetValue(property, target); return true; } catch { // swallow } return false; } private static void SetValue(KeyValuePair property, Object target) { if(property.Value.Type.GetTypeInfo().IsEnum) { property.Key.SetValue(target, Enum.ToObject(property.Key.PropertyType, property.Value.Value)); return; } if(!property.Value.Type.IsValueType() && property.Key.PropertyType == property.Value.Type) { property.Key.SetValue(target, GetValue(property.Value.Value, property.Key.PropertyType)); return; } if(property.Key.PropertyType == typeof(Boolean)) { property.Key.SetValue(target, Convert.ToBoolean(property.Value.Value)); return; } _ = property.Key.TrySetBasicType(property.Value.Value, target); } private static Object GetValue(Object source, Type targetType) { if(source == null) { return null; } Object target = null; source.CreateTarget(targetType, false, ref target); switch(source) { case String _: target = source; break; case IList sourceList when target is Array targetArray: for(Int32 i = 0; i < sourceList.Count; i++) { try { targetArray.SetValue( sourceList[i].GetType().IsValueType() ? sourceList[i] : sourceList[i].CopyPropertiesToNew(), i); } catch { // ignored } } break; case IList sourceList when target is IList targetList: MethodInfo addMethod = targetType.GetMethods() .FirstOrDefault( m => m.Name.Equals(Formatters.Json.AddMethodName) && m.IsPublic && m.GetParameters().Length == 1); if(addMethod == null) { return target; } foreach(Object item in sourceList) { try { _ = targetList.Add(item.GetType().IsValueType() ? item : item.CopyPropertiesToNew()); } catch { // ignored } } break; default: _ = source.CopyPropertiesTo(target); break; } return target; } private static Dictionary GetSourceMap(Object source) { // select distinct properties because they can be duplicated by inheritance PropertyInfo[] sourceProperties = Runtime.PropertyTypeCache .RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead) .ToArray(); return sourceProperties .Select(x => x.Name) .Distinct() .ToDictionary( x => x.ToLowerInvariant(), x => new TypeValuePair(sourceProperties.First(y => y.Name == x).PropertyType, sourceProperties.First(y => y.Name == x).GetValue(source))); } internal class TypeValuePair { public TypeValuePair(Type type, Object value) { this.Type = type; this.Value = value; } public Type Type { get; } public Object Value { get; } } internal class PropertyInfoComparer : IEqualityComparer { public Boolean Equals(PropertyInfo x, PropertyInfo y) => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType; public Int32 GetHashCode(PropertyInfo obj) => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode(); } } }