6

I have some data that needs to be send in SOAP format to a server. This server will immediately acknowledge that it received the messages. After a few hours I get (possibly from another server) a SOAP message that contains information about the processed data.

I read Stackoverflow: How to send SOAP request and receive response. However, the answers are 8 years old. Although they may still work, It may be that there are newer techniques.

And indeed it seems: Microsoft has System.Web.Services.Protocols, with classes like SoapMessage, SoapClientMessage, SoapServerMessage, etc.

Looking at the classes I find a lot of SOAP like classes (headers, extensions, client messages, server messages... Normally the provided examples give me an indication to how these classes work together and how to use them. In the MSDN documents I can only find examples of how to process already existing SOAP messages.

Given some data that needs to be sent, how can I wrap this data somehow in one of these SOAP classes and send this message?

Are these classes meant for this purpose? Or should I stick to the 2011 method where you'd create a SOAP Web request by formatting the XML data in soap format yourself, as the above mentioned Stackoverflow question suggests?

I'm awfully sorry, normally I would write things I have tried. Alas I don't see the relation between the provided SoapMessage classes. I haven't got a clue how to use them.

Addition after comments

I'm using windows server / visual studio (newest versions) / .NET (newest versions) / C# (newest versions).

The communication with the server is mutual authenticated. The certificate that I need to use to communicate with the server, is in PEM (CER / CRT) format. The privated key is RSA. This certificate is issued by a proper CA, the server will also use certificates used by a proper CA. So I don't need to create a new certificate (in fact, it won't be accepted). If needed, I'm willing to convert the certificates using programs like OpenSsl and the like.

I've tried to use Apache TomCat to communicate, but I have the feeling that that's way too much for the task of sending one SOAP message per day and waiting for one answer per day.

Maybe because java is a complete new technique for me, it was difficult for me to see the contents of the received messages. So back to C# and .NET.

I was planning to create a DLL, to be used by a console app. The function would have some data in a stream as input. It would create the soap message, send it, wait for reply that the message was received correctly, and wait (possible several hours) for a new Soap message containing the results of the processed data. To make proper reporting, and cancellation possible, I guess it is best to do this using async-await

If sending the order and waiting for the result can't be done in one application, I'm willing to create a windows service that that listens to the input, but I prefer to keep it simple.

The (virtual) computer will only be used for this task, so no one else will need to listen to port 443. There will be one order message send per day, and one result message per day.

Harald Coppoolse
  • 24,051
  • 6
  • 48
  • 92
  • 1
    Just an err of caution, in my experience using 8 yr old technology (WCF, SOAP, etc) doesn't gel well with new versions of IIS (or Kestral if you go .Net Core). If you can use REST it would be 10 times simpler and future-proof. Perhaps asking the 3rd party vendor their roadmap/plans. – Jeremy Thompson Apr 16 '19 at 02:10
  • 1
    Couple of Questions 1. What is happening between the time you make the web call and the time you get response back? 2. What is done with the data that you send in the web request? – smehaffie Apr 16 '19 at 03:58
  • 1
    Are you writing an client for an existing server or both client and server? – Daniel W. Apr 17 '19 at 09:41
  • 1
    I send an order to do something to an existing server. I get immediately a response that my order is accepted. So in that part I'm a Client. A few hours later, some Client will send me a message containing the results of my Order. In that part I will be a server. My Order contains enough information for the client to know where to send the results of the order, and how to identify this order. – Harald Coppoolse Apr 17 '19 at 10:24
  • 1
    Jeremy: about changing the interface: it is the government, who uses this process to communicate with many companies. Request to change protocol is out of the question. – Harald Coppoolse Apr 17 '19 at 10:26
  • Why you don't let VS create all the methods for `wsdl` address? This is really easy – Amirhossein Mehrvarzi Apr 17 '19 at 19:11

3 Answers3

3

Personally, I use ServiceStack to create both client and server

https://docs.servicestack.net/soap-support

Or SoapHttpClient nuget

