SOAP
11 minute read
Introduction to SOAP
SOAP (Simple Object Access Protocol) is a standardized, XML-based messaging protocol for exchanging structured information in web services. Developed in 1998 and later maintained by the World Wide Web Consortium (W3C), SOAP was designed to enable communication between applications running on different operating systems, with different technologies and programming languages.
Unlike REST, which is an architectural style, SOAP is a protocol with specific standards and rules. It typically works with HTTP, but can also operate over other protocols such as SMTP, TCP, or JMS.
Key Features of SOAP
- XML-Based: All SOAP messages use XML format
- Protocol Independence: Can work over different transport protocols (HTTP, SMTP, TCP)
- Strongly Typed: Enforces strict contracts through WSDL
- Built-in Error Handling: Through SOAP faults
- Security: WS-Security provides enterprise-level security features
- Reliability: WS-ReliableMessaging ensures message delivery
- Transaction Support: Through WS-AtomicTransaction and WS-Coordination
- Stateless by Default: But supports stateful operations if needed
- Extensibility: Through SOAP extensions and WS-* specifications
SOAP vs REST
Here’s how SOAP compares to REST:
| Feature | SOAP | REST |
|---|---|---|
| Format | XML only | Any format (JSON, XML, HTML, etc.) |
| Protocol | Multiple protocols | HTTP primarily |
| Service Definition | WSDL (required) | OpenAPI/Swagger (optional) |
| Bandwidth | Higher (XML verbose) | Lower (typically JSON) |
| Cache | Not cacheable | Cacheable |
| Security | WS-Security, enterprise features | HTTPS, JWT, OAuth |
| Transaction Support | Built-in | Must implement custom |
| Failure Recovery | Retry logic, error info | Typically none |
| Performance | Slower due to XML parsing | Faster, lighter weight |
| Learning Curve | Steeper | Simpler |
When to Use SOAP
SOAP is particularly well-suited for:
- Enterprise Integration: When connecting complex enterprise systems
- Formal Contracts: When you need a strict, enforceable contract
- Stateful Operations: When maintaining state across operations is required
- Reliable Messaging: When guaranteed delivery is essential
- Complex Transactions: When you need ACID transaction support
- Advanced Security: When WS-Security features are needed
- Independent Transport: When you need to use non-HTTP transport layers
SOAP Message Structure
A SOAP message is an XML document with a specific structure:
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header>
<!-- Optional header information -->
<auth:Credentials xmlns:auth="http://example.org/auth">
<auth:Username>username</auth:Username>
<auth:Password>password</auth:Password>
</auth:Credentials>
</soap:Header>
<soap:Body>
<!-- The main payload of the message -->
<m:GetStockPrice xmlns:m="http://example.org/stock">
<m:StockName>MSFT</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
SOAP Envelope
The root element of every SOAP message. It contains:
- Header: Optional element with metadata (authentication, transaction info)
- Body: Required element containing the actual request or response data
SOAP Fault
When an error occurs, SOAP returns a standardized fault message:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<soap:Fault>
<soap:Code>
<soap:Value>soap:Sender</soap:Value>
</soap:Code>
<soap:Reason>
<soap:Text xml:lang="en">Invalid stock symbol</soap:Text>
</soap:Reason>
<soap:Detail>
<ex:ValidationError xmlns:ex="http://example.org/faults">
<ex:Message>Stock symbol 'XYZ' is not valid</ex:Message>
</ex:ValidationError>
</soap:Detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
WSDL: Web Service Description Language
SOAP services are described using WSDL, an XML-based interface description language. WSDL defines:
- Service endpoints
- Available operations
- Message formats
- Data types
- Binding information
A simplified WSDL example:
<?xml version="1.0"?>
<definitions name="StockQuote"
targetNamespace="http://example.org/stockquote"
xmlns:tns="http://example.org/stockquote"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<!-- Data type definitions -->
<types>
<xsd:schema targetNamespace="http://example.org/stockquote">
<xsd:element name="GetStockPrice">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="StockName" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="StockPriceResponse">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Price" type="xsd:decimal"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
</types>
<!-- Message definitions -->
<message name="GetStockPriceInput">
<part name="parameters" element="tns:GetStockPrice"/>
</message>
<message name="GetStockPriceOutput">
<part name="parameters" element="tns:StockPriceResponse"/>
</message>
<!-- Port type (interface) -->
<portType name="StockQuotePortType">
<operation name="GetStockPrice">
<input message="tns:GetStockPriceInput"/>
<output message="tns:GetStockPriceOutput"/>
</operation>
</portType>
<!-- Binding (protocol details) -->
<binding name="StockQuoteSoapBinding" type="tns:StockQuotePortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="GetStockPrice">
<soap:operation soapAction="http://example.org/GetStockPrice"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<!-- Service definition -->
<service name="StockQuoteService">
<port name="StockQuotePort" binding="tns:StockQuoteSoapBinding">
<soap:address location="http://example.org/stockquote"/>
</port>
</service>
</definitions>
Consuming SOAP Services in .NET
In .NET, you can consume SOAP services using the WCF Client or HttpClient with manual XML handling.
Using Connected Services (Recommended)
Visual Studio and the .NET CLI provide tooling to generate strongly-typed clients from WSDL:
# Install the dotnet-svcutil tool
dotnet tool install --global dotnet-svcutil
# Generate client code from WSDL
dotnet-svcutil http://example.org/stockquote?wsdl -o StockQuoteClient
This generates a strongly-typed client class that you can use directly:
using System;
using System.ServiceModel;
using System.Threading.Tasks;
/// <summary>
/// Demonstrates SOAP client usage with generated proxy.
/// </summary>
public sealed class SoapClientExample
{
/// <summary>
/// Gets stock price using the generated SOAP client.
/// </summary>
public async Task<decimal> GetStockPriceAsync(string stockSymbol)
{
// Create the binding and endpoint
BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress endpoint = new EndpointAddress("http://example.org/stockquote");
// Create the client
StockQuoteServiceClient client = new StockQuoteServiceClient(binding, endpoint);
try
{
// Call the SOAP operation
StockPriceResponse response = await client.GetStockPriceAsync(
new GetStockPrice { StockName = stockSymbol });
return response.Price;
}
finally
{
// Properly close the client
if (client.State == CommunicationState.Faulted)
{
client.Abort();
}
else
{
await client.CloseAsync();
}
}
}
}
Manual SOAP Request with HttpClient
For more control or when tooling is unavailable, use HttpClient directly:
using System;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
/// <summary>
/// SOAP client using HttpClient for manual XML handling.
/// </summary>
public sealed class ManualSoapClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _serviceUrl;
private static readonly XNamespace SoapNs = "http://www.w3.org/2003/05/soap-envelope";
private static readonly XNamespace ServiceNs = "http://example.org/stock";
/// <summary>
/// Initializes a new instance of the <see cref="ManualSoapClient"/> class.
/// </summary>
public ManualSoapClient(HttpClient httpClient, string serviceUrl)
{
_httpClient = httpClient;
_serviceUrl = serviceUrl;
}
/// <summary>
/// Gets the stock price for the specified symbol.
/// </summary>
public async Task<decimal> GetStockPriceAsync(
string stockSymbol,
CancellationToken cancellationToken = default)
{
// Build the SOAP envelope
XDocument soapEnvelope = new XDocument(
new XElement(SoapNs + "Envelope",
new XAttribute(XNamespace.Xmlns + "soap", SoapNs),
new XElement(SoapNs + "Body",
new XElement(ServiceNs + "GetStockPrice",
new XAttribute(XNamespace.Xmlns + "m", ServiceNs),
new XElement(ServiceNs + "StockName", stockSymbol)))));
string soapXml = soapEnvelope.ToString();
// Create HTTP request
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _serviceUrl);
request.Content = new StringContent(soapXml, Encoding.UTF8, "application/soap+xml");
request.Headers.Add("SOAPAction", "http://example.org/GetStockPrice");
// Send request
using HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
// Parse response
string responseXml = await response.Content.ReadAsStringAsync(cancellationToken);
XDocument responseDoc = XDocument.Parse(responseXml);
// Extract the price from the response
XElement? priceElement = responseDoc.Descendants(ServiceNs + "Price").FirstOrDefault();
if (priceElement is null)
{
throw new InvalidOperationException("Price element not found in SOAP response");
}
return decimal.Parse(priceElement.Value);
}
/// <inheritdoc/>
public void Dispose()
{
_httpClient.Dispose();
}
}
Handling SOAP Faults
using System;
using System.Xml.Linq;
/// <summary>
/// Represents a SOAP fault returned by the service.
/// </summary>
public sealed class SoapFaultException : Exception
{
/// <summary>
/// Gets the SOAP fault code.
/// </summary>
public string FaultCode { get; }
/// <summary>
/// Gets the SOAP fault reason.
/// </summary>
public string FaultReason { get; }
/// <summary>
/// Gets the SOAP fault detail.
/// </summary>
public string? FaultDetail { get; }
/// <summary>
/// Initializes a new instance of the <see cref="SoapFaultException"/> class.
/// </summary>
public SoapFaultException(string faultCode, string faultReason, string? faultDetail = null)
: base($"SOAP Fault: {faultCode} - {faultReason}")
{
FaultCode = faultCode;
FaultReason = faultReason;
FaultDetail = faultDetail;
}
}
/// <summary>
/// Helper class for parsing SOAP faults.
/// </summary>
public static class SoapFaultParser
{
private static readonly XNamespace SoapNs = "http://www.w3.org/2003/05/soap-envelope";
/// <summary>
/// Parses a SOAP response and throws if it contains a fault.
/// </summary>
public static void ThrowIfFault(string soapResponse)
{
XDocument doc = XDocument.Parse(soapResponse);
XElement? faultElement = doc.Descendants(SoapNs + "Fault").FirstOrDefault();
if (faultElement is null)
{
return;
}
string faultCode = faultElement
.Element(SoapNs + "Code")?
.Element(SoapNs + "Value")?
.Value ?? "Unknown";
string faultReason = faultElement
.Element(SoapNs + "Reason")?
.Element(SoapNs + "Text")?
.Value ?? "Unknown error";
string? faultDetail = faultElement
.Element(SoapNs + "Detail")?
.ToString();
throw new SoapFaultException(faultCode, faultReason, faultDetail);
}
}
Creating SOAP Services with CoreWCF
For creating SOAP services in modern .NET, use CoreWCF - the community-supported port of WCF to .NET Core/.NET 5+:
dotnet add package CoreWCF.Primitives
dotnet add package CoreWCF.Http
Define the Service Contract
using CoreWCF;
using System.Runtime.Serialization;
using System.Threading.Tasks;
/// <summary>
/// Service contract for the stock quote service.
/// </summary>
[ServiceContract(Namespace = "http://example.org/stockquote")]
public interface IStockQuoteService
{
/// <summary>
/// Gets the current stock price.
/// </summary>
[OperationContract]
Task<StockPriceResponse> GetStockPriceAsync(GetStockPriceRequest request);
/// <summary>
/// Gets the stock history for a date range.
/// </summary>
[OperationContract]
Task<StockHistoryResponse> GetStockHistoryAsync(GetStockHistoryRequest request);
}
/// <summary>
/// Request for getting stock price.
/// </summary>
[DataContract(Namespace = "http://example.org/stockquote")]
public sealed class GetStockPriceRequest
{
/// <summary>
/// Gets or sets the stock symbol.
/// </summary>
[DataMember(Order = 1)]
public string StockSymbol { get; set; } = string.Empty;
}
/// <summary>
/// Response containing stock price.
/// </summary>
[DataContract(Namespace = "http://example.org/stockquote")]
public sealed class StockPriceResponse
{
/// <summary>
/// Gets or sets the stock symbol.
/// </summary>
[DataMember(Order = 1)]
public string StockSymbol { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the current price.
/// </summary>
[DataMember(Order = 2)]
public decimal Price { get; set; }
/// <summary>
/// Gets or sets the price change from previous close.
/// </summary>
[DataMember(Order = 3)]
public decimal Change { get; set; }
/// <summary>
/// Gets or sets the timestamp of the price.
/// </summary>
[DataMember(Order = 4)]
public DateTime Timestamp { get; set; }
}
/// <summary>
/// Request for getting stock history.
/// </summary>
[DataContract(Namespace = "http://example.org/stockquote")]
public sealed class GetStockHistoryRequest
{
/// <summary>
/// Gets or sets the stock symbol.
/// </summary>
[DataMember(Order = 1)]
public string StockSymbol { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the start date.
/// </summary>
[DataMember(Order = 2)]
public DateTime StartDate { get; set; }
/// <summary>
/// Gets or sets the end date.
/// </summary>
[DataMember(Order = 3)]
public DateTime EndDate { get; set; }
}
/// <summary>
/// Response containing stock history.
/// </summary>
[DataContract(Namespace = "http://example.org/stockquote")]
public sealed class StockHistoryResponse
{
/// <summary>
/// Gets or sets the stock symbol.
/// </summary>
[DataMember(Order = 1)]
public string StockSymbol { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the historical prices.
/// </summary>
[DataMember(Order = 2)]
public List<HistoricalPrice> Prices { get; set; } = [];
}
/// <summary>
/// Represents a historical price entry.
/// </summary>
[DataContract(Namespace = "http://example.org/stockquote")]
public sealed class HistoricalPrice
{
/// <summary>
/// Gets or sets the date.
/// </summary>
[DataMember(Order = 1)]
public DateTime Date { get; set; }
/// <summary>
/// Gets or sets the opening price.
/// </summary>
[DataMember(Order = 2)]
public decimal Open { get; set; }
/// <summary>
/// Gets or sets the closing price.
/// </summary>
[DataMember(Order = 3)]
public decimal Close { get; set; }
/// <summary>
/// Gets or sets the high price.
/// </summary>
[DataMember(Order = 4)]
public decimal High { get; set; }
/// <summary>
/// Gets or sets the low price.
/// </summary>
[DataMember(Order = 5)]
public decimal Low { get; set; }
/// <summary>
/// Gets or sets the trading volume.
/// </summary>
[DataMember(Order = 6)]
public long Volume { get; set; }
}
Implement the Service
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
/// <summary>
/// Implementation of the stock quote service.
/// </summary>
public sealed class StockQuoteService : IStockQuoteService
{
private readonly IStockDataProvider _dataProvider;
private readonly ILogger<StockQuoteService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="StockQuoteService"/> class.
/// </summary>
public StockQuoteService(
IStockDataProvider dataProvider,
ILogger<StockQuoteService> logger)
{
_dataProvider = dataProvider;
_logger = logger;
}
/// <inheritdoc/>
public async Task<StockPriceResponse> GetStockPriceAsync(GetStockPriceRequest request)
{
_logger.LogInformation("Getting stock price for {Symbol}", request.StockSymbol);
StockData data = await _dataProvider.GetCurrentPriceAsync(request.StockSymbol);
return new StockPriceResponse
{
StockSymbol = request.StockSymbol,
Price = data.Price,
Change = data.Change,
Timestamp = DateTime.UtcNow
};
}
/// <inheritdoc/>
public async Task<StockHistoryResponse> GetStockHistoryAsync(GetStockHistoryRequest request)
{
_logger.LogInformation(
"Getting stock history for {Symbol} from {Start} to {End}",
request.StockSymbol,
request.StartDate,
request.EndDate);
IReadOnlyList<HistoricalPrice> history = await _dataProvider.GetHistoryAsync(
request.StockSymbol,
request.StartDate,
request.EndDate);
return new StockHistoryResponse
{
StockSymbol = request.StockSymbol,
Prices = history.ToList()
};
}
}
Configure ASP.NET Core with CoreWCF
using CoreWCF;
using CoreWCF.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add CoreWCF services
builder.Services.AddServiceModelServices();
builder.Services.AddServiceModelMetadata();
// Register the service implementation
builder.Services.AddSingleton<IStockDataProvider, StockDataProvider>();
builder.Services.AddTransient<StockQuoteService>();
WebApplication app = builder.Build();
// Configure CoreWCF
app.UseServiceModel(serviceBuilder =>
{
// Add the service endpoint
serviceBuilder.AddService<StockQuoteService>(serviceOptions =>
{
serviceOptions.DebugBehavior.IncludeExceptionDetailInFaults = true;
});
// Configure SOAP binding
BasicHttpBinding basicHttpBinding = new BasicHttpBinding();
serviceBuilder.AddServiceEndpoint<StockQuoteService, IStockQuoteService>(
basicHttpBinding,
"/StockQuoteService.svc");
// Enable WSDL metadata
ServiceMetadataBehavior serviceMetadataBehavior = app.Services
.GetRequiredService<ServiceMetadataBehavior>();
serviceMetadataBehavior.HttpGetEnabled = true;
});
app.Run();
WS-Security with CoreWCF
For enterprise-grade security, configure WS-Security:
using CoreWCF;
using CoreWCF.Channels;
using CoreWCF.Configuration;
using CoreWCF.Security;
using System.Security.Cryptography.X509Certificates;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddServiceModelServices();
builder.Services.AddTransient<StockQuoteService>();
WebApplication app = builder.Build();
app.UseServiceModel(serviceBuilder =>
{
serviceBuilder.AddService<StockQuoteService>();
// Configure secure binding with message-level security
WSHttpBinding secureBinding = new WSHttpBinding(SecurityMode.Message);
secureBinding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
serviceBuilder.AddServiceEndpoint<StockQuoteService, IStockQuoteService>(
secureBinding,
"/SecureStockQuoteService.svc");
// Configure service credentials
serviceBuilder.ConfigureServiceHostBase<StockQuoteService>(host =>
{
host.Credentials.ServiceCertificate.Certificate =
new X509Certificate2("service.pfx", "password");
host.Credentials.ClientCertificate.Authentication.CertificateValidationMode =
X509CertificateValidationMode.ChainTrust;
});
});
app.Run();
SOAP Protocol Comparison
- Formal contract through WSDL
- Built-in error handling (SOAP Faults)
- WS-Security for enterprise security
- Protocol independence (HTTP, SMTP, TCP)
- Transaction support (WS-AtomicTransaction)
- Reliable messaging (WS-ReliableMessaging)
- Strong typing with XSD schemas
- Verbose XML format increases payload size
- Higher complexity than REST
- Steeper learning curve
- Performance overhead from XML parsing
- Limited mobile/browser support
- Legacy perception in modern development
When to Choose SOAP vs REST
| Scenario | Recommended |
|---|---|
| Public APIs for web/mobile apps | REST |
| Enterprise B2B integration | SOAP |
| Banking/Financial services | SOAP |
| Healthcare systems (HL7, etc.) | SOAP |
| Simple CRUD operations | REST |
| Formal contract requirements | SOAP |
| Transaction support needed | SOAP |
| Legacy system integration | SOAP |
| High-performance requirements | REST |
| Browser-based clients | REST |