DTO to Entity Mapping Tool

16,212

Solution 1

You can have a look on the two most used Object-Object mapper:

AutoMapper

AutoMapper is a simple little library built to solve a deceptively complex problem - getting rid of code that mapped one object to another. This type of code is rather dreary and boring to write, so why not invent a tool to do it for us?

Value Injecter

ValueInjecter lets you define your own convention-based matching algorithms (ValueInjections) in order to match up (inject) source values to destination values.

There is a comparison article on SO: AutoMapper vs ValueInjecter

Solution 2

You can use GeDA for mapping any entity to a DTO object, it comes with either annotations or DSL support.

http://inspire-software.com/confluence/display/GeDA/FAQ

There are only basic examples on the wiki but jUnits of source code are full of useful examples

You can get it from sourceforge or google code manually or via maven dependency

Details are here: http://inspire-software.com/confluence/display/GeDA/GeDA+-+Generic+DTO+Assembler

Share:
16,212
Dmitriy Melnik
Author by

Dmitriy Melnik

Senior C# developer

Updated on June 04, 2022

Comments

  • Dmitriy Melnik
    Dmitriy Melnik almost 2 years

    I have an entity class Person and its corresponding DTO class 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; }
    }
    

    After receiving a DTO object I have to convert it into a person entity. Now I do it completely manually. The code looks like this.

    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;
      }
    }
    

    The code is in fact trivial. But as the number of entities grows so does the number of mapper classes. The result is lots of similar code. Another issue is that when there are mode associations I have to add constructor parameteres for additional repositories. I tried to inject a some kind of a repository factory instead, but it smelled a bad-known Service Locator so I reverted to an original solution.

    Unit testing of these mappers also results in a number of similar-looking test methods.

    With all this been said I wonder if there exists a solution that can reduce the amount of manually written code and make the unit testing easier.

    Thanks in advance.

    UPDATE

    I'd accomplished the task with Value Injecter but then I realized that I could safely remove it and the rest would 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");
            }
        }  
    

    Mapping of each entity is performed with a concrete class derived from BaseEntityMapper. The one for Person entities looks like this.

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

    Explicitly calling MapAssociation protects against future properties renamings.

  • Dmitriy Melnik
    Dmitriy Melnik about 12 years
    Thanks. Value Injecter have guided me to the final solution that I described in my post update.
  • RayLoveless
    RayLoveless over 8 years
    Careful using these libraries( automapper at least ) because they use reflection to do the mappings and kill your performance! I recently saw some code that was mapping a collection of 100 objects and it was taking 3 seconds. I wouldn't use these libraries. You may be care but developers that come after will abuse them.