Mapster
Mapster copied to clipboard
Mapster.Tool and DI
Try codegen with DI. Standard NetCore webapi template (7.0)
Program.cs
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
...
builder.Services.AddScoped<SomeService>();
builder.Services.Scan(selector => selector.FromCallingAssembly()
.AddClasses().AsMatchingInterface().WithSingletonLifetime());
...
IApiMapper interface
[Mapper]
public interface IApiMapper
{
Dest MapToExisting(DTO.Source dto, DTO.Dest customer);
}
Mapping register
public class Mapping : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<DTO.Source, DTO.Dest>()
.Map(d => d.String, s => MapContext.Current.GetService<SomeService>().SomeValue())
.Ignore(s => s.Ignore);
}
}
Controller method
public class WeatherForecastController : ControllerBase
{
private readonly IApiMapper mapper;
public WeatherForecastController(IApiMapper mapper)
{
this.mapper = mapper;
}
[HttpGet(Name = "GetWeatherForecast")]
public DTO.Dest Get()
{
var source = new DTO.Source { Name = "test" };
var dest = new DTO.Dest { Ignore = "ignore" };
return mapper.MapToExisting(source, dest);
}
}
And got error:
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Mapping must be called using ServiceAdapter
at Mapster.TypeAdapterExtensions.GetService[TService](MapContext context)
at mapster_codegen.ApiMapper.MapToExisting(Source p1, Dest p2) in O:\Projects\test\mapster-codegen\Mappers\ApiMapper.g.cs:line 12
at mapster_codegen.Controllers.WeatherForecastController.Get() in O:\Projects\test\mapster-codegen\Controllers\WeatherForecastController.cs:line 28
at lambda_method4(Closure, Object, Object[])
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object
controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Generated code:
public partial class ApiMapper : IApiMapper
{
public DTO.Dest MapToExisting(DTO.Source p1, DTO.Dest p2)
{
DTO.Dest result = p2;
result.Name = p1.Name;
result.String = Mapster.MapContext.Current.GetService<SomeService>().SomeValue();
return result;
}
}
If I use runtime mapper (using ServiceMapper) it works good, but if I use codegen mapper (IApiMapper) I get the error.
Mapster version 7.4.0-pre05 Mapster.Tool version 8.4.0-pre05 .Net 7.0
Hi @heggi,
This might be a long shot, but can you try to inject IMapper? Like this: builder.Services.AddTransient<IMapper, Mapper>();
If that doesn't work, could you post a complete code sample please?
I want use generated code for mapping with using service from DI in mapping config.
Runtime mapper (standard Mapper) work good.
More simple example (console app)
Program.cs
using Mapster;
using mapster_test;
using MapsterMapper;
using Microsoft.Extensions.DependencyInjection;
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(TypeAdapterConfig.GlobalSettings);
serviceCollection.AddScoped<IMapper, ServiceMapper>();
serviceCollection.AddScoped<SomeService>();
using var sp = serviceCollection.BuildServiceProvider();
var source = new Source { Name = "test" };
var mapper = sp.GetRequiredService<IMapper>();
var dest1 = mapper.Map<Dest>(source);
Console.WriteLine($"{dest1.Name} {dest1.String}");
Other classes
namespace mapster_test;
public class Mapping : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<Source, Dest>()
.Map(d => d.String, s => MapContext.Current.GetService<SomeService>().SomeValue())
.Ignore(s => s.Ignore);
}
}
public struct Dest
{
public string Name { get; set; }
public string Ignore { get; set; }
public string String { get; set; }
}
public struct Source
{
public string Name { get; set; }
}
public class SomeService
{
public string SomeValue()
{
return "Some string";
}
}
Use codegen (got error). In Program.cs replace some string and got next code:
// See https://aka.ms/new-console-template for more information
using Mapster;
using mapster_test;
using MapsterMapper;
using Microsoft.Extensions.DependencyInjection;
// Mapster config
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(TypeAdapterConfig.GlobalSettings);
serviceCollection.AddScoped<IMapper, ServiceMapper>();
serviceCollection.AddScoped<SomeService>();
// serviceCollection.AddTransient<ITestMapping, TestMapping>(); // uncomment after first build
using var sp = serviceCollection.BuildServiceProvider();
var source = new Source { Name = "test" };
var codeGenMapper = sp.GetRequiredService<ITestMapping>();
var dest2 = codeGenMapper.MapToDest(source);
Console.WriteLine($"{dest1.Name} {dest1.String}");
Add Interface
using Mapster;
namespace mapster_test;
[Mapper]
public interface ITestMapping
{
Dest MapToDest(Source source);
}
Okay, I narrowed it down to MapContext.Current being null. I will investigate some more :)
So I found that MapContext.cs was changed in a commit that fixed issue #266. It seems that AsyncLocal breaks DI with generated code in ASP.NET Core (and possibly other things as well, such as parameters).
Basically the way Mapster works now we have to choose between two evils -- ThreadStatic or AsyncLocal. Currently Mapster will use AsyncLocal in .NET 6 and .NET Standard, and ThreadStatic in older frameworks. I'm guessing that ThreadStatic will probably mostly work in ASP.NET Core 6 workloads, but will also probably break in peculiar ways if used in async contexts.
As a quickfix, I can add a check for a preprocessing directive in the code linked above to force ThreadStatic to be used, which might solve some of these issues temporarily (but could also introduce new issues in different parts of the code).
On a longer-term horizon, the whole MapContext code probably needs to be revamped and either implement AsyncLocal properly to transfer state properly between scopes, or require injection of the MapContext by the end user and let the end user handle state and contexts (which should be trivial with ASP.NET Core DI).
I'm just thinking aloud here, but very eager to hear if there are other ideas on how to solve this.
Tag: @Geestarraw
Hello, using 8.4.0-pre06
and Di when build a project i got
dotnet mapster extension -a "/src/YourJobs.Services/bin/Debug/net6.0/YourJobs.Services.dll"
Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly 'Mapster.DependencyInjection, Version=1.0.1.0, Culture=neutral, PublicKeyToken=e64997d676a9c1d3'. The system cannot find the file specified.
File name: 'Mapster.DependencyInjection, Version=1.0.1.0, Culture=neutral, PublicKeyToken=e64997d676a9c1d3'
at YourJobs.Services.JobOffers.JobOfferConfig.Register(TypeAdapterConfig config)
at Mapster.TypeAdapterConfig.Apply(IEnumerable`1 registers) in C:\Projects\Mapster\src\Mapster\TypeAdapterConfig.cs:line 662
at Mapster.TypeAdapterConfig.Scan(Assembly[] assemblies) in C:\Projects\Mapster\src\Mapster\TypeAdapterConfig.cs:line 651
at Mapster.Tool.Program.GenerateExtensions(ExtensionOptions opt) in C:\Projects\Mapster\src\Mapster.Tool\Program.cs:line 386
at CommandLine.ParserResultExtensions.WithParsed[T](ParserResult`1 result, Action`1 action)
at Mapster.Tool.Program.Main(String[] args) in C:\Projects\Mapster\src\Mapster.Tool\Program.cs:line 18
public void Register(TypeAdapterConfig config)
{
var textNormalizer = MapContext.Current.GetService<ITextNormalizer>();
config.NewConfig<CreateJobOfferRequest, Infrastructure.Entities.Models.JobOffer>()
.Map(x => x.Slug, src => textNormalizer.SlugWithRemapToAscii(src.Vacancy)).MapToConstructor(true)
....
without using MapContext.Current
it build perfectly.