Click here to Skip to main content
14,355,411 members

A Generic Mapper with Value Tuples and Generic Tests.

Rate this:
5.00 (1 vote)
Please Sign up or sign in to vote.
5.00 (1 vote)
2 Oct 2019CPOL
This piece illustrates how to construct a simple generic mapper that will copy every property value from one class to another where the name and type of the property is the same in both classes; it goes on to suggest a way of constructing generic tests that can be used for any instance of the

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 typeof 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(  // outer collection
      classBPropInfos,     // inner collection
      a => a.Name,      // outer key , match on the Name property
      b => b.Name,      // inner key, match on the Name property
      (a, b) => (a, b)  //project into a ValueTuple
      );

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);
 //....check for argument exceptions
 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(           // outer collection
        classBProps,             // inner collection
        a => a.Name, // outer key  
        b => b.Name,     // inner key 
        (a, b) => (a, b)//project into ValueTuple
              ).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.

//In the base test class
protected abstract (string NameA, string NameB) Get2PropNamesToForceMatch();
//In the derived test class
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]                                 //test fail message
 [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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

George Swan
Student
Wales Wales
No Biography provided

Comments and Discussions

 
QuestionHow does this compare to AutoMapper? Pin
Simon Hughes8-Oct-19 1:19
memberSimon Hughes8-Oct-19 1:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 30 Sep 2019

Stats

1.5K views
41 downloads
4 bookmarked