views:

390

answers:

3

I have had this problem for the last day. I have created a SOAP Extension following the MSDN articles and a load of blog posts but I just can't get it to work. Ok Some code:

public class EncryptionExtension : SoapExtension
{

    Stream _stream;
    public override object GetInitializer(Type serviceType)
    {
        return typeof(EncryptionExtension);
    }

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return attribute;
    }

    public override void Initialize(object initializer)
    {
    }

    public override void ProcessMessage(SoapMessage message)
    {

        switch (message.Stage)
        {

            case SoapMessageStage.BeforeSerialize:
                break;

            case SoapMessageStage.AfterSerialize:
                break;

            case SoapMessageStage.BeforeDeserialize:
                break;

            case SoapMessageStage.AfterDeserialize:
                break;
            default:
                throw new Exception("invalid stage");
        }

    }
    public override Stream ChainStream(Stream stream)
    {
        _stream = stream;
        return stream;
    }
}

There is also an attribute class:

[AttributeUsage(AttributeTargets.Method)]
public class EncryptionExtensionAttribute : SoapExtensionAttribute
{

    public override Type ExtensionType
    {
        get { return typeof(EncryptionExtension); }
    }

    public override int Priority
    {
        get;
        set;
    }
}

So when the message comes in I can see the inbound SOAP request when I debug at the BeforeDeserialization and AfterDeserialization, which is great. My web service method is then called. Which is simply:

[WebMethod()]
[EncryptionExtension]
public string HelloWorld()
{
    return "Hello world";
}

The process then hops back into my SoapExtension. Putting break points at BeforeSerialization and AfterSerialization I see that the outbound stream contains nothing. I am not surprised that it is empty on the BeforeSerialization but i am surprised that it is empty at AfterSerialization. This creates a problem because I need to get hold of the outbound stream so I can encrypt it.

Can someone tell me why the outbound stream is empty? I have followed this MSDN article which indiciates it shouldn't be http://msdn.microsoft.com/en-us/library/ms972410.aspx. Am I missing some configuration or something else?

A: 

The only way I've ever gotten a SOAP Extension to work is to start with the MSDN example, and get the example to work. Only once it's working, I then change it, little by little, testing each step along the way, until it does what I want.

That may even tell me what I did wrong, but it's never been enough for me to remember for next time. Usually something to do with Streams, though.

John Saunders
Yeah, I've noticed that the example in the SoapExtension class overview doc seems to be correct (AFAICT), but the more complete doc describing the whole process contradicts the usage of the streams passed to and returned by ChainStream. My testing found that the example had them right, not the general doc.
Rob Parker
@Rob, if you found a documentation error, I hope you reported it to Microsoft.
John Saunders
A: 

In order to be able to manipulate output, you'll need to do more in the ChainStream method than just just returning the same stream.

You'll also have to actually DO something in the ProcessMessage method. There is nothing happening there in your provided code.

This is a good read on SOAP Extensions: http://hyperthink.net/blog/inside-of-chainstream/. Be sure to also read the comments about better naming than oldStream and NewStream. Personally, calling them wireStream and appStream, make things much clearer to me.

chyne
I tried this example before posting. The default behaviour of the SOAPExtension classes ChainStream method is to just return the stream. Which is what i have done, the only reason I put it in a variable is so that I can see the contents of the stream when I debug.
Michael Edwards
A: 

I found this question among the top hits for a google search on "SoapExtension MSDN" (which also finds the doc with example code as the top hit), so here are some helpful suggestions to anyone else trying to make sense of the sometimes confusing or contradictory docs on coding Soap extensions.

If you are modifying the serialized message (as a stream) you need to create and return a different stream from the ChainStream override. Otherwise, you're saying that your extension doesn't modify the stream and just lets it pass through. The example uses a MemoryStream, and that's probably what you have to use because of the weird design: When ChainStream is called you don't know if you are sending or receiving, so you have to be prepared to handle it either way. I think even if you only process it in one direction you still have to handle the other direction and copy the data from one stream to the other anyway because you are inserting yourself in the chain without knowing which way it is.

private Stream _transportStream; // The stream closer to the network transport.
private MemoryStream _accessStream; // The stream closer to the message access.

public override Stream ChainStream(Stream stream)
{
    // You have to save these streams for later.
    _transportStream = stream;
    _accessStream = new MemoryStream();
    return _accessStream;
}

Then you have to handle the AfterSerialize and BeforeDeserialize cases in ProcessMessage. I have them calling ProcessTransmitStream(message) and ProcessReceivedStream(message) respectively to help keep the process clear.

ProcessTransmitStream takes its input from _accessStream (after first resetting the Postion of this MemoryStream to 0) and writes its output to _transportStream--which may allow very limited access (no seek, etc), so I suggest processing first into a local MemoryStream buffer and then copying that (after resetting its Postion to 0) into the _transportStream. (Or if you process it into a byte array or string you can just write from that directly into the _transportStream. My use case was compression/decompression so I'm biased towards handling it all as streams.)

ProcessReceivedStream takes its input from _transportStream and writes its output to _accessStream. In this case you should probably first copy the _transportStream into a local MemoryStream buffer (and then reset the buffer's Position to 0) which you can access more conveniently. (Or you can just read the entire _transportStream directly into a byte array or other form if that's how you need it.) Make sure you reset the _accessStream.Position = 0 before returning so that it is ready for the next link in the chain to read from it.

That's for changing the serialized stream. If you aren't changing the stream then you should not override ChainStream (thus taking your extension out of the chain of stream processing). Instead you would do your processing in the BeforeSerialize and/or AfterDeserialize stages. In those stages you don't modify or access the streams but instead work on the message object itself such as adding a custom SoapHeader to the message.Headers collection in the BeforeSerialize stage.

The SoapMessage class itself is abstract, so what you really get is either a SoapClientMessage or a SoapServerMessage. The docs say you get a SoapClientMessage on the client side and a SoapServerMessage on the server side (experimenting in the debugger should be able to confirm or correct that). They seem pretty similar in terms of what you can access, but you have to cast to the right one to access it properly; using the wrong one would fail, and the base SoapMessage type declared for the parameter to ProcessMessage doesn't give you access to everything.

I haven't looked at the attribute stuff yet (it won't be a part of what I'm coding), so I can't help with how to use that part.

Rob Parker