Introduction
Mapping is the process of transferring data from a producer class to a consumer class. It tends to be a repetitive and code-bloating exercise so it’s much better to automate the process by using a mapper. The mapper illustrated here uses reflection to identify matching property names in the producer and consumer classes. It transfers property values from the producer properties to the properties in the consumer class that have the same name. A mapper is often used with a simple Data Transfer Object (DTO) to transfer values between project modules. The mapping takes place from the producer module to the DTO and then from the DTO to the consumer module. This gives a good clean separation of concerns. Only the data required by the consumer module is transferred and the modules do not need to know anything about each other; they only need to know about the DTO.
How Does It Work?
The mapper uses the typeo
f method to get the Type
for both the producer class and the consumer class. It then uses the Type.GetProperties()
method to return an array of type PropertyInfo
that holds the metadata for every property in the class. The PropertyInfo
class also has methods for getting and setting the value of the property that it relates to. These methods are used to get the value of the producer class property and to set the value of the consumer class property.
Type classAType = typeof(ClassA);
PropertyInfo[] classAProps = classAType.GetProperties();
Type classBType = typeof(ClassB);
PropertyInfo[] classBProps = classBType.GetProperties();
The mapper has to pair a PropertyInfo
in ClassA
with a PropertyInfo
in ClassB
where the property name is the same. It has a Map
method that does the actual mapping. That method needs to be able to iterate through the pairs, getting the value from the producer pair member and setting the consumer pair member's value to it. A neat way to store the pairs is as a ValueTuple.
A Digression into ValueTuples
An important thing to remember about value tuples is that they are value types not reference types so you can't 'new them up'. They are declared the same way as you would declare a method that has parameters but without a return value and method name. So a tuple array for storing the pairs would be simply (PropertyInfo classAInfo, PropertyInfo classBInfo)[]
. Deconstructing the tuples is easy,
var (classAInfo, classBInfo) = matchingProperties[1];
classAInfo
and classBInfo
can now be used as variables independently of the tuple. Not all variables in the tuple have to be assigned; you can use the underscore character to indicate that the variable is discarded.
var (classAInfo, _) = matchingProperties[1];
It's easy to confuse value tuples with system.Tuples
. An important difference between the two is that ValueTuples
never use the Tuple
qualifier in any declaration. They are used as if they were an anonymous type. Another difference is that systen.Tuples
are very clunky.
Linking Things Together
Matching the pair members together from separate PropertyInfo
arrays into a collection of tuples can be done using a Linq
query.
IEnumerable<(PropertyInfo classA, PropertyInfo classB)> matchingProperties =
from a in classAProps
join b in classBProps on a.Name equals b.Name
select (
a,
b
);
If you want to confuse the laity, you could always write the query using fluent syntax.
IEnumerable<(PropertyInfo classAInfo, PropertyInfo classBInfo)> matchingProperties =
classAPropInfos.Join(
classBPropInfos,
a => a.Name,
b => b.Name,
(a, b) => (a, b)
);
The query returns an IEnumerable
, it can return a List<T>
by adding .ToList()
after the closing bracket, but, as all that's required is to iterate over the collection, there is no need for the enhanced functionality, complexity and memory requirements of a List<T>
.
The Map Method
The method is very simple. The matchingProperties
enumerable is enumerated, getting and setting the property values as it goes.
public void Map(ClassA producer, ClassB consumer)
{
foreach (var (classAInfo, classBInfo) in matchingProperties)
{
classBInfo.SetValue(consumer, classAInfo.GetValue(producer));
}
}
The construction of the enumerable is best placed inside the Mapper's constructor so that the Map
function does not have to rebuild it on each call to the method. It's well worth minimizing the calls to methods that use reflection as they tend to be a tad tardy.
Forced Mapping
The Map
method is a bit limited as it will only match properties with identical names. It's advantageous, on occasions, to be able to map properties that have different names but are of the same Type
such as long Id
and long RecordNumber
. All that's needed to do this is to have a method that adds a new tuple containing the names of the properties to be paired to the matchingProperties
collection. The matchingProperties
variable needs to be converted to a list so that it can be added to.
public void ForceMatch(string propNameA, string propNameB)
{
var propA = classAProps.FirstOrDefault(a => a.Name == propNameA) ;
var propB = classBProps.FirstOrDefault(a => a.Name == propNameB);
matchingProperties.Add((propA, propB));
}
There's a problem with this method - it's using 'magic strings' as the parameters. Magic strings are strings where the contents of the strings affect the functionality of the method. The strings are supposed to be the names of properties but the compiler will happily accept any nonsense as the string's content and it's only at 'run time' that the errors come to light. What's more, if the actual property name is changed, you will have to rummage through the code looking for literal string references to the property and amend them. A way out of this difficulty is to use the nameof
operator when calling the method.
mapper.ForceMatch(nameof(student.ForeName), nameof(dto.FirstName));
nameof
looks like a method call but it's actually a compiler instruction to look up the name of the property from the class definition and use that in the compiled code.
Excluding Matches
It's sometimes helpful to be able to remove a certain match from the list of matching properties. This is easily achieved by a simple search of the list.
public bool Exclude(string propName)
{
var target = matchingProperties.FirstOrDefault(p => p.classA.Name == propName||
p.classB.Name==propName);
return matchingProperties.Remove(target);
}
A Generic Mapper
So far, the mapper has used two specific classes, ClassA
and ClassB
. By using Generics, it's possible to define the mapper to accept any two classes. The Mapper
is defined using placeholders for the two classes. By convention, these placeholders have the character T
prepended to them. So the class definition starts with:
public class Mapper<TClassA, TClassB> : IMapper<TClassA, TClassB>
where TClassA : class
where TClassB : class
The where
statements define the constraint that TClassA
and TClassB
objects have to be classes. To instruct the compiler to use the classes, Student
and Dto
, instantiate the mapper like this.
Mapper<Student, Dto> mapper = new Mapper<Student, Dto>();
Here's the complete definition of the Mapper
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Mapper
{
public class Mapper<TClassA, TClassB> : IMapper<TClassA, TClassB>
where TClassA : class
where TClassB : class
{
private readonly List<(PropertyInfo classA, PropertyInfo classB)> matchingProperties;
private readonly PropertyInfo[] classAProps;
private readonly PropertyInfo[] classBProps;
public Mapper()
{
Type classAType = typeof(TClassA);
classAProps = classAType.GetProperties();
Type classBType = typeof(TClassB);
classBProps = classBType.GetProperties();
matchingProperties =
classAProps.Join(
classBProps,
a => a.Name,
b => b.Name,
(a, b) => (a, b)
).ToList();
}
public void Map(TClassA producer, TClassB consumer)
{
foreach (var (classAInfo, classBInfo) in matchingProperties)
{
if (classAInfo.PropertyType.FullName != classBInfo.PropertyType.FullName)
throw new InvalidOperationException(
$"{Constants.NoMatchPropTypes} {classAInfo.Name}, {classBInfo.Name}");
classBInfo.SetValue(consumer, classAInfo.GetValue(producer));
}
}
public void Map(TClassB producer, TClassA consumer)
{
foreach (var (classAInfo, classBInfo) in matchingProperties)
{
if (classAInfo.PropertyType.FullName != classBInfo.PropertyType.FullName)
throw new InvalidOperationException(
$"{Constants.NoMatchPropTypes} {classBInfo.Name}, {classAInfo.Name}");
classAInfo.SetValue(consumer, classBInfo.GetValue(producer));
}
}
public void ForceMatch(string propNameA, string propNameB)
{
var propA = classAProps.FirstOrDefault(a => a.Name == propNameA);
var propB = classBProps.FirstOrDefault(a => a.Name == propNameB);
if (propA == null)
throw new ArgumentException($"{Constants.PropNullOrMissing} {nameof(propNameA)}");
if (propB == null)
throw new ArgumentException($"{Constants.PropNullOrMissing} {nameof(propNameB)}");
if (propA.PropertyType.FullName != propB.PropertyType.FullName)
throw new ArgumentException($"{Constants.NoMatchPropTypes} {propNameA}, {propNameB}");
matchingProperties.Add((propA, propB));
}
public bool Exclude(string propName)
{
var target = matchingProperties.FirstOrDefault(p =>
p.classA.Name == propName || p.classB.Name == propName);
return matchingProperties.Remove(target);
}
public int GetMappingsTotal => matchingProperties.Count;
}
}
Unit Testing Generic Methods.
When it comes to testing the generic mapper, it's important that the tests are also generic so that they can be run using any specific instance of the class. To get a good separation between the generic tests and the implementation of the tests, the generic tests are placed in an abstract base class and the methods needed to run the tests with a specific implementation of the mapper are defined in a derived class. The base class is defined like this:
public abstract class MapperTestsGeneric<TClassA, TClassB>
where TClassA : class
where TClassB : class
....
The derived class is defined as:
public class MapperUnitTests : MapperTestsGeneric<ClassA, ClassB>
{
...
The TClassA
and TClass
placeholders have been replaced with the specific classes, ClassA
and ClassB
. When the class is compiled, the generic base class will use these classes. To get an idea of how the tests are constructed, have a look at the unit test for the ForceMatch
method.
[TestMethod]
public void ForceMatchAddsATupleToMatchingProperties()
{
Mapper<TClassA, TClassB> mapper = new Mapper<TClassA, TClassB>();
(string NameA, string NameB) = Get2PropNamesToForceMatch();
int mappings = mapper.GetMappingsTotal;
mapper.ForceMatch(NameA, NameB);
Assert.IsTrue(mappings + 1 == mapper.GetMappingsTotal);
}
This tests the functionality of the ForceMatch
method. All that ForceMatch
does is to add a tuple to the matchingProperties
list. The test uses the method Get2PropNamesToForceMatch
to provide the names of the two properties to be matched. The names of these properties depend upon the specific classes that the mapper is using. So the method is defined in the base class but overwritten in the derived class.
protected abstract (string NameA, string NameB) Get2PropNamesToForceMatch();
protected override (string NameA, string NameB) Get2PropNamesToForceMatch()
{
return (nameof(ClassA.Code), nameof(ClassB.CodeName));
}
The ForceMatch
method is supposed to throw an exception when no match is found for the property names so a test for this would be something like:
[TestMethod]
[ExpectedException(typeof(ArgumentException), "Different property Types were allowed")]
public void ForceMatchThrowsArgumentExceptionWhenMatchTypesDoNotMatch()
{
Mapper<TClassA, TClassB> mapper = new Mapper<TClassA, TClassB>();
(string NameA, string NameB) = Get2PropNamesToForceMatchFromPropsWithDifferentTypes();
mapper.ForceMatch(NameA, NameB);
}
Testing the Map Method
Unit tests should have just the one assert statement. The Map
function tests cheat a bit by having one assertion but the assertion helper methods test multiple properties. Their tests pass if all the properties are mapped as expected and fail if one or more does not. My preference is to stop at this level rather than have a separate test for every property. There is a danger, with unit testing, that digging too far into the code results in testing that the compiler works rather than testing the functionality of the method. Here's the generic test to test that the properties with matching names are mapped from TClassA
to TClassB
.
[TestMethod]
public void MapAtoBMapsSameNamePropertyValuesFromAtoB()
{
Mapper<TClassA, TClassB> mapper = new Mapper<TClassA, TClassB>();
TClassA a = CreateSampleClassA();
TClassA unmappedA = CreateSampleClassA();
TClassB b = CreateSampleClassB();
mapper.Map(a, b);
Assert.IsTrue(AreSameNamePropsMappedFromAtoB(b, unmappedA));
}
The helper method is overridden in the derived class:
protected override bool AreSameNamePropsMappedFromAToB(ClassB b, ClassA unmappedA) =>
b.Name == unmappedA.Name &&
b.Age == unmappedA.Age &&
b.Cash == unmappedA.Cash &&
b.Date == unmappedA.Date &&
b.Employee == unmappedA.Employee;
The reason that another instance of ClassA
is used, rather than the one that was a parameter of the Map
method, is to make sure that the mapping was from ClassA
to ClassB
. If the instance of ClassA
that was a parameter of the Map
method was used, the test would pass even if the mapping was from ClassB
to ClassA
.
Conclusion
A simple mapper is easily developed using reflection, Linq queries and value tuples. The use of generics increases the utility value of the mapper as it allows the mapper to be employed with any given instance of the producer and consumer class. Finally, unit testing of a generic class can be simplified by defining an abstract base class to hold the generic tests and by using a derived class to include the implementation details for a specific instance of the generic class.
References
History
- 2nd October, 2019: Initial version