问题
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?
回答1:
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.
回答2:
I ran into this post while trying to write a SoapExtension that would log my web service activity at the soap level. This script is tested and works to log activity to a text file when used on the server side. The client side is not supported.
To use just replace 'C:\Your Destination Directory' with the actual directory you want to use for log file writes.
This work cost me an entire day so I am posting it in hopes that others won't have to do the same.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.IO;
using System.Net;
using System.Reflection;
public class WebServiceActivityLogger : SoapExtension
{
string fileName = null;
public override object GetInitializer(Type serviceType)
{
return Path.Combine(@"C:\Your Destination Directory", serviceType.Name + " - " + DateTime.Now.ToString("yyyy-MM-dd HH.mm") + ".txt");
}
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
return Path.Combine(@"C:\Your Destination Directory", methodInfo.DeclaringType.Name + " - " + DateTime.Now.ToString("yyyy-MM-dd HH.mm") + ".txt");
}
public override void Initialize(object initializer)
{
fileName = initializer as string;
}
Dictionary<int, ActivityLogData> logDataDictionary = new Dictionary<int, ActivityLogData>();
private ActivityLogData LogData
{
get
{
ActivityLogData rtn;
if (!logDataDictionary.TryGetValue(System.Threading.Thread.CurrentThread.ManagedThreadId, out rtn))
return null;
else
return rtn;
}
set
{
int threadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
if(logDataDictionary.ContainsKey(threadId))
{
if (value != null)
logDataDictionary[threadId] = value;
else
logDataDictionary.Remove(threadId);
}
else if(value != null)
logDataDictionary.Add(threadId, value);
}
}
private class ActivityLogData
{
public string methodName;
public DateTime startTime;
public DateTime endTime;
public Stream transportStream;
public Stream accessStream;
public string inputSoap;
public string outputSoap;
public bool endedInError;
}
public override Stream ChainStream(Stream stream)
{
if (LogData == null)
LogData = new ActivityLogData();
var logData = LogData;
logData.transportStream = stream;
logData.accessStream = new MemoryStream();
return logData.accessStream;
}
public override void ProcessMessage(SoapMessage message)
{
if (LogData == null)
LogData = new ActivityLogData();
var logData = LogData;
if (message is SoapServerMessage)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeDeserialize:
//Take the data from the transport stream coming in from the client
//and copy it into inputSoap log. Then reset the transport to the beginning
//copy it to the access stream that the server will use to read the incoming message.
logData.startTime = DateTime.Now;
logData.inputSoap = GetSoapMessage(logData.transportStream);
Copy(logData.transportStream, logData.accessStream);
logData.accessStream.Position = 0;
break;
case SoapMessageStage.AfterDeserialize:
//Capture the method name after deserialization and it is now known. (was buried in the incoming soap)
logData.methodName = GetMethodName(message);
break;
case SoapMessageStage.BeforeSerialize:
//Do nothing here because we are not modifying the soap
break;
case SoapMessageStage.AfterSerialize:
//Take the serialized soap data captured by the access stream and
//write it into the log file. But if an error has occurred write the exception details.
logData.endTime = DateTime.Now;
logData.accessStream.Position = 0;
if (message.Exception != null)
{
logData.endedInError = true;
if (message.Exception.InnerException != null && message.Exception is System.Web.Services.Protocols.SoapException)
logData.outputSoap = GetFullExceptionMessage(message.Exception.InnerException);
else
logData.outputSoap = GetFullExceptionMessage(message.Exception);
}
else
logData.outputSoap = GetSoapMessage(logData.accessStream);
//Transfer the soap data as it was created by the service
//to the transport stream so it is received the client unmodified.
Copy(logData.accessStream, logData.transportStream);
LogRequest(logData);
break;
}
}
else if (message is SoapClientMessage)
{
throw new NotSupportedException("This extension must be ran on the server side");
}
}
private void LogRequest(ActivityLogData logData)
{
try
{
//Create the directory if it doesn't exist
var directoryName = Path.GetDirectoryName(fileName);
if (!Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
using (var fs = new FileStream(fileName, FileMode.Append, FileAccess.Write))
{
var sw = new StreamWriter(fs);
sw.WriteLine("--------------------------------------------------------------");
sw.WriteLine("- " + logData.methodName + " executed in " + (logData.endTime - logData.startTime).TotalMilliseconds.ToString("#,###,##0") + " ms");
sw.WriteLine("--------------------------------------------------------------");
sw.WriteLine("* Input received at " + logData.startTime.ToString("HH:mm:ss.fff"));
sw.WriteLine();
sw.WriteLine("\t" + logData.inputSoap.Replace("\r\n", "\r\n\t"));
sw.WriteLine();
if (!logData.endedInError)
sw.WriteLine("* Output sent at " + logData.endTime.ToString("HH:mm:ss.fff"));
else
sw.WriteLine("* Output ended in Error at " + logData.endTime.ToString("HH:mm:ss.fff"));
sw.WriteLine();
sw.WriteLine("\t" + logData.outputSoap.Replace("\r\n", "\r\n\t"));
sw.WriteLine();
sw.Flush();
sw.Close();
}
}
finally
{
LogData = null;
}
}
private void Copy(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
private string GetMethodName(SoapMessage message)
{
try
{
return message.MethodInfo.Name;
}
catch
{
return "[Method Name Unavilable]";
}
}
private string GetSoapMessage(Stream message)
{
if(message == null || message.CanRead == false)
return "[Message Soap was Unreadable]";
var rtn = new StreamReader(message).ReadToEnd();
message.Position = 0;
return rtn;
}
private string GetFullExceptionMessage(System.Exception ex)
{
Assembly entryAssembly = System.Reflection.Assembly.GetEntryAssembly();
string Rtn = ex.Message.Trim() + "\r\n\r\n" +
"Exception Type: " + ex.GetType().ToString().Trim() + "\r\n\r\n" +
ex.StackTrace.TrimEnd() + "\r\n\r\n";
if (ex.InnerException != null)
Rtn += "Inner Exception\r\n\r\n" + GetFullExceptionMessage(ex.InnerException);
return Rtn.Trim();
}
}
Add this to the web.config of your server.
<system.web>
<webServices>
<soapExtensionTypes>
<add type="[Your Namespace].WebServiceActivityLogger, [Assembly Namespace], Version=1.0.0.0, Culture=neutral" priority="1" group="0" />
</soapExtensionTypes>
</webServices>
</system.web>
回答3:
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.
回答4:
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.
来源:https://stackoverflow.com/questions/1211407/soap-extension-stream-empty-after-serialization