diff --git a/Directory.Packages.props b/Directory.Packages.props
index 361cbacd9c..c57b8c8f07 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -145,6 +145,7 @@
+
diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 4167492d16..e304510e48 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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.MySQL.Pomelo", "src\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo.csproj", "{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}"
EndProject
Global
@@ -1467,6 +1471,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
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1718,6 +1730,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}
{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
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..7c085a146e
--- /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/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs
new file mode 100644
index 0000000000..028a72bb67
--- /dev/null
+++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs
@@ -0,0 +1,27 @@
+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(IAbpMapperlyMapper<,>) == 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(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition() ||
+ typeof(IAbpReverseMapperlyMapper<,>) == x.GetGenericTypeDefinition()));
+ 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/IAbpMapperlyMapper.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs
new file mode 100644
index 0000000000..eac0f6644c
--- /dev/null
+++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs
@@ -0,0 +1,23 @@
+namespace Volo.Abp.Mapperly;
+
+public interface IAbpMapperlyMapper
+{
+ TDestination Map(TSource source);
+
+ void Map(TSource source, TDestination destination);
+
+ void BeforeMap(TSource source);
+
+ void AfterMap(TSource source, TDestination destination);
+}
+
+public interface IAbpReverseMapperlyMapper : IAbpMapperlyMapper
+{
+ TSource ReverseMap(TDestination destination);
+
+ void ReverseMap(TDestination destination, TSource source);
+
+ void BeforeReverseMap(TDestination destination);
+
+ void AfterReverseMap(TDestination destination, TSource source);
+}
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/MapperBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs
new file mode 100644
index 0000000000..39d9dce995
--- /dev/null
+++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs
@@ -0,0 +1,32 @@
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Mapperly;
+
+public abstract class MapperBase : IAbpMapperlyMapper, 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)
+ {
+ }
+}
+
+public abstract class TwoWayMapperBase : MapperBase, IAbpReverseMapperlyMapper
+{
+ public abstract TSource ReverseMap(TDestination destination);
+
+ public abstract void ReverseMap(TDestination destination, TSource source);
+
+ public virtual void BeforeReverseMap(TDestination destination)
+ {
+ }
+
+ public virtual void AfterReverseMap(TDestination destination, TSource source)
+ {
+ }
+}
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..7685aa03ac
--- /dev/null
+++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
@@ -0,0 +1,145 @@
+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;
+using Volo.Abp.Reflection;
+
+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.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary());
+ mapper.AfterMap((TSource)source, destination);
+ return destination;
+ }
+
+ var reverseMapper = ServiceProvider.GetService>();
+ if (reverseMapper != null)
+ {
+ reverseMapper.BeforeReverseMap((TSource)source);
+ var destination = reverseMapper.ReverseMap((TSource)source);
+ TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination));
+ reverseMapper.AfterReverseMap((TSource)source, destination);
+ return destination;
+ }
+
+ throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperlyMapper))} or" +
+ $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper))} was found");
+ }
+
+ 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.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties);
+ mapper.AfterMap(source, destination);
+ return destination;
+ }
+
+ var reverseMapper = ServiceProvider.GetService>();
+ if (reverseMapper != null)
+ {
+ reverseMapper.BeforeReverseMap(source);
+ var destinationExtraProperties = GetExtraProperties(destination);
+ reverseMapper.ReverseMap(source, destination);
+ TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties);
+ reverseMapper.AfterReverseMap(source, destination);
+ return destination;
+ }
+
+ throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperlyMapper))} or" +
+ $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper))} was found");
+ }
+
+ 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(MapExtraPropertiesAttribute? mapExtraPropertiesAttribute, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty)
+ {
+ if (mapExtraPropertiesAttribute != null &&
+ typeof(IHasExtraProperties).IsAssignableFrom(typeof(TDestination)) &&
+ typeof(IHasExtraProperties).IsAssignableFrom(typeof(TSource)))
+ {
+ MapExtraProperties(
+ source!.As(),
+ destination!.As(),
+ destinationExtraProperty,
+ mapExtraPropertiesAttribute.DefinitionChecks,
+ mapExtraPropertiesAttribute.IgnoredProperties,
+ mapExtraPropertiesAttribute.MapToRegularProperties
+ );
+ }
+ }
+ protected virtual void MapExtraProperties(
+ IHasExtraProperties source,
+ IHasExtraProperties destination,
+ ExtraPropertyDictionary destinationExtraProperty,
+ MappingPropertyDefinitionChecks? definitionChecks = null,
+ string[]? ignoredProperties = null,
+ bool mapToRegularProperties = false)
+ {
+ var result = destinationExtraProperty.IsNullOrEmpty()
+ ? new Dictionary()
+ : new Dictionary(destinationExtraProperty);
+
+ if (source.ExtraProperties != null && destination.ExtraProperties != null)
+ {
+ ExtensibleObjectMapper
+ .MapExtraPropertiesTo(
+ typeof(TSource),
+ typeof(TDestination),
+ 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..2ea799a4c9
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ net9.0
+ Volo.Abp.Mapperly.Tests
+ Volo.Abp.Mapperly.Tests
+
+
+
+
+
+
+
+
+
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..1c07bff75b
--- /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 : MapperBase
+{
+ 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..89861f6947
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs
@@ -0,0 +1,60 @@
+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 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_Objects_With_Existing_Target_Object()
+ {
+ var dto = new MyEntityDto {Id = Guid.Empty, Number = 42};
+
+ _objectMapper.Map(new MyEntity { Id = Guid.NewGuid(), Number = 43 }, dto);
+
+ dto.Number.ShouldBe(43);
+ dto.Id.ShouldNotBe(Guid.Empty);
+ }
+
+ [Fact]
+ public void Should_Map_Enum()
+ {
+ var dto = _objectMapper.Map(MyEnum.Value3);
+ dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3
+ }
+
+ [Fact]
+ public void Should_Throw_Exception_If_Mapper_Is_Not_Found()
+ {
+ var exception = Assert.Throws(() =>_objectMapper.Map(new MyEntity()));
+ exception.Message.ShouldBe("No " +
+ "Volo.Abp.Mapperly.IAbpMapperlyMapper or " +
+ "Volo.Abp.Mapperly.IAbpReverseMapperlyMapper" +
+ " was found");
+ }
+}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs
new file mode 100644
index 0000000000..60f4294aeb
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_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 AbpMapperlyModule_Specific_ObjectMapper_Tests : AbpIntegratedTest
+{
+ private readonly IObjectMapper _objectMapper;
+
+ public AbpMapperlyModule_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/AbpMapperly_Dependency_Injection_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs
new file mode 100644
index 0000000000..1aa6288f3e
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs
@@ -0,0 +1,68 @@
+using System;
+using Riok.Mapperly.Abstractions;
+using Shouldly;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.ObjectMapping;
+using Volo.Abp.Testing;
+using Xunit;
+
+namespace Volo.Abp.Mapperly;
+
+public class MyDIClass
+{
+ public string Id { get; set; }
+
+ public DateTime Birthday { get; set; }
+}
+
+public class MyDIClassDto
+{
+ public string Id { get; set; }
+
+ public DateTime Birthday { get; set; }
+}
+
+public class BirthdayCalculatorService : ITransientDependency
+{
+ public DateTime Birthday => DateTime.Parse("2025-01-01");
+}
+
+[Mapper]
+public partial class MyDIClassMapper : MapperBase
+{
+ private readonly BirthdayCalculatorService _birthdayCalculatorService;
+
+ public MyDIClassMapper(BirthdayCalculatorService birthdayCalculatorService)
+ {
+ _birthdayCalculatorService = birthdayCalculatorService;
+ }
+
+ public override partial MyDIClassDto Map(MyDIClass source);
+
+ public override partial void Map(MyDIClass source, MyDIClassDto destination);
+
+ public override void AfterMap(MyDIClass source, MyDIClassDto destination)
+ {
+ destination.Birthday = _birthdayCalculatorService.Birthday;
+ }
+}
+
+public class AbpMapperly_Dependency_Injection_Tests : AbpIntegratedTest
+{
+ private readonly IObjectMapper _objectMapper;
+ private readonly BirthdayCalculatorService _birthdayCalculatorService;
+
+ public AbpMapperly_Dependency_Injection_Tests()
+ {
+ _objectMapper = GetRequiredService();
+ _birthdayCalculatorService = GetRequiredService();
+ }
+
+ [Fact]
+ public void DI_Test()
+ {
+ var myClass = new MyDIClass { Id = "1", Birthday = DateTime.Now };
+ var myClassDto = _objectMapper.Map(myClass);
+ myClassDto.Birthday.ShouldBe(_birthdayCalculatorService.Birthday);
+ }
+}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
new file mode 100644
index 0000000000..5eeebe6b82
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
@@ -0,0 +1,86 @@
+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 MyReverseClass
+{
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+}
+
+public class MyReverseClassDto
+{
+ public string Id { get; set; }
+
+ public string Name { get; set; }
+}
+
+[Mapper]
+public partial class MyReverseClassMapper : TwoWayMapperBase
+{
+ public override partial MyReverseClassDto Map(MyReverseClass source);
+
+ public override partial void Map(MyReverseClass source, MyReverseClassDto destination);
+
+ public override partial MyReverseClass ReverseMap(MyReverseClassDto destination);
+
+ public override partial void ReverseMap(MyReverseClassDto destination, MyReverseClass source);
+
+ public override void BeforeReverseMap(MyReverseClassDto destination)
+ {
+ destination.Name = "BeforeReverseMap " + destination.Name;
+ }
+
+ public override void AfterReverseMap(MyReverseClassDto destination, MyReverseClass source)
+ {
+ source.Name = destination.Name + " AfterReverseMap";
+ }
+}
+
+public class AbpReverseMapperly_Tests : AbpIntegratedTest
+{
+ private readonly IObjectMapper _objectMapper;
+
+ public AbpReverseMapperly_Tests()
+ {
+ _objectMapper = ServiceProvider.GetRequiredService();
+ }
+
+ [Fact]
+ public void Map_Test()
+ {
+ var myClass = new MyReverseClass { Id = "1", Name = "Test" };
+ var myClassDto = _objectMapper.Map(myClass);
+ myClassDto.Name.ShouldBe("Test");
+
+ myClass.Id = "2";
+ myClass.Name = "Test2";
+
+ _objectMapper.Map(myClass, myClassDto);
+
+ myClassDto.Id.ShouldBe("2");
+ myClassDto.Name.ShouldBe("Test2");
+ }
+
+ [Fact]
+ public void ReverseMap_Test()
+ {
+ var myClassDto = new MyReverseClassDto { Id = "1", Name = "Test" };
+ var myClass = _objectMapper.Map(myClassDto);
+ myClass.Name.ShouldBe("BeforeReverseMap Test AfterReverseMap");
+
+ myClassDto.Id = "2";
+ myClassDto.Name = "Test2";
+
+ _objectMapper.Map(myClassDto, myClass);
+
+ myClass.Id.ShouldBe("2");
+ myClass.Name.ShouldBe("BeforeReverseMap Test2 AfterReverseMap");
+ }
+}
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/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs
new file mode 100644
index 0000000000..49be64b61a
--- /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 : MapperBase
+{
+ public override partial MyEntityDto Map(MyEntity source);
+
+ public override partial void Map(MyEntity source, MyEntityDto destination);
+}
+
+[Mapper]
+public partial class MyEnumMapper : MapperBase
+{
+ 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 : MapperBase
+{
+ public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source);
+
+ public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonDto destination);
+}
+
+[Mapper]
+[MapExtraProperties(MapToRegularProperties = true)]
+public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : MapperBase
+{
+ [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 9dbce186ee..d6f3bab715 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -221,6 +221,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",