用AutoMapper实现集合的多态映射

本文关键字:多态 映射 集合 AutoMapper 实现 | 更新日期: 2023-09-27 18:11:58

TL;DR:我在多态映射方面遇到了麻烦。我做了一个github repo与一个测试套件,说明我的问题。请在这里找到:链接到REPO

我正在实现一个保存/加载功能。要做到这一点,我需要确保我正在序列化的领域模型是以一种序列化友好的方式表示的。为了完成这个任务,我创建了一组dto,其中包含进行有意义的保存或加载所需的最少的信息集。

类似于域:

public interface IDomainType
{
  int Prop0 { get; set; }
}
public class DomainType1 : IDomainType
{
  public int Prop1 { get; set; }
  public int Prop0 { get; set; }
}
public class DomainType2 : IDomainType
{
  public int Prop2 { get; set; }
  public int Prop0 { get; set; }
}
public class DomainCollection
{
  public IEnumerable<IDomainType> Entries { get; set; }
}

…对于dto

public interface IDto
{
  int P0 { get; set; }
}
public class Dto1 : IDto
{
  public int P1 { get; set; }
  public int P0 { get; set; }
}
public class Dto2 : IDto
{
  public int P2 { get; set; }
  public int P0 { get; set; }
}
public class DtoCollection
{
  private readonly IList<IDto> entries = new List<IDto>();
  public IEnumerable<IDto> Entries => this.entries;
  public void Add(IDto entry) { this.entries.Add(entry); }
}

这个想法是DomainCollection表示应用程序的当前状态。目标是将DomainCollection映射到DtoCollection,生成DtoCollection的实例,该实例包含映射到域的IDto的适当实现。反之亦然

这里有一个额外的小技巧,不同的具体域类型来自不同的插件程序集,所以我需要找到一种优雅的方式让AutoMapper(或类似的,如果你知道一个更好的映射框架)为我做繁重的工作。

使用structuremap,我已经能够从插件中找到并加载所有配置文件,并使用它们配置应用程序IMapper。

我已经尝试创建这样的配置文件…

public class CollectionMappingProfile : Profile
{
  public CollectionMappingProfile()
  {
    this.CreateMap<IDomainType, IDto>().ForMember(m => m.P0, a => a.MapFrom(x => x.Prop0)).ReverseMap();
    this.CreateMap<DtoCollection, DomainCollection>().
       ForMember(fc => fc.Entries, opt => opt.Ignore()).
       AfterMap((tc, fc, ctx) => fc.Entries = tc.Entries.Select(e => ctx.Mapper.Map<IDomainType>(e)).ToArray());
    this.CreateMap<DomainCollection, DtoCollection>().
       AfterMap((fc, tc, ctx) =>
                {
                  foreach (var t in fc.Entries.Select(e => ctx.Mapper.Map<IDto>(e))) tc.Add(t);
                });
}
public class DomainProfile1 : Profile
{
  public DomainProfile1()
  {
    this.CreateMap<DomainType1, Dto1>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1))
      .IncludeBase<IDomainType, IDto>().ReverseMap();
  }
}
public class DomainProfile2 : Profile
{
  public DomainProfile2()
  {
    this.CreateMap<DomainType2, IDto>().ConstructUsing(f => new Dto2()).As<Dto2>();
    this.CreateMap<DomainType2, Dto2>().ForMember(m => m.P2, a => a.MapFrom(x => x.Prop2))
      .IncludeBase<IDomainType, IDto>().ReverseMap();
  }
}
然后,我编写了一个测试套件,以确保在将该特性与应用程序集成时,映射将按照预期的方式运行。我发现每当dto被映射到Domain时(比如Load), AutoMapper会创建IDomainType的代理,而不是将它们解析到Domain。

我怀疑问题出在我的地图配置文件上,但我已经没有人才了。提前感谢您的意见。

这是另一个链接到github的repo

用AutoMapper实现集合的多态映射

我在研究多态映射问题时偶然发现了这个问题。答案很好,但如果您想从基映射的角度来处理它,并且有许多派生类,则可以尝试以下方法:

CreateMap<VehicleEntity, VehicleDto>()
    .IncludeAllDerived();
CreateMap<CarEntity, CarDto>();
CreateMap<TrainEntity, TrainDto>();
CreateMap<BusEntity, BusDto>();

我花了一点时间重新组织了这个仓库。我甚至模仿了一个核心项目和两个插件。这确保了当测试最终开始通过时,我不会得到假阳性的结果。

我发现这个解决方案有两个(大概)部分。

1)我滥用了AutoMapper的。reversemap()配置方法。我假设它会执行我所做的任何自定义映射的倒数。不是这样的!它只做简单的反转。很好。关于它的一些问题/答案:1、2

2)我没有完全正确地定义映射继承。我来拆解。

2.1)我的DomainProfiles遵循这个模式:

public class DomainProfile1 : Profile
{
  public DomainProfile1()
  {
    this.CreateMap<DomainType1, IDto>().ConstructUsing(f => new Dto1()).As<Dto1>();
    this.CreateMap<DomainType1, Dto1>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1))
      .IncludeBase<IDomainType, IDto>().ReverseMap();
    this.CreateMap<Dto1, IDomainType>().ConstructUsing(dto => new DomainType1()).As<DomainType1>();
  }
}

所以现在知道了。reversemap()不是这里要使用的东西,很明显Dto1和DomainType1之间的映射定义得很差。另外,DomainType1和IDto之间的映射没有链接回基本iddomaintype到IDto映射。这也是一个问题。最终结果:

public class DomainProfile1 : Profile
{
  public DomainProfile1()
  {
    this.CreateMap<DomainType1, IDto>().IncludeBase<IDomainType, IDto>().ConstructUsing(f => new Dto1()).As<Dto1>();
    this.CreateMap<DomainType1, Dto1>().IncludeBase<DomainType1, IDto>().ForMember(m => m.P1, a => a.MapFrom(x => x.Prop1));
    this.CreateMap<Dto1, IDomainType>().IncludeBase<IDto, IDomainType>().ConstructUsing(dto => new DomainType1()).As<DomainType1>();
    this.CreateMap<Dto1, DomainType1>().IncludeBase<Dto1, IDomainType>().ForMember(m => m.Prop1, a => a.MapFrom(x => x.P1));
  }
}

现在映射的每个方向都被显式定义了,并且继承得到了尊重。

2.2) IDomainType和IDto的最基本映射位于配置文件内部,该配置文件还定义了"集合"类型的映射。这意味着,一旦我把项目分开来模拟插件架构,只测试最简单的继承的测试就会以新的方式失败——无法找到基本映射。我所要做的就是将这些映射放入它们自己的配置文件中,并在测试中使用该配置文件。这就是好的SRP。

在我把自己的答案标记为被接受的答案之前,我会把我学到的东西应用到我的实际项目中。希望我明白了,也希望这对其他人有帮助。

有用链接:

这是一个很好的重构练习。我承认,我把它作为建立我的例子的起点。所以,谢谢@Olivier。