将多个 WSDL 版本反序列化为同一个对象

发布于 2024-10-25 18:17:24 字数 1029 浏览 5 评论 0原文

我正在使用另一家公司的网络服务,他们有多个版本正在运行,每个较新的版本只添加了新的字段/对象,但更改了一些元素名称。

我希望能够使用相同代码使用任何版本。

具体来说,在一个版本中,搜索方法返回:

以及在不同版本中:

因此,由于该元素的更改,wsdl.exe 生成的代理现在无法同时使用两者。

  1. 最好的解决方案是让其他公司修复他们的服务以不更改元素名称,但在这种情况下这是不太可能的
  2. 我认为工作解决方案的最佳选择是手动发送和获取 SOAP 请求,并且修改元素名称,然后手动反序列化,到目前为止似乎可行。 ——但是需要做很多工作
    • 我刚刚确认手动加载 xml(在使用 string.Replace 更改元素名称后)会将任何版本的服务反序列化为所需的对象
  3. 或者通过修改生成的内容来执行类似的操作代理人:
    • 如果我可以在生成的代理尝试反序列化肥皂响应之前拦截并修改它
    • 如果我可以在运行时修改服务的 XmlTypeAttribute
  4. 我还考虑过拥有一系列接口,因此每个类都会有旧类 Data3 的接口:IData3、IData2、IData1 我认为这至少允许我向下转换。并将每个版本放入不同的命名空间中。
  5. 我刚刚研究了一些鸭子打字技术,它们可能可行,但似乎不太可靠。
  6. 还有其他方法可以从多个元素名称反序列化吗?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

青瓷清茶倾城歌 2024-11-01 18:17:24

没有办法做到这一点。不同的版本是不同的。没有办法提前知道它们有多相似。

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.

叹沉浮 2024-11-01 18:17:24

我在原来的问题中提到的选项 2 可以工作:(这不是一个完整的示例,但应该相当明显您需要修改什么才能使其在您的情况下工作,同时标记此 wiki,以便任何人都可以简化此操作将来)

此处描述的解决方案:手动发出 Soap 请求,但在处理响应后使用所有 wsdl.exe 生成的类来反序列化并保存数据。

  • 这不是一个简单的解决方案,但它比使用 wsdl.exe 生成的方法调用要宽松得多,并且任何使用生成的类的方法仍然可以完美地工作
  • 我相信它能够加载目标对象和目标对象之间常见的任何元素源响应,因此如果新版本仅添加字段:
    1. 从较新版本加载将包含除新字段之外的所有数据
    2. 从旧版本加载,新字段将为空
  • 另一个好处是您可以从任何地方加载 xml 字符串(从磁盘读取,上传到网页)到 NormalizeSummaryVersion 中,该过程的其余部分将完全相同,从而产生给定其兼容的对象。

设置 WebRequest 的过程如下:(我的是一个具有基本身份验证的 https Web 服务,无法使 req.Credentials 正常工作,因此我手动添加该标头)

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);

然后将 xml 数据写入该流对于 webmethod:这是此方法的主要缺点,我还没有找到生成肥皂信封的可靠方法,对于我的服务,它似乎并不关心 xmlns 中列出的版本:ver 所以我使用这个字符串并传入 SerializeObject(SearchCriteria)

//{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>

注意: 下面是我的概念验证代码,我我确信它可以被清理并简化相当多的量。

这样我就可以读取来自服务的 xml 响应。接下来,我调用 NormalizeSummaryVersion 来重命名可能的节点名称差异,如果需要,还可以处理其中的任何其他节点或数据。

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

    return xmlString;
}

因此,现在节点具有通用的名称和格式(额外或缺失的节点似乎并不重要,它只是忽略它们或使用此反序列化方法将它们设置为默认值)

ProcessLikeService 提取我的 XmlArray想要反序列化 soapenv:Envelope 元素,并将其放入新的 XmlDocument 中,然后将其转换回字符串。

因此,在 NormalizeSummaryVersionGetData() 内部 XmlDocumentprocessedDoc 都将是这个 xml,无论 Soap 响应来自哪个版本:

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

最后我我能够使用通用的 XmlDeserialize 方法来获取我想要的对象。 (我对所有这些的主要调用实际上返回 GetData(xmlString).searchReturn 因为

[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;
}

通用 Deserialize 方法:

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 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();
}
指尖微凉心微凉 2024-11-01 18:17:24

