wcf icon indicating copy to clipboard operation
wcf copied to clipboard

Not possible to create Mtom client that has a stream as part of the request object

Open evolvedlight opened this issue 3 years ago • 2 comments

Describe the bug A Soap client requires we send them a string ID and a streamed file together. The default service client is currently for a object that takes a string and a byte[]. Ideally, we'd be able to change this so that it takes a string ID and a Stream, and that Stream is turned into the response. However, at the moment we get the error "In order to use Streams with the MessageContract programming model, the type doTransfer must have a single member with MessageBodyMember attribute and the member type must be Stream"

The request looks like this:

[System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.3")]
    [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
    [System.ServiceModel.MessageContractAttribute(WrapperName="doTransfer", WrapperNamespace="XXX.com/XX/XX/filetransfer", IsWrapped=true)]
    public partial class doTransfer
    {
        
        [System.ServiceModel.MessageBodyMemberAttribute(Namespace="XXX.com/XX/XX/filetransfer", Order=0)]
        [System.Xml.Serialization.XmlElementAttribute(Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
        public string transferId;
        
        [System.ServiceModel.MessageBodyMemberAttribute(Namespace="XXX.com/XX/XX/filetransfer", Order=1)]
        [System.Xml.Serialization.XmlElementAttribute(Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
        public Stream fileData;
        
        public doTransfer()
        {
        }
        
        public doXXX(string stringId, Stream fileData)
        {
            this.transferId = transferId;
            this.fileData = fileData;
        }
    }

To Reproduce Steps to reproduce the behavior: This is more of a question of "is it even possible at all" - but if this doesn't make sense I can create a sample project

Expected behavior It to work and send the data streamed to the web service

Additional context I can't change the server at all, only the client. It's a java soap web service.

evolvedlight avatar Oct 10 '22 13:10 evolvedlight

The behavior seems to match the documentation. From https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/large-data-and-streaming :

As shown in the following message contract, you can have only a single body member in your message contract that is a stream. If you want to communicate additional information with the stream, this information must be a carried in message headers. The message body is exclusively reserved for the stream content.

benken-parasoft avatar Nov 01 '22 17:11 benken-parasoft

As mentioned by @benken-parasoft, this is behaving as designed. I was reading through the code and I don't believe there's a technical reason why this shouldn't be possible other than the current implementation isn't written in a way to support it. It's a non-trivial amount of work to refactor/redesign the current code to do this though so it's not something which would happen in the short term. But I might have a potential workaround for you. Something along these lines might work:

[ServiceContract]
public interface IService1
{
    [OperationContract]
    void DoTransfer(string stringId, DataStream fileData);
}

public class DataStream : IXmlSerializable 
{
    private Stream _data;

    public DataStream(Stream data) { _data = data; }
    public XmlSchema GetSchema() { return null; }
    public void ReadXml(XmlReader reader) { }
    public void WriteXml(XmlWriter writer)
    {
        if (writer is XmlDictionaryWriter dictWriter)
        {
            var streamProvider = new DataStreamProvider(_data);
            writer.WriteValue(streamProvider);
        }
        else
        {
            byte[] dataArray;
            using (MemoryStream ms = new MemoryStream())
            {
                _data.CopyTo(ms);
                dataArray = ms.ToArray();
            }
            writer.WriteBase64(dataArray, 0, dataArray.Length);
        }
    }

    private class DataStreamProvider : IStreamProvider
    {
        Stream _stream;
        internal DataStreamProvider(Stream stream) { _stream = stream; }
        public Stream GetStream() => _stream;
        public void ReleaseStream(Stream stream) { // Noop }
    }
}

With this approach, WCF doesn't use it's own implementation for special handling of streams. The Mtom xml writer is an XmlDictionaryWriter and handles placing items as a separate mime part via XmlDictionaryWriter.WriteValue(IStreamProvider streamProvider). By implementing IXmlSerializable, you are saying you are going to take the responsibility for writing the data directly to the XmlWriter. This code casts it to XmlDictionaryWriter to get access to the relevant WriteValue method.

mconnew avatar Aug 17 '23 22:08 mconnew