Table of Contents
Last updated: 2024-08-15

Mapper


Mapper Mapping

A mapper is a collection of one or more mappings between a source and a target POCO model. Both models handle by a Repository.

public class PdmsToSasMapper : Mapper<PDMSchemaRepository, SasRepository>
{
}

We declare a mapper PdmsToSasMapper between the source model repository for PDMSchema (PDMSchemaRepository) and the target model repository for Share-A-space 7 (SasRepository). On the PdmsToSasMapper the developer can invoke the mapping with the method Mapping.

Note

Examples in this article are mappings to the Share-A-space 7 model. The usage and constructs of the Mapping Framework are the same for ShareAspace Nova even though the model is different.

var sourceRepository = new PDMSchemaRepository();
var targetRepository = new SasRepository();

var mapper = new PdmsToSasMapper();
mapper.Mapping(sourceRepository, targetRepository).Wait();
Note

That the Mapping method is asynchronous so the Wait() or await is needed.

The mapper automatically finds declared mappings in the same .NET assembly, the developer only needs to implement the base Mapping class.

If there is need for looking in additional assemblies for mappings, the assemblies can be provided by a call to the Mapper base constructor.

Mapping


A mapping is the implementation of how one concept in the source model is mapped to the target model. The map logic itself is implemented in the implementation of the Map method.

Mapper Mapping Map

public abstract class PdmsToSasMapping<TSource, TTarget> : Mapping<TSource, PDMSRepository, TTarget, SasRepository>
    where TSource : class
    where TTarget : class
{
}

The base mapping class takes four generic parameters TSource that is the source object for the mapping, TSourceRepository is the same source repository used by the mapper declaration, TTarget the result object of the mapping and TTargetRepository the same target repository used by the mapper declaration. In the example above a new base mapping is declared. This one specific for our PDMSchema to Nova mapping. This of convenience for the developer since TSourceRepository and TTargetRepository will not change for the mapper. The developer can focus on specifying only the TSource and TTarget for the mapping.

Let us look at how the organization concept in PDMSchema is mapped to Share-A-space 7.

using PDMS = Eurostep.Toolbox.PDMS12.Model;
using SAS = Eurostep.Toolbox.SAsBASE70031.Model;

public class OrganizationMapping : PdmsToSasMapping<PDMS.Organization, OrganizationMapping.Target>
{
    public OrganizationMapping()
    {
        this.IdentifiedBy = (left, right) =>
            {
                if (left.Id == right.Id)
                {
                    return true;
                }
                return false;
            };
    }

    public override async Task Map()
    {
        SAS.Organization org = this.CreateTarget(t => t.Organization);
        ObjectIdentifier id = this.CreateTarget(t => t.Identifier);
        id.Owner = org;
        if (string.IsNullOrEmpty(this.SourceObject.Id))
        {
            id.Name = "DEFAULT";
            this.ReportError("Missing Organization identifier for instance #{0}", this.SourceObject.GetInstanceNumber());
        }
        else
        {
            id.Name = this.SourceObject.Id;
        }
    }

    public class Target
    {
        public SAS.Organization Organization { get; set; }
        public ObjectIdentifier Identifier { get; set; }
    }
}

We start of by declaring namespace alias for both the source and target model namespaces.

using PDMS = Eurostep.Toolbox.PDMS12.Model;
using SAS = Eurostep.Toolbox.SAsBASE70031.Model;

This is very useful since both models have objects with the same name (e.g. Organization), instead of writing out the complete namespace we can write PDMS.Organization and SAS.Organization.

public class OrganizationMapping : PdmsToSasMapping<PDMS.Organization, OrganizationMapping.Target>

The implementation of the mapping base type has PDMS.Organization as TSource, but it has an internally declared class named Target as TTarget.

public class Target
{
    public SAS.Organization Organization { get; set; }
    public ObjectIdentifier Identifier { get; set; }
}

A mapping can take any .NET class as TSource or TTarget and in our example the Target class is used because the target output of the mapping has multiple objects while the source only has one. There is no requirement to group multiple targets into its own class but in some cases it makes things simpler for the developer.

public OrganizationMapping()
{
    this.IdentifiedBy = (left, right) =>
        {
            if (left.Id == right.Id)
            {
                return true;
            }

            return false;
        };
}

In the constructor of the OrganizationMapping a C# lambda is declared for IdentifiedBy. This is a way of overriding the default behavior of how a mapping is identified. When a mapping is called again with the same exact source input the previous result target can be returned, thus not producing duplicates.

By default a mapping is identified by the source unique hash code (with the .Net method GetHashCode()), this is handled by the mapping toolkit. In some cases like in this example the mapping is identified by some other unique constraints and we can then override the behavior with IdentifiedBy. In the example above Organization from the PDMSchema source can be declared multiple times with the same Id, this is not allowed in the target Share-A-space 7 model where only one Organization can exist with a given Id. In the IdentifiedBy override we then only need to describe how two PDMS.Organization should be compared for them to be equal, in this example by their Id.

