One goal of the Standardized Service Contract
principle is to avoid having to transform data at runtime. This means
that the more successfully and broadly we are able to apply this
principle, the less need we will have for patterns like Data Model
Transformation . However, even when seeking service contract standardization, there are situations where this pattern is necessary.
For example:
When an IT
enterprise has multiple domain service inventories, each collection of
services can be subject to different design standards. In this case,
when required to enable cross-inventory communication (or when creating a
service composition comprised of services from multiple service
inventories), any disparity in message data models will need to be
overcome by applying Data Model Transformation.
When data needs to be shared between different organizations (or organizational entities), Data Model Transformation will generally be required unless Canonical Schema has been successfully applied via the use of custom or industry-standard message schemas.
When
services encapsulate legacy systems and resources, they will inevitably
need to transform data between legacy data models and the standardized
data model defined in the service contracts. In this case, Data Model
Transformation is carried out within the service architecture.
When services within a service inventory are not all successfully standardized (meaning the Standardized Service Contract principle was not applied to its full extent), Data Model Transformation will be required to enable the necessary interoperability.
Data Model Transformation is generally carried out by creating mapping logic between disparate schemas or data types (Figure 1).
This type of logic can often become complex and is sometimes even
impossible to develop when the disparity between data models is too
large.
For
example, you may encounter mandatory fields in one model that don’t
exist in the other. In such a case, transforming in one direction may
work, but transforming in the opposite direction may not. The following
example demonstrates by showing how we may not be able to determine
which value(s) belong in the middleNames element:
Example 1. Transforming from Data Model #1 to Data Model #2 works, but the opposite transformation is more difficult.
Data Model #1
<person1> <foreNames>Max</foreNames> <middleNames>Carl</middleNames> <surNames>von Sydow</surNames> </person1>
Data Model #2
<person2> <name>Max Carl von Sydow</name> </person2>
|
Besides the potential
complexity of mapping logic, there are other well-known impacts of
applying this pattern. The additional logic will introduce development
and governance effort, and can further affect the performance of
services and service compositions (sometimes significantly so,
especially with more complex mapping logic).
The following sections briefly show three ways to apply Data Model Transformation using .NET technologies.
Object-to-Object
A message sent by a service
consumer to a service can be serialized from XML into an object,
translated into another object, and then serialized into XML again. This
may be a suitable approach when you must use all or most of the data in
the message, either for passing the information onto another service or
for some other purpose.
The first step in this
process is to understand the mapping requirements. Let’s take, for
example, a scenario where we need to transform data defined by a person
type into a customer type (Figure 2). The logic behind this transformation could be as follows:
map the person.id field to the customer.id field
map the person.foreName field to the customer.firstName field
map the person.surName field to the customer.lastName field
map the person.address.phone field to the customer.phone field
map the person.gender field to the customer.gender field
After generating proxy
clients for the different services with the “Add service reference”
feature of Visual Studio, we also have .NET code that represents a
person and a customer. The translation could then be programmed using a
static extension method as shown here:
Example 2.
public static Customer TransformPersonToCustomer (this Person person) { Customer customer = new Customer(); customer.id = person.Id; customer.firstName = person.ForeName; customer.lastName = person.SurName; customer.phone = person.Address.PhoneNr; customer.gender = IntToEnum<customergenderType>((int) person.gender); return customer; } public static T IntToEnum<T>(int value) { return (T)Enum.ToObject(typeof(T), value); }
|
This
code was written knowing that person and customer use the same integer
representations of male and female. The next example shows how this
translation logic is applied on the message retrieved from the first
service before sending it to the second. (Note that since this logic was
written as an extension method it can be called as if it was a method
of the person class.)
Example 3.
PersonServiceClient personService = new PersonServiceClient(); var aPerson = personService.GetPerson(2); var customerFromPerson = getPersonData.TransformPersonToCustomer(); CustomerServiceClient customerService = new CustomerServiceClient(); customerService.UpdateCustomer(customerFromPerson);
|
LINQ-to-XML
Sometimes you may want to
transform a type but you only need to transform a subset of the overall
type. In those cases, deserializing a large document and building a
large object graph can be wasteful and can make code more sensitive to
future changes in data structures.
To handle this situation you can use LINQ-to-XML on the raw message that is returned from a service, as follows:
Example 4.
Message messageOut = channel.Request(messageIn); XmlReader readResponse = messageOut.GetReaderAtBodyContents(); XmlDocument doc = new XmlDocument(); doc.Load(readResponse); var xDoc = XDocument.Parse(doc.OuterXml); XNamespace xmlns2 = xDoc.Root.Attribute("xmlns").Value; var transformed = from d in xDoc.Descendants (xmlns2 + "personElement")
select new Customer { firstName = d.Element(xmlns2 + "foreName").Value, lastName = d.Element(xmlns2 + "surName").Value, id = Convert.ToInt32(d.Element(xmlns2 + "id").Value), gender = (Customer.genderType) Enum.Parse(typeof(Customer.genderType), d.Element(xmlns2 + "gender").Value), phone = d.Element(xmlns2 + "address"). Element(xmlns2 + "phonenr").Value, };
|
In
this example, the response content from the message issued by the
service is received; along with the namespace, the incoming XML
structure is transformed into an object. Note that this code would
continue to work even if the namespace of the response changes. The only
change that can break this code is if one of the elements that are
explicitly asked for is removed or has its name altered. An additional
benefit of this approach is that we can process data types that are not
otherwise easily handled in WCF, such as the xsd:choice construct.
XSLT Transformation
An option that is useful for
avoiding deserialization is XSLT. By defining mapping logic with the
XSLT language, we only take the data that we are interested in into our
objects and leave the rest. XSLT can be used by different parts of the
.NET platform and is a commonly supported industry standard, meaning
that it may also be used with some legacy systems.
Here’s a sample XSLT transformation:
Example 5.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:b="http://schemas.example.org/enterprise/models/v1" xmlns:a="http://schemas.example.org/enterprise/models/v2"> <xsl:template match="/"> <a:customerElement> <a:id> <xsl:value-of select="b:personElement/b:id"/> </a:id> <a:firstName> <xsl:value-of select="b:personElement/b:foreName"/> </a:firstName> <a:lastName> <xsl:value-of select="b:personElement/b:surName"/> </a:lastName> <a:phone> <xsl:value-ofselect= "b:personElement/b:address/b:phonenr"/> </a:phone> <a:gender> <xsl:value-of select="b:personElement/b:gender"/> </a:gender> </a:customerElement> </xsl:template> </xsl:stylesheet>
|
The
semantics of this transformation logic are quite straight forward in
that the markup shows how to find the values for an element.
Using WCF, you could apply this XSLT transformation as follows:
Example 6.
Message messageOut = channel.Request(messageIn);
XmlReader readResponse = messageOut.GetReaderAtBodyContents();
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load("XMLMessages/TransformationPersonToCustomer.xslt");
using (MemoryStream ms = new MemoryStream())
{
XmlWriterSettings ws = new XmlWriterSettings();
ws.Encoding = Encoding.UTF8;
using (XmlWriter xmlWriter = XmlWriter.Create(ms, ws))
{
xslt.Transform(readResponse, xmlWriter);
}
xmlWriter
}