Skip to content

Conversation

@mokarchi
Copy link

@mokarchi mokarchi commented Oct 2, 2025

Overview

This update significantly enhances the Mapster dependency injection system with a comprehensive configuration API inspired by AutoMapper's AddAutoMapper method, while maintaining full backward compatibility with existing code. This package provides enhanced dependency injection and configuration support for Mapster, making it easier to integrate with ASP.NET Core applications.

Key Improvements

1. Fluent Configuration API

Instead of manually creating and configuring TypeAdapterConfig outside of DI, you can now configure Mapster inline:

// Old way (still works)
var config = new TypeAdapterConfig();
config.NewConfig<Source, Destination>();
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();

// New way
services.AddMapster(config =>
{
    config.NewConfig<Source, Destination>();
});

2. Assembly Scanning

Automatically discover and register mapping configurations:

services.AddMapster(options =>
{
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());
});

Supports both existing patterns:

  • IRegister - Existing pattern for mapping configurations
  • IMapFrom<T> - New pattern allowing DTOs to define their own mappings

3. ServiceMapper by Default

The new API uses ServiceMapper by default, providing full dependency injection support in mappings:

config.NewConfig<User, UserDto>()
    .Map(dest => dest.DisplayName, 
         src => MapContext.Current.GetService<IUserService>().GetDisplayName(src));

4. Precompilation Support

Precompile frequently used mappings at startup for better runtime performance:

services.AddMapster(options =>
{
    options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));
    options.TypePairsToPrecompile.Add((typeof(Product), typeof(ProductDto)));
});

5. Frozen Configuration

Lock configuration for maximum performance in production:

services.AddMapsterFrozen(config =>
{
    config.NewConfig<User, UserDto>();
    config.NewConfig<Product, ProductDto>();
});

6. Module System

Create reusable, testable mapping configurations:

public class UserMappingModule : IMapsterModule
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    }
}

services.AddMapsterModule<UserMappingModule>();

7. Build Hooks

Execute logic before and after compilation:

services.AddMapster(
    config => { /* configure */ },
    actions =>
    {
        actions.OnBeforeCompile = cfg => { /* pre-compilation logic */ };
        actions.OnAfterCompile = cfg => { /* post-compilation logic */ };
    });

8. MapContext Factory

Customize how MapContext scopes are created:

public class CustomMapContextFactory : IMapContextFactory
{
    public IDisposable CreateScope(IServiceProvider serviceProvider)
    {
        // Custom scope creation logic
    }
}

services.AddSingleton<IMapContextFactory, CustomMapContextFactory>();

Quick Start

Basic Usage (Backward Compatible)

services.AddMapster();

With Inline Configuration

services.AddMapster(config =>
{
    config.NewConfig<Source, Destination>()
        .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
});

With Options

services.AddMapster(options =>
{
    // Configure mappings
    options.ConfigureAction = config =>
    {
        config.NewConfig<Source, Destination>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    };

    // Scan assemblies for IRegister and IMapFrom<T> implementations
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());

    // Precompile specific type pairs for better performance
    options.TypePairsToPrecompile.Add((typeof(Source), typeof(Destination)));

    // Use ServiceMapper for DI support (default: true)
    options.UseServiceMapper = true;

    // Freeze configuration after setup (optional)
    options.FreezeConfiguration = false;
});

Frozen Configuration (High Performance)

services.AddMapsterFrozen(config =>
{
    config.NewConfig<Source, Destination>()
        .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
});

With Build Actions

services.AddMapster(
    config =>
    {
        config.NewConfig<Source, Destination>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    },
    actions =>
    {
        actions.OnBeforeCompile = cfg => {
            // Execute logic before compilation
        };
        actions.OnAfterCompile = cfg => {
            // Execute logic after compilation
        };
        actions.PrecompileTypePairs = true;
        actions.Freeze = false;
    });

Using Modules

// Define a module
public class UserMappingModule : IMapsterModule
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    }
}

// Register the module
services.AddMapsterModule<UserMappingModule>();

Assembly Scanning

// Scan current assembly
services.ScanMapster(Assembly.GetExecutingAssembly());

