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.
Once you have the extension installed you add the service reference by right-clicking Connected Services and clicking Add Connected Service.
The connected services window will open up and you will click Microsoft WCF Web Service Reference Provider.
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.
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.
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.
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
- Calling Salesforce SOAP APIs from a .NET Core or UWP App
- Using the XmlSerializer as an XmlObjectSerializer with WCF
- Microsoft WCF Web Service Reference Provider
- Add header information to my Web Service Reference request
- Using the XmlSerializer Class
- ASP.NET Core : Getting Clean with SOAP
- ServiceModel Metadata Utility Tool (Svcutil.exe)
- Command-line build with csc.exe
2 responses to “Calling a SOAP Service from ASP.NET Core or .NET Core”
Did you find that some methods were missing in Core versus when you import a wsdl file or something in the .Net Framework projects?
LikeLike
Hey mate,
I thinks I’ve ran on the same problem. Please check this. https://stackoverflow.com/questions/52908503/wcf-in-asp-net-core-2-0-could-not-establish-trust-relationship-for-the-ssl-tls
Any help will be most appreciated.
LikeLike