A client has asked you to consume a new web service. “Nice one!”, you think. “That is nice and straight forward.”

Consuming a REST service would be nice and straight forward, but it is a SOAP service.

I could go into detail about what the differences are between SOAP and REST, but for the purpose of this post you just need to know they are both ways to send data between applications and SOAP is not as popular as REST has become so you may never have been asked to do this before.

It is not as bad as it sounds though and SOAP is still a great way to provide a service. It is just not as easy to consume as a REST service.

I develop using a Mac, so this post makes the assumption you do too or that you will have enough detail here to do the equivalent on another operating system. Where I can I will try and give you an equivalent.

Accessing the web service

To use a SOAP web service you need the link (web address) to that service.

For this post I will use a test service that provides basic mathematical functions (add, subtract, multiply and divide) provided by DNE Online:

http://www.dneonline.com/calculator.asmx

If you click on the link above you will get a basic page explaining what that service does and what functions are available.

Now, onto the important piece: the WSDL file. A Web Service Description Language (WSDL) file, usually an XML file, is a file that technically describes the service.

This will include things like, but not limited to, the methods that are available; what parameters you need to pass to those methods; what error handling is required; the structure of any models you need to use and any service configuration or authentication that is required.

To access the WSDL file a link is usually provided on the service page itself, but the majority of WSDL files can be accessed by simply appending ?wsdl to the end of the web service address, so in the case of the service we are using above we can access the WSDL file by following the link to:

http://www.dneonline.com/calculator.asmx?wsdl

Creating the client in C#

We have the WSDL file now, but how do we use it?

We need to write a client that will handle connecting to the service and provide us with the necessary action methods to allow us to use the service.

With our calculator service example above, we only have four methods, so it would be quite straight forward to do, but we need to think bigger and think how we would do this if we had a much more complex service to work with.

Fortunately Microsoft has a very useful tool that allows us to scaffold the client based on the WSDL file. This is a great time-saver and takes any human error out of the equation. It handles all the models, action methods, namespaces, exception handling, service contracts and the configuration and bindings of the service endpoints.

This tool is called the ServiceModel Metadata Utility Tool (Svcutil).

At the time of writing the Svcutil is at version 2.0.x and is not supported in projects using ASP.NET Core 2.2, so you will need to use a temporary project to create the scaffolded client.

We will do this anyway as it is easier to take the scaffolded files and drop them in our project. They will need tidying up anyway.

Let’s start by creating a directory, navigating into it and creating a new console project. We will use a console project, so we can easily test the service we create.

mkdir DemoCalculator
cd DemoCalculator
dotnet new console

Then you will need to install the dotnet-svcutil package.

dotnet tool install --global dotnet-svcutil

Now we have everything we need to scaffold the SOAP service into our project. Run the svcutil command with the address of your wsdl. In this case we will use our example from above.

dotnet svcutil http://www.dneonline.com/calculator.asmx\?wsdl

If you have any problems accessing this file I have included the WSDL in my Git account at:

https://raw.githubusercontent.com/adamstacey/DemoCalculator/develop/calculator.wsdl

At the time or writing the example on the Microsoft site says to use the command dotnet-svcutil this did not work for me and I had to use donet svcutil.

If this has all worked for you then you should now have a new file at ../DemoCalculator/ServiceReference/Reference.cs.

Tidying up your client

You can access all the files I reference below from my personal Git account at:

https://github.com/adamstacey/DemoCalculator

If you open the Reference.cs you will see it looks a bit of a mess and has lots of classes rolled into one. We will tidy that up.

Looking at the class we have some interfaces, enumerations, a service, a channel, some models and a client. Let’s start by adding the following folders to our project:

  • Clients
  • Interfaces
  • Repositories

Interfaces

If we start with the interfaces we have two:

  • CalculatorSoap
  • CalculatorSoapChannel

Add the interface CalculatorSoap to a new file called ICalculatorService.cs to the Interfaces folder. We add the prefix as that tells us the file is an interface.

[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="ServiceReference.CalculatorSoap")]
public interface CalculatorSoap
{
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/Add", ReplyAction="*")]
    System.Threading.Tasks.Task<int> AddAsync(int intA, int intB);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/Subtract", ReplyAction="*")]
    System.Threading.Tasks.Task<int> SubtractAsync(int intA, int intB);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/Multiply", ReplyAction="*")]
    System.Threading.Tasks.Task<int> MultiplyAsync(int intA, int intB);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/Divide", ReplyAction="*")]
    System.Threading.Tasks.Task<int> DivideAsync(int intA, int intB);
}

Couple of rules to tidy up the files:

  • Add a namespace and for the purposes of this, I have kept it simple and made the namespace the same for all files.
  • Remove the GeneratedCodeAttribute and DebuggerStepThroughAttribute decorators as we don’t need them.
  • Remove the fully-qualified namespaces and use usings, for example instead of System.ServiceModel.ServiceContractAttribute reduce it to just ServiceContractAttribute and add using System.ServiceModel;.
  • Simplify the attribute names. The svcutil tool is quite “old-school” and it still uses the attribute ending on decorator names. For example, ServiceContractAttribute can be simplified to ServiceContract.
  • Add comments as necessary. Although not necessary it is good practice and if you use something like StyleCop you will need to.

You should then end up with something that looks like this:

// -----------------------------------------------------------------------
// <copyright company="Web Illumination Ltd." file="ICalculatorService.cs">
//     Web Illumination Ltd. All rights reserved.
// </copyright>
// <author>
//      Adam Stacey, Solution Architect
//      me@adamstacey.co.uk
// </author>
// -----------------------------------------------------------------------
namespace AdamStacey.DemoCalculator
{
    #region Usings
    using System.ServiceModel;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Calculator service.
    /// </summary>
    [ServiceContract(ConfigurationName = "AdamStacey.DemoCalculator.ICalculatorService")]
    public interface ICalculatorService
    {
        /// <summary>
        /// Adds asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        [OperationContract(Action = "http://tempuri.org/Add", ReplyAction = "*")]
        Task<int> AddAsync(int intA, int intB);

        /// <summary>
        /// Subtracts asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        [OperationContract(Action = "http://tempuri.org/Subtract", ReplyAction = "*")]
        Task<int> SubtractAsync(int intA, int intB);

        /// <summary>
        /// Multiplies asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        [OperationContract(Action = "http://tempuri.org/Multiply", ReplyAction = "*")]
        Task<int> MultiplyAsync(int intA, int intB);

        /// <summary>
        /// Divides asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        [OperationContract(Action = "http://tempuri.org/Divide", ReplyAction = "*")]
        Task<int> DivideAsync(int intA, int intB);
    }
}

Create a new file for CalculatorSoapChannel named ICalculatorChannel.cs and add it to the Interfaces folder. Once tidied it should look something like:

// -----------------------------------------------------------------------
// <copyright company="Web Illumination Ltd." file="ICalculatorChannel.cs">
//     Web Illumination Ltd. All rights reserved.
// </copyright>
// <author>
//      Adam Stacey, Solution Architect
//      me@adamstacey.co.uk
// </author>
// -----------------------------------------------------------------------
namespace AdamStacey.DemoCalculator
{
    #region Usings
    using System.ServiceModel;
    #endregion

    /// <summary>
    /// Calculator channel.
    /// </summary>
    public interface ICalculatorChannel : ICalculatorService, IClientChannel
    {
    }
}

Clients

The next thing we need to do is tidy up the client. We will remove the bindings as we will move this to a separate repository file where we can abstract the methods we want to use out, so if we want to change the service we use at a later date we can without breaking our contract. Think SOLID people! I will discuss SOLID principles at a later date in more detail.

The tidied up version should look something like this:

// -----------------------------------------------------------------------
// <copyright company="Web Illumination Ltd." file="CalculatorClient.cs">
//     Web Illumination Ltd. All rights reserved.
// </copyright>
// <author>
//      Adam Stacey, Solution Architect
//      me@adamstacey.co.uk
// </author>
// -----------------------------------------------------------------------
namespace AdamStacey.DemoCalculator
{
    #region Usings
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Calculator client.
    /// </summary>
    public class CalculatorClient : ClientBase<ICalculatorService>, ICalculatorService
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorClient"/> class.
        /// </summary>
        public CalculatorClient()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorClient"/> class.
        /// </summary>
        /// <param name="endpointConfigurationName">Endpoint configuration name.</param>
        public CalculatorClient(string endpointConfigurationName) : base(endpointConfigurationName)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorClient"/> class.
        /// </summary>
        /// <param name="endpointConfigurationName">Endpoint configuration name.</param>
        /// <param name="remoteAddress">Remote address.</param>
        public CalculatorClient(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorClient"/> class.
        /// </summary>
        /// <param name="endpointConfigurationName">Endpoint configuration name.</param>
        /// <param name="remoteAddress">Remote address.</param>
        public CalculatorClient(string endpointConfigurationName, EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorClient"/> class.
        /// </summary>
        /// <param name="binding">The binding.</param>
        /// <param name="endpoint">The endpoint.</param>
        public CalculatorClient(Binding binding, EndpointAddress endpoint) : base(binding, endpoint)
        {
        }

        /// <summary>
        /// Adds asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> AddAsync(int intA, int intB)
        {
            return await Channel.AddAsync(intA, intB);
        }

        /// <summary>
        /// Subtracts asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> SubtractAsync(int intA, int intB)
        {
            return await Channel.SubtractAsync(intA, intB);
        }

        /// <summary>
        /// Multiplies asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> MultiplyAsync(int intA, int intB)
        {
            return await Channel.MultiplyAsync(intA, intB);
        }

        /// <summary>
        /// Divides asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> DivideAsync(int intA, int intB)
        {
            return await Channel.DivideAsync(intA, intB);
        }
    }
}

Repositories

In the repository, we then set up the bindings to communicate with the calculator service.

We also set up the methods we want to provide, which in this case is the same as the methods in the client.

// -----------------------------------------------------------------------
// <copyright company="Web Illumination Ltd." file="CalculatorRepository.cs">
//     Web Illumination Ltd. All rights reserved.
// </copyright>
// <author>
//      Adam Stacey, Solution Architect
//      me@adamstacey.co.uk
// </author>
// -----------------------------------------------------------------------
namespace AdamStacey.DemoCalculator
{
    #region Usings
    using System;
    using System.ServiceModel;
    using System.Threading.Tasks;
    using System.Xml;
    #endregion

    /// <summary>
    /// Calculator repository.
    /// </summary>
    public class CalculatorRepository
    {
        /// <summary>
        /// The <see cref="ICalculatorChannel"/> proxy.
        /// </summary>
        private readonly ICalculatorChannel proxy;

        /// <summary>
        /// Initializes a new instance of the <see cref="T:AdamStacey.DemoCalculator.CalculatorRepository"/> class.
        /// </summary>
        /// <param name="endpointAddress">Endpoint address.</param>
        /// <param name="timeout">The timeout.</param>
        public CalculatorRepository(string endpointAddress, double timeout)
        {
            BasicHttpBinding binding = new BasicHttpBinding
            {
                SendTimeout = TimeSpan.FromSeconds(timeout),
                MaxBufferSize = int.MaxValue,
                MaxReceivedMessageSize = int.MaxValue,
                AllowCookies = true,
                ReaderQuotas = XmlDictionaryReaderQuotas.Max
            };

            binding.Security.Mode = BasicHttpSecurityMode.Transport;

            EndpointAddress address = new EndpointAddress(endpointAddress);

            ChannelFactory<ICalculatorChannel> factory = new ChannelFactory<ICalculatorChannel>(binding, address);

            this.proxy = factory.CreateChannel();
        }

        /// <summary>
        /// Adds asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> AddAsync(int intA, int intB)
        {
            return await this.proxy.AddAsync(intA, intB);
        }

        /// <summary>
        /// Subtracts asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> SubtractAsync(int intA, int intB)
        {
            return await this.proxy.SubtractAsync(intA, intB);
        }

        /// <summary>
        /// Multiplies asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> MultiplyAsync(int intA, int intB)
        {
            return await this.proxy.MultiplyAsync(intA, intB);
        }

        /// <summary>
        /// Divides asynchronously.
        /// </summary>
        /// <returns>The calculation.</returns>
        /// <param name="intA">Int a.</param>
        /// <param name="intB">Int b.</param>
        public async Task<int> DivideAsync(int intA, int intB)
        {
            return await this.proxy.DivideAsync(intA, intB);
        }
    }
}

At this point we are largely good to go and can start testing the service. As we have setup a basic console app this is nice and easy to do.

Testing the service

To test the service we can simply use the Program.cs to write a basic console app:

// -----------------------------------------------------------------------
// <copyright company="Web Illumination Ltd." file="Program.cs">
//     Web Illumination Ltd. All rights reserved.
// </copyright>
// <author>
//      Adam Stacey, Solution Architect
//      me@adamstacey.co.uk
// </author>
// -----------------------------------------------------------------------
namespace AdamStacey.DemoCalculator
{
    #region Usings
    using System;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Test program.
    /// </summary>
    class Program
    {
        /// <summary>
        /// The entry point of the program, where the program control starts and ends.
        /// </summary>
        /// <param name="args">The command-line arguments.</param>
        static void Main(string[] args)
        {
            Console.WriteLine("Welcome to the Calculator!");

            RunCalculation();
        }

        /// <summary>
        /// Runs the calculation.
        /// </summary>
        static void RunCalculation()
        {
            CalculatorRepository calculatorRepository = new CalculatorRepository("http://www.dneonline.com/calculator.asmx", 1000);

            Console.WriteLine("#####################################");
            Console.WriteLine("What calculation would you like to perform:");
            Console.WriteLine("  1. Addition");
            Console.WriteLine("  2. Subtraction");
            Console.WriteLine("  3. Multiplication");
            Console.WriteLine("  4. Division");

            Console.WriteLine("Enter the number of your decision:");
            int decision = Convert.ToInt32(Console.ReadLine());

            Console.WriteLine("Enter your first number:");
            int intA = Convert.ToInt32(Console.ReadLine());

            Console.WriteLine("Enter your second number:");
            int intB = Convert.ToInt32(Console.ReadLine());

            Task<int> calculation;

            switch (decision)
            {
                case 1:
                    calculation = calculatorRepository.AddAsync(intA, intB);
                    calculation.Wait();
                    Console.WriteLine($"{intA} + {intB} = {calculation.Result}");
                    break;

                case 2:
                    calculation = calculatorRepository.SubtractAsync(intA, intB);
                    calculation.Wait();
                    Console.WriteLine($"{intA} - {intB} = {calculation.Result}");
                    break;

                case 3:
                    calculation = calculatorRepository.MultiplyAsync(intA, intB);
                    calculation.Wait();
                    Console.WriteLine($"{intA} × {intB} = {calculation.Result}");
                    break;

                case 4:
                    if (intB == 0)
                    {
                        Console.WriteLine("Error: You cannot divide a number by 0!");
                    }
                    else
                    {
                        calculation = calculatorRepository.DivideAsync(intA, intB);
                        calculation.Wait();
                        Console.WriteLine($"{intA} ÷ {intB} = {calculation.Result}");
                    }

                    break;

                default:
                    Console.WriteLine("Error: You must enter a number between 1 and 4!");
                    break;
            }

            Console.WriteLine("Would you like to do another calculation? Enter Y for yes and anything else for no:");

            if (Console.ReadLine() == "Y")
            {
                RunCalculation();
            }
            else
            {
                Console.WriteLine("#####################################");
                Console.WriteLine("Thank you for using the calculator!");
            }
        }
    }
}

If all goes well, you should be able to run the app and enter some numbers to get some calculations.

Please note this is a very basic test example and the code is not pretty. Feel free to use it and improve it.

Next steps

For the purposes of demonstrating the use of a SOAP service via a WSDL file we have used a simple service.

However, the chances are that the services you may look at will be more complicated and instead of passing simple parameters it may require the use of specific models to store values for the requests and the responses.

The other thing to consider is the configuration. When you write a client you may want to use a model (or a provider class) to store the configuration data. This could be the endpoint, the timeout value and any other configuration values you may use.

References

Photo credit

unsplash-logoClem Onojeghuo

Adam Stacey

I am Adam Stacey, the guy behind AdNav! I setup AdNav as a way to write up any technical challenges, how I overcame them, opinions on tech and much rambling. I try and cut through any technical jargon to make it friendly and easy to understand.

View all posts

Add comment

Your email address will not be published. Required fields are marked *

Adam Stacey

I am Adam Stacey, the guy behind AdNav! I setup AdNav as a way to write up any technical challenges, how I overcame them, opinions on tech and much rambling. I try and cut through any technical jargon to make it friendly and easy to understand.