// Or specify multiple assemblies
services.ScanMapster(
    Assembly.GetExecutingAssembly(),
    typeof(SomeType).Assembly
);

Pattern Support

IRegister Pattern (Existing)

public class UserMappingRegister : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    }
}

IMapFrom Pattern (New)

public class UserDto : IMapFrom<User>
{
    public string FullName { get; set; }

    public void ConfigureMapping(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
    }
}

Advanced Scenarios

Custom MapContext Factory

services.AddSingleton<IMapContextFactory, CustomMapContextFactory>();

Combining Multiple Configurations

services.AddMapster(options =>
{
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());
    options.Modules.Add(new UserMappingModule());
    options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));
    options.FreezeConfiguration = true;
    options.ConfigureAction = config =>
    {
        // Additional inline configuration
        config.Default.IgnoreNullValues(true);
    };
});

API Reference

MapsterOptions

  • AssembliesToScan: Assemblies to scan for mapping configurations
  • TypePairsToPrecompile: Type pairs to precompile
  • UseServiceMapper: Use ServiceMapper (true) or basic Mapper (false)
  • FreezeConfiguration: Freeze config after build
  • ConfigureAction: Action to configure TypeAdapterConfig
  • Modules: Modules to register

MapsterBuildActions

  • OnBeforeCompile: Action executed before compilation
  • OnAfterCompile: Action executed after compilation
  • ScanAssemblies: Enable assembly scanning
  • PrecompileTypePairs: Enable type pair precompilation
  • Freeze: Freeze configuration

Extension Methods

  • AddMapster(): Basic registration (backward compatible)
  • AddMapster(Action<MapsterOptions>): Register with options
  • AddMapster(Action<TypeAdapterConfig>, Action<MapsterBuildActions>): Register with config and actions
  • AddMapsterWithConfig(TypeAdapterConfig, Action<MapsterBuildActions>): Use existing config
  • AddMapsterFrozen(Action<TypeAdapterConfig>): Register with frozen config
  • AddMapsterModule<TModule>(): Register a module
  • ScanMapster(params Assembly[]): Scan assemblies

Migration Guide

Minimal Migration (No Changes Required)

Your existing code continues to work without any changes:

services.AddMapster(); // Still works exactly as before

Recommended Migration

To take advantage of new features, update your code to:

services.AddMapster(options =>
{
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());
    options.UseServiceMapper = true; // Default, but explicit
});

Full Feature Migration

For maximum benefits:

services.AddMapster(options =>
{
    // Scan for mappings
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());

    // Precompile hot paths
    options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));

    // Configure inline
    options.ConfigureAction = config =>
    {
        config.Default.IgnoreNullValues(true);
    };

    // Use ServiceMapper for DI
    options.UseServiceMapper = true;

    // Freeze for production
    options.FreezeConfiguration = Environment.IsProduction();
});

Mapster.DependencyInjection - Usage Examples

Example 1: Simple ASP.NET Core Setup

// Program.cs or Startup.cs
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add Mapster with inline configuration
        builder.Services.AddMapster(config =>
        {
            config.NewConfig<User, UserDto>()
                .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
        });

        var app = builder.Build();
        app.MapControllers();
        app.Run();
    }
}

// Controller
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMapper _mapper;

    public UsersController(IMapper mapper)
    {
        _mapper = mapper;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        var userDto = _mapper.Map<UserDto>(user);
        return Ok(userDto);
    }
}

Example 2: Assembly Scanning with IRegister

// Mapping configuration class
public class UserMappingConfig : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}")
            .Map(dest => dest.Age, src => DateTime.Now.Year - src.BirthYear);
    }
}

// DI Setup
builder.Services.AddMapster(options =>
{
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());
});

Example 3: Using IMapFrom Pattern

// DTO with built-in mapping configuration
public class UserDto : IMapFrom<User>
{
    public string FullName { get; set; }
    public int Age { get; set; }

    public void ConfigureMapping(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}")
            .Map(dest => dest.Age, src => DateTime.Now.Year - src.BirthYear);
    }
}