我现在认为最好的选择是使用 SoapExtension 并设置 SoapExtensionAttribute 以在您需要修改响应的任何方法上使用它来触发。

  1. 修改生成的代码并将属性 [ModifyResponseExtensionAttribute] 添加到任何需要修改的方法,在您的情况下,您可能需要多个 SoapExtension 类
  2. 将以下类添加到您的项目:

    公共类ModifyResponseExtension:SoapExtension
    {
        流inStream;
        流出流;
    
        // 将表示 SOAP 请求或 SOAP 响应的 Stream 保存到
        // 本地内存缓冲区。
        公共重写 Stream ChainStream(Stream 流)
        {
            inStream = 流;
            outStream = new MemoryStream();
            返回输出流;
        }
    
        //这可以从用于启用此功能的属性中获取属性
        公共覆盖对象 GetInitializer(LogicalMethodInfo methodInfo,SoapExtensionAttribute 属性)
        {
            返回空值;
        }
    
        //当配置文件启用时,这将具有默认设置
        公共覆盖对象 GetInitializer(Type WebServiceType)
        {
            返回空值;
        }
    
        // 接收GetInitializer返回的对象——在这里设置任何选项
        公共覆盖无效初始化(对象初始值设定项)
        {
        }
    
        // 如果 SoapMessageStage 满足 SoapRequest 或
        // SoapResponse 仍然以 SOAP 格式发送或接收,
        // 将其保存到文件中。
        公共重写 void ProcessMessage(SoapMessage 消息)
        {
            开关(消息.阶段)
            {
                案例 SoapMessageStage.BeforeSerialize:
                    休息;
                案例 SoapMessageStage.AfterSerialize:
                    //这是在请求被序列化之后,我不需要修改它,所以只需按原样复制流
                    输出流位置 = 0;
                    复制(输出流,输入流);
                    //不确定是否需要这个(MSDN没有)但我喜欢关闭东西
                    outStream.Close();
                    inStream.Close();
                    休息;
                案例 SoapMessageStage.BeforeDeserialize:
                    //这是在Response被反序列化之前,修改这里
                    //如果需要的话还可以根据SoapMessage对象中的某些内容进行修改
                    修改响应消息();
                    休息;
                案例 SoapMessageStage.AfterDeserialize:
                    休息;
            }
        }
    
        私有无效修改响应消息()
        {
            TextReader 阅读器 = new StreamReader(inStream);
            TextWriter writer = new StreamWriter(outStream);
    
            //这里使用StringBuilder进行替换
            StringBuilder sb = new StringBuilder(reader.ReadToEnd());
    
            //修改流,以便它将使用当前版本反序列化(此处降级到 Version1_1)
            sb.Replace("SummaryData_Version2_2Impl", "SummaryData_Version1_1Impl")
                .Replace("SummaryData_Version3_3Impl", "SummaryData_Version1_1Impl")
                .Replace("SummaryData_Version4_4Impl", "SummaryData_Version1_1Impl");
            //替换命名空间
            sb.Replace("http://version2_2", "http://version1_1")
                .Replace("http://version3_3", "http://version1_1")
                .Replace("http://version4_4", "http://version1_1");
    
            //注意:如果需要,可以在此处输出到日志消息,使用 sb.ToString() 检查版本响应之间的差异
    
            writer.WriteLine(sb.ToString());
            writer.Flush();
    
            //不确定是否需要这个(MSDN没有)但我喜欢关闭东西
            inStream.Close();
    
            输出流位置 = 0;
        }
    
        void Copy(流自,流至)
        {
            TextReader 阅读器 = new StreamReader(from);
            TextWriter writer = new StreamWriter(to);
            writer.WriteLine(reader.ReadToEnd());
            writer.Flush();
        }
    }
    
    // 为 SOAP 扩展创建一个 SoapExtensionAttribute,可以
    // 应用于 XML Web 服务方法。
    [AttributeUsage(AttributeTargets.Method)]
    公共类修改响应扩展属性:SoapExtensionAttribute
    {
        私有 int 优先级;
    
        公共覆盖类型 ExtensionType
        {
            获取 { 返回 typeof(ModifyResponseExtension); }
        }
    
        公共覆盖 int 优先级
        {
            获取{返回优先级; }
            设置{优先级=值; }
        }
    }
    

因此,很有可能在需要时手动修改 wsdl.exe 生成的类的请求/响应。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文