Mapster: generate dto, async after map actions, dependency injection
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 "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a "$(TargetDir)$(ProjectName).dll"" />
</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.
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.