// DI Setup - scanning will automatically find and configure IMapFrom implementations
builder.Services.ScanMapster(Assembly.GetExecutingAssembly());

Example 4: Dependency Injection in Mappings

// Service to be injected
public interface IUserService
{
    string GetUserDisplayName(User user);
}

public class UserService : IUserService
{
    public string GetUserDisplayName(User user)
    {
        return $"{user.Title} {user.FirstName} {user.LastName}";
    }
}

// Mapping configuration
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddMapster(config =>
{
    config.NewConfig<User, UserDto>()
        .Map(dest => dest.DisplayName, 
             src => MapContext.Current.GetService<IUserService>().GetUserDisplayName(src));
});

// Usage in controller
public class UsersController : ControllerBase
{
    private readonly IMapper _mapper;

    public UsersController(IMapper mapper)
    {
        _mapper = mapper;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var user = await _userRepository.GetByIdAsync(id);
        // ServiceMapper automatically injects IServiceProvider into MapContext
        var userDto = _mapper.Map<UserDto>(user);
        return Ok(userDto);
    }
}

Example 5: High-Performance Frozen Configuration

// For production scenarios where configuration won't change
builder.Services.AddMapsterFrozen(config =>
{
    config.NewConfig<User, UserDto>();
    config.NewConfig<Product, ProductDto>();
    config.NewConfig<Order, OrderDto>();
});

Example 6: Precompiling Hot Paths

// Precompile frequently used mappings for better performance
builder.Services.AddMapster(options =>
{
    options.ConfigureAction = config =>
    {
        config.NewConfig<User, UserDto>();
        config.NewConfig<Product, ProductDto>();
    };

    // Precompile these mappings at startup
    options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));
    options.TypePairsToPrecompile.Add((typeof(Product), typeof(ProductDto)));
});

Example 7: Modular Configuration

// Define reusable modules
public class UserMappingModule : IMapsterModule
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<User, UserDto>()
            .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");

        config.NewConfig<UserDto, User>()
            .Map(dest => dest.FirstName, src => src.FullName.Split(' ')[0])
            .Map(dest => dest.LastName, src => src.FullName.Split(' ')[1]);
    }
}

public class ProductMappingModule : IMapsterModule
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<Product, ProductDto>();
        config.NewConfig<ProductDto, Product>();
    }
}

// Register modules
builder.Services.AddMapster(options =>
{
    options.Modules.Add(new UserMappingModule());
    options.Modules.Add(new ProductMappingModule());
});

// Or use generic method
builder.Services.AddMapsterModule<UserMappingModule>();

Example 8: Build Hooks for Logging

builder.Services.AddMapster(
    config =>
    {
        config.NewConfig<User, UserDto>();
    },
    actions =>
    {
        actions.OnBeforeCompile = cfg =>
        {
            _logger.LogInformation("Starting Mapster compilation...");
        };

        actions.OnAfterCompile = cfg =>
        {
            _logger.LogInformation("Mapster compilation completed.");
        };

        actions.PrecompileTypePairs = true;
        actions.Freeze = true;
    });

Example 9: Combining Multiple Configurations

builder.Services.AddMapster(options =>
{
    // Scan for IRegister and IMapFrom
    options.AssembliesToScan.Add(Assembly.GetExecutingAssembly());
    options.AssembliesToScan.Add(typeof(ExternalDto).Assembly);

    // Add modules
    options.Modules.Add(new CoreMappingModule());

    // Precompile hot paths
    options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));
    options.TypePairsToPrecompile.Add((typeof(Product), typeof(ProductDto)));

    // Global configuration
    options.ConfigureAction = config =>
    {
        config.Default.IgnoreNullValues(true);
        config.Default.MapToConstructor(true);
    };

    // Enable ServiceMapper for DI
    options.UseServiceMapper = true;

    // Freeze for production
    options.FreezeConfiguration = true;
});

Example 10: Using Existing Configuration

// Create configuration elsewhere
var config = new TypeAdapterConfig();
config.NewConfig<User, UserDto>();

// Apply additional configuration from modules
var module = new UserMappingModule();
module.Register(config);