https://github.com/pmorelli92/SoapHttpClient

Or my example from way back when

Is it possible that I can convert simple string to SOAP Message and send it?

The answer depends on what framework or libraries do you plan to use?

Margus
  • 18,332
  • 12
  • 51
  • 101
  • I've given more information in my question. Hope this will answer your questions. – Harald Coppoolse Apr 15 '19 at 07:48
  • The simplest proper way to do this would be to use Reactive eXtensions and create an observable to handle the request, its failure, retry policy, scheduling and tests. Create a service and hide your soap client calls behind it. In real life you might choose to use Quartz jobs, Polly or 100 other small things, but in reality you do not need them. Certificate should be installed on the debugging machine as well as deployed virtual machine. Just remember that "Messy success beats perfect", start somewhere make it work, and improve on it. – Margus Apr 15 '19 at 09:25
3

Here is sample C# Console client and server code (they are in the same sample but this is only for demo purpose, of course) that uses HTTPS.

For the client side, we reuse the SoapHttpClientProtocol class, but for the server side, unfortunately, we cannot reuse anything because classes are completely tied to ASP.NET's (IIS) HttpContext class

For the server side, we use HttpListener, so, depending on your configuration, the server side will probably require admin rights to be able to call HttpListener's Prefixes.Add(url).

The code doesn't uses client certificate, but you can add this where I placed // TODO comments

The code assumes there is a certificate associated with the url and port used. If there's not (use netsh http show sslcert to dump all associated certs), you can use the procedure described here to add one: https://stackoverflow.com/a/11457719/403671

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;

namespace SoapTests
{
    class Program
    {
        static void Main(string[] args)
        {
            // code presumes there is an sslcert associated with the url/port below
            var url = "https://127.0.0.1:443/";
            using (var server = new MyServer(url, MyClient.NamespaceUri))
            {
                server.Start(); // requests will occur on other threads
                using (var client = new MyClient())
                {
                    client.Url = url;
                    Console.WriteLine(client.SendTextAsync("hello world").Result);
                }
            }
        }
    }

    [WebServiceBinding(Namespace = NamespaceUri)]
    public class MyClient : SoapHttpClientProtocol
    {
        public const string NamespaceUri = "http://myclient.org/";

        public async Task<string> SendTextAsync(string text)
        {
            // TODO: add client certificates using this.ClientCertificates property
            var result = await InvokeAsync(nameof(SendText), new object[] { text }).ConfigureAwait(false);
            return result?[0]?.ToString();
        }

        // using this method is not recommended, as async is preferred
        // but we need it with this attribute to make underlying implementation happy
        [SoapDocumentMethod]
        public string SendText(string text) => SendTextAsync(text).Result;

        // this is the new Task-based async model (TAP) wrapping the old Async programming model (APM)
        public Task<object[]> InvokeAsync(string methodName, object[] input, object state = null)
        {
            if (methodName == null)
                throw new ArgumentNullException(nameof(methodName));

            return Task<object[]>.Factory.FromAsync(
                beginMethod: (i, c, o) => BeginInvoke(methodName, i, c, o),
                endMethod: EndInvoke,
                arg1: input,
                state: state);
        }
    }

    // server implementation
    public class MyServer : TinySoapServer
    {
        public MyServer(string url, string namespaceUri)
            : base(url)
        {
            if (namespaceUri == null)
                throw new ArgumentNullException(nameof(namespaceUri));

            NamespaceUri = namespaceUri;
        }

        // must be same as client namespace in attribute
        public override string NamespaceUri { get; }

        protected override bool HandleSoapMethod(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement)
        {
            switch (requestMethodElement.LocalName)
            {
                case "SendText":
                    // get the input
                    var text = requestMethodElement["text", NamespaceUri]?.InnerText;
                    text += " from server";

                    AddSoapResult(outputDocument, requestMethodElement, responseMethodElement, text);
                    return true;
            }
            return false;
        }
    }

    // simple generic SOAP server
    public abstract class TinySoapServer : IDisposable
    {
        private readonly HttpListener _listener;

