开发者

Deseralize multiple WSDL versions to same object

I'm consuming a webservice from another company, they have multiple versions running, each newer version has only added new fields/objects, BUT changes some of the element names.

I would like the ability to consume any of the versions with the same code.

Specifically In one version a search method returns: <searchReturn><SummaryData_Version1Impl /><SummaryData_Version1Impl /></searchReturn>

and in a different version: <searchReturn><SummaryData_Version2Impl /><SummaryData_Version2Impl /></searchReturn>

So right now the proxy generated by wsdl.exe cannot work with both because of that element change.

  1. The best solution would be to make the other company fix their service to not change the element names, but that is fairly unlikely in this situation
  2. I'm thinking my best bet for a working solution is to send and get the SOAP request manually, and modify the element names then deserialize manually which so far has seemed like it would work. -- But would require quite a bit of work
    • I just confirmed that manually loading the xml (after changing the element name with string.Replace) will deserialize any version of the service into the needed objects
  3. Alternatively do a similar thing by modifying the generated proxy:
    • If i could intercept and modify the soap response before the generated proxy tries to deserialize it
    • If I could modify the XmlTypeAttribute of the service at runtime
  4. I've also thought of having a series 开发者_开发问答of interfaces, so each class would have the interfaces of the older class Data3 : IData3, IData2, IData1 Which I'm thinking would allow me to at least cast downward. And put each version into a different namespace.
  5. There is a couple duck typing techniques I have just looked into slightly which might be able to work, but seems less reliable.
  6. Is there any other way to deserialize from multiple element names?


There is no way to do this. The different versions are different. There's no way to know, ahead of time, how similar they are.


I got Option 2 that I mentioned in my original question to work: (This is not a complete example, but should be fairly obvious what you need to modify to get it to work in your situation, also marking this wiki so anyone can simplify this in the future)

Solution Described Here: Manually making the Soap Request, but using all the wsdl.exe generated classes to deserialize and hold the data, after processing the response.

  • Not a simple solution, but it is far more lenient than using the wsdl.exe generated method calls, and any methods that use the the generated classes will still work perfectly
  • I believe its able to load whatever elements are common between the target object and the source response, so if new versions only add fields:
    1. loading from a newer version will have all the data excluding the new fields
    2. loading from an older version, newer fields will be null
  • Another benefit is that you can load and xml string from anywhere (read from disk, uploaded to webpage) into NormalizeSummaryVersion and the rest of the process will work exactly the same, resulting in the objects given its compatible.

