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). 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 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/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.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); + } +} 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/HostBuilderExtensions.cs b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs new file mode 100644 index 00000000..79b1e5b2 --- /dev/null +++ b/src/Mapster.DependencyInjection/HostBuilderExtensions.cs @@ -0,0 +1,63 @@ +namespace Mapster.DependencyInjection; + +public static class HostBuilderExtensions +{ + + /// + /// 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 = "", + bool useGlobalConfig = true, + bool registerDefaultMapper = true, + Action? configureMappers = default) + { + + return builder.ConfigureServices((ctx, services) => + { + configureMappers?.Invoke(ctx, services); + + if (!ctx.IsRegistered(nameof(UseMapster))) + { + 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(); + } + } + }); + } + + #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); + } + + internal static bool IsRegistered(this IDictionary properties, string registeredKey, bool newIsRegistered = true) + { + if (properties.TryGetValue(registeredKey, out var value) && + value is bool registeredValue && + registeredValue) + { + return true; + } + properties[registeredKey] = newIsRegistered; + return false; + } + #endregion +} 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 new file mode 100644 index 00000000..75ffa1a4 --- /dev/null +++ b/src/Mapster.DependencyInjection/MapsterOptions.cs @@ -0,0 +1,44 @@ +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 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; } + +} + diff --git a/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs new file mode 100644 index 00000000..a4c380ba --- /dev/null +++ b/src/Mapster.DependencyInjection/MapsterOptionsExtensions.cs @@ -0,0 +1,30 @@ +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. + /// The updated instance. + public static TypeAdapterConfig ApplyTo(this MapsterOptions options, TypeAdapterConfig config) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(config); + + 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/ServiceCollectionExtensions.cs b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs index 36204baf..1547e204 100644 --- a/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Mapster.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,13 +1,88 @@ -using MapsterMapper; -using Microsoft.Extensions.DependencyInjection; - -namespace Mapster -{ - public static class ServiceCollectionExtensions - { - public static void AddMapster(this IServiceCollection serviceCollection) - { - serviceCollection.AddTransient(); - } - } +namespace Mapster.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Adds to the specified . + /// + /// 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 AddMapsterOptions( + this IServiceCollection services, + HostBuilderContext context, + string sectionName = "", + Func? configuration = default) + { + if (context.IsRegistered(nameof(AddMapsterOptions))) + { + return services; + } + if (configuration is null) + { + if (sectionName is not { Length: > 0 }) + { + sectionName = MapsterOptions.DefaultName; + } + configuration = ctx => ctx.Configuration.GetSection(sectionName); + } + var configSection = configuration.Invoke(context); + return services.Configure(configSection); + } + /// + /// Adds Mapster as Singleton to the specified . + /// + /// 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) + { + 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; + }); + + } + + /// + /// 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 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)); } }