Categories
Technology

Calling a Web Method in C# without Service Reference

This is a SOAP request

Last week I gave you a method that, using SOAP and HttpWebRequest, allowed you to invoke a Web Method without a WSDL or a Web Reference. Well today, I’ll give you an improved version of that method. In fact I’ll give you a whole ready-to-use class with additional functionality.

Recapping, if you need to call a web method but fail to have a WSDL or can’t use a Web or Service Reference because you want your code to be dynamic, then SOAP and HttpWebRequests are the way to go. You can then create a Proxy having the same API (methods, inputs, outputs) as the WebService you’re trying to call. For instance:

internal class ExampleAPIProxy
{
    private static WebService ExampleAPI = new WebService("http://.../example.asmx");    // DEFAULT location of the WebService, containing the WebMethods

    public static void ChangeUrl(string webserviceEndpoint)
    {
        ExampleAPI = new WebService(webserviceEndpoint);
    }

    public static string ExampleWebMethod(string name, int number)
    {
        ExampleAPI.PreInvoke();

        ExampleAPI.AddParameter("name", name);                    // Case Sensitive! To avoid typos, just copy the WebMethod's signature and paste it
        ExampleAPI.AddParameter("number", number.ToString());     // all parameters are passed as strings
        try
        {
            ExampleAPI.Invoke("ExampleWebMethod");                // name of the WebMethod to call (Case Sentitive again!)
        }
        finally { ExampleAPI.PosInvoke(); }

        return ExampleAPI.ResultString;                           // you can either return a string or an XML, your choice
    }
}

For that to work you’ll need the WebService class.

/// <summary>
/// This class is an alternative when you can't use Service References. It allows you to invoke Web Methods on a given Web Service URL.
/// Based on the code from http://stackoverflow.com/questions/9482773/web-service-without-adding-a-reference
/// </summary>
public class WebService
{
    public string Url { get; private set; }
    public string Method { get; private set; }
    public Dictionary<string, string> Params = new Dictionary<string, string>();
    public XDocument ResponseSOAP = XDocument.Parse("<root/>");
    public XDocument ResultXML = XDocument.Parse("<root/>");
    public string ResultString = String.Empty;

    private Cursor InitialCursorState;

    public WebService()
    {
        Url = String.Empty;
        Method = String.Empty;
    }
    public WebService(string baseUrl)
    {
        Url = baseUrl;
        Method = String.Empty;
    }
    public WebService(string baseUrl, string methodName)
    {
        Url = baseUrl;
        Method = methodName;
    }

    // Public API

    /// <summary>
    /// Adds a parameter to the WebMethod invocation.
    /// </summary>
    /// <param name="name">Name of the WebMethod parameter (case sensitive)</param>
    /// <param name="value">Value to pass to the paramenter</param>
    public void AddParameter(string name, string value)
    {
        Params.Add(name, value);
    }

    public void Invoke()
    {
        Invoke(Method, true);
    }

    /// <summary>
    /// Using the base url, invokes the WebMethod with the given name
    /// </summary>
    /// <param name="methodName">Web Method name</param>
    public void Invoke(string methodName)
    {
        Invoke(methodName, true);
    }

    /// <summary>
    /// Cleans all internal data used in the last invocation, except the WebService's URL.
    /// This avoids creating a new WebService object when the URL you want to use is the same.
    /// </summary>
    public void CleanLastInvoke()
    {
        ResponseSOAP = ResultXML = null;
        ResultString = Method = String.Empty;
        Params = new Dictionary<string, string>();
    }

    #region Helper Methods

    /// <summary>
    /// Checks if the WebService's URL and the WebMethod's name are valid. If not, throws ArgumentNullException.
    /// </summary>
    /// <param name="methodName">Web Method name (optional)</param>
    private void AssertCanInvoke(string methodName = "")
    {
        if (Url == String.Empty)
            throw new ArgumentNullException("You tried to invoke a webservice without specifying the WebService's URL.");
        if ((methodName == "") && (Method == String.Empty))
            throw new ArgumentNullException("You tried to invoke a webservice without specifying the WebMethod.");
    }

    private void ExtractResult(string methodName)
    {
        // Selects just the elements with namespace http://tempuri.org/ (i.e. ignores SOAP namespace)
        XmlNamespaceManager namespMan = new XmlNamespaceManager(new NameTable());
        namespMan.AddNamespace("foo", "http://tempuri.org/");

        XElement webMethodResult = ResponseSOAP.XPathSelectElement("//foo:" + methodName + "Result", namespMan);
        // If the result is an XML, return it and convert it to string
        if (webMethodResult.FirstNode.NodeType == XmlNodeType.Element)
        {
            ResultXML = XDocument.Parse(webMethodResult.FirstNode.ToString());
            ResultXML = Utils.RemoveNamespaces(ResultXML);
            ResultString = ResultXML.ToString();
        }
        // If the result is a string, return it and convert it to XML (creating a root node to wrap the result)
        else
        {
            ResultString = webMethodResult.FirstNode.ToString();
            ResultXML = XDocument.Parse("<root>" + ResultString + "</root>");
        }
    }

    /// <summary>
    /// Invokes a Web Method, with its parameters encoded or not.
    /// </summary>
    /// <param name="methodName">Name of the web method you want to call (case sensitive)</param>
    /// <param name="encode">Do you want to encode your parameters? (default: true)</param>
    private void Invoke(string methodName, bool encode)
    {
        AssertCanInvoke(methodName);
        string soapStr =
            @"<?xml version=""1.0"" encoding=""utf-8""?>
                <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
                   xmlns:xsd=""http://www.w3.org/2001/XMLSchema""
                   xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
                  <soap:Body>
                    <{0} xmlns=""http://tempuri.org/"">
                      {1}
                    </{0}>
                  </soap:Body>
                </soap:Envelope>";

        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(Url);
        req.Headers.Add("SOAPAction", "\"http://tempuri.org/" + methodName + "\"");
        req.ContentType = "text/xml;charset=\"utf-8\"";
        req.Accept = "text/xml";
        req.Method = "POST";

        using (Stream stm = req.GetRequestStream())
        {
            string postValues = "";
            foreach (var param in Params)
            {
                if (encode) postValues += string.Format("<{0}>{1}</{0}>", HttpUtility.HtmlEncode(param.Key), HttpUtility.HtmlEncode(param.Value));
                else postValues += string.Format("<{0}>{1}</{0}>", param.Key, param.Value);
            }

            soapStr = string.Format(soapStr, methodName, postValues);
            using (StreamWriter stmw = new StreamWriter(stm))
            {
                stmw.Write(soapStr);
            }
        }

        using (StreamReader responseReader = new StreamReader(req.GetResponse().GetResponseStream()))
        {
            string result = responseReader.ReadToEnd();
            ResponseSOAP = XDocument.Parse(Utils.UnescapeString(result));
            ExtractResult(methodName);
        }
    }

    /// <summary>
    /// This method should be called before each Invoke().
    /// </summary>
    internal void PreInvoke()
    {
        CleanLastInvoke();
        InitialCursorState = Cursor.Current;
        Cursor.Current = Cursor.WaitCursor;
        // feel free to add more instructions to this method
    }

    /// <summary>
    /// This method should be called after each (successful or unsuccessful) Invoke().
    /// </summary>
    internal void PosInvoke()
    {
        Cursor.Current = InitialCursorState;
        // feel free to add more instructions to this method
    }

    #endregion
}

And lastly the Utils class:

public static class Utils
{
    /// <summary>
    /// Remove all xmlns:* instances from the passed XmlDocument to simplify our xpath expressions
    /// </summary>
    public static XDocument RemoveNamespaces(XDocument oldXml)
    {
        // FROM: http://social.msdn.microsoft.com/Forums/en-US/bed57335-827a-4731-b6da-a7636ac29f21/xdocument-remove-namespace?forum=linqprojectgeneral
        try
        {
            XDocument newXml = XDocument.Parse(Regex.Replace(
                oldXml.ToString(),
                @"(xmlns:?[^=]*=[""][^""]*[""])",
                "",
                RegexOptions.IgnoreCase | RegexOptions.Multiline)
            );
            return newXml;
        }
        catch (XmlException error)
        {
            throw new XmlException(error.Message + " at Utils.RemoveNamespaces");
        } 
    }

    /// <summary>
    /// Remove all xmlns:* instances from the passed XmlDocument to simplify our xpath expressions
    /// </summary>
    public static XDocument RemoveNamespaces(string oldXml)
    {
        XDocument newXml = XDocument.Parse(oldXml);
        return RemoveNamespaces(newXml);
    }

    /// <summary>
    /// Converts a string that has been HTML-enconded for HTTP transmission into a decoded string.
    /// </summary>
    /// <param name="escapedString">String to decode.</param>
    /// <returns>Decoded (unescaped) string.</returns>
    public static string UnescapeString(string escapedString)
    {
        return HttpUtility.HtmlDecode(escapedString);
    }
}

31 replies on “Calling a Web Method in C# without Service Reference”

Hi I am getting error in HttpUtility what is the name space of this and

ResponseSOAP.XPathSelectElement – Error: ResponseSOAP does not contains the defination of WaXPathSelectElement tCursor

Cursor.WaitCursor – Error: cursor does not contains the defination of WaitCursor

Regards
Qasim

First of all congratulations for this great job, which already helped me a lot. However now I have a question: How should I add a complex object to web method parameters? I already tried to serialize to JSON and to XML and nothing works…

This works for my web services on local host? How do i make it work for online web services?

indeed: awesome. I am looking for something like this also and (like Orlando) add WS security to the request.

Thanks for your work!Now I want to add WS-Security into Soap message.What can I do in your codes?

Your article is very helpful to me..
But if my web service return a passed by reference value, how should I retrieve it?

For example, Webservice method

public string GetMessage(string message, ref string strOutput)
{
strOutput = “Test”;
return “Hello World: ” + message;
}

Are you sure you want to return a passed by reference output? I doubt the code I presented handles that kind of parameters. Why don’t you return an XML instead? Something like

It looks like this other service also has a “request” element wrapped around the action values but in this case its not called “request” its more specific to the name of the service so that part isn’t going to be dynamic. I can see this getting very complicated as the body get more and more elaborate. And, I don’t see enough info in the wsdl to figure out everything that would need to be passed.

I enhanced the code a bit to support both credentials and custom namespace but I do have one issue. The “body” actually has a “request” element inside it and it requires a custom namespace. The main custom namespace is in the WSDL as is the actual Action name (tempuri is not used on this particular service). However the namespace that goes in the request is not incuded in the WSDL so I don’t know how the end user would know to supply it. I doubt you have help with more details which I probably cant supply but, without the namesspace on the the “request” element, the service returns a 200 but no data. I’m going to try another service that I think will likely work but this isn’t going to work for everything I need it to because I can’t (currently) determine how to pass the correct namespace since its not in the WSDL. Using a service reference figures it out somehow.

it needs to look something like this:

<{0} xmlns=””” + serviceNamespace + @”””>

{1}

&lt;/{0}&gt;

Thanks
Steve

It’s a good idea to use namespaces. I had some problems with them, and since at the time I had a short deadline, I used the default namespace together with the custom RemoveNamespaces utils method. Good luck Steve ;)

Awesome work Diogo! I have to complete a task by today, and I was stuggling to get the wsdl or proxy client file due to remote technical issues. I am going to try this and see if it works! Will update here the results if everything goes smooth.

Nice work! Very helpful. I’ll be implementing this today.

However, can you define [Utils.RemoveNamespaces(ResultXML)] for us please?

Comments are closed.