Extension method to serialize generic objects as a SOAP formatted stream
Asked Answered
G

1

9

I'm having a hard time trying to figure out a generic extension method that would serialize a given object as SOAP formatted. The actual implementation looks somewhat like this:

Foobar.cs

[Serializable, XmlRoot("foobar"), DataContract]
public class Foobar
{
    [XmlAttribute("foo"), DataMember]
    public string Foo { get; set; }

    [XmlAttribute("bar"), DataMember]
    public string Bar { get; set; }

    public Foobar() {}
}

Lipsum.cs

[Serializable, XmlRoot("lipsum"), XmlType("lipsum"), DataContract]
public class Lipsum
{
    private List<Foobar> lipsum = new List<Foobar>();

    [XmlElement("foobar"), DataMember]
    public List<Foobar> Lipsum { get { return lipsum; } }
}

}

Extensions.cs

public static void SerializeToSoap<T>(this Stream target, T source)
{
    XmlTypeMapping xmlTypeMapping = (new SoapReflectionImporter().ImportTypeMapping(typeof(T)));
    XmlSerializer xmlSerializer = new XmlSerializer(xmlTypeMapping);
    xmlSerializer.Serialize(target, source);
}

Program.cs

static void Main()
{
    Lipsum lipsum = new Lipsum();
    lipsum.Lipsum.Add(
        new Foobar()
        {
            Foo = "Lorem",
            Bar = "Ipsum"
        }
    );

    using (MemoryStream persistence = new MemoryStream())
    {
        persistence.SerializeToSoap<Lipsum>(lipsum);
        Console.WriteLine(Encoding.Default.GetString(persistence.ToArray()));
        Console.WriteLine(Environment.NewLine);
    }
}

EXCEPTION

System.InvalidOperationException: Token StartElement in state Epilog would result in an invalid XML document.
   at System.Xml.XmlTextWriter.AutoComplete(Token token)
   at System.Xml.XmlTextWriter.WriteStartElement(String prefix, String localName, String ns)
   at System.Xml.Serialization.XmlSerializationWriter.WriteStartElement(String name, String ns, Object o, Boolean writePrefixed, XmlSerializerNamespaces xmlns)
   at System.Xml.Serialization.XmlSerializationWriter.WriteArray(String name, String ns, Object o, Type type)
   at System.Xml.Serialization.XmlSerializationWriter.WriteReferencedElement(String name, String ns, Object o, Type ambientType)
   at System.Xml.Serialization.XmlSerializationWriter.WriteReferencedElements()
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriter1.
   Write4_Lipsum(Object o)

On the other hand both XML and JSON serialization (using XmlSerializer and DataContractJsonSerializer respectively) is working fine with the following expected results:

<?xml version="1.0"?>
<lipsum xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <foobar foo="Lorem" bar="Ipsum" />
</lipsum>

{"Lipsum":[{"Foo":"Lorem","Bar":"Ipsum"}]}

Any advice will be sincerely appreciated. Thanks much in advance.

UPDATE

As one of the commenters remarked, there's a SoapFormatter class but given that I was aware it can't deal with generic types haven't included that snippet. So anyway this would be the code for that scenario:

public static void SerializeToSoap<T>(this Stream target, T source)
{
    SoapFormatter soapFormatter = new SoapFormatter();
    soapFormatter.Serialize(target, source);
}

Which throws the following exception:

Exception caught: Soap Serializer does not support serializing Generic Types : System.Collections.Generic.List`1[Foobar].

UPDATE 2

Following the lead given by Merlyn Morgan-Graham I've been tried to feed the SoapFormatter with a non-generic object, so after a little juggling with MemoryStream I've ended up with this:

using (MemoryStream xmlStream = new MemoryStream())
{
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
    xmlSerializer.Serialize(xmlStream, lipsum);

    using (MemoryStream soapStream = new MemoryStream()) 
    {
        SoapFormatter soapFormatter = new SoapFormatter();
        soapFormatter.Serialize(soapStream, Encoding.Default.GetString(xmlStream.ToArray()));
        Console.WriteLine(Encoding.Default.GetString(soapStream.ToArray()));
        Console.WriteLine(Environment.NewLine);
    }
}

Which surprisingly enough, character entities aside, outputs a decent SOAP message:

<SOAP-ENV:Envelope
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0"
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <SOAP-ENC:string id="ref-1">
            &#60;?xml version=&#34;1.0&#34;?&#62;&#60;lipsum
            xmlns:xsi=&#34;http://www.w3.org/2001/XMLSchema-instance&#34;
            xmlns:xsd=&#34;http://www.w3.org/2001/XMLSchema&#34;&#62;&#60;
            foobar foo=&#34;Lorem&#34; bar=&#34;Ipsum&#34;/&#62;&#60;/lipsum&#62;
        </SOAP-ENC:string>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Gatling answered 19/6, 2011 at 7:0 Comment(7)
pedant: that is XML serialisation; there is a SOAP serializer too, but it is obsoleted (thankfully). Also; XmlSerializer doesn't need [Serializable] or [DataMember]. I'll read the question too, honest :) (edit: interesting; don't know!)Zuber
Thanks for the comment - I'd really like to know about the SOAP Serializer, I know the SoapFormatter but it can't handle generic types (I'm updating the sample with that snippet).Gatling
Thanks for the head's up - I'll remove the [Serializable] but I'll keep the [DataMember] as the same class might or might not be serialized to JSON and XML via DataContractSerializer and DataContractJsonSerializer respectively.Gatling
SoapFormatter is the (now largely deprecated) one I was thinking of. Re DCS, IIRC to use DataMember it needs to be marked DataContract.Zuber
Also, lipsum.Foobar.Add doesn't compile.Canarese
@M M-G: true - fixed that. Thanks.Gatling
@Marc Gravell: Curiously enough if I remove the [Serializable] attribute I get the following exception: Type 'Namespace.Of.Lipsum' in Assembly 'Namespace.Of.Project, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.Gatling
C
6

I (half) solved the problem by wrapping the serialization with another element, ala this forum post: http://forums.asp.net/p/1510998/3607468.aspx, and this blog post: http://sandblogaspnet.blogspot.com/2009/07/serialization-in-net-3.html

public static void SerializeToSoap<T>(this Stream target, T source)
{
    XmlTypeMapping xmlTypeMapping = (new SoapReflectionImporter().ImportTypeMapping(typeof(T)));
    XmlSerializer xmlSerializer = new XmlSerializer(xmlTypeMapping);
    using (var xmlWriter = new XmlTextWriter(target, Encoding.UTF8))
    {
        xmlWriter.WriteStartDocument();
        xmlWriter.WriteStartElement("root");
        xmlSerializer.Serialize(xmlWriter, source);
        xmlWriter.WriteFullEndElement();
    }
}

The document looks really strange, though, and doesn't contain a SOAP Envelope. Admittedly I know very little about SOAP, so maybe you know how to solve those problems :)

Edit:

Looking at reflected source in System.Web.Services.Protocols.SoapHttpClientProtocol, it looks like the SoapReflectionImporter and XmlSerializer are used, but the SOAP envelope and body are generated directly within the serialization code. No special helpers are exposed to the user. So you'll probably have to wrap that part of the message with custom code.

On the plus side, the code looks fairly simple - just write the correct elements with the proper namespaces/attributes.

Canarese answered 19/6, 2011 at 8:38 Comment(1)
@M M-G: Thanks a lot for the contribution - I'm in a somewhat similar position regarding SOAP but I'll try to decorate your code with this msdn.microsoft.com/en-us/library/b29kkt2s(v=vs.71).aspxGatling

© 2022 - 2024 — McMap. All rights reserved.