Setting up the WebRequest goes like this: (mine was a https webservice with Basic authentication, couldn't get req.Credentials to work correctly so I add that header manually)

WebRequest req = WebRequest.Create(url);
req.Headers.Add("SOAPAction", soapAction);
req.ContentType = "text/xml;";
req.Method = WebRequestMethods.Http.Post;
req.Headers.Add(HttpRequestHeader.Authorization, "Basic " + basicAuthEncoded);

Then write to that stream the xml data for the webmethod: This is the main downfall of this method, I haven't found a reliable way to generate the soap envelope yet, for my service it doesn't seem to care about the version listed in xmlns:ver so I'm using this string with SerializeObject(SearchCriteria) passed into it

//{0} is the soapAction
//{1} is the xml for that call

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ver="fake">
   <soapenv:Header/>
   <soapenv:Body>
      <ver:{0}>
         {1}
      </ver:{0}>
   </soapenv:Body>
</soapenv:Envelope>

Note: Below is my proof of concept code, I'm sure it could be cleaned up and simplified a decent amount.

With that I'm able to read in the xml response from the service. Next I call NormalizeSummaryVersion which renames the possible node name differences, could also process any other nodes or data in this if needed.

public string NormalizeSummaryVersion(string xmlString)
{
    xmlString = Regex.Replace(xmlString,"SummaryData_Version2_2Impl|SummaryData_Version3_3Impl|SummaryData_Version4_4Impl",
                                "SummaryData_Version1_1Impl");

    return xmlString;
}

So now the nodes have a common name and format (extra or missing nodes don't seem to matter, it just ignores them or sets them to default with this method of deserialization)

ProcessLikeService extracts the XmlArray that I want to deserialize out of the soapenv:Envelope elements, and puts it into a new XmlDocument, and I convert that back to a string.

So after NormalizeSummaryVersion and inside of GetData() XmlDocument processedDoc will be this xml, no matter what version the Soap Response was from:

<?xml version="1.0" encoding="utf-16"?>
<searchReturn>
  <SummaryData_Version1_1Impl>
    <customerFirstName>first</customerFirstName>
    <customerLastName>last</customerLastName>
  </SummaryData_Version1_1Impl>
</searchReturn>

And finally I'm able to use a generic XmlDeserialize method to get the objects I want. (My main call for all of this actually returns GetData(xmlString).searchReturn because

[XmlRoot("searchReturn")]
public class SearchReturn
{
    [XmlElement("SummaryData_Version1_1Impl", typeof(SummaryData))]
    public SummaryData[] searchReturn;
}

public SearchReturn GetData(string xmlString)
{
    System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
    doc.LoadXml(xmlString);


    System.Xml.XmlNode DataNode = doc.SelectSingleNode("//searchReturn");

    System.Xml.XmlDocument processedDoc = new System.Xml.XmlDocument();
    processedDoc.AppendChild(processedDoc.ImportNode(DataNode, true));


    SearchReturn data = Deserialize<SearchReturn>(processedDoc);
    return data;
}

And the generic Deserialize method:

public static T Deserialize<T>(XmlDocument xml)
{
    XmlSerializer s = new XmlSerializer(typeof(T));

    using (XmlReader reader = new XmlNodeReader(xml))
    {
        try
        {
            return (T)s.Deserialize(reader);
        }
        catch (Exception)
        {
            throw;
        }
    }
    throw new NotSupportedException();
}


I now think the best option is to use a SoapExtension and set a SoapExtensionAttribute to trigger using that on any methods you need to modify the response for.

  1. Modify the generated code and add the attribute [ModifyResponseExtensionAttribute] to any methods that require the modifications, in your case you might need multiple SoapExtension classes
  2. Add the following classes to your project:

    public class ModifyResponseExtension : SoapExtension
    {
        Stream inStream;
        Stream outStream;
    
        // Save the Stream representing the SOAP request or SOAP response into
        // a local memory buffer.
        public override Stream ChainStream(Stream stream)
        {
            inStream = stream;
            outStream = new MemoryStream();
            return outStream;
        }
    
        //This can get properties out of the Attribute used to enable this
        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
        {
            return null;
        }
    
        //This would have default settings when enabled by config file
        public override object GetInitializer(Type WebServiceType)
        {
            return null;
        }
    
        // Receive the object returned by GetInitializer-- set any options here
        public override void Initialize(object initializer)
        {
        }
    
        //  If the SoapMessageStage is such that the SoapRequest or
        //  SoapResponse is still in the SOAP format to be sent or received,
        //  save it out to a file.
        public override void ProcessMessage(SoapMessage message)
        {
            switch (message.Stage)
            {
                case SoapMessageStage.BeforeSerialize:
                    break;
                case SoapMessageStage.AfterSerialize:
                    //This is after the Request has been serialized, I don't need to modify this so just copy the stream as-is
                    outStream.Position = 0;
                    Copy(outStream, inStream);
                    //Not sure if this is needed (MSDN does not have it) but I like closing things
                    outStream.Close();
                    inStream.Close();
                    break;
                case SoapMessageStage.BeforeDeserialize:
                    //This is before the Response has been deserialized, modify here
                    //Could also modify based on something in the SoapMessage object if needed
                    ModifyResponseMessage();
                    break;
                case SoapMessageStage.AfterDeserialize:
                    break;
            }
        }
    
        private void ModifyResponseMessage()
        {
            TextReader reader = new StreamReader(inStream);
            TextWriter writer = new StreamWriter(outStream);
    
            //Using a StringBuilder for the replacements here
            StringBuilder sb = new StringBuilder(reader.ReadToEnd());
    
            //Modify the stream so it will deserialize with the current version (downgrading to Version1_1 here)
            sb.Replace("SummaryData_Version2_2Impl", "SummaryData_Version1_1Impl")
                .Replace("SummaryData_Version3_3Impl", "SummaryData_Version1_1Impl")
                .Replace("SummaryData_Version4_4Impl", "SummaryData_Version1_1Impl");
            //Replace the namespace
            sb.Replace("http://version2_2", "http://version1_1")
                .Replace("http://version3_3", "http://version1_1")
                .Replace("http://version4_4", "http://version1_1");
    
            //Note: Can output to a log message here if needed, with sb.ToString() to check what is different between the version responses
    
            writer.WriteLine(sb.ToString());
            writer.Flush();
    
            //Not sure if this is needed (MSDN does not have it) but I like closing things
            inStream.Close();
    
            outStream.Position = 0;
        }
    
        void Copy(Stream from, Stream to)
        {
            TextReader reader = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }
    }
    
    // Create a SoapExtensionAttribute for the SOAP Extension that can be
    // applied to an XML Web service method.
    [AttributeUsage(AttributeTargets.Method)]
    public class ModifyResponseExtensionAttribute : SoapExtensionAttribute
    {
        private int priority;
    
        public override Type ExtensionType
        {
            get { return typeof(ModifyResponseExtension); }
        }
    
        public override int Priority
        {
            get { return priority; }
            set { priority = value; }
        }
    }
    

So it is very possible to manually modify the request/responses of the wsdl.exe generated class when needed.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