I have a Person entity class and its corresponding DTO PersonDto class.
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; } }
After receiving the DTO object, I have to convert it to a person object. Now I do it completely manually. The code is as follows.
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) { ...
The code is actually trivial. But as the number of entities grows, the number of matching classes. The result is a lot of similar code. Another problem is that when there are mode associations, I have to add constructor parameters for additional repositories. Instead, I tried to introduce some kind of factory repository, but it felt a poorly known Service Locator , so I went back to the original solution.
Unit testing of these cards also leads to a number of similar testing methods.
With all of this in mind, I wonder if there is a solution that can reduce the amount of hand-written code and facilitate device testing.
Thanks in advance.
UPDATE
I completed the task with Value Injecter , but then I realized that I can safely delete it, and the rest will still work. Here is the resulting solution.
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"); } }
Each object is BaseEntityMapper using a specific class derived from BaseEntityMapper . The object for Person objects is as follows.
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); } }
An explicit call to MapAssociation protects against renaming future properties.