From 80c83856f936a7344a284cb2e6e5cc6eea0e4a05 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 15 Aug 2023 21:29:34 +0800 Subject: [PATCH 1/8] Object2Object mapping enhancements. --- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 52 +++++++++++++++++++ ...apperModule_Specific_ObjectMapper_Tests.cs | 21 ++++++++ 2 files changed, 73 insertions(+) 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() { From e05d105247cbca8e9fa4fd9292fce7ef80e33a54 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 16 Aug 2023 09:19:48 +0800 Subject: [PATCH 2/8] Update DefaultObjectMapper.cs --- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 9751a4a063..17c821f3ce 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -7,7 +7,6 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using Volo.Abp.DependencyInjection; -using Volo.Abp.Reflection; namespace Volo.Abp.ObjectMapping; @@ -134,21 +133,27 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency 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) + var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceGenericTypeDefinition, destinationGenericTypeDefinition); + var specificMapper = serviceScope.ServiceProvider.GetService(mapperType); + if (specificMapper == 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 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 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)!})!); + ? method.Invoke(specificMapper, new [] { sourceItem })! + : method.Invoke(specificMapper, new [] { sourceItem, Activator.CreateInstance(destinationGenericTypeDefinition)! })!); } return (TDestination)result!; From 9d9fa7075fd6d5d0079d71412a87f816d355766b Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 16 Aug 2023 13:43:21 +0800 Subject: [PATCH 3/8] Support more collection types. --- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 88 ++++++++++++++++--- ...apperModule_Specific_ObjectMapper_Tests.cs | 40 +++++++-- 2 files changed, 111 insertions(+), 17 deletions(-) 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 17c821f3ce..21a001266a 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -124,16 +124,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency 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<>)) + if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { - //skip, not a collection return default; } - var sourceGenericTypeDefinition = typeof(TSource).GenericTypeArguments[0]; - var destinationGenericTypeDefinition = typeof(TDestination).GenericTypeArguments[0]; - var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceGenericTypeDefinition, destinationGenericTypeDefinition); + var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); var specificMapper = serviceScope.ServiceProvider.GetService(mapperType); if (specificMapper == null) { @@ -148,17 +144,87 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency x.GetParameters().Length == (destination == null ? 1 : 2)); }); - var result = Activator.CreateInstance(typeof(Collection<>).MakeGenericType(destinationGenericTypeDefinition))!.As(); - foreach (var sourceItem in (IEnumerable)source!) + var sourceList = source!.As(); + var result = definitionGenericType.IsGenericType + ? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As() + : Array.CreateInstance(destinationArgumentType, sourceList.Count); + + for (var i = 0; i < sourceList.Count; i++) { - result.Add(destination == null - ? method.Invoke(specificMapper, new [] { sourceItem })! - : method.Invoke(specificMapper, new [] { sourceItem, Activator.CreateInstance(destinationGenericTypeDefinition)! })!); + 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); + } + else + { + result[i] = invokeResult; + } } return (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 af94bac5b8..9eb537cec5 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,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.Extensions.DependencyInjection; using Shouldly; @@ -29,20 +30,47 @@ public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest [Fact] public void Specific_Object_Mapper_Should_Be_Used_For_Collections_If_Registered() { - var dtos = _objectMapper.Map, ICollection>(new List() + _objectMapper.Map, IEnumerable>(new List() { new MyEntity { Number = 42 } - }); - dtos.First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. - dtos = _objectMapper.Map, ICollection>(new List() + _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, Collection>(new Collection() + { + new MyEntity { Number = 42 } + }, new Collection() //When mapping to an existing collection, the destination collection is cleared first + { + new MyEntityDto2 { Number = 44 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, IList>(new List() + { + new MyEntity { Number = 42 } + }, new Collection() //When mapping to an existing collection, the destination collection is cleared first + { + new MyEntityDto2 { Number = 44 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, List>(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. + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }, new MyEntityDto2[] //When mapping to an existing collection, the destination collection is cleared first + { + new MyEntityDto2 { Number = 44 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. } [Fact] From 7614573a181d211374eed4216d602b77645112fe Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 16 Aug 2023 14:21:10 +0800 Subject: [PATCH 4/8] Add `Collection Mapping` document. --- docs/en/Object-To-Object-Mapping.md | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index c12b260921..f184468bbc 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -326,3 +326,51 @@ A single class may implement more than one `IObjectMapper > This approach is powerful since `MyCustomUserMapper` can inject any other service and use in the `Map` methods. +#### Collection Mapping + +You only requires configuration of element types, not of any array or list type that might be used. + +For example, we might have a simple source and destination type: + +````csharp +public class Source +{ + public int Value { get; set; } +} + +public class Destination +{ + public int Value { get; set; } +} +```` + +All the basic generic collection types are supported: + +````csharp +IObjectMapper _objectMapper //inject it. + +var sources = new[] +{ + new Source { Value = 5 }, + new Source { Value = 6 }, + new Source { Value = 7 } +}; + +IEnumerable ienumerableDest = _objectMapper.Map>(sources); +ICollection icollectionDest = _objectMapper.Map>(sources); +ICollection collectionDest = _objectMapper.Map>(sources); +IList ilistDest = _objectMapper.Map>(sources); +List listDest = _objectMapper.Map>(sources); +Destination[] arrayDest = _objectMapper.Map(sources); +```` + +To be specific, the source collection types supported include: + +* `IEnumerable` +* `ICollection` +* `Collection` +* `IList` +* `List` +* `T[]` + +> When mapping to an existing collection, the destination collection is cleared first. From 7211245c8521090861a289f19b09db3e4a48f9f0 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 16 Aug 2023 14:28:36 +0800 Subject: [PATCH 5/8] Update Object-To-Object-Mapping.md --- docs/en/Object-To-Object-Mapping.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index f184468bbc..f3eab7de67 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -335,12 +335,12 @@ For example, we might have a simple source and destination type: ````csharp public class Source { - public int Value { get; set; } + public int Value { get; set; } } public class Destination { - public int Value { get; set; } + public int Value { get; set; } } ```` From e70a0ac86a10bfd0dc8bf5df3f0cb6422cfea0e5 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 16 Aug 2023 20:29:15 +0800 Subject: [PATCH 6/8] Clear destination collection. --- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 16 +++- ...apperModule_Specific_ObjectMapper_Tests.cs | 79 ++++++++++++++++--- 2 files changed, 83 insertions(+), 12 deletions(-) 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 21a001266a..4286672a7e 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -149,6 +149,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency ? 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 @@ -158,6 +164,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency if (definitionGenericType.IsGenericType) { result.Add(invokeResult); + destination?.As().Add(invokeResult); } else { @@ -165,7 +172,14 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency } } - return (TDestination)result!; + 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) 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 9eb537cec5..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 @@ -30,47 +30,104 @@ public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest [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 } - }, new Collection() //When mapping to an existing collection, the destination collection is cleared first + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination2 = new Collection() { new MyEntityDto2 { Number = 44 } - }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + }; + 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 } - }, new Collection() //When mapping to an existing collection, the destination collection is cleared first - { - new MyEntityDto2 { Number = 44 } }).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 } - }, new List() //When mapping to an existing collection, the destination collection is cleared first - { - new MyEntityDto2 { Number = 44 } }).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 } - }, new MyEntityDto2[] //When mapping to an existing collection, the destination collection is cleared first - { - new MyEntityDto2 { Number = 44 } }).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] From b8ce549f93b04163437ec2191073ddb0c8b3a4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 18 Aug 2023 10:12:52 +0300 Subject: [PATCH 7/8] Update Object-To-Object-Mapping.md --- docs/en/Object-To-Object-Mapping.md | 53 +++++------------------------ 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index f3eab7de67..40efdd7ee6 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -320,57 +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. -#### Collection Mapping - -You only requires configuration of element types, not of any array or list type that might be used. - -For example, we might have a simple source and destination type: - -````csharp -public class Source -{ - public int Value { get; set; } -} - -public class Destination -{ - public int Value { get; set; } -} -```` - -All the basic generic collection types are supported: - -````csharp -IObjectMapper _objectMapper //inject it. - -var sources = new[] -{ - new Source { Value = 5 }, - new Source { Value = 6 }, - new Source { Value = 7 } -}; - -IEnumerable ienumerableDest = _objectMapper.Map>(sources); -ICollection icollectionDest = _objectMapper.Map>(sources); -ICollection collectionDest = _objectMapper.Map>(sources); -IList ilistDest = _objectMapper.Map>(sources); -List listDest = _objectMapper.Map>(sources); -Destination[] arrayDest = _objectMapper.Map(sources); -```` - -To be specific, the source collection types supported include: +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[]` +* `T[]` (array) + +**Example:** -> When mapping to an existing collection, the destination collection is cleared first. +````csharp +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` From d31827579d91cdb000db98da89157af6a3c54ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 18 Aug 2023 10:20:15 +0300 Subject: [PATCH 8/8] Refactor styling --- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 4286672a7e..6a2ab27870 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -137,12 +137,20 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency 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 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