C# Sockets send/receive problems and questions - c#

Good morning all,
First of all apologies for the somewhat generic title. If in the course of this message I can come up with something a little more detailed, I will certainly change it.
I am working on a project that will contain 3 programs. The purpose is to be able to send a notification to all clients connected. For this there is a server, client, and console program.
The message itself will be an RTF file, but the notification also requires a sending department (string), and a display timer (TimeSpan).
Most of the project is done. The logic is mostly complete, it is multithreaded, all the classes are ready, and most tests work.
The problem I am having is that the server seems to not receive the data from the console in the right order. Thereby causing all sorts of problems.
The process from console to server is as follows:
Console first selects all relevant information for the notification:
Users (List, gathered from LDAP)
Expiration date and time (DateTime)
Time before close is allowed (long, amount of ticks)
Department (string)
RTF file
The server is already active, and has a separate thread for the console connections
Console connects to server
Server creates a separate thread to handle the console connection
Console sends the username of the logged on user to the server
Server checks whether user is allowed to create notification (for now it will always return true)
Console sends all the relevant information in the following order:
MessageID (string)
RecipientCount (number of users, used to loop appropriately from server side)
Recipients (foreach loop of List)
Department (string)
VisibleTime (long)
Expiration (DateTime)
and finally RTF file
I have used System.Diagnostics.Trace to check whether all information is sent correctly, and in the right order. This all checks out. But the thing is that roughly 75% of the time the server side seems to receive the RTF file at the moment it should be receiving the visibleTime.
The code is as follows:
private void SendMessage()
{
SendToServer(Environment.UserName);
if (bool.Parse(ReadFromServer()))
{
// User is allowed, continue
string messageID = DateTime.Now.ToUniversalTime().Ticks.ToString();
SendToServer(messageID); // MessageID
string recipientCount = lvRecipients.Items.Count.ToString();
SendToServer(lvRecipients.Items.Count.ToString()); // Amount of recipients
foreach (string item in lvRecipients.Items) // Loop to send each recipient
{
SendToServer(item);
}
string department = TB_Department.Text;
SendToServer(department); // Send department string
string visibleTime = TimeSpan.FromSeconds(SLIDER_VisibleTime.Value).Ticks.ToString();
SendToServer(visibleTime); // Send message visibility time
string expiration = DateTime.Now.ToUniversalTime().AddMinutes(2).ToString();
SendToServer(expiration); //TODO add UI control for this
SendRTFToServer(); // Send RTF file
MessageBox.Show(
"Your designated MessageID is: " + messageID + Environment.NewLine +
"Message upload is succesful.",
"Complete",
MessageBoxButton.OK);
}
else
{
// User is not allowed. Report to user. Disconnect (will be managed by the finally block)
MessageBox.Show(
"You are not allowed to upload messages to the server.",
"Access denied",
MessageBoxButton.OK,
MessageBoxImage.Stop);
return;
}
}
private void SendToServer(string toSend)
{
StreamWriter writer = new StreamWriter(server.GetStream());
writer.WriteLine(toSend);
writer.Flush();
}
private void SendRTFToServer()
{
StreamReader rtfFile = new StreamReader(File.Open(RTFLocation, FileMode.Open, FileAccess.Read));
StreamWriter sw = new StreamWriter(server.GetStream());
sw.Write(rtfFile.ReadToEnd());
sw.Flush();
server.GetStream().Flush();
}
private string ReadFromServer()
{
server.GetStream().Flush();
StreamReader reader = new StreamReader(server.GetStream());
return reader.ReadLine();
}
And from the server:
private void Connect()
{
string username = ReadFromConsole();
if (IsUserAllowed(username)) // Receive username
SendToConsole(bool.TrueString); // Send confirmation
else
{
SendToConsole(bool.FalseString); // Send denial
console.Close();
return;
}
string messageID = ReadFromConsole(); // Receive MessageID
string recipientCount = ReadFromConsole();
int numOfRecipients = int.Parse(recipientCount); // Receive and parse number of recipients
List<string> recipients = new List<string>();
for (int i = 0; i < numOfRecipients; i++)
{
string recipient = ReadFromConsole();
recipients.Add(recipient); // Receive recipient, add to list (required for Message)
}
string department = ReadFromConsole(); // Receive department string
string visibleTime = ReadFromConsole();
string expiration = ReadFromConsole();
StoreRTF(messageID); // Receive and store RTF file
console.Close(); // Connection is done, close
Message message = new Message(messageID, department, recipients, visibleTime, expiration);
}
private void SendToConsole(string toSend)
{
// Open client stream, and write information to it.
StreamWriter writer = new StreamWriter(console.GetStream());
writer.WriteLine(toSend);
writer.Flush();
}
private string ReadFromConsole()
{
// Read information from client stream
StreamReader reader = new StreamReader(console.GetStream());
return reader.ReadLine();
}
private void StoreRTF(string messageID)
{
// Check/create folder for Message storage
string messageFolder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + #"\BMNotify\";
if (!Directory.Exists(messageFolder))
Directory.CreateDirectory(messageFolder);
// Create file to store message in
Stream rtfFile = File.Create(messageFolder + messageID + ".rtf");
// Store information from stream, and close resources
console.GetStream().CopyTo(rtfFile);
rtfFile.Close();
rtfFile.Dispose();
}
And the message class:
public class Message
{
internal string messageID;
internal string department;
internal List<string> recipients;
internal TimeSpan visibleAtLeast;
internal DateTime messageExpiration;
private static List<Message> allMessages; // Will hold te collection of Message's
public Message(string _messageID, string _department, List<string> _recipients, string visibleTime, string expiration)
{
messageID = _messageID;
recipients = _recipients;
department = _department;
visibleAtLeast = TimeSpan.FromTicks(long.Parse(visibleTime));
messageExpiration = DateTime.Parse(expiration);
if (allMessages == null)
allMessages = new List<Message>(); // Initialize if required
allMessages.Add(this);
}
internal Stream GetRTF()
{
return File.Open
(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + #"\BMNotify\" + messageID + ".rtf",
FileMode.Open,
FileAccess.Read,
FileShare.Read);
}
static public List<Message> AllMessages()
{
if (allMessages == null)
allMessages = new List<Message>(); // Initialize if required
return allMessages;
}
static public void RemoveMessage(Message message)
{
allMessages.Remove(message);
}
}
If anyone could shed any light on this, or tell me what I should change.. Or basically anything that could get me going again, I would be very grateful!

Your problem may stem from the fact that you're using a StreamReader to read data from the connection. Internally StreamReader buffers data it reads from the underlying Stream. You are creating a new StreamReader every time you attempt to read from the connection, and then discarding it once you've read a single line. In doing so, you're also discarding any data read from the connection that was buffered by the StreamReader (which may have constituted all or part of the following fields).
You should probably attempt to create a single StreamReader on the network stream, and use that same instance for all reads.

Related

BizTalk SMTP send mail with attachments (no orchestration, custom send pipeline component)

I hope someone can help me out with this since when I drop a JSON test file in my rcv-folder the only result is that in the BizTalk console (in 'Running instances'), the message only states 'Queued (Awaiting processing)'. I am not sure where my issue is in the code.
I am supposed to receive a JSON which will contain some info and possibly multiple attachments (in Base64 format) and then send the mail out (with attachments in correct format, i.e. PDF, txt, xls) to a certain email-address. One of the requirements is to not use an orchestration. But I am stuck and have no idea what I am doing anymore. What makes this question different from others is that I have no orchestration in my solution. Everything will be processed in a custom send pipeline component.
The sendpipeline (in encode stage) does contain my custom component and also the MIME/SMIME encoder. I am using the SMTP-adapter.
I have created the custom pipeline component with a wizard and my initial plan was in the form below:
Receive the file (in JSON-format. It will be transformed into XML for further processing). This will all be taken care of in my rcv-pipeline and this step already works for me.
Pick out all the necessary variables from XML needed to send in the e-mail. These variables are the ones I want to show in the E-mail text. Not sure how to explain it better.
Pick out all the attachments in base64, loop through them, convert to 'regular files' and then attach them to the mail (with correct filenames, extension etc.)
The XML looks like below:
ArchiveobjectsListErrands
- ArchiveobjectErrand
* UUID (Here are the variables I need to show in the E-mail. The plain text, so to say)
- ArchiveobjectListPaper
- Attachments
* Name
* Extension
* Size
* Base64String (Base64 string which will be needed to be fetched (in GetAttachments) and then processed in (ProcessAttachments))
The code I have is below:
public Microsoft.BizTalk.Message.Interop.IBaseMessage Execute(Microsoft.BizTalk.Component.Interop.IPipelineContext pContext, Microsoft.BizTalk.Message.Interop.IBaseMessage pInMsg)
{
// 1) Read file with XPathNavigator (https://learn.microsoft.com/en-us/dotnet/standard/data/xml/extract-xml-data-using-xpathnavigator)
XPathNavigator nav = ReadXmlFromMsgBox(pInMsg);
var outMsg = pContext.GetMessageFactory().CreateMessage();
outMsg.Context = PipelineUtil.CloneMessageContext(pInMsg.Context);
// 2) Pick out the necessary vars that the registrator requires
GetRegistratorProperties(nav, pContext, outMsg);
// 3) Read attachments
var attachments = GetAttachments(pInMsg, nav);
// 4) Processa attachments
ProcessAttachments(pContext, outMsg, attachments);
// 5) Send message along for further processing in the send pipeline
return outMsg;
}
private void GetRegistratorProperties(XPathNavigator _nav, IPipelineContext _pContext, IBaseMessage _msg)
{
var bodyPart = _pContext.GetMessageFactory().CreateMessagePart();
bodyPart.ContentType = "text/application";
bodyPart.PartProperties.Write("EmailBodyText", "http://schemas.microsoft.com/BizTalk/2003/smtp-properties", "EmailBodyText.");
bodyPart.PartProperties.Write("Subject", "http://schemas.microsoft.com/BizTalk/2003/smtp-properties", "Registratorsubject - Create errand");
_msg.AddPart("Body", bodyPart, true); // True for body but false for attachments
}
private void ProcessAttachments(IPipelineContext _pContext, IBaseMessage _msg, IList<Attachment> _attachments)
{
var msgPart = _pContext.GetMessageFactory().CreateMessagePart();
//outMsg.Context = PipelineUtil.CloneMessageContext(_msg.Context);
int i = 0;
foreach (var item in _attachments)
{
msgPart.PartProperties.Write("FileName", "http://schemas.microsoft.com/BizTalk/2003/mime-properties", item.filnamn+item.extension);
msgPart.PartProperties.Write("ContentDescription", "http://schemas.microsoft.com/BizTalk/2003/mime-properties", item.contentType);
msgPart.Data = new MemoryStream(BytesFromBase64String(item.base64));
//bodyPart.Charset = "utf-8";
msgPart.ContentType = item.contentType;
//_pInMsg.AddPart("Attachment part " + i.ToString(), bodyPart, false);
_msg.AddPart("Attachment part " + i.ToString(), msgPart, false);
i++;
}
}
private IList<Attachment> GetAttachments(IBaseMessage pInMsg, XPathNavigator _nav)
{
XPathNodeIterator iterator = _nav.Select("Path to attachments in xml");
IList<Attachment> myList = new List<Attachment>();
while (iterator.MoveNext())
{
XPathNavigator node = iterator.Current;
Attachment atttachments = new Attachment();
atttachments.filenamne = node.SelectSingleNode("Name").Value;
atttachments.extension = node.SelectSingleNode("Extension").Value;
atttachments.contentType = node.SelectSingleNode("Mimetype").Value;
atttachments.base64 = node.SelectSingleNode("Base64String").Value;
myList.Add(atttachments);
}
return myList;
}
private XPathNavigator ReadXmlFromMsgBox(IBaseMessage pInMsg)
{
// Using XPathNavigator to avoid creating a XMLDoc in memory
Stream originalMessage = pInMsg.BodyPart.GetOriginalDataStream();
XPathNavigator _navigator = new XPathDocument(originalMessage).CreateNavigator();
return _navigator;
}
[Serializable]
private class FileStreamFactory : IStreamFactory
{
byte[] _data;
public FileStreamFactory(byte[] data)
{
_data = data;
}
public Stream CreateStream()
{
return new MemoryStream(_data);
}
}
private static byte[] BytesFromBase64String(string msg)
{
return Convert.FromBase64String(msg);
}
#endregion
}
I can show some example file of the XML if deemed necessary. I avoided it due to brevity and also since it is quite large.
I would greatly appreciate if anyone could help out with how the code is supposed to look to achieve what is needed, a mail with some text and attachments named correctly regarding filename and extension.
Body text and Attachments are just different segments in a MIME encoded email, usually a plain text one is the first one.
If it has a status of 'Queued (Awaiting processing)', then it sounds like either
the host instance that is expecting to process it is not in a running state. Fix: Start the host instance.
the send port is not in a Started sate. Fix: set the send port to Started
or the send port has a service window set on it. Fix: disable the service window.

Deserializing Protobuf c# from wire

I am using the protobuf google implementation for c#.
The story so far..I have a server in c++ and clients in c# and the talk via protobuf messages with TCP.
On the client side, I retrieve the buffers returned from the tcpClient.BeginReceive via the callback provided and append them (so I guess it is cleared from the netstream junk). Then on a worker thread, I try to deserialize the messages.
On the server side the serialization code is:
google::protobuf::uint32 msgSize = msg->ByteSize();
int prefix_length = sizeof(msgSize);
int buffer_length = msgSize + prefix_length;
google::protobuf::uint8 buffer[buffer_length];
google::protobuf::io::ArrayoutputStream array_output(buffer, buffer_length, msgSize);
google::protobuf::io::CodedOutputStream coded_output(&array_output);
coded_output.WriteVarint32(msgSize);
msg->SerializeToCodedStream(&coded_output);
//Send to socket
mSendBuffer.Write(buffer, buffer_length);
On the client I read each chunk using CodedInputStream, read the first number and dispatch the prefix+the msg it contains for deserialization
Figuring out the length:
using (var input = new CodedInputStream(chunk))
{
int len = input.ReadInt32();
int requiredLength = len + input.Position; //(we re sure each time the codedInput starts from the beginning)
byte[] read = await AccumulateResources(requiredLength, chunk);
byte[] msg = new byte[requiredLength];
Buffer.BlockCopy(read, 0, msg , 0 , requiredLength);
Dispatch(msg);
}
Deserialization:
using (var ms = new MemoryStream(buff))
{
ServerMessageFrame msg = null;
try
{
msg = ServerMessageFrame.Parser.ParseDelimitedFrom(ms);
}
catch(Exception e)
{
Logger.Write(conn.clntPrefx + " " + e.ToString());
}
//Use the message
}
The error Messages I receive are:
1)System.InvalidOperationExeption: Wire Type is invalid
2)Protocol message contained invalid tag (zero).
The communication is correct at the beginning and it desyncs at some point, from where it breaks (until I read another msg which starts with a prefixed value aka is not a partial message)
My question is the de/serialization sound or I am missing the mark completely like there is something that I don't account for at all.

sending message by user id in c# bot

i'm trying to send message to People user name for example my friends username is #...
but no thing happend
no Error
and no send
can i use this library ?
or just with tlsharp?
how can i found apiid and hashid for my bot?
using Telegram.Bot;
and
public partial class Form1 : Form
{
Telegram.Bot. TelegramBotClient bot = new TelegramBotClient("token");
Thread a;
public Form1()
{
InitializeComponent();
}
public void GetUpdates()
{
int offset = 0;
while (true)
{
Telegram.Bot.Types.Update[] updates =
bot.GetUpdatesAsync(offset).Result;
foreach (var update in updates)
{
offset = update.Id + 1;
if (update.Message == null)
continue;
var from = update.Message.From.FirstName;
var text = update.Message.Text;
string chatid = update.Message.Chat.Id;
string username = update.Message.From.Username;
// label1.BeginInvoke(delegate { label1.Text = label1.BeginInvoke(delegate { label1.Text = string.Format("sender:{0}\ntext:{1}\ncatid:{2}", from, text, chatid); }); });
// lblshow.Text = string.Format("sender:{0}\ntext:{1}\ncatid:{2}", from, text, chatid);
//label1.Text = string.Format("sender:{0}\ntext:{1}\ncatid:{2}", from, text, chatid);
this.BeginInvoke((System.Windows.Forms.MethodInvoker)delegate () {textBox1.Text = string.Format("sender:{0}\ntext:{1}\ncusername:{2}", from, text, username); });
bot.SendTextMessageAsync( chatid, "سلام بر شما");
//is this correct for send to people?
bot.SendTextMessageAsync("#Hoda.....", "hi");
}
}
you cant send message to user by user Name.telegram bot API only accept user Id. (except channels).
When you try to send messages to users you must know their chatID (chatID is a long number which is unique for every user in telegram and never changes while username can be changed) or you must have stored their chatIDs in a database or a file or ...
This means a user must have sent at least one message to your bot before this includes the /start command. Then your bot can find out their chatID and using that chatID, you can send whatever you want to that user unless he/she has blocked your bot by pressing Delete and Stop button when trying to delete the conversation between him/her and your bot.
You can but don't pass # symbol, If the username is #me pass "me" to SendTextMessageAsync without passing #.

help to optimize large array of user reminders

every night i have a trigger that executes asp.net page with method that first retrieves from DB users with needed details and then emails them with information they need like ie. users that does not have an image in their profile. Once i get data from DB i build large string with encoded HTML and assign it to MailMessage.Body tag. There are about 20000 users per action/night who get the mail. Stored procedures just get users based on criteria i need nothing to optimize there (after proper indexes been made).
How do i optimize it? Can i create sort of queue in ASP.NET that it will be executed all day long without one time action? Maybe i can use async actions to send few emails per time.
Need advice, if you have question about how i perform each action in order to help me don't be afraid to ask, i will answer i just don't know what place in code may interest you.
protected void Page_Load(object sender, EventArgs e)
{
string type = Request.QueryString["type"];
string spName = "";
string message = "";
switch (type)
{
//getUsersWithoutPictures
case "uwp":
spName = "getUsersWithoutPictures";
message = getUsersWithoutPicturesTemplate();
break;
//getUsersWithoutText
case "uwt":
spName = "getUsersWithoutText";
message = getUsersWithoutTextTemplate();
break;
//getUsersWithoutEnteringWebsiteForTwoWeeks
case "uwewftw":
spName = "getUsersWithoutEnteringWebsiteForTwoWeeks";
message = getUsersWithoutEnteringWebsiteForTwoWeeksTemplate();
break;
//getUsersWithoutHobbies
case "uwh":
spName = "getUsersWithoutHobbies";
message = getUsersWithoutHobbiesTemplate();
break;
//getUsersWithoutCharateristics
case "uwc":
spName = "getUsersWithoutCharateristics";
message = getUsersWithoutCharateristicsTemplate();
break;
//getUsersWithoutActivation
case "uwa":
spName = "getUsersWithoutActivation";
message = getUsersWithoutActivationTemplate();
break;
default:
Response.Write("failure");
Response.End();
break;
}
DataTable recipientsList = new DataTable();
using (SqlConnection cn = cms.connect("someconnectionstring"))
{
SqlDataAdapter adp = new SqlDataAdapter(spName, cn);
adp.SelectCommand.CommandType = CommandType.StoredProcedure;
adp.Fill(recipientsList);
}
foreach (DataRow row in recipientsList.Rows)
{
try
{
IPHostEntry emailHost = Dns.GetHostEntry(row["email"].ToString().Remove(0, row["email"].ToString().LastIndexOf("#") + 1));
MailMessage myMessage = new MailMessage(new MailAddress("support#" + row["registratioSite"].ToString()), new MailAddress(row["email"].ToString()));
myMessage.Subject = "";
myMessage.Body = getGenericEmailTemplate(AffDomains.getFullDomain(row["registratioSite"].ToString()), message, row["email"].ToString(), row["usernumber"].ToString(), row["nick"].ToString());
myMessage.IsBodyHtml = true;
SmtpClient mySmtp = new SmtpClient("mysmtp.server.com");
mySmtp.Send(myMessage);
}
catch (Exception)
{
}
}
Response.Write("success");
Response.End();
}
private string getGenericEmailTemplate(string domain, string message, string email, string usernumber, string nick)
{
return "some html";
}
private string getUsersWithoutPicturesTemplate()
{
return "some message";
}
private string getUsersWithoutTextTemplate()
{
return "some message 2";
}
private string getUsersWithoutEnteringWebsiteForTwoWeeksTemplate()
{
return "some message 3";
}
private string getUsersWithoutHobbiesTemplate()
{
return "some message 4";
}
private string getUsersWithoutCharateristicsTemplate()
{
return "some message 5";
}
private string getUsersWithoutActivationTemplate()
{
return "some message 6";
}
Some pointers based on your current implementation:
Do this once per week. Is your site really so important that the users must be tortured every single night?
It looks like you're going to email each user several times, eg if they don't have a picture and haven't entered for two weeks they will get two emails. Combine all the events into one email.
Batch your emails into small groups based on which notifications they ought to receive, eg BCC 5 users at a time who don't have images in their profile.
Return only the data you need from the stored procs.
Move everything you can outside of the main loop eg creation of the EmailHost, MailMessage object and set the relevant properties inside the loop.
If you want this solution to scale way beyond its current limits however, you might want to think about some sort of multithreaded (or distrubuted) producer/consumer queue. I won't elaborate further here - there are plenty of examples on the web.

SOAP Extension Stream Empty After Serialization

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?
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.
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>
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.
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.

Categories