Calling a SOAP Service from ASP.NET Core or .NET Core

When developing with ASP.NET Core or .NET Core if you need to generate a SOAP service reference you have a few options. You can use ServiceModel Metadata Utility Tool (Svcutil.exe) to generate the reference code in a console window. I use this method quite often because when I begin interacting with a SOAP service I like to use LINQPad as a playground to explore the service. The following command generates client code from a running service or online metadata documents.

svcutil http://service/metadataEndpoint

For LINQPad I use the following command to merge the config information into the LINQPad config file.

svcutil /config:"\LINQPad\LINQPad.config" /mergeConfig http://service/metadataEndpoint

I then use the C# compiler (csc.exe) to build the code into a library I can reference in LINQPad or any other project I would like by using the following command.

csc /target:library File.cs

Svcutil.exe has a lot of other great usages besides just code generation including the ability to export metadata for services, contracts, and data types in compiled services and the ability to validate service implementations without hosting the service.

Another way to generate the SOAP reference code is using the Microsoft WCF Web Service Reference Provider. This is an extension from the WCF Core Team that you can install from the Visual Studio Marketplace or within Visual Studio by going to Tools -> Extensions and Updates and searching for Microsoft WCF Web Service Reference Provider.

Extensions and Updates

Install Extension

Once you have the extension installed you add the service reference by right-clicking Connected Services and clicking Add Connected Service.

Step 1

The connected services window will open up and you will click Microsoft WCF Web Service Reference Provider.

Add WCF Web Service

The Configure WCF Web Service Reference wizard will open. You will need to enter the Uri of the service you want to reference and click Go. Change the NameSpace to something appropriate for your application and click Next.

Step 2

In this step you are offered a few settings for further refining the configuration for the generated service reference. Of particular interest is the option to Reuse types in reference assemblies. This is useful when types needed for generating the service reference code are already referenced in your project, and it is necessary to reuse the existing types to avoid a compile-time type clash. I typically leave these options alone.

Step 3

Click next and you are taken to the Client Option form where you can adjust the access level for the generated classes. Click Finish when you are done.

Step 4

This will download metadata from the SOAP service, generate the service reference code in a file named reference.cs, and add it to your project under the Connected Services node. The project file (.csproj) will also be updated with NuGet package references required for your project to compile and run on the target platform.

You can now create instances of the WCF client types generated by the tool and communicate with your web service as desired.

Some Bumps in the Road

There are a few other items I want to point out that tripped me up. First the tooling that generates the client proxy code lacks support for the SOAP headers which is typically used by APIs for things like authentication. Daniel Stolt wrote up a good article on how to use IClientMessageInspector to add the required headers. Another method and the one I used most recently was to use OperationContextScope to add the headers I needed. You can see in the code below we create an instance of our SOAP client. We then create an instance of the clients credentials object. We then in a using block create a new OperationContextScope which allows us to interact with our OperationContext object. We then create the message header we need for the credentials and add it to the outgoing message headers. Now we can call our SOAP service and handle the result.

//Create instance of SOAP client
ImportManager.ImportManagerSoapClient soapClient = new ImportManagerSoapClient(new BasicHttpsBinding(BasicHttpsSecurityMode.Transport), new EndpointAddress("https://soapservice.com/ImportManager.asmx"));
//Create instance of credentials
ImportManager.SC_Credentials credentials = new ImportManager.SC_Credentials();

using (new OperationContextScope(soapClient.InnerChannel))
{
   //Create message header containing the credentials
   var header = MessageHeader.CreateHeader("SC_Credentials",
   "http://soapservice.com", credentials, new CFMessagingSerializer(typeof(SC_Credentials)));
   //Add the credentials message header to the outgoing request
   OperationContext.Current.OutgoingMessageHeaders.Add(header);

   try
   {
      var result = soapClient.SomeMethod();
   }
   catch (Exception ex)
   {
      throw;
   }
}

