I’m sure you’re all aware of how mapping business entities to DTO’s is quite a menial task and that there are several strategies to deal with that.
I employ more than one of those strategies.
But one that I use quite a bit, mainly because of speed of development, is the nhibernate/automapper combo.
Once the handler, repositories, queryobjects et all (or whatever infrastructure you use to get the data to your view) is in place, all one needs to do is, add a rightly named property to the dto and magically it gets populated with the desired value.
You don’t even have to adapt any test thanks to the AssertConfigurationIsValid method of AutoMapper.
However, ever since I first started using this way of working, two things about it all have been bothering me :
1. The need for a ‘Registry’ type class (or a static method, or whatever), where one registers all the mappings.
F.i. :
public static class Boot
{
public static void Strap()
{
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagDto, BetalingsaanvraagErelonenViewModel>()
.ForMember(dest => dest.TotaalBedrag, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagLijnDto, BetalingsaanvraagLijnViewModel>()
.ForMember(dest => dest.Totaal, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.StavingsDocumentDto, BetalingsaanvraagDocumentViewModel>();
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagDto, BetalingsaanvraagGrondenViewModel>()
.ForMember(dest => dest.TotaalBedrag, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagLijnDto, BetalingsaanvraagLijnViewModel>()
.ForMember(dest => dest.Totaal, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.StavingsDocumentDto, BetalingsaanvraagDocumentViewModel>();
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagDto, BetalingsaanvraagVorderingsstatenViewModel>()
.ForMember(ba => ba.TotaalBedrag, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.BetalingsAanvraagLijnDto, BetalingsaanvraagLijnViewModel>()
.ForMember(ba => ba.Totaal, options => options.Ignore());
CreateMap<GetBetalingsAanvraagByIdentificatieResponse.StavingsDocumentDto, BetalingsaanvraagDocumentViewModel>();
CreateMap<BetalingsaanvraagLijnViewModel, UpdateBetalingsLijnBedragRequest>();
...
}
}
2. The fact that the AssertConfigurationIsValid test just isn’t enough to avoid run-time errors.
The first issue can be avoided by putting the container to work.
Let’s just wrap the static automapper stuff into some objects :
public interface IMapper
{
void Configure();
//for testing only, curse you C# generics
object Map(object target);
}
public interface IMapper<in TFrom, out TTo> : IMapper
{
TTo Map(TFrom from);
IEnumerable<TTo> Map(IEnumerable<TFrom> fromList);
}
public class Mapper<TFrom, TTo> : IMapper<TFrom, TTo>
{
public virtual void Configure()
{
AutoMapper.Mapper.CreateMap<TFrom, TTo>();
}
public object Map(object @from)
{
return Map((TFrom)@from);
}
public virtual TTo Map(TFrom @from)
{
return AutoMapper.Mapper.Map<TFrom, TTo>(@from);
}
public virtual IEnumerable<TTo> Map(IEnumerable<TFrom> fromList)
{
return fromList.Select(Map);
}
}
Create some mappers, f.i. :
public class ProjectDetailMapper : Mapper<Project, ProjectDetailDto> { }
public class GetProjectDetailHandler : IGetProjectDetailHandler
{
private readonly IGetProjectQuery query;
private readonly IMapper<Project, ProjectDetailDto> mapper;
public GetDetailBetalingsAanvraagHandler(
IGetProjectQuery query,
IMapper<Project, ProjectDetailDto> mapper)
{
this.query = query;
this.mapper = mapper;
}
public ProjectDetailDto Handle(string id)
{
return mapper.Map(query.UniqueResult(id));
}
}
And register them in the container :
public class MyRegistry : Registry
{
public MyRegistry()
{
Scan(cfg =>
{
cfg.TheCallingAssembly();
cfg.WithDefaultConventions();
cfg.ConnectImplementationsToTypesClosing(typeof(IMapper<,>))
.OnAddedPluginTypes(ex => ex.Singleton().OnCreation(map => ((IMapper)map).Configure()));
});
}
}
Above example assumes you’re using structuremap, but I’ve done the same with both windsor and unity.
Registering the mappers as singletons and calling their Configure method effectively does the same thing as the Boot.Strap() method above.
Except now this bookkeeping code is handled by the container.
The advantage of the method above, I find, is the fact that the mapping can be put right next to the feature, where I feel it belongs.
Mapping is not a cross-cutting concern.
The second issue, however is far more serious.
Look again at the Boot.Strap() example.
If I forget to add a mapping to the initialize method, there is no way I will find out until I actually try to map this in the app. Either through automated acceptance testing or through manual ‘labor’.
So how can we test this better ?
The refactoring described above will help us a lot.
Maintaining the same level :
Using the Boot.Strap() method we can easily test configuration by simple executing the method and then calling : ‘AutoMapper.Mapper.AssertConfigurationIsValid()’.
The same thing can be achieved by retrieving all Mapper derived types, instantiating one of each, call the configure methods and then call AssertConfigurationIsValid, like so :
[Test]
public void ConfigurationIsValid()
{
var mappers = ScanAssemblyForMappers();
mappers.ForEach(m => m.Configure());
AutoMapper.Mapper.AssertConfigurationIsValid();
}
Upping the level :
Using the same reflection technique and with the help of quickgenerate we can test that everything we want to map, can actually be mapped.
[Test]
public void CanMap()
{
var mappers = ScanAssemblyForMappers();
mappers.ForEach(m => m.Configure());
mappers.ForEach(m => m.Map(new AutoMapperTestBuilder().One(GetSourceType(m))));
}
The AutoMapperTestBuilder class is derived from quickgenerate’s DomainGenerator and sets up some stuff in the constructor.
public class AutoMapperTestBuilder : DomainGenerator
{
public AutoMapperTestBuilder()
{
With<Mededeling>(
opt => opt.StartingValue(() =>
new[]
{
new Mededeling(new StringGenerator(1, 34).GetRandomValue()),
new Mededeling(
new IntGenerator(100, 999).GetRandomValue(),
new IntGenerator(1000, 9999).GetRandomValue(),
new IntGenerator(10000, 99999).GetRandomValue())
}
.PickOne()));
OneToOne<Project, Location>((p, g) => p.Location = l);
...
}
}
Now the only that can go wrong is that I declare a dependency on a IMapper and I forget to actually implement this.
Fortunately, in that case, the unit test that verifies the Container configuration fails
.