        protected TinySoapServer(string url)
        {
            if (url == null)
                throw new ArgumentNullException(nameof(url));

            _listener = new HttpListener();
            _listener.Prefixes.Add(url); // this requires some rights if not used on localhost
        }

        public abstract string NamespaceUri { get; }
        protected abstract bool HandleSoapMethod(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement);

        public async void Start()
        {
            _listener.Start();
            do
            {
                var ctx = await _listener.GetContextAsync().ConfigureAwait(false);
                ProcessRequest(ctx);
            }
            while (true);
        }

        protected virtual void ProcessRequest(HttpListenerContext context)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            // TODO: add a call to context.Request.GetClientCertificate() to validate client cert
            using (var stream = context.Response.OutputStream)
            {
                ProcessSoapRequest(context, stream);
            }
        }

        protected virtual void AddSoapResult(XmlDocument outputDocument, XmlElement requestMethodElement, XmlElement responseMethodElement, string innerText)
        {
            if (outputDocument == null)
                throw new ArgumentNullException(nameof(outputDocument));

            if (requestMethodElement == null)
                throw new ArgumentNullException(nameof(requestMethodElement));

            if (responseMethodElement == null)
                throw new ArgumentNullException(nameof(responseMethodElement));

            var result = outputDocument.CreateElement(requestMethodElement.LocalName + "Result", NamespaceUri);
            responseMethodElement.AppendChild(result);
            result.InnerText = innerText ?? string.Empty;
        }

        protected virtual void ProcessSoapRequest(HttpListenerContext context, Stream outputStream)
        {
            // parse input
            var input = new XmlDocument();
            input.Load(context.Request.InputStream);

            var ns = new XmlNamespaceManager(new NameTable());
            const string soapNsUri = "http://schemas.xmlsoap.org/soap/envelope/";
            ns.AddNamespace("soap", soapNsUri);
            ns.AddNamespace("x", NamespaceUri);

            // prepare output
            var output = new XmlDocument();
            output.LoadXml("<Envelope xmlns='" + soapNsUri + "'><Body/></Envelope>");
            var body = output.SelectSingleNode("//soap:Body", ns);

            // get the method name, select the first node in our custom namespace
            bool handled = false;
            if (input.SelectSingleNode("//x:*", ns) is XmlElement requestElement)
            {
                var responseElement = output.CreateElement(requestElement.LocalName + "Response", NamespaceUri);
                body.AppendChild(responseElement);

                if (HandleSoapMethod(output, requestElement, responseElement))
                {
                    context.Response.ContentType = "application/soap+xml; charset=utf-8";
                    context.Response.StatusCode = (int)HttpStatusCode.OK;
                    var writer = new XmlTextWriter(outputStream, Encoding.UTF8);
                    output.WriteTo(writer);
                    writer.Flush();
                    handled = true;
                }
            }

            if (!handled)
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }
        }

        public void Stop() => _listener.Stop();
        public virtual void Dispose() => _listener.Close();
    }
}
Simon Mourier
  • 117,251
  • 17
  • 221
  • 269
  • This was exactly what I was using. The reason that it didn't work, was the netsh: in all other stackoverflow answers that I read about HttpListener and security, you were the first who mentioned it. I used tge correct Netsh command, and it worked! You're a genius! – Harald Coppoolse Apr 19 '19 at 14:55
0

The simplest modern answer is to declare a simple class that defines the structure of your message and then serialize it using HttpClient to send it.

However, SOAP is a standard built for description based messaging so the still relevant recommendation is to generate your client code from the wsdl description using a "service reference" then use the generated client object.

I would however recommend, like others have pointed out that you try to move to REST services instead (assuming this is possible). The code is less complex, the system is far simpler to use and it's a global standard.

Here is a comparison and example of both ...

https://smartbear.com/blog/test-and-monitor/understanding-soap-and-rest-basics/

War
  • 8,164
  • 2
  • 43
  • 89