I want to point out the second issue I ran into and which took me a couple hours to resolve. In the MessageHeader.CreateHeader() method call above we are passing CFMessagingSerializer(typeof(Sc_Credentials)). This is a custom class that derives from XmlObjectSerializer and uses XmlSerializer internally to serialize the credentials object. This was required because the SOAP service I needed to call required many of the object’s members to be XML attributes. Unfortunately the DataContractSerializer used by WCF does not support XML attributes. So to shape the XML as we needed it we create a wrapper class for the XmlSerializer that makes it look and work like an XmlObjectSerializer. XmlObjectSerializer is an abstract class with only a few methods we need to implement. Luckily I came across an article from Andrew Arnott where he explains the issue and has the code for CFMessagingSerlizer class.

namespace Microsoft.ServiceModel.Samples
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Text;
    using System.Xml;
    using System.Xml.Serialization;
    using System.Runtime.Serialization;

    public class CFMessagingSerializer : XmlObjectSerializer
    {
        readonly Type objectType;
        XmlSerializer serializer;

        public CFMessagingSerializer(Type objectType)
            : this(objectType, null, null)
        {
        }

        public CFMessagingSerializer(Type objectType, string wrapperName, string wrapperNamespace)
        {
            if (objectType == null)
                throw new ArgumentNullException("objectType");
            if ((wrapperName == null) != (wrapperNamespace == null))
                throw new ArgumentException("wrapperName and wrapperNamespace must be either both null or both non-null.");
            if (wrapperName == string.Empty)
                throw new ArgumentException("Cannot be the empty string.", "wrapperName");

            this.objectType = objectType;
            if (wrapperName != null)
            {
                XmlRootAttribute root = new XmlRootAttribute(wrapperName);
                root.Namespace = wrapperNamespace;
                this.serializer = new XmlSerializer(objectType, root);
            }
            else
                this.serializer = new XmlSerializer(objectType);
        }

        public override bool IsStartObject(XmlDictionaryReader reader)
        {
            throw new NotImplementedException();
        }

        public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
        {
            Debug.Assert(serializer != null);
            if (reader == null) throw new ArgumentNullException("reader");
            if (!verifyObjectName)
                throw new NotSupportedException();

            return serializer.Deserialize(reader);
        }

        public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
        {
            throw new NotImplementedException();
        }

        public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            if (writer.WriteState != WriteState.Element)
                throw new SerializationException(string.Format("WriteState '{0}' not valid. Caller must write start element before serializing in contentOnly mode.",
                    writer.WriteState));
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (XmlDictionaryWriter bufferWriter = XmlDictionaryWriter.CreateTextWriter(memoryStream, Encoding.UTF8))
                {
                    serializer.Serialize(bufferWriter, graph);
                    bufferWriter.Flush();
                    memoryStream.Position = 0;
                    using (XmlReader reader = new XmlTextReader(memoryStream))
                    {
                        reader.MoveToContent();
                        writer.WriteAttributes(reader, false);
                        if (reader.Read()) // move off start node (we want to skip it)
                        {
                            while (reader.NodeType != XmlNodeType.EndElement) // also skip end node.
                                writer.WriteNode(reader, false); // this will take us to the start of the next child node, or the end node.
                            reader.ReadEndElement(); // not necessary, but clean
                        }
                    }
                }
            }
        }

        public override void WriteEndObject(XmlDictionaryWriter writer)
        {
            throw new NotImplementedException();
        }

        public override void WriteObject(XmlDictionaryWriter writer, object graph)
        {
            Debug.Assert(serializer != null);
            if (writer == null) throw new ArgumentNullException("writer");
            serializer.Serialize(writer, graph);
        }
    }
}

Well that is all for today and I hope this will be helpful for others.

Related Links

One thought on “Calling a SOAP Service from ASP.NET Core or .NET Core

Leave a Reply to joshuagarrison27 Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s