The use of the IdentifiedBy override should be used with great care because it can affect the performance of the mapper. If you have a mapping that have the potential to run often or you don’t know how much it will be used you should always create your own source type and override the GetHashCode() and Equals(object obj) methods. The source type implementation for the OrganizationMapping example looks like this.

public class Source
{
    public Organization Organization { get; set; }

    public override bool Equals(object obj)
    {
        if (!(obj is Source))
        {
            return false;
        }

        Source source = (Source) obj;

        if (this.Organization.InstanceNumber == source.Organization.InstanceNumber)
        {
            return true;
        }

        if (this.Organization.Id == source.Organization.Id)
        {
            return true;
        }

        return false;
    }

    public override int GetHashCode()
    {
        return this.Organization.Id.GetHashCode();
    }
}

The GetHasCode() method uses the hash code from the organization Id, but we also implemented the Equals method. This is because if we get a hash collision (this is when we get the same hash number but the objects are different) we need a way to check if they are equal or not.

public class PartMapping : ProductMapping<Part>
{
    public PartMapping()
    {
        this.When =
            source =>
            source.GetProductRelatedProductCategoryForProducts(this.SourceRepository)
                    .Any(d => d.Name == "part" || d.Name == "tool" || d.Name == "raw material");
    }
}

public class ProductAsDocumentMapping : ProductMapping<SAS.Document>
{
    public ProductAsDocumentMapping()
    {
        this.When =
            source =>
            source.GetProductRelatedProductCategoryForProducts(this.SourceRepository)
            .Any(d => d.Name == "document");
    }
}

In the constructor it is also possible to override when a mapping should be run. If you have some constraints that needs to be fulfilled for a mapping to be executed. E.g. in the mapping between PDMSchema and Share-A-space 7, an PDMSchema Product should be mapped to a Share-A-space 7 Part if it has a category name “part”, “tool” or “raw” but if it has the category “document” it should be mapped to a Share-A-space 7 Document. In the example above we accomplish this requirement by overriding the When and inspect that the category has the appropriate name.

protected override async Task Map()

The Map method is where you put all the map logic for your mapping. This method must be marked as async since mappings have an asynchronous execution of mappings and sub-mappings.

Methods

To make it easier to do mappings there are methods and properties accessible from the Map method.

CreateTarget

To create targets model instances the CreateTarget method should be used. There are two implementations of this method, one that takes zero arguments (used when the mapping target is a model object), and one that takes a lambda expression for selecting a property on the target class like in the OrganizationMap example.