// Use the existing configuration
builder.Services.AddMapsterWithConfig(config, actions =>
{
    actions.Freeze = true;
});

Example 11: Conditional Configuration by Environment

builder.Services.AddMapster(options =>
{
    options.ConfigureAction = config =>
    {
        config.NewConfig<User, UserDto>();
    };

    // Only freeze in production
    options.FreezeConfiguration = builder.Environment.IsProduction();

    // Only precompile in production
    if (builder.Environment.IsProduction())
    {
        options.TypePairsToPrecompile.Add((typeof(User), typeof(UserDto)));
    }
});

Example 12: Testing Configuration

[TestClass]
public class MappingTests
{
    [TestMethod]
    public void User_Should_Map_To_UserDto()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddMapster(config =>
        {
            config.NewConfig<User, UserDto>()
                .Map(dest => dest.FullName, src => $"{src.FirstName} {src.LastName}");
        });

        var serviceProvider = services.BuildServiceProvider();
        var mapper = serviceProvider.GetRequiredService<IMapper>();

        var user = new User
        {
            FirstName = "John",
            LastName = "Doe"
        };

        // Act
        var userDto = mapper.Map<UserDto>(user);

        // Assert
        Assert.AreEqual("John Doe", userDto.FullName);
    }
}

Backward Compatibility

  • All existing code continues to work without changes
  • Existing AddMapster() method preserved
  • No breaking changes to public API
  • ServiceMapper can be disabled: options.UseServiceMapper = false

- Added new DI extension methods in `ServiceCollectionExtensions.cs`:
  `AddMapster` with options, `AddMapsterWithConfig`, `AddMapsterFrozen`,
  `AddMapsterModule`, and `ScanMapster` for assembly scanning.
- Introduced `MapsterOptions` for flexible configuration, including
  assembly scanning, precompilation, and module registration.
- Added `IMapContextFactory` and `DefaultMapContextFactory` for managing
  `MapContext` scopes.
- Created `MapsterBuilder` and `MapsterBuildActions` to handle build
  phases, including precompilation and freezing configurations.
- Enhanced `MapsterBuilder` to support `IMapFrom<T>` pattern and
  improved error handling during precompilation.
- Added unit tests in `EnhancedDITests.cs` to validate new DI features.
- Added `StartupEnhanced.cs` to demonstrate advanced Mapster DI usage.
- Introduced `User` and `UserDto` models for testing and examples.
- Preserved backward compatibility with the original `AddMapster` method.
@andrerav
Copy link
Contributor

andrerav commented Oct 3, 2025

This is a somewhat peculiar PR.

A couple of questions;

  • Do you have an issue # that this PR is intended to fix?
  • Did you use LLMs to implement this?

@DocSvartz @stagep What are your thoughts on this?

@stagep
Copy link

stagep commented Oct 4, 2025

@andrerav A few thoughts.
Is this a solution looking for a problem?
Adding fluent configuration increases the burden on providing help with configuration issues.
If you want performance enhancements, then use Mapster.Tool.
If it was implemented then it should be a separate project such as Mapster.Fluent as it is just providing extension methods then it should itself be an extension.
I also find it unusual that so much effort was put into the PR without any discussion.

@mokarchi
Copy link
Author

mokarchi commented Oct 4, 2025

Thanks for the feedback!
I understand your concern about whether this is a "solution looking for a problem." Let me explain why I believe this feature is useful: Based on existing issues like #266 and #519 (which highlight problems with DI and MapContext, such as errors when using MapContext.Current.GetService in DI scenarios or with code generation), this PR reduces the friction of migrating from AutoMapper by providing a more fluent and inline configuration API. It also adds advanced options like precompile (for performance optimization) without needing source generators. If you think it's better as a separate project (e.g., Mapster.Fluent), I'm happy to move it there and maintain it as an extension. However, I believe it belongs in core Mapster because it strengthens the DI integration, making it more robust for ASP.NET Core users without breaking existing setups.

@DocSvartz
Copy link

@andrerav I don't mind adding a fluent configuration api.

