diff --git a/src/Mapster.Fluent/Configs/FrozenTypeAdapterConfig.cs b/src/Mapster.Fluent/Configs/FrozenTypeAdapterConfig.cs new file mode 100644 index 0000000..9a889af --- /dev/null +++ b/src/Mapster.Fluent/Configs/FrozenTypeAdapterConfig.cs @@ -0,0 +1,120 @@ +using Mapster.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Mapster.Fluent.Configs +{ + public class FrozenTypeAdapterConfig : BaseTypeAdapterConfigDecorator, ITypeAdapterConfig + { + + private readonly ConcurrentDictionary _frozentypes = new(); + private readonly ITypeAdapterConfig _dummyConfig = new TypeAdapterConfig(); + + public bool IsTotalFrozen { get; private set; } + + + + private FrozenTypeAdapterConfig(ITypeAdapterConfig config, bool isTotalFrozen, bool IsGlobal = false ) + { + IsTotalFrozen = isTotalFrozen; + } + + public FrozenTypeAdapterConfig(bool IsGlobal = false) : base(IsGlobal) + { + } + + public FrozenTypeAdapterConfig(ITypeAdapterConfig config, bool IsGlobal = false) : base(config, IsGlobal) + { + } + + public override TypeAdapterSetter ForType(Type sourceType, Type destinationType) + { + if (IsTotalFrozen || + _frozentypes.TryGetValue(new TypeTuple(sourceType, destinationType), out _)) + { + _dummyConfig.Clear(); + return _dummyConfig.NewConfig(sourceType,destinationType); + } + + return base.ForType(sourceType, destinationType); + } + + + public void FrozenTypes(Type sourceType, Type destinationType) + { + var types = new TypeTuple(sourceType, destinationType); + _frozentypes.TryAdd(types, types); + } + + public void FrozenTypes(TypeTuple types) + { + Compile(types.Source, types.Destination); + _frozentypes.TryAdd(types, types); + } + + public void FrozenTypes() + { + Compile(typeof(TSource), typeof(TDestination)); + + var types = new TypeTuple(typeof(TSource), typeof(TDestination)); + _frozentypes.TryAdd(types, types); + } + + public void DeepFreeze() + { + var keys = RuleMap.Keys.ToList(); + Compile(); + + foreach (var item in keys) + { + _frozentypes.TryAdd(item, item); + } + + IsTotalFrozen = true; + } + + + + public override ITypeAdapterConfig Clone() + { + var result = new FrozenTypeAdapterConfig(base.Clone(), IsTotalFrozen); + + if (IsTotalFrozen) + result.DeepFreeze(); + + else if(_frozentypes.Any()) + { + foreach(var type in _frozentypes) + { + result.FrozenTypes(type.Key); + } + } + + return result; + } + + public override ITypeAdapterConfig Fork(Action action, [CallerFilePath] string key1 = "", [CallerLineNumber] int key2 = 0) + { + return base.Fork(action, key1, key2); + } + + public override void Apply(IEnumerable registers) + { + foreach (var item in registers) + { + item.Register(this); + } + } + + public override void Remove(Type sourceType, Type destinationType) + { + if (IsTotalFrozen || + _frozentypes.TryGetValue(new TypeTuple(sourceType, destinationType), out _)) ; + else + base.Remove(sourceType, destinationType); + } + } +} diff --git a/src/Mapster.Fluent/Mapster.Fluent.csproj b/src/Mapster.Fluent/Mapster.Fluent.csproj index f2f9d41..ef01780 100644 --- a/src/Mapster.Fluent/Mapster.Fluent.csproj +++ b/src/Mapster.Fluent/Mapster.Fluent.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/Mapster.Fluent/MapsterOptions.cs b/src/Mapster.Fluent/MapsterOptions.cs index 2e1405e..360b8e5 100644 --- a/src/Mapster.Fluent/MapsterOptions.cs +++ b/src/Mapster.Fluent/MapsterOptions.cs @@ -20,6 +20,6 @@ public class MapsterOptions /// public bool UseServiceMapper { get; set; } = true; - public Action ConfigureAction { get; set; } + public Action ConfigureAction { get; set; } } } diff --git a/src/Mapster.Fluent/ServiceCollectionExtensions.cs b/src/Mapster.Fluent/ServiceCollectionExtensions.cs index 55e23c1..9d7ffb9 100644 --- a/src/Mapster.Fluent/ServiceCollectionExtensions.cs +++ b/src/Mapster.Fluent/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using MapsterMapper; +using Mapster.Fluent.Configs; +using MapsterMapper; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System; @@ -18,7 +19,7 @@ public static class ServiceCollectionExtensions /// The service collection for method chaining. public static IServiceCollection AddMapsterFluent( this IServiceCollection serviceCollection, - Action configure, + Action configure, Action options = null) { if (serviceCollection == null) throw new ArgumentNullException(nameof(serviceCollection)); @@ -27,10 +28,10 @@ public static IServiceCollection AddMapsterFluent( var mapsterOptions = new MapsterOptions(); options?.Invoke(mapsterOptions); - var innerConfig = new TypeAdapterConfig(); - IFluentMapperConfig config = new FluentTypeAdapterConfig(innerConfig); - configure.Invoke(config); - mapsterOptions.ConfigureAction?.Invoke(config); + var innerConfig = new FrozenTypeAdapterConfig(); + + configure.Invoke(innerConfig); + mapsterOptions.ConfigureAction?.Invoke(innerConfig); // Assembly scanning if (mapsterOptions.AssembliesToScan?.Any() == true) @@ -38,7 +39,7 @@ public static IServiceCollection AddMapsterFluent( innerConfig.Scan(mapsterOptions.AssembliesToScan.ToArray()); } - serviceCollection.TryAddSingleton(config.GetInnerConfig()); + serviceCollection.TryAddSingleton(innerConfig); if (mapsterOptions.UseServiceMapper) { serviceCollection.TryAddTransient(); @@ -60,14 +61,14 @@ public static IServiceCollection AddMapsterFluent( /// The service collection for method chaining. public static IServiceCollection AddMapsterWithConfig( this IServiceCollection serviceCollection, - TypeAdapterConfig existingConfig) + ITypeAdapterConfig existingConfig) { if (serviceCollection == null) throw new ArgumentNullException(nameof(serviceCollection)); if (existingConfig == null) throw new ArgumentNullException(nameof(existingConfig)); - IFluentMapperConfig config = new FluentTypeAdapterConfig(existingConfig); + ITypeAdapterConfig config = new FrozenTypeAdapterConfig(existingConfig); - serviceCollection.TryAddSingleton(config.GetInnerConfig()); + serviceCollection.TryAddSingleton(config); serviceCollection.TryAddTransient(); serviceCollection.TryAddSingleton(); diff --git a/tests/Mapster.Fluent.Tests/MapsterDITests.cs b/tests/Mapster.Fluent.Tests/MapsterDITests.cs index b44b3b3..e8755f6 100644 --- a/tests/Mapster.Fluent.Tests/MapsterDITests.cs +++ b/tests/Mapster.Fluent.Tests/MapsterDITests.cs @@ -1,4 +1,5 @@ -using MapsterMapper; +using Mapster.Fluent.Configs; +using MapsterMapper; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -99,14 +100,14 @@ public void AddMapsterFluent_RegistersTypeAdapterConfigAsSingleton() var provider = services.BuildServiceProvider(); // Act - TypeAdapterConfig config1, config2; + ITypeAdapterConfig config1, config2; using (var scope1 = provider.CreateScope()) { - config1 = scope1.ServiceProvider.GetRequiredService(); + config1 = scope1.ServiceProvider.GetRequiredService(); } using (var scope2 = provider.CreateScope()) { - config2 = scope2.ServiceProvider.GetRequiredService(); + config2 = scope2.ServiceProvider.GetRequiredService(); } // Assert @@ -253,7 +254,7 @@ public void ScanMapster_WithValidAssembly_RegistersIRegisterImplementations() var provider = services.BuildServiceProvider(); var mapper = provider.GetRequiredService(); - var config = provider.GetRequiredService(); + var config = provider.GetRequiredService(); // Assert mapper.ShouldNotBeNull(); @@ -338,7 +339,7 @@ public void AddMapsterFluent_WithUseServiceMapperTrue_RegistersServiceMapper() } // ===== ADD MAPSTER WITH CONFIG TESTS ===== - + [Ignore("To prevent modification, access to TypeAdapterConfig should not be granted.")] [TestMethod] public void AddMapsterWithConfig_WithValidConfig_RegistersMapperAndConfig() { @@ -353,7 +354,7 @@ public void AddMapsterWithConfig_WithValidConfig_RegistersMapperAndConfig() services.AddMapsterWithConfig(existingConfig); var provider = services.BuildServiceProvider(); var mapper = provider.GetRequiredService(); - var config = provider.GetRequiredService(); + var config = provider.GetRequiredService(); // Assert mapper.ShouldNotBeNull(); @@ -385,7 +386,7 @@ public void AddMapsterWithConfig_WithNullConfig_ThrowsArgumentNullException() // Act & Assert Should.Throw(() => services.AddMapsterWithConfig(null)); } - + [Ignore("To prevent modification, access to TypeAdapterConfig should not be granted.")] [TestMethod] public void AddMapsterWithConfig_RegistersTypeAdapterConfigAsSingleton() { @@ -396,14 +397,14 @@ public void AddMapsterWithConfig_RegistersTypeAdapterConfigAsSingleton() var provider = services.BuildServiceProvider(); // Act - TypeAdapterConfig config1, config2; + ITypeAdapterConfig config1, config2; using (var scope1 = provider.CreateScope()) { - config1 = scope1.ServiceProvider.GetRequiredService(); + config1 = scope1.ServiceProvider.GetRequiredService(); } using (var scope2 = provider.CreateScope()) { - config2 = scope2.ServiceProvider.GetRequiredService(); + config2 = scope2.ServiceProvider.GetRequiredService(); } // Assert @@ -513,6 +514,59 @@ public void ScanMapster_UsesServiceMapperByDefault() // Assert mapper.ShouldBeOfType(); } + + [TestMethod] + public void FrozenConfigIsWork() + { + // Arrange + var services = new ServiceCollection(); + + // Act + services.AddMapsterFluent( + config => + { + config.NewConfig() + .Map(dest => dest.DisplayName, src => "Frozen DisplayName"); + config.NewConfig() + .Map(dest => dest.FullName, src => "Frozen FullName") + .Ignore(dest => dest.Id); + + config.FrozenTypes(); + config.FrozenTypes(); + + config.NewConfig() + .Map(dest => dest.FullName, src => $"{src.LastName} {src.FirstName}") + .Ignore(dest => dest.Id); + + config.NewConfig() + .Map(dest => dest.DisplayName, src => $"Product: {src.Name}"); + + }, + options => + { + options.AssembliesToScan = [Assembly.GetExecutingAssembly()]; + options.UseServiceMapper = true; + }); + + var provider = services.BuildServiceProvider(); + + // Assert + using var scope = provider.CreateScope(); + var mapper = scope.ServiceProvider.GetRequiredService(); + + // Test scanned mapping (from IRegister) + var user = new TestUser { FirstName = "Integration", LastName = "Test" }; + var userDto = mapper.Map(user); + userDto.FullName.ShouldBe("Frozen FullName"); + + // Test fluent configuration + var product = new TestProduct { Name = "Widget" }; + var productDto = mapper.Map(product); + productDto.DisplayName.ShouldBe("Frozen DisplayName"); + + // Test ServiceMapper type + mapper.ShouldBeOfType(); + } } @@ -545,7 +599,7 @@ public class TestProductDto public class TestUserMappingConfig : IRegister { - public void Register(TypeAdapterConfig config) + public void Register(ITypeAdapterConfig config) { config.NewConfig() .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");