SAS.Organization org = this.CreateTarget(t => t.Organization);
...
public class Target
{
   public SAS.Organization Organization { get; set; }
...

Create

There is also a Create<T>() method that can be used for creating target model instances.

SAS.Part part = this.Create<SAS.Part>();

All of these create methods adds the created instances to the target repository. This also helps the mapper keeping a trace of source to target mappings (can be used as a mapping log).

Source/Target Object and Repository

In the map method it is possible to access the source object through the property SourceObject, the same goes for the target object that can be accessed through TargetObject. The source and target repository can be accessed through the SourceRepository resp. TargetRepository.

Non model object mapping Return

If the developer is building a utility mapping the target may not be a target model object. In that case the result of a mapping can be returned by the Return method.

public class ToUpperMapping : PdmsToSasMapping<string, string>
{
    protected override async Task Map()
    {
        this.Return(this.SourceObject.ToUpper());
    }
}

The example may not be that useful but as shown, a mapping can be between any two objects making it possible to implement useful utility mappings.

Invocation of sub mappings

It is possible to invoke any mapping from inside the Map method, this is even highly recommended, keeping the mapping small and focusing on one mapping concept.

E.g. to call the ToUpperMapping with the SourceObject names input looks like the following.

string nameToUpper = await this.Require<ToUpperMapping>().Map(this.SourceObject.Name);

All mappings are asynchronous and that is why the await keyword is needed for awaiting the control flow to return from the asynchronous operation (there is more information about async and await on MSDN).

Report Error/Warning

In the OrganizationMapping we could also see the following code.

if (string.IsNullOrEmpty(this.SourceObject.Id))
{
    id.Name = "DEFAULT";
    this.ReportError("Missing Organization identifier for instance #{0}", this.SourceObject.GetInstanceNumber());
}

If the PDMSchema organization did not have an ID we used the id “DEFAULT” but we also reported an error telling the user that this Organization instance is missing an identifier. This will tell the mapper that an error has occurred. Even if an error is reported and the mappings will be marked as a failure the rest of the mappings will continue. This allows for all errors and warnings for a mapping to be collected and reported to the user. It is however possible to have the mapper stop as soon as an error is encountered. This is done by setting the BreakOnError property to true on the mapper.

There are built in methods for reporting errors, warnings, and information messages. These messages are then available on the mapper instance through the Errors, Warnings and Information properties. There is also OnError, OnWarning, and OnInformation events that a developer can subscribe to.

Public Mapping

Mappings can be declared in many different formats, between source and target model objects, sub-mappings, and utility mappings. As a result the the mapping declaration must be decorated. All mappers decorated with the PublicMapping attribute will be picked up by the mapping framework. If a mapping is not decorated as public, that mapping will only be executed when called by code (typically from a Map method within the call tree of a Public mapping).

[PublicMapping]
public class ProductMapping : PdmsToSasMapping<Product, Item>
{
    protected override async Task Map()
    {
        var partResult = await PartMapping.Map<PartMapping>(this.SourceObject);
        if (partResult != null)
        {
            this.Return(partResult.Item);
            return;
        }

        var documentResult = await ProductAsDocumentMapping.Map<ProductAsDocumentMapping>(this.SourceObject);
        if (documentResult != null)
        {
            this.Return(documentResult.Item);
            return;
        }

        this.ReportWarning("Product could not be mapped for instance #{0}", this.SourceObject.GetInstanceNumber());
    }
}

The above ProductMapping is public, the mapper will run all Product instances in the source repository against this mapping. The PartMapping and ProductAsDocumentMapping are only called as sub-mappings from the ProductMapping.

Overrides

It is possible to override any mapping with another. The only requirement is that the TSource and TTarget are assignable to the overriding mapping (sub types are allowed).

In the example below, the ProductMapping is overridden with a new implementation that is not calling the sub-mapping ProductAsDocumentMapping.

[OverrideMapping(typeof(ProductMapping))]
public class ProductOverrideMapping : PdmsToSasMapping<Product, Item>
{
    protected override async Task Map()
    {
        var partResult = await PartMapping.Map<PartMapping>(this.SourceObject);
        if (partResult != null)
        {
            this.Return(partResult.Item);
            return;
        }

        this.ReportWarning("Product could not be mapped for instance #{0}", this.SourceObject.InstanceNumber);
    }
}

For each place the ProductMapping is used (even as part of sub-mapping in other mappings) the ProductMapping is replaced by the ProductOverrideMapping.

Extending with new mappings

To extend or override an existing mapper with new mappings. Reference the mapper assembly to the new project and then create a new Mapper.

public class ProjectPdmsToSasMapper : Eurostep.ModelFramework.Mapping.Mapper<PDMSRepository, SasRepository>
{
    public ProjectPdmsToSasMapper()
        : base(typeof(PdmsToSasMapper).Assembly, typeof(ProjectPdmsToSasMapper).Assembly)
    {
    }
}

In the constructor of the mapper, import the assembly files that contains the base mappings. In the example above we import the default PdmsToSasMapper assembly with all its mappings but also the new ProjectPdmsToSasMapper assembly with all its mappings.

Testing

To make testing of mappings easy a helper class is provided in the Eurostep.ModelFramework.Mapping.Testing.dll assembly named TestMapping.

public class PdmsToSasTest<TMapping> : TestMapping<TMapping, int, IPdmsObject, PDMSchemaRepository, int, ISasObject, SasRepository>
    where TMapping : IMapping<PDMSchemaRepository, SasRepository>
{
 
}

In your mapper test project you can then create a helper class for your specific mapper as in the example above for PDMSchema to Nova.

The helper class can then be used within the test cases to test the different a mapping scenarios.

public class ActionMethodToActivityMethodMappingTests
{
    private PdmsToSasTest<ActionMethodToActivityMethodMapping> mapping;

    [SetUp]
    public void Setup()
    {
        this.mapping = new PdmsToSasTest<ActionMethodToActivityMethodMapping>();
        this.mapping.Source.BeginCommit();
    }

    [Test]
    public void ShouldMapActionMethodToActivityMethodIncludingName()
    {
        var actionMethod = new ActionMethod();
        actionMethod.InstanceNumber = 1;
        actionMethod.Name = "actionMethodName";
        this.mapping.Add(actionMethod);

        SasRepository result = this.mapping.Map();

        Assert.That(this.mapping.Errors.Count(), Is.EqualTo(0));

        ActivityMethod activityMethod = result.FindByType<ActivityMethod>().FirstOrDefault();
        Assert.That(activityMethod, Is.Not.Null);

        ObjectName name = result.FindByType<ObjectName>().FirstOrDefault();
        Assert.That(name, Is.Not.Null);
        Assert.That(name.Name, Is.EqualTo(actionMethod.Name));
        Assert.That(name.Role, Is.EqualTo(Constants.ObjectNameRole.Name));

        var descriptions = result.FindByType<ObjectDescription>();
        Assert.That(descriptions, Is.Empty);
    }
}

Above we are testing the ActionMethodToActivityMethodMapping. We start by setting up the test by creating an ActionMethod and adding it to the test mapping, we act by running the mapping with the Map method and last assert that we get the expected output.