From 25c6d5571f17ba2cd2bbbe7e33eab3660303e23e Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Mon, 5 Jan 2026 14:35:50 +0100 Subject: [PATCH 1/9] feat(Configuration): Add MapsterOptions with seperated config and settings props --- .../ConfigOptions.cs | 37 ++++++ .../MapsterOptions.cs | 121 ++++++++++++++++++ .../ServiceCollectionExtensions.cs | 83 +++++++++++- .../SettingsOptions.cs | 67 ++++++++++ 4 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 src/Mapster.DependencyInjection/ConfigOptions.cs create mode 100644 src/Mapster.DependencyInjection/MapsterOptions.cs create mode 100644 src/Mapster.DependencyInjection/SettingsOptions.cs diff --git a/src/Mapster.DependencyInjection/ConfigOptions.cs b/src/Mapster.DependencyInjection/ConfigOptions.cs new file mode 100644 index 00000000..3ff8cc22 --- /dev/null +++ b/src/Mapster.DependencyInjection/ConfigOptions.cs @@ -0,0 +1,37 @@ +namespace Mapster.DependencyInjection; + +/// +/// Engine-level configuration switches applied to . +/// +public sealed record MapsterConfigOptions +{ + /// + /// Require that every destination member has a source. + /// + public bool RequireDestinationMemberSource { get; init; } + + /// + /// Require explicit mapping (disable implicit mapping). + /// + public bool RequireExplicitMapping { get; init; } + + /// + /// Require explicit mapping even for primitive types. + /// + public bool RequireExplicitMappingPrimitive { get; init; } + + /// + /// Allow implicit inheritance on destination types. + /// + public bool AllowImplicitDestinationInheritance { get; init; } + + /// + /// Allow implicit inheritance on source types. + /// + public bool AllowImplicitSourceInheritance { get; init; } = true; + + /// + /// Generate self-contained code (no shared helpers) for source generation. + /// + public bool SelfContainedCodeGeneration { get; init; } +} diff --git a/src/Mapster.DependencyInjection/MapsterOptions.cs b/src/Mapster.DependencyInjection/MapsterOptions.cs new file mode 100644 index 00000000..4eaa322a --- /dev/null +++ b/src/Mapster.DependencyInjection/MapsterOptions.cs @@ -0,0 +1,121 @@ +namespace Mapster.DependencyInjection; + +/// +/// Options for configuring Mapster through DI or configuration binding. +/// +public sealed record MapsterOptions +{ + /// + /// The default configuration section name used for options binding. + /// + public const string DefaultSectionName = "Mapster"; + + /// + /// Engine-level configuration switches applied to . + /// + public MapsterConfigOptions Config { get; init; } = new(); + + /// + /// Global default mapping settings applied to . + /// + public MapsterSettingsOptions Settings { get; init; } = new(); + + public void ApplyTo(TypeAdapterConfig config) + { + ArgumentNullException.ThrowIfNull(config); + + var defaultConfig = TypeAdapterConfig.GlobalSettings; + var defaultSettings = defaultConfig.Default.Settings; + + if (Config.RequireDestinationMemberSource != defaultConfig.RequireDestinationMemberSource) + { + config.RequireDestinationMemberSource = Config.RequireDestinationMemberSource; + } + + if (Config.RequireExplicitMapping != defaultConfig.RequireExplicitMapping) + { + config.RequireExplicitMapping = Config.RequireExplicitMapping; + } + + if (Config.RequireExplicitMappingPrimitive != defaultConfig.RequireExplicitMappingPrimitive) + { + config.RequireExplicitMappingPrimitive = Config.RequireExplicitMappingPrimitive; + } + + if (Config.AllowImplicitDestinationInheritance != defaultConfig.AllowImplicitDestinationInheritance) + { + config.AllowImplicitDestinationInheritance = Config.AllowImplicitDestinationInheritance; + } + + if (Config.AllowImplicitSourceInheritance != defaultConfig.AllowImplicitSourceInheritance) + { + config.AllowImplicitSourceInheritance = Config.AllowImplicitSourceInheritance; + } + + if (Config.SelfContainedCodeGeneration != defaultConfig.SelfContainedCodeGeneration) + { + config.SelfContainedCodeGeneration = Config.SelfContainedCodeGeneration; + } + + if (!string.IsNullOrWhiteSpace(Settings.NameMatchingStrategy)) + { + config.Default.ApplyNameMatchingStrategy(Settings.NameMatchingStrategy); + } + + if (Settings.MapToConstructor) + { + config.Default.MapToConstructor(Settings.MapToConstructor); + } + + if (Settings.PreserveReference) + { + config.Default.PreserveReference(Settings.PreserveReference); + } + + if (Settings.ShallowCopyForSameType) + { + config.Default.ShallowCopyForSameType(Settings.ShallowCopyForSameType); + } + + if (Settings.IgnoreNullValues) + { + config.Default.IgnoreNullValues(Settings.IgnoreNullValues); + } + + if (Settings.MapEnumByName != (defaultSettings.MapEnumByName ?? false)) + { + config.Default.Settings.MapEnumByName = Settings.MapEnumByName; + } + + if (Settings.IgnoreNonMapped) + { + config.Default.IgnoreNonMapped(Settings.IgnoreNonMapped); + } + + if (Settings.AvoidInlineMapping != (defaultSettings.AvoidInlineMapping ?? false)) + { + config.Default.AvoidInlineMapping(Settings.AvoidInlineMapping); + } + + if (Settings.Unflattening != (defaultSettings.Unflattening ?? false)) + { + config.Default.Unflattening(Settings.Unflattening); + } + + if (Settings.SkipDestinationMemberCheck) + { + config.Default.Settings.SkipDestinationMemberCheck = Settings.SkipDestinationMemberCheck; + } + + if (Settings.EnableNonPublicMembers) + { + config.Default.EnableNonPublicMembers(Settings.EnableNonPublicMembers); + } + + if (Settings.MaxDepth > 0) + { + config.Default.MaxDepth(Settings.MaxDepth); + } + } +} + diff --git a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs index 36204baf..f23dab4e 100644 --- a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,13 +1,82 @@ -using MapsterMapper; -using Microsoft.Extensions.DependencyInjection; +namespace Mapster.DependencyInjection; -namespace Mapster +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static void AddMapster(this IServiceCollection serviceCollection) { - public static void AddMapster(this IServiceCollection serviceCollection) + serviceCollection.AddTransient(); + } + + public static IServiceCollection AddMapster(this IServiceCollection services, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + + services.AddOptions().Configure(configureOptions); + return services.AddMapsterCore(); + } + + public static IServiceCollection AddMapster(this IServiceCollection services, IConfiguration configuration, string sectionName = MapsterOptions.DefaultSectionName) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configuration); + + var section = configuration.GetSection(sectionName); + + if (section.Exists()) + { + services.AddOptions().Bind(section); + } + else { - serviceCollection.AddTransient(); + services.AddOptions(); } + + return services.AddMapsterCore(); + } + + /// + /// Adds an additional configuration callback that will be applied to the DI-provided . + /// This enables modular configuration (for example per assembly or feature). + /// + public static IServiceCollection AddMapsterConfig(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + return services.AddMapsterConfig((cfg, _) => configure(cfg)); + } + + /// + /// Adds an additional configuration callback that will be applied to the DI-provided . + /// The allows resolving services used by mapping configuration. + /// + public static IServiceCollection AddMapsterConfig(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services.AddSingleton(new MapsterConfigRegistration(configure)); + return services; + } + + private static IServiceCollection AddMapsterCore(this IServiceCollection services) + { + services.AddSingleton(sp => + { + var config = TypeAdapterConfig.GlobalSettings.Clone(); + var options = sp.GetRequiredService>().Value; + options.ApplyTo(config); + + foreach (var reg in sp.GetServices()) + { + reg.Configure(config, sp); + } + + return config; + }); + + services.AddTransient(); + return services; } -} \ No newline at end of file +} diff --git a/src/Mapster.DependencyInjection/SettingsOptions.cs b/src/Mapster.DependencyInjection/SettingsOptions.cs new file mode 100644 index 00000000..8917f43e --- /dev/null +++ b/src/Mapster.DependencyInjection/SettingsOptions.cs @@ -0,0 +1,67 @@ +namespace Mapster.DependencyInjection; + +/// +/// Global default mapping settings applied to . +/// +public sealed record MapsterSettingsOptions +{ + /// + /// Name matching strategy (Exact, Flexible, IgnoreCase, ToCamelCase, FromCamelCase). + /// + public string? NameMatchingStrategy { get; init; } + + /// + /// Enable mapping to constructors when available. + /// + public bool MapToConstructor { get; init; } + + /// + /// Preserve reference cycles when mapping. + /// + public bool PreserveReference { get; init; } + + /// + /// Use shallow copy optimization when source and destination types are the same. + /// + public bool ShallowCopyForSameType { get; init; } + + /// + /// Ignore null source values. + /// + public bool IgnoreNullValues { get; init; } + + /// + /// Map enums by name instead of numeric value. + /// + public bool MapEnumByName { get; init; } + + /// + /// Ignore members not mapped explicitly. + /// + public bool IgnoreNonMapped { get; init; } + + /// + /// Avoid inline mapping when generating code. + /// + public bool AvoidInlineMapping { get; init; } + + /// + /// Enable unflattening (map nested source to flat destination). + /// + public bool Unflattening { get; init; } + + /// + /// Skip destination member checks when mapping. + /// + public bool SkipDestinationMemberCheck { get; init; } + + /// + /// Allow mapping to non-public members. + /// + public bool EnableNonPublicMembers { get; init; } + + /// + /// Maximum mapping depth (0 = unlimited). + /// + public int MaxDepth { get; init; } +} From 7fd5e8c83968d954820163fb4c32527a6f1bce3e Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sat, 31 Jan 2026 19:05:12 +0100 Subject: [PATCH 2/9] feat: Enhance ServiceCollection- and HostBuilderExtensions with MapsterOptions configuration including --- .../HostBuilderExtensions.cs | 33 +++ .../MapsterOptions.cs | 200 ++++++++---------- .../ServiceCollectionExtensions.cs | 131 +++++------- 3 files changed, 173 insertions(+), 191 deletions(-) create mode 100644 src/Mapster.DependencyInjection/HostBuilderExtensions.cs diff --git a/src/Mapster.DependencyInjection/HostBuilderExtensions.cs b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs new file mode 100644 index 00000000..a9afd908 --- /dev/null +++ b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs @@ -0,0 +1,33 @@ +namespace Mapster.DependencyInjection; + +public static class HostBuilderExtensions +{ + public static IHostBuilder UseMapster( + this IHostBuilder builder, + Action? configure = null) + { + return builder.ConfigureServices(services => services.AddMapster(configure)); + } + + public static IHostBuilder UseMapster( + this IHostBuilder builder, + string sectionName = "", + Func? configSection = null) + { + + if (configSection is null) + { + if (sectionName is not { Length: > 0 }) + { + sectionName = MapsterOptions.DefaultName; + } + configSection = ctx => ctx.Configuration.GetSection(sectionName); + } + + return builder.ConfigureServices((context, services) => + { + var section = configSection(context); + services.AddMapster(section); + }); + } +} diff --git a/src/Mapster.DependencyInjection/MapsterOptions.cs b/src/Mapster.DependencyInjection/MapsterOptions.cs index 4eaa322a..b158c991 100644 --- a/src/Mapster.DependencyInjection/MapsterOptions.cs +++ b/src/Mapster.DependencyInjection/MapsterOptions.cs @@ -8,114 +8,96 @@ public sealed record MapsterOptions /// /// The default configuration section name used for options binding. /// - public const string DefaultSectionName = "Mapster"; - - /// - /// Engine-level configuration switches applied to . - /// - public MapsterConfigOptions Config { get; init; } = new(); - - /// - /// Global default mapping settings applied to . - /// - public MapsterSettingsOptions Settings { get; init; } = new(); - - public void ApplyTo(TypeAdapterConfig config) - { - ArgumentNullException.ThrowIfNull(config); - - var defaultConfig = TypeAdapterConfig.GlobalSettings; - var defaultSettings = defaultConfig.Default.Settings; - - if (Config.RequireDestinationMemberSource != defaultConfig.RequireDestinationMemberSource) - { - config.RequireDestinationMemberSource = Config.RequireDestinationMemberSource; - } - - if (Config.RequireExplicitMapping != defaultConfig.RequireExplicitMapping) - { - config.RequireExplicitMapping = Config.RequireExplicitMapping; - } - - if (Config.RequireExplicitMappingPrimitive != defaultConfig.RequireExplicitMappingPrimitive) - { - config.RequireExplicitMappingPrimitive = Config.RequireExplicitMappingPrimitive; - } - - if (Config.AllowImplicitDestinationInheritance != defaultConfig.AllowImplicitDestinationInheritance) - { - config.AllowImplicitDestinationInheritance = Config.AllowImplicitDestinationInheritance; - } - - if (Config.AllowImplicitSourceInheritance != defaultConfig.AllowImplicitSourceInheritance) - { - config.AllowImplicitSourceInheritance = Config.AllowImplicitSourceInheritance; - } - - if (Config.SelfContainedCodeGeneration != defaultConfig.SelfContainedCodeGeneration) - { - config.SelfContainedCodeGeneration = Config.SelfContainedCodeGeneration; - } - - if (!string.IsNullOrWhiteSpace(Settings.NameMatchingStrategy)) - { - config.Default.ApplyNameMatchingStrategy(Settings.NameMatchingStrategy); - } - - if (Settings.MapToConstructor) - { - config.Default.MapToConstructor(Settings.MapToConstructor); - } - - if (Settings.PreserveReference) - { - config.Default.PreserveReference(Settings.PreserveReference); - } - - if (Settings.ShallowCopyForSameType) - { - config.Default.ShallowCopyForSameType(Settings.ShallowCopyForSameType); - } - - if (Settings.IgnoreNullValues) - { - config.Default.IgnoreNullValues(Settings.IgnoreNullValues); - } - - if (Settings.MapEnumByName != (defaultSettings.MapEnumByName ?? false)) - { - config.Default.Settings.MapEnumByName = Settings.MapEnumByName; - } - - if (Settings.IgnoreNonMapped) - { - config.Default.IgnoreNonMapped(Settings.IgnoreNonMapped); - } - - if (Settings.AvoidInlineMapping != (defaultSettings.AvoidInlineMapping ?? false)) - { - config.Default.AvoidInlineMapping(Settings.AvoidInlineMapping); - } - - if (Settings.Unflattening != (defaultSettings.Unflattening ?? false)) - { - config.Default.Unflattening(Settings.Unflattening); - } - - if (Settings.SkipDestinationMemberCheck) - { - config.Default.Settings.SkipDestinationMemberCheck = Settings.SkipDestinationMemberCheck; - } - - if (Settings.EnableNonPublicMembers) - { - config.Default.EnableNonPublicMembers(Settings.EnableNonPublicMembers); - } - - if (Settings.MaxDepth > 0) - { - config.Default.MaxDepth(Settings.MaxDepth); - } - } + public const string DefaultName = "Mapster"; + + /// + /// Require that every destination member has a source. + /// + public bool RequireDestinationMemberSource { get; set; } + + /// + /// Require explicit mapping (disable implicit mapping). + /// + public bool RequireExplicitMapping { get; set; } + + /// + /// Require explicit mapping even for primitive types. + /// + public bool RequireExplicitMappingPrimitive { get; set; } + + /// + /// Allow implicit inheritance on destination types. + /// + public bool AllowImplicitDestinationInheritance { get; set; } + + /// + /// Allow implicit inheritance on source types. + /// + public bool AllowImplicitSourceInheritance { get; set; } = true; + + /// + /// Generate self-contained code (no shared helpers) for source generation. + /// + public bool SelfContainedCodeGeneration { get; set; } + + /// + /// Name matching strategy (Exact, Flexible, IgnoreCase, ToCamelCase, FromCamelCase). + /// + public string? NameMatchingStrategy { get; set; } + + /// + /// Enable mapping to constructors when available. + /// + public bool MapToConstructor { get; set; } + + /// + /// Preserve reference cycles when mapping. + /// + public bool PreserveReference { get; set; } + + /// + /// Use shallow copy optimization when source and destination types are the same. + /// + public bool ShallowCopyForSameType { get; set; } + + /// + /// Ignore null source values. + /// + public bool IgnoreNullValues { get; set; } + + /// + /// Map enums by name instead of numeric value. + /// + public bool MapEnumByName { get; set; } + + /// + /// Ignore members not mapped explicitly. + /// + public bool IgnoreNonMapped { get; set; } + + /// + /// Avoid inline mapping when generating code. + /// + public bool AvoidInlineMapping { get; set; } + + /// + /// Enable unflattening (map nested source to flat destination). + /// + public bool Unflattening { get; set; } + + /// + /// Skip destination member checks when mapping. + /// + public bool SkipDestinationMemberCheck { get; set; } + + /// + /// Allow mapping to non-public members. + /// + public bool EnableNonPublicMembers { get; set; } + + /// + /// Maximum mapping depth (0 = unlimited). + /// + public int MaxDepth { get; set; } } diff --git a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs index f23dab4e..1bf5affe 100644 --- a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,82 +1,49 @@ -namespace Mapster.DependencyInjection; - -public static class ServiceCollectionExtensions -{ - public static void AddMapster(this IServiceCollection serviceCollection) - { - serviceCollection.AddTransient(); - } - - public static IServiceCollection AddMapster(this IServiceCollection services, Action configureOptions) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configureOptions); - - services.AddOptions().Configure(configureOptions); - return services.AddMapsterCore(); - } - - public static IServiceCollection AddMapster(this IServiceCollection services, IConfiguration configuration, string sectionName = MapsterOptions.DefaultSectionName) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configuration); - - var section = configuration.GetSection(sectionName); - - if (section.Exists()) - { - services.AddOptions().Bind(section); - } - else - { - services.AddOptions(); - } - - return services.AddMapsterCore(); - } - - /// - /// Adds an additional configuration callback that will be applied to the DI-provided . - /// This enables modular configuration (for example per assembly or feature). - /// - public static IServiceCollection AddMapsterConfig(this IServiceCollection services, Action configure) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); - - return services.AddMapsterConfig((cfg, _) => configure(cfg)); - } - - /// - /// Adds an additional configuration callback that will be applied to the DI-provided . - /// The allows resolving services used by mapping configuration. - /// - public static IServiceCollection AddMapsterConfig(this IServiceCollection services, Action configure) - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); - - services.AddSingleton(new MapsterConfigRegistration(configure)); - return services; - } - - private static IServiceCollection AddMapsterCore(this IServiceCollection services) - { - services.AddSingleton(sp => - { - var config = TypeAdapterConfig.GlobalSettings.Clone(); - var options = sp.GetRequiredService>().Value; - options.ApplyTo(config); - - foreach (var reg in sp.GetServices()) - { - reg.Configure(config, sp); - } - - return config; - }); - - services.AddTransient(); - return services; - } -} +namespace Mapster.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds Mapster mapping services to the specified and optionally configures . + /// + /// The to which the Mapster services will be added. Cannot be null. + /// An optional delegate to configure . If null, default options are used. + /// The same instance so that additional calls can be chained. + public static IServiceCollection AddMapster( + this IServiceCollection services, + Action? configure = null) + { + if (configure is not null) + { + services.Configure(configure); + } + else + { + services.AddOptions(); + } + + return services.AddMapster(); + } + + /// + /// Adds Mapster to the specified using configuration, calls to register Mapster services. + /// + /// The to insert to. + /// The instance to use + /// The same as provided, added up with the Mapster services + public static IServiceCollection AddMapster(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + return services.AddMapster(); + } + + /// + /// Adds Mapster mapping services to the specified . + /// + /// The to which the Mapster services will be added. + /// The same instance so that additional calls can be chained. + public static IServiceCollection AddMapster(this IServiceCollection services) + { + services.TryAddTransient(); + return services; + } +} \ No newline at end of file From de8e59772b94bbba4d74b978794a0007377c6491 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sat, 31 Jan 2026 19:16:46 +0100 Subject: [PATCH 3/9] chore(Options): Clean up Options sub settings, which are type specific to be set and add ApplyTo function for Options to TypeAdapterConfig Mapping --- .../ConfigOptions.cs | 37 ---------- .../GlobalUsings.cs | 7 ++ .../Mapster.DependencyInjection.csproj | 3 + .../MapsterOptions.cs | 59 ---------------- .../MapsterOptionsExtensions.cs | 31 +++++++++ .../SettingsOptions.cs | 67 ------------------- 6 files changed, 41 insertions(+), 163 deletions(-) delete mode 100644 src/Mapster.DependencyInjection/ConfigOptions.cs create mode 100644 src/Mapster.DependencyInjection/GlobalUsings.cs create mode 100644 src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs delete mode 100644 src/Mapster.DependencyInjection/SettingsOptions.cs diff --git a/src/Mapster.DependencyInjection/ConfigOptions.cs b/src/Mapster.DependencyInjection/ConfigOptions.cs deleted file mode 100644 index 3ff8cc22..00000000 --- a/src/Mapster.DependencyInjection/ConfigOptions.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Mapster.DependencyInjection; - -/// -/// Engine-level configuration switches applied to . -/// -public sealed record MapsterConfigOptions -{ - /// - /// Require that every destination member has a source. - /// - public bool RequireDestinationMemberSource { get; init; } - - /// - /// Require explicit mapping (disable implicit mapping). - /// - public bool RequireExplicitMapping { get; init; } - - /// - /// Require explicit mapping even for primitive types. - /// - public bool RequireExplicitMappingPrimitive { get; init; } - - /// - /// Allow implicit inheritance on destination types. - /// - public bool AllowImplicitDestinationInheritance { get; init; } - - /// - /// Allow implicit inheritance on source types. - /// - public bool AllowImplicitSourceInheritance { get; init; } = true; - - /// - /// Generate self-contained code (no shared helpers) for source generation. - /// - public bool SelfContainedCodeGeneration { get; init; } -} diff --git a/src/Mapster.DependencyInjection/GlobalUsings.cs b/src/Mapster.DependencyInjection/GlobalUsings.cs new file mode 100644 index 00000000..c0d86336 --- /dev/null +++ b/src/Mapster.DependencyInjection/GlobalUsings.cs @@ -0,0 +1,7 @@ +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using MapsterMapper; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Options; +global using Mapster; \ No newline at end of file diff --git a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj index 246ad210..15c1fac7 100644 --- a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj +++ b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj @@ -8,12 +8,15 @@ true Mapster.DependencyInjection.snk 10.0.0-pre01 + enable + enable + diff --git a/src/Mapster.DependencyInjection/MapsterOptions.cs b/src/Mapster.DependencyInjection/MapsterOptions.cs index b158c991..75ffa1a4 100644 --- a/src/Mapster.DependencyInjection/MapsterOptions.cs +++ b/src/Mapster.DependencyInjection/MapsterOptions.cs @@ -40,64 +40,5 @@ public sealed record MapsterOptions /// public bool SelfContainedCodeGeneration { get; set; } - /// - /// Name matching strategy (Exact, Flexible, IgnoreCase, ToCamelCase, FromCamelCase). - /// - public string? NameMatchingStrategy { get; set; } - - /// - /// Enable mapping to constructors when available. - /// - public bool MapToConstructor { get; set; } - - /// - /// Preserve reference cycles when mapping. - /// - public bool PreserveReference { get; set; } - - /// - /// Use shallow copy optimization when source and destination types are the same. - /// - public bool ShallowCopyForSameType { get; set; } - - /// - /// Ignore null source values. - /// - public bool IgnoreNullValues { get; set; } - - /// - /// Map enums by name instead of numeric value. - /// - public bool MapEnumByName { get; set; } - - /// - /// Ignore members not mapped explicitly. - /// - public bool IgnoreNonMapped { get; set; } - - /// - /// Avoid inline mapping when generating code. - /// - public bool AvoidInlineMapping { get; set; } - - /// - /// Enable unflattening (map nested source to flat destination). - /// - public bool Unflattening { get; set; } - - /// - /// Skip destination member checks when mapping. - /// - public bool SkipDestinationMemberCheck { get; set; } - - /// - /// Allow mapping to non-public members. - /// - public bool EnableNonPublicMembers { get; set; } - - /// - /// Maximum mapping depth (0 = unlimited). - /// - public int MaxDepth { get; set; } } diff --git a/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs new file mode 100644 index 00000000..1061d478 --- /dev/null +++ b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Mapster.DependencyInjection; + +/// +/// Helpers to apply to . +/// +internal static class MapsterOptionsExtensions +{ + /// + /// Applies the settings from to the specified . + /// + /// The instance containing the settings to apply. + /// The instance to which the settings will be applied. Can be . + [return: NotNullIfNotNull(nameof(config))] + public static TypeAdapterConfig? ApplyTo(this MapsterOptions options, TypeAdapterConfig? config) + { + ArgumentNullException.ThrowIfNull(options); + if (config is null) return null; + + config.RequireDestinationMemberSource = options.RequireDestinationMemberSource; + config.RequireExplicitMapping = options.RequireExplicitMapping; + config.RequireExplicitMappingPrimitive = options.RequireExplicitMappingPrimitive; + config.AllowImplicitDestinationInheritance = options.AllowImplicitDestinationInheritance; + config.AllowImplicitSourceInheritance = options.AllowImplicitSourceInheritance; + config.SelfContainedCodeGeneration = options.SelfContainedCodeGeneration; + + return config; + } +} \ No newline at end of file diff --git a/src/Mapster.DependencyInjection/SettingsOptions.cs b/src/Mapster.DependencyInjection/SettingsOptions.cs deleted file mode 100644 index 8917f43e..00000000 --- a/src/Mapster.DependencyInjection/SettingsOptions.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace Mapster.DependencyInjection; - -/// -/// Global default mapping settings applied to . -/// -public sealed record MapsterSettingsOptions -{ - /// - /// Name matching strategy (Exact, Flexible, IgnoreCase, ToCamelCase, FromCamelCase). - /// - public string? NameMatchingStrategy { get; init; } - - /// - /// Enable mapping to constructors when available. - /// - public bool MapToConstructor { get; init; } - - /// - /// Preserve reference cycles when mapping. - /// - public bool PreserveReference { get; init; } - - /// - /// Use shallow copy optimization when source and destination types are the same. - /// - public bool ShallowCopyForSameType { get; init; } - - /// - /// Ignore null source values. - /// - public bool IgnoreNullValues { get; init; } - - /// - /// Map enums by name instead of numeric value. - /// - public bool MapEnumByName { get; init; } - - /// - /// Ignore members not mapped explicitly. - /// - public bool IgnoreNonMapped { get; init; } - - /// - /// Avoid inline mapping when generating code. - /// - public bool AvoidInlineMapping { get; init; } - - /// - /// Enable unflattening (map nested source to flat destination). - /// - public bool Unflattening { get; init; } - - /// - /// Skip destination member checks when mapping. - /// - public bool SkipDestinationMemberCheck { get; init; } - - /// - /// Allow mapping to non-public members. - /// - public bool EnableNonPublicMembers { get; init; } - - /// - /// Maximum mapping depth (0 = unlimited). - /// - public int MaxDepth { get; init; } -} From c0bba85a15351abb77332a6c445cc3cf908784e4 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 20:13:48 +0100 Subject: [PATCH 4/9] feat(Config): Allow read from IConfiguration and configure TypeAdapterConfig from UseMapster Extension - Collected ambiguous signatures to one UseMapster Method - Renamed the ServiceCollectionExtensions to get rid of ambiguous warnings and enable clearer identification from the extension name --- .../HostBuilderExtensions.cs | 62 +++++++++++---- .../ServiceCollectionExtensions.cs | 77 ++++++++++++++----- 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/src/Mapster.DependencyInjection/HostBuilderExtensions.cs b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs index a9afd908..79b1e5b2 100644 --- a/src/Mapster.DependencyInjection/HostBuilderExtensions.cs +++ b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs @@ -2,32 +2,62 @@ namespace Mapster.DependencyInjection; public static class HostBuilderExtensions { - public static IHostBuilder UseMapster( - this IHostBuilder builder, - Action? configure = null) - { - return builder.ConfigureServices(services => services.AddMapster(configure)); - } + /// + /// Adds Mapster services to the including options binding and registration. + /// + /// The host builder. + /// The configuration section name to bind from. Defaults to . + /// If will be used, otherwise a new instance of will be used. + /// If changed to , Mapster's default registration () will not be added. + /// Optional delegate to configure additional services. + /// The host builder for chaining. + /// + /// To apply custom configuration, use the delegate to register services before Mapster services are added.
+ ///
public static IHostBuilder UseMapster( this IHostBuilder builder, string sectionName = "", - Func? configSection = null) + bool useGlobalConfig = true, + bool registerDefaultMapper = true, + Action? configureMappers = default) { - if (configSection is null) + return builder.ConfigureServices((ctx, services) => { - if (sectionName is not { Length: > 0 }) + configureMappers?.Invoke(ctx, services); + + if (!ctx.IsRegistered(nameof(UseMapster))) { - sectionName = MapsterOptions.DefaultName; + services + .AddMapsterOptions(ctx, sectionName) + .AddTypeAdapterConfig(ctx, useGlobalConfig); + + // check registration here to avoid breaking changes for those who use the current AddMapster extension method directly which doesn't requires HostBuilderContext + if (registerDefaultMapper && !ctx.IsRegistered(nameof(ServiceCollectionExtensions.AddMapster))) + { + services.AddMapster(); + } } - configSection = ctx => ctx.Configuration.GetSection(sectionName); - } + }); + } + + #region Registration Check to not duplicate registrations + internal static bool IsRegistered(this HostBuilderContext builder, string registeredKey, bool newIsRegistered = true) + { + return builder.Properties.IsRegistered(registeredKey, newIsRegistered); + } - return builder.ConfigureServices((context, services) => + internal static bool IsRegistered(this IDictionary properties, string registeredKey, bool newIsRegistered = true) + { + if (properties.TryGetValue(registeredKey, out var value) && + value is bool registeredValue && + registeredValue) { - var section = configSection(context); - services.AddMapster(section); - }); + return true; + } + properties[registeredKey] = newIsRegistered; + return false; } + #endregion } diff --git a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs index 1bf5affe..1547e204 100644 --- a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs @@ -3,37 +3,76 @@ public static class ServiceCollectionExtensions { /// - /// Adds Mapster mapping services to the specified and optionally configures . + /// Adds to the specified . /// - /// The to which the Mapster services will be added. Cannot be null. - /// An optional delegate to configure . If null, default options are used. + /// The to which the Mapster options will be added. + /// The providing context for the host builder. + /// The configuration section name to bind from. Defaults to . + /// A delegate to provide a custom instance for binding. /// The same instance so that additional calls can be chained. - public static IServiceCollection AddMapster( + public static IServiceCollection AddMapsterOptions( this IServiceCollection services, - Action? configure = null) + HostBuilderContext context, + string sectionName = "", + Func? configuration = default) { - if (configure is not null) + if (context.IsRegistered(nameof(AddMapsterOptions))) { - services.Configure(configure); + return services; } - else + if (configuration is null) { - services.AddOptions(); + if (sectionName is not { Length: > 0 }) + { + sectionName = MapsterOptions.DefaultName; + } + configuration = ctx => ctx.Configuration.GetSection(sectionName); } - - return services.AddMapster(); + var configSection = configuration.Invoke(context); + return services.Configure(configSection); } - /// - /// Adds Mapster to the specified using configuration, calls to register Mapster services. + /// Adds Mapster as Singleton to the specified . /// - /// The to insert to. - /// The instance to use - /// The same as provided, added up with the Mapster services - public static IServiceCollection AddMapster(this IServiceCollection services, IConfiguration configuration) + /// The to which the Mapster services will be added. Cannot be null. + /// If will be used, otherwise a new instance of will be used. + /// If true, uses an existing registered instance as base if available. + /// If true, applies the configured to the . + /// Delegate to configure . If empty, the configuration defined by the previous Settings will be used. + /// The same instance so that additional calls can be chained. + /// + /// The configuration options will be applyed in the order of parameters in the method signature.
+ /// Important: If is and an existing is found, this will replace the config created by !
+ ///
+ public static IServiceCollection AddTypeAdapterConfig( + this IServiceCollection services, + HostBuilderContext context, + bool useGlobalConfig = true, + bool useExisting = false, + bool readFromOptions = true, + Func? configure = default) { - services.Configure(configuration); - return services.AddMapster(); + if (context.IsRegistered(nameof(AddTypeAdapterConfig))) + { + return services; + } + return services.AddSingleton(serviceProvider => + { + var config = useGlobalConfig ? TypeAdapterConfig.GlobalSettings : new TypeAdapterConfig(); + + if (useExisting && serviceProvider.GetService() is TypeAdapterConfig existingConfig) + { + config = existingConfig; // TODO: Find a way to apply to existing config instead of replacing it + } + + if (readFromOptions && serviceProvider.GetService>()?.Value is MapsterOptions options) + { + config = options.ApplyTo(config); + } + + return configure?.Invoke(config) ?? config; + }); + } /// From b972923afe20016a22e0a675e5bacd2d7352b131 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 22:49:57 +0100 Subject: [PATCH 5/9] docs(Configuration): Add configuration Docs --- docs/articles/packages/DI-Configuration.md | 128 +++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/articles/packages/DI-Configuration.md diff --git a/docs/articles/packages/DI-Configuration.md b/docs/articles/packages/DI-Configuration.md new file mode 100644 index 00000000..16149aff --- /dev/null +++ b/docs/articles/packages/DI-Configuration.md @@ -0,0 +1,128 @@ +--- +uid: Mapster.Packages.DependencyInjection.Configuration +title: "Packages - Configuration Support via DI" +--- + +## Installation + +This package allows you to configure Mapster through `appsettings.json` and HostBuilder integration. + +```nuget +PM> Install-Package Mapster.DependencyInjection +``` + +For basic dependency injection setup, see [Dependency Injection Support](xref:Mapster.Packages.DependencyInjection). + +## Usage + +### HostBuilder Integration + +`UseMapster` simplifies setup by binding `MapsterOptions` from configuration and registering `TypeAdapterConfig`: + +```csharp +var host = new HostBuilder() + .UseMapster() // binds from the "Mapster" section by default + .Build(); +``` + +By default, `UseMapster` registers `TypeAdapterConfig.GlobalSettings`. To use an isolated `TypeAdapterConfig` instance instead: + +```csharp +var host = new HostBuilder() + .UseMapster(useGlobalConfig: false) + .Build(); +``` + +To bind from a different configuration section: + +```csharp +var host = new HostBuilder() + .UseMapster(sectionName: "CustomMapster") + .Build(); +``` + +If you want options/config registration without adding the default `IMapper` (`ServiceMapper`) registration: + +```csharp +var host = new HostBuilder() + .UseMapster(registerDefaultMapper: false) + .Build(); +``` + +### Configure Mapster in appsettings + +You can configure Mapster's global `TypeAdapterConfig` switches through your `appsettings.json` file: + +```json +{ + "Mapster": { + "RequireExplicitMapping": true, + "AllowImplicitSourceInheritance": true + } +} +``` + +These options are bound to `IOptions` and applied to `TypeAdapterConfig` on registration. + +> [!NOTE] +> Expect the `MapsterOptions` to match the properties available on `TypeAdapterConfig`. + +### Advanced Service Collection Extensions + +For finer control, you can use the individual extension methods directly: + +#### AddMapsterOptions + +Registers `MapsterOptions` and binds them from configuration: + +```csharp +services.AddMapsterOptions(context, sectionName: "CustomMapster"); +``` + +Or provide a custom configuration delegate: + +```csharp +services.AddMapsterOptions(context, configuration: ctx => ctx.Configuration.GetSection("MySection")); +``` + +#### AddTypeAdapterConfig + +Registers `TypeAdapterConfig` as a singleton with several configuration options: + +| Parameter | Default | Description | +| ----------------- | ------- | -------------------------------------------- | +| `useGlobalConfig` | `true` | Use `TypeAdapterConfig.GlobalSettings` | +| `useExisting` | `false` | Use existing registered config as base | +| `readFromOptions` | `true` | Apply bound `MapsterOptions` to the config | +| `configure` | `null` | Additional configuration delegate | + +> [!IMPORTANT] +> Configuration options are applied in the order of the parameters. If `useExisting` is `true` and an existing `TypeAdapterConfig` is found, it will replace the config created by `useGlobalConfig`. + +```csharp +services.AddTypeAdapterConfig(context, useGlobalConfig: false, configure: config => +{ + config.RequireExplicitMapping = true; + config.NewConfig() + .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}"); + return config; +}); +``` + +Complete example with `UseMapster`: + +```csharp +var host = new HostBuilder() + .UseMapster(useGlobalConfig: false, configureMappers: (ctx, services) => + { + services.AddTypeAdapterConfig(ctx, configure: config => + { + config.NewConfig() + .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}"); + return config; + }); + }) + .Build(); +``` + +For service injection during mapping, see [Dependency Injection Support](xref:Mapster.Packages.DependencyInjection). From 66973731c6a5cdf1f93020acfc1773bb669c8e8c Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 22:50:59 +0100 Subject: [PATCH 6/9] chore: Add toc entry --- docs/articles/packages/toc.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/articles/packages/toc.yml b/docs/articles/packages/toc.yml index 7558022c..4f2478c2 100644 --- a/docs/articles/packages/toc.yml +++ b/docs/articles/packages/toc.yml @@ -9,6 +9,9 @@ - name: Dependency Injection uid: Mapster.Packages.DependencyInjection href: Dependency-Injection.md +- name: Configuration Support + uid: Mapster.Packages.DependencyInjection.Configuration + href: DI-Configuration.md - name: "EF 6 and EF Core Support" uid: Mapster.Packages.EntityFramework href: EF-6-and-EF-Core.md From 63994d728a6ccaedbd51d17648e01260083a713c Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 22:53:00 +0100 Subject: [PATCH 7/9] chore: adjust nullability --- .../MapsterOptionsExtensions.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs index 1061d478..a4c380ba 100644 --- a/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs +++ b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; namespace Mapster.DependencyInjection; @@ -12,12 +11,12 @@ internal static class MapsterOptionsExtensions /// Applies the settings from to the specified . /// /// The instance containing the settings to apply. - /// The instance to which the settings will be applied. Can be . - [return: NotNullIfNotNull(nameof(config))] - public static TypeAdapterConfig? ApplyTo(this MapsterOptions options, TypeAdapterConfig? config) + /// The instance to which the settings will be applied. + /// The updated instance. + public static TypeAdapterConfig ApplyTo(this MapsterOptions options, TypeAdapterConfig config) { ArgumentNullException.ThrowIfNull(options); - if (config is null) return null; + ArgumentNullException.ThrowIfNull(config); config.RequireDestinationMemberSource = options.RequireDestinationMemberSource; config.RequireExplicitMapping = options.RequireExplicitMapping; From 0beb67e819baa9f7f995348ba1a40751bc7fdc47 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 22:54:35 +0100 Subject: [PATCH 8/9] test(Configuration): Provide Test coverage for new features --- .../GlobalUsings.cs | 10 ++ .../HostBuilderExtensionsTests.cs | 115 ++++++++++++++++++ .../Mapster.DependencyInjection.Tests.csproj | 2 + .../MapsterOptionsTests.cs | 66 ++++++++++ .../ServiceCollectionExtensionsTests.cs | 97 +++++++++++++++ 5 files changed, 290 insertions(+) create mode 100644 src/Mapster.DependencyInjection.Tests/GlobalUsings.cs create mode 100644 src/Mapster.DependencyInjection.Tests/HostBuilderExtensionsTests.cs create mode 100644 src/Mapster.DependencyInjection.Tests/MapsterOptionsTests.cs create mode 100644 src/Mapster.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs diff --git a/src/Mapster.DependencyInjection.Tests/GlobalUsings.cs b/src/Mapster.DependencyInjection.Tests/GlobalUsings.cs new file mode 100644 index 00000000..83b11fb4 --- /dev/null +++ b/src/Mapster.DependencyInjection.Tests/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.Collections.Generic; +global using MapsterMapper; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Options; +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Shouldly; +global using Mapster; diff --git a/src/Mapster.DependencyInjection.Tests/HostBuilderExtensionsTests.cs b/src/Mapster.DependencyInjection.Tests/HostBuilderExtensionsTests.cs new file mode 100644 index 00000000..c95ad952 --- /dev/null +++ b/src/Mapster.DependencyInjection.Tests/HostBuilderExtensionsTests.cs @@ -0,0 +1,115 @@ +namespace Mapster.DependencyInjection.Tests; + +[TestClass] +public class HostBuilderExtensionsTests +{ + [TestMethod] + public void UseMapster_WithConfigureMappers_AllowsCustomTypeAdapterConfigConfiguration() + { + // Arrange + var host = new HostBuilder() + .UseMapster( + configureMappers: (ctx, services) => + { + services.AddTypeAdapterConfig(ctx, useGlobalConfig: false, configure: config => + { + config.NewConfig() + .Map(dest => dest.Value, src => src.Value * 2); + return config; + }); + }) + .Build(); + + // Act + var mapper = host.Services.GetRequiredService(); + var dest = mapper.Map(new ValueSource { Value = 5 }); + + // Assert + dest.Value.ShouldBe(10); + } + + [TestMethod] + public void UseMapster_Default_RegistersServices() + { + // Arrange + var host = new HostBuilder() + .UseMapster() + .Build(); + + // Act + Assert + host.Services.GetRequiredService>().ShouldNotBeNull(); + host.Services.GetRequiredService().ShouldBe(TypeAdapterConfig.GlobalSettings); + host.Services.GetRequiredService().ShouldBeOfType(); + } + + [TestMethod] + public void UseMapster_WhenCalledTwice_RunsConfigureMappersEachTime_ButRegistersCoreServicesOnce() + { + // Arrange + var configureMappersCalls = 0; + var typeAdapterConfigRegistrations = 0; + var mapperRegistrations = 0; + var mapsterOptionsConfigureRegistrations = 0; + + var host = new HostBuilder() + .UseMapster( + configureMappers: (_, _) => configureMappersCalls++) + .UseMapster( + configureMappers: (_, _) => configureMappersCalls++) + .ConfigureServices((_, services) => + { + typeAdapterConfigRegistrations = services.Count(d => d.ServiceType == typeof(TypeAdapterConfig)); + mapperRegistrations = services.Count(d => d.ServiceType == typeof(IMapper)); + mapsterOptionsConfigureRegistrations = services.Count(d => d.ServiceType == typeof(IConfigureOptions)); + }) + .Build(); + + // Act + Assert + configureMappersCalls.ShouldBe(2); + + typeAdapterConfigRegistrations.ShouldBe(1); + mapperRegistrations.ShouldBe(1); + mapsterOptionsConfigureRegistrations.ShouldBe(1); + + host.Services.GetRequiredService().ShouldNotBeNull(); + } + + [TestMethod] + public void UseMapster_WhenUseGlobalConfigFalse_RegistersNewTypeAdapterConfigInstance() + { + // Arrange + var host = new HostBuilder() + .UseMapster(useGlobalConfig: false) + .Build(); + + // Act + var config = host.Services.GetRequiredService(); + + // Assert + ReferenceEquals(config, TypeAdapterConfig.GlobalSettings).ShouldBeFalse(); + } + + [TestMethod] + public void UseMapster_WhenRegisterMapsterSingletonFalse_DoesNotRegisterIMapper() + { + // Arrange + var host = new HostBuilder() + .UseMapster(registerDefaultMapper: false) + .Build(); + + // Act + Assert + host.Services.GetService().ShouldBeNull(); + host.Services.GetRequiredService().ShouldNotBeNull(); + host.Services.GetRequiredService>().ShouldNotBeNull(); + } + + private sealed class ValueSource + { + public int Value { get; set; } + } + + private sealed class ValueDest + { + public int Value { get; set; } + } +} diff --git a/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj b/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj index 4a3b0ed0..00ba2029 100644 --- a/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj +++ b/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj @@ -4,6 +4,8 @@ net10.0;net9.0;net8.0; true false + enable + enable diff --git a/src/Mapster.DependencyInjection.Tests/MapsterOptionsTests.cs b/src/Mapster.DependencyInjection.Tests/MapsterOptionsTests.cs new file mode 100644 index 00000000..ee298965 --- /dev/null +++ b/src/Mapster.DependencyInjection.Tests/MapsterOptionsTests.cs @@ -0,0 +1,66 @@ +namespace Mapster.DependencyInjection.Tests; + +[TestClass] +public class MapsterOptionsTests +{ + [TestMethod] + public void UseMapster_BindsDefaultSection() + { + var host = new HostBuilder() + .ConfigureAppConfiguration(cfg => + { + cfg.AddInMemoryCollection(new[] + { + new KeyValuePair("Mapster:RequireExplicitMapping", "true"), + }); + }) + .UseMapster(useGlobalConfig: false) + .Build(); + + var options = host.Services.GetRequiredService>().Value; + options.RequireExplicitMapping.ShouldBe(true); + + var config = host.Services.GetRequiredService(); + ReferenceEquals(config, TypeAdapterConfig.GlobalSettings).ShouldBeFalse(); + config.RequireExplicitMapping.ShouldBe(true); + } + + [TestMethod] + public void UseMapster_WithSectionName_BindsCustomSection() + { + var host = new HostBuilder() + .ConfigureAppConfiguration(cfg => + { + cfg.AddInMemoryCollection(new[] + { + new KeyValuePair("CustomMapster:RequireDestinationMemberSource", "true"), + }); + }) + .UseMapster(sectionName: "CustomMapster", useGlobalConfig: false) + .Build(); + + var options = host.Services.GetRequiredService>().Value; + options.RequireDestinationMemberSource.ShouldBe(true); + } + + [TestMethod] + public void UseMapster_WhenNoSection_UsesDefaultOptionsValues() + { + using var host = new HostBuilder() + // intentionally no appsettings / Mapster section + .UseMapster(sectionName: "", useGlobalConfig: false) + .Build(); + + var options = host.Services.GetRequiredService>().Value; + options.RequireDestinationMemberSource.ShouldBeFalse(); + options.RequireExplicitMapping.ShouldBeFalse(); + options.RequireExplicitMappingPrimitive.ShouldBeFalse(); + options.AllowImplicitDestinationInheritance.ShouldBeFalse(); + options.AllowImplicitSourceInheritance.ShouldBeTrue(); + options.SelfContainedCodeGeneration.ShouldBeFalse(); + + host.Services.GetRequiredService().ShouldNotBeNull(); + host.Services.GetRequiredService().ShouldBeOfType(); + } +} + diff --git a/src/Mapster.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs b/src/Mapster.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..f7de8dc2 --- /dev/null +++ b/src/Mapster.DependencyInjection.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,97 @@ +namespace Mapster.DependencyInjection.Tests; + +[TestClass] +public class ServiceCollectionExtensionsTests +{ + [TestMethod] + public void AddMapster_Parameterless_RegistersServiceMapper_WhenTypeAdapterConfigRegistered() + { + var services = new ServiceCollection(); + services.AddSingleton(TypeAdapterConfig.GlobalSettings); + services.AddMapster(); + + using var sp = services.BuildServiceProvider(); + + var mapper = sp.GetRequiredService(); + mapper.ShouldNotBeNull(); + mapper.ShouldBeOfType(); + + var config = sp.GetRequiredService(); + config.ShouldBe(TypeAdapterConfig.GlobalSettings); + } + + [TestMethod] + public void AddMapsterOptions_DefaultSection_BindsOptions() + { + using var host = new HostBuilder() + .ConfigureAppConfiguration(cfg => + { + cfg.AddInMemoryCollection(new[] + { + new KeyValuePair("Mapster:RequireExplicitMapping", "true"), + new KeyValuePair("Mapster:AllowImplicitSourceInheritance", "false"), + }); + }) + .ConfigureServices((ctx, services) => + { + services.AddMapsterOptions(ctx); + }) + .Build(); + + var options = host.Services.GetRequiredService>().Value; + options.RequireExplicitMapping.ShouldBe(true); + options.AllowImplicitSourceInheritance.ShouldBe(false); + } + + [TestMethod] + public void AddMapsterOptions_WithConfigurationDelegate_BindsOptions() + { + using var host = new HostBuilder() + .ConfigureAppConfiguration(cfg => + { + cfg.AddInMemoryCollection(new[] + { + new KeyValuePair("Custom:RequireExplicitMapping", "true"), + }); + }) + .ConfigureServices((ctx, services) => + { + services.AddMapsterOptions( + ctx, + configuration: c => c.Configuration.GetSection("Custom")); + }) + .Build(); + + host.Services.GetRequiredService>().Value.RequireExplicitMapping.ShouldBe(true); + } + + [TestMethod] + public void AddTypeAdapterConfig_AppliesBoundOptions_AndConfigureDelegate() + { + using var host = new HostBuilder() + .ConfigureAppConfiguration(cfg => + { + cfg.AddInMemoryCollection(new[] + { + new KeyValuePair("Mapster:RequireExplicitMapping", "true"), + }); + }) + .ConfigureServices((ctx, services) => + { + services + .AddMapsterOptions(ctx) + .AddTypeAdapterConfig(ctx, useGlobalConfig: false, configure: config => + { + config.RequireDestinationMemberSource = true; + return config; + }) + .AddMapster(); + }) + .Build(); + + var config = host.Services.GetRequiredService(); + ReferenceEquals(config, TypeAdapterConfig.GlobalSettings).ShouldBeFalse(); + config.RequireExplicitMapping.ShouldBe(true); + config.RequireDestinationMemberSource.ShouldBe(true); + } +} From 1336b2c897adfdae2839e0d7472f09b9087e1e02 Mon Sep 17 00:00:00 2001 From: DevTKSS Date: Sun, 1 Feb 2026 23:41:23 +0100 Subject: [PATCH 9/9] chore: Update to file scoped namespaces and apply pragma warning where needed for IDE0130 --- .../InjectionTest.cs | 157 +++++++------ .../ServiceMapper.cs | 207 +++++++++--------- .../TypeAdapterExtensions.cs | 27 +-- 3 files changed, 189 insertions(+), 202 deletions(-) diff --git a/src/Mapster.DependencyInjection.Tests/InjectionTest.cs b/src/Mapster.DependencyInjection.Tests/InjectionTest.cs index a1a1cb15..f6d6a435 100644 --- a/src/Mapster.DependencyInjection.Tests/InjectionTest.cs +++ b/src/Mapster.DependencyInjection.Tests/InjectionTest.cs @@ -1,106 +1,99 @@ -using System; -using MapsterMapper; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; +namespace Mapster.DependencyInjection.Tests; -namespace Mapster.DependencyInjection.Tests +[TestClass] +public class InjectionTest { - [TestClass] - public class InjectionTest + [TestMethod] + public void Injection() { - [TestMethod] - public void Injection() - { - var config = new TypeAdapterConfig(); - config.NewConfig() - .Map(dto => dto.Name, _ => MapContext.Current.GetService().GetName()); - - IServiceCollection sc = new ServiceCollection(); - sc.AddScoped(); - sc.AddSingleton(config); - sc.AddScoped(); + var config = new TypeAdapterConfig(); + config.NewConfig() + .Map(dto => dto.Name, _ => MapContext.Current.GetService().GetName()); - var sp = sc.BuildServiceProvider(); - using (var scope = sp.CreateScope()) - { - var mapper = scope.ServiceProvider.GetService(); - var poco = new Poco { Id = "bar" }; - var dto = mapper.Map(poco); - dto.Name.ShouldBe("foo"); - } - } + IServiceCollection sc = new ServiceCollection(); + sc.AddScoped(); + sc.AddSingleton(config); + sc.AddScoped(); - [TestMethod, ExpectedException(typeof(InvalidOperationException))] - public void NoServiceAdapter_InjectionError() + var sp = sc.BuildServiceProvider(); + using (var scope = sp.CreateScope()) { - var expectedValue = MapContext.Current.GetService().GetName(); - var config = ConfigureMapping(expectedValue); - - IServiceCollection sc = new ServiceCollection(); - sc.AddScoped(); - sc.AddSingleton(config); - // We should use ServiceMapper in normal code - // but for this test we want to be sure the code will generate the InvalidOperationException - sc.AddScoped(); - - var sp = sc.BuildServiceProvider(); - using var scope = sp.CreateScope(); var mapper = scope.ServiceProvider.GetService(); - MapToDto(mapper, expectedValue); - } - - private static TypeAdapterConfig ConfigureMapping(string expectedValue) - { - var config = new TypeAdapterConfig(); - config.NewConfig() - .Map(dto => dto.Name, _ => expectedValue); - return config; - } - - private static void MapToDto(IMapper mapper, string expectedValue) - { var poco = new Poco { Id = "bar" }; var dto = mapper.Map(poco); - dto.Name.ShouldBe(expectedValue); + dto.Name.ShouldBe("foo"); } + } - [TestMethod] - public void GetMapperInstanceFromServiceCollection_ShouldNotFaceException() - { - const string expectedValue = "foobar"; - var config = ConfigureMapping(expectedValue); - ServiceCollection serviceCollection = new(); - serviceCollection.AddSingleton(config); - serviceCollection.AddMapster(); - var serviceProvider = serviceCollection.BuildServiceProvider(); - using var scope = serviceProvider.CreateScope(); - var mapper = scope.ServiceProvider.GetRequiredService(); - MapToDto(mapper, expectedValue); - } + [TestMethod, ExpectedException(typeof(InvalidOperationException))] + public void NoServiceAdapter_InjectionError() + { + var expectedValue = MapContext.Current.GetService().GetName(); + var config = ConfigureMapping(expectedValue); + + IServiceCollection sc = new ServiceCollection(); + sc.AddScoped(); + sc.AddSingleton(config); + // We should use ServiceMapper in normal code + // but for this test we want to be sure the code will generate the InvalidOperationException + sc.AddScoped(); + + var sp = sc.BuildServiceProvider(); + using var scope = sp.CreateScope(); + var mapper = scope.ServiceProvider.GetService(); + MapToDto(mapper, expectedValue); } - public interface IMockService + private static TypeAdapterConfig ConfigureMapping(string expectedValue) { - string GetName(); + var config = new TypeAdapterConfig(); + config.NewConfig() + .Map(dto => dto.Name, _ => expectedValue); + return config; } - public class MockService : IMockService + private static void MapToDto(IMapper mapper, string expectedValue) { - public string GetName() - { - return "foo"; - } + var poco = new Poco { Id = "bar" }; + var dto = mapper.Map(poco); + dto.Name.ShouldBe(expectedValue); } - public class Poco + [TestMethod] + public void GetMapperInstanceFromServiceCollection_ShouldNotFaceException() { - public string Id { get; set; } + const string expectedValue = "foobar"; + var config = ConfigureMapping(expectedValue); + ServiceCollection serviceCollection = new(); + serviceCollection.AddSingleton(config); + serviceCollection.AddMapster(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var mapper = scope.ServiceProvider.GetRequiredService(); + MapToDto(mapper, expectedValue); } +} - public class Dto +public interface IMockService +{ + string GetName(); +} + +public class MockService : IMockService +{ + public string GetName() { - public string Id { get; set; } - public string Name { get; set; } + return "foo"; } -} \ No newline at end of file +} + +public class Poco +{ + public string Id { get; set; } +} + +public class Dto +{ + public string Id { get; set; } + public string Name { get; set; } +} diff --git a/src/Mapster.DependencyInjection/ServiceMapper.cs b/src/Mapster.DependencyInjection/ServiceMapper.cs index 1d909893..d07d714e 100644 --- a/src/Mapster.DependencyInjection/ServiceMapper.cs +++ b/src/Mapster.DependencyInjection/ServiceMapper.cs @@ -1,104 +1,103 @@ -using System; -using Mapster; - -namespace MapsterMapper -{ - public class ServiceMapper : Mapper - { - internal const string DI_KEY = "Mapster.DependencyInjection.sp"; - private readonly IServiceProvider _serviceProvider; - - public ServiceMapper(IServiceProvider serviceProvider, TypeAdapterConfig config) : base(config) - { - _serviceProvider = serviceProvider; - } - - /// - /// Create mapping builder. - /// - /// Source type to create mapping builder. - /// Source object to create mapping builder. - /// - public override ITypeAdapterBuilder From(TSource source) - { - return base.From(source) - .AddParameters(DI_KEY, _serviceProvider); - } - - - /// - /// Perform mapping from source object to type of destination. - /// - /// Destination type to create mapping builder. - /// Source object to create mapping builder. - /// Type of destination object that mapped. - public override TDestination Map(object source) - { - using var scope = new MapContextScope(); - scope.Context.Parameters[DI_KEY] = _serviceProvider; - return base.Map(source); - } - - - /// - /// Perform mapping from type of source to type of destination. - /// - /// Source type to map. - /// Destination type to map. - /// Source object to map. - /// Type of destination object that mapped. - public override TDestination Map(TSource source) - { - using var scope = new MapContextScope(); - scope.Context.Parameters[DI_KEY] = _serviceProvider; - return base.Map(source); - } - - - /// - /// Perform mapping from type of source to type of destination. - /// - /// Source type to map. - /// Destination type to map. - /// Source object to map. - /// Destination object to map. - /// Type of destination object that mapped. - public override TDestination Map(TSource source, TDestination destination) - { - using var scope = new MapContextScope(); - scope.Context.Parameters[DI_KEY] = _serviceProvider; - return base.Map(source, destination); - } - - - /// - /// Perform mapping source object from source type to destination type. - /// - /// Source object to map. - /// Source type to map. - /// Destination type to map. - /// Mapped object. - public override object Map(object source, Type sourceType, Type destinationType) - { - using var scope = new MapContextScope(); - scope.Context.Parameters[DI_KEY] = _serviceProvider; - return base.Map(source, sourceType, destinationType); - } - - - /// - /// Perform mapping source object from source type to destination type. - /// - /// Source object to map. - /// Destination object to map. - /// Source type to map. - /// Destination type to map. - /// - public override object Map(object source, object destination, Type sourceType, Type destinationType) - { - using var scope = new MapContextScope(); - scope.Context.Parameters[DI_KEY] = _serviceProvider; - return base.Map(source, destination, sourceType, destinationType); - } - } -} +#pragma warning disable IDE0130 // Disable namespace check for file +namespace MapsterMapper; +#pragma warning restore IDE0130 // Disable namespace check for file + +public class ServiceMapper : Mapper +{ + internal const string DI_KEY = "Mapster.DependencyInjection.sp"; + private readonly IServiceProvider _serviceProvider; + + public ServiceMapper(IServiceProvider serviceProvider, TypeAdapterConfig config) : base(config) + { + _serviceProvider = serviceProvider; + } + + /// + /// Create mapping builder. + /// + /// Source type to create mapping builder. + /// Source object to create mapping builder. + /// + public override ITypeAdapterBuilder From(TSource source) + { + return base.From(source) + .AddParameters(DI_KEY, _serviceProvider); + } + + + /// + /// Perform mapping from source object to type of destination. + /// + /// Destination type to create mapping builder. + /// Source object to create mapping builder. + /// Type of destination object that mapped. + public override TDestination Map(object source) + { + using var scope = new MapContextScope(); + scope.Context.Parameters[DI_KEY] = _serviceProvider; + return base.Map(source); + } + + + /// + /// Perform mapping from type of source to type of destination. + /// + /// Source type to map. + /// Destination type to map. + /// Source object to map. + /// Type of destination object that mapped. + public override TDestination Map(TSource source) + { + using var scope = new MapContextScope(); + scope.Context.Parameters[DI_KEY] = _serviceProvider; + return base.Map(source); + } + + + /// + /// Perform mapping from type of source to type of destination. + /// + /// Source type to map. + /// Destination type to map. + /// Source object to map. + /// Destination object to map. + /// Type of destination object that mapped. + public override TDestination Map(TSource source, TDestination destination) + { + using var scope = new MapContextScope(); + scope.Context.Parameters[DI_KEY] = _serviceProvider; + return base.Map(source, destination); + } + + + /// + /// Perform mapping source object from source type to destination type. + /// + /// Source object to map. + /// Source type to map. + /// Destination type to map. + /// Mapped object. + public override object Map(object source, Type sourceType, Type destinationType) + { + using var scope = new MapContextScope(); + scope.Context.Parameters[DI_KEY] = _serviceProvider; + return base.Map(source, sourceType, destinationType); + } + + + /// + /// Perform mapping source object from source type to destination type. + /// + /// Source object to map. + /// Destination object to map. + /// Source type to map. + /// Destination type to map. + /// + public override object Map(object source, object destination, Type sourceType, Type destinationType) + { + using var scope = new MapContextScope(); + scope.Context.Parameters[DI_KEY] = _serviceProvider; + return base.Map(source, destination, sourceType, destinationType); + } +} + diff --git a/src/Mapster.DependencyInjection/TypeAdapterExtensions.cs b/src/Mapster.DependencyInjection/TypeAdapterExtensions.cs index f715bce3..93752422 100644 --- a/src/Mapster.DependencyInjection/TypeAdapterExtensions.cs +++ b/src/Mapster.DependencyInjection/TypeAdapterExtensions.cs @@ -1,22 +1,17 @@ -using System; -using System.Collections.Generic; -using MapsterMapper; +namespace Mapster; -namespace Mapster +public static class TypeAdapterExtensions { - public static class TypeAdapterExtensions + internal static U GetValueOrDefault(this IDictionary dict, T key) { - internal static U GetValueOrDefault(this IDictionary dict, T key) - { - return dict.TryGetValue(key, out var value) ? value : default; - } + return dict.TryGetValue(key, out var value) ? value : default; + } - public static TService GetService(this MapContext context) - { - var sp = (IServiceProvider) context?.Parameters.GetValueOrDefault(ServiceMapper.DI_KEY); - if (sp == null) - throw new InvalidOperationException("Mapping must be called using ServiceAdapter"); - return (TService)sp.GetService(typeof(TService)); - } + public static TService GetService(this MapContext context) + { + var sp = (IServiceProvider) context?.Parameters.GetValueOrDefault(ServiceMapper.DI_KEY); + if (sp == null) + throw new InvalidOperationException("Mapping must be called using ServiceAdapter"); + return (TService)sp.GetService(typeof(TService)); } }