@mokarchi Are all these features aimed at simplifying migration from AutoMapper to Mapster?

I didn't quite understand the difference between Frozen and Precompile config. Both of these extensions perform precompilation. According to the description, freezing should prevent the configuration from being changed again.
I may not have noticed this, but I didn't see anything related to it in the code or test sample.

@mokarchi
Copy link
Author

mokarchi commented Oct 4, 2025

@mokarchi Are all these features aimed at simplifying migration from AutoMapper to Mapster?

To answer your question about the goal: Yes, a key aim of this PR is to simplify migration from AutoMapper to Mapster by offering a familiar, fluent API (like inline config and builder patterns). This makes it easier for AutoMapper users to adopt Mapster with minimal code changes. However, it also benefits existing Mapster users by addressing pain points like manual config setup outside DI and adding performance optimizations like precompile and freeze without requiring source generators

@mokarchi
Copy link
Author

mokarchi commented Oct 4, 2025

I didn't quite understand the difference between Frozen and Precompile config. Both of these extensions perform precompilation. According to the description, freezing should prevent the configuration from being changed again.

Regarding your question about Frozen vs. Precompile:

  • Precompile: This compiles delegates for specific type pairs (from the PreCompilePairs list) during the build phase. It pre-warms the cache for those mappings, improving startup and runtime performance by avoiding lazy compilation. It’s optional and targets key mappings in your app.
  • Frozen: This builds on precompile (if pairs are specified) but goes further by making the entire TypeAdapterConfig immutable after build. It locks rules, prevents further mutations (e.g., guards against calls like ForType() post-freeze), and enables a fast-path O(1) lookup at runtime using pre-cached delegates without dynamic resolution. Frozen is ideal for production environments where the config shouldn’t change after startup.

@DocSvartz
Copy link

OK,
But I don't see any of this happening in your PR code. The only freeze function simply compile all config for type available at the time of its call.

@mokarchi
Copy link
Author

mokarchi commented Oct 4, 2025

Given your feedback and the concerns raised by @stagep about the scope of this PR, IIf you and the team think this PR doesn’t fit Mapster’s core at this time, I’m fully supportive of closing it and will use your feedback to refine my approach for future contributions if you feel it’s not the right fit for Mapster’s core at this time. My goal was to address user needs while maintaining Mapster’s simplicity and performance focus, but I respect that this might require more discussion.

@stagep
Copy link

stagep commented Oct 4, 2025

@mokarchi Don't make any changes. We are just discussing ideas. I specifically started my first comment with "a few thoughts". I am not making any judgment of good or bad. I was just throwing a few things out there to discuss and that is what we are doing. @DocSvartz has been the major code contributor to Mapster recently. I generally answer "support" related issues that deal with configuration or issues that do not involve code changes. Personally, I didn't come from AutoMapper, but I know about their licensing changes, so I can understand where you are coming from. I also know that fluent configuration is preferred by some rather than imperative configuration. Let's keep the discussion going @DocSvartz @andrerav

@andrerav
Copy link
Contributor

andrerav commented Oct 4, 2025

I would argue that a change like this should start its life as an extension to Mapster, not as core functionality. One possible compromise is that we create a new repository in the Mapster organization (for example .../Mapster.Fluent) and give @mokarchi write access to that repository. When the extension is proven to be reliable and useful, we can consider including it in the main repository. What do you think @stagep and @DocSvartz?

@stagep
Copy link

stagep commented Oct 4, 2025

@andrerav @DocSvartz I like that idea because it guarantees that the Mapster core is not touched, and that the addition of fluent configuration is 100% an extension.

@DocSvartz
Copy link

@andrerav  I like that idea.

@mokarchi
Copy link
Author

mokarchi commented Oct 5, 2025

I completely agree that starting this as an extension (like in a new repo under the Mapster organization, e.g., Mapster.Fluent) is a great compromise to keep the core untouched while testing its reliability and usefulness. If the team decides to go that route, I'm fully on board and would be thrilled to have write access to maintain and iterate on it. I'm excited to support it and contribute in a way that aligns with Mapster's goals. Let me know how I can help next! @DocSvartz @andrerav

