Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac and Automapper

I'm confused about the extent to which Autofac can be used with Automapper to map objects. I've read quite a bit of material online about integrating the two packages, but almost all seem to focus on how to create instances of IMapper from Automapper Profiles, using code something like this, which defines an Autofac Module (CSContainer.Instance is a static instance of Autofac's IContainer):

public class AutoMapperModule : Module
{
    private static object ServiceConstructor( Type type )
    {
        return CSContainer.Instance.Resolve( type );
    }

    protected override void Load( ContainerBuilder builder )
    {
        base.Load( builder );

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        builder.RegisterAssemblyTypes( assemblies )
            .Where( t => typeof(Profile).IsAssignableFrom( t ) && !t.IsAbstract && t.IsPublic )
            .As<Profile>();

        builder.Register( c => new MapperConfiguration( cfg =>
            {
                cfg.ConstructServicesUsing( ServiceConstructor );

                foreach( var profile in c.Resolve<IEnumerable<Profile>>() )
                {
                    cfg.AddProfile( profile );
                }
            } ) )
            .AsSelf()
            .AutoActivate()
            .SingleInstance();

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

(see http://www.protomatter.co.uk/blog/development/2017/02/modular-automapper-registrations-with-autofac/ for an explanation of this approach)

What I'd like to do is have Automapper call Autofac to create objects. Right now the only way I can see to do this is by doing something like this:

CreateMap() .ConstructUsing( src => CSContainer.Instance.Resolve() );

This works, but feels kludgy. It'd be neater if Automapper tried to discover how to resolve instances using Autofac automagically, behind the scenes as it were.

I thought something like this might do the trick (this is a modified version of that first Autofac Module I cited above):

public class AutoMapperModule : Module
{
    protected override void Load( ContainerBuilder builder )
    {
        base.Load( builder );

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        builder.RegisterAssemblyTypes( assemblies )
            .Where( t => typeof(Profile).IsAssignableFrom( t ) && !t.IsAbstract && t.IsPublic )
            .As<Profile>();

        builder.Register( c => new MapperConfiguration( cfg =>
            {
                cfg.ForAllMaps( ( map, opts ) =>
                    opts.ConstructUsing( ( x, y ) => CSContainer.Instance.Resolve(map.DestinationType) ) );

                foreach( var profile in c.Resolve<IEnumerable<Profile>>() )
                {
                    cfg.AddProfile( profile );
                }
            } ) )
            .AsSelf()
            .AutoActivate()
            .SingleInstance();

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

but this resulted in Autofac throwing an exception, complaining about me trying to re-use a builder that had already been defined (apologies, I don't have the exact wording handy).

Is it possible to integrate Automapper and Autofac so that Automapper resolves new instances via Autofac? If so, what's the best way to do it?

Additional Info

So I implemented the suggested answer as follows:

    protected override void Load( ContainerBuilder builder )
    {
        base.Load( builder );

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        builder.RegisterAssemblyTypes( assemblies )
            .Where( t => typeof(Profile).IsAssignableFrom( t ) && !t.IsAbstract && t.IsPublic )
            .As<Profile>();

        builder.Register( c => new MapperConfiguration( cfg =>
            {
                cfg.ConstructServicesUsing( ServiceConstructor );

                foreach( var profile in c.Resolve<IEnumerable<Profile>>() )
                {
                    cfg.AddProfile( profile );
                }
            } ) )
            .AsSelf()
            .AutoActivate()
            .SingleInstance();

        builder.Register( c =>
            {
                // these are the changed lines
                var scope = c.Resolve<ILifetimeScope>();
                return new Mapper(c.Resolve<MapperConfiguration>(), scope.Resolve );
            } )
            .As<IMapper>()
            .SingleInstance();
    }

But this leads to an Automapper exception, complaining about the object I'm trying to create via a call to Map() must have zero arguments, or only optional arguments. Yet all the object's constructor arguments are also registered with Autofac, and it has no problem creating instances of the objects by itself elsewhere in my code. It's just when Automapper tries to create an instance that things go haywire.

like image 554
Mark Olbert Avatar asked Jan 03 '23 10:01

Mark Olbert


1 Answers

From the docs and the source it looks like you can pass a function to the Mapper constructor that will run all service location through the function.

public Mapper(IConfigurationProvider configurationProvider, Func<Type, object> serviceCtor)

This blog article has more detailed examples and explains how to get ASP.NET Core DI wired up with AutoMapper. Don't get hung up on it being ASP.NET Core - you'll follow the same principle for Autofac.

builder.Register(
  ctx =>
  {
    var scope = ctx.Resolve<ILifetimeScope>();
    return new Mapper(
      ctx.Resolve<IConfigurationProvider>(),
      scope.Resolve);
  })
  .As<IMapper>()
  .InstancePerLifetimeScope();

Your job will just be to figure out how to register your configuration as IConfigurationProvider.

like image 185
Travis Illig Avatar answered Jan 04 '23 23:01

Travis Illig