У меня есть класс сущности 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;
}
}
Код на самом деле тривиален.Но с ростом числа сущностей растет и число классов картографирования.В результате получается много похожего кода.Другая проблема заключается в том, что когда есть ассоциации режимов, мне нужно добавить параметры конструктора для дополнительных репозиториев.Вместо этого я попытался внедрить какую-то фабрику хранилищ, но пахло плохо известным Service Locator
, поэтому я вернулся к оригинальному решению.
Модульное тестирование этих картографов также приводит к появлению нескольких похожих методов тестирования.
Учитывая все сказанное, мне интересно, существует ли решение, которое может уменьшить количество написанного вручную кодаи упростить юнит-тестирование.
Заранее спасибо.
ОБНОВЛЕНИЕ
Я выполнил задачу с Value Injecter
, но потом понял, что могу безопасно удалить ее, а остальное все равно будет работать.Вот результирующее решение.
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
защищает от переименования будущих свойств.