MapsterMapper / Mapster

A fast, fun and stimulating object to object Mapper
MIT License
4.3k stars 328 forks source link

Mapster.Tool and DI #519

Open heggi opened 1 year ago

heggi commented 1 year ago

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

andrerav commented 1 year ago

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?

heggi commented 1 year ago

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);
}
andrerav commented 1 year ago

Okay, I narrowed it down to MapContext.Current being null. I will investigate some more :)

andrerav commented 1 year ago

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).

andrerav commented 1 year ago

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

wondertalik commented 1 year ago

Hello, using 8.4.0-pre06and 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.Currentit build perfectly.