Browse Source

Enhance object mapping capabilities by adding collection mapping support and optimizing method invocation caching

pull/23435/head
maliming 10 months ago
parent
commit
6df1a02c15
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 136
      framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
  2. 122
      framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs
  3. 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 (!DefaultObjectMapper.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();

122
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; }
@ -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,11 +182,71 @@ 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)
{
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;
}
return !hasDestination || parameters[1].ParameterType == destinationArgumentType;
})
.ToList();
if (methods.Count == 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 (methods.Count > 1)
{
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}'.");
}
var method = methods[0];
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();
}
public static bool IsCollectionGenericType<TSource, TDestination>(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType)
{
sourceArgumentType = default!;
destinationArgumentType = default!;
definitionGenericType = default!;
sourceArgumentType = null!;
destinationArgumentType = null!;
definitionGenericType = null!;
if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) ||
(!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray))
@ -249,7 +273,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
sourceArgumentType = typeof(TSource).GetElementType()!;
}
if (sourceArgumentType == default!)
if (sourceArgumentType == null!)
{
return false;
}
@ -272,7 +296,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency
definitionGenericType = typeof(Array);
}
return destinationArgumentType != default!;
return destinationArgumentType != null!;
}
protected virtual TDestination AutoMap<TSource, TDestination>(object source)

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