diff --git a/docs/en/framework/infrastructure/luckypenny-automapper.md b/docs/en/framework/infrastructure/luckypenny-automapper.md new file mode 100644 index 0000000000..d3d516f1ce --- /dev/null +++ b/docs/en/framework/infrastructure/luckypenny-automapper.md @@ -0,0 +1,151 @@ +```json +//[doc-seo] +{ + "Description": "Learn how to use the Volo.Abp.LuckyPenny.AutoMapper package to integrate the commercial AutoMapper (LuckyPenny) with ABP Framework." +} +``` + +# LuckyPenny AutoMapper Integration + +## Introduction + +[AutoMapper](https://automapper.org/) became a commercial product starting from version 15.x. The free open-source version (14.x) contains a [security vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x) — a DoS (Denial of Service) vulnerability — and no patch will be released for the 14.x series. The patched version is only available in the commercial editions (15.x and later). + +The existing [Volo.Abp.AutoMapper](https://www.nuget.org/packages/Volo.Abp.AutoMapper) package uses AutoMapper 14.x and remains available for existing users. If you hold a valid [LuckyPenny AutoMapper commercial license](https://automapper.io/), the `Volo.Abp.LuckyPenny.AutoMapper` package provides the same ABP AutoMapper integration built on the patched commercial version. + +> If you don't need to use AutoMapper, you can migrate to [Mapperly](object-to-object-mapping.md#mapperly-integration), which is free and open-source. See the [AutoMapper to Mapperly migration guide](../../release-info/migration-guides/AutoMapper-To-Mapperly.md). + +## Installation + +Install the `Volo.Abp.LuckyPenny.AutoMapper` NuGet package to your project: + +````bash +dotnet add package Volo.Abp.LuckyPenny.AutoMapper +```` + +Then add `AbpLuckyPennyAutoMapperModule` to your module's `[DependsOn]` attribute, replacing the existing `AbpAutoMapperModule`: + +````csharp +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] +public class MyModule : AbpModule +{ + // ... +} +```` + +> **Note:** `Volo.Abp.LuckyPenny.AutoMapper` and `Volo.Abp.AutoMapper` should **not** be used together in the same application. They are mutually exclusive — choose one or the other. + +## Usage + +`Volo.Abp.LuckyPenny.AutoMapper` is a drop-in replacement for `Volo.Abp.AutoMapper`. All the same APIs, options, and extension methods are available. Refer to the [AutoMapper Integration](object-to-object-mapping.md#automapper-integration) section of the Object to Object Mapping documentation for full usage details. + +The only difference from a user perspective is the module class name: + +| | Package | Module class | +|---|---|---| +| Free (14.x, has security vulnerability) | `Volo.Abp.AutoMapper` | `AbpAutoMapperModule` | +| Commercial (patched) | `Volo.Abp.LuckyPenny.AutoMapper` | `AbpLuckyPennyAutoMapperModule` | + +## License Configuration + +The commercial AutoMapper uses an honor-system license. Without a configured key, everything works normally but a warning is written to the logs under the `LuckyPennySoftware.AutoMapper.License` category. To configure your license key, use `AbpAutoMapperOptions.Configurators`: + +````csharp +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.LicenseKey = "YOUR_LICENSE_KEY"; + }); + }); + } +} +```` + +It is recommended to read the key from configuration rather than hardcoding it: + +````csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var licenseKey = context.Configuration["AutoMapper:LicenseKey"]; + + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.LicenseKey = licenseKey; + }); + }); +} +```` + +````json +{ + "AutoMapper": { + "LicenseKey": "YOUR_LICENSE_KEY" + } +} +```` + +To suppress the license warning in non-production environments (e.g. unit tests or local development), filter the log category in `Program.cs`: + +````csharp +builder.Logging.AddFilter("LuckyPennySoftware.AutoMapper.License", LogLevel.None); +```` + +Or in `appsettings.Development.json`: + +````json +{ + "Logging": { + "LogLevel": { + "LuckyPennySoftware.AutoMapper.License": "None" + } + } +} +```` + +> **Client-side applications** (Blazor WebAssembly, MAUI, WPF, etc.) should **not** set the license key to avoid exposing it on the client. Use the log filter above to silence the warning instead. + +## Obtaining a License + +AutoMapper offers a **free Community License** and several paid plans. + +### Community License (Free) + +A free license is available to organizations that meet **all** of the following criteria: + +- Annual gross revenue under **$5,000,000 USD** +- Never received more than **$10,000,000 USD** in outside capital (private equity or venture capital) +- Registered non-profits with an annual budget under **$5,000,000 USD** also qualify + +> Government and quasi-government agencies do **not** qualify for the Community License. + +Register for the Community License at: [https://luckypennysoftware.com/community](https://luckypennysoftware.com/community) + +### Paid Plans + +For organizations that do not meet the Community License criteria, paid plans are available at [https://luckypennysoftware.com/purchase](https://luckypennysoftware.com/purchase). For questions, contact [sales@luckypennysoftware.com](mailto:sales@luckypennysoftware.com). + +## Migration from Volo.Abp.AutoMapper + +To migrate an existing project from `Volo.Abp.AutoMapper` to `Volo.Abp.LuckyPenny.AutoMapper`: + +1. Replace the NuGet package reference in all `*.csproj` files: + ````diff + - + + + ```` + +2. Replace the module dependency in all `*.cs` files: + ````diff + -[DependsOn(typeof(AbpAutoMapperModule))] + +[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + ```` + +3. No other code changes are required. All types (`AbpAutoMapperOptions`, `IMapperAccessor`, `AutoMapperExpressionExtensions`, etc.) remain in the same namespaces. diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 39218e7c86..5dcf526266 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -224,6 +224,8 @@ public class MyProfile : Profile } ```` +> AutoMapper 14.x contains a [known vulnerability (GHSA-rvv3-g6hj-g44x)](https://github.com/advisories/GHSA-rvv3-g6hj-g44x). ABP Framework has applied a code-level mitigation (`MaxDepth = 64`) to address this. If you hold a commercial AutoMapper license, you can use [Volo.Abp.LuckyPenny.AutoMapper](luckypenny-automapper.md) to upgrade to the officially patched version. Alternatively, you can migrate to [Mapperly](../../../release-info/migration-guides/AutoMapper-To-Mapperly.md). + ## 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`. 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 c293dd9a37..f19449d04f 100644 --- a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -6,6 +6,8 @@ The AutoMapper library is **no longer free for commercial use**. For more detail 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. +> **Already have a commercial AutoMapper license?** Use the [Volo.Abp.LuckyPenny.AutoMapper](../../framework/infrastructure/luckypenny-automapper.md) package instead. It is a drop-in replacement for `Volo.Abp.AutoMapper` built on the patched commercial version of AutoMapper, requiring only two changes to your project. + ## Migration Steps Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations: diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 1302600c09..26d462fb4f 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -48,6 +48,7 @@ + @@ -189,6 +190,7 @@ + diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs new file mode 100644 index 0000000000..ea746500d6 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Volo.Abp.AutoMapper; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; + +namespace AutoMapper; + +public static class AbpAutoMapperExtensibleObjectExtensions +{ + public static IMappingExpression MapExtraProperties( + this IMappingExpression mappingExpression, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[]? ignoredProperties = null, + bool mapToRegularProperties = false) + where TDestination : IHasExtraProperties + where TSource : IHasExtraProperties + { + return mappingExpression + .ForMember( + x => x.ExtraProperties, + y => y.MapFrom( + (source, destination, extraProps) => + { + var result = extraProps.IsNullOrEmpty() + ? new Dictionary() + : new Dictionary(extraProps); + + if (source.ExtraProperties == null || destination.ExtraProperties == null) + { + return result; + } + + ExtensibleObjectMapper + .MapExtraPropertiesTo( + source.ExtraProperties, + result, + definitionChecks, + ignoredProperties + ); + + return result; + }) + ) + .ForSourceMember(x => x.ExtraProperties, x => x.DoNotValidate()) + .AfterMap((source, destination, context) => + { + if (mapToRegularProperties) + { + destination.SetExtraPropertiesToRegularProperties(); + } + }); + } + + public static IMappingExpression IgnoreExtraProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasExtraProperties + where TSource : IHasExtraProperties + { + return mappingExpression.Ignore(x => x.ExtraProperties); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml new file mode 100644 index 0000000000..1715698ccd --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xsd b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xsd new file mode 100644 index 0000000000..ffa6fc4b78 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs new file mode 100644 index 0000000000..93cd0ef03b --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.AutoMapper; +using Volo.Abp.ObjectMapping; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpAutoMapperServiceCollectionExtensions +{ + public static IServiceCollection AddAutoMapperObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient() + ); + } + + public static IServiceCollection AddAutoMapperObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient, AutoMapperAutoObjectMappingProvider>() + ); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3fb111fede --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Volo.Abp.LuckyPenny.AutoMapper.Tests")] diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg new file mode 100644 index 0000000000..e0c33eaab1 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.framework" +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json new file mode 100644 index 0000000000..f06064e723 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.abppkg.analyze.json @@ -0,0 +1,73 @@ +{ + "name": "Volo.Abp.LuckyPenny.AutoMapper", + "hash": "", + "contents": [ + { + "namespace": "Volo.Abp.AutoMapper", + "dependsOnModules": [ + { + "declaringAssemblyName": "Volo.Abp.ObjectMapping", + "namespace": "Volo.Abp.ObjectMapping", + "name": "AbpObjectMappingModule" + }, + { + "declaringAssemblyName": "Volo.Abp.ObjectExtending", + "namespace": "Volo.Abp.ObjectExtending", + "name": "AbpObjectExtendingModule" + }, + { + "declaringAssemblyName": "Volo.Abp.Auditing", + "namespace": "Volo.Abp.Auditing", + "name": "AbpAuditingModule" + } + ], + "implementingInterfaces": [ + { + "name": "IAbpModule", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IAbpModule" + }, + { + "name": "IOnPreApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPreApplicationInitialization" + }, + { + "name": "IOnApplicationInitialization", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationInitialization" + }, + { + "name": "IOnPostApplicationInitialization", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IOnPostApplicationInitialization" + }, + { + "name": "IOnApplicationShutdown", + "namespace": "Volo.Abp", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.IOnApplicationShutdown" + }, + { + "name": "IPreConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPreConfigureServices" + }, + { + "name": "IPostConfigureServices", + "namespace": "Volo.Abp.Modularity", + "declaringAssemblyName": "Volo.Abp.Core", + "fullName": "Volo.Abp.Modularity.IPostConfigureServices" + } + ], + "contentType": "abpModule", + "name": "AbpLuckyPennyAutoMapperModule", + "summary": null + } + ] +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj new file mode 100644 index 0000000000..661c9e7e5b --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo.Abp.LuckyPenny.AutoMapper.csproj @@ -0,0 +1,29 @@ + + + + + + + net8.0;net9.0;net10.0 + enable + Nullable + Volo.Abp.LuckyPenny.AutoMapper + Volo.Abp.LuckyPenny.AutoMapper + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs new file mode 100644 index 0000000000..c8754abd37 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConfigurationContext.cs @@ -0,0 +1,19 @@ +using System; +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperConfigurationContext : IAbpAutoMapperConfigurationContext +{ + public IMapperConfigurationExpression MapperConfiguration { get; } + + public IServiceProvider ServiceProvider { get; } + + public AbpAutoMapperConfigurationContext( + IMapperConfigurationExpression mapperConfigurationExpression, + IServiceProvider serviceProvider) + { + MapperConfiguration = mapperConfigurationExpression; + ServiceProvider = serviceProvider; + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs new file mode 100644 index 0000000000..39c21e2ca7 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs @@ -0,0 +1,29 @@ +using System; +using System.Linq; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperConventionalRegistrar : DefaultConventionalRegistrar +{ + protected readonly Type[] OpenTypes = { + typeof(IValueResolver<,,>), + typeof(IMemberValueResolver<,,,>), + typeof(ITypeConverter<,>), + typeof(IValueConverter<,>), + typeof(IMappingAction<,>) + }; + + protected override bool IsConventionalRegistrationDisabled(Type type) + { + return !type.GetInterfaces().Any(x => x.IsGenericType && OpenTypes.Contains(x.GetGenericTypeDefinition())) || + base.IsConventionalRegistrationDisabled(type); + } + + protected override ServiceLifetime? GetDefaultLifeTimeOrNull(Type type) + { + return ServiceLifetime.Transient; + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs new file mode 100644 index 0000000000..f6ab5a5408 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperOptions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Volo.Abp.Collections; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperOptions +{ + public List> Configurators { get; } + + public ITypeList ValidatingProfiles { get; set; } + + /// + /// Default MaxDepth applied to all maps that don't have an explicit MaxDepth configured. + /// Set to null to disable the default MaxDepth behavior. + /// Default: 64. + /// + public int? DefaultMaxDepth { get; set; } = 64; + + public AbpAutoMapperOptions() + { + Configurators = new List>(); + ValidatingProfiles = new TypeList(); + } + + public void AddMaps(bool validate = false) + { + var assembly = typeof(TModule).Assembly; + + Configurators.Add(context => + { + context.MapperConfiguration.AddMaps(assembly); + }); + + if (validate) + { + var profileTypes = assembly + .DefinedTypes + .Where(type => typeof(Profile).IsAssignableFrom(type) && !type.IsAbstract && !type.IsGenericType); + + foreach (var profileType in profileTypes) + { + ValidatingProfiles.Add(profileType); + } + } + } + + public void AddProfile(bool validate = false) + where TProfile : Profile, new() + { + Configurators.Add(context => + { + context.MapperConfiguration.AddProfile(); + }); + + if (validate) + { + ValidateProfile(typeof(TProfile)); + } + } + + public void ValidateProfile(bool validate = true) + where TProfile : Profile + { + ValidateProfile(typeof(TProfile), validate); + } + + public void ValidateProfile(Type profileType, bool validate = true) + { + if (validate) + { + ValidatingProfiles.AddIfNotContains(profileType); + } + else + { + ValidatingProfiles.Remove(profileType); + } + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs new file mode 100644 index 0000000000..51833d1a84 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AbpLuckyPennyAutoMapperModule.cs @@ -0,0 +1,76 @@ +using System; +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Volo.Abp.Auditing; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper; + +[DependsOn( + typeof(AbpObjectMappingModule), + typeof(AbpObjectExtendingModule), + typeof(AbpAuditingModule) +)] +public class AbpLuckyPennyAutoMapperModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpAutoMapperConventionalRegistrar()); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAutoMapperObjectMapper(); + + context.Services.AddSingleton(sp => + { + using (var scope = sp.CreateScope()) + { + var options = scope.ServiceProvider.GetRequiredService>().Value; + + var mapperConfigurationExpression = sp.GetRequiredService>().Value; + var autoMapperConfigurationContext = new AbpAutoMapperConfigurationContext(mapperConfigurationExpression, scope.ServiceProvider); + + foreach (var configurator in options.Configurators) + { + configurator(autoMapperConfigurationContext); + } + + if (options.DefaultMaxDepth.HasValue) + { + mapperConfigurationExpression.Internal().ForAllMaps((typeMap, _) => + { + if (typeMap.MaxDepth == 0) + { + typeMap.MaxDepth = options.DefaultMaxDepth.Value; + } + }); + } + + var loggerFactory = sp.GetService() ?? NullLoggerFactory.Instance; + var mapperConfiguration = new MapperConfiguration(mapperConfigurationExpression, loggerFactory); + + foreach (var profileType in options.ValidatingProfiles) + { + mapperConfiguration.Internal().AssertConfigurationIsValid(((Profile)Activator.CreateInstance(profileType)!).ProfileName); + } + + return mapperConfiguration; + } + }); + + context.Services.AddTransient(sp => sp.GetRequiredService().CreateMapper(sp.GetService)); + + context.Services.AddTransient(sp => new MapperAccessor() + { + Mapper = sp.GetRequiredService() + }); + context.Services.AddTransient(provider => provider.GetRequiredService()); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs new file mode 100644 index 0000000000..fcddf444ca --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperAutoObjectMappingProvider.cs @@ -0,0 +1,30 @@ +using Volo.Abp.ObjectMapping; +namespace Volo.Abp.AutoMapper; + +public class AutoMapperAutoObjectMappingProvider : AutoMapperAutoObjectMappingProvider, IAutoObjectMappingProvider +{ + public AutoMapperAutoObjectMappingProvider(IMapperAccessor mapperAccessor) + : base(mapperAccessor) + { + } +} + +public class AutoMapperAutoObjectMappingProvider : IAutoObjectMappingProvider +{ + public IMapperAccessor MapperAccessor { get; } + + public AutoMapperAutoObjectMappingProvider(IMapperAccessor mapperAccessor) + { + MapperAccessor = mapperAccessor; + } + + public virtual TDestination Map(object source) + { + return MapperAccessor.Mapper.Map(source); + } + + public virtual TDestination Map(TSource source, TDestination destination) + { + return MapperAccessor.Mapper.Map(source, destination); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs new file mode 100644 index 0000000000..e955eef24c --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq.Expressions; +using AutoMapper; +using Volo.Abp.Auditing; + +namespace Volo.Abp.AutoMapper; + +public static class AutoMapperExpressionExtensions +{ + public static IMappingExpression Ignore(this IMappingExpression mappingExpression, Expression> destinationMember) + { + return mappingExpression.ForMember(destinationMember, opts => opts.Ignore()); + } + + public static IMappingExpression IgnoreHasCreationTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasCreationTime + { + return mappingExpression.Ignore(x => x.CreationTime); + } + + public static IMappingExpression IgnoreMayHaveCreatorProperties( + this IMappingExpression mappingExpression) + where TDestination : IMayHaveCreator + { + return mappingExpression.Ignore(x => x.CreatorId); + } + + public static IMappingExpression IgnoreCreationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : ICreationAuditedObject + { + return mappingExpression + .IgnoreHasCreationTimeProperties() + .IgnoreMayHaveCreatorProperties(); + } + + public static IMappingExpression IgnoreHasModificationTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasModificationTime + { + return mappingExpression.Ignore(x => x.LastModificationTime); + } + + public static IMappingExpression IgnoreModificationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IModificationAuditedObject + { + return mappingExpression + .IgnoreHasModificationTimeProperties() + .Ignore(x => x.LastModifierId); + } + + public static IMappingExpression IgnoreAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreModificationAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreSoftDeleteProperties( + this IMappingExpression mappingExpression) + where TDestination : ISoftDelete + { + return mappingExpression.Ignore(x => x.IsDeleted); + } + + public static IMappingExpression IgnoreHasDeletionTimeProperties( + this IMappingExpression mappingExpression) + where TDestination : IHasDeletionTime + { + return mappingExpression + .IgnoreSoftDeleteProperties() + .Ignore(x => x.DeletionTime); + } + + public static IMappingExpression IgnoreDeletionAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IDeletionAuditedObject + { + return mappingExpression + .IgnoreHasDeletionTimeProperties() + .Ignore(x => x.DeleterId); + } + + public static IMappingExpression IgnoreFullAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IFullAuditedObject + { + return mappingExpression + .IgnoreAuditedObjectProperties() + .IgnoreDeletionAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreMayHaveCreatorProperties( + this IMappingExpression mappingExpression) + where TDestination : IMayHaveCreator + { + return mappingExpression + .Ignore(x => x.Creator); + } + + public static IMappingExpression IgnoreCreationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : ICreationAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreMayHaveCreatorProperties(); + } + + public static IMappingExpression IgnoreModificationAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IModificationAuditedObject + { + return mappingExpression + .IgnoreModificationAuditedObjectProperties() + .Ignore(x => x.LastModifier); + } + + public static IMappingExpression IgnoreAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IAuditedObject + { + return mappingExpression + .IgnoreCreationAuditedObjectProperties() + .IgnoreModificationAuditedObjectProperties(); + } + + public static IMappingExpression IgnoreDeletionAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IDeletionAuditedObject + { + return mappingExpression + .IgnoreDeletionAuditedObjectProperties() + .Ignore(x => x.Deleter); + } + + + public static IMappingExpression IgnoreFullAuditedObjectProperties( + this IMappingExpression mappingExpression) + where TDestination : IFullAuditedObject + { + return mappingExpression + .IgnoreAuditedObjectProperties() + .IgnoreDeletionAuditedObjectProperties(); + } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs new file mode 100644 index 0000000000..f7dece6633 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IAbpAutoMapperConfigurationContext.cs @@ -0,0 +1,11 @@ +using System; +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public interface IAbpAutoMapperConfigurationContext +{ + IMapperConfigurationExpression MapperConfiguration { get; } + + IServiceProvider ServiceProvider { get; } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs new file mode 100644 index 0000000000..9997289a22 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/IMapperAccessor.cs @@ -0,0 +1,8 @@ +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +public interface IMapperAccessor +{ + IMapper Mapper { get; } +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs new file mode 100644 index 0000000000..5751180185 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/AutoMapper/MapperAccessor.cs @@ -0,0 +1,8 @@ +using AutoMapper; + +namespace Volo.Abp.AutoMapper; + +internal class MapperAccessor : IMapperAccessor +{ + public IMapper Mapper { get; set; } = default!; +} diff --git a/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs new file mode 100644 index 0000000000..33d73a77e6 --- /dev/null +++ b/framework/src/Volo.Abp.LuckyPenny.AutoMapper/Volo/Abp/ObjectMapping/AbpAutoMapperObjectMapperExtensions.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using Volo.Abp.AutoMapper; + +namespace Volo.Abp.ObjectMapping; + +public static class AbpAutoMapperObjectMapperExtensions +{ + public static IMapper GetMapper(this IObjectMapper objectMapper) + { + return objectMapper.AutoObjectMappingProvider.GetMapper(); + } + + public static IMapper GetMapper(this IAutoObjectMappingProvider autoObjectMappingProvider) + { + if (autoObjectMappingProvider is AutoMapperAutoObjectMappingProvider autoMapperAutoObjectMappingProvider) + { + return autoMapperAutoObjectMappingProvider.MapperAccessor.Mapper; + } + + throw new AbpException($"Given object is not an instance of {typeof(AutoMapperAutoObjectMappingProvider).AssemblyQualifiedName}. The type of the given object it {autoObjectMappingProvider.GetType().AssemblyQualifiedName}"); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs new file mode 100644 index 0000000000..e9bc5464ea --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/AutoMapper/AbpAutoMapperExtensibleDtoExtensions_Tests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace AutoMapper; + +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] + 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.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg new file mode 100644 index 0000000000..037b12d73d --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "test.framework" +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj new file mode 100644 index 0000000000..384447a180 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo.Abp.LuckyPenny.AutoMapper.Tests.csproj @@ -0,0 +1,21 @@ + + + + + + net10.0 + Volo.Abp.LuckyPenny.AutoMapper.Tests + Volo.Abp.LuckyPenny.AutoMapper.Tests + + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs new file mode 100644 index 0000000000..82a0369aa0 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Basic_Tests.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperModule_Basic_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpAutoMapperModule_Basic_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Replace_IAutoObjectMappingProvider() + { + Assert.True(ServiceProvider.GetRequiredService() is AutoMapperAutoObjectMappingProvider); + } + + [Fact] + public void Should_Get_Internal_Mapper() + { + _objectMapper.GetMapper().ShouldNotBeNull(); + _objectMapper.AutoObjectMappingProvider.GetMapper().ShouldNotBeNull(); + } + + [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 + } + + //[Fact] TODO: Disabled because of https://github.com/AutoMapper/AutoMapper/pull/2379#issuecomment-355899664 + /*public void Should_Not_Map_Objects_With_AutoMap_Attributes() + { + Assert.ThrowsAny(() => + { + _objectMapper.Map(new MyEntity {Number = 42}); + }); + }*/ +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs new file mode 100644 index 0000000000..03b90e63ea --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_MaxDepth_Tests.cs @@ -0,0 +1,102 @@ +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AbpAutoMapperModule_MaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_MaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Set_Default_MaxDepth_For_All_Maps() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(64); + } +} + +public class AbpAutoMapperModule_CustomMaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_CustomMaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Not_Override_Custom_MaxDepth() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(10); + } + + [DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) + )] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.CreateMap().MaxDepth(10); + }); + }); + } + } +} + +public class AbpAutoMapperModule_DisabledMaxDepth_Tests : AbpIntegratedTest +{ + private readonly IConfigurationProvider _configurationProvider; + + public AbpAutoMapperModule_DisabledMaxDepth_Tests() + { + _configurationProvider = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Not_Set_MaxDepth_When_Disabled() + { + var typeMap = _configurationProvider.Internal().FindTypeMapFor(); + typeMap.ShouldNotBeNull(); + typeMap.MaxDepth.ShouldBe(0); + } + + [DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) + )] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.DefaultMaxDepth = null; + options.Configurators.Add(ctx => + { + ctx.MapperConfiguration.CreateMap(); + }); + }); + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs new file mode 100644 index 0000000000..47b684538c --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/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.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +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 AutoMapper. + 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.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs new file mode 100644 index 0000000000..c57f141837 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperExpressionExtensions_Tests.cs @@ -0,0 +1,177 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Volo.Abp.Auditing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapperExpressionExtensions_Tests +{ + [Fact] + public void Should_Ignore_Configured_Property() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .Ignore(x => x.Value2) + .Ignore(x => x.Value3) + ); + + var obj2 = mapper.Map( + new SimpleClass1 + { + Value1 = "v1", + Value2 = "v2" + } + ); + + obj2.Value1.ShouldBe("v1"); + obj2.Value2.ShouldBeNull(); + obj2.Value3.ShouldBeNull(); + } + + [Fact] + public void Should_Ignore_Audit_Properties() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .IgnoreFullAuditedObjectProperties() + ); + + var obj2 = mapper.Map( + new SimpleClassAudited1 + { + CreationTime = DateTime.Now, + CreatorId = Guid.NewGuid(), + LastModificationTime = DateTime.Now, + LastModifierId = Guid.NewGuid(), + DeleterId = Guid.NewGuid(), + DeletionTime = DateTime.Now, + IsDeleted = true + } + ); + + obj2.CreationTime.ShouldBe(default); + obj2.CreatorId.ShouldBeNull(); + obj2.LastModificationTime.ShouldBe(default); + obj2.LastModifierId.ShouldBeNull(); + obj2.DeleterId.ShouldBeNull(); + obj2.DeletionTime.ShouldBeNull(); + obj2.IsDeleted.ShouldBeFalse(); + } + + [Fact] + public void Should_Ignore_Audit_Properties_With_User() + { + var mapper = CreateMapper( + cfg => cfg + .CreateMap() + .IgnoreFullAuditedObjectProperties() + ); + + var obj2 = mapper.Map( + new SimpleClassAuditedWithUser1 + { + CreationTime = DateTime.Now, + CreatorId = Guid.NewGuid(), + LastModificationTime = DateTime.Now, + LastModifierId = Guid.NewGuid(), + DeleterId = Guid.NewGuid(), + DeletionTime = DateTime.Now, + IsDeleted = true, + Creator = new SimpleUser(), + Deleter = new SimpleUser(), + LastModifier = new SimpleUser() + } + ); + + obj2.CreationTime.ShouldBe(default); + obj2.CreatorId.ShouldBeNull(); + obj2.LastModificationTime.ShouldBe(default); + obj2.LastModifierId.ShouldBeNull(); + obj2.DeleterId.ShouldBeNull(); + obj2.DeletionTime.ShouldBeNull(); + obj2.IsDeleted.ShouldBeFalse(); + obj2.Creator.ShouldBeNull(); + obj2.Deleter.ShouldBeNull(); + obj2.LastModifier.ShouldBeNull(); + } + + private static IMapper CreateMapper(Action configure) + { + var configuration = new MapperConfiguration(configure, NullLoggerFactory.Instance); + configuration.AssertConfigurationIsValid(); + return configuration.CreateMapper(); + } + + public class SimpleClass1 + { + public string Value1 { get; set; } + public string Value2 { get; set; } + } + + public class SimpleClass2 + { + public string Value1 { get; set; } + public string Value2 { get; set; } + public string Value3 { get; set; } + } + + public class SimpleClassAudited1 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + } + + public class SimpleClassAudited2 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + } + + public class SimpleClassAuditedWithUser1 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + public SimpleUser Creator { get; set; } + public SimpleUser LastModifier { get; set; } + public SimpleUser Deleter { get; set; } + } + + public class SimpleClassAuditedWithUser2 : IFullAuditedObject + { + public DateTime CreationTime { get; set; } + public Guid? CreatorId { get; set; } + public DateTime? LastModificationTime { get; set; } + public Guid? LastModifierId { get; set; } + public bool IsDeleted { get; set; } + public DateTime? DeletionTime { get; set; } + public Guid? DeleterId { get; set; } + public SimpleUser Creator { get; set; } + public SimpleUser LastModifier { get; set; } + public SimpleUser Deleter { get; set; } + } + + public class SimpleUser + { + + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs new file mode 100644 index 0000000000..d484fc03ff --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapperTestModule.cs @@ -0,0 +1,19 @@ +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.AutoMapper; + +[DependsOn( + typeof(AbpLuckyPennyAutoMapperModule), + typeof(AbpObjectExtendingTestModule) +)] +public class AutoMapperTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.AddMaps(); + }); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs new file mode 100644 index 0000000000..c71dac7c53 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_ConfigurationValidation_Tests.cs @@ -0,0 +1,70 @@ +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_ConfigurationValidation_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AutoMapper_ConfigurationValidation_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Validate_Configuration() + { + _objectMapper.Map(new MySourceClass { Value = "42" }).Value.ShouldBe("42"); + _objectMapper.Map(new MySourceClass { Value = "42" }).ValueNotMatched.ShouldBe(null); + } + + [DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.AddMaps(validate: true); //Adds all profiles in the TestModule assembly by validating configurations + options.ValidateProfile(validate: false); //Exclude a profile from the configuration validation + }); + } + } + + public class ValidatedProfile : Profile + { + public ValidatedProfile() + { + CreateMap(); + } + } + + public class NonValidatedProfile : Profile + { + public NonValidatedProfile() + { + CreateMap(); + } + } + + public class MySourceClass + { + public string Value { get; set; } + } + + public class MyClassValidated + { + public string Value { get; set; } + } + + public class MyClassNonValidated + { + public string ValueNotMatched { get; set; } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs new file mode 100644 index 0000000000..02f4b7bfd0 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_CustomServiceConstruction_Tests.cs @@ -0,0 +1,87 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_CustomServiceConstruction_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AutoMapper_CustomServiceConstruction_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Custom_Service_Construction() + { + var source = new SourceModel + { + Name = nameof(SourceModel) + }; + + _objectMapper.Map(source).Name.ShouldBe(nameof(CustomMappingAction)); + } + + [DependsOn(typeof(AbpLuckyPennyAutoMapperModule))] + public class TestModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // Replace the build-in IMapper with a custom one to use ConstructServicesUsing. + context.Services.Replace(ServiceDescriptor.Transient(sp => sp.GetRequiredService().CreateMapper())); + + Configure(options => + { + options.AddMaps(); + options.Configurators.Add(configurationContext => + { + configurationContext.MapperConfiguration.ConstructServicesUsing(type => + type.Name.Contains(nameof(CustomMappingAction)) + ? new CustomMappingAction(nameof(CustomMappingAction)) + : Activator.CreateInstance(type)); + }); + }); + } + } + + public class SourceModel + { + public string Name { get; set; } + } + + public class DestModel + { + public string Name { get; set; } + } + + public class MapperActionProfile : Profile + { + public MapperActionProfile() + { + CreateMap().AfterMap(); + } + } + + public class CustomMappingAction : IMappingAction + { + private readonly string _name; + + public CustomMappingAction(string name) + { + _name = name; + } + + public void Process(SourceModel source, DestModel destination, ResolutionContext context) + { + destination.Name = _name; + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs new file mode 100644 index 0000000000..fcc77276e5 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/AutoMapper_Dependency_Injection_Tests.cs @@ -0,0 +1,83 @@ +using System; +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Testing; +using Xunit; +using IObjectMapper = Volo.Abp.ObjectMapping.IObjectMapper; + +namespace Volo.Abp.AutoMapper; + +public class AutoMapper_Dependency_Injection_Tests : AbpIntegratedTest +{ + [Fact] + public void Should_Registered_AutoMapper_Service() + { + GetService().ShouldNotBeNull(); + } + + [Fact] + public void Custom_MappingAction_Test() + { + var sourceModel = new SourceModel + { + Name = "Source" + }; + + using (var scope = ServiceProvider.CreateScope()) + { + scope.ServiceProvider.GetRequiredService().Map(sourceModel).Name.ShouldBe(nameof(CustomMappingActionService)); + } + + CustomMappingAction.IsDisposed.ShouldBeTrue(); + } + + public class SourceModel + { + public string Name { get; set; } + } + + public class DestModel + { + public string Name { get; set; } + } + + public class MapperActionProfile : Profile + { + public MapperActionProfile() + { + CreateMap().AfterMap(); + } + } + + public class CustomMappingAction : IMappingAction, IDisposable + { + public static bool IsDisposed = false; + + private readonly CustomMappingActionService _customMappingActionService; + + public CustomMappingAction(CustomMappingActionService customMappingActionService) + { + _customMappingActionService = customMappingActionService; + } + + public void Process(SourceModel source, DestModel destination, ResolutionContext context) + { + destination.Name = _customMappingActionService.GetName(); + } + + public void Dispose() + { + IsDisposed = true; + } + } + + public class CustomMappingActionService : ITransientDependency + { + public string GetName() + { + return nameof(CustomMappingActionService); + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs new file mode 100644 index 0000000000..7d4bfbe707 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/ObjectMapperExtensions_Tests.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using System; +using Volo.Abp.AutoMapper.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.AutoMapper; + +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.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs new file mode 100644 index 0000000000..95cd55215f --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntity.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntity +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs new file mode 100644 index 0000000000..0c498ca0c6 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntityDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs new file mode 100644 index 0000000000..a4a6942e55 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDto2.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyEntityDto2 +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs new file mode 100644 index 0000000000..89a9048069 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityDtoWithMappingMethods.cs @@ -0,0 +1,43 @@ +using System; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper.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.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs new file mode 100644 index 0000000000..6bf16fce8e --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEntityToMyEntityDto2Mapper.cs @@ -0,0 +1,39 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.AutoMapper.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.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..61b23ea6f4 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..6e553adfb9 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyEnumDto.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.AutoMapper.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs new file mode 100644 index 0000000000..295ff6b08e --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyMapProfile.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using Volo.Abp.ObjectExtending.TestObjects; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyMapProfile : Profile +{ + public MyMapProfile() + { + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); + + CreateMap() + .MapExtraProperties(ignoredProperties: new[] { "CityName" }); + + CreateMap() + .ForMember(x => x.Name, y => y.Ignore()) + .ForMember(x => x.Age, y => y.Ignore()) + .ForMember(x => x.IsActive, y => y.Ignore()) + .MapExtraProperties(mapToRegularProperties: true); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs new file mode 100644 index 0000000000..304fe6e7b4 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/AutoMapper/SampleClasses/MyNotMappedDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.AutoMapper.SampleClasses; + +public class MyNotMappedDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs new file mode 100644 index 0000000000..b709fed153 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/AbpLuckyPennyMultiLingualObjectsTestModule.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Autofac; +using Volo.Abp.AutoMapper; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Settings; + +namespace Volo.Abp.MultiLingualObjects; + +[DependsOn( + typeof(AbpAutofacModule), + typeof(AbpLocalizationModule), + typeof(AbpSettingsModule), + typeof(AbpObjectMappingModule), + typeof(AbpMultiLingualObjectsModule), + typeof(AbpTestBaseModule), + typeof(AbpLuckyPennyAutoMapperModule) +)] +public class AbpLuckyPennyMultiLingualObjectsTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.DefinitionProviders.Add(); + }); + context.Services.AddAutoMapperObjectMapper(); + Configure(options => + { + options.AddProfile(validate: true); + }); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs new file mode 100644 index 0000000000..9053472c48 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectManager_Tests.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.AutoMapper; +using Volo.Abp.Localization; +using Volo.Abp.MultiLingualObjects.TestObjects; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.MultiLingualObjects; + +public class MultiLingualObjectManager_Tests : AbpIntegratedTest +{ + private readonly IMultiLingualObjectManager _multiLingualObjectManager; + private readonly MultiLingualBook _book; + private readonly List _books; + private readonly IMapperAccessor _mapperAccessor; + private readonly FrozenDictionary _testTranslations = new Dictionary + { + ["ar"] = "C# التعمق في", + ["zh-Hans"] = "深入理解C#", + ["en"] = "C# in Depth" + }.ToFrozenDictionary(); + + public MultiLingualObjectManager_Tests() + { + _multiLingualObjectManager = ServiceProvider.GetRequiredService(); + + //Single Lookup + _book = GetTestBook("en", "zh-Hans"); + //Bulk lookup + _books = new List + { + //has no translations + GetTestBook(), + //english only + GetTestBook("en"), + //arabic only + GetTestBook("ar"), + //arabic + english + GetTestBook("en","ar"), + //arabic + english + chineese + GetTestBook("en", "ar", "zh-Hans") + }; + _mapperAccessor = ServiceProvider.GetRequiredService(); + } + + MultiLingualBook GetTestBook(params string[] included) + { + var id = Guid.NewGuid(); + var res = new MultiLingualBook(id, 100); + + foreach (var language in included) + { + res.Translations.Add(new MultiLingualBookTranslation + { + Language = language, + Name = _testTranslations[language], + }); + } + + return res; + } + + [Fact] + public async Task GetTranslationAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetTranslationFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book.Translations); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task Should_Get_Specified_Language() + { + using (CultureHelper.Use("zh-Hans")) + { + var translation = await _multiLingualObjectManager.GetTranslationAsync(_book, culture: "en"); + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + } + + [Fact] + public async Task GetBulkTranslationsAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + foreach (var (entity, translation) in translations) + { + if (entity.Translations.Any(x => x.Language == "en")) + { + translation.ShouldNotBeNull(); + translation.Name.ShouldBe(_testTranslations["en"]); + } + else + { + translation.ShouldBeNull(); + } + } + } + } + + [Fact] + public async Task GetBulkTranslationsFromListAsync() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books.Select(x => x.Translations)); + foreach (var translation in translations) + { + translation?.Name.ShouldBe(_testTranslations["en"]); + } + } + } + + [Fact] + public async Task TestBulkMapping() + { + using (CultureHelper.Use("en-us")) + { + var translations = await _multiLingualObjectManager.GetBulkTranslationsAsync(_books); + var translationsDict = translations.ToDictionary(x => x.entity.Id, x => x.translation); + var mapped = _mapperAccessor.Mapper.Map, List>(_books, options => + { + options.Items.Add(nameof(MultiLingualBookTranslation), translationsDict); + }); + Assert.Equal(mapped.Count, _books.Count); + for (int i = 0; i < mapped.Count; i++) + { + var og = _books[i]; + var m = mapped[i]; + Assert.Equal(og.Translations.FirstOrDefault(x => x.Language == "en")?.Name, m.Name); + } + } + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs new file mode 100644 index 0000000000..ded671c8c2 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/MultiLingualObjectTestProfile.cs @@ -0,0 +1,23 @@ +namespace Volo.Abp.MultiLingualObjects; + +using System; +using System.Collections.Generic; +using global::AutoMapper; +using Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualObjectTestProfile : Profile +{ + public MultiLingualObjectTestProfile() + { + CreateMap() + .ForMember(x => x.Name, + x => x.MapFrom((src, target, member, context) => + { + if (context.Items.TryGetValue(nameof(MultiLingualBookTranslation), out var translationsRaw) && translationsRaw is IReadOnlyDictionary translations) + { + return translations.GetValueOrDefault(src.Id)?.Name; + } + return null; + })); + } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs new file mode 100644 index 0000000000..3eb61e6cf3 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBook.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBook : IMultiLingualObject +{ + public MultiLingualBook(Guid id, decimal price) + { + Id = id; + Price = price; + } + + public Guid Id { get; } + + public decimal Price { get; set; } + + public ICollection Translations { get; set; } = new List(); +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs new file mode 100644 index 0000000000..5131d306b2 --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookDto.cs @@ -0,0 +1,12 @@ +using System; + +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookDto +{ + public Guid Id { get; set; } + + public string? Name { get; set; } + + public decimal Price { get; set; } +} diff --git a/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs new file mode 100644 index 0000000000..31ce5d74db --- /dev/null +++ b/framework/test/Volo.Abp.LuckyPenny.AutoMapper.Tests/Volo/Abp/MultiLingualObjects/TestObjects/MultiLingualBookTranslation.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.MultiLingualObjects.TestObjects; + +public class MultiLingualBookTranslation : IObjectTranslation +{ + public string? Name { get; set; } + + public required string Language { get; set; } +} diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 6fbc34e80c..b64a7cb7fe 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -142,6 +142,7 @@ $projects = ( "framework/src/Volo.Abp.Autofac", "framework/src/Volo.Abp.Autofac.WebAssembly", "framework/src/Volo.Abp.AutoMapper", + "framework/src/Volo.Abp.LuckyPenny.AutoMapper", "framework/src/Volo.Abp.AzureServiceBus", "framework/src/Volo.Abp.BackgroundJobs.Abstractions", "framework/src/Volo.Abp.BackgroundJobs",