@DocSvartz
Copy link

@andrerav @stagep do need to Freeze configuration when using outside of DI cases?

@andrerav
Copy link
Contributor

andrerav commented Oct 6, 2025

@DocSvartz @stagep @mokarchi I think the config freeze option should either be anchored in a security concern (which I can't really surmise from the current proposal), or a benchmark that shows a clear and tangible performance advantage.

@DocSvartz
Copy link

DocSvartz commented Oct 6, 2025

@andrerav My expectation of freezing mostly coincides with the description @mokarchi gave above.
The main problem is that TypeAdapterConfig only guarantees caching. There is no blocking of subsequent changes. And besides, subsequent changes will lead to configuration failures (wiki description).

I see two solutions:

  1. Introduce in TypeAdapterConfig freezing blocking feature.
  2. If it turns out to highlight the ITypeAdapterConfig interface and then @mokarchi will be able to do it in its implementation (or just through the decorator). But it will require even more changes.

@andrerav @stagep @mokarchi Perhaps you see another solution?

Upd: For now, can inherit from TypeAdapterConfig.
I just thought it was sealed :)

@mokarchi
Copy link
Author

mokarchi commented Oct 6, 2025

Regarding @DocSvartz’s question about whether Freeze is needed outside DI cases: I believe Freeze can be valuable in both DI (ServiceMapper) and non-DI (Mapper) scenarios. In non-DI cases, Freeze ensures that the TypeAdapterConfig remains immutable after setup, preventing accidental modifications during runtime that could lead to configuration errors (as noted in the wiki). This is particularly useful in long-running apps where config stability is critical. For DI scenarios, it also enables a fast-path O(1) lookup by caching compiled delegates, which aligns with Mapster’s performance focus.

@andrerav, I completely agree that Freeze needs a clear justification, either through a security concern or a tangible performance benefit. From a security perspective, making TypeAdapterConfig immutable prevents unintended runtime changes that could introduce bugs or inconsistencies, especially in production environments with multiple threads or services accessing the config. For performance, Freeze enables O(1) lookup by pre-caching compiled delegates and skipping dynamic resolution, which can be a bottleneck in high-throughput scenarios.

@DocSvartz, I really appreciate your proposed solutions for implementing Freeze. I agree that the lack of immutability in TypeAdapterConfig can lead to configuration failures, as you mentioned. Here’s my take on your suggestions:

Adding a freezing/blocking feature directly to TypeAdapterConfig: This seems like the most straightforward approach. We could add an IsFrozen flag and guard mutating methods (e.g., ForType(), NewConfig()) to throw an InvalidOperationException if called post-freeze. This keeps the core simple and aligns with Mapster’s philosophy.
Introducing an ITypeAdapterConfig interface with a decorator: This is an elegant solution for extensibility, allowing custom implementations like mine to handle freezing without modifying the core. Since you noted TypeAdapterConfig isn’t sealed, inheriting from it could work for now, which is simpler than introducing a new interface. I’d lean toward this approach for a Mapster.Fluent extension to keep the core untouched, as @stagep and @andrerav suggested.
I can prototype the inheritance-based approach (e.g., a FrozenTypeAdapterConfig class) in a separate branch or repo and share it for feedback. Which approach do you prefer, or is there another solution you’d like me to explore?


public class FrozenTypeAdapterConfig : TypeAdapterConfig
{
    public bool IsFrozen { get; private set; }

    public void Freeze()
    {
        // Compile all rules for known types
        foreach (var rule in Rules) // Assuming Rules is accessible
        {
            var mapFunc = GetMapFunction(rule.SourceType, rule.DestinationType);
            CompiledDelegates[new TypeTuple(rule.SourceType, rule.DestinationType)] = mapFunc.Compile();
        }
        IsFrozen = true; // Mark as frozen
    }

    public override IMapperConfig ForType<TSource, TDestination>()
    {
        if (IsFrozen)
            throw new InvalidOperationException("Cannot modify configuration after freeze.");
        return base.ForType<TSource, TDestination>();
    }
}

