Reputation: 2844
A Java shop vendor we're collaborating with needs us (a .NET shop) to provide a RESTful service conforming to a WSDL they've sent us. There are several differences between the XML we're returning and what the vendor expects. Is my process fundamentally wrong, or is there just a difference in the way Java and .NET do serialisation?
My process:
The contract-first idea is new to me, but is covered well in posts like this. So armed with a little knowledge I've run the WSDL and associated .XSDs through SvcUtil.exe to generate C# (some relevant excerpts below), and I'm using these types as the return types for my service.
I've also tried generating the code using wsdl.exe
and xsd.exe
, but neither significantly changed the outcome.
The differences:
Results and code excerpts:
The vendor expects the service to return messages looking like this:
<?xml version="1.0" encoding="UTF-8"?>
<gms:GetClassificationResponse xmlns:gms="http://vendor.com/metadata/cms/services/integration/gms" xmlns:class="http://vendor.com/metadata/model/classification" xmlns:core="http://vendor.com/metadata/model/core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://vendor.com/metadata/cms/services/integration/gms ../gms_services.xsd">
<class:ClassificationVersion>
<class:Code codeValue="01">
<Name core:type="Preferred">Northland Region</Name>
<class:Code codeValue="001">
<Name core:type="Preferred">Far North District</Name>
<class:Code codeValue="500206">
<Name core:type="Preferred">North Cape</Name>
</class:Code>
<class:Code codeValue="500207">
<Name core:type="Preferred">Houhora</Name>
</class:Code>
<!-- Truncated -->
</class:Code>
<class:Code codeValue="002">
<Name core:type="Preferred">Whangarei District</Name>
<!-- Children omitted -->
</class:Code>
<!-- Truncated -->
</class:Code>
<class:Code codeValue="02">
<Name core:type="Preferred">Auckland Region</Name>
<!-- Children omitted -->
</class:Code>
<!-- Truncated -->
</class:ClassificationVersion>
</gms:GetClassificationResponse>
We're producing results like this:
<GetClassificationResponseType xmlns="http://schemas.datacontract.org/2004/07/metadata.cms.services.integration.gms" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ClassificationVersion>
<actionField>ADD</actionField>
<actionFieldSpecified>false</actionFieldSpecified>
<anyAttrField xmlns:a="http://schemas.datacontract.org/2004/07/System.Xml" i:nil="true"/>
<descriptionField i:nil="true"/>
<detailField>full</detailField>
<idField i:nil="true"/>
<itemsField xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays" i:nil="true"/>
<lastUpdateField>0001-01-01T00:00:00</lastUpdateField>
<lastUpdateFieldSpecified>false</lastUpdateFieldSpecified>
<nameField i:nil="true"/>
<uriField i:nil="true"/>
<isPreferredField>false</isPreferredField>
<itemField i:nil="true"/>
<validFromField>0001-01-01T00:00:00</validFromField>
<validFromFieldSpecified>false</validFromFieldSpecified>
<validToField>0001-01-01T00:00:00</validToField>
<validToFieldSpecified>false</validToFieldSpecified>
<Code>
<CodeType>
<actionField>ADD</actionField>
<actionFieldSpecified>false</actionFieldSpecified>
<anyAttrField xmlns:a="http://schemas.datacontract.org/2004/07/System.Xml" i:nil="true"/>
<descriptionField i:nil="true"/>
<detailField>full</detailField>
<idField>Whangarei</idField>
<itemsField xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays" i:nil="true"/>
<lastUpdateField>0001-01-01T00:00:00</lastUpdateField>
<lastUpdateFieldSpecified>false</lastUpdateFieldSpecified>
<nameField>
<ContextualStringType>
<anyField/>
<isStructuredField>true</isStructuredField>
<actionField>ADD</actionField>
<actionFieldSpecified>false</actionFieldSpecified>
<anyAttrField xmlns:a="http://schemas.datacontract.org/2004/07/System.Xml" i:nil="true"/>
<langField i:nil="true"/>
<typeField>Preferred</typeField>
</ContextualStringType>
</nameField>
<uriField i:nil="true"/>
<Code>
<CodeType>
<actionField>ADD</actionField>
<actionFieldSpecified>false</actionFieldSpecified>
<anyAttrField xmlns:a="http://schemas.datacontract.org/2004/07/System.Xml" i:nil="true"/>
<descriptionField i:nil="true"/>
<detailField>full</detailField>
<idField>Springs Flat</idField>
<itemsField xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays" i:nil="true"/>
<lastUpdateField>0001-01-01T00:00:00</lastUpdateField>
<lastUpdateFieldSpecified>false</lastUpdateFieldSpecified>
<nameField>
<ContextualStringType>
<anyField/>
<isStructuredField>true</isStructuredField>
<actionField>ADD</actionField>
<actionFieldSpecified>false</actionFieldSpecified>
<anyAttrField xmlns:a="http://schemas.datacontract.org/2004/07/System.Xml" i:nil="true"/>
<langField i:nil="true"/>
<typeField>Preferred</typeField>
</ContextualStringType>
</nameField>
<uriField i:nil="true"/>
<Code/>
<category i:nil="true"/>
<codeValue>502001</codeValue>
<level xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays" i:nil="true"/>
</CodeType>
And some of the generated code
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("svcutil", "4.0.30319.1")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://mtna.us/metadata/cms/services/integration/gms")]
[DataContract]
public partial class GetClassificationResponseType
{
private ClassificationVersionType classificationVersionField;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute(Namespace = "http://mtna.us/metadata/model/classification", Order = 0)]
[DataMember]
public ClassificationVersionType ClassificationVersion
{
get
{
return this.classificationVersionField;
}
set
{
this.classificationVersionField = value;
}
}
}
...
/// <remarks/>
[System.CodeDom.Compiler.GeneratedCodeAttribute("svcutil", "4.0.30319.1")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace = "http://vendor.com/metadata/model/classification")]
public partial class ClassificationVersionType : ObjectVersionType
{
private LevelType1[] levelField;
private CodeType[] codeField;
private bool isViewField;
private string basisField;
public ClassificationVersionType()
{
this.isViewField = false;
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("Level", Order = 0)]
public LevelType1[] Level
{
get
{
return this.levelField;
}
set
{
this.levelField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("Code", Order = 1)]
public CodeType[] Code
{
get
{
return this.codeField;
}
set
{
this.codeField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
[System.ComponentModel.DefaultValueAttribute(false)]
public bool isView
{
get
{
return this.isViewField;
}
set
{
this.isViewField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute(DataType = "anyURI")]
public string basis
{
get
{
return this.basisField;
}
set
{
this.basisField = value;
}
}
}
Upvotes: 2
Views: 874
Reputation: 2844
We eventually got this working, with a lot of help from the vendor. I've since left the organisation - but I kept a copy of the code.
We used this - https://msdn.microsoft.com/en-us/library/aa395223(v=vs.110).aspx - our full implementation of which follows below.
We decorated the service interface with DispatchByBodyElementBehavior
:
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[ServiceContract(Namespace = "http://vendor.com/project/wsdl"), XmlSerializerFormat, DispatchByBodyElementBehavior]
public interface IContractFirstService
We also had to modify the code generated by SvcUtil.exe, but not massively. There were just a few places where things needed tweaking. Something to do with array limits. Sorry, I can't remember the detail. I'm pretty sure it was fairly obvious what to do from the compile/run-time errors.
Here's how we implemented DispatchByBodyElementBehavior
:
using System;
using System.Diagnostics;
using System.ServiceModel.Channels;
using System.Collections.Generic;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;
/*
* This code was written based on the sample provided at http://msdn.microsoft.com/en-us/library/aa395223(v=vs.110).aspx.
* The purpose of this is to allow dispatching of the incoming SOAP requests based on the payload XML element.
* This allows the more standard document/literal service contract design to be used and does not rely on SOAP actions.
*/
namespace OurOrganisation.Services.VendorIntegrationServices
{
class DispatchByBodyElementOperationSelector : IDispatchOperationSelector
{
Dictionary<XmlQualifiedName, string> dispatchDictionary;
string defaultOperationName;
public DispatchByBodyElementOperationSelector(Dictionary<XmlQualifiedName, string> dispatchDictionary, string defaultOperationName)
{
try
{
this.dispatchDictionary = dispatchDictionary;
this.defaultOperationName = defaultOperationName;
}
catch (Exception ex)
{
Logger.Write(ex.Message);
throw;
}
}
#region IDispatchOperationSelector Members
private Message CreateMessageCopy(Message message, XmlDictionaryReader body)
{
Message copy = Message.CreateMessage(message.Version, message.Headers.Action, body);
copy.Headers.CopyHeaderFrom(message, 0);
copy.Properties.CopyProperties(message.Properties);
return copy;
}
public string SelectOperation(ref System.ServiceModel.Channels.Message message)
{
try
{
XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
XmlQualifiedName lookupQName = new XmlQualifiedName(bodyReader.LocalName, bodyReader.NamespaceURI);
message = CreateMessageCopy(message, bodyReader);
if (dispatchDictionary.ContainsKey(lookupQName))
{
return dispatchDictionary[lookupQName];
}
else
{
return defaultOperationName;
}
}
catch (Exception ex)
{
Logger.Write(ex.Message);
throw;
}
}
#endregion
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
sealed class DispatchByBodyElementBehaviorAttribute : Attribute, IContractBehavior
{
#region IContractBehavior Members
public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
// no binding parameters need to be set here
return;
}
public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
// this is a dispatch-side behavior which doesn't require
// any action on the client
return;
}
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
// We iterate over the operation descriptions in the contract and
// try to locate an DispatchBodyElementAttribute behaviors on each
// operation. If found, we add the operation, keyed by QName of the body element
// that selects which calls shall be dispatched to this operation to a
// dictionary.
//Logger.Write("starting ApplyDispatchBehavior");
try
{
Dictionary<XmlQualifiedName, string> dispatchDictionary = new Dictionary<XmlQualifiedName, string>();
foreach (OperationDescription operationDescription in contractDescription.Operations)
{
DispatchBodyElementAttribute dispatchBodyElement =
operationDescription.Behaviors.Find<DispatchBodyElementAttribute>();
if (dispatchBodyElement != null)
{
dispatchDictionary.Add(dispatchBodyElement.QName, operationDescription.Name);
//Logger.Write(string.Format("method {0} added", operationDescription.Name));
}
}
// Lastly, we create and assign and instance of our operation selector that
// gets the dispatch dictionary we've just created.
dispatchRuntime.OperationSelector =
new DispatchByBodyElementOperationSelector(
dispatchDictionary,
dispatchRuntime.UnhandledDispatchOperation.Name);
}
catch(Exception ex)
{
Logger.Write(ex.Message);
throw;
}
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{
//
}
#endregion
}
[AttributeUsage(AttributeTargets.Method)]
sealed class DispatchBodyElementAttribute : Attribute, IOperationBehavior
{
XmlQualifiedName qname;
public DispatchBodyElementAttribute(string name)
{
qname = new XmlQualifiedName(name);
}
public DispatchBodyElementAttribute(string name, string ns)
{
qname = new XmlQualifiedName(name, ns);
}
internal XmlQualifiedName QName
{
get { return qname; }
set { qname = value; }
}
#region IOperationBehavior Members
public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription, System.ServiceModel.Dispatcher.ClientOperation clientOperation)
{
}
public void ApplyDispatchBehavior(OperationDescription operationDescription, System.ServiceModel.Dispatcher.DispatchOperation dispatchOperation)
{
}
public void Validate(OperationDescription operationDescription)
{
}
#endregion
}
internal class Logger
{
public static void Write(string message)
{
string sSource;
string sLog;
string sEvent;
sSource = "MAL dispatch by body extension";
sLog = "Application";
sEvent = message;
if (!EventLog.SourceExists(sSource))
EventLog.CreateEventSource(sSource, sLog);
EventLog.WriteEntry(sSource, sEvent);
}
}
}
Upvotes: 1