I found a way that might not be the most elegant, but gets the job done. The idea is to delegate all the work to a custom SoapExtension-derived class, and actually do nothing in the WebMethod itself - the WebMethod is just there as an endpoint for the call:
[WebMethod]
public Foo GimmeFoo()
{
return null;
}
But here's the magic: you write a SoapExtension that intercepts all SOAP traffic and, when the SoapMessageStage.AfterSerialize stage arrives, you stick in there your already-serialized payload:
public class SerializationPassThrough : SoapExtension
{
private Stream oldStream;
private Stream newStream;
// Other overrides...
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
// ...
break;
case SoapMessageStage.AfterSerialize:
string newOutput = ReadPreCookedResponseFromDB();
RewriteOutput(newOutput);
break;
case SoapMessageStage.BeforeDeserialize:
// ...
break;
case SoapMessageStage.AfterDeserialize:
// ...
break;
default:
throw new Exception("invalid stage");
}
}
private void RewriteOutput(string output)
{
newStream.Position = 0;
StreamWriter sw = new StreamWriter(newStream);
sw.Write(output);
sw.Flush();
newStream.Position = 0;
Copy(newStream, oldStream);
newStream.Position = 0;
}
private void Copy(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
string toWrite = reader.ReadToEnd();
writer.WriteLine(toWrite);
writer.Flush();
}
}
The final touch: you need to instruct the ASP.NET runtime to use your SoapExtension. You can do so via an attribute on the WebMethod, or in web.config (which is what I did):
<system.web>
<webServices>
<soapExtensionTypes>
<add type="SoapSerializationTest.SerializationPassThrough, SoapSerializationTest" priority="1" />
</soapExtensionTypes>
</webServices>
<system.web>
The original code samples from which I derived these snippets are at http://msdn.microsoft.com/en-us/library/7w06t139(VS.85).aspx.