@DocSvartz
Copy link

@mokarchi Create a prototype based on inheritance.
In any case, this is the fastest way to implement it right now.

@mokarchi
Copy link
Author

mokarchi commented Oct 7, 2025

@mokarchi Create a prototype based on inheritance. In any case, this is the fastest way to implement it right now.

Before I proceed, could you clarify where you’d like me to create this prototype?

@andrerav
Copy link
Contributor

andrerav commented Oct 7, 2025

@mokarchi I've created a new repository here: https://github.com/MapsterMapper/Mapster.Fluent

Please fork it, create your changes, and submit a PR. And we can continue the technical discussion on that PR.

@DocSvartz
Copy link

@mokarchi The inheritance based approach won't work now either.

@andrerav
Copy link
Contributor

andrerav commented Oct 8, 2025

@DocSvartz How so? I think it should work fine to add a package reference to Mapster and inherit from there?

@mokarchi
Copy link
Author

mokarchi commented Oct 8, 2025

@DocSvartz, You're right the inheritance-based approach doesn't work as expected because the methods in TypeAdapterConfig aren't virtual, making it impossible to override them for immutability. I've been working on an alternative solution (using a decorator pattern with a custom interface to handle the guards and freeze logic), and I'll submit a PR to Mapster.Fluent soon to address this.

@DocSvartz
Copy link

@andrerav will need to make the methods virtual.

@andrerav
Copy link
Contributor

andrerav commented Oct 8, 2025

@DocSvartz Oh, yes of course 🤦 But I can see in his fork that he has apparently found a way around it.

@DocSvartz
Copy link

@mokarchi Your approach will only work with inline config. When uploading assemblies, you will still pass the standard config, which not blocking.

@mokarchi
Copy link
Author

mokarchi commented Oct 8, 2025

@DocSvartz, I completely agree that the current approach only works for inline config and doesn't handle scanned assemblies properly, as it passes the standard TypeAdapterConfig without blocking mutations. You're right. this limits the immutability to inline setups. I'm working on extending the wrapper to cover assemblies as well (e.g., by applying the decorator after scanning), and I'll submit a PR to Mapster.Fluent soon to address this. If you have any specific suggestions on how to handle this better, I'd love to hear them

@DocSvartz
Copy link

@mokarchi I don't have any good thoughts for the current situation yet.

The only thing is, if the configuration is already frozen at the time of scanning, then there is no need to scan.

But you had a different order, first scan, then checking for freezing.

@stagep
Copy link

stagep commented Oct 8, 2025

@andrerav @DocSvartz @mokarchi Just my two cents, but shouldn't the focus first be on the extension methods that provide fluent configuration before we get into the nitty-gritty of specifics of Freeze etc. Let's deliver what can bring immediate value and then we can discuss more specific edge cases.

@DocSvartz
Copy link

@stagep I focused on this feature because it clearly didn't work the way the @mokarchi intended. At the same time, judging by the current description in the wiki, it may lead to configuration broken.

@mokarchi don't have to include everything in the first version.

@mokarchi
Copy link
Author

mokarchi commented Oct 8, 2025

@stagep, I agree that focusing on delivering immediate value with fluent extension methods makes sense, and we can tackle edge cases like Freeze later.

Could you please clarify what specific features you'd like to see prioritized in the first version of Mapster.Fluent?

@DocSvartz
Copy link

@stagep I vote roughly for the following distribution:

  1. 1,2,3
  2. 6,8
  3. 4,5,7

@stagep
Copy link

stagep commented Oct 8, 2025

@stagep I vote roughly for the following distribution:

  1. 1,2,3
  2. 6,8
  3. 4,5,7

100% agree

@andrerav
Copy link
Contributor

andrerav commented Oct 8, 2025

Sounds good to me as well :)

@mokarchi
Copy link
Author

mokarchi commented Oct 8, 2025

Please review and share feedback. PR
@DocSvartz @stagep @andrerav

@stagep
Copy link

stagep commented Oct 10, 2025

Updated PR became MapsterMapper/Mapster.Fluent#1

@stagep stagep closed this Oct 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants