SOAP

Comparing SOAP to REST APIs and working with SOAP services in .NET applications

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:

FeatureSOAPREST
FormatXML onlyAny format (JSON, XML, HTML, etc.)
ProtocolMultiple protocolsHTTP primarily
Service DefinitionWSDL (required)OpenAPI/Swagger (optional)
BandwidthHigher (XML verbose)Lower (typically JSON)
CacheNot cacheableCacheable
SecurityWS-Security, enterprise featuresHTTPS, JWT, OAuth
Transaction SupportBuilt-inMust implement custom
Failure RecoveryRetry logic, error infoTypically none
PerformanceSlower due to XML parsingFaster, lighter weight
Learning CurveSteeperSimpler

When to Use SOAP

SOAP is particularly well-suited for:

  1. Enterprise Integration: When connecting complex enterprise systems
  2. Formal Contracts: When you need a strict, enforceable contract
  3. Stateful Operations: When maintaining state across operations is required
  4. Reliable Messaging: When guaranteed delivery is essential
  5. Complex Transactions: When you need ACID transaction support
  6. Advanced Security: When WS-Security features are needed
  7. 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.

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

✅ SOAP Advantages

  • 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

⚠️ SOAP Considerations

  • 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

ScenarioRecommended
Public APIs for web/mobile appsREST
Enterprise B2B integrationSOAP
Banking/Financial servicesSOAP
Healthcare systems (HL7, etc.)SOAP
Simple CRUD operationsREST
Formal contract requirementsSOAP
Transaction support neededSOAP
Legacy system integrationSOAP
High-performance requirementsREST
Browser-based clientsREST

Resources