Specifying constructors for inaccessible instances when using Automapper and Autofac

Viewed 243

Scenario

Assume we have a class Target with the following two constructors (one objec, or two objects and an Enum).

public Target(paramOne) { ... }

public Target(paramOne, paramTwo, paramTwoConfigEnum) { ... }

We then have a ClassA which needs to perform a mapping operation from some object to an instance of Target. One version of this would be the following, which relies on existign mapping rules (for automapper), and dependency injection using AutoFac for providing the parameters to perform the mapping. This uses the last of the two constructors above (3 params):

// Behind the scenes this performs a call like: new Target(p1, p2, myEnum)
// with p1, p2 and 
var result = _mapper.Map<List<Target>>(someOtherObject);

Next, we have two other classes ClassB, and ClassC. Both of these need to perform similar mapping operations, but here the resulting objects are classes that contain instances of Target; in other words, there is an behind-the-scenes, implicit mapping from someOtherObject into Target here too:

// Behind the scenes this performs calls conceptually similar to the following 
// (NB: The second line here will call new Target() with 3 params, as above):
//   var x = new ClassContainingTarget(..)
//   x.instOfTarget = _mapper.Map<List<Target>>(someOtherObject);
var result = _mapper.Map<ClassContainingTarget>(anotherSourceObject);

The first challenge

For ClassB, the call operation requires values for all three parameters to Target, and values are provided for these via DI.

For ClassC however, paramTwo and paramTwoConfigEnum are not only not needed; they can't be provided via DI in this context. In other words, I what I would like to happen is for the other constructor in Target to be called in this case.

Attempted solution

I realized I can specify which constructor to use when setting up the rules for AutoFac, and override these in specific cases, so I've experimented with the following general setup in my ContainerBuilder:

 // This specifies that the constructor that takes a single param of type ParamOne
 // should be used by default:
 builder.RegisterType<Target>().AsSelf().UsingConstructor(typeof(ParamOne));

With this setup, all the Map() calls above will result in the second (single-param) constructor of Target being used, including in the case of ClassC, where that is exactly what I want.

For the mapping in ClassA then, I can override this logic by replacing the Map() operation shown above with the following:

// Direct manipulation of the rules for mapping to Target, since I'm
// mapping directly to Target. As mentioned below, this does not appear
// to be possible when mapping to classes that contain Target (i.e. 
// when Target is mapped implicitly). 
result = _mapper.Map<List<Target>>(
               someOtherObject,
               options => 
                   options.ConstructServicesUsing(t => new Target(_p1, _p2, myEnum)));

This actualy works in part: Mapping in ClassA causes the 3-param constructor to be called, while that in ClassC causes the 1-param constructor to be called.

Remaining problem

Now the problem remains with ClassB however: I can't see any way to configure it such that it will call the 3-param constructor for Target, since that instantiation and mapping is defined at a lower level, so to speak.

So my question then: Is there any way for me to specify (either from ClassB, or somewhere else) that when Target is instantiated from ClassB, it should use some specific constructor?

Or alternatively, is there some better strategy to get around this problem?

2 Answers

If you want to resolve Target parameters from DI, you'll have to have them registered in container as well (you probably have this, just double-checking):

builder.RegisterType<ParamOne>().AsSelf().UsingConstructor(() => new ParamOne());
builder.RegisterType<ParamTwo>().AsSelf().UsingConstructor(() => new ParamTwo());
builder.RegisterType<ParamTwoEnum>().AsSelf().UsingConstructor(() => ParamTwoEnum.Default);

Then you can use ConstructUsingServiceLocator() as Lucian suggested and a type converter to which you can inject parameters via DI. Mapping configuration:

CreateMap<ClassA, Target>();
CreateMap<ClassB, Target>()
    .ConvertUsing<ClassBToTargetTypeConverter>();
CreateMap<ClassC, Target>()
    .ConstructUsingServiceLocator();

The ClassBToTargetTypeConverter:

public class ClassBToTargetTypeConverter : ITypeConverter<ClassB, Target>
{
    private readonly ParamOne _paramOne;
    private readonly ParamTwo _paramTwo;
    private readonly ParamTwoEnum _paramTwoConfigParamTwoEnum;

    public ClassBToTargetTypeConverter(ParamOne paramOne, ParamTwo paramTwo, ParamTwoEnum paramTwoConfigParamTwoEnum)
    {
        _paramOne = paramOne;
        _paramTwo = paramTwo;
        _paramTwoConfigParamTwoEnum = paramTwoConfigParamTwoEnum;
    }

    public Target Convert(ClassB source, Target destination, ResolutionContext context)
    {
        return new Target(_paramOne, _paramTwo, _paramTwoConfigParamTwoEnum);
    }
}
Summary:
  • ClassA to Target is mapped normally using source object properties
  • ClassB to Target is mapped using type converter which in turn construct Target using constructor with three parameters which are resolved from container
  • ClassC to Target is mapped directly using DI, where Target is registered as to be constructed using constructor with only one parameter

Side note: using Autofac you have the freedom to switch between using the type converter for ClassC or for ClassB and use DI for the other. But! If you were to use default .NET Core DI engine, you'll have to use the type converter for ClassC to Target mapping as DI is designed to be greedy and chooses the constructor with the most parameters it can fill. What that means is if you let .NET Core DI to construct Target by itself having all three parameters registered in service collection, then it would choose constructor with three parameters over constructor with only one parameter, because it's greedy.

I eventually ended up using a different approach to solve this problem. This explains more or less what I did:

Making the second parameter default to null allows this constructor to be used in all cases. Note that I'm leaving out the third parameter altogether; instead, I'll be relying on Automapper to populate it's property instead (that is precisely what I was not able to do originally, and why I was trying to use specific constructors; the next section of code shows how I managed to set that up in th end).

public Target(ParamOne, ParamTwo = null) { ... }

public MyEnumType ConfigEnum {get; set;}

Now when setting upt the mapping from e.g. ClassB to Target, I specify that it should pick up the value for ConfigEnumProp from a value passed in via context (an item with they key "MyConfigEnum"):

// Map to ConfigEnum in Target NOT from the source, but from a value
// passed in via context by the caller:
CreateMap<ClassB, SectionsDTO>()
   .ForMember(dest => dest.ConfigEnum,
        opt => opt.MapFrom((src, dest, destMember, context) => 
                   context.Items["MyConfigEnum"]))
                 

This allows me to pass the required enum value as one value when mapping from ClassA...

var result = _mapper.Map<List<Target>>(instanceOfClassA,
                 options => options.Items["MyConfigEnum"] = valueWhenMappingFromA);
            

...and as a different value when mapping from ClassB:

var result = _mapper.Map<List<Target>>(instanceOfClassB,
                 options => options.Items["MyConfigEnum"] = someOtherValue);
                 

Finally, in cases where ParamTwo and ConfigEnum are not required, they can simply be left out - the constructor will work fine and the property will retain its default value (as Enums do) and otherwise be ignored.

Related