diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d9885ddbd..679149e4e9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -144,6 +144,7 @@ + diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 78f7ae0011..7f992078e2 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -491,6 +491,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Timing.Tests", "test\Volo.Abp.Timing.Tests\Volo.Abp.Timing.Tests.csproj", "{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly", "src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj", "{AF556046-54CD-48BC-9740-3E926DB8B510}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly.Tests", "test\Volo.Abp.Mapperly.Tests\Volo.Abp.Mapperly.Tests.csproj", "{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1465,6 +1469,14 @@ Global {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Debug|Any CPU.Build.0 = Debug|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.ActiveCfg = Release|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.Build.0 = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.Build.0 = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1712,6 +1724,8 @@ Global {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA} {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {AF556046-54CD-48BC-9740-3E926DB8B510} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7} = {447C8A77-E5F0-4538-8687-7383196D04EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs new file mode 100644 index 0000000000..516e7b969f --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectMapping; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpAutoMapperServiceCollectionExtensions +{ + public static IServiceCollection AddMapperlyObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient() + ); + } + + public static IServiceCollection AddMapperlyObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient, MapperlyAutoObjectMappingProvider>() + ); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj new file mode 100644 index 0000000000..672936d4f7 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj @@ -0,0 +1,29 @@ + + + + + + + net9.0 + enable + Nullable + Volo.Abp.Mapperly + Volo.Abp.Mapperly + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs new file mode 100644 index 0000000000..14165be56e --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs @@ -0,0 +1,19 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Mapperly; + +public abstract class AbpMapperlyBase : IAbpMapperly, ITransientDependency +{ + public abstract TDestination Map(TSource source); + + public abstract void Map(TSource source, TDestination destination); + + public virtual void BeforeMap(TSource source) + { + + } + public virtual void AfterMap(TSource source, TDestination destination) + { + + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs new file mode 100644 index 0000000000..fa209810c2 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar +{ + protected override bool IsConventionalRegistrationDisabled(Type type) + { + return !type.GetInterfaces().Any(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition()) || + base.IsConventionalRegistrationDisabled(type); + } + + protected override List GetExposedServiceTypes(Type type) + { + var exposedServiceTypes = base.GetExposedServiceTypes(type); + var mapperlyInterfaces = type.GetInterfaces() + .Where(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition()).ToList(); + return exposedServiceTypes + .Union(mapperlyInterfaces) + .Distinct() + .ToList(); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs new file mode 100644 index 0000000000..60381163eb --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Auditing; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly; + +[DependsOn( + typeof(AbpObjectMappingModule), + typeof(AbpObjectExtendingModule), + typeof(AbpAuditingModule) +)] +public class AbpMapperlyModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpMapperlyConventionalRegistrar()); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs new file mode 100644 index 0000000000..8de26067ef --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs @@ -0,0 +1,12 @@ +namespace Volo.Abp.Mapperly; + +public interface IAbpMapperly +{ + TDestination Map(TSource source); + + void Map(TSource source, TDestination destination); + + void BeforeMap(TSource source); + + void AfterMap(TSource source, TDestination destination); +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs new file mode 100644 index 0000000000..705a1af72d --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Mapperly; + +[AttributeUsage(AttributeTargets.Class)] +public class MapExtraPropertiesAttribute : Attribute +{ + public MappingPropertyDefinitionChecks DefinitionChecks { get; set; } = MappingPropertyDefinitionChecks.Null; + + public string[]? IgnoredProperties { get; set; } + + public bool MapToRegularProperties { get; set; } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs new file mode 100644 index 0000000000..62b7346197 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Mapperly; + +public static class MapExtraPropertiesInvoker +{ + private delegate void MapMethodDelegate( + object targetInstance, + IHasExtraProperties source, + IHasExtraProperties destination, + ExtraPropertyDictionary destinationExtraProperty, + MappingPropertyDefinitionChecks? definitionChecks, + string[]? ignoredProperties, + bool mapToRegularProperties); + + private readonly static ConcurrentDictionary<(Type, Type), MapMethodDelegate> Cache = new(); + + private readonly static MethodInfo MethodDefinition = typeof(MapperlyAutoObjectMappingProvider).GetMethod("MapExtraProperties", BindingFlags.Instance | BindingFlags.NonPublic)!; + + public static void Invoke( + object targetInstance, + IHasExtraProperties source, + IHasExtraProperties destination, + ExtraPropertyDictionary destinationExtraProperty, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[]? ignoredProperties = null, + bool mapToRegularProperties = false) + { + var key = (typeof(TSource), typeof(TDestination)); + + var action = Cache.GetOrAdd(key, static key => + { + var genericMethod = MethodDefinition.MakeGenericMethod(key.Item1, key.Item2); + + var targetParam = Expression.Parameter(typeof(object), "target"); + var sourceParam = Expression.Parameter(typeof(IHasExtraProperties), "source"); + var destParam = Expression.Parameter(typeof(IHasExtraProperties), "destination"); + var checksParam = Expression.Parameter(typeof(MappingPropertyDefinitionChecks?), "checks"); + var destinationExtraPropertyParam = Expression.Parameter(typeof(ExtraPropertyDictionary), "destinationExtraProperty"); + var ignoredParam = Expression.Parameter(typeof(string[]), "ignored"); + var mapFlagParam = Expression.Parameter(typeof(bool), "mapFlag"); + + var instanceCast = Expression.Convert(targetParam, typeof(MapperlyAutoObjectMappingProvider)); + + var call = Expression.Call( + instanceCast, + genericMethod, + sourceParam, + destParam, + destinationExtraPropertyParam, + checksParam, + ignoredParam, + mapFlagParam + ); + + var lambda = Expression.Lambda( + call, + targetParam, sourceParam, destParam, destinationExtraPropertyParam, checksParam, ignoredParam, mapFlagParam + ); + + return lambda.Compile(); + }); + + action(targetInstance, source, destination, destinationExtraProperty, definitionChecks, ignoredProperties, mapToRegularProperties); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs new file mode 100644 index 0000000000..550590f269 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly; + +public class MapperlyAutoObjectMappingProvider : MapperlyAutoObjectMappingProvider, IAutoObjectMappingProvider +{ + public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } +} + +public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider +{ + protected IServiceProvider ServiceProvider { get; } + + public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public virtual TDestination Map(object source) + { + var mapper = ServiceProvider.GetService>(); + if (mapper != null) + { + mapper.BeforeMap((TSource)source); + var destination = mapper.Map((TSource)source); + TryMapExtraProperties(mapper, (TSource)source, destination, new ExtraPropertyDictionary()); + mapper.AfterMap((TSource)source, destination); + return destination; + } + + throw new AbpException($"No {nameof(IAbpMapperly)} mapper found for {typeof(TSource).FullName} to {typeof(TDestination).FullName}"); + } + + public virtual TDestination Map(TSource source, TDestination destination) + { + var mapper = ServiceProvider.GetService>(); + if (mapper != null) + { + mapper.BeforeMap(source); + var destinationExtraProperties = GetExtraProperties(destination); + mapper.Map(source, destination); + TryMapExtraProperties(mapper, source, destination, destinationExtraProperties); + mapper.AfterMap(source, destination); + return destination; + } + + throw new AbpException($"No {nameof(IAbpMapperly)} mapper found for {typeof(TSource).FullName} to {typeof(TDestination).FullName}"); + } + + protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) + { + var extraProperties = new ExtraPropertyDictionary(); + if (destination is not IHasExtraProperties hasExtraProperties) + { + return extraProperties; + } + + foreach (var property in hasExtraProperties.ExtraProperties) + { + extraProperties.Add(property.Key, property.Value); + } + return extraProperties; + } + + protected virtual void TryMapExtraProperties(IAbpMapperly mapper, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) + { + var mapToRegularPropertiesAttribute = mapper.GetType().GetSingleAttributeOrNull(); + if (mapToRegularPropertiesAttribute != null && + typeof(IHasExtraProperties).IsAssignableFrom(typeof(TDestination)) && + typeof(IHasExtraProperties).IsAssignableFrom(typeof(TSource))) + { + MapExtraPropertiesInvoker.Invoke(this, + source!.As(), + destination!.As(), + destinationExtraProperty, + mapToRegularPropertiesAttribute.DefinitionChecks, + mapToRegularPropertiesAttribute.IgnoredProperties, + mapToRegularPropertiesAttribute.MapToRegularProperties + ); + } + } + + protected virtual void MapExtraProperties( + IHasExtraProperties source, + IHasExtraProperties destination, + ExtraPropertyDictionary destinationExtraProperty, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[]? ignoredProperties = null, + bool mapToRegularProperties = false) + where TSource : IHasExtraProperties + where TDestination : IHasExtraProperties + { + var result = destinationExtraProperty.IsNullOrEmpty() + ? new Dictionary() + : new Dictionary(destinationExtraProperty); + + if (source.ExtraProperties != null && destination.ExtraProperties != null) + { + ExtensibleObjectMapper + .MapExtraPropertiesTo( + source.ExtraProperties, + result, + definitionChecks, + ignoredProperties + ); + } + + ObjectHelper.TrySetProperty(destination, x => x.ExtraProperties, () => new ExtraPropertyDictionary(result)); + if (mapToRegularProperties) + { + destination.SetExtraPropertiesToRegularProperties(); + } + } +} diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs index eae85f77d7..48a3d37916 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs @@ -188,7 +188,7 @@ public static class ExtensibleObjectMapper return false; } - if (definitionChecks != null) + if (definitionChecks != null && definitionChecks.Value != MappingPropertyDefinitionChecks.Null) { if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Source)) { diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs index 04668476a0..f717e97714 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs @@ -5,20 +5,25 @@ namespace Volo.Abp.ObjectExtending; [Flags] public enum MappingPropertyDefinitionChecks : byte { + /// + /// Same as Null, We need to use this in Attribute to avoid null checks. + /// + Null = 0, + /// /// No check. Copy all extra properties from the source to the destination. /// - None = 0, + None = 1 << 0, /// /// Copy the extra properties defined for the source class. /// - Source = 1, + Source = 1 << 1, /// /// Copy the extra properties defined for the destination class. /// - Destination = 2, + Destination = 1 << 2, /// /// Copy extra properties defined for both of the source and destination classes. diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs new file mode 100644 index 0000000000..9e5fe4f8cf --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectExtending.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace Mapperly; + +public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest +{ + private readonly Volo.Abp.ObjectMapping.IObjectMapper _objectMapper; + + public AbpAutoMapperExtensibleDtoExtensions_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42) + .SetProperty("ChildCount", 2) + .SetProperty("Sex", "male") + .SetProperty("CityName", "Adana"); + + var personDto = new ExtensibleTestPersonDto() + .SetProperty("ExistingDtoProperty", "existing-value"); + + _objectMapper.Map(person, personDto); + + personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes + personDto.GetProperty("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values + personDto.GetProperty("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination + personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes + } + + [Fact] + public void MapExtraProperties_Also_Should_Map_To_RegularProperties() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42); + + var personDto = new ExtensibleTestPersonWithRegularPropertiesDto() + .SetProperty("IsActive", true); + + _objectMapper.Map(person, personDto); + + //Defined in both classes + personDto.HasProperty("Name").ShouldBe(false); + personDto.Name.ShouldBe("John"); + + //Defined in both classes + personDto.HasProperty("Age").ShouldBe(false); + personDto.Age.ShouldBe(42); + + //Should not clear existing values + personDto.HasProperty("IsActive").ShouldBe(false); + personDto.IsActive.ShouldBe(true); + } + + [Fact(Skip = "Mapperly requires IHasExtraProperties.ExtraPropertyDictionary to be marked as nullable")] + public void MapExtraPropertiesTo_Should_Ignored_If_ExtraProperties_Is_Null() + { + var person = new ExtensibleTestPerson(); + person.SetExtraPropertiesAsNull(); + + var personDto = new ExtensibleTestPersonDto(); + personDto.SetExtraPropertiesAsNull(); + + Should.NotThrow(() => _objectMapper.Map(person, personDto)); + + person.ExtraProperties.ShouldBe(null); + personDto.ExtraProperties.ShouldBeEmpty(); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj new file mode 100644 index 0000000000..f58c4cf848 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + net9.0 + Volo.Abp.Mapperly.Tests + Volo.Abp.Mapperly.Tests + + + + + + + + + + diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs new file mode 100644 index 0000000000..0080517c36 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpAutoMapperModule_Specific_ObjectMapper_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Use_Specific_Object_Mapper_If_Registered() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + 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 Mapperly. + destinationArray.First().Number.ShouldBe(40); + ReferenceEquals(returnArray, destinationArray).ShouldBeFalse(); + } + + [Fact] + public void Specific_Object_Mapper_Should_Support_Multiple_IObjectMapper_Interfaces() + { + var myEntityDto2 = _objectMapper.Map(new MyEntity { Number = 42 }); + myEntityDto2.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var myEntity = _objectMapper.Map(new MyEntityDto2 { Number = 42 }); + myEntity.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntityDto2 { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + + [Fact] + public void Should_Use_Destination_Object_Constructor_If_Available() + { + var id = Guid.NewGuid(); + var dto = _objectMapper.Map(new MyEntity { Number = 42, Id = id }); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Destination_Object_MapFrom_Method_If_Available() + { + var id = Guid.NewGuid(); + var dto = new MyEntityDtoWithMappingMethods(); + _objectMapper.Map(new MyEntity { Number = 42, Id = id }, dto); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Create_New_Object() + { + var id = Guid.NewGuid(); + var entity = _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Map_Existing_Object() + { + var id = Guid.NewGuid(); + var entity = new MyEntity(); + _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }, entity); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs new file mode 100644 index 0000000000..f8d39985d8 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Riok.Mapperly.Abstractions; +using Shouldly; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class MyClass +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +public class MyClassDto +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +[Mapper] +public partial class MyClassMapper : AbpMapperlyBase +{ + public override partial MyClassDto Map(MyClass source); + + public override partial void Map(MyClass source, MyClassDto destination); + + public override void BeforeMap(MyClass source) + { + source.Name = "BeforeMap " + source.Name; + } + + public override void AfterMap(MyClass source, MyClassDto destination) + { + destination.Name = source.Name + " AfterMap"; + } +} + +public class AbpMapperlyBeforeAndAfterMethod_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpMapperlyBeforeAndAfterMethod_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void BeforeAndAfterMethods_Should_Be_Called_When_Mapping() + { + var myClass = new MyClass { Id = "1", Name = "Test" }; + + var myClassDto = _objectMapper.Map(myClass); + myClassDto.Name.ShouldBe("BeforeMap Test AfterMap"); + } +} 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 new file mode 100644 index 0000000000..5edad59b1b --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpMapperlyModule_Basic_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Replace_IAutoObjectMappingProvider() + { + Assert.True(ServiceProvider.GetRequiredService() is MapperlyAutoObjectMappingProvider); + } + + [Fact] + public void Should_Map_Objects_With_AutoMap_Attributes() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + dto.Number.ShouldBe(42); + } + + [Fact] + public void Should_Map_Enum() + { + var dto = _objectMapper.Map(MyEnum.Value3); + dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3 + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs new file mode 100644 index 0000000000..8ff5ca2eb1 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Mapperly; + +[DependsOn( + typeof(AbpMapperlyModule), + typeof(AbpObjectExtendingTestModule) +)] +public class MapperlyTestModule : AbpModule +{ + +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs new file mode 100644 index 0000000000..4cfd58a760 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class ObjectMapperExtensions_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public ObjectMapperExtensions_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Map_Objects_With_AutoMap_Attributes() + { + var dto = _objectMapper.Map( + new MyEntity + { + Number = 42 + } + ); + + dto.As().Number.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs new file mode 100644 index 0000000000..8d4666a184 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs @@ -0,0 +1,44 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectExtending.TestObjects; + +[Mapper] +public partial class MyEntityMapper : AbpMapperlyBase +{ + public override partial MyEntityDto Map(MyEntity source); + + public override partial void Map(MyEntity source, MyEntityDto destination); +} + +[Mapper] +public partial class MyEnumMapper : AbpMapperlyBase +{ + public override partial MyEnumDto Map(MyEnum source); + + public override void Map(MyEnum source, MyEnumDto destination) + { + destination = Map(source); + } +} + +[Mapper] +[MapExtraProperties(IgnoredProperties = ["CityName"])] +public partial class ExtensibleTestPersonMapper : AbpMapperlyBase +{ + public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source); + + public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonDto destination); +} + +[Mapper] +[MapExtraProperties(MapToRegularProperties = true)] +public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : AbpMapperlyBase +{ + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Name))] + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Age))] + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.IsActive))] + public override partial ExtensibleTestPersonWithRegularPropertiesDto Map(ExtensibleTestPerson source); + + public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonWithRegularPropertiesDto destination); +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs new file mode 100644 index 0000000000..f137e476f1 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntity +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs new file mode 100644 index 0000000000..7630b2d14e --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs new file mode 100644 index 0000000000..9d00eff9c4 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityDto2 +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs new file mode 100644 index 0000000000..bf8c774599 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs @@ -0,0 +1,43 @@ +using System; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly.SampleClasses; + +//TODO: Move tests to Volo.Abp.ObjectMapping test project +public class MyEntityDtoWithMappingMethods : IMapFrom, IMapTo +{ + public Guid Key { get; set; } + + public int No { get; set; } + + public MyEntityDtoWithMappingMethods() + { + + } + + public MyEntityDtoWithMappingMethods(MyEntity entity) + { + MapFrom(entity); + } + + public void MapFrom(MyEntity source) + { + Key = source.Id; + No = source.Number; + } + + MyEntity IMapTo.MapTo() + { + return new MyEntity + { + Id = Key, + Number = No + }; + } + + void IMapTo.MapTo(MyEntity destination) + { + destination.Id = Key; + destination.Number = No; + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs new file mode 100644 index 0000000000..cd73c98747 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs @@ -0,0 +1,39 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityToMyEntityDto2Mapper : IObjectMapper, IObjectMapper, ITransientDependency +{ + public MyEntityDto2 Map(MyEntity source) + { + return new MyEntityDto2 + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntityDto2 Map(MyEntity source, MyEntityDto2 destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } + + public MyEntity Map(MyEntityDto2 source) + { + return new MyEntity + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntity Map(MyEntityDto2 source, MyEntity destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..fe2b72300c --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Mapperly.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..40d28d0501 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Mapperly.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3, + Value +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs new file mode 100644 index 0000000000..9fd2c556fb --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyNotMappedDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index e916a379d2..83bbeb7bde 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -220,6 +220,7 @@ $projects = ( "framework/src/Volo.Abp.Ldap", "framework/src/Volo.Abp.Localization.Abstractions", "framework/src/Volo.Abp.MailKit", + "framework/src/Volo.Abp.Mapperly", "framework/src/Volo.Abp.Maui.Client", "framework/src/Volo.Abp.Localization", "framework/src/Volo.Abp.MemoryDb",