DTO到实体映射工具

8

我有一个实体类Person及其对应的DTO类PersonDto

public class Person: Entity
{
  public virtual string Name { get; set; }
  public virtual string Phone { get; set; }
  public virtual string Email { get; set; }
  public virtual Sex Sex { get; set; }
  public virtual Position Position { get; set; }
  public virtual Division Division { get; set; }
  public virtual Organization Organization { get; set; }
}

public class PersonDto: Dto
{
  public string Name { get; set; }
  public string Phone { get; set; }
  public string Email { get; set; }
  public Guid SexId { get; set; }
  public Guid PositionId { get; set; }
  public Guid DivisionId { get; set; }
  public Guid OrganizationId { get; set; }
}

在收到一个DTO对象后,我需要将其转换为人类实体。现在我完全手动完成这个过程。代码如下:

public class PersonEntityMapper: IEntityMapper<Person, PersonDto>
{
  private IRepository<Person> _personRepository;
  private IRepository<Sex> _sexRepository;
  private IRepository<Position> _positionRepository;
  private IRepository<Division> _divisionRepository;
  private IRepository<Organization> _organizationRepository;

  public PersonEntityMapper(IRepository<Person> personRepository,
                            IRepository<Sex> sexRepository,
                            IRepository<Position> positionRepository,
                            IRepository<Division> divisionRepository,
                            IRepository<Organization> organizationRepository)
  {
    ... // Assigning repositories
  }

  Person Map(PersonDto dto)
  {
    Person person = CreateOrLoadPerson(dto);

    person.Name = dto.Name;
    person.Phone = dto.Phone;
    person.Email = dto.Email;

    person.Sex = _sexRepository.LoadById(dto.SexId);
    person.Position = _positionRepository.LoadById(dto.PositionId);
    person.Division = _divisionRepository.LoadById(dto.DivisionId);
    person.Organization = _organizationRepository.LoadById(dto.OrganizationId);

    return person;
  }
}

实际上,这段代码非常简单。但是随着实体的数量增加,映射器类的数量也会增加。结果是有很多相似的代码。另一个问题是当有更多关联时,我必须为其他存储库添加构造函数参数。我尝试注入某种类型的存储库工厂,但闻起来像一个不好的“服务定位器”,所以我回到了原始的解决方案。
对这些映射器进行单元测试也会产生许多外观相似的测试方法。
在说完所有这些之后,我想知道是否存在一种解决方案,可以减少手动编写的代码量,并使单元测试更容易。
谢谢提前。
更新
我使用了价值注入器完成了任务,但后来我意识到我可以安全地将其删除,其余部分仍然可以正常工作。以下是最终的解决方案。
public abstract class BaseEntityMapper<TEntity, TDto> : IEntityMapper<TEntity, TDto>
        where TEntity : Entity, new()
        where TDto : BaseDto
    {
        private readonly IRepositoryFactory _repositoryFactory;

        protected BaseEntityMapper(IRepositoryFactory repositoryFactory)
        {
            _repositoryFactory = repositoryFactory;
        }

        public TEntity Map(TDto dto)
        {
            TEntity entity = CreateOrLoadEntity(dto.State, dto.Id);

            MapPrimitiveProperties(entity, dto);
            MapNonPrimitiveProperties(entity, dto);

            return entity;
        }

        protected abstract void MapNonPrimitiveProperties(TEntity entity, TDto dto);

        protected void MapPrimitiveProperties<TTarget, TSource>(TTarget target, TSource source, string prefix = "")
        {
            var targetProperties = target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);
            var sourceProperties = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);

            foreach (var targetProperty in targetProperties) {
                foreach (var sourceProperty in sourceProperties) {
                    if (sourceProperty.Name != string.Format("{0}{1}", prefix, targetProperty.Name)) continue;
                    targetProperty.SetValue(target, sourceProperty.GetValue(source, null), null);
                    break;
                }
            }
        }

        protected void MapAssociation<TTarget, T>(TTarget target, Expression<Func<T>> expression, Guid id) where T : Entity
        {
            var repository = _repositoryFactory.Create<T>();
            var propertyInfo = (PropertyInfo)((MemberExpression)expression.Body).Member;
            propertyInfo.SetValue(target, repository.LoadById(id), null);
        }

        private TEntity CreateOrLoadEntity(DtoState dtoState, Guid entityId)
        {
            if (dtoState == DtoState.Created) return new TEntity();

            if (dtoState == DtoState.Updated) {
                      return _repositoryFactory.Create<TEntity>().LoadById(entityId);
            }
            throw new BusinessException("Unknown DTO state");
        }
    }  

每个实体的映射都是使用从BaseEntityMapper派生出的具体类来执行的。 Person实体的映射器如下所示。

public class PersonEntityMapper: BaseEntityMapper<Person, PersonDto>
    {
        public PersonEntityMapper(IRepositoryFactory repositoryFactory) : base(repositoryFactory) {}

        protected override void MapNonPrimitiveProperties(Person entity, PersonDto dto)
        {
            MapAssociation(entity, () => entity.Sex, dto.SexId);
            MapAssociation(entity, () => entity.Position, dto.PositionId);
            MapAssociation(entity, () => entity.Organization, dto.OrganizationId);
            MapAssociation(entity, () => entity.Division, dto.DivisionId);
        }
    }

显式调用 MapAssociation 可以防止未来的属性重命名。


请勿在问题正文中回答您的问题,请在问题下添加新答案并回答它。如果您认为这是最佳答案,则接受您的答案。 - Iman Mahmoudinasab
2个回答

6

您可以查看两个最常用的对象映射器:

AutoMapper

AutoMapper是一个简单的小库,旨在解决一个看似复杂的问题 - 摆脱将一个对象映射到另一个对象的代码。这种类型的代码写起来相当枯燥无味,那为什么不发明一个工具来代替我们呢?

Value Injecter

ValueInjecter允许您定义自己的基于约定的匹配算法(ValueInjections),以便将源值与目标值匹配(注入)。

SO上有一篇比较文章:AutoMapper vs ValueInjecter


谢谢。Value Injecter引导我到了我在帖子更新中描述的最终解决方案。 - Dmitriy Melnik
1
使用这些库(至少是 Automapper)要小心,因为它们使用反射来进行映射并降低了性能!我最近看到一些代码正在映射100个对象的集合,并且需要3秒才能完成。我不会使用这些库。您可能会小心使用,但是接下来的开发人员可能会滥用它们。 - RayLoveless

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接