diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index c12b260921..40efdd7ee6 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -320,9 +320,22 @@ public class MyCustomUserMapper : IObjectMapper, ITransientDepend } ```` -ABP automatically discovers and registers the `MyCustomUserMapper` and it is automatically used whenever you use the `IObjectMapper` to map `User` to `UserDto`. - -A single class may implement more than one `IObjectMapper` each for a different object pairs. +ABP automatically discovers and registers the `MyCustomUserMapper` and it is automatically used whenever you use the `IObjectMapper` to map `User` to `UserDto`. A single class may implement more than one `IObjectMapper` each for a different object pairs. > This approach is powerful since `MyCustomUserMapper` can inject any other service and use in the `Map` methods. +Once you implement `IObjectMapper`, ABP can automatically convert a collection of `User` objects to a collection of `UserDto` objects. The following generic collection types are supported: + +* `IEnumerable` +* `ICollection` +* `Collection` +* `IList` +* `List` +* `T[]` (array) + +**Example:** + +````csharp +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` 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..6a2ab27870 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -1,5 +1,11 @@ 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; namespace Volo.Abp.ObjectMapping; @@ -19,6 +25,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 +54,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 +99,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 +122,131 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return AutoMap(source, destination); } + protected virtual TDestination? TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination) + { + if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + { + return default; + } + + var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); + var specificMapper = serviceScope.ServiceProvider.GetService(mapperType); + if (specificMapper == null) + { + //skip, no specific mapper + return default; + } + + var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; + var method = MethodInfoCache.GetOrAdd( + cacheKey, + _ => + { + return specificMapper + .GetType() + .GetMethods() + .First(x => + x.Name == nameof(IObjectMapper.Map) && + x.GetParameters().Length == (destination == null ? 1 : 2) + ); + } + ); + + 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 + ? method.Invoke(specificMapper, new [] { sourceList[i] })! + : method.Invoke(specificMapper, new [] { 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. + return (TDestination)result; + } + + //Return the destination if destination exists. The parameter reference equals with return object. + return destination ?? (TDestination)result; + } + + protected virtual bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) + { + sourceArgumentType = default!; + destinationArgumentType = default!; + definitionGenericType = default!; + + if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || + (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) + { + return false; + } + + 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 == default!) + { + return false; + } + + 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 destinationArgumentType != default!; + } + 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..46f028596d 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,7 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.AutoMapper.SampleClasses; @@ -24,6 +27,109 @@ 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() + { + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination = new List() + { + new MyEntityDto2 { Number = 44 } + }; + var returnIEnumerable = _objectMapper.Map, IEnumerable>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIEnumerable.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIEnumerable).ShouldBeTrue(); + + // ICollection + _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnICollection = _objectMapper.Map, ICollection>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnICollection.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnICollection).ShouldBeTrue(); + + // Collection + _objectMapper.Map, Collection>(new Collection() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination2 = new Collection() + { + new MyEntityDto2 { Number = 44 } + }; + var returnCollection = _objectMapper.Map, Collection>( + new Collection() + { + new MyEntity { Number = 42 } + }, destination2); + returnCollection.First().Number.ShouldBe(43); + ReferenceEquals(destination2, returnCollection).ShouldBeTrue(); + + // IList + _objectMapper.Map, IList>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnIList = _objectMapper.Map, IList>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIList).ShouldBeTrue(); + + // List + _objectMapper.Map, List>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnList = _objectMapper.Map, List>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnList).ShouldBeTrue(); + + // Array + _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destinationArray = new MyEntityDto2[] + { + new MyEntityDto2 { Number = 40 } + }; + var returnArray = _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }, destinationArray); + + returnArray.First().Number.ShouldBe(43); + + // array should not be changed. Same as AutoMapper. + destinationArray.First().Number.ShouldBe(40); + ReferenceEquals(returnArray, destinationArray).ShouldBeFalse(); + } + [Fact] public void Should_Use_Destination_Object_Constructor_If_Available() {