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