Регистрация SOAP-запроса и ответа на стороне сервера
Я пытаюсь создать службу ведения журналов для всех вызовов методов SOAP в моем веб-сервисе ASP.NET. Я просматривал сообщения журнала SOAP от консольного приложения и пошаговое руководство по расширениям SOAP на MSDN ( http://msdn.microsoft.com/en-us/library/s25h0swd%28v=vs.100%29.aspx). но они, кажется, не покрывают это полностью.
Я не хочу изменять сообщение SOAP, просто зарегистрируйте его в таблице базы данных. То, что я пытаюсь сделать, это прочитать поток сообщений SOAP, разобрать его как XML, записать XML и позволить вызову идти своим чередом. Но когда я читаю поток, он расходуется / утилизируется. Я попытался скопировать содержимое потока, чтобы не прерывать поток.
По прохождению ProcessMessage
Метод должен выглядеть примерно так:
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
// Write the SOAP message out to a file.
WriteOutput( message );
break;
case SoapMessageStage.BeforeDeserialize:
// Write the SOAP message out to a file.
WriteInput( message );
break;
case SoapMessageStage.AfterDeserialize:
break;
default:
throw new Exception("invalid stage");
}
}
Мне удалось разобрать поток без проблем во время BeforeDeserialize
этап, но потом ProcessMessage
снова вызывается в AfterSerialize
этап и к тому времени поток используется и больше не содержит никаких данных.
Согласно модификации сообщений SOAP с использованием расширений SOAP ( http://msdn.microsoft.com/en-us/library/esw638yk%28v=vs.100%29.aspx), вызов SOAP проходит через следующие шаги:
Серверная сторона получает сообщение с запросом и готовит ответ
- ASP.NET на веб-сервере получает сообщение SOAP.
- Новый экземпляр расширения SOAP создается на веб-сервере.
- На веб-сервере, если это расширение SOAP выполняется впервые с этой веб-службой на стороне сервера, метод GetInitializer вызывается на расширении SOAP, работающем на сервере.
- Метод Initialize вызывается.
- Метод ChainStream вызывается.
- Метод ProcessMessage вызывается с SoapMessageStage, установленным в BeforeDeserialize.
- ASP.NET десериализует аргументы в XML.
- Метод ProcessMessage вызывается с SoapMessageStage, установленным в AfterDeserialize.
- ASP.NET создает новый экземпляр класса, реализующий веб-службу, и вызывает метод веб-службы, передавая десериализованные аргументы. Этот объект находится на том же компьютере, что и веб-сервер.
- Метод веб-службы выполняет свой код, в конечном итоге устанавливая возвращаемое значение и любые параметры out.
- Метод ProcessMessage вызывается с SoapMessageStage, установленным в BeforeSerialize.
- ASP.NET на веб-сервере сериализует возвращаемое значение и параметры в XML.
- Метод ProcessMessage вызывается с SoapMessageStage, установленным в AfterSerialize.
- ASP.NET отправляет ответное сообщение SOAP по сети обратно клиенту веб-службы.
Шаг 6 выполняется правильно, и SOAP XML регистрируется. Тогда он не должен делать ничего больше, пока сервер не обработает вызов, не сделает то, что ему нужно (шаг 10), и не вернет ответ (шаг 13). Вместо этого он сразу звонит ProcessMessage
снова в AfterSerialize
этап, но на этот раз поток уже потрачен и выдает исключение, когда я пытаюсь войти в систему.
Согласно прохождению я должен использовать ChainStream
метод, и он должен быть запущен в шаге 5 выше. Когда я звоню, он запускается дважды, один раз перед BeforeDeserialize
и однажды AfterSerialize
,
Я попытался скопировать поток сообщений в отдельный поток и использовать его для регистрации, а также для установки какого-либо статуса, если BeforeDeserialize
уже запустили, но проблема все еще сохраняется.
Мне все еще нужен код в AfterSerialize
обрабатывать ответ, который отправляется обратно клиенту. Но если я попытаюсь удалить свой код в AfterSerialize
и только запустить код в BeforeDeserialize' I get a
HTTP 400: неверный запрос.
Все это происходит до фактического вызова метода, поэтому я даже не дохожу до кода внутри метода (шаг 10).
2 ответа
Мое решение основано на mikebridge, но мне пришлось сделать несколько изменений. Инициализаторы должны были быть включены, и возникали исключения, если вы пытались получить доступ к информации о сообщениях мыла на стадии, когда она была недоступна.
public class SoapLoggingExtension : SoapExtension
{
private Stream _originalStream;
private Stream _workingStream;
private static String _initialMethodName;
private static string _methodName;
private static String _xmlResponse;
/// <summary>
/// Side effects: saves the incoming stream to
/// _originalStream, creates a new MemoryStream
/// in _workingStream and returns it.
/// Later, _workingStream will have to be created
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public override Stream ChainStream(Stream stream)
{
_originalStream = stream;
_workingStream = new MemoryStream();
return _workingStream;
}
/// <summary>
/// Process soap message
/// </summary>
/// <param name="message"></param>
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
//Get soap call as a xml string
var xmlRequest = GetSoapEnvelope(_workingStream);
//Save the inbound method name
_methodName = message.MethodInfo.Name;
CopyStream(_workingStream, _originalStream);
//Log call
LogSoapRequest(xmlRequest, _methodName, LogObject.Direction.OutPut);
break;
case SoapMessageStage.BeforeDeserialize:
CopyStream(_originalStream, _workingStream);
//Get xml string from stream before it is used
_xmlResponse = GetSoapEnvelope(_workingStream);
break;
case SoapMessageStage.AfterDeserialize:
//Method name is only available after deserialize
_methodName = message.MethodInfo.Name;
LogSoapRequest(_xmlResponse, _methodName, LogObject.Direction.InPut);
break;
}
}
/// <summary>
/// Returns the XML representation of the Soap Envelope in the supplied stream.
/// Resets the position of stream to zero.
/// </summary>
private String GetSoapEnvelope(Stream stream)
{
stream.Position = 0;
StreamReader reader = new StreamReader(stream);
String data = reader.ReadToEnd();
stream.Position = 0;
return data;
}
private void CopyStream(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
public override object GetInitializer(Type serviceType)
{
return serviceType.FullName;
}
//Never needed to use this initializer, but it has to be implemented
public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
{
throw new NotImplementedException();
//return ((TraceExtensionAttribute)attribute).Filename;
}
public override void Initialize(object initializer)
{
if (String.IsNullOrEmpty(_methodName))
{
_initialMethodName = _methodName;
_waitForResponse = false;
}
}
private void LogSoapRequest(String xml, String methodName, LogObject.Direction direction)
{
String connectionString = String.Empty;
String callerIpAddress = String.Empty;
String ipAddress = String.Empty;
try
{
//Only log outbound for the response to the original call
if (_waitForResponse && xml.IndexOf("<" + _initialMethodName + "Response") < 0)
{
return;
}
if (direction == LogObject.Direction.InPut) {
_waitForResponse = true;
_initialMethodName = methodName;
}
connectionString = GetSqlConnectionString();
callerIpAddress = GetClientIp();
ipAddress = GetClientIp(HttpContext.Current.Request.UserHostAddress);
//Log call here
if (!String.IsNullOrEmpty(_methodName) && xml.IndexOf("<" + _initialMethodName + "Response") > 0)
{
//Reset static values to initial
_methodName = String.Empty;
_initialMethodName = String.Empty;
_waitForResponse = false;
}
}
catch (Exception ex)
{
//Error handling here
}
}
private static string GetClientIp(string ip = null)
{
if (String.IsNullOrEmpty(ip))
{
ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
}
if (String.IsNullOrEmpty(ip) || ip.Equals("unknown", StringComparison.OrdinalIgnoreCase))
{
ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
}
if (ip == "::1")
ip = "127.0.0.1";
return ip;
}
}
Переменная methodName используется для определения того, на какой входящий вызов мы ожидаем ответа. Это, конечно, необязательно, но в моем решении я делаю несколько звонков на другие веб-сервисы, но я хочу зарегистрировать только ответ на первый звонок.
Вторая часть заключается в том, что вам нужно добавить правильные строки в ваш файл web.config. Очевидно, он чувствителен к тому, что не включает определение типа класса целиком (в этом примере определено только имя класса, что не сработало. Класс никогда не инициализировался.):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add group="High" priority="1" type="WsNs.SoapLoggingExtension, WsNs, Version=1.0.0.0, Culture=neutral" />
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
Вот мой первый снимок, вдохновленный этим и этим.
SoapExtension имеет всевозможные побочные эффекты и скрытые временные зависимости при работе с потоком и когда переменные инициализируются или неинициализируются, так что это хрупкий код. Я обнаружил, что ключ заключается в том, чтобы скопировать исходный поток в поток памяти, а затем снова вернуться в точно нужные моменты.
public class SoapLoggingExtension : SoapExtension
{
private Stream _originalStream;
private Stream _workingStream;
private string _methodName;
private List<KeyValuePair<string, string>> _parameters;
private XmlDocument _xmlResponse;
private string _url;
/// <summary>
/// Side effects: saves the incoming stream to
/// _originalStream, creates a new MemoryStream
/// in _workingStream and returns it.
/// Later, _workingStream will have to be created
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public override Stream ChainStream(Stream stream)
{
_originalStream = stream;
_workingStream = new MemoryStream();
return _workingStream;
}
/// <summary>
/// AUGH, A TEMPLATE METHOD WITH A SWITCH ?!?
/// Side-effects: everywhere
/// </summary>
/// <param name="message"></param>
public override void ProcessMessage(SoapMessage message)
{
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
var xmlRequest = GetSoapEnvelope(_workingStream);
CopyStream(_workingStream, _originalStream);
LogResponse(xmlRequest, GetIpAddress(), _methodName, _parameters); // final step
break;
case SoapMessageStage.BeforeDeserialize:
CopyStream(_originalStream, _workingStream);
_xmlResponse = GetSoapEnvelope(_workingStream);
_url = message.Url;
break;
case SoapMessageStage.AfterDeserialize:
SaveCallInfo(message);
break;
}
}
private void SaveCallInfo(SoapMessage message)
{
_methodName = message.MethodInfo.Name;
// the parameter value is converted to a string for logging,
// but this may not be suitable for all applications.
ParameterInfo[] parminfo = message.MethodInfo.InParameters;
_parameters = parminfo.Select((t, i) => new KeyValuePair<string, String>(
t.Name, Convert.ToString(message.GetInParameterValue(i)))).ToList();
}
private void LogResponse(
XmlDocument xmlResponse,
String ipaddress,
string methodName,
IEnumerable<KeyValuePair<string, string>> parameters)
{
// SEND TO LOGGER HERE!
}
/// <summary>
/// Returns the XML representation of the Soap Envelope in the supplied stream.
/// Resets the position of stream to zero.
/// </summary>
private XmlDocument GetSoapEnvelope(Stream stream)
{
XmlDocument xml = new XmlDocument();
stream.Position = 0;
StreamReader reader = new StreamReader(stream);
xml.LoadXml(reader.ReadToEnd());
stream.Position = 0;
return xml;
}
private void CopyStream(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
// GLOBAL VARIABLE DEPENDENCIES HERE!!
private String GetIpAddress()
{
try
{
return HttpContext.Current.Request.UserHostAddress;
}
catch (Exception)
{
// ignore error;
return "";
}
}