简体   繁体   中英

Dependency injection in AutoMapper for custom classes

I am having a Entity and Dto and trying to custom map those classes to have EmpUrl=baseurl+EmpPath.I am planning to get baseurl from configuration file and mapping via DI but getting below error:

System.MissingMethodException: Cannot dynamically create an instance of type 'MappingProfile'. Reason: No parameterless constructor defined.

 public MyMappingProfile(IOptions <ApplicationSettings> applicationsettings)
        {
            CreateMap<Emp, EmpDto>()

            .ForMember(e => e.EmpUrl, e => e.MapFrom(src => applicationsettings.Value.BaseDomain + src.EmpPath));
             
        }

The class to register dependency is below:

public class MyModule : Autofac.Module
    {
        private readonly IConfiguration Configuration;

        public ApplicationModule(IConfiguration configuration)
        {
            Configuration = configuration;
            
        }
        protected override void Load(ContainerBuilder builder)
        {

            // Register Automapper profiles
            var config = new MapperConfiguration(cfg => { cfg.AddMaps(typeof(MappingProfile).Assembly); });


config.AssertConfigurationIsValid();

        builder.Register(c => config)
            .AsSelf()
            .SingleInstance();

        builder.Register(c => c.Resolve<MapperConfiguration>().CreateMapper(c.Resolve))
            .As<IMapper>()
            .InstancePerLifetimeScope();

    
            var settings = new ApplicationSettings();
            Configuration.Bind(settings);
            builder.RegisterInstance(Options.Create(settings));

        }
      
    }

Startup.cs:

public void ConfigureContainer(ContainerBuilder container)
        {
            container.RegisterModule(new ApplicationModule(Configuration));                
           
        }

What am I doing wrong here?

Assuming that MyMappingProfile inherits from the AutoMapper.Profile class, there are a couple issues here:

  1. The cfg.AddMaps method is called immediately during construction of the MapperConfiguration instance (per AutoMapper v11.0.1 source ). This is during application startup loading of the ApplicationModule (not during lazy resolution of dependencies from the container at runtime). The AddMaps call, which creates a new instance of MyMappingProfile , is executed even before the ApplicationSettings are bound from configuration and added to the DI container.
  2. Likely related to #1, the cfg.AddMaps method call uses .NET System.Activator.CreateInstance directly to instantiate the mapping profile (eg MyMappingProfile ) and does not pass any arguments, which is why you're getting the "No parameterless constructor defined" error. This flow does not use the serviceCtor argument passed to the MapperConfiguration.CreateMapper method. (AutoMapper MapperConfigurationExpression.AddMaps calls AddMapsCore , which calls AddProfile(Type) per v11.0.1 source )

One approach to resolve IOptions<ApplicationSettings> at runtime from the DI container using AutoMapper is by using the service factory method ( serviceCtor ) passed to MapperConfiguration.CreateMapper .

The mapping profile could be something like:

public class MyMappingProfile : Profile
{
    public MyMappingProfile()
    {
        CreateMap<Emp, EmpDto>()
            .ForMember(dest => dest.EmpUrl, e => e.MapFrom((src, dest, destMember, ctx) =>
            {
                if (string.IsNullOrWhiteSpace(src.EmpPath))
                {
                    return null;
                }

                var applicationSettings = (IOptions<ApplicationSettings>)ctx.Options.ServiceCtor(typeof(IOptions<ApplicationSettings>));
                return applicationSettings.Value.BaseDomain + src.EmpPath;
            }));
    }
}

To keep the mapping clean or reuse the logic, you could extract the inline MapFrom logic into a local method in the mapping profile class, a separate custom Value Resolver (or Member Value Resolver) used with opt.MapFrom , or a separate custom Value Converter used with opt.ConvertUsing . A custom converter or resolver can use dependencies injected via the constructor rather than using the ResolutionContext explicitly.

If you try only the mapping profile changes, then Autofac will throw an ObjectDisposedException with a message like This resolve operation has already ended. When registering components using lambdas, the IComponentContext 'c' parameter to the lambda cannot be stored. Instead, either resolve IComponentContext again from 'c', or resolve a Func<> based factory to create subsequent components from. This resolve operation has already ended. When registering components using lambdas, the IComponentContext 'c' parameter to the lambda cannot be stored. Instead, either resolve IComponentContext again from 'c', or resolve a Func<> based factory to create subsequent components from. from this line:

var applicationSettings = (IOptions<ApplicationSettings>)ctx.Options.ServiceCtor(typeof(IOptions<ApplicationSettings>));

The solution is to change your ApplicationModule.Load from

builder.Register(c => c.Resolve<MapperConfiguration>()
    .CreateMapper(c.Resolve))
    .As<IMapper>()
    .InstancePerLifetimeScope();

to

builder.Register(c =>
    {
        var ctx = c.Resolve<IComponentContext>();
        var mapperConfig = c.Resolve<MapperConfiguration>();
        return mapperConfig.CreateMapper(ctx.Resolve);
    })
    .As<IMapper>()
    .InstancePerLifetimeScope();

This is based on personal experience and is also covered by other SO questions such as This resolve operation has already ended. Autofac, Automapper & IMemberValueResolver . Dennis Doomen gives a good explanation of this Autofac design nuance in The curious case of a deadlock in Autofac .

So to summarize, use the temporary container for resolving dependencies needed at construction time, but use the global container to resolve any run-time dependencies.

Another software design alternative is to not resolve the ApplicationSettings from the DI container inside the AutoMapper profile at all. Instead, the class that calls IMapper.Map could inject the IOptions<ApplicationSettings> via constructor injection and then either transform the EmpDto.EmpUrl in place or add a separate property like EmpDto.EmpFullUrl that adds the base domain prefix to an EmpDto.EmpPath property mapped verbatim from the Emp.EmpPath via AutoMapper.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM