DataContractSerializer serializing an IXmlSerializable doesn't support XmlWriter.WriteValue(object)

Open
#117,842 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Stale
Tech stack
csharp
Domain
api, backend

Research direction

Start by reading src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlSerializableWriter.cs, especially its WriteValue(object value) handling, and compare it with the inner XmlMtomWriter behavior described in the issue. Reproduce the failure with the ByteArrayWrapper example and XmlDictionaryWriter; done means DataContractSerializer can pass the IStreamProvider through without the InvalidCastException.

Written by the indexing model from the issue text.

Description

area-Serialization
Description

This is an mtom scenario. When writing with an XmlMtomWriter, if you call WriteValue(object value) and the object you pass implements IStreamProvider, then it stores the IStreamProvider instance and writes out a place holder in the xml. When you call WriteEndElement on the final element, it uses IStreamProvider.GetStream() to get the payload for a multi-part mime part.

When serializing using XmlSerializer, calling Serialize and passing in an XmlMtomWriter, if a class in the object graph implements IXmlSerializable, it calls IXmlSerializable.WriteXml passing the XmlMtomWriter. This enables a class to pass an IStreamProvider to WriteValue(object value) and the correct mtom output to be generated.

When serializing with DataContractSerializer and having implemented IXmlSerializable in a type in the object graph, the XmlWriter passed to IXmlSerializable.WriteXml is not the same XmlWriter as was passed to DataContractSerializer.WriteObject. It's wrapped in a XmlSerializableWriter instance. This class passes through most of the calls to the inner XmlWriter, which is the XmlMtomWriter that was passed to WriteObject, but not with the method WriteValue(object value). This method falls through to the base XmlWriter class which attempts to convert the object to a String via a convertor, and then would pass that string to WriteString (if it didn't currently thrown an exception) which then passes the string through to the inner writer. By not overriding WriteValue(object value), serialization using IStreamProvider is not possible with DataContractSerializer.

Reproduction Steps

Serialize the following object using the xml writer from XmlDictionaryWriter.CreateTextWriter. All the XmlDictionaryWriter implementations support writing IStreamProvider via WriteValue(object value).

public class ByteArrayWrapper : IStreamProvider, IXmlSerializable
{
    public byte[] Data { get; set; } = Array.Empty<byte>();
    public Stream GetStream()
    {
        return new FixedMemoryStream(Data);
    }

    public void ReleaseStream(Stream stream)
    {
        stream.Dispose();
    }

    public XmlSchema? GetSchema() => null;

    public void ReadXml(XmlReader reader)
    {
        reader.ReadStartElement();
        var buffer = new byte[8192]; // 8KB buffer size
        using (var ms = new MemoryStream())
        {
            int bytesRead;
            while ((bytesRead = reader.ReadContentAsBase64(buffer, 0, buffer.Length)) > 0)
            {
                ms.Write(buffer, 0, bytesRead);
            }
            Data = ms.ToArray();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteValue(this);
    }

    // Implicit conversion from ByteArrayWrapper to byte[]
    public static implicit operator byte[](ByteArrayWrapper wrapper)
    {
        return wrapper?.Data ?? Array.Empty<byte>();
    }

    // Implicit conversion from byte[] to ByteArrayWrapper
    public static implicit operator ByteArrayWrapper(byte[] data)
    {
        return new ByteArrayWrapper { Data = data ?? Array.Empty<byte>() };
    }
}

public class FixedMemoryStream : MemoryStream
{
    public FixedMemoryStream(byte[] buffer) : base(buffer) { }
    public FixedMemoryStream(byte[] buffer, int index, int count) : base(buffer, index, count) { }
    public FixedMemoryStream(int capacity) : base(capacity) { }
    public FixedMemoryStream() : base() { }
}
Expected behavior

The XmlDictionaryWriter passed to DataContractSerializer.WriteObject has WriteValue(object value) called and doesn't throw an exception (unless the type happens to be convertable to a string).

Actual behavior

The serializer throws:

Unhandled exception. System.InvalidCastException: Xml type 'List of xdt:untypedAtomic' does not support a conversion from Clr type 'ByteArrayWrapper' to Clr type 'String'.
Regression?

I don't believe this is a regression.

Known Workarounds

Use XmlSerializer instead. This could be a big deal for WCF if you have a large contract.

Configuration

No response

Other information

The use of FixedMemoryStream is to work around #117789 which breaks this scenario using a regular MemoryStream with WCF. It's probably replaceable with MemoryStream outside of WCF if you aren't using BufferedStream.

Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/runtime

All issues in dotnet/runtime

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.