Mapper
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.
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.