From 178d3f56d42b4e5acb7e349470f4a644d4c5214e Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 9 Jul 2025 17:41:28 +0800 Subject: [PATCH 01/48] feat: Add `Volo.Abp.Mapperly` module --- Directory.Packages.props | 1 + framework/Volo.Abp.sln | 14 ++ ...bpAutoMapperServiceCollectionExtensions.cs | 22 ++ .../Volo.Abp.Mapperly.csproj | 29 +++ .../Volo/Abp/Mapperly/AbpMapperlyBase.cs | 19 ++ .../AbpMapperlyConventionalRegistrar.cs | 26 +++ .../Volo/Abp/Mapperly/AbpMapperlyModule.cs | 25 +++ .../Volo/Abp/Mapperly/IAbpMapperly.cs | 12 ++ .../Mapperly/MapExtraPropertiesAttribute.cs | 14 ++ .../Abp/Mapperly/MapExtraPropertiesInvoker.cs | 71 +++++++ .../MapperlyAutoObjectMappingProvider.cs | 123 +++++++++++ .../ObjectExtending/ExtensibleObjectMapper.cs | 2 +- .../MappingPropertyDefinitionChecks.cs | 11 +- ...AutoMapperExtensibleDtoExtensions_Tests.cs | 82 ++++++++ .../Volo.Abp.Mapperly.Tests.csproj | 18 ++ ...apperModule_Specific_ObjectMapper_Tests.cs | 191 ++++++++++++++++++ .../AbpMapperlyBeforeAndAfterMethod_Tests.cs | 59 ++++++ .../Mapperly/AbpMapperlyModule_Basic_Tests.cs | 38 ++++ .../Volo/Abp/Mapperly/MapperlyTestModule.cs | 13 ++ .../Mapperly/ObjectMapperExtensions_Tests.cs | 32 +++ .../Mapperly/SampleClasses/MapperlyMappers.cs | 44 ++++ .../Abp/Mapperly/SampleClasses/MyEntity.cs | 10 + .../Abp/Mapperly/SampleClasses/MyEntityDto.cs | 10 + .../Mapperly/SampleClasses/MyEntityDto2.cs | 10 + .../MyEntityDtoWithMappingMethods.cs | 43 ++++ .../MyEntityToMyEntityDto2Mapper.cs | 39 ++++ .../Volo/Abp/Mapperly/SampleClasses/MyEnum.cs | 8 + .../Abp/Mapperly/SampleClasses/MyEnumDto.cs | 9 + .../Mapperly/SampleClasses/MyNotMappedDto.cs | 10 + nupkg/common.ps1 | 1 + 30 files changed, 982 insertions(+), 4 deletions(-) create mode 100644 framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs create mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs 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", From 3daa6c51c3c9bc6f98934890b077d77de8876428 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 9 Jul 2025 20:04:58 +0800 Subject: [PATCH 02/48] feat: Implement reverse mapping functionality and add related tests --- .../Volo/Abp/Mapperly/AbpMapperlyBase.cs | 15 +++- .../AbpMapperlyConventionalRegistrar.cs | 3 +- .../Volo/Abp/Mapperly/IAbpMapperly.cs | 13 +++- .../Abp/Mapperly/MapExtraPropertiesInvoker.cs | 8 +-- .../MapperlyAutoObjectMappingProvider.cs | 10 +++ .../AbpMapperly_Dependency_Injection_Tests.cs | 68 ++++++++++++++++++ .../Abp/Mapperly/AbpReverseMapperly_Tests.cs | 72 +++++++++++++++++++ 7 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs create mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs index 14165be56e..cc86dd8be8 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs @@ -10,10 +10,23 @@ public abstract class AbpMapperlyBase : IAbpMapperly : AbpMapperlyBase, IAbpReverseMapperly +{ + 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/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs index fa209810c2..a4af516425 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs @@ -17,7 +17,8 @@ public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar { var exposedServiceTypes = base.GetExposedServiceTypes(type); var mapperlyInterfaces = type.GetInterfaces() - .Where(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition()).ToList(); + .Where(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition() || + x.IsGenericType && typeof(IAbpReverseMapperly<,>) == x.GetGenericTypeDefinition()).ToList(); return exposedServiceTypes .Union(mapperlyInterfaces) .Distinct() diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs index 8de26067ef..e64bc38e85 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs @@ -1,6 +1,6 @@ namespace Volo.Abp.Mapperly; -public interface IAbpMapperly +public interface IAbpMapperly { TDestination Map(TSource source); @@ -10,3 +10,14 @@ public interface IAbpMapperly void AfterMap(TSource source, TDestination destination); } + +public interface IAbpReverseMapperly : IAbpMapperly +{ + 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/MapExtraPropertiesInvoker.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs index 62b7346197..f1eb60ec9f 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs @@ -31,10 +31,10 @@ public static class MapExtraPropertiesInvoker string[]? ignoredProperties = null, bool mapToRegularProperties = false) { - var key = (typeof(TSource), typeof(TDestination)); - - var action = Cache.GetOrAdd(key, static key => + var mapExtraProperties = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), static key => { + Check.NotNull(MethodDefinition, nameof(MethodDefinition)); + var genericMethod = MethodDefinition.MakeGenericMethod(key.Item1, key.Item2); var targetParam = Expression.Parameter(typeof(object), "target"); @@ -66,6 +66,6 @@ public static class MapExtraPropertiesInvoker return lambda.Compile(); }); - action(targetInstance, source, destination, destinationExtraProperty, definitionChecks, ignoredProperties, mapToRegularProperties); + mapExtraProperties(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 index 550590f269..289f5d2c68 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -37,6 +37,16 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return destination; } + var reverseMapper = ServiceProvider.GetService>(); + if (reverseMapper != null) + { + reverseMapper.BeforeReverseMap((TSource)source); + var destination = reverseMapper.ReverseMap((TSource)source); + TryMapExtraProperties(reverseMapper.As>(), destination, (TSource)source, GetExtraProperties(destination)); + reverseMapper.AfterReverseMap((TSource)source, destination); + return destination; + } + throw new AbpException($"No {nameof(IAbpMapperly)} mapper found for {typeof(TSource).FullName} to {typeof(TDestination).FullName}"); } 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..cecd4d5272 --- /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 : AbpMapperlyBase +{ + 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..c0604f726b --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs @@ -0,0 +1,72 @@ +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 : AbpReverseMapperlyBase +{ + 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"); + } + + [Fact] + public void ReverseMap_Test() + { + var myClassDto = new MyReverseClassDto { Id = "1", Name = "Test" }; + + var myClass = _objectMapper.Map(myClassDto); + + myClass.Name.ShouldBe("BeforeReverseMap Test AfterReverseMap"); + } +} From 715b25ed7c815c10ab7d4291bec53a310e883743 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 9 Jul 2025 20:14:59 +0800 Subject: [PATCH 03/48] Update Riok.Mapperly package references --- framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj | 2 +- .../test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj index 672936d4f7..7c085a146e 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj +++ b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj @@ -23,7 +23,7 @@ - + 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 index f58c4cf848..2ea799a4c9 100644 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj @@ -13,6 +13,5 @@ - From 1f95fb511e0ff6cbc5c1aecd60532bd0d0118570 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Jul 2025 13:23:00 +0800 Subject: [PATCH 04/48] Enhance Mapperly mapping and update related tests --- .../AbpMapperlyConventionalRegistrar.cs | 6 ++-- .../Volo/Abp/Mapperly/IAbpMapperly.cs | 2 +- .../MapperlyAutoObjectMappingProvider.cs | 18 +++++++++-- .../Mapperly/AbpMapperlyModule_Basic_Tests.cs | 24 +++++++++++++- ...erlyModule_Specific_ObjectMapper_Tests.cs} | 4 +-- .../Mapperly/ObjectMapperExtensions_Tests.cs | 32 ------------------- 6 files changed, 45 insertions(+), 41 deletions(-) rename framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/{AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs => AbpMapperlyModule_Specific_ObjectMapper_Tests.cs} (97%) delete mode 100644 framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs index a4af516425..5c1627b0a2 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs @@ -16,9 +16,9 @@ public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar protected override List GetExposedServiceTypes(Type type) { var exposedServiceTypes = base.GetExposedServiceTypes(type); - var mapperlyInterfaces = type.GetInterfaces() - .Where(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition() || - x.IsGenericType && typeof(IAbpReverseMapperly<,>) == x.GetGenericTypeDefinition()).ToList(); + var mapperlyInterfaces = type.GetInterfaces().Where(x => + x.IsGenericType && (typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition() || + typeof(IAbpReverseMapperly<,>) == x.GetGenericTypeDefinition())); return exposedServiceTypes .Union(mapperlyInterfaces) .Distinct() diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs index e64bc38e85..5132fc980e 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs @@ -1,6 +1,6 @@ namespace Volo.Abp.Mapperly; -public interface IAbpMapperly +public interface IAbpMapperly { TDestination Map(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 index 289f5d2c68..28385da5f5 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Data; using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectMapping; +using Volo.Abp.Reflection; namespace Volo.Abp.Mapperly; @@ -47,7 +48,8 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return destination; } - throw new AbpException($"No {nameof(IAbpMapperly)} mapper found for {typeof(TSource).FullName} to {typeof(TDestination).FullName}"); + throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperly))} or" + + $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperly))} was found"); } public virtual TDestination Map(TSource source, TDestination destination) @@ -63,7 +65,19 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return destination; } - throw new AbpException($"No {nameof(IAbpMapperly)} mapper found for {typeof(TSource).FullName} to {typeof(TDestination).FullName}"); + var reverseMapper = ServiceProvider.GetService>(); + if (reverseMapper != null) + { + reverseMapper.BeforeReverseMap(source); + var destinationExtraProperties = GetExtraProperties(destination); + reverseMapper.ReverseMap(source, destination); + TryMapExtraProperties(reverseMapper.As>(), source, destination, destinationExtraProperties); + reverseMapper.AfterReverseMap(source, destination); + return destination; + } + + throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperly))} or" + + $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperly))} was found"); } protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) 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 index 5edad59b1b..96573fca52 100644 --- 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 @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using System; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Mapperly.SampleClasses; using Volo.Abp.ObjectMapping; @@ -29,10 +30,31 @@ public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest(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.IAbpMapperly or " + + "Volo.Abp.Mapperly.IAbpReverseMapperly" + + " was found"); + } } 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/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs similarity index 97% rename from framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs rename to framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs index 0080517c36..60f4294aeb 100644 --- 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/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs @@ -11,11 +11,11 @@ using Xunit; namespace Volo.Abp.Mapperly; -public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest +public class AbpMapperlyModule_Specific_ObjectMapper_Tests : AbpIntegratedTest { private readonly IObjectMapper _objectMapper; - public AbpAutoMapperModule_Specific_ObjectMapper_Tests() + public AbpMapperlyModule_Specific_ObjectMapper_Tests() { _objectMapper = ServiceProvider.GetRequiredService(); } 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 deleted file mode 100644 index 4cfd58a760..0000000000 --- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ObjectMapperExtensions_Tests.cs +++ /dev/null @@ -1,32 +0,0 @@ -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); - } -} From de946d6b72d20360fc0cdc5b711da493a681e88a Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Jul 2025 14:03:05 +0800 Subject: [PATCH 05/48] feat: Refactor mapping interfaces and classes for improved consistency and functionality --- .../AbpMapperlyConventionalRegistrar.cs | 6 ++--- ...pperlyBase.cs => AbpMapperlyMapperBase.cs} | 4 ++-- ...{IAbpMapperly.cs => IAbpMapperlyMapper.cs} | 4 ++-- .../MapperlyAutoObjectMappingProvider.cs | 22 +++++++++---------- .../AbpMapperlyBeforeAndAfterMethod_Tests.cs | 2 +- .../Mapperly/AbpMapperlyModule_Basic_Tests.cs | 4 ++-- .../AbpMapperly_Dependency_Injection_Tests.cs | 2 +- .../Abp/Mapperly/AbpReverseMapperly_Tests.cs | 2 +- .../Mapperly/SampleClasses/MapperlyMappers.cs | 8 +++---- 9 files changed, 27 insertions(+), 27 deletions(-) rename framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/{AbpMapperlyBase.cs => AbpMapperlyMapperBase.cs} (68%) rename framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/{IAbpMapperly.cs => IAbpMapperlyMapper.cs} (73%) diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs index 5c1627b0a2..028a72bb67 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs @@ -9,7 +9,7 @@ public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar { protected override bool IsConventionalRegistrationDisabled(Type type) { - return !type.GetInterfaces().Any(x => x.IsGenericType && typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition()) || + return !type.GetInterfaces().Any(x => x.IsGenericType && typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition()) || base.IsConventionalRegistrationDisabled(type); } @@ -17,8 +17,8 @@ public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar { var exposedServiceTypes = base.GetExposedServiceTypes(type); var mapperlyInterfaces = type.GetInterfaces().Where(x => - x.IsGenericType && (typeof(IAbpMapperly<,>) == x.GetGenericTypeDefinition() || - typeof(IAbpReverseMapperly<,>) == x.GetGenericTypeDefinition())); + x.IsGenericType && (typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition() || + typeof(IAbpReverseMapperlyMapper<,>) == x.GetGenericTypeDefinition())); return exposedServiceTypes .Union(mapperlyInterfaces) .Distinct() diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs similarity index 68% rename from framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs rename to framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs index cc86dd8be8..008c3635ea 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyBase.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs @@ -2,7 +2,7 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.Mapperly; -public abstract class AbpMapperlyBase : IAbpMapperly, ITransientDependency +public abstract class AbpMapperlyMapperBase : IAbpMapperlyMapper, ITransientDependency { public abstract TDestination Map(TSource source); @@ -16,7 +16,7 @@ public abstract class AbpMapperlyBase : IAbpMapperly : AbpMapperlyBase, IAbpReverseMapperly +public abstract class AbpReverseMapperlyMapperBase : AbpMapperlyMapperBase, IAbpReverseMapperlyMapper { public abstract TSource ReverseMap(TDestination destination); diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs similarity index 73% rename from framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs rename to framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs index 5132fc980e..3d391cfe1f 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperly.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs @@ -1,6 +1,6 @@ namespace Volo.Abp.Mapperly; -public interface IAbpMapperly +public interface IAbpMapperlyMapper { TDestination Map(TSource source); @@ -11,7 +11,7 @@ public interface IAbpMapperly void AfterMap(TSource source, TDestination destination); } -public interface IAbpReverseMapperly : IAbpMapperly +public interface IAbpReverseMapperlyMapper : IAbpMapperlyMapper { TSource ReverseMap(TDestination destination); diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 28385da5f5..6e0e36d071 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -28,7 +28,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider public virtual TDestination Map(object source) { - var mapper = ServiceProvider.GetService>(); + var mapper = ServiceProvider.GetService>(); if (mapper != null) { mapper.BeforeMap((TSource)source); @@ -38,23 +38,23 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return destination; } - var reverseMapper = ServiceProvider.GetService>(); + var reverseMapper = ServiceProvider.GetService>(); if (reverseMapper != null) { reverseMapper.BeforeReverseMap((TSource)source); var destination = reverseMapper.ReverseMap((TSource)source); - TryMapExtraProperties(reverseMapper.As>(), destination, (TSource)source, GetExtraProperties(destination)); + TryMapExtraProperties(reverseMapper.As>(), destination, (TSource)source, GetExtraProperties(destination)); reverseMapper.AfterReverseMap((TSource)source, destination); return destination; } - throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperly))} or" + - $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperly))} was found"); + 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>(); + var mapper = ServiceProvider.GetService>(); if (mapper != null) { mapper.BeforeMap(source); @@ -65,19 +65,19 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return destination; } - var reverseMapper = ServiceProvider.GetService>(); + var reverseMapper = ServiceProvider.GetService>(); if (reverseMapper != null) { reverseMapper.BeforeReverseMap(source); var destinationExtraProperties = GetExtraProperties(destination); reverseMapper.ReverseMap(source, destination); - TryMapExtraProperties(reverseMapper.As>(), source, destination, destinationExtraProperties); + TryMapExtraProperties(reverseMapper.As>(), source, destination, destinationExtraProperties); reverseMapper.AfterReverseMap(source, destination); return destination; } - throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperly))} or" + - $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperly))} was found"); + throw new AbpException($"No {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpMapperlyMapper))} or" + + $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper))} was found"); } protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) @@ -95,7 +95,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return extraProperties; } - protected virtual void TryMapExtraProperties(IAbpMapperly mapper, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) + protected virtual void TryMapExtraProperties(IAbpMapperlyMapper mapper, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) { var mapToRegularPropertiesAttribute = mapper.GetType().GetSingleAttributeOrNull(); if (mapToRegularPropertiesAttribute != null && 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 index f8d39985d8..94e2b4ef7e 100644 --- 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 @@ -22,7 +22,7 @@ public class MyClassDto } [Mapper] -public partial class MyClassMapper : AbpMapperlyBase +public partial class MyClassMapper : AbpMapperlyMapperBase { public override partial MyClassDto Map(MyClass source); 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 index 96573fca52..89861f6947 100644 --- 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 @@ -53,8 +53,8 @@ public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest(() =>_objectMapper.Map(new MyEntity())); exception.Message.ShouldBe("No " + - "Volo.Abp.Mapperly.IAbpMapperly or " + - "Volo.Abp.Mapperly.IAbpReverseMapperly" + + "Volo.Abp.Mapperly.IAbpMapperlyMapper or " + + "Volo.Abp.Mapperly.IAbpReverseMapperlyMapper" + " was found"); } } 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 index cecd4d5272..ecc0f7ecde 100644 --- 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 @@ -28,7 +28,7 @@ public class BirthdayCalculatorService : ITransientDependency } [Mapper] -public partial class MyDIClassMapper : AbpMapperlyBase +public partial class MyDIClassMapper : AbpMapperlyMapperBase { private readonly BirthdayCalculatorService _birthdayCalculatorService; 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 index c0604f726b..cb0b493f5a 100644 --- 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 @@ -22,7 +22,7 @@ public class MyReverseClassDto } [Mapper] -public partial class MyReverseClassMapper : AbpReverseMapperlyBase +public partial class MyReverseClassMapper : AbpReverseMapperlyMapperBase { public override partial MyReverseClassDto Map(MyReverseClass source); 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 index 8d4666a184..22ba9be77e 100644 --- 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 @@ -4,7 +4,7 @@ using Volo.Abp.Mapperly.SampleClasses; using Volo.Abp.ObjectExtending.TestObjects; [Mapper] -public partial class MyEntityMapper : AbpMapperlyBase +public partial class MyEntityMapper : AbpMapperlyMapperBase { public override partial MyEntityDto Map(MyEntity source); @@ -12,7 +12,7 @@ public partial class MyEntityMapper : AbpMapperlyBase } [Mapper] -public partial class MyEnumMapper : AbpMapperlyBase +public partial class MyEnumMapper : AbpMapperlyMapperBase { public override partial MyEnumDto Map(MyEnum source); @@ -24,7 +24,7 @@ public partial class MyEnumMapper : AbpMapperlyBase [Mapper] [MapExtraProperties(IgnoredProperties = ["CityName"])] -public partial class ExtensibleTestPersonMapper : AbpMapperlyBase +public partial class ExtensibleTestPersonMapper : AbpMapperlyMapperBase { public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source); @@ -33,7 +33,7 @@ public partial class ExtensibleTestPersonMapper : AbpMapperlyBase +public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : AbpMapperlyMapperBase { [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Name))] [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Age))] From 74589a9323a83dd996bd63cf0c251b92ced4db41 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Jul 2025 15:18:46 +0800 Subject: [PATCH 06/48] Remove MapExtraPropertiesInvoker and update mapping logic --- .../Abp/Mapperly/MapExtraPropertiesInvoker.cs | 71 ------------------- .../MapperlyAutoObjectMappingProvider.cs | 8 +-- 2 files changed, 4 insertions(+), 75 deletions(-) delete mode 100644 framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs deleted file mode 100644 index f1eb60ec9f..0000000000 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesInvoker.cs +++ /dev/null @@ -1,71 +0,0 @@ -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 mapExtraProperties = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), static key => - { - Check.NotNull(MethodDefinition, nameof(MethodDefinition)); - - 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(); - }); - - mapExtraProperties(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 index 6e0e36d071..19f677515b 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -102,7 +102,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider typeof(IHasExtraProperties).IsAssignableFrom(typeof(TDestination)) && typeof(IHasExtraProperties).IsAssignableFrom(typeof(TSource))) { - MapExtraPropertiesInvoker.Invoke(this, + MapExtraProperties( source!.As(), destination!.As(), destinationExtraProperty, @@ -120,8 +120,6 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider MappingPropertyDefinitionChecks? definitionChecks = null, string[]? ignoredProperties = null, bool mapToRegularProperties = false) - where TSource : IHasExtraProperties - where TDestination : IHasExtraProperties { var result = destinationExtraProperty.IsNullOrEmpty() ? new Dictionary() @@ -130,7 +128,9 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider if (source.ExtraProperties != null && destination.ExtraProperties != null) { ExtensibleObjectMapper - .MapExtraPropertiesTo( + .MapExtraPropertiesTo( + typeof(TSource), + typeof(TDestination), source.ExtraProperties, result, definitionChecks, From 53710b76b6e8c79e3cc5c1a625cf0392f901d764 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 13 Jul 2025 15:14:26 +0800 Subject: [PATCH 07/48] Fix generic type parameters in reverse mapper interfaces and classes for consistency --- .../Abp/Mapperly/AbpMapperlyMapperBase.cs | 2 +- .../Volo/Abp/Mapperly/IAbpMapperlyMapper.cs | 2 +- .../MapperlyAutoObjectMappingProvider.cs | 24 +++++++++---------- .../Abp/Mapperly/AbpReverseMapperly_Tests.cs | 10 +++++++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs index 008c3635ea..6b7e9928ba 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs @@ -16,7 +16,7 @@ public abstract class AbpMapperlyMapperBase : IAbpMapperl } } -public abstract class AbpReverseMapperlyMapperBase : AbpMapperlyMapperBase, IAbpReverseMapperlyMapper +public abstract class AbpReverseMapperlyMapperBase : AbpMapperlyMapperBase, IAbpReverseMapperlyMapper { public abstract TSource ReverseMap(TDestination destination); diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs index 3d391cfe1f..eac0f6644c 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs @@ -11,7 +11,7 @@ public interface IAbpMapperlyMapper void AfterMap(TSource source, TDestination destination); } -public interface IAbpReverseMapperlyMapper : IAbpMapperlyMapper +public interface IAbpReverseMapperlyMapper : IAbpMapperlyMapper { TSource ReverseMap(TDestination destination); diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 19f677515b..34fa1b2c53 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -33,17 +33,17 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { mapper.BeforeMap((TSource)source); var destination = mapper.Map((TSource)source); - TryMapExtraProperties(mapper, (TSource)source, destination, new ExtraPropertyDictionary()); + TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary()); mapper.AfterMap((TSource)source, destination); return destination; } - var reverseMapper = ServiceProvider.GetService>(); + var reverseMapper = ServiceProvider.GetService>(); if (reverseMapper != null) { reverseMapper.BeforeReverseMap((TSource)source); var destination = reverseMapper.ReverseMap((TSource)source); - TryMapExtraProperties(reverseMapper.As>(), destination, (TSource)source, GetExtraProperties(destination)); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), destination, (TSource)source, GetExtraProperties(destination)); reverseMapper.AfterReverseMap((TSource)source, destination); return destination; } @@ -60,18 +60,18 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider mapper.BeforeMap(source); var destinationExtraProperties = GetExtraProperties(destination); mapper.Map(source, destination); - TryMapExtraProperties(mapper, source, destination, destinationExtraProperties); + TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties); mapper.AfterMap(source, destination); return destination; } - var reverseMapper = ServiceProvider.GetService>(); + var reverseMapper = ServiceProvider.GetService>(); if (reverseMapper != null) { reverseMapper.BeforeReverseMap(source); var destinationExtraProperties = GetExtraProperties(destination); reverseMapper.ReverseMap(source, destination); - TryMapExtraProperties(reverseMapper.As>(), source, destination, destinationExtraProperties); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties); reverseMapper.AfterReverseMap(source, destination); return destination; } @@ -95,10 +95,9 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider return extraProperties; } - protected virtual void TryMapExtraProperties(IAbpMapperlyMapper mapper, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) + protected virtual void TryMapExtraProperties(MapExtraPropertiesAttribute? mapExtraPropertiesAttribute, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) { - var mapToRegularPropertiesAttribute = mapper.GetType().GetSingleAttributeOrNull(); - if (mapToRegularPropertiesAttribute != null && + if (mapExtraPropertiesAttribute != null && typeof(IHasExtraProperties).IsAssignableFrom(typeof(TDestination)) && typeof(IHasExtraProperties).IsAssignableFrom(typeof(TSource))) { @@ -106,13 +105,12 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider source!.As(), destination!.As(), destinationExtraProperty, - mapToRegularPropertiesAttribute.DefinitionChecks, - mapToRegularPropertiesAttribute.IgnoredProperties, - mapToRegularPropertiesAttribute.MapToRegularProperties + mapExtraPropertiesAttribute.DefinitionChecks, + mapExtraPropertiesAttribute.IgnoredProperties, + mapExtraPropertiesAttribute.MapToRegularProperties ); } } - protected virtual void MapExtraProperties( IHasExtraProperties source, IHasExtraProperties destination, 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 index cb0b493f5a..0e5dbc8c11 100644 --- 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 @@ -22,7 +22,7 @@ public class MyReverseClassDto } [Mapper] -public partial class MyReverseClassMapper : AbpReverseMapperlyMapperBase +public partial class MyReverseClassMapper : AbpReverseMapperlyMapperBase { public override partial MyReverseClassDto Map(MyReverseClass source); @@ -68,5 +68,13 @@ public class AbpReverseMapperly_Tests : AbpIntegratedTest 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"); } } From 9e0d0413d0cf6c23d7fb4db17a9e8183bcb88d35 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 13 Jul 2025 15:24:36 +0800 Subject: [PATCH 08/48] Fix parameter order in TryMapExtraProperties call --- .../Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs | 2 +- .../Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 34fa1b2c53..7685aa03ac 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -43,7 +43,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { reverseMapper.BeforeReverseMap((TSource)source); var destination = reverseMapper.ReverseMap((TSource)source); - TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), destination, (TSource)source, GetExtraProperties(destination)); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination)); reverseMapper.AfterReverseMap((TSource)source, destination); return destination; } 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 index 0e5dbc8c11..49c3c0ff4c 100644 --- 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 @@ -58,15 +58,21 @@ public class AbpReverseMapperly_Tests : AbpIntegratedTest 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"; From 9a72a79c67de156f859eea9d621f0e485b3a7faf Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Jul 2025 21:12:25 +0800 Subject: [PATCH 09/48] Rename mapper base classes --- .../Mapperly/{AbpMapperlyMapperBase.cs => MapperBase.cs} | 4 ++-- .../Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs | 2 +- .../Mapperly/AbpMapperly_Dependency_Injection_Tests.cs | 2 +- .../Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs | 2 +- .../Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) rename framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/{AbpMapperlyMapperBase.cs => MapperBase.cs} (68%) diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs similarity index 68% rename from framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs rename to framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs index 6b7e9928ba..39d9dce995 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyMapperBase.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs @@ -2,7 +2,7 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.Mapperly; -public abstract class AbpMapperlyMapperBase : IAbpMapperlyMapper, ITransientDependency +public abstract class MapperBase : IAbpMapperlyMapper, ITransientDependency { public abstract TDestination Map(TSource source); @@ -16,7 +16,7 @@ public abstract class AbpMapperlyMapperBase : IAbpMapperl } } -public abstract class AbpReverseMapperlyMapperBase : AbpMapperlyMapperBase, IAbpReverseMapperlyMapper +public abstract class TwoWayMapperBase : MapperBase, IAbpReverseMapperlyMapper { public abstract TSource ReverseMap(TDestination destination); 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 index 94e2b4ef7e..1c07bff75b 100644 --- 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 @@ -22,7 +22,7 @@ public class MyClassDto } [Mapper] -public partial class MyClassMapper : AbpMapperlyMapperBase +public partial class MyClassMapper : MapperBase { public override partial MyClassDto Map(MyClass source); 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 index ecc0f7ecde..1aa6288f3e 100644 --- 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 @@ -28,7 +28,7 @@ public class BirthdayCalculatorService : ITransientDependency } [Mapper] -public partial class MyDIClassMapper : AbpMapperlyMapperBase +public partial class MyDIClassMapper : MapperBase { private readonly BirthdayCalculatorService _birthdayCalculatorService; 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 index 49c3c0ff4c..5eeebe6b82 100644 --- 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 @@ -22,7 +22,7 @@ public class MyReverseClassDto } [Mapper] -public partial class MyReverseClassMapper : AbpReverseMapperlyMapperBase +public partial class MyReverseClassMapper : TwoWayMapperBase { public override partial MyReverseClassDto Map(MyReverseClass source); 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 index 22ba9be77e..49be64b61a 100644 --- 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 @@ -4,7 +4,7 @@ using Volo.Abp.Mapperly.SampleClasses; using Volo.Abp.ObjectExtending.TestObjects; [Mapper] -public partial class MyEntityMapper : AbpMapperlyMapperBase +public partial class MyEntityMapper : MapperBase { public override partial MyEntityDto Map(MyEntity source); @@ -12,7 +12,7 @@ public partial class MyEntityMapper : AbpMapperlyMapperBase +public partial class MyEnumMapper : MapperBase { public override partial MyEnumDto Map(MyEnum source); @@ -24,7 +24,7 @@ public partial class MyEnumMapper : AbpMapperlyMapperBase [Mapper] [MapExtraProperties(IgnoredProperties = ["CityName"])] -public partial class ExtensibleTestPersonMapper : AbpMapperlyMapperBase +public partial class ExtensibleTestPersonMapper : MapperBase { public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source); @@ -33,7 +33,7 @@ public partial class ExtensibleTestPersonMapper : AbpMapperlyMapperBase +public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : MapperBase { [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Name))] [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Age))] From 05df9e6f3dd94d32f2d3a6cddc989e04287101b7 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 13:06:45 +0300 Subject: [PATCH 10/48] Update Angular build executor and options on dev-app Switched build executor from 'browser' to 'application' and updated build options to use 'browser' and array format for 'polyfills'. Also removed unused options from the development configuration for cleaner setup. --- npm/ng-packs/apps/dev-app/project.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/npm/ng-packs/apps/dev-app/project.json b/npm/ng-packs/apps/dev-app/project.json index 1213acbb2f..958cdfb3ee 100644 --- a/npm/ng-packs/apps/dev-app/project.json +++ b/npm/ng-packs/apps/dev-app/project.json @@ -6,13 +6,13 @@ "prefix": "app", "targets": { "build": { - "executor": "@angular-devkit/build-angular:browser", + "executor": "@angular-devkit/build-angular:application", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/apps/dev-app", "index": "apps/dev-app/src/index.html", - "main": "apps/dev-app/src/main.ts", - "polyfills": "apps/dev-app/src/polyfills.ts", + "browser": "apps/dev-app/src/main.ts", + "polyfills": ["apps/dev-app/src/polyfills.ts"], "tsConfig": "apps/dev-app/tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -141,12 +141,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" From 70110ff372f69b83380c35fa41794655f2349f5c Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 13:08:31 +0300 Subject: [PATCH 11/48] Update Angular config for latest builder and TypeScript on app-nolayers in templates --- templates/app-nolayers/angular/angular.json | 15 ++++++--------- templates/app-nolayers/angular/tsconfig.json | 5 +++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/templates/app-nolayers/angular/angular.json b/templates/app-nolayers/angular/angular.json index bfdb10c374..6ed0811fb5 100644 --- a/templates/app-nolayers/angular/angular.json +++ b/templates/app-nolayers/angular/angular.json @@ -19,12 +19,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/MyProjectName", "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -137,12 +137,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -168,8 +165,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/test.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/app-nolayers/angular/tsconfig.json b/templates/app-nolayers/angular/tsconfig.json index 29df0877f5..eeb0e33782 100644 --- a/templates/app-nolayers/angular/tsconfig.json +++ b/templates/app-nolayers/angular/tsconfig.json @@ -11,10 +11,11 @@ "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", - "module": "es2020", + "module": "esnext", "skipLibCheck": true, + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], "paths": { From 02bd0ec275b78b49d989e7e1983ef43b50e71850 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 13:09:06 +0300 Subject: [PATCH 12/48] Update Angular build config and TypeScript settings on app --- templates/app/angular/angular.json | 15 ++++++--------- templates/app/angular/tsconfig.json | 5 +++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/templates/app/angular/angular.json b/templates/app/angular/angular.json index bfdb10c374..6ed0811fb5 100644 --- a/templates/app/angular/angular.json +++ b/templates/app/angular/angular.json @@ -19,12 +19,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/MyProjectName", "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -137,12 +137,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -168,8 +165,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/test.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/app/angular/tsconfig.json b/templates/app/angular/tsconfig.json index 29df0877f5..eeb0e33782 100644 --- a/templates/app/angular/tsconfig.json +++ b/templates/app/angular/tsconfig.json @@ -11,10 +11,11 @@ "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", - "module": "es2020", + "module": "esnext", "skipLibCheck": true, + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], "paths": { From 9298b9ae4a95fcdefb85350e8edef1f36c772d14 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 13:09:18 +0300 Subject: [PATCH 13/48] Update Angular build config and TypeScript settings on module --- templates/module/angular/angular.json | 15 ++++++--------- templates/module/angular/tsconfig.prod.json | 7 ++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/templates/module/angular/angular.json b/templates/module/angular/angular.json index b6145d12f1..02fd552ffe 100644 --- a/templates/module/angular/angular.json +++ b/templates/module/angular/angular.json @@ -58,12 +58,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/dev-app", "index": "projects/dev-app/src/index.html", - "main": "projects/dev-app/src/main.ts", - "polyfills": "projects/dev-app/src/polyfills.ts", + "browser": "projects/dev-app/src/main.ts", + "polyfills": ["projects/dev-app/src/polyfills.ts"], "tsConfig": "projects/dev-app/tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -131,12 +131,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -162,8 +159,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "projects/dev-app/src/test.ts", - "polyfills": "projects/dev-app/src/polyfills.ts", + "browser": "projects/dev-app/src/test.ts", + "polyfills": ["projects/dev-app/src/polyfills.ts"], "tsConfig": "projects/dev-app/tsconfig.spec.json", "karmaConfig": "projects/dev-app/karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/module/angular/tsconfig.prod.json b/templates/module/angular/tsconfig.prod.json index bd2235775b..28b0a75f51 100644 --- a/templates/module/angular/tsconfig.prod.json +++ b/templates/module/angular/tsconfig.prod.json @@ -9,10 +9,11 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2017", - "module": "es2020", + "target": "es2020", + "module": "esnext", + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], }, From aba19d57751942fa1eb473b33dbd94b6ebb276aa Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 13:27:20 +0300 Subject: [PATCH 14/48] Update TypeScript lib to es2020 in all packages Changed the 'lib' setting from 'es2018' to 'es2020' in all package tsconfig.lib.json files and updated tsconfig.base.json to use 'es2020'. Also added 'esModuleInterop' to tsconfig.base.json for improved module compatibility. --- npm/ng-packs/packages/account-core/tsconfig.lib.json | 2 +- npm/ng-packs/packages/account/tsconfig.lib.json | 2 +- npm/ng-packs/packages/components/tsconfig.lib.json | 2 +- npm/ng-packs/packages/core/tsconfig.lib.json | 2 +- npm/ng-packs/packages/feature-management/tsconfig.lib.json | 2 +- npm/ng-packs/packages/identity/tsconfig.lib.json | 2 +- npm/ng-packs/packages/oauth/tsconfig.lib.json | 2 +- npm/ng-packs/packages/permission-management/tsconfig.lib.json | 2 +- npm/ng-packs/packages/setting-management/tsconfig.lib.json | 2 +- npm/ng-packs/packages/tenant-management/tsconfig.lib.json | 2 +- npm/ng-packs/packages/theme-basic/tsconfig.lib.json | 2 +- npm/ng-packs/packages/theme-shared/tsconfig.lib.json | 2 +- npm/ng-packs/source-code-requirements/tsconfig.lib.json | 2 +- npm/ng-packs/tsconfig.base.json | 3 ++- 14 files changed, 15 insertions(+), 14 deletions(-) diff --git a/npm/ng-packs/packages/account-core/tsconfig.lib.json b/npm/ng-packs/packages/account-core/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/account-core/tsconfig.lib.json +++ b/npm/ng-packs/packages/account-core/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/account/tsconfig.lib.json b/npm/ng-packs/packages/account/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/account/tsconfig.lib.json +++ b/npm/ng-packs/packages/account/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/components/tsconfig.lib.json b/npm/ng-packs/packages/components/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/components/tsconfig.lib.json +++ b/npm/ng-packs/packages/components/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/core/tsconfig.lib.json b/npm/ng-packs/packages/core/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/core/tsconfig.lib.json +++ b/npm/ng-packs/packages/core/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/feature-management/tsconfig.lib.json b/npm/ng-packs/packages/feature-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/feature-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/feature-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/identity/tsconfig.lib.json b/npm/ng-packs/packages/identity/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/identity/tsconfig.lib.json +++ b/npm/ng-packs/packages/identity/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/oauth/tsconfig.lib.json b/npm/ng-packs/packages/oauth/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/oauth/tsconfig.lib.json +++ b/npm/ng-packs/packages/oauth/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/permission-management/tsconfig.lib.json b/npm/ng-packs/packages/permission-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/permission-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/permission-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/setting-management/tsconfig.lib.json b/npm/ng-packs/packages/setting-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/setting-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/setting-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json b/npm/ng-packs/packages/tenant-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/tenant-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json b/npm/ng-packs/packages/theme-basic/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-basic/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json b/npm/ng-packs/packages/theme-shared/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-shared/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/source-code-requirements/tsconfig.lib.json b/npm/ng-packs/source-code-requirements/tsconfig.lib.json index 884bdb3be7..df57fcc946 100644 --- a/npm/ng-packs/source-code-requirements/tsconfig.lib.json +++ b/npm/ng-packs/source-code-requirements/tsconfig.lib.json @@ -6,7 +6,7 @@ "declaration": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"] + "lib": ["dom", "es2020"] }, "angularCompilerOptions": { "enableIvy": true, diff --git a/npm/ng-packs/tsconfig.base.json b/npm/ng-packs/tsconfig.base.json index d24de94706..f863f18520 100644 --- a/npm/ng-packs/tsconfig.base.json +++ b/npm/ng-packs/tsconfig.base.json @@ -10,7 +10,8 @@ "importHelpers": true, "target": "es2020", "module": "esnext", - "lib": ["es2017", "dom"], + "lib": ["es2020", "dom"], + "esModuleInterop": true, "baseUrl": "./", "allowSyntheticDefaultImports": true, "paths": { From 3b808f3b000cbc923075087930372f7b7790259d Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 23 Jul 2025 14:10:24 +0300 Subject: [PATCH 15/48] Update TypeScript lib target to es2020 on schematics --- .../__libraryName@kebab__/tsconfig.lib.json.template | 2 +- .../__libraryName@kebab__/tsconfig.lib.json.template | 2 +- npm/ng-packs/packages/schematics/tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template index 5b574d313d..7b5ac72780 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template @@ -10,7 +10,7 @@ "types": [], "lib": [ "dom", - "es2018" + "es2020" ] }, "exclude": [ diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template index 5b574d313d..7b5ac72780 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template @@ -10,7 +10,7 @@ "types": [], "lib": [ "dom", - "es2018" + "es2020" ] }, "exclude": [ diff --git a/npm/ng-packs/packages/schematics/tsconfig.json b/npm/ng-packs/packages/schematics/tsconfig.json index 591691b578..dcccc6bde1 100644 --- a/npm/ng-packs/packages/schematics/tsconfig.json +++ b/npm/ng-packs/packages/schematics/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": "tsconfig", - "lib": ["es2019", "dom"], + "lib": ["es2020", "dom"], "declaration": true, "module": "commonjs", "moduleResolution": "node", From a080ebc69ea82fb397fbdb1a19c5e1b4f3290cdb Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Fri, 25 Jul 2025 15:33:21 +0300 Subject: [PATCH 16/48] Add async auth guard support for OAuth routes Introduced asyncAuthGuard and asyncAbpOAuthGuard to enable asynchronous authentication checks, particularly for OAuth flows involving code exchange. Updated app routes and OAuth module provider to use the new async guards where appropriate. --- .../core/src/lib/abstracts/auth.guard.ts | 6 ++++ .../oauth/src/lib/guards/oauth.guard.ts | 32 ++++++++++++++++++- .../providers/oauth-module-config.provider.ts | 7 +++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts b/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts index 50473e4df1..020f750555 100644 --- a/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts @@ -21,3 +21,9 @@ export const authGuard: CanActivateFn = () => { console.error('You should add @abp/ng-oauth packages or create your own auth packages.'); return false; }; + + +export const asyncAuthGuard: CanActivateFn = () => { + console.error('You should add @abp/ng-oauth packages or create your own auth packages.'); + return false; +}; \ No newline at end of file diff --git a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts index 6f751804cd..e7f31643aa 100644 --- a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts +++ b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts @@ -6,7 +6,7 @@ import { CanActivateFn, } from '@angular/router'; -import { Observable } from 'rxjs'; +import { Observable, interval, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs'; import { OAuthService } from 'angular-oauth2-oidc'; import { AuthService, IAbpGuard } from '@abp/ng.core'; @@ -53,3 +53,33 @@ export const abpOAuthGuard: CanActivateFn = ( authService.navigateToLogin(params); return false; }; + +export const asyncAbpOAuthGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + const authService = inject(AuthService); + const code = new URLSearchParams(window.location.search).get('code'); + + if (code) { + return firstValueFrom( + interval(100).pipe( + map(() => authService.isAuthenticated), + filter(isAuthenticated => isAuthenticated), + take(1), + timeout(3000), + catchError(() => { + authService.navigateToLogin({ returnUrl: state.url }); + return of(false); + }) + ) + ); + } + + if (authService.isAuthenticated) { + return true; + } + + authService.navigateToLogin({ returnUrl: state.url }); + return false; +}; diff --git a/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts b/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts index 92ecc86012..534882dd13 100644 --- a/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts +++ b/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts @@ -2,6 +2,7 @@ import { AuthService, AuthGuard, authGuard, + asyncAuthGuard, ApiInterceptor, PIPE_TO_LOGIN_FN_KEY, CHECK_AUTHENTICATION_STATE_FN_KEY, @@ -11,7 +12,7 @@ import { import { Provider, makeEnvironmentProviders, inject, provideAppInitializer } from '@angular/core'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; -import { AbpOAuthGuard, abpOAuthGuard } from '../guards'; +import { AbpOAuthGuard, abpOAuthGuard, asyncAbpOAuthGuard, } from '../guards'; import { OAuthConfigurationHandler } from '../handlers'; import { OAuthApiInterceptor } from '../interceptors'; import { AbpOAuthService, OAuthErrorFilterService } from '../services'; @@ -31,6 +32,10 @@ export function provideAbpOAuth() { { provide: authGuard, useValue: abpOAuthGuard, + }, + { + provide: asyncAuthGuard, + useValue: asyncAbpOAuthGuard, }, { provide: ApiInterceptor, From d436f9020bf3487af03efaea7a8f356aa06b791c Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Mon, 28 Jul 2025 16:14:13 +0300 Subject: [PATCH 17/48] Update the separate multi-tenancy article --- .../2025-07-26-Separate-Tenant-Schema/POST.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md index 5567b42aad..e63db935a5 100644 --- a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md +++ b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md @@ -22,7 +22,7 @@ In the shared database model, all the application data stored in a single physic ![single-shared-database](single-shared-database.png) -This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for must applications. +This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for most applications. In this model, a single database table may contain data of multiple tenants. Each row in these tables have a `TenantId` field which is used to distinguish the tenant data and isolate a tenant's data from other tenant users. To make your entities multi-tenant aware, all you have to do is to implement the `IMultiTenant` interface provided by the ABP Framework. From 341707264bcc1c9b5c44397883f9bfa4ccde68d9 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Mon, 28 Jul 2025 23:31:07 +0300 Subject: [PATCH 18/48] Refactor locale initialization to support async and injector Updated provideAppInitializer to use an async function and pass the Injector to localeInitializer. Modified localeInitializer to accept an optional Injector parameter for improved flexibility and testability. --- .../src/lib/providers/core-module-config.provider.ts | 11 +++++++---- .../packages/core/src/lib/utils/initial-utils.ts | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index fafb51ff6d..3ebe64360f 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -1,4 +1,4 @@ -import { makeEnvironmentProviders, Provider, inject, provideAppInitializer } from '@angular/core'; +import { makeEnvironmentProviders, Provider, provideAppInitializer, inject, Injector } from '@angular/core'; import { TitleStrategy } from '@angular/router'; import { HTTP_INTERCEPTORS, @@ -107,13 +107,16 @@ export function provideAbpCore(...features: CoreFeature[]) { }), ), provideAppInitializer(() => { - getInitialData(); - localeInitializer(); + const injector = inject(Injector); inject(LocalizationService); inject(LocalStorageListenerService); inject(RoutesHandler); - }), + return (async (): Promise => { + await getInitialData(); + await localeInitializer(injector); + })(); + }), LocaleProvider, CookieLanguageProvider, { diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index 3bc9b6f2b5..254d61896d 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -52,10 +52,10 @@ export async function getInitialData() { await lastValueFrom(result$); } -export function localeInitializer() { - const injector = inject(Injector); - const sessionState = injector.get(SessionStateService); - const { registerLocaleFn }: ABP.Root = injector.get(CORE_OPTIONS); +export function localeInitializer(injector?: Injector) { + const currentInjector = injector || inject(Injector); + const sessionState = currentInjector.get(SessionStateService); + const { registerLocaleFn }: ABP.Root = currentInjector.get(CORE_OPTIONS); const lang = sessionState.getLanguage() || 'en'; From a9fae7ff2973811eb19c2dd6b26f4f66b32071fe Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Tue, 29 Jul 2025 17:22:18 +0300 Subject: [PATCH 19/48] Move localeInitializer call to getInitialData The localeInitializer invocation was moved from the app initializer in core-module-config.provider.ts to the getInitialData function in initial-utils.ts. This change centralizes initialization logic and ensures locale setup occurs after initial data is loaded. --- .../core/src/lib/providers/core-module-config.provider.ts | 2 -- npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index 3ebe64360f..fa24f66e45 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -107,14 +107,12 @@ export function provideAbpCore(...features: CoreFeature[]) { }), ), provideAppInitializer(() => { - const injector = inject(Injector); inject(LocalizationService); inject(LocalStorageListenerService); inject(RoutesHandler); return (async (): Promise => { await getInitialData(); - await localeInitializer(injector); })(); }), LocaleProvider, diff --git a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts index 254d61896d..0c65073ef6 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/initial-utils.ts @@ -50,6 +50,7 @@ export async function getInitialData() { }), ); await lastValueFrom(result$); + await localeInitializer(injector); } export function localeInitializer(injector?: Injector) { From 4e462b577d3b6368d1c90217fe702df47ed9fb49 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 30 Jul 2025 09:27:56 +0300 Subject: [PATCH 20/48] Refactor app initializer to use async function Simplifies the app initializer by making it an async function directly instead of returning an async IIFE. This improves readability and maintains the same initialization behavior. --- .../core/src/lib/providers/core-module-config.provider.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index fa24f66e45..631f25217c 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -106,14 +106,11 @@ export function provideAbpCore(...features: CoreFeature[]) { headerName: 'RequestVerificationToken', }), ), - provideAppInitializer(() => { + provideAppInitializer(async () => { inject(LocalizationService); inject(LocalStorageListenerService); inject(RoutesHandler); - - return (async (): Promise => { - await getInitialData(); - })(); + await getInitialData(); }), LocaleProvider, CookieLanguageProvider, From de1db9761a909d3757bd4459919c55808f767691 Mon Sep 17 00:00:00 2001 From: sumeyye Date: Wed, 30 Jul 2025 09:32:16 +0300 Subject: [PATCH 21/48] update: remove unused imports --- .../core/src/lib/providers/core-module-config.provider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts index 631f25217c..e8ca5ea711 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/core-module-config.provider.ts @@ -1,4 +1,4 @@ -import { makeEnvironmentProviders, Provider, provideAppInitializer, inject, Injector } from '@angular/core'; +import { makeEnvironmentProviders, Provider, provideAppInitializer, inject } from '@angular/core'; import { TitleStrategy } from '@angular/router'; import { HTTP_INTERCEPTORS, @@ -23,7 +23,7 @@ import { ABP, SortableItem } from '../models'; import { AuthErrorFilterService } from '../abstracts'; import { DEFAULT_DYNAMIC_LAYOUTS } from '../constants'; import { LocalizationService, LocalStorageListenerService, AbpTitleStrategy } from '../services'; -import { DefaultQueueManager, getInitialData, localeInitializer } from '../utils'; +import { DefaultQueueManager, getInitialData } from '../utils'; import { CookieLanguageProvider, IncludeLocalizationResourcesProvider, LocaleProvider } from './'; import { TimezoneInterceptor } from '../interceptors'; From f5d1731793d0f15828ebefb383130a1cd90a9a88 Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Wed, 30 Jul 2025 12:06:26 +0300 Subject: [PATCH 22/48] Refactor OAuth guard to use OAuthService and timer Replaces usage of AuthService.isAuthenticated and interval with OAuthService.hasValidAccessToken() and timer for checking authentication status. Also injects EnvironmentService to access OAuth configuration and updates logic to check responseType before waiting for authentication. --- .../oauth/src/lib/guards/oauth.guard.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts index e7f31643aa..1311a8d7a8 100644 --- a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts +++ b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts @@ -6,10 +6,10 @@ import { CanActivateFn, } from '@angular/router'; -import { Observable, interval, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs'; +import { Observable, timer, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs'; import { OAuthService } from 'angular-oauth2-oidc'; -import { AuthService, IAbpGuard } from '@abp/ng.core'; +import { AuthService, IAbpGuard, EnvironmentService } from '@abp/ng.core'; /** * @deprecated Use `abpOAuthGuard` *function* instead. @@ -58,14 +58,17 @@ export const asyncAbpOAuthGuard: CanActivateFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, ) => { + const oAuthService = inject(OAuthService); const authService = inject(AuthService); - const code = new URLSearchParams(window.location.search).get('code'); + const environmentService = inject(EnvironmentService); + + const { oAuthConfig } = environmentService.getEnvironment(); - if (code) { + if (oAuthConfig?.responseType === 'code') { return firstValueFrom( - interval(100).pipe( - map(() => authService.isAuthenticated), - filter(isAuthenticated => isAuthenticated), + timer(0, 100).pipe( + map(() => oAuthService.hasValidAccessToken()), + filter(Boolean), take(1), timeout(3000), catchError(() => { @@ -76,7 +79,7 @@ export const asyncAbpOAuthGuard: CanActivateFn = ( ); } - if (authService.isAuthenticated) { + if (oAuthService.hasValidAccessToken()) { return true; } From 6f8c40d85793a3b3576be5fb01816e9bf382615f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 30 Jul 2025 15:09:44 +0300 Subject: [PATCH 23/48] Add link to the article --- docs/en/framework/architecture/multi-tenancy/index.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 0a035d110d..7a5017b369 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -42,6 +42,8 @@ ABP supports all the following approaches to store the tenant data in the databa [Saas module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches. +> You can see the community article *[Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)* for more details about different database architectures with practical implementation details. + ## Usage Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible. @@ -443,8 +445,9 @@ The [Tenant Management module](../../../modules/tenant-management.md) provides a ### A note about separate database per tenant approach in open source version While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [Saas module (PRO)](../../../modules/saas.md). -Alternatively you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. +Alternatively, you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. ## See Also * [Features](../../infrastructure/features.md) +* [Article: Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9) From 913578e3f7b926d8022d09fdccaccba7f21e4d5b Mon Sep 17 00:00:00 2001 From: sumeyye Date: Thu, 31 Jul 2025 09:57:23 +0300 Subject: [PATCH 24/48] remove: ngcc option check from the publish script --- npm/ng-packs/scripts/publish.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/npm/ng-packs/scripts/publish.ts b/npm/ng-packs/scripts/publish.ts index 56778c5a41..cedfcf3c44 100644 --- a/npm/ng-packs/scripts/publish.ts +++ b/npm/ng-packs/scripts/publish.ts @@ -47,7 +47,7 @@ program.parse(process.argv); if (program.preview) await replaceWithPreview(program.nextVersion); - await execa('yarn', ['build', '--noInstall', '--skipNgcc'], { stdout: 'inherit' }); + await execa('yarn', ['build', '--noInstall'], { stdout: 'inherit' }); await execa('yarn', ['build:schematics'], { stdout: 'inherit' }); } catch (error) { console.error(error.stderr); @@ -98,12 +98,7 @@ program.parse(process.argv); })(); async function updateVersion(version: string) { - - await execa( - 'yarn', - ['update-version', version], - { stdout: 'inherit', cwd: '../' }, - ); + await execa('yarn', ['update-version', version], { stdout: 'inherit', cwd: '../' }); await execa('yarn', ['replace-with-tilde']); } From 00a1950d09158e6c37904bd230096770983d484e Mon Sep 17 00:00:00 2001 From: berkansasmaz Date: Fri, 1 Aug 2025 09:53:36 +0300 Subject: [PATCH 25/48] docs: Update version mapping with new ABP Studio and ABP versions --- docs/en/studio/version-mapping.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index 51e0de5bbd..6fe3ba95be 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -4,6 +4,8 @@ This document provides a general overview of the relationship between various ve | **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| +| 1.1.2 | 9.2.3 | +| 1.1.0 - 1.1.1 | 9.2.2 | | 1.0.2 | 9.2.1 | | 1.0.1 | 9.2.0 | | 1.0.0 | 9.2.0 | From 6ddae0f82da817ab807ce23e118e7ec0a71f178a Mon Sep 17 00:00:00 2001 From: berkansasmaz Date: Fri, 1 Aug 2025 09:53:50 +0300 Subject: [PATCH 26/48] docs: Update release notes for versions 1.1.0 to 1.1.2 with new features and enhancements --- docs/en/studio/release-notes.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index d2db328fbf..5601fe11d0 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -2,6 +2,33 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. +## 1.1.2 (2025-07-31) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.3`) +* Configured LeptonX Lite logos in the templates. +* Added browser tab memory feature to remember previously selected tabs. +* Enhanced tools section with default credentials display for first-time tool usage. +* Improved module and package loading with better error handling. + +## 1.1.1 (2025-07-22) + +* Enhanced tools section with clear cookies option. +* Fixed language management module name display for imported modules. +* Improved update window messaging with "Skip this version" option. +* Fixed Docker Compose file issues in microservice template. +* Resolved RabbitMQ tool cookie problems. + +## 1.1.0 (2025-07-16) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.2`) +* Enhanced UI scaling for all windows and improved user experience. +* Added tools section in solution runner main area with basic Grafana dashboard for microservice template. +* Improved container management during application building. +* Enhanced background task exception handling. +* Added public account module package reference to Blazor WebApp client. +* Fixed tenant database context updating errors. +* Improved optional module selection UI with better documentation integration. + ## 1.0.2 (2025-06-24) * Enhanced the ABP NuGet package installation experience. From 1cd9c6985ea975a5e5f9da36b65a6db5f6c16efb Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Fri, 1 Aug 2025 10:34:07 +0300 Subject: [PATCH 27/48] Add AsyncPipe to component imports and declarations --- .../extensible-form/extensible-form-prop.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts index e31a3a46cc..b1ba5c3fe6 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -47,7 +47,7 @@ import { eExtensibleComponents } from '../../enums/components'; import { ExtensibleDateTimePickerComponent } from '../date-time-picker/extensible-date-time-picker.component'; import { NgxValidateCoreModule } from '@ngx-validate/core'; import { ExtensibleFormPropService } from '../../services/extensible-form-prop.service'; -import { NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-form-multiselect.component'; @@ -67,6 +67,7 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f ShowPasswordDirective, PermissionDirective, LocalizationPipe, + AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet, From 6df1a02c15896ce0fcd32eab3298a1e81740e4d7 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 1 Aug 2025 20:47:09 +0800 Subject: [PATCH 28/48] Enhance object mapping capabilities by adding collection mapping support and optimizing method invocation caching --- .../MapperlyAutoObjectMappingProvider.cs | 136 ++++++++++++++++++ .../Abp/ObjectMapping/DefaultObjectMapper.cs | 122 +++++++++------- .../Mapperly/AbpMapperlyModule_Basic_Tests.cs | 31 ++++ 3 files changed, 240 insertions(+), 49 deletions(-) diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index 7685aa03ac..a3a9abc234 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/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 : MapperlyAutoObjectMap public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider { + protected static readonly ConcurrentDictionary> MapCache = new(); + protected IServiceProvider ServiceProvider { get; } public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) @@ -28,6 +34,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider public virtual TDestination Map(object source) { + if (TryToMapCollection((TSource)source, default, out var collectionResult)) + { + return collectionResult; + } + var mapper = ServiceProvider.GetService>(); if (mapper != null) { @@ -54,6 +65,11 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider public virtual TDestination Map(TSource source, TDestination destination) { + if (TryToMapCollection(source, destination, out var collectionResult)) + { + return collectionResult; + } + var mapper = ServiceProvider.GetService>(); if (mapper != null) { @@ -80,6 +96,126 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider $" {TypeHelper.GetFullNameHandlingNullableAndGenerics(typeof(IAbpReverseMapperlyMapper))} was found"); } + protected virtual bool TryToMapCollection(TSource source, TDestination? destination, out TDestination collectionResult) + { + if (!DefaultObjectMapper.IsCollectionGenericType(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(); + var result = definitionGenericType.IsGenericType + ? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As() + : Array.CreateInstance(destinationArgumentType, sourceList.Count); + + if (destination != null && !destination.GetType().IsArray) + { + //Clear destination collection if destination not an array, We won't change array just same behavior as AutoMapper. + destination.As().Clear(); + } + + for (var i = 0; i < sourceList.Count; i++) + { + var invokeResult = destination == null + ? invoker(this, sourceList[i]!, null!) + : invoker(this, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!); + + if (definitionGenericType.IsGenericType) + { + result.Add(invokeResult); + destination?.As().Add(invokeResult); + } + else + { + result[i] = invokeResult; + } + } + + if (destination != null && destination.GetType().IsArray) + { + //Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper. + 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 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.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>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); + } + protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) { var extraProperties = new ExtraPropertyDictionary(); diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index 30733b1e24..6b0e723fbb 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/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 : DefaultObjectMapper, IObjectMapper< public class DefaultObjectMapper : IObjectMapper, ITransientDependency { - protected static ConcurrentDictionary MethodInfoCache { get; } = new ConcurrentDictionary(); + protected static readonly ConcurrentDictionary> 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.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.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.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(); 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(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) + protected virtual Func 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.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.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.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.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>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); + } + + public static bool IsCollectionGenericType(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(object source) 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 index 89861f6947..50a799e5a3 100644 --- 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 @@ -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, List>(new List + { + 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(); + { + new MyEntityDto() { Number = 44 }; + new MyEntityDto() { Number = 45 }; + } + + dto = _objectMapper.Map, List>(new List + { + 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() { From b89031b671450d2f9b3f9ced48179922bcfa29a6 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 2 Aug 2025 10:35:47 +0800 Subject: [PATCH 29/48] Add `ObjectMappingHelper` to enhance performance. --- .../MapperlyAutoObjectMappingProvider.cs | 2 +- .../Abp/ObjectMapping/DefaultObjectMapper.cs | 59 +------------ .../Abp/ObjectMapping/ObjectMappingHelper.cs | 85 +++++++++++++++++++ 3 files changed, 87 insertions(+), 59 deletions(-) create mode 100644 framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs index a3a9abc234..677649b79a 100644 --- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -98,7 +98,7 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider protected virtual bool TryToMapCollection(TSource source, TDestination? destination, out TDestination collectionResult) { - if (!DefaultObjectMapper.IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + if (!ObjectMappingHelper.IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { collectionResult = default!; return false; diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index 6b0e723fbb..faf8c4e258 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -123,7 +123,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency protected virtual bool TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult) { - if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + if (!ObjectMappingHelper.IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { collectionResult = default!; return false; @@ -242,63 +242,6 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return Expression.Lambda>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); } - public static bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) - { - sourceArgumentType = null!; - destinationArgumentType = null!; - definitionGenericType = null!; - - if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || - (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) - { - return false; - } - - var supportedCollectionTypes = new[] - { - typeof(IEnumerable<>), - typeof(ICollection<>), - typeof(Collection<>), - typeof(IList<>), - typeof(List<>) - }; - - if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition())) - { - sourceArgumentType = typeof(TSource).GenericTypeArguments[0]; - } - - if (typeof(TSource).IsArray) - { - sourceArgumentType = typeof(TSource).GetElementType()!; - } - - if (sourceArgumentType == null!) - { - return false; - } - - definitionGenericType = typeof(List<>); - if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition())) - { - destinationArgumentType = typeof(TDestination).GenericTypeArguments[0]; - - if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) || - typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>)) - { - definitionGenericType = typeof(Collection<>); - } - } - - if (typeof(TDestination).IsArray) - { - destinationArgumentType = typeof(TDestination).GetElementType()!; - definitionGenericType = typeof(Array); - } - - return destinationArgumentType != null!; - } - protected virtual TDestination AutoMap(object source) { return AutoObjectMappingProvider.Map(source); diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs new file mode 100644 index 0000000000..cbfd33988b --- /dev/null +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Volo.Abp.ObjectMapping; + +public static class ObjectMappingHelper +{ + private static readonly ConcurrentDictionary<(Type, Type), (Type sourceArgumentType, Type destinationArgumentType, Type definitionGenericType)?> Cache = new(); + + public static bool IsCollectionGenericType( + out Type sourceArgumentType, + out Type destinationArgumentType, + out Type definitionGenericType) + { + var cached = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), _ => IsCollectionGenericTypeInternal()); + if (cached == null) + { + sourceArgumentType = destinationArgumentType = definitionGenericType = null!; + return false; + } + + (sourceArgumentType, destinationArgumentType, definitionGenericType) = cached.Value; + return true; + } + + private static (Type, Type, Type)? IsCollectionGenericTypeInternal() + { + Type sourceArgumentType = null!; + Type destinationArgumentType = null!; + Type definitionGenericType = null!; + + if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || + (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) + { + return null; + } + + var supportedCollectionTypes = new[] + { + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(Collection<>), + typeof(IList<>), + typeof(List<>) + }; + + if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition())) + { + sourceArgumentType = typeof(TSource).GenericTypeArguments[0]; + } + + if (typeof(TSource).IsArray) + { + sourceArgumentType = typeof(TSource).GetElementType()!; + } + + if (sourceArgumentType == null) + { + return null; + } + + definitionGenericType = typeof(List<>); + if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition())) + { + destinationArgumentType = typeof(TDestination).GenericTypeArguments[0]; + + if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) || + typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>)) + { + definitionGenericType = typeof(Collection<>); + } + } + + if (typeof(TDestination).IsArray) + { + destinationArgumentType = typeof(TDestination).GetElementType()!; + definitionGenericType = typeof(Array); + } + + return (sourceArgumentType, destinationArgumentType, definitionGenericType); + } +} From 5c37d7b20bede457916031606eacfe9429308409 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 2 Aug 2025 10:46:52 +0800 Subject: [PATCH 30/48] [Identity] Replace AutoMapper with Mapperly. --- .../Volo.Abp.Identity.Application.csproj | 2 +- .../Identity/AbpIdentityApplicationMappers.cs | 19 ++++ .../Identity/AbpIdentityApplicationModule.cs | 11 +-- ...ntityApplicationModuleAutoMapperProfile.cs | 15 --- .../AbpIdentityBlazorAutoMapperProfile.cs | 18 ---- .../AbpIdentityBlazorMappers.cs | 25 +++++ .../AbpIdentityBlazorModule.cs | 13 +-- .../Volo.Abp.Identity.Blazor.csproj | 2 +- .../Volo.Abp.Identity.Domain.csproj | 2 +- .../Abp/Identity/AbpIdentityDomainModule.cs | 11 +-- .../Abp/Identity/IdentityDomainMappers.cs | 33 +++++++ .../Identity/IdentityDomainMappingProfile.cs | 15 --- .../AbpIdentityWebAutoMapperProfile.cs | 57 ----------- .../AbpIdentityWebMappers.cs | 94 +++++++++++++++++++ .../AbpIdentityWebModule.cs | 11 +-- .../Volo.Abp.Identity.Web.csproj | 2 +- 16 files changed, 188 insertions(+), 142 deletions(-) create mode 100644 modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs delete mode 100644 modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs delete mode 100644 modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs delete mode 100644 modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs delete mode 100644 modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs create mode 100644 modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj index 0b3693b5d1..7125af8060 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj @@ -18,7 +18,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs new file mode 100644 index 0000000000..164da27aae --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs @@ -0,0 +1,19 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Identity; +using Volo.Abp.Mapperly; + +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class IdentityRoleToIdentityRoleDtoMapper : MapperBase +{ + public override partial IdentityRoleDto Map(IdentityRole source); + public override partial void Map(IdentityRole source, IdentityRoleDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs index 673a0605c7..07865ab8f5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; @@ -8,18 +8,13 @@ namespace Volo.Abp.Identity; [DependsOn( typeof(AbpIdentityDomainModule), typeof(AbpIdentityApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpPermissionManagementApplicationModule) )] public class AbpIdentityApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs deleted file mode 100644 index a546a9bca1..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; - -namespace Volo.Abp.Identity; - -public class AbpIdentityApplicationModuleAutoMapperProfile : Profile -{ - public AbpIdentityApplicationModuleAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs deleted file mode 100644 index 52a450785e..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; - -namespace Volo.Abp.Identity.Blazor; - -public class AbpIdentityBlazorAutoMapperProfile : Profile -{ - public AbpIdentityBlazorAutoMapperProfile() - { - CreateMap() - .MapExtraProperties() - .Ignore(x => x.Password) - .Ignore(x => x.RoleNames); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs new file mode 100644 index 0000000000..614ca6ae32 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs @@ -0,0 +1,25 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Identity.Blazor; + +[Mapper] +[MapExtraProperties] +public partial class IdentityUserDtoToIdentityUserUpdateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial IdentityUserUpdateDto Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial void Map(IdentityUserDto source, IdentityUserUpdateDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class IdentityRoleDtoToIdentityRoleUpdateDtoMapper : MapperBase +{ + public override partial IdentityRoleUpdateDto Map(IdentityRoleDto source); + public override partial void Map(IdentityRoleDto source, IdentityRoleUpdateDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs index 3367b42a1b..89aba05f89 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs @@ -1,7 +1,7 @@ using Localization.Resources.AbpUi; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.BlazoriseUI; using Volo.Abp.Identity.Localization; using Volo.Abp.Localization; @@ -16,7 +16,7 @@ namespace Volo.Abp.Identity.Blazor; [DependsOn( typeof(AbpIdentityApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpPermissionManagementBlazorModule), typeof(AbpBlazoriseUIModule) )] @@ -26,12 +26,7 @@ public class AbpIdentityBlazorModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -42,7 +37,7 @@ public class AbpIdentityBlazorModule : AbpModule { options.AdditionalAssemblies.Add(typeof(AbpIdentityBlazorModule).Assembly); }); - + Configure(options => { options.Resources diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj b/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj index 355cb4294a..777fb1c31e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj @@ -8,7 +8,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj index fadd6e4109..a1bde79665 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj @@ -23,7 +23,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs index baf0871c58..590beb8e09 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Domain; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Modularity; @@ -19,7 +19,7 @@ namespace Volo.Abp.Identity; typeof(AbpDddDomainModule), typeof(AbpIdentityDomainSharedModule), typeof(AbpUsersDomainModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class AbpIdentityDomainModule : AbpModule { @@ -35,12 +35,7 @@ public class AbpIdentityDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs new file mode 100644 index 0000000000..f7d41c9891 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs @@ -0,0 +1,33 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.Users; + +namespace Volo.Abp.Identity; + +[Mapper] +public partial class IdentityUserToUserEtoMapper : MapperBase +{ + public override partial UserEto Map(IdentityUser source); + public override partial void Map(IdentityUser source, UserEto destination); +} + +[Mapper] +public partial class IdentityClaimTypeToIdentityClaimTypeEtoMapper : MapperBase +{ + public override partial IdentityClaimTypeEto Map(IdentityClaimType source); + public override partial void Map(IdentityClaimType source, IdentityClaimTypeEto destination); +} + +[Mapper] +public partial class IdentityRoleToIdentityRoleEtoMapper : MapperBase +{ + public override partial IdentityRoleEto Map(IdentityRole source); + public override partial void Map(IdentityRole source, IdentityRoleEto destination); +} + +[Mapper] +public partial class OrganizationUnitToOrganizationUnitEtoMapper : MapperBase +{ + public override partial OrganizationUnitEto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitEto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs deleted file mode 100644 index ae9e5b5065..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Abp.Users; - -namespace Volo.Abp.Identity; - -public class IdentityDomainMappingProfile : Profile -{ - public IdentityDomainMappingProfile() - { - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs deleted file mode 100644 index e032b3352f..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs +++ /dev/null @@ -1,57 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Abp.Identity.Web.Pages.Identity.Roles; -using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel; -using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel; - -namespace Volo.Abp.Identity.Web; - -public class AbpIdentityWebAutoMapperProfile : Profile -{ - public AbpIdentityWebAutoMapperProfile() - { - CreateUserMappings(); - CreateRoleMappings(); - } - - protected virtual void CreateUserMappings() - { - //List - CreateMap() - .Ignore(x => x.Password); - - //CreateModal - CreateMap() - .MapExtraProperties() - .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.IsAssigned, opt => opt.Ignore()); - - //EditModal - CreateMap() - .MapExtraProperties() - .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.IsAssigned, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) - .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()); - } - - protected virtual void CreateRoleMappings() - { - //List - CreateMap(); - - //CreateModal - CreateMap() - .MapExtraProperties(); - - //EditModal - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs new file mode 100644 index 0000000000..1877dac1d8 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs @@ -0,0 +1,94 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.Identity.Web.Pages.Identity.Roles; +using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel; +using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel; + +namespace Volo.Abp.Identity.Web; + +[Mapper] +public partial class IdentityUserDtoToEditUserModalModelUserInfoViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))] + public override partial EditUserModalModel.UserInfoViewModel Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))] + public override partial void Map(IdentityUserDto source, EditUserModalModel.UserInfoViewModel destination); +} + +[Mapper] +[MapExtraProperties] +public partial class CreateUserModalModelUserInfoViewModelToIdentityUserCreateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))] + public override partial IdentityUserCreateDto Map(CreateUserModalModel.UserInfoViewModel source); + + [MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))] + public override partial void Map(CreateUserModalModel.UserInfoViewModel source, IdentityUserCreateDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class IdentityRoleDtoToCreateUserModalModelAssignedRoleViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial CreateUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source); + + [MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial void Map(IdentityRoleDto source, CreateUserModalModel.AssignedRoleViewModel destination); +} + +[Mapper] +public partial class EditUserModalModelUserInfoViewModelToIdentityUserUpdateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial IdentityUserUpdateDto Map(EditUserModalModel.UserInfoViewModel source); + + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial void Map(EditUserModalModel.UserInfoViewModel source, IdentityUserUpdateDto destination); +} + +[Mapper] +public partial class IdentityRoleDtoToEditUserModalModelAssignedRoleViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial EditUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial void Map(IdentityRoleDto source, EditUserModalModel.AssignedRoleViewModel destination); +} + +[Mapper] +public partial class IdentityUserDtoToEditUserModalModelDetailViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))] + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))] + public override partial EditUserModalModel.DetailViewModel Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))] + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))] + public override partial void Map(IdentityUserDto source, EditUserModalModel.DetailViewModel destination); +} + +[Mapper] +public partial class IdentityRoleDtoToEditModalModelRoleInfoModelMapper : MapperBase +{ + public override partial EditModalModel.RoleInfoModel Map(IdentityRoleDto source); + public override partial void Map(IdentityRoleDto source, EditModalModel.RoleInfoModel destination); +} + +[Mapper] +[MapExtraProperties] +public partial class CreateModalModelRoleInfoModelToIdentityRoleCreateDtoMapper : MapperBase +{ + public override partial IdentityRoleCreateDto Map(CreateModalModel.RoleInfoModel source); + public override partial void Map(CreateModalModel.RoleInfoModel source, IdentityRoleCreateDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class EditModalModelRoleInfoModelToIdentityRoleUpdateDtoMapper : MapperBase +{ + public override partial IdentityRoleUpdateDto Map(EditModalModel.RoleInfoModel source); + public override partial void Map(EditModalModel.RoleInfoModel source, IdentityRoleUpdateDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs index ae3e8ebc72..6d2faf6467 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.PageToolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Identity.Localization; using Volo.Abp.Identity.Web.Navigation; @@ -19,7 +19,7 @@ using Volo.Abp.Threading; namespace Volo.Abp.Identity.Web; [DependsOn(typeof(AbpIdentityApplicationContractsModule))] -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] [DependsOn(typeof(AbpPermissionManagementWebModule))] [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] public class AbpIdentityWebModule : AbpModule @@ -51,12 +51,7 @@ public class AbpIdentityWebModule : AbpModule options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj b/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj index 8a77024b52..54bd5fd28b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj @@ -40,7 +40,7 @@ - + From e1eeb866175b51db3ddaabec32034bc9316ea3de Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 2 Aug 2025 14:37:44 +0800 Subject: [PATCH 31/48] Add `AutoMapper` reference in `Account` to fix the build problem. --- .../Volo.Abp.Account.Application.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj index d8c149f8b5..dd23b42528 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj @@ -21,6 +21,7 @@ + From 7c75110d6d250c5a1ceb49e726874de958bc724f Mon Sep 17 00:00:00 2001 From: Fahri Gedik Date: Sun, 3 Aug 2025 00:57:07 +0300 Subject: [PATCH 32/48] Add Czech translations for Marked Items and comments Added new Czech localization strings for Marked Items, favorites, and comment approval features in cs.json. This update supports new UI elements and system messages related to marking items, filtering favorites, and comment moderation. --- .../CmsKit/Localization/Resources/cs.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json index c530dd064b..17e549f7a7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/cs.json @@ -15,6 +15,7 @@ "CmsKit.Ratings": "Hodnocení", "CmsKit.Reactions": "Reakce", "CmsKit.Tags": "Tagy", + "CmsKit.MarkedItems": "Označené položky", "CmsKit:0002": "Obsah již existuje!", "CmsKit:0003": "Entitu {0} nelze označit.", "CmsKit:Blog:0001": "Daný slimák ({Slug}) již existuje!", @@ -26,8 +27,19 @@ "CmsKit:Rating:0001": "Entitu {EntityType} nelze hodnotit.", "CmsKit:Reaction:0001": "Entita {EntityType} nemůže mít reakce.", "CmsKit:Tag:0002": "Entitu nelze označit!", + "CmsKit:MarkedItem:ToggleConfirmation": "Opravdu chcete přepnout označenou položku?", + "ToggleFavorite": "Přidat/Odebrat z oblíbených", + "FavoritesFilterMessage": "Pro filtrování oblíbených se přihlaste", + "FilterOnFavorites": "Filtrovat podle oblíbených", "CommentAuthorizationExceptionMessage": "Tyto komentáře nejsou povoleny pro veřejné zobrazení.", + "CmsKit:Modals:Login": "Přihlášení", + "CmsKit:Modals:LoginModalDefaultMessage": "Pro pokračování se přihlaste!", + "CmsKit:Modals:YouAreNotAuthenticated": "Tato operace pro vás není autorizována.", "CommentDeletionConfirmationMessage": "Tento komentář a všechny odpovědi budou smazány!", + "CmsKit:MarkedItem:0001": "Entitu {EntityType} nelze označit.", + "CmsKit:MarkedItem:0002": "Pro typ entity '{EntityType}' nebyla nalezena žádná definice.", + "CmsKit:MarkedItem:0003": "Definice pro typ entity '{EntityType}' již existuje. Každý typ entity by měl mít pouze jednu definici.", + "CmsKit:MarkedItem:LoginMessage": "Pro označení této položky se přihlaste.", "Comments": "Komentáře", "Content": "Obsah", "ContentDeletionConfirmationMessage": "Opravdu chcete tento obsah smazat?", @@ -134,6 +146,7 @@ "SamplePageMessage": "Ukázková stránka pro modul Pro", "SaveChanges": "Uložit změny", "Script": "Skript", + "SelectLayout": "Vybrat rozložení", "SelectAll": "Vybrat vše", "Send": "Poslat", "SendMessage": "Poslat zprávu", @@ -216,6 +229,8 @@ "Feature:ReactionEnableDescription": "Systém reakcí CMS Kit, který umožňuje uživatelům posílat reakce entitám, jako jsou BlogPost, Komentáře atd.", "Feature:TagEnable": "Označování", "Feature:TagEnableDescription": "Systém značek CMS Kit, který umožňuje označování entit, jako je BlogPost.", + "Feature:MarkedItemEnable": "Označená položka", + "Feature:MarkedItemEnableDescription": "Systém označování CMS Kit, který umožňuje uživatelům označovat entity jako oblíbené.", "DeleteBlogPostMessage": "Blog bude smazán. Jsi si jistá?", "CaptchaCode": "Captcha kód", "CommentTextRequired": "Komentář je povinný", @@ -230,6 +245,37 @@ "TagsHelpText": "Značky by měly být odděleny čárkami (např.: tag1, tag2, tag3)", "ThisPartOfContentCouldntBeLoaded": "Tato část obsahu nemohla být načtena.", "DuplicateCommentAttemptMessage": "Byl zjištěn duplicitní pokus o vložení komentáře. Váš komentář již byl odeslán.", + "NoBlogPostYet": "Zatím žádný blogový příspěvek!", + "CmsKit:Comment": "Komentář", + "CmsKitCommentOptions:RequireApprovement": "Vyžadovat schválení komentářů", + "CmsKitCommentOptions:RequireApprovementDescription": "Když je povoleno, komentáře budou vyžadovat schválení před zveřejněním.", + "CommentFilter:ApproveState": "Stav schválení", + "ApproveState": "Stav schválení", + "CommentFilter:0": "Vše", + "CommentFilter:1": "Schváleno", + "CommentFilter:2": "Zamítnuto", + "CommentFilter:4": "Čeká", + "ApprovedSuccessfully": "Úspěšně schváleno", + "ApprovalRevokedSuccessfully": "Schválení úspěšně odvoláno", + "Approve": "Schválit", + "Disapproved": "Zamítnuto", + "CommentAlertMessage": "Čeká {0} komentářů na schválení", + "Settings:Menu:CmsKit": "CMS", + "CommentsAwaitingApproval": "Komentáře čekající na schválení", + "JustNow": "Právě teď", + "MinuteAgo": "před 1 minutou", + "MinutesAgo": "před {0} minutami", + "HourAgo": "před 1 hodinou", + "HoursAgo": "před {0} hodinami", + "YesterdayAt": "Včera v {0}", + "DayAt": "{0} v {1}", + "MonthDayAt": "{0} {1} v {2}", + "FullDate": "{0} {1} {2}", + "Minute": "m", + "Hour": "h", + "Day": "d", + "Week": "t", + "CommentSubmittedForApproval": "Váš komentář byl odeslán ke schválení.", "ChooseAnActionForBlog": "Vyberte akci pro blog", "AssignBlogPostsToOtherBlog": "Přiřaďte blogové příspěvky k jinému blogu", "SelectAnBlogToAssign": "Vyberte blog, ke kterému chcete přiřadit blogové příspěvky", From 25f8983c2bc394c060f0560429f3cef0c5dbf67b Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 3 Aug 2025 11:58:57 +0800 Subject: [PATCH 33/48] Replace AutoMapper with Mapperly in the account module. --- .../Volo.Abp.Account.Application.csproj | 2 +- .../Account/AbpAccountApplicationMappers.cs | 21 +++++++++++++++ .../Account/AbpAccountApplicationModule.cs | 10 +++---- ...countApplicationModuleAutoMapperProfile.cs | 15 ----------- .../AbpAccountBlazorAutoMapperProfile.cs | 20 -------------- .../AbpAccountBlazorMappers.cs | 27 +++++++++++++++++++ .../AbpAccountBlazorModule.cs | 15 ++++------- .../Volo.Abp.Account.Blazor.csproj | 2 +- .../AbpAccountWebAutomapperProfile.cs | 15 ----------- .../AbpAccountWebMappers.cs | 13 +++++++++ .../AbpAccountWebModule.cs | 14 ++++------ .../Volo.Abp.Account.Web.csproj | 2 +- 12 files changed, 77 insertions(+), 79 deletions(-) create mode 100644 modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs delete mode 100644 modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs delete mode 100644 modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs create mode 100644 modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs delete mode 100644 modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs create mode 100644 modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj index dd23b42528..b4530a5dd7 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj @@ -21,7 +21,7 @@ - + diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs new file mode 100644 index 0000000000..dcfaf275dd --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs @@ -0,0 +1,21 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Identity; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Account; + +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToProfileDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(ProfileDto.HasPassword))] + public override partial ProfileDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(ProfileDto.HasPassword))] + public override partial void Map(IdentityUser source, ProfileDto destination); + + public override void AfterMap(IdentityUser source, ProfileDto destination) + { + destination.HasPassword = source.PasswordHash != null; + } +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs index 283e06a934..04f25d33d8 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs @@ -1,4 +1,4 @@ -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -12,7 +12,8 @@ namespace Volo.Abp.Account; typeof(AbpAccountApplicationContractsModule), typeof(AbpIdentityApplicationModule), typeof(AbpUiNavigationModule), - typeof(AbpEmailingModule) + typeof(AbpEmailingModule), + typeof(AbpMapperlyModule) )] public class AbpAccountApplicationModule : AbpModule { @@ -23,11 +24,6 @@ public class AbpAccountApplicationModule : AbpModule options.FileSets.AddEmbedded(); }); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs deleted file mode 100644 index 0c117d98d0..0000000000 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Abp.Identity; - -namespace Volo.Abp.Account; - -public class AbpAccountApplicationModuleAutoMapperProfile : Profile -{ - public AbpAccountApplicationModuleAutoMapperProfile() - { - CreateMap() - .ForMember(dest => dest.HasPassword, - op => op.MapFrom(src => src.PasswordHash != null)) - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs deleted file mode 100644 index af59d75321..0000000000 --- a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AutoMapper; -using Volo.Abp.Account.Blazor.Pages.Account; -using Volo.Abp.AutoMapper; -using Volo.Abp.Identity; - -namespace Volo.Abp.Account.Blazor; - -public class AbpAccountBlazorAutoMapperProfile : Profile -{ - public AbpAccountBlazorAutoMapperProfile() - { - CreateMap() - .MapExtraProperties() - .Ignore(x => x.PhoneNumberConfirmed) - .Ignore(x => x.EmailConfirmed); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs new file mode 100644 index 0000000000..98ef9f9ab6 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs @@ -0,0 +1,27 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Account.Blazor.Pages.Account; +using Volo.Abp.Mapperly; +using Volo.Abp.Identity; + +namespace Volo.Abp.Account.Blazor; + +[Mapper] +[MapExtraProperties] +public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial PersonalInfoModel Map(ProfileDto source); + + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial void Map(ProfileDto source, PersonalInfoModel destination); +} + +[Mapper] +[MapExtraProperties] +public partial class PersonalInfoModelToUpdateProfileDtoMapper : MapperBase +{ + public override partial UpdateProfileDto Map(PersonalInfoModel source); + public override partial void Map(PersonalInfoModel source, UpdateProfileDto destination); +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs index 61b42e1807..6a44f156ee 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs +++ b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs @@ -2,7 +2,7 @@ using Volo.Abp.Account.Blazor.Pages.Account; using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending.Modularity; @@ -13,21 +13,16 @@ namespace Volo.Abp.Account.Blazor; [DependsOn( typeof(AbpAspNetCoreComponentsWebThemingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAccountApplicationContractsModule) )] public class AbpAccountBlazorModule : AbpModule { private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner(); - + public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -39,7 +34,7 @@ public class AbpAccountBlazorModule : AbpModule options.AdditionalAssemblies.Add(typeof(AbpAccountBlazorModule).Assembly); }); } - + public override void PostConfigureServices(ServiceConfigurationContext context) { OneTimeRunner.Run(() => diff --git a/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj b/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj index 0eaa3cd978..7590216d06 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj +++ b/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj @@ -10,7 +10,7 @@ - + diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs deleted file mode 100644 index cdc92e8b19..0000000000 --- a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Volo.Abp.Account.Web.Pages.Account; -using Volo.Abp.Identity; -using AutoMapper; -using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo; - -namespace Volo.Abp.Account.Web; - -public class AbpAccountWebAutoMapperProfile : Profile -{ - public AbpAccountWebAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs new file mode 100644 index 0000000000..09a2e8373c --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Account.Web; + +[Mapper] +[MapExtraProperties] +public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase +{ + public override partial AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel Map(ProfileDto source); + public override partial void Map(ProfileDto source, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel destination); +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs index 1b41deaff7..f246beed84 100644 --- a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs +++ b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs @@ -8,7 +8,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.ExceptionHandling; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Identity.AspNetCore; @@ -24,14 +24,14 @@ namespace Volo.Abp.Account.Web; [DependsOn( typeof(AbpAccountApplicationContractsModule), typeof(AbpIdentityAspNetCoreModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpExceptionHandlingModule) )] public class AbpAccountWebModule : AbpModule { private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner(); - + public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.PreConfigure(options => @@ -59,11 +59,7 @@ public class AbpAccountWebModule : AbpModule ConfigureProfileManagementPage(); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -96,7 +92,7 @@ public class AbpAccountWebModule : AbpModule }); } - + public override void PostConfigureServices(ServiceConfigurationContext context) { OneTimeRunner.Run(() => diff --git a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj index efedb773fe..3f52eae0a4 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj +++ b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj @@ -39,7 +39,7 @@ - + From a75b8a2d6a1fa9da74fd9245c880c1f2dd3e8f02 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 3 Aug 2025 18:25:07 +0800 Subject: [PATCH 34/48] Add Mapperly integration documentation Resolve #23424 --- .../fundamentals/object-extensions.md | 16 +++ .../object-to-object-mapping.md | 111 +++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/docs/en/framework/fundamentals/object-extensions.md b/docs/en/framework/fundamentals/object-extensions.md index b12be63dc0..2e01b3b1e3 100644 --- a/docs/en/framework/fundamentals/object-extensions.md +++ b/docs/en/framework/fundamentals/object-extensions.md @@ -394,6 +394,22 @@ public class MyProfile : Profile It has the same parameters with the `MapExtraPropertiesTo` method. +#### Mapperly Integration + +If you're using the [Mapperly](https://github.com/riok/mapperly) library, the ABP also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. + +You can use the `MapExtraProperties` attribute to Mapperly class. + +````csharp +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToProfileDtoMapper : MapperBase +{ + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} +```` + ## Entity Framework Core Database Mapping If you're using the EF Core, you can map an extra property to a table field in the database. Example: diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 8961d804c7..f841907c05 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -84,7 +84,7 @@ public class UserAppService : ApplicationService } ```` -You should have defined the mappings before to be able to map objects. See the AutoMapper integration section to learn how to define mappings. +You should have defined the mappings before to be able to map objects. See the AutoMapper/Mapperly integration section to learn how to define mappings. ## AutoMapper Integration @@ -217,13 +217,102 @@ public class MyProfile : Profile } ```` +## Mapperly Integration + +[Mapperly](https://github.com/riok/mapperly) is a .NET source generator for generating object mappings. [Volo.Abp.Mapperly](https://www.nuget.org/packages/Volo.Abp.Mapperly) package defines the Mapperly integration for the `IObjectMapper`. + +Once you define mappings class as below, you can use the `IObjectMapper` interface just like explained before. + +### Define Mapping Classes + +You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process. + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase` class. + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); +} +```` + +### Before and After Mapping Methods + +The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping. + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial void BeforeMap(User source) + { + //TODO: Perform actions before the mapping + } + + public override partial void AfterMap(User source, UserDto destination) + { + //TODO: Perform actions after the mapping + } + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); + + public override partial void BeforeReverseMap(UserDto destination) + { + //TODO: Perform actions before the reverse mapping + } + + public override partial void AfterReverseMap(UserDto destination, User source) + { + //TODO: Perform actions after the reverse mapping + } +} +```` + +### Mapping the Object Extensions + +[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects. + +````csharp +[Mapper] +[MapExtraProperties] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more. + +### More Mapperly Features + +Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. + ## Advanced Topics ### IObjectMapper Interface -Assume that you have created a **reusable module** which defines AutoMapper profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). +Assume that you have created a **reusable module** which defines AutoMapper/Mapperly profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). -`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper even if the final application uses another default object mapping library. +`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper/Mapperly library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper/Mapperly even if the final application uses another default object mapping library. `IObjectMapper` is used to contextualize the object mapper, so you can use different libraries for different modules/contexts. @@ -281,6 +370,8 @@ public class UserAppService : ApplicationService While using the contextualized object mapper is same as the normal object mapper, you should register the contextualized mapper in your module's `ConfigureServices` method: +When using AutoMapper: + ````csharp [DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule @@ -298,6 +389,20 @@ public class MyModule : AbpModule } ```` +When using Mapperly: + +````csharp +[DependsOn(typeof(AbpMapperlyModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Use Mapperly for MyModule + context.Services.AddMapperlyObjectMapper(); + } +} +```` + `IObjectMapper` is an essential feature for a reusable module where it can be used in multiple applications each may use a different library for object to object mapping. All pre-built ABP modules are using it. But, for the final application, you can ignore this interface and always use the default `IObjectMapper` interface. ### IObjectMapper Interface From 981a014031e721c26952c74f646cd5084c97d3ba Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 3 Aug 2025 19:01:03 +0800 Subject: [PATCH 35/48] Add AutoMapper to Mapperly migration guide --- .../AutoMapper-To-Mapperly.md | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md diff --git a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md new file mode 100644 index 0000000000..4311d00214 --- /dev/null +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -0,0 +1,191 @@ +# Migrating from AutoMapper to Mapperly + +## Introduction + +The AutoMapper library is no longer free for commercial use. See [this article](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/) for more details. + +ABP framework provides the AutoMapper and Mapperly integration. If you have a project that uses AutoMapper and don't have any license for AutoMapper, you can migrate to Mapperly by following the steps below. + +## Migration Steps + +Please open your project with the IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform a global search and replace. + +1. Replace `Volo.Abp.AutoMapper` to `Volo.Abp.Mapperly` in all `*.csproj` files. + +2. Replace `using Volo.Abp.AutoMapper;` to `using Volo.Abp.Mapperly;` in all `*.cs` files. + +3. Replace `AbpAutoMapperModule` with `AbpMapperlyModule` in all `*.cs` files. + +4. Replace `AddAutoMapperObjectMapper` to `AddMapperlyObjectMapper` in all `*.cs` files. + +5. Remove `Configure` code section. + +6. Check the AutoMapper's `Profile` class to add all new Mapperly mapping classes. + +## Example + +Here is an AutoMapper's `Profile` class: + +```csharp +public class ExampleAutoMapper : Profile +{ + public AbpIdentityApplicationModuleAutoMapperProfile() + { + CreateMap() + .MapExtraProperties() + .Ignore(x => x.IsLockedOut) + .Ignore(x => x.SupportTwoFactor) + .Ignore(x => x.RoleNames); + + CreateMap(); + + CreateMap() + .MapExtraProperties(); + + CreateMap() + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.RoleId, src => src.MapFrom(r => r.Id)); + + CreateMap() + .ForMember(dest => dest.Active, src => src.MapFrom(r => r.IsActive ? "Yes" : "No")) + .ForMember(dest => dest.EmailConfirmed, src => src.MapFrom(r => r.EmailConfirmed ? "Yes" : "No")) + .ForMember(dest => dest.TwoFactorEnabled, src => src.MapFrom(r => r.TwoFactorEnabled ? "Yes" : "No")) + .ForMember(dest => dest.AccountLookout, src => src.MapFrom(r => r.LockoutEnd != null && r.LockoutEnd > DateTime.UtcNow ? "Yes" : "No")) + .Ignore(x => x.Roles); + } +} +``` + +And the Mapperly mapping class: + +```csharp +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial IdentityUserDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} + +[Mapper] +public partial class IdentityUserClaimToIdentityUserClaimDtoMapper : MapperBase +{ + public override partial IdentityUserClaimDto Map(IdentityUserClaim source); + + public override partial void Map(IdentityUserClaim source, IdentityUserClaimDto destination); +} + +[Mapper] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitDtoMapper : MapperBase +{ + public override partial OrganizationUnitDto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitDto destination); +} + +[Mapper] +public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase +{ + public override partial OrganizationUnitRoleDto Map(OrganizationUnitRole source); + public override partial void Map(OrganizationUnitRole source, OrganizationUnitRoleDto destination); + + public override partial OrganizationUnitRole ReverseMap(OrganizationUnitRoleDto destination); + public override partial void ReverseMap(OrganizationUnitRoleDto destination, OrganizationUnitRole source); +} + +[Mapper] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial OrganizationUnitWithDetailsDto Map(OrganizationUnit source); + + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial void Map(OrganizationUnit source, OrganizationUnitWithDetailsDto destination); +} + +[Mapper] +public partial class IdentityRoleToOrganizationUnitRoleDtoMapper : MapperBase +{ + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial OrganizationUnitRoleDto Map(IdentityRole source); + + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial void Map(IdentityRole source, OrganizationUnitRoleDto destination); +} + +[Mapper] +public partial class IdentityUserToIdentityUserExportDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial IdentityUserExportDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial void Map(IdentityUser source, IdentityUserExportDto destination); + + public override void AfterMap(IdentityUser source, IdentityUserExportDto destination) + { + destination.Active = source.IsActive ? "Yes" : "No"; + destination.EmailConfirmed = source.EmailConfirmed ? "Yes" : "No"; + destination.TwoFactorEnabled = source.TwoFactorEnabled ? "Yes" : "No"; + destination.AccountLookout = source.LockoutEnd != null && source.LockoutEnd > DateTime.UtcNow ? "Yes" : "No"; + } +} +``` + +## Mapperly Mapping Class + +You need to create a new Mapperly mapping class for each source and destination type. + +The `Mapper` attribute is used to mark the class as a Mapperly mapping class. + +The `MapperIgnoreTarget` attribute is used to replace the `Ignore` method. + +The `MapExtraProperties` attribute is used to replace the `MapExtraProperties` method. + +The `TwoWayMapperBase` class is used to replace the `ReverseMap` method. + +The `AfterMap` method is used to perform actions after the mapping. + +### Dependency Injection in Mapper Class + +All the Mapperly mapping classes will be added to the DI container. If you want to inject a service in your Mapper class, You just need to add the service to the constructor of the Mapper class. + +```csharp +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + public IdentityUserToIdentityUserDtoMapper(MyService myService) + { + _myService = myService; + } + + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); + + public override void AfterMap(IdentityUser source, IdentityUserDto destination) + { + destination.MyProperty = _myService.GetMyProperty(source.MyProperty); + } +} +``` + +## Mapperly Documentation + +Please refer to the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. + +Key points: + +- [Mapperly Configuration](https://mapperly.riok.app/docs/configuration/mapper/) +- [Mapperly Enums](https://mapperly.riok.app/docs/configuration/enum/) +- [Mapperly Flattening and unflattening](https://mapperly.riok.app/docs/configuration/flattening/) From bf7349ba3cd9cddd10d060006b30e2fb5636898e Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 4 Aug 2025 20:54:42 +0800 Subject: [PATCH 36/48] Refactor collection type handling in ObjectMappingHelper and enhance tests for mapping collections --- .../Abp/ObjectMapping/ObjectMappingHelper.cs | 56 +++++++++---------- .../Mapperly/AbpMapperlyModule_Basic_Tests.cs | 25 ++++++++- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs index cbfd33988b..5949edaba9 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs @@ -28,16 +28,17 @@ public static class ObjectMappingHelper private static (Type, Type, Type)? IsCollectionGenericTypeInternal() { - Type sourceArgumentType = null!; - Type destinationArgumentType = null!; - Type definitionGenericType = null!; - - if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || - (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) + if (!IsCollectionGenericTypeInternal(typeof(TSource), out var sourceArgumentType, out _) || + !IsCollectionGenericTypeInternal(typeof(TDestination), out var destinationArgumentType, out var definitionGenericType)) { return null; } + return (sourceArgumentType, destinationArgumentType, definitionGenericType); + } + + private static bool IsCollectionGenericTypeInternal(Type type, out Type elementType, out Type definitionGenericType) + { var supportedCollectionTypes = new[] { typeof(IEnumerable<>), @@ -47,39 +48,34 @@ public static class ObjectMappingHelper typeof(List<>) }; - if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition())) - { - sourceArgumentType = typeof(TSource).GenericTypeArguments[0]; - } - - if (typeof(TSource).IsArray) - { - sourceArgumentType = typeof(TSource).GetElementType()!; - } - - if (sourceArgumentType == null) + if (type.IsArray) { - return null; + elementType = type.GetElementType()!; + definitionGenericType = type; + return true; } - definitionGenericType = typeof(List<>); - if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition())) + if (type.IsGenericType && + supportedCollectionTypes.Contains(type.GetGenericTypeDefinition()) || + type.GetInterfaces().Any(i => i.IsGenericType && supportedCollectionTypes.Contains(i.GetGenericTypeDefinition()))) { - destinationArgumentType = typeof(TDestination).GenericTypeArguments[0]; + elementType = type.GetGenericArguments()[0]; + definitionGenericType = type.GetGenericTypeDefinition(); + if (definitionGenericType == typeof(IEnumerable<>) || + definitionGenericType == typeof(IList<>)) + { + definitionGenericType = typeof(List<>); + } - if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) || - typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>)) + if (definitionGenericType == typeof(ICollection<>)) { definitionGenericType = typeof(Collection<>); } + return true; } - if (typeof(TDestination).IsArray) - { - destinationArgumentType = typeof(TDestination).GetElementType()!; - definitionGenericType = typeof(Array); - } - - return (sourceArgumentType, destinationArgumentType, definitionGenericType); + elementType = null!; + definitionGenericType = null!; + return false; } } 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 index 50a799e5a3..3f0773f0a1 100644 --- 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 @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Mapperly.SampleClasses; @@ -55,13 +57,23 @@ public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest, MyEntityDto[]>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }.AsReadOnly()); + + dto2.Length.ShouldBe(2); + dto2[0].Number.ShouldBe(42); + dto2[1].Number.ShouldBe(43); + var dtoList = new List(); { new MyEntityDto() { Number = 44 }; new MyEntityDto() { Number = 45 }; } - dto = _objectMapper.Map, List>(new List + _objectMapper.Map, List>(new List { new MyEntity { Number = 42 }, new MyEntity { Number = 43 } @@ -70,6 +82,17 @@ public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest, MyEntityDto[]>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }.AsReadOnly(), dtoArray); + + dtoArray.Length.ShouldBe(2); + dtoArray[0].Number.ShouldBe(42); + dtoArray[1].Number.ShouldBe(43); } [Fact] From e4942b2ba72af2d2f672ff885acb549258e6f1f8 Mon Sep 17 00:00:00 2001 From: Enis Necipoglu Date: Tue, 5 Aug 2025 08:40:56 +0300 Subject: [PATCH 37/48] Update C# code samples for correct namespace usages on Blazor --- docs/en/tutorials/todo/layered/index.md | 116 +++++++++++++----------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index 03c06f75c9..98d0a0286e 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -171,13 +171,13 @@ This application has a single [entity](../../../framework/architecture/domain-dr using System; using Volo.Abp.Domain.Entities; -namespace TodoApp +namespace TodoApp; + +public class TodoItem : BasicAggregateRoot { - public class TodoItem : BasicAggregateRoot - { - public string Text { get; set; } = string.Empty; - } + public string Text { get; set; } = string.Empty; } + ```` `BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. @@ -281,15 +281,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace TodoApp +namespace TodoApp; + +public interface ITodoAppService : IApplicationService { - public interface ITodoAppService : IApplicationService - { - Task> GetListAsync(); - Task CreateAsync(string text); - Task DeleteAsync(Guid id); - } + Task> GetListAsync(); + Task CreateAsync(string text); + Task DeleteAsync(Guid id); } + ```` ### Data Transfer Object @@ -299,14 +299,14 @@ namespace TodoApp ````csharp using System; -namespace TodoApp +namespace TodoApp; + +public class TodoItemDto { - public class TodoItemDto - { - public Guid Id { get; set; } - public string Text { get; set; } = string.Empty; - } + public Guid Id { get; set; } + public string Text { get; set; } = string.Empty; } + ```` This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`. @@ -323,19 +323,18 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace TodoApp +namespace TodoApp; + +public class TodoAppService : ApplicationService, ITodoAppService { - public class TodoAppService : ApplicationService, ITodoAppService - { - private readonly IRepository _todoItemRepository; + private readonly IRepository _todoItemRepository; - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; } + + // TODO: Implement the methods here... } ```` @@ -412,8 +411,8 @@ Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* proje using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Web.Pages -{ +namespace TodoApp.Web.Pages; + public class IndexModel : TodoAppPageModel { public List TodoItems { get; set; } @@ -430,7 +429,7 @@ namespace TodoApp.Web.Pages TodoItems = await _todoAppService.GetListAsync(); } } -} + ```` This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. @@ -578,34 +577,41 @@ using Microsoft.AspNetCore.Components; using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Blazor.Pages +{{if UI=="Blazor" || UI=="BlazorWebApp"}} +namespace TodoApp.Blazor.Client.Pages; +{{else if UI=="BlazorServer"}} +namespace TodoApp.Blazor.Pages; +{{else if UI=="MAUIBlazor"}} +namespace TodoApp.MauiBlazor.Pages; +{{end}} + +namespace TodoApp.Blazor.Pages; + +public partial class Index { - public partial class Index - { - [Inject] - private ITodoAppService TodoAppService { get; set; } + [Inject] + private ITodoAppService TodoAppService { get; set; } - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } = string.Empty; + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } = string.Empty; - protected override async Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); } } ``` From 76cb91ef0ee1e6611a53d27fcbfb9e2e330e3962 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 5 Aug 2025 13:49:35 +0800 Subject: [PATCH 38/48] Return a new list from `WebRequestResources`. Otherwise, the content in `Resources` may return inconsistent data. --- .../Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs index e68b671fc6..ee3c1730dc 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs @@ -30,6 +30,6 @@ public class WebRequestResources : IWebRequestResources, IScopedDependency } Resources.Add(path, resources); - return resources; + return resources.ToList(); } } From f30ae61213de4b6074c1b95c851c78f87209768d Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Tue, 5 Aug 2025 06:10:51 +0000 Subject: [PATCH 39/48] Update part-05.md --- docs/en/tutorials/modular-crm/part-05.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 139c4a9a8b..d2082667aa 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -167,7 +167,7 @@ The `ReplaceDbContext` attribute allows the use of the `ModularCrmDbContext` cla ````csharp public class ModularCrmDbContext : AbpDbContext, - IProductsDbContext, + ICatalogDbContext, IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE { public DbSet Products { get; set; } From 1314d1a975ce35238d634b8d0cad478f970d8cdb Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 5 Aug 2025 15:09:13 +0800 Subject: [PATCH 40/48] Add comments to clarify the return value in `WebRequestResources`. --- .../Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs index ee3c1730dc..4e346b1274 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs @@ -30,6 +30,8 @@ public class WebRequestResources : IWebRequestResources, IScopedDependency } Resources.Add(path, resources); + + // Create a new list as return value to avoid any changes to the list will affect the value in Resources dictionary. return resources.ToList(); } } From 1cba552e1ac37bbffc09049c88107391bdf5b85a Mon Sep 17 00:00:00 2001 From: Enis Necipoglu Date: Tue, 5 Aug 2025 13:50:04 +0300 Subject: [PATCH 41/48] Remove redundant namespace declaration in Blazor tutorial --- docs/en/tutorials/todo/layered/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index 98d0a0286e..e2279e3d57 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -585,8 +585,6 @@ namespace TodoApp.Blazor.Pages; namespace TodoApp.MauiBlazor.Pages; {{end}} -namespace TodoApp.Blazor.Pages; - public partial class Index { [Inject] From 3a158c53fb59b65fc4c9b28f4f68f1bb7856af05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87elik?= Date: Tue, 5 Aug 2025 17:14:24 +0300 Subject: [PATCH 42/48] Add localization for CRUD page creation Added English localization strings for 'Create CRUD Page' and its option, describing the generation of a sample CRUD page with a Book entity. --- .../AbpIoLocalization/Www/Localization/Resources/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index de05cb68f6..de80e3529c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -431,6 +431,8 @@ "WhoWeAre_Expert": "About Me", "CreateSolutionFolder": "Create Solution Folder", "CreateSolutionFolderOption": "Specifies if the project will be in a new folder in the output folder or directly the output folder.", + "CreateCrudPage": "Create CRUD Page", + "CreateCrudPageOption": "Generates a sample CRUD page with a Book entity to demonstrate basic operations (Create, Read, Update, Delete).", "BooksPageTitle": "ABP Books", "BooksPageDescription": "Explore ABP books to deepen your understanding and mastery of ABP.", "PackageDetailPage_NuGetPackageInstallationOptions": "There are three ways to install {0} NuGet package to your project", From a9da3689c0226cdc551cab6e2e18ce57cad57fa2 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 6 Aug 2025 09:36:28 +0800 Subject: [PATCH 43/48] Add documentation for lists and arrays mapping --- .../infrastructure/object-to-object-mapping.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index f841907c05..52da7a258a 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -302,6 +302,23 @@ public partial class UserToUserDtoMapper : MapperBase It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more. + +### Lists and Arrays Support + +ABP Mapperly integration also supports mapping lists and arrays. Same as [IObjectMapper Interface](#iobjectmappertsource-tdestination-interface) section. Example: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} + +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` + ### More Mapperly Features Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. From b19e2e7a29050044e1cc622ea2b5ae001acd0656 Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Wed, 6 Aug 2025 09:43:38 +0300 Subject: [PATCH 44/48] Revise mapperly docs --- .../fundamentals/object-extensions.md | 2 +- .../object-to-object-mapping.md | 13 +++--- .../AutoMapper-To-Mapperly.md | 45 ++++++++----------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/docs/en/framework/fundamentals/object-extensions.md b/docs/en/framework/fundamentals/object-extensions.md index 2e01b3b1e3..87ece27f75 100644 --- a/docs/en/framework/fundamentals/object-extensions.md +++ b/docs/en/framework/fundamentals/object-extensions.md @@ -398,7 +398,7 @@ It has the same parameters with the `MapExtraPropertiesTo` method. If you're using the [Mapperly](https://github.com/riok/mapperly) library, the ABP also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. -You can use the `MapExtraProperties` attribute to Mapperly class. +You can use the `MapExtraProperties` attribute to Mapperly class: ````csharp [Mapper] diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 52da7a258a..5a9ea5f82a 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -225,7 +225,7 @@ Once you define mappings class as below, you can use the `IObjectMapper` interfa ### Define Mapping Classes -You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process. +You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process: ````csharp [Mapper] @@ -236,7 +236,7 @@ public partial class UserToUserDtoMapper : MapperBase } ```` -If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase` class. +If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase` class: ````csharp [Mapper] @@ -252,7 +252,7 @@ public partial class UserToUserDtoMapper : TwoWayMapperBase ### Before and After Mapping Methods -The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping. +The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping: ````csharp [Mapper] @@ -288,7 +288,7 @@ public partial class UserToUserDtoMapper : TwoWayMapperBase ### Mapping the Object Extensions -[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects. +[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects: ````csharp [Mapper] @@ -302,10 +302,11 @@ public partial class UserToUserDtoMapper : MapperBase It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more. - ### Lists and Arrays Support -ABP Mapperly integration also supports mapping lists and arrays. Same as [IObjectMapper Interface](#iobjectmappertsource-tdestination-interface) section. Example: +ABP Mapperly integration also supports mapping lists and arrays as explained in the [IObjectMapper Interface](#iobjectmappertsource-tdestination-interface) section. + +**Example**: ````csharp [Mapper] diff --git a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md index 4311d00214..2b0c5b623d 100644 --- a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -2,27 +2,22 @@ ## Introduction -The AutoMapper library is no longer free for commercial use. See [this article](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/) for more details. +The AutoMapper library is **no longer free for commercial use**. For more details, you can refer to [this announcement post](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/). -ABP framework provides the AutoMapper and Mapperly integration. If you have a project that uses AutoMapper and don't have any license for AutoMapper, you can migrate to Mapperly by following the steps below. +ABP Framework provides both AutoMapper and Mapperly integrations. If your project currently uses AutoMapper and you don't have a commercial license, you can switch to Mapperly by following the steps outlined below. ## Migration Steps -Please open your project with the IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform a global search and replace. - -1. Replace `Volo.Abp.AutoMapper` to `Volo.Abp.Mapperly` in all `*.csproj` files. - -2. Replace `using Volo.Abp.AutoMapper;` to `using Volo.Abp.Mapperly;` in all `*.cs` files. +Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations: +1. Replace `Volo.Abp.AutoMapper` with `Volo.Abp.Mapperly` in all `*.csproj` files. +2. Replace `using Volo.Abp.AutoMapper;` with `using Volo.Abp.Mapperly;` in all `*.cs` files. 3. Replace `AbpAutoMapperModule` with `AbpMapperlyModule` in all `*.cs` files. +4. Replace `AddAutoMapperObjectMapper` with `AddMapperlyObjectMapper` in all `*.cs` files. +5. Remove any code sections that configure `Configure`. +6. Review any existing AutoMapper `Profile` classes and ensure all newly created Mapperly mapping classes are registered appropriately. (You can refer to the example below for guidance) -4. Replace `AddAutoMapperObjectMapper` to `AddMapperlyObjectMapper` in all `*.cs` files. - -5. Remove `Configure` code section. - -6. Check the AutoMapper's `Profile` class to add all new Mapperly mapping classes. - -## Example +**Example:** Here is an AutoMapper's `Profile` class: @@ -146,21 +141,19 @@ public partial class IdentityUserToIdentityUserExportDtoMapper : MapperBase @@ -184,7 +177,7 @@ public partial class IdentityUserToIdentityUserDtoMapper : MapperBase Date: Wed, 6 Aug 2025 15:07:28 +0300 Subject: [PATCH 45/48] Add article on persistent background jobs with ABP and Quartz Introduces a comprehensive tutorial on building persistent background jobs in ABP Framework using Quartz.NET with PostgreSQL persistence. The article covers Quartz configuration, database setup via EF migrations, business logic structure, implementation of a robust background job, scheduling strategies, and best practices for reliability and maintainability. --- .../Post.md | 546 ++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md diff --git a/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md new file mode 100644 index 0000000000..1c50a56c8f --- /dev/null +++ b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md @@ -0,0 +1,546 @@ + +# How to Build Persistent Background Jobs with ABP Framework and Quartz + +## Introduction + +In modern SaaS applications, automated background processing is essential for delivering reliable user experiences. Whether you're sending subscription reminders, processing payments, or generating reports, background jobs ensure critical tasks happen on schedule without blocking your main application flow. + +### What is `Quartz.NET`? + +`Quartz.NET` is a powerful, open-source job scheduling library for .NET applications that provides cron-based scheduling for complex time patterns, job persistence across application restarts, clustering support for high-availability scenarios, flexible trigger types, and the ability to pass parameters to jobs through job data maps. It's the de facto standard for enterprise-grade job scheduling in the .NET ecosystem. + +### Quartz Storage Options: In-Memory vs Persistent + +When configuring **Quartz**, you have two primary storage options, each with significant implications for how your application behaves: + +### 🧠 In-Memory Storage (`RAMJobStore`) +- Keeps all job information in application memory. +- **Very fast** – no database overhead. +- **Volatile** – all jobs, triggers, and schedules are lost when the application stops or restarts. +- Best suited for: + - Development environments. + - Scenarios where job loss is acceptable. + +### 🗃️ Persistent Storage (`JobStoreTX` or similar) +- Stores all job information in a database. +- **Reliable** – schedules persist across: + - Application restarts + - Server crashes + - Deployments +- **Supports horizontal scaling** – multiple application instances can share the same job queue. +- **Slight performance overhead** due to database I/O. +- Best choice for: + - Production systems. + - Any scenario where **business continuity and reliability** are critical. + +### How ABP Simplifies Quartz Integration + +ABP handles Quartz configuration, dependency injection, and lifecycle management automatically. Developers define jobs using `QuartzBackgroundWorkerBase` and access services via `ICachedServiceProvider`, following ABP's standard conventions and leveraging optimal service caching for background job scenarios. + +### Benefits of the Integration + +- Full support for ABP’s cross-cutting concerns (e.g., multi-tenancy, localization) +- Robust scheduling powered by Quartz +- Built-in logging, error handling, and performance monitoring +- Scales easily without modifying business logic + +### Real-World Use Case: Subscription Reminders + +In this tutorial, we'll build a subscription reminder system that monitors client subscriptions, identifies those nearing expiration, sends professional email reminders seven days before expiration, tracks reminder history to prevent duplicates, and runs automatically every day at 9:00 AM using Quartz scheduling with PostgreSQL persistence. This system demonstrates how ABP and Quartz work together to solve real business problems with clean, maintainable code that follows enterprise-grade patterns. + +## Installing and Configuring Quartz + +Getting Quartz up and running in an ABP application is straightforward thanks to ABP's dedicated integration package. We'll replace the default background job system with Quartz for persistent job storage and robust scheduling capabilities. + +### Adding the Quartz Package + +The easiest way to add Quartz support to your ABP application is using the ABP CLI. Open a terminal in your project directory and run: + +```bash +abp add-package Volo.Abp.BackgroundWorkers.Quartz +``` + +This command automatically adds the necessary NuGet package reference and updates your module dependencies. The ABP CLI handles all the heavy lifting, ensuring you get the correct version that matches your ABP Framework version. + +### Configuring Quartz for Persistent Storage + +Once the package is installed, you need to configure Quartz to use your database (in my case it is PostgreSQL) for job persistence. This configuration goes in your main module's `PreConfigureServices` method: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpBackgroundJobsQuartzModule), + typeof(AbpBackgroundWorkersQuartzModule) +)] +public class MySaaSApplicationModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureAuthentication(context, configuration); + ConfigureUrls(configuration); + ConfigureImpersonation(context, configuration); + ConfigureQuartz(); // Add this line + } + + private void ConfigureQuartz() + { + PreConfigure(options => + { + options.Properties = new NameValueCollection + { + ["quartz.scheduler.instanceName"] = "QuartzScheduler", + ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", + ["quartz.jobStore.tablePrefix"] = "qrtz_", + ["quartz.jobStore.dataSource"] = "myDS", + ["quartz.dataSource.myDS.connectionString"] = _configuration.GetConnectionString("Default"), + ["quartz.dataSource.myDS.provider"] = "Npgsql", + ["quartz.serializer.type"] = "json" + }; + }); + } +} +``` + +This configuration tells Quartz to store all job information in your PostgreSQL database using tables prefixed with "qrtz_". The key points are: + +- **Job Store Type**: Uses ADO.NET with transaction support for reliable job persistence +- **Connection String**: Shares your application's existing database connection +- **Table Prefix**: Keeps Quartz tables separate with the "qrtz_" prefix +- **JSON Serialization**: Makes job data readable and debuggable +- **PostgreSQL Provider**: Uses Npgsql for optimal PostgreSQL integration + +When your application starts, ABP automatically initializes the Quartz scheduler with these settings. Any background workers you create will be registered and scheduled automatically, with their state persisted to the database for reliability across application restarts. + +For detailed installation options and advanced configuration scenarios, check the official [ABP documentation.](https://abp.io/docs/latest/framework/infrastructure/background-workers/quartz) + + +## Database Setup for Quartz + +With Quartz configured for persistent storage, we need to create the necessary database tables where Quartz will store job definitions, triggers, and execution history. Rather than running SQL scripts directly against the database, we'll use Entity Framework migrations to maintain consistency with ABP's database management approach. + +### Creating an Empty Migration for Quartz Tables + +Instead of executing raw SQL scripts against the database, we created an empty Entity Framework migration and populated it with the required Quartz table definitions. This approach keeps all database changes within the migration system, ensuring they're version-controlled, repeatable, and consistent across different environments. + +To create the empty migration, we used the standard Entity Framework CLI command: + +```bash +dotnet ef migrations add AddQuartzTables +``` + +This generates a new migration file with empty `Up` and `Down` methods that we can populate with the Quartz table creation scripts. + +### Adding Quartz SQL Schema to Migration + +Once the empty migration was created, we populated it with the PostgreSQL-specific SQL needed to create all Quartz tables. The SQL scripts were obtained from the official Quartz repository, which provides database schema scripts for various database providers: + +```csharp +public partial class AddQuartzTables : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + CREATE TABLE qrtz_job_details ( + sched_name VARCHAR(120) NOT NULL, + job_name VARCHAR(200) NOT NULL, + job_group VARCHAR(200) NOT NULL, + description VARCHAR(250) NULL, + job_class_name VARCHAR(250) NOT NULL, + is_durable BOOLEAN NOT NULL, + is_nonconcurrent BOOLEAN NOT NULL, + is_update_data BOOLEAN NOT NULL, + requests_recovery BOOLEAN NOT NULL, + job_data BYTEA NULL, + PRIMARY KEY (sched_name, job_name, job_group) + ); + + CREATE TABLE qrtz_triggers ( + sched_name VARCHAR(120) NOT NULL, + trigger_name VARCHAR(200) NOT NULL, + trigger_group VARCHAR(200) NOT NULL, + job_name VARCHAR(200) NOT NULL, + job_group VARCHAR(200) NOT NULL, + -- ... additional columns and constraints + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details(sched_name, job_name, job_group) + ); + + -- Additional tables: qrtz_simple_triggers, qrtz_cron_triggers, + -- qrtz_simprop_triggers, qrtz_blob_triggers, qrtz_calendars, + -- qrtz_paused_trigger_grps, qrtz_fired_triggers, qrtz_scheduler_state, qrtz_locks + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + DROP TABLE IF EXISTS qrtz_locks; + DROP TABLE IF EXISTS qrtz_scheduler_state; + -- ... drop all other Quartz tables in reverse order + DROP TABLE IF EXISTS qrtz_triggers; + DROP TABLE IF EXISTS qrtz_job_details; + "); + } +} +``` + +The complete SQL scripts for all supported database providers, including PostgreSQL, MySQL, SQL Server, and others, can be found in the official `Quartz.NET` repository. You should use the script that matches your specific database provider and version requirements. + +### Why Use Migrations Instead of Direct SQL Scripts? + +This migration-based approach offers several important advantages over running SQL scripts directly: + +**Version Control Integration**: The migration becomes part of your codebase, tracked in source control alongside your application changes. This means every developer and deployment environment gets the exact same database schema. + +**Rollback Capability**: The `Down` method provides a clean way to remove Quartz tables if needed, something that's much harder to manage with standalone SQL scripts. + +**Environment Consistency**: Whether you're setting up a development machine, staging server, or production deployment, running DBMigrator or `dotnet ef database update` command ensures the same schema is created everywhere. + +**Integration with ABP's Database Management**: This approach aligns perfectly with how ABP manages all other database changes, keeping your database evolution strategy consistent. + +The Quartz tables created by this migration handle all aspects of job persistence - from storing job definitions and triggers to tracking execution history and managing scheduler state. With these tables in place, your Quartz scheduler can reliably persist jobs across application restarts and coordinate work across multiple application instances if needed. + +After creating this migration, running DBMigrator `dotnet ef database update` will create all the necessary Quartz infrastructure in your PostgreSQL database, ready to store and manage your background jobs. + +For complete SQL scripts for your specific database provider, visit the official [Quartz documentation.](https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html#creating-and-initializing-database) +## Building the Business Logic + +Before implementing our Quartz background job, we needed to create the essential business entities and services that our subscription reminder system would work with. Since this article focuses on Quartz integration rather than general ABP development patterns, we'll keep this section brief and move quickly to the background job implementation. + +### Core Entities and Services + +For our subscription reminder system, we created the following core components: + +**Entities:** +- **`Client`**: Represents customers with subscription information (Name, Email, SubscriptionEnd, IsActive) +- **`ReminderLog`**: Tracks when reminder emails have been sent to prevent duplicates + +**Application Services:** +- **`ClientAppService`**: Handles CRUD operations and provides methods to find clients with expiring subscriptions +- **`ReminderLogAppService`**: Manages reminder history and prevents duplicate notifications +- **`EmailService`**: Sends professional HTML reminder emails via SMTP + +**Data Transfer Objects (DTOs):** +- Complete set of DTOs for both entities following ABP conventions +- Input/output DTOs for all service operations + +### Business Logic Overview + +The system follows standard ABP patterns with entities inheriting from `FullAuditedAggregateRoot`, services implementing `ICrudAppService` interfaces, and proper AutoMapper configurations for entity-DTO mapping. We also included a data seeder to create sample clients for testing purposes. + +The key business methods our background job will use are: +- `GetExpiringClientsAsync()` - Finds clients whose subscriptions expire in the next 7 days +- `CreateAsync()` - Logs when a reminder has been sent +- `SendSubscriptionExpiryReminderAsync()` - Sends professional email reminders + +### Focus on Background Operations + +Rather than diving deep into ABP entity creation, repository patterns, or service layer implementation details, we'll move directly to the heart of this article: implementing robust background jobs with Quartz. The entities and services we created simply provide the business context for our background job to operate within. + +The real value of this tutorial lies in showing how ABP's `QuartzBackgroundWorkerBase` integrates seamlessly with your business logic to create reliable, persistent background operations that survive application restarts and scale across multiple instances. + +Let's now implement the background job that ties everything together and demonstrates the power of ABP + Quartz integration. + + +## Implementing the Background Job (The ABP Way) + +This is where the magic happens. ABP's integration with Quartz provides a clean, powerful way to create background jobs that follow framework conventions while leveraging Quartz's robust scheduling capabilities. Let's dive into how we implemented our subscription reminder job and explore the advanced features ABP provides. + +### Creating a QuartzBackgroundWorkerBase Job + +Instead of implementing Quartz's raw `IJob` interface, ABP provides `QuartzBackgroundWorkerBase`, which integrates seamlessly with ABP's dependency injection, logging, and lifecycle management systems: + +```csharp +public class SubscriptionExpiryNotifierJob : QuartzBackgroundWorkerBase +{ + public SubscriptionExpiryNotifierJob() + { + // Configure the job to run daily at 9:00 AM + JobDetail = JobBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .Build(); + + Trigger = TriggerBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .WithCronSchedule("0 0 9 * * ?") // Every day at 9:00 AM + .Build(); + + ScheduleJob = async scheduler => + { + if (!await scheduler.CheckExists(JobDetail.Key)) + { + await scheduler.ScheduleJob(JobDetail, Trigger); + } + }; + } + + public override async Task Execute(IJobExecutionContext context) + { + // Use ICachedServiceProvider for better performance and proper scoping + var serviceProvider = ServiceProvider.GetRequiredService(); + + // These services will be cached and reused throughout the job execution + var clientAppService = serviceProvider.GetRequiredService(); + var reminderLogAppService = serviceProvider.GetRequiredService(); + var emailService = serviceProvider.GetRequiredService(); + + Logger.LogInformation("🔄 Starting subscription expiry notification job..."); + + // 1. Get clients expiring in 7 days + var expiringClients = await clientAppService.GetExpiringClientsAsync(7); + + Logger.LogInformation("📋 Found {Count} clients with expiring subscriptions", expiringClients.Count); + + // 2. Process each client + foreach (var client in expiringClients) + { + await ProcessClientAsync(client, emailService, reminderLogAppService); + } + + Logger.LogInformation("✅ Job completed successfully"); + } +} +``` + +### Key Implementation Features + +**Constructor-Based Configuration**: Unlike traditional Quartz jobs that require external scheduling code, ABP's approach lets you define both the job and its schedule directly in the constructor. This keeps related configuration together and makes the job self-contained. + +**ABP Service Integration**: The `ICachedServiceProvider` gives you access to any service in ABP's dependency injection container, enabling you to use application services, repositories, domain services, or any other ABP component with optimized caching and proper scoping. + +**Built-in Logging**: The `Logger` property provides access to ABP's logging infrastructure, automatically including context like correlation IDs and tenant information in multi-tenant applications. + +**Custom Scheduling Logic**: The `ScheduleJob` property allows you to customize how the job gets registered with Quartz. In our example, we check if the job already exists before scheduling it, preventing duplicate registrations during application restarts. + +### Understanding Quartz Trigger Types + +Quartz provides several trigger types to handle different scheduling requirements. Choosing the right trigger type is crucial for your job's behavior and performance. + +#### CronTrigger - Complex Time-Based Scheduling + +CronTrigger uses cron expressions for sophisticated scheduling patterns. This is what we used for our daily subscription reminders: + +```csharp +// Daily at 9:00 AM +Trigger = TriggerBuilder.Create() + .WithIdentity("DailyReminder") + .WithCronSchedule("0 0 9 * * ?") + .Build(); + +// Every weekday at 2:30 PM +Trigger = TriggerBuilder.Create() + .WithIdentity("WeekdayReport") + .WithCronSchedule("0 30 14 ? * MON-FRI") + .Build(); + +// First day of every month at midnight +Trigger = TriggerBuilder.Create() + .WithIdentity("MonthlyCleanup") + .WithCronSchedule("0 0 0 1 * ?") + .Build(); +``` + +**Cron Expression Format**: `Seconds Minutes Hours Day-of-Month Month Day-of-Week Year(optional)` +- `0 0 9 * * ?` = 9:00 AM every day +- `0 */15 * * * ?` = Every 15 minutes +- `0 0 12 ? * SUN` = Every Sunday at noon + +#### SimpleTrigger - Interval-Based Scheduling + +SimpleTrigger is perfect for jobs that need to run at regular intervals or a specific number of times: + +```csharp +// Run every 30 seconds indefinitely +Trigger = TriggerBuilder.Create() + .WithIdentity("HealthCheck") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInSeconds(30) + .RepeatForever()) + .Build(); + +// Run every 5 minutes, but only 10 times +Trigger = TriggerBuilder.Create() + .WithIdentity("LimitedRetry") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(5) + .WithRepeatCount(9)) // 0-based, so 9 = 10 executions + .Build(); + +// One-time execution after 1 hour delay +Trigger = TriggerBuilder.Create() + .WithIdentity("DelayedCleanup") + .StartAt(DateTimeOffset.UtcNow.AddHours(1)) + .Build(); +``` + +#### CalendarIntervalTrigger - Calendar-Aware Intervals + +CalendarIntervalTrigger handles intervals that need to respect calendar boundaries: + +```csharp +// Every month on the same day (handles varying month lengths) +Trigger = TriggerBuilder.Create() + .WithIdentity("MonthlyBilling") + .WithCalendarIntervalSchedule(x => x + .WithIntervalInMonths(1)) + .Build(); + +// Every week, starting Monday +Trigger = TriggerBuilder.Create() + .WithIdentity("WeeklyReport") + .WithCalendarIntervalSchedule(x => x + .WithIntervalInWeeks(1)) + .Build(); +``` + +#### DailyTimeIntervalTrigger - Time Windows + +DailyTimeIntervalTrigger runs jobs within specific time windows on certain days: + +```csharp +// Every 2 hours between 8 AM and 6 PM, Monday through Friday +Trigger = TriggerBuilder.Create() + .WithIdentity("BusinessHoursSync") + .WithDailyTimeIntervalSchedule(x => x + .OnMondayThroughFriday() + .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(8, 0)) + .EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(18, 0)) + .WithIntervalInHours(2)) + .Build(); +``` + +### Choosing the Right Trigger Type + +For different scenarios, you'd choose different trigger types: + +- **Daily/Weekly/Monthly Operations**: Use **CronTrigger** for maximum flexibility +- **High-Frequency Tasks**: Use **SimpleTrigger** for performance (every few seconds/minutes) +- **Business Calendar Operations**: Use **CalendarIntervalTrigger** for month-end reports, quarterly tasks +- **Business Hours Operations**: Use **DailyTimeIntervalTrigger** for operations that should only run during specific hours + +### Automatic Job Registration + +One of ABP's most powerful features is automatic job discovery and registration. When your application starts, ABP automatically: + +1. **Scans for Background Workers**: ABP discovers all classes inheriting from `QuartzBackgroundWorkerBase` +2. **Registers with DI Container**: Each job is registered as a service in the dependency injection container +3. **Schedules with Quartz**: ABP calls the `ScheduleJob` delegate to register the job with the Quartz scheduler +4. **Handles Lifecycle**: ABP manages starting and stopping jobs with the application lifecycle + +This means you simply create your job class, and ABP handles everything else. No manual registration, no startup code, no configuration files - it just works. + +### Understanding Misfire Handling + +Misfires occur when a scheduled job cannot execute at its intended time, typically due to system downtime, resource constraints, or the scheduler being paused. Quartz provides several misfire instructions to handle these scenarios: + +#### CronTrigger Misfire Instructions + +For cron-based schedules like our daily reminder job, Quartz offers these misfire behaviors: + +**`MisfireInstruction.DoNothing`** (Default): +```csharp +Trigger = TriggerBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionDoNothing()) + .Build(); +``` +- Skips all missed executions +- Waits for the next naturally scheduled time +- Best for jobs where missing executions is acceptable + +**`MisfireInstruction.FireOnceNow`**: +```csharp +.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionFireAndProceed()) +``` +- Immediately executes one missed job upon recovery +- Then continues with the normal schedule +- Useful when you need to catch up on missed work + +**`MisfireInstruction.IgnoreMisfires`**: +```csharp +.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionIgnoreMisfires()) +``` +- Executes all missed jobs immediately upon recovery +- Can cause a burst of executions after extended downtime +- Use carefully to avoid overwhelming the system + +#### SimpleTrigger Misfire Instructions + +Simple triggers have their own set of misfire behaviors: + +**`MisfireInstruction.FireNow`**: Execute immediately when recovered +**`MisfireInstruction.RescheduleNowWithExistingRepeatCount`**: Start over with remaining repeat count +**`MisfireInstruction.RescheduleNowWithRemainingRepeatCount`**: Continue as if no misfire occurred +**`MisfireInstruction.RescheduleNextWithExistingCount`**: Wait for next interval, keep original repeat count + +### Real-World Misfire Considerations + +For our subscription reminder system, we chose the default `DoNothing` behavior because: + +- **Business Logic**: Sending yesterday's reminder today might confuse customers +- **Duplicate Prevention**: Our job checks for existing reminders, so running late won't cause duplicate emails +- **Resource Management**: We avoid overwhelming the email system after extended downtime + +However, for other scenarios you might choose differently: +- **Financial reporting**: Use `FireOnceNow` to ensure reports are always generated +- **Data synchronization**: Use `IgnoreMisfires` to process all missed sync operations +- **Cache warming**: Use `DoNothing` since stale cache warming provides no value + +### Advanced Job Features + +**Error Handling and Resilience**: Our job implementation includes comprehensive error handling for individual client processing, ensuring one failed email doesn't stop the entire batch: + +```csharp +try +{ + await emailService.SendSubscriptionExpiryReminderAsync(/*...*/); + await LogReminderAsync(client.Id, client.SubscriptionEnd, "Email sent successfully", reminderLogAppService); +} +catch (Exception ex) +{ + Logger.LogError(ex, "❌ Failed to send reminder to {ClientName}", client.Name); + await LogReminderAsync(client.Id, client.SubscriptionEnd, $"Failed: {ex.Message}", reminderLogAppService); +} +``` + +**Duplicate Prevention**: The job checks for existing reminders to prevent sending multiple emails on the same day, even if the job runs multiple times: + +```csharp +private async Task AlreadySentTodayAsync(Guid clientId, IReminderLogAppService reminderLogAppService) +{ + var todayReminders = await reminderLogAppService.GetByClientIdAsync(clientId); + var today = DateTime.UtcNow.Date; + + return todayReminders.Any(r => r.ReminderDate.Date == today); +} +``` + +This implementation demonstrates how ABP's `QuartzBackgroundWorkerBase` provides a clean, powerful foundation for building robust background jobs that integrate seamlessly with your business logic while leveraging Quartz's enterprise-grade scheduling capabilities. + +## Conclusion + +You've successfully built a production-ready subscription reminder system that demonstrates the powerful synergy between ABP Framework and `Quartz.NET`. This isn't just a tutorial example - it's a robust, enterprise-grade solution that handles real business requirements. + +### What We Accomplished + +**✅ Enterprise-Grade Reliability**: PostgreSQL persistence ensures jobs survive restarts and deployments +**✅ ABP Best Practices**: Used `QuartzBackgroundWorkerBase`, `ICachedServiceProvider`, and ABP's logging infrastructure +**✅ Real Business Value**: Automated subscription reminders with duplicate prevention and audit logging +**✅ Flexible Scheduling**: Explored cron expressions, trigger types, and misfire handling strategies + +### The Power of ABP + Quartz Integration + +The combination delivers exceptional value through automatic job discovery, persistent scheduling, built-in dependency injection, and seamless framework integration. You get enterprise reliability with developer-friendly simplicity. + +### Final Thoughts + +Complex background processing doesn't have to be complicated to implement. ABP's thoughtful abstractions combined with Quartz's proven engine create a development experience that's both powerful and enjoyable. + +Whether you're building subscription management, financial reporting, or data synchronization, these patterns provide a solid foundation for reliable, maintainable solutions. + +**Happy coding, and may your background jobs never miss a beat!** 🚀 From 02d8bfd45ccaf4ecc7ea210c3dffd4a2d42526a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ahmet=20=C3=87elik?= Date: Wed, 6 Aug 2025 16:58:08 +0300 Subject: [PATCH 46/48] Add 'ConnectionString' localization key to en.json --- .../AbpIoLocalization/Www/Localization/Resources/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index de80e3529c..f1656af336 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -433,6 +433,7 @@ "CreateSolutionFolderOption": "Specifies if the project will be in a new folder in the output folder or directly the output folder.", "CreateCrudPage": "Create CRUD Page", "CreateCrudPageOption": "Generates a sample CRUD page with a Book entity to demonstrate basic operations (Create, Read, Update, Delete).", + "ConnectionString": "Connection string", "BooksPageTitle": "ABP Books", "BooksPageDescription": "Explore ABP books to deepen your understanding and mastery of ABP.", "PackageDetailPage_NuGetPackageInstallationOptions": "There are three ways to install {0} NuGet package to your project", From 3d06e2af811af399402d27ff246a302593cda718 Mon Sep 17 00:00:00 2001 From: Super Date: Thu, 7 Aug 2025 02:27:08 +0800 Subject: [PATCH 47/48] Fix NullReferenceException in ClientProxyUrlBuilder --- .../Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 09ec183929..69248680cc 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -224,7 +224,7 @@ public class ClientProxyUrlBuilder : ITransientDependency return true; } - protected virtual Task ConvertValueToStringAsync(object value) + protected virtual Task ConvertValueToStringAsync(object? value) { if (value is DateTime dateTimeValue) { @@ -236,6 +236,6 @@ public class ClientProxyUrlBuilder : ITransientDependency return Task.FromResult(dateTimeValue.ToString("yyyy-MM-ddTHH:mm:ss.fffffff").TrimEnd('0').TrimEnd('.')); } - return Task.FromResult(value.ToString()!); + return Task.FromResult(value?.ToString() ?? string.Empty); } } From 78ad62308c4d85f5e89be0f0ce1b590acf36f1c3 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 7 Aug 2025 13:12:06 +0800 Subject: [PATCH 48/48] Remove HTML encoding from error page output --- .../Views/MultiTenancyMiddlewareErrorPage.Designer.cs | 10 +++++----- .../Views/MultiTenancyMiddlewareErrorPage.cshtml | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs index 751eec21bb..e1c2947c97 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs @@ -42,10 +42,10 @@ using Volo.Abp.AspNetCore.RazorViews; #nullable disable WriteLiteral("\n"); WriteLiteral("\n