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>
Nano Taboada
  • 4,148
  • 11
  • 61
  • 89
  • 1
    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!) – Marc Gravell Jun 19 '11 at 07:33
  • 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). – Nano Taboada Jun 19 '11 at 07:40
  • 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. – Nano Taboada Jun 19 '11 at 07:52
  • SoapFormatter is the (now largely deprecated) one I was thinking of. Re DCS, IIRC to use DataMember it needs to be marked DataContract. – Marc Gravell Jun 19 '11 at 08:00
  • 1
    Also, `lipsum.Foobar.Add` doesn't compile. – Merlyn Morgan-Graham Jun 19 '11 at 08:10
  • @M M-G: true - fixed that. Thanks. – Nano Taboada Jun 19 '11 at 08:32
  • @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.` – Nano Taboada Jun 19 '11 at 09:05

1 Answers1

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.

Merlyn Morgan-Graham
  • 58,163
  • 16
  • 128
  • 183
  • @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 http://msdn.microsoft.com/en-us/library/b29kkt2s(v=vs.71).aspx – Nano Taboada Jun 19 '11 at 09:11