3
votes

My first time working with Autofac to inject AutoMapper's IMapper interface into classes that have an object mapping requirement. I have made some progress, with a little help, getting the various dependencies added to AutoMapper's register using Assembly Scanning:

builder.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies())
    .AsClosedTypesOf(typeof(ITypeConverter<,>))
    .AsImplementedInterfaces();
builder.RegisterAssemblyTypes(typeof(AutoMapperExtensions).Assembly)
    .AssignableTo<Profile>().As<Profile>();

builder.Register(context => {
    var profiles = context.Resolve<IEnumerable<Profile>>();
    return new MapperConfiguration(x => {
        foreach (var profile in profiles) x.AddProfile(profile);
    });
}).SingleInstance().AutoActivate().AsSelf();

builder.Register(context => {
    var componentContext = context.Resolve<IComponentContext>();
    var config = componentContext.Resolve<MapperConfiguration>();
    return config.CreateMapper();
}).As<IMapper>();

This works perfectly for an ITypeConverter<,> that doesn't have any injected dependencies:

public class SourceToDestinationTypeConverter : ITypeConverter<SourceModel, DestinationModel> {
    public DestinationModel Convert(SourceModel source, DestinationModel destination, ResolutionContext context) {
        if (source.Items == null) {
            return null;
        }

        return new DestinationModel {
            FirstItem = source.Items.FirstOrDefault(),
            LastItem = source.Items.LastOrDefault()
        };
    }
}

However from the moment I add a dependency, in this contrived example, a validator:

public class SourceToDestinationTypeConverter : ITypeConverter<SourceModel, DestinationModel> {
    private readonly IValidator<SourceModel> _validator;

    public SourceToDestinationTypeConverter(IValidator<SourceModel> validator) {
        _validator = validator;
    }

    public DestinationModel Convert(SourceModel source, DestinationModel destination, ResolutionContext context) {
        if (!_validator.Validate(source)) return null;

        return new DestinationModel {
            FirstItem = source.Items.FirstOrDefault(),
            LastItem = source.Items.LastOrDefault()
        };
    }
}

The following exception is thrown:

Application.TypeConverters.SourceToDestinationTypeConverter needs to have a constructor with 0 args or only optional args

It seems clear to me that AutoMapper needs to be told to use Autofac to fulfil the dependencies. However, I haven't been able to find out how to tell it to do so.

The full solution is available on GitHub if further clarification of the error is required.

2

2 Answers

3
votes

NOTE: Travis Illig has provided a hollistic answer to the question which I am marking as the answer as it answers the question in a broad and generic way. However, I also wanted to document the specific solution to my question.

You need to be fairly careful of how you wire up the dependency resolver to AutoMapper, to be precise you must resolve the component context within the closure - failing to do so will result in the context being disposed before AutoMapper ever gets a chance to resolve it's dependencies.

Solution #1

In my example, the following code block that registers the IMapper using the previously defined MapperConfiguration:

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

Can be trivially adapted by using an overload of MapperConfiguration.CreateMapper() that accepts a Func<Type, object> as an argument named serviceCtor that AutoMapper will use to construct dependencies:

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

It's essential that the Component Context context is used as it is declared within the closure, attempting to use c will result in the following exception:

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.

Solution #2

Using a very similar technique to Solution #1 it is possible to use the IMapperConfiguration.ConstructServiceUsing(Func<Type, object>) which provides for more readable code. The original code:

builder.Register(c => {
    var profiles = c.Resolve<IEnumerable<Profile>>();
    return new MapperConfiguration(x => {
        foreach (var profile in profiles) x.AddProfile(profile);           
    });
}).SingleInstance().AsSelf();

And the updated code with the call to x.ConstructServiceUsing(constructor):

builder.Register(c => {
    var profiles = c.Resolve<IEnumerable<Profile>>();
    var context = c.Resolve<IComponentContext>();
    return new MapperConfiguration(x => {
        foreach (var profile in profiles) x.AddProfile(profile);
        x.ConstructServicesUsing(context.Resolve);                   
    });
}).SingleInstance().AsSelf();

Again if you fail to create an instance of IComponentContext within the closure / lambda the context will have been disposed before the Mapper creates the dependencies.

2
votes

I'm guessing you forgot to add the call to ConstructServicesUsing during AutoMapper configuration. Make sure to do this

Exactly how to integrate Autofac with your app really depends on what kind of app you have (Windows Service? MVC? Web API? Windows Forms? UAP?) and what your expectations around lifetime scope usage are. None of that was included in your question. However, if you search the web for "autofac constructservicesusing" you come up with plenty of examples including several other StackOverflow questions on the same topic.

Here's a simple example that shows pretty much exactly what you're doing. If you're using an MVC or Web API app and need per-request-scope support, I have a full blog walkthrough on that.

By way of a note, I'm not sure if the AutoActivate call is really necessary. SingleInstance is thread-safe and it doesn't actually take that long to build an AutoMapper profile lazily. You may want to try without it, especially if AutoMapper itself isn't executing that part until after AutoActivate has run.