Browse Source

Merge pull request #23435 from abpframework/Mapperly-Map-Collection

Enhance object mapping capabilities by adding collection mapping support.
pull/23443/head
Engincan VESKE 6 months ago
committed by GitHub
parent
commit
bce846e88f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 136
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
  2. 135
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs
  3. 85
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs
  4. 31
      framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs

136
framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs

@ -1,5 +1,9 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
@ -19,6 +23,8 @@ public class MapperlyAutoObjectMappingProvider<TContext> : MapperlyAutoObjectMap
public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
{
protected static readonly ConcurrentDictionary<string, Func<object, object, object, object?>> MapCache = new();
protected IServiceProvider ServiceProvider { get; }
public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider)
@ -28,6 +34,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
public virtual TDestination Map<TSource, TDestination>(object source)
{
if (TryToMapCollection<TSource, TDestination>((TSource)source, default, out var collectionResult))
{
return collectionResult;
}
var mapper = ServiceProvider.GetService<IAbpMapperlyMapper<TSource, TDestination>>();
if (mapper != null)
{
@ -54,6 +65,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
public virtual TDestination Map<TSource, TDestination>(TSource source, TDestination destination)
{
if (TryToMapCollection<TSource, TDestination>(source, destination, out var collectionResult))
{
return collectionResult;
}
var mapper = ServiceProvider.GetService<IAbpMapperlyMapper<TSource, TDestination>>();
if (mapper != null)
{
@ -80,6 +96,126 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
$" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper<TSource, TDestination>))} was found");
}
protected virtual bool TryToMapCollection<TSource, TDestination>(TSource source, TDestination? destination, out TDestination collectionResult)
{
if (!ObjectMappingHelper.IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
{
collectionResult = default!;
return false;
}
var mapperType = typeof(IAbpMapperlyMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType);
var mapper = ServiceProvider.GetService(mapperType);
if (mapper == null)
{
mapperType = typeof(IAbpReverseMapperlyMapper<,>).MakeGenericType(destinationArgumentType, sourceArgumentType);
mapper = ServiceProvider.GetService(mapperType);
if (mapper == null)
{
//skip, no specific mapper
collectionResult = default!;
return false;
}
}
var invoker = MapCache.GetOrAdd(
$"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}",
_ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null));
var sourceList = source!.As<IList>();
var result = definitionGenericType.IsGenericType
? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As<IList>()
: Array.CreateInstance(destinationArgumentType, sourceList.Count);
if (destination != null && !destination.GetType().IsArray)
{
//Clear destination collection if destination not an array, We won't change array just same behavior as AutoMapper.
destination.As<IList>().Clear();
}
for (var i = 0; i < sourceList.Count; i++)
{
var invokeResult = destination == null
? invoker(this, sourceList[i]!, null!)
: invoker(this, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!);
if (definitionGenericType.IsGenericType)
{
result.Add(invokeResult);
destination?.As<IList>().Add(invokeResult);
}
else
{
result[i] = invokeResult;
}
}
if (destination != null && destination.GetType().IsArray)
{
//Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper.
collectionResult = (TDestination)result;
return true;
}
//Return the destination if destination exists. The parameter reference equals with return object.
collectionResult = destination ?? (TDestination)result;
return true;
}
protected virtual Func<object, object, object, object?> CreateMapDelegate(
Type mapperType,
Type sourceArgumentType,
Type destinationArgumentType,
bool hasDestination)
{
var methods = typeof(MapperlyAutoObjectMappingProvider)
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(Map))
.Where(x =>
{
var parameters = x.GetParameters();
return (hasDestination || parameters.Length == 1) &&
(!hasDestination || parameters.Length == 2);
})
.ToList();
if (methods.Count == 0)
{
throw new AbpException($"Could not find a method named '{nameof(Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (methods.Count > 1)
{
throw new AbpException($"Found more than one method named '{nameof(Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
var method = methods[0].MakeGenericMethod(sourceArgumentType, destinationArgumentType);
var instanceParam = Expression.Parameter(typeof(object), "mapper");
var sourceParam = Expression.Parameter(typeof(object), "source");
var destinationParam = Expression.Parameter(typeof(object), "destination");
var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!);
var callParams = new List<Expression>
{
Expression.Convert(sourceParam, sourceArgumentType)
};
if (hasDestination)
{
callParams.Add(Expression.Convert(destinationParam, destinationArgumentType));
}
var call = Expression.Call(instanceCast, method, callParams);
var callConvert = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<object, object, object, object?>>(callConvert, instanceParam, sourceParam, destinationParam).Compile();
}
protected virtual ExtraPropertyDictionary GetExtraProperties<TDestination>(TDestination destination)
{
var extraProperties = new ExtraPropertyDictionary();

135
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Volo.Abp.DependencyInjection;
@ -25,7 +26,7 @@ public class DefaultObjectMapper<TContext> : DefaultObjectMapper, IObjectMapper<
public class DefaultObjectMapper : IObjectMapper, ITransientDependency
{
protected static ConcurrentDictionary<string, MethodInfo> MethodInfoCache { get; } = new ConcurrentDictionary<string, MethodInfo>();
protected static readonly ConcurrentDictionary<string, Func<object, object, object, object?>> MapCache = new();
public IAutoObjectMappingProvider AutoObjectMappingProvider { get; }
protected IServiceProvider ServiceProvider { get; }
@ -122,7 +123,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
protected virtual bool TryToMapCollection<TSource, TDestination>(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult)
{
if (!IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
if (!ObjectMappingHelper.IsCollectionGenericType<TSource, TDestination>(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType))
{
collectionResult = default!;
return false;
@ -137,46 +138,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
return false;
}
var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}";
var method = MethodInfoCache.GetOrAdd(
cacheKey,
_ =>
{
var methods = specificMapper
.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(IObjectMapper<object, object>.Map))
.Where(x =>
{
var parameters = x.GetParameters();
if (destination == null && parameters.Length != 1 ||
destination != null && parameters.Length != 2 ||
parameters[0].ParameterType != sourceArgumentType)
{
return false;
}
return destination == null || parameters[1].ParameterType == destinationArgumentType;
})
.ToList();
if (methods.IsNullOrEmpty())
{
throw new AbpException($"Could not find a method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" +
$" in the type '{mapperType}'.");
}
if (methods.Count > 1)
{
throw new AbpException($"Found more than one method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" +
$" in the type '{mapperType}'.");
}
return methods.First();
}
);
var invoker = MapCache.GetOrAdd(
$"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}",
_ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null));
var sourceList = source!.As<IList>();
var result = definitionGenericType.IsGenericType
@ -192,8 +156,8 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
for (var i = 0; i < sourceList.Count; i++)
{
var invokeResult = destination == null
? method.Invoke(specificMapper, new [] { sourceList[i] })!
: method.Invoke(specificMapper, new [] { sourceList[i], Activator.CreateInstance(destinationArgumentType)! })!;
? invoker(specificMapper, sourceList[i]!, null!)
: invoker(specificMapper, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!);
if (definitionGenericType.IsGenericType)
{
@ -218,61 +182,64 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
return true;
}
protected virtual bool IsCollectionGenericType<TSource, TDestination>(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType)
protected virtual Func<object, object, object, object?> CreateMapDelegate(
Type mapperType,
Type sourceArgumentType,
Type destinationArgumentType,
bool hasDestination)
{
sourceArgumentType = default!;
destinationArgumentType = default!;
definitionGenericType = default!;
if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) ||
(!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray))
{
return false;
}
var methods = mapperType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(x => x.Name == nameof(IObjectMapper<object, object>.Map))
.Where(x =>
{
var parameters = x.GetParameters();
if (!hasDestination && parameters.Length != 1 ||
hasDestination && parameters.Length != 2 ||
parameters[0].ParameterType != sourceArgumentType)
{
return false;
}
var supportedCollectionTypes = new[]
{
typeof(IEnumerable<>),
typeof(ICollection<>),
typeof(Collection<>),
typeof(IList<>),
typeof(List<>)
};
return !hasDestination || parameters[1].ParameterType == destinationArgumentType;
})
.ToList();
if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition()))
if (methods.Count == 0)
{
sourceArgumentType = typeof(TSource).GenericTypeArguments[0];
throw new AbpException($"Could not find a method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (typeof(TSource).IsArray)
if (methods.Count > 1)
{
sourceArgumentType = typeof(TSource).GetElementType()!;
throw new AbpException($"Found more than one method named '{nameof(IObjectMapper<object, object>.Map)}'" +
$" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" +
$" in the type '{mapperType}'.");
}
if (sourceArgumentType == default!)
{
return false;
}
var method = methods[0];
definitionGenericType = typeof(List<>);
if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition()))
{
destinationArgumentType = typeof(TDestination).GenericTypeArguments[0];
var instanceParam = Expression.Parameter(typeof(object), "mapper");
var sourceParam = Expression.Parameter(typeof(object), "source");
var destinationParam = Expression.Parameter(typeof(object), "destination");
if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) ||
typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>))
{
definitionGenericType = typeof(Collection<>);
}
}
var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!);
var callParams = new List<Expression>
{
Expression.Convert(sourceParam, sourceArgumentType)
};
if (typeof(TDestination).IsArray)
if (hasDestination)
{
destinationArgumentType = typeof(TDestination).GetElementType()!;
definitionGenericType = typeof(Array);
callParams.Add(Expression.Convert(destinationParam, destinationArgumentType));
}
return destinationArgumentType != default!;
var call = Expression.Call(instanceCast, method, callParams);
var callConvert = Expression.Convert(call, typeof(object));
return Expression.Lambda<Func<object, object, object, object?>>(callConvert, instanceParam, sourceParam, destinationParam).Compile();
}
protected virtual TDestination AutoMap<TSource, TDestination>(object source)

85
framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs

@ -0,0 +1,85 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace Volo.Abp.ObjectMapping;
public static class ObjectMappingHelper
{
private static readonly ConcurrentDictionary<(Type, Type), (Type sourceArgumentType, Type destinationArgumentType, Type definitionGenericType)?> Cache = new();
public static bool IsCollectionGenericType<TSource, TDestination>(
out Type sourceArgumentType,
out Type destinationArgumentType,
out Type definitionGenericType)
{
var cached = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), _ => IsCollectionGenericTypeInternal<TSource, TDestination>());
if (cached == null)
{
sourceArgumentType = destinationArgumentType = definitionGenericType = null!;
return false;
}
(sourceArgumentType, destinationArgumentType, definitionGenericType) = cached.Value;
return true;
}
private static (Type, Type, Type)? IsCollectionGenericTypeInternal<TSource, TDestination>()
{
Type sourceArgumentType = null!;
Type destinationArgumentType = null!;
Type definitionGenericType = null!;
if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) ||
(!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray))
{
return null;
}
var supportedCollectionTypes = new[]
{
typeof(IEnumerable<>),
typeof(ICollection<>),
typeof(Collection<>),
typeof(IList<>),
typeof(List<>)
};
if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition()))
{
sourceArgumentType = typeof(TSource).GenericTypeArguments[0];
}
if (typeof(TSource).IsArray)
{
sourceArgumentType = typeof(TSource).GetElementType()!;
}
if (sourceArgumentType == null)
{
return null;
}
definitionGenericType = typeof(List<>);
if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition()))
{
destinationArgumentType = typeof(TDestination).GenericTypeArguments[0];
if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) ||
typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>))
{
definitionGenericType = typeof(Collection<>);
}
}
if (typeof(TDestination).IsArray)
{
destinationArgumentType = typeof(TDestination).GetElementType()!;
definitionGenericType = typeof(Array);
}
return (sourceArgumentType, destinationArgumentType, definitionGenericType);
}
}

31
framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Mapperly.SampleClasses;
@ -41,6 +42,36 @@ public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest<MapperlyTestModul
dto.Id.ShouldNotBe(Guid.Empty);
}
[Fact]
public void Should_Map_Collection()
{
var dto = _objectMapper.Map<List<MyEntity>, List<MyEntityDto>>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
});
dto.Count.ShouldBe(2);
dto[0].Number.ShouldBe(42);
dto[1].Number.ShouldBe(43);
var dtoList = new List<MyEntityDto>();
{
new MyEntityDto() { Number = 44 };
new MyEntityDto() { Number = 45 };
}
dto = _objectMapper.Map<List<MyEntity>, List<MyEntityDto>>(new List<MyEntity>
{
new MyEntity { Number = 42 },
new MyEntity { Number = 43 }
}, dtoList);
dtoList.Count.ShouldBe(2);
dtoList[0].Number.ShouldBe(42);
dtoList[1].Number.ShouldBe(43);
}
[Fact]
public void Should_Map_Enum()
{

Loading…
Cancel
Save