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 7048d183c7..9751a4a063 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -1,6 +1,13 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; using Volo.Abp.DependencyInjection; +using Volo.Abp.Reflection; namespace Volo.Abp.ObjectMapping; @@ -19,6 +26,8 @@ public class DefaultObjectMapper : DefaultObjectMapper, IObjectMapper< public class DefaultObjectMapper : IObjectMapper, ITransientDependency { + protected static ConcurrentDictionary MethodInfoCache { get; } = new ConcurrentDictionary(); + public IAutoObjectMappingProvider AutoObjectMappingProvider { get; } protected IServiceProvider ServiceProvider { get; } @@ -46,6 +55,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency { return specificMapper.Map(source); } + + var result = TryToMapCollection(scope, source, default); + if (result != null) + { + return result; + } } if (source is IMapTo mapperSource) @@ -85,6 +100,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency { return specificMapper.Map(source, destination); } + + var result = TryToMapCollection(scope, source, destination); + if (result != null) + { + return result; + } } if (source is IMapTo mapperSource) @@ -102,6 +123,37 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return AutoMap(source, destination); } + protected virtual TDestination? TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination) + { + if (!typeof(TSource).IsGenericType || typeof(TSource).GetGenericTypeDefinition() != typeof(ICollection<>) || + !typeof(TDestination).IsGenericType || typeof(TDestination).GetGenericTypeDefinition() != typeof(ICollection<>)) + { + //skip, not a collection + return default; + } + + var sourceGenericTypeDefinition = typeof(TSource).GenericTypeArguments[0]; + var destinationGenericTypeDefinition = typeof(TDestination).GenericTypeArguments[0]; + var specificGenericTypeDefinitionMapper = serviceScope.ServiceProvider.GetService(typeof(IObjectMapper<,>).MakeGenericType(sourceGenericTypeDefinition, destinationGenericTypeDefinition)); + if (specificGenericTypeDefinitionMapper == null) + { + //skip, no specific mapper + return default; + } + + var cacheKey = $"{specificGenericTypeDefinitionMapper.GetType().FullName}-{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; + var method = MethodInfoCache.GetOrAdd(cacheKey, x => specificGenericTypeDefinitionMapper.GetType().GetMethods().First(m => m.Name == nameof(IObjectMapper.Map) && m.GetParameters().Length == (destination == null ? 1 : 2))); + var result = Activator.CreateInstance(typeof(Collection<>).MakeGenericType(destinationGenericTypeDefinition))!.As(); + foreach (var sourceItem in (IEnumerable)source!) + { + result.Add(destination == null + ? method.Invoke(specificGenericTypeDefinitionMapper, new [] {sourceItem})! + : method.Invoke(specificGenericTypeDefinitionMapper, new [] {sourceItem, Activator.CreateInstance(destinationGenericTypeDefinition)!})!); + } + + return (TDestination)result!; + } + protected virtual TDestination AutoMap(object source) { return AutoObjectMappingProvider.Map(source); diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs index 3b1e3553bd..af94bac5b8 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.AutoMapper.SampleClasses; @@ -24,6 +26,25 @@ public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest dto.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. } + [Fact] + public void Specific_Object_Mapper_Should_Be_Used_For_Collections_If_Registered() + { + var dtos = _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }); + dtos.First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + dtos = _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }, new List() //When mapping to an existing collection, the destination collection is cleared first + { + new MyEntityDto2 { Number = 44 } + }); + dtos.First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + [Fact] public void Should_Use_Destination_Object_Constructor_If_Available() {