使用 Automapper 和 Autofac 时为不可访问的实例指定构造函数

Specifying constructors for inaccessible instances when using Automapper and Autofac

场景

假设我们有一个 class Target 具有以下两个构造函数(一个对象,或两个对象和一个枚举)。

public Target(paramOne) { ... }

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

然后我们有一个 ClassA 需要执行从某个对象到 Target 的实例的映射操作。下面是一个版本,它依赖于现有的映射规则(对于自动映射器),以及使用 AutoFac 的依赖注入来提供执行映射的参数。这使用了上面两个构造函数中的最后一个(3 个参数):

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

接下来,我们还有另外两个 classes ClassBClassC。这两者都需要执行类似的映射操作,但这里生成的对象是 classes that contain instances of Target;换句话说,这里也有一个从 someOtherObjectTarget 的幕后隐式映射:

// 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);

第一次挑战

对于ClassB,调用操作需要Target的所有三个参数的值,并且通过DI为这些参数提供值。

然而,对于ClassCparamTwoparamTwoConfigEnum不仅不需要;在这种情况下,它们不能通过 DI 提供。换句话说,我希望在这种情况下调用 Target 中的另一个构造函数。

尝试的解决方案

我意识到我可以在设置 AutoFac 规则时指定使用哪个构造函数,并在特定情况下覆盖它们,所以我在 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));

使用此设置,上述所有 Map() 调用将导致使用 Target 的第二个(单参数)构造函数,包括 ClassC 的情况,其中这正是我想要的。

对于 ClassA 中的映射,我可以通过将上面显示的 Map() 操作替换为以下内容来覆盖此逻辑:

// 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)));

这实际上部分起作用:ClassA 中的映射导致调用 3 参数构造函数,而 ClassC 中的映射导致调用 1 参数构造函数。

剩余问题

现在问题仍然存在于 ClassB 但是:我看不到任何配置它的方法,以便它调用 Target 的 3 参数构造函数,因为实例化和映射是可以这么说,在较低级别定义。

那么我的问题是:我有什么方法可以指定(从 ClassB 或其他地方)当 TargetClassB 实例化时,它应该使用一些特定的构造函数?

或者,是否有更好的策略来解决这个问题?

如果你想解析来自 DI 的 Target 参数,你还必须在容器中注册它们(你可能有这个,只需仔细检查):

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

然后你可以使用 Lucian 建议的 ConstructUsingServiceLocator() 和一个你可以通过 DI 注入参数的类型转换器。映射配置:

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

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);
    }
}
概括:
  • ClassATarget 使用源对象属性正常映射
  • ClassBTarget 是使用类型转换器映射的,类型转换器又使用从容器
  • 解析的三个参数的构造函数构造 Target
  • ClassCTarget 直接使用 DI 映射,其中 Target 被注册为使用只有一个参数的构造函数构造

旁注:使用 Autofac,您可以自由地在 ClassCClassB 类型转换器之间切换,另一个使用 DI。但!如果您要使用默认的 .NET Core DI 引擎,则必须使用类型转换器进行 ClassCTarget 的映射,因为 DI 被设计为贪婪的并选择具有最多参数的构造函数可以填。这意味着如果你让 .NET Core DI 自己构造 Target 并在服务集合中注册所有三个参数,那么它会选择具有三个参数的构造函数而不是只有一个参数的构造函数,因为它是贪婪的。

我最终使用不同的方法解决了这个问题。这或多或少地解释了我所做的事情:

使第二个参数默认为 null 允许在所有情况下使用此构造函数。请注意,我完全省略了第三个参数;相反,我将依靠 Automapper 来填充它的 属性 (这正是我最初无法做到的,也是我尝试使用特定构造函数的原因;下一部分代码展示了我是如何管理的在最后设置它)。

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

public MyEnumType ConfigEnum {get; set;}

现在设置映射时,例如ClassBTarget,我指定它应该从通过 context 传入的值中获取 ConfigEnumProp 的值(带有键 "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"]))
                 

这允许我在从 ClassA...

映射时将所需的枚举值作为一个值传递
var result = _mapper.Map<List<Target>>(instanceOfClassA,
                 options => options.Items["MyConfigEnum"] = valueWhenMappingFromA);
            

...从 ClassB 映射时作为不同的值:

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

最后,在不需要 ParamTwoConfigEnum 的情况下,可以将它们简单地省略 - 构造函数将正常工作并且 属性 将保留其默认值(就像枚举一样),否则将被忽略。