Mapster brings some new ideas to object mapping world. Model generation, async custom actions after map, dependency injection. In this post, I will try to explore these new ideas. Source code can be found on GitHub.

Getting started

In your solution folder run the following commands to enable dotnet tools and install mapster code generator.

dotnet new tool-manifest

Enabling tool manifest in the solution directory will create a local configuration file, which will help other team members to install the same tools for the project.

dotnet tool install Mapster.Tool

Mapster provides several ways to generate mapping files, for this example, let's generate automatically after build.

    <ItemGroup>
        <Generated Include="**\*.g.cs" />
    </ItemGroup>
    <Target Name="CleanGenerated">
        <Delete Files="@(Generated)" />
    </Target>

<Target Name="Mapster" AfterTargets="AfterBuild">
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
</Target>

One thing to keep in mind, code generation is done after build. If you change mapster configs and start the project, nothing will change. Correct flow is build project and then start it.

    public class MapsterConfig : ICodeGenerationRegister
    {
        public void Register(CodeGenerationConfig config)
        {
            config.AdaptTo("[name]Dto")
                .ForType<WeatherForecast>();
        }
    }

Advanced model generation

public class MapsterConfig : ICodeGenerationRegister
{
    public void Register(CodeGenerationConfig config)
    {
        config.AdaptTo("[name]Dto")
            .ForType<WeatherForecast>(
            cfg =>
            {
                cfg.Ignore(poco => poco.TemperatureC);
                cfg.Map(poco => poco.Summary, "MappedSummary");
            });
    }
}

The model can be modified by ignoring, renaming, changing the type of the property. When you change the property name, you MUST map that property manually.

TypeAdapterConfig<WeatherForecast, WeatherForecastDto>
    .NewConfig()
    .Map(dest => dest.MappedSummary, src => src.Summary);

Type safety

RequireExplicitMapping will make sure that all mappings should be registered. RequireDestinationMemberSource checks if all destination properties have mappings. .Compile() will perform all checks at compile time and throw exceptions if there are any issues.

TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = true;
TypeAdapterConfig.GlobalSettings.Compile();

After map actions

AfterMapping extension allows you to execute code after mapping is done and do final modifications to result if needed.

TypeAdapterConfig<WeatherForecast, WeatherForecastDto>
    .NewConfig()
    .Map(dest => dest.MappedSummary, src => src.Summary)
    .AfterMapping((dest, d) => { d.MappedSummary = "AfterMapping " + d.MappedSummary; });

Dependency Injection and async after map actions

For dependency injection to work we need to change how mapper is used. Configuration and mapping must be switched from static instances to services. First, we must configure required services by adding singleton for all mapping configurations and scoped mapping service.

public void ConfigureServices(IServiceCollection services)
{
    //configure mapster
    var config = new TypeAdapterConfig();
    services.AddSingleton(config);
    services.AddScoped<IMapper, ServiceMapper>();

    TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
    TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = true;
    TypeAdapterConfig.GlobalSettings.Compile();
    //-------
}

For testing purposes, I created three services all of them return time when they were created.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TypeAdapterConfig config)
{
    //Mapping configurations
    config.NewConfig<WeatherForecast, WeatherForecastDto>()
            .Map(dest => dest.MappedSummary, src => src.Summary)
            .AfterMappingAsync(async (dest) =>
            {
                Thread.Sleep(1000);
                var transient = await MapContext.Current.GetService<TransientService>().DateAsync();
                var scoped = await MapContext.Current.GetService<ScopedService>().DateAsync();
                var singleton = await MapContext.Current.GetService<SingletonService>().DateAsync();

                dest.MappedSummary = $"{transient}, {scoped}, {singleton}";
            });
    //--------

Mapping function itself also change to async version.

private readonly IMapper _mapper;

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

[HttpGet]
public async Task<IEnumerable<WeatherForecastDto>> Get()
{
    var result = new List<WeatherForecastDto>();

    foreach (var forecast in Forecasts())
    {
        result.Add(await _mapper.From(forecast)
            .AdaptToTypeAsync<WeatherForecastDto>());
    }

    return result;
}

Now that we have all the required services, we can do a simple test. Execute endpoint twice and compare dates to see if service instances were created as expected.


First-run results.


Second run results.

Transient service results change for each temperature value, scoped service time is the same for request and singleton always shows the same date. From this simple test we see that DI and service scope in mapping works as we expect.

Summary

Mapster brings really great additions to otherwise mundane mapping. Code generation, after map actions, dependency injection are things that more and more often come to mind while working on a large legacy project, and from the initial look, Mapster handles these cases pretty well.

There are still some questions unanswered: how does it handle complex objects? testing I saw in github that some people had issues with concurrency. Code maintainability, since part of logic, would be moved to mapping.

I'm looking forward to using this library in my next project.