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.
Related
e.g. I want to select a character and save his number
private Storage storage;
void Awake()
{
storage = new Storage();
}
public void SelectChar1()
{
numberChar = 1;//byte
storage.Save(DataPlayerSave);//save works fine
}
on awakening, the number is loaded
private DataPlayerSave dataPlayer;
private byte numberChar;
private void Awake()
{
dataPlayer = (DataPlayerSave)storage.Load(new DataPlayerSave());
numberChar = dataPlayer.numerChar;
}
I tried to divide the data into several parts and one large file, the result is almost always the same (sometimes everything works)
public class Storage()
{
public object Load(object saveDataByDefault)
{
filePath = Application.persistentDataPath + "/saves/GameSave.save";
if (!File.Exists(filePath))
{
if (saveDataByDefault != null)
{
Save(saveDataByDefault);
return saveDataByDefault;
}
}
var file = File.Open(filePath, FileMode.Open);
var saveData = formatter.Deserialize(file);
file.Close();
return saveData;
}
}
There are also similar classes that load data at the beginning of the scene. If there are 2 or more of them, then it gives an error, if 1, then everything works. I tried to set the sequence using the Coroutine did not help.
When loading data it gives an error "InvalidCastException: Specified cast is not valid."
dataPlayer = (DataPlayerSave)storage.Load(new DataPlayerSave());
if formatter is BinaryFormatter: please don't do that - it will hurt you; as for the exception: fundamentally, use a debugger and step through the code. In particular, if you say that the exception is coming from:
dataPlayer = (DataPlayerSave)storage.Load(new DataPlayerSave());
then we can assume that Load is not returning a DataPlayerSave. So: what is it? We can't tell you, but: you can find out:
var obj = storage.Load(new DataPlayerSave());
var type = obj.GetType(); // put a break-point here
Log(type.FullName); // or just log it
dataPlayer = (DataPlayerSave)obj;
and investigate what exactly obj is. Note that BinaryFormatter is very brittle as you change types (rename, move, refactor, etc) - but that isn't even the top reason not to use it.
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.
I followed the Hololens Tutorials provided by Microsoft and encountered some problems regarding Azure services in 301 (https://learn.microsoft.com/en-us/windows/mixed-reality/mr-azure-301#chapter-4--setup-debug-canvas) and 302 (https://learn.microsoft.com/en-us/windows/mixed-reality/mr-azure-302). I think the problem has something to do with the endpoint, because the error code 404 appears when checking https://westeurope.api.cognitive.microsoft.com/.
I use the Hololens first generation, Unity 2017.4.27f1, HoloToolkit-Unity-Examples-2017.4.3.0 and Visual Studio Community 2017. I've tried some of the Hololens tutorials, which all worked fine with these versions. My code is the one provided by Microsoft for the Tutorials 301 and 302. I've changed the location in Azure from
'global' to 'Western Europe' by creating a new ressource group and have now https://westeurope.api.cognitive.microsoft.com/ as my endpoint, I also created new authentification keys for the APIs and updated them in my code.
Instead of myKey I inserted the first key of the respective API (Translator Text, Computer Vision). The code below is from Tutorial 302 and the class 'VisionManager.cs' (which seems to be the responsible one for the communication with the services of the Computer Vision API).
public static VisionManager instance;
// you must insert your service key here!
private string authorizationKey = "myKey";
private const string ocpApimSubscriptionKeyHeader = "Ocp-Apim-Subscription-Key";
private string visionAnalysisEndpoint = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0"; // This is where you need to update your endpoint, if you set your location to something other than west-us.
internal byte[] imageBytes;
internal string imagePath;
/// <summary>
/// Call the Computer Vision Service to submit the image.
/// </summary>
public IEnumerator AnalyseLastImageCaptured()
{
WWWForm webForm = new WWWForm();
using (UnityWebRequest unityWebRequest = UnityWebRequest.Post(visionAnalysisEndpoint, webForm))
{
// gets a byte array out of the saved image
imageBytes = GetImageAsByteArray(imagePath);
unityWebRequest.SetRequestHeader("Content-Type", "application/octet-stream");
unityWebRequest.SetRequestHeader(ocpApimSubscriptionKeyHeader, authorizationKey);
// the download handler will help receiving the analysis from Azure
unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
// the upload handler will help uploading the byte array with the request
unityWebRequest.uploadHandler = new UploadHandlerRaw(imageBytes);
unityWebRequest.uploadHandler.contentType = "application/octet-stream";
yield return unityWebRequest.SendWebRequest();
long responseCode = unityWebRequest.responseCode;
try
{
string jsonResponse = null;
jsonResponse = unityWebRequest.downloadHandler.text;
// The response will be in Json format
// therefore it needs to be deserialized into the classes AnalysedObject and TagData
AnalysedObject analysedObject = new AnalysedObject();
analysedObject = JsonUtility.FromJson<AnalysedObject>(jsonResponse);
if (analysedObject.tags == null)
{
Debug.Log("analysedObject.tagData is null");
}
else
{
Dictionary<string, float> tagsDictionary = new Dictionary<string, float>();
foreach (TagData td in analysedObject.tags)
{
TagData tag = td as TagData;
tagsDictionary.Add(tag.name, tag.confidence);
}
ResultsLabel.instance.SetTagsToLastLabel(tagsDictionary);
}
}
catch (Exception exception)
{
Debug.Log("Json exception.Message: " + exception.Message);
}
yield return null;
}
}
[System.Serializable]
public class TagData
{
public string name;
public float confidence;
}
[System.Serializable]
public class AnalysedObject
{
public TagData[] tags;
public string requestId;
public object metadata;
}
// Use this for initialization
void Start () {
}
private void Awake()
{
// allows this instance to behave like a singleton
instance = this;
}
// Update is called once per frame
void Update () {
}
/// <summary>
/// Returns the contents of the specified file as a byte array.
/// </summary>
private static byte[] GetImageAsByteArray(string imageFilePath)
{
FileStream fileStream = new FileStream(imageFilePath, FileMode.Open, FileAccess.Read);
BinaryReader binaryReader = new BinaryReader(fileStream);
return binaryReader.ReadBytes((int)fileStream.Length);
}
I expected the output to be like the examples in the tutorials, but instead I got the same GUI results without any updates. In 301 I could see the same screen, but the text field 'You just said' and the 'Translation' never updated when I spoke. In 302 I could airtap and make a screenshot and could see at first 'Analyzing' and later on 'I see:' but without any results of detected objects. So it seems to me, that the Azure APIs services can't be contacted and I don't get any analysis data in return (probably you notice, that I'm quite new to the whole topic, so maybe my assumption is wrong).
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.
void ReadContent(string path)
{
Contract.Requires(path!=null);
string contentofileasstring = filehelperobj.GetContent(path);
if(String.IsNullOrEmpty(contentofileasstring ))
{
throw new FileContentException(path + "No content found");
}
m_xmlobj = contentofileasstring ;
}
Is my assumption of the usage of code contracts and exceptions right in this case. Do you think it is logical to replace the exception with a code contract(or vice versa)?
code not tested.Just an example scenario
I would probably go for an implementation which looks like the following:
private void ReadContent(string path)
{
Contract.Requires<FileMissingException>(File.Exists(path));
string content = filehelperobj.GetContent(path);
m_xmlobj = content;
}
Post Edit
As it's the content you want to validate, I would put a Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>())); inside the filehelperobj.GetContent(string) method. Then if the content being read was null or empty, I would throw an exception. e.g.
public string GetContent(string path)
{
Contract.Requires<FileMissingException>(File.Exists(path));
Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>()));
using(var reader = new StreamReader(File.OpenRead(path)))
{
var content = reader.ReadToEnd();
if(String.IsNullOrEmpty(content))
throw new FileContentException("No content found at file: " + path);
return content;
}
}
Well assuming you had the lines the wrong way round (ie, test the path for null before trying to use it) then yes, it is a valid pre-condition and therefore should be a code contract.