diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 7685aa03ac..677649b79a 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/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 : MapperlyAutoObjectMap public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { + protected static readonly ConcurrentDictionary> MapCache = new(); + protected IServiceProvider ServiceProvider { get; } public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) @@ -28,6 +34,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider public virtual TDestination Map(object source) { + if (TryToMapCollection((TSource)source, default, out var collectionResult)) + { + return collectionResult; + } + var mapper = ServiceProvider.GetService>(); if (mapper != null) { @@ -54,6 +65,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider public virtual TDestination Map(TSource source, TDestination destination) { + if (TryToMapCollection(source, destination, out var collectionResult)) + { + return collectionResult; + } + var mapper = ServiceProvider.GetService>(); if (mapper != null) { @@ -80,6 +96,126 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper))} was found"); } + protected virtual bool TryToMapCollection(TSource source, TDestination? destination, out TDestination collectionResult) + { + if (!ObjectMappingHelper.IsCollectionGenericType(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(); + var result = definitionGenericType.IsGenericType + ? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As() + : 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().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().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 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.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>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); + } + protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) { var extraProperties = new ExtraPropertyDictionary(); diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index 30733b1e24..faf8c4e258 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/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 : DefaultObjectMapper, IObjectMapper< public class DefaultObjectMapper : IObjectMapper, ITransientDependency { - protected static ConcurrentDictionary MethodInfoCache { get; } = new ConcurrentDictionary(); + protected static readonly ConcurrentDictionary> MapCache = new(); public IAutoObjectMappingProvider AutoObjectMappingProvider { get; } protected IServiceProvider ServiceProvider { get; } @@ -122,7 +123,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency protected virtual bool TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult) { - if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + if (!ObjectMappingHelper.IsCollectionGenericType(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.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.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.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(); 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(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) + protected virtual Func 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.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.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.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.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>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); } protected virtual TDestination AutoMap(object source) diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs new file mode 100644 index 0000000000..cbfd33988b --- /dev/null +++ b/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( + out Type sourceArgumentType, + out Type destinationArgumentType, + out Type definitionGenericType) + { + var cached = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), _ => IsCollectionGenericTypeInternal()); + if (cached == null) + { + sourceArgumentType = destinationArgumentType = definitionGenericType = null!; + return false; + } + + (sourceArgumentType, destinationArgumentType, definitionGenericType) = cached.Value; + return true; + } + + private static (Type, Type, Type)? IsCollectionGenericTypeInternal() + { + 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); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs index 89861f6947..50a799e5a3 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs +++ b/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, List>(new List + { + 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(); + { + new MyEntityDto() { Number = 44 }; + new MyEntityDto() { Number = 45 }; + } + + dto = _objectMapper.Map, List>(new List + { + 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() {