سؤال

I've been reading through StackOverflow posts regarding converting a DTO's entityID to a Domain's entity using NHibernate and AutoMapper. There's certainly a lot of information out there on it, but everyone seems to have a different suggestion, many of which suggest using a different tool altogether (ValueInjecter). Furthermore, a lot of the information I have found is dated several years. As such, I'm addressing this question again in hopes of clearing things up for me.

I have the following DTO class:

public class PlaylistDto
{
    public Guid Id { get; set;
    public Guid StreamId { get; set; }
    public List<PlaylistItemDto> Items { get; set; }
}

and a corresponding Domain:

public class Playlist
{
    public Guid Id { get; set;
    public Stream Stream { get; set; }
    //  Use interfaces so NHibernate can inject with its own collection implementation.
    public IList<PlaylistItem> Items { get; set; }
}

To start, I declare my intent to map these two entities to each other:

Mapper.CreateMap<Playlist, PlaylistDto>().ReverseMap();
Mapper.CreateMap<PlaylistItem, PlaylistItemDto>().ReverseMap();
Mapper.CreateMap<Stream, StreamDto>().ReverseMap();

ReverseMap allows me to easily declare two-way mappings.

At this point, I am able to successfully convert a Playlist to a PlaylistDto without much effort:

//  Singular:
PlaylistDto playlistDto = Mapper.Map<Playlist, PlaylistDto>(playlist);

//  Collection:
List<PlaylistDto> playlistDtos = Mapper.Map<List<Playlist>, List<PlaylistDto>>(playlists);

This works great. No extra code is needed. However, problems arise when I attempt to map the other direction.

A playlistDto only stores an ID reference to its Stream. If I convert the DTO to a domain, like so:

Playlist playlist = Mapper.Map<PlaylistDto, Playlist>(playlistDto);

Then playlist's Stream is always null regardless of playlistDto's StreamID.

I would like to add an intermediary step which allows the Domain's entity to be fetched via NHibernate using the Dto's entityId.

I was not using AutoMapper, I would achieve this via:

playlist.Stream = StreamDao.Get(playlistDto.StreamId);

With that said, I have questions:

  • What is the agreed upon simplest method for achieving this using AutoMapper?
  • Is ValueInjecter truly a choice I should be considering here? Am I going down a path of forcing AutoMapper to do things which will lead to headaches?
  • If ValueInjecter is preferred... is it still maintained? The project looks extremely dated. In addition, I saw mentions that ValueInjecter does not support collections. This would be a huge turn-off if this is the case.

A few examples I have seen which potentially solve my issue:

Using AutoMapper to unflatten a DTO:

Mapper.CreateMap<Person, Domain.Person>()
    .ForMember(dest => dest.Address, opt => opt.ResolveUsing( src => { return new Address() {Address1 = src.Address, City = src.City, State = src.State }; }))

AutoMapper map IdPost to Post:

public class Id2EntityConverter<TEntity> : ITypeConverter<int, TEntity> where TEntity : EntityBase
{
    public Id2EntityConverter()
    {
        Repository = ObjectFactory.GetInstance<Repository<TEntity>>();
    }

    private IRepository<TEntity> Repository { get; set; }

    public TEntity ConvertToEntity(int id)
    {
        var toReturn = Repository.Get(id);
        return toReturn;
    }

    #region Implementation of ITypeConverter<int,TEntity>

    public TEntity Convert(ResolutionContext context)
    {
        return ConvertToEntity((int)context.SourceValue);
    }

    #endregion
}

(there's more to this, but this is the gist of it)

Advice appreciated. Thanks!

هل كانت مفيدة؟

المحلول

Id2Entity converter is what we use extensively on a very large project and it works flawlessly . Trick here is that you scan all your entities and set up a mapping from int, to your type. If you need the full code, let me know.

Here is the class that creates the mappings.

public class AutoMapperGlobalConfiguration : IGlobalConfiguration
    {
        private readonly IConfiguration _configuration;

        public AutoMapperGlobalConfiguration(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        private void RegisterAssembly(Assembly assembly)
        {
            //add all defined profiles
            var query = assembly.GetExportedTypes()
                .Where(x => x.CanBeCastTo(typeof(Profile)));

            foreach (Type type in query)
            {
                var profile = ObjectFactory.GetInstance(type).As<Profile>();
                _configuration.AddProfile(profile);


                Mapper.AddProfile(profile);

            }
        }

        public void Configure()
        {
            _configuration.RecognizePostfixes("Id");

            var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.StartsWith("DM."));

            //create maps for all Id2Entity converters
            MapAllEntities(_configuration);

            assemblies.Each(RegisterAssembly);
        }

        private static void MapAllEntities(IProfileExpression configuration)
        {
            //get all types from the domain assembly and create maps that
            //convert int -> instance of the type using Id2EntityConverter
            var openType = typeof(Id2EntityConverter<>);
            var idType = typeof(int);

            var persistentEntties = typeof(Domain.Entities).Assembly.GetTypes()
               .Where(t => typeof(EntityBase).IsAssignableFrom(t))
               .Select(t => new
               {
                   EntityType = t,
                   ConverterType = openType.MakeGenericType(t)
               });
            foreach (var e in persistentEntties)
            {
                var map = configuration.CreateMap(idType, e.EntityType);
                map.ConvertUsing(e.ConverterType);
            }
     }
}

نصائح أخرى

Here's my current solution. I think I prefer this over an Id2Entity converter because this just seems a lot easier to debug / not so clever. Sometimes being less clever, but more debuggable is key.

If anyone has an idea on how to do this simply, but less manually, I would love to hear it.

Don't use AfterMap() because then you can't call AssertConfigurationIsValid() without ignoring a lot of properties. Better to just call inside of ForMemeber.

/// <summary>
///     Initialize the AutoMapper mappings for the solution.
///     http://automapper.codeplex.com/
/// </summary>
private static void CreateAutoMapperMaps()
{
    AutofacRegistrations.RegisterDaoFactory();
    ILifetimeScope scope = AutofacRegistrations.Container.BeginLifetimeScope();
    var daoFactory = scope.Resolve<IDaoFactory>();

    Mapper.CreateMap<Error, ErrorDto>()
          .ReverseMap();

    IPlaylistItemDao playlistItemDao = daoFactory.GetPlaylistItemDao();
    IPlaylistDao playlistDao = daoFactory.GetPlaylistDao();
    IStreamDao streamDao = daoFactory.GetStreamDao();
    IUserDao userDao = daoFactory.GetUserDao();

    Mapper.CreateMap<Playlist, PlaylistDto>()
          .ReverseMap()
          .ForMember(playlist => playlist.FirstItem,
                     opt => opt.MapFrom(playlistDto => playlistItemDao.Get(playlistDto.FirstItemId)))
          .ForMember(playlist => playlist.NextPlaylist,
                     opt => opt.MapFrom(playlistDto => playlistDao.Get(playlistDto.NextPlaylistId)))
          .ForMember(playlist => playlist.PreviousPlaylist,
                     opt => opt.MapFrom(playlistDto => playlistDao.Get(playlistDto.PreviousPlaylistId)))
          .ForMember(playlist => playlist.Stream,
                     opt => opt.MapFrom(playlistDto => streamDao.Get(playlistDto.StreamId)));

    Mapper.CreateMap<PlaylistItem, PlaylistItemDto>()
          .ReverseMap()
          .ForMember(playlistItem => playlistItem.NextItem,
                     opt => opt.MapFrom(playlistItemDto => playlistItemDao.Get(playlistItemDto.NextItemId)))
          .ForMember(playlistItem => playlistItem.PreviousItem,
                     opt => opt.MapFrom(playlistItemDto => playlistItemDao.Get(playlistItemDto.PreviousItemId)))
          .ForMember(playlistItem => playlistItem.Playlist,
                     opt => opt.MapFrom(playlistItemDto => playlistDao.Get(playlistItemDto.PlaylistId)));

    Mapper.CreateMap<ShareCode, ShareCodeDto>().ReverseMap();

    Mapper.CreateMap<Stream, StreamDto>()
          .ReverseMap()
          .ForMember(stream => stream.User,
                     opt => opt.MapFrom(streamDto => userDao.Get(streamDto.UserId)))
          .ForMember(stream => stream.FirstPlaylist,
                     opt => opt.MapFrom(streamDto => playlistDao.Get(streamDto.FirstPlaylistId)));

    Mapper.CreateMap<User, UserDto>().ReverseMap();
    Mapper.CreateMap<Video, VideoDto>().ReverseMap();

    Mapper.AssertConfigurationIsValid();
}

You are missing the (application) service layer (the one which is called by the client/presentation layer).

This layer receives a request (get by email for e.g).

It then takes the domain model/entity from the domain services and converts it into a DTO; using a DTOprovider and sends it back to the client in a response message with the DTO as part of the content.

The same layer receives the dto in another request (for e.g save), it reconstructs the domain model/entity using one or several domain services, validates the model to be persisted, if it is valid, it saves the domain model using its corresponding domain service and returns the response with a result of success and maybe a new Id; else if invalid returns a failure with the validation results. (Returning the dto back is optional...)

Check out the layers from this link: http://msdn.microsoft.com/en-us/library/ee658109.aspx

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top