|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article includes an object I put together that helps alleviate a common problem, mapping a particular object to a separate object. Common scenarios include:
It is not by any means any attempt at an OR/M. There are already lots of great OR/Ms available to use and I'm not a fan of NIH mentality. This object serves a different purpose. BackgroundI have been using a Domain Model pattern using DDD with nHibernate for most apps and I'm not going to be changing that any time soon. I like having the rules and behaviour inside the domain objects and being able to thoroughly unit test them without having the database and every service under the sun running. One thing I have found that increases development friction is the process of mapping objects against other objects. You tend to end up with code that has your domain objects setting properties onto your DTO objects with usually the same named properties or very similar, for example: // customer created elsewhere
CustomerDTO customerDTO = new CustomerDTO();
customerDTO.DOB = customer.DOB;
Granted the new initializers syntax has made this a bit more bearable: CustomerDTO customerDTO = new CustomerDTO()
{
DOB = customer.DOB
};
However this is still more than I would like to have to write. Requirements of Use
The Implementation, BDD StyleAs per my usual approach, I drove out the design using BDD (TDD with emphasis on behaviour), I find this very useful for objects like this because you can then concentrate on getting the API for the caller right before going down the route of creating all the code from the inside out and finding the API cumbersome to use. Null CheckingI start simple, my first test looks like this: [TestFixture]
public class When_trying_to_apply_mappings_with_nulls :
simple_object_mapper_specification
{
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void Should_throw_null_arg_exception_if_source_is_null()
{
SimpleObjectMapper<Car, CarDto> sut = createSUT();
sut.Apply((Car)null);
}
}
And to make this pass: public class SimpleObjectMapper<Source, Destination>
where Source : new()
where Destination : new()
{
public virtual Destination Apply(Source source)
{
if (source == null)
throw new ArgumentNullException
("source", "source cannot be null");
return null;
}
}
Testing ConventionsWe start to flesh out the bare minimum, By making use of Generics, we get strong typing and don't have to go down the casting route. We now want to check our conventions about the properties are working, now we need to have some objects to map against, so I create the following in the test project: public class Car
{
public string Model { get; set; }
public string Make { get; set; }
public string Color { get; set; }
public Engine Engine { get; set; }
}
public class CarDto
{
public string FullDescription { get; set; }
public string Color { get; set; }
public EngineDto Engine { get; set; }
}
public class Engine
{
public int MaxRPM { get; set; }
public int Capacity { get; set; }
}
public class EngineDto
{
public int Capacity { get; set; }
}
Note that we have some mismatches of properties and one that matches the convention. [TestFixture]
public class When_simple_object_mapper_is_mapping_properties_by_convention :
simple_object_mapper_specification
{
[Test]
public void Should_set_values_where_name_matches_and_same_type()
{
SimpleObjectMapper<Car, CarDto> sut = createSUT();
carDto = sut.Apply(car);
Assert.That(carDto.Color, Is.EqualTo("Red"));
}
}
As we have seen above, only the public virtual Destination Apply(Source source)
{
var destination = new Destination();
if (source == null)
throw new ArgumentNullException
("source", "source cannot be null");
var sourceProps = typeof(Source).GetProperties();
var destinationProps = typeof(Destination).GetProperties();
foreach (var sourceProp in sourceProps)
foreach (var destinationProp in destinationProps)
if((sourceProp.Name.Equals
(destinationProp.Name, StringComparison.InvariantCultureIgnoreCase)
&& (sourceProp.PropertyType.Equals(destinationProp.PropertyType))))
{
var sourceVal = sourceProp.GetValue(source, null);
destinationProp.SetValue(destination, sourceVal, null);
}
return destination;
}
Okay, so we have added some reflection here to get properties of the source and destination types, we then enumerate over each and check for any matches, this gives us the passed test, now to refactor: public class PropertyMatch
{
public PropertyInfo SourceProperty;
public PropertyInfo DestinationProperty;
public PropertyMatch(PropertyInfo sourceProp, PropertyInfo destinationProp)
{
SourceProperty = sourceProp;
DestinationProperty = destinationProp;
}
}
The reason for this class will become clear after the next code sample: public class SimpleObjectMapper<Source, Destination>
where Source : new()
where Destination : new()
{
protected IList<PropertyMatch> propertyMatches = new List<PropertyMatch>();
public SimpleObjectMapper()
{
var sourceProps = typeof(Source).GetProperties();
var destinationProps = typeof(Destination).GetProperties();
foreach (var sourceProp in sourceProps)
foreach (var destinationProp in destinationProps)
if((sourceProp.Name.Equals(destinationProp.Name,
StringComparison.InvariantCultureIgnoreCase)
&& (sourceProp.PropertyType.Equals
(destinationProp.PropertyType))))
propertyMatches.Add(new PropertyMatch
(sourceProp, destinationProp));
}
public virtual Destination Apply(Source source)
{
var destination = new Destination();
if (source == null)
throw new ArgumentNullException
("source", "source cannot be null");
foreach (var propertyMatch in propertyMatches)
{
var sourceVal = propertyMatch.SourceProperty.GetValue(source, null);
propertyMatch.DestinationProperty.SetValue(destination, sourceVal, null);
}
return destination;
}
}
Reflecting over types isn't the most efficient task you can do, therefore we can improve the performance for multiple calls to our Note that this code has changed slightly in that I am now taking advantage of the Testing Awkward MappingsThe convention based mapping we have set up is pretty good and should get us most of the way there. However there are times where you need more control over the setting of properties that don't follow the convention. Let's get a test written to spec this out: [TestFixture]
public class When_simple_object_mapper_is_mapping_properties_explicitly :
simple_object_mapper_specification
{
[Test]
public void Should_map_any_explicit_mappings_specified()
{
SimpleObjectMapper<Car, CarDto> sut = createSUT();
carDto = sut.Explicit((model,dto) => dto.FullDescription = model.Make +
" - " + model.Model)
.Apply(car);
Assert.That(carDto.FullDescription, Is.EqualTo("Audi - A4"));
}
}
The The other thing you may have picked up on is the fluent interface API, you can already start to see how readable it would be if you needed to specify a list of explicit mappings. Now we need to get this to pass: public class SimpleObjectMapper<Source, Destination>
where Source : new()
where Destination : new()
{
protected IList<PropertyMatch> propertyMatches = new List<PropertyMatch>();
protected IList<Action<Source, Destination>> explicitActions =
new List<Action<Source, Destination>>();
public SimpleObjectMapper()
{
var sourceProps = typeof(Source).GetProperties();
var destinationProps = typeof(Destination).GetProperties();
foreach (var sourceProp in sourceProps)
foreach (var destinationProp in destinationProps)
if((sourceProp.Name.Equals(destinationProp.Name,
StringComparison.InvariantCultureIgnoreCase)
&& (sourceProp.PropertyType.Equals(destinationProp.PropertyType))))
propertyMatches.Add(new PropertyMatch(sourceProp, destinationProp));
}
public virtual SimpleObjectMapper<Source, Destination> Explicit
(Action<Source, Destination> explicitAction)
{
explicitActions.Add(explicitAction);
return this;
}
public virtual Destination Apply(Source source)
{
var destination = new Destination();
if (source == null)
throw new ArgumentNullException
("source", "source cannot be null");
foreach (var propertyMatch in propertyMatches)
{
var sourceVal = propertyMatch.SourceProperty.GetValue(source, null);
propertyMatch.DestinationProperty.SetValue(destination, sourceVal, null);
}
foreach (var action in explicitActions)
action(source, destination);
return destination;
}
}
The implementation of this becomes very simple thanks to the power of Lambdas, we simply keep a list of Nested ObjectsSo far we have only used properties that represent a straight forward mapping. However what about the [TestFixture]
public class When_simple_object_mapper_is_mapping_a_deep_property :
simple_object_mapper_specification
{
[Test]
public void Should_use_specified_object_mapper_in_explicit_to_perform_mapping()
{
SimpleObjectMapper<Engine, EngineDto> engineMapper =
new SimpleObjectMapper<Engine, EngineDto>();
SimpleObjectMapper<Car, CarDto> sut = createSUT();
carDto = sut.Explicit((model,dto) => dto.Engine =
engineMapper.Apply(model.Engine))
.Apply(car);
Assert.That(carDto.Engine.Capacity, Is.EqualTo(1998));
}
}
This demonstrates the power gained from using Lambda statements. We haven't needed to make any changes to the Collection MappingsIt's often the case when performing mappings that instead of working with a single domain object, you may be working with a collection. In this case, you want to be able to enumerate over this collection and get a collection of mapped objects back. Let's write a test to enable the [TestFixture]
public class When_simple_object_mapper_is_asked_to_map_object_collections :
simple_object_mapper_specification
{
[Test]
public void Should_be_able_to_map_properties_for_each_source()
{
SimpleObjectMapper<Car, CarDto> sut = createSUT();
Car car2 = new Car()
{
Color = "Blue"
};
IEnumerable<Car> cars = new List<Car>() { car, car2 };
IEnumerable<CarDto> results = sut.Apply(cars);
Assert.That(results.Any(item => item.Color == car.Color));
Assert.That(results.Any(item => item.Color == car2.Color));
}
}
One thing to note is that instead of a new method I have chosen just to overload the public virtual IEnumerable<Destination> Apply(IEnumerable<Source> sources)
{
return sources.Select<Source, Destination>(source => Apply(source));
}
Easy, once again Lambda statements to the rescue to save me from having to write a Managing MappersWe now have a way to define a It functions similar to an IoC container (Windsor, StructureMap, etc.) in that you register the instances with it and then are able to retrieve the instance further down the line, it is a Typical Usage ScenarioFor this example, I'm going to use a Web application example: // ... inside Global.asax
protected void Application_Start()
{
BootStrapper.RegisterMappers();
}
public static class BootStrapper
{
public static void RegisterMappers()
{
SimpleObjectMapperContainer.RegisterMapper
(new SimpleObjectMapper<Administrator, AdministratorDTO>());
SimpleObjectMapperContainer.RegisterMapper
(new SimpleObjectMapper<Location, LocationDTO>());
SimpleObjectMapperContainer.RegisterMapper
(new SimpleObjectMapper<Staff, StaffDTO>()
.Explicit((staff, staffDTO) => staffDTO.LocationId = staff.Location.Id)
.Explicit((staff, staffDTO) => staffDTO.LocationDescription =
staff.Location.Value)
.Explicit((staff, staffDTO) => staffDTO.Photo.Data = staff.Photo));
}
}
At application start up, all we do is register our var adminMapper = SimpleObjectMapper.RetrieveMapper<Administrator, AdministratorDTO>();
var dto = adminMapper.Apply(admin);
Then to get hold of the instance we simply use the right type parameters. Going ForwardFeel free to make adjustments to the code. What I would ask is if you please let me know, I could introduce the changes and update the source here.
History
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||