Skip to content

Repository files navigation

RestToSoapBridge

RestToSoapBridge is a library for ASP.NET Core that automatically exposes existing REST controllers as a SOAP service.

After registering the library and adding its middleware, controller methods become available as SOAP operations, and a WSDL document is generated automatically.

Features

  • Automatic WSDL generation
  • SOAP 1.1 and SOAP 1.2 support
  • Automatic discovery of ASP.NET Core controllers
  • Complex DTO support
  • Nested object support
  • Collection and array support
  • Asynchronous method support
  • Dependency Injection integration
  • Nullable value support
  • Automatic SOAP request deserialization
  • Automatic SOAP response serialization
  • SOAP Fault generation
  • Configurable endpoint path
  • Configurable service name and XML namespace
  • Support for primitive types, enums, GUIDs, dates and binary data

Requirements

  • .NET 9.0
  • ASP.NET Core Web API
  • Controllers registered through ASP.NET Core MVC

Installation

Add a reference to the RestToSoapBridge.Library project or install the corresponding NuGet package.

Register the library in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddRestToSoapBridge(options =>
{
    options.EndpointPath = "/soap";
    options.ServiceName = "MyAwesomeApi";
    options.TargetNamespace = "http://mycompany.com/api/";
});

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();

app.UseRestToSoapBridge();

app.MapControllers();

app.Run();

After starting the application, the WSDL document will be available at:

http://localhost:5000/soap?wsdl

The SOAP endpoint will be available at:

http://localhost:5000/soap

Basic example

Existing REST controller:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/users")]
public sealed class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet("{id:long}")]
    public async Task<UserDto?> GetUserById(
        long id,
        CancellationToken cancellationToken)
    {
        return await _userService.GetByIdAsync(
            id,
            cancellationToken);
    }

    [HttpPost]
    public async Task<UserDto> CreateUser(
        CreateUserRequest request,
        CancellationToken cancellationToken)
    {
        return await _userService.CreateAsync(
            request,
            cancellationToken);
    }
}

The controller methods will be exposed as SOAP operations:

GetUserById
CreateUser

REST routes and HTTP verbs do not affect SOAP operation names.

Supported method signatures

Synchronous methods:

public UserDto GetUser(long id);

Asynchronous methods:

public Task<UserDto> GetUserAsync(long id);

Methods without a return value:

public void DeleteUser(long id);

Asynchronous methods without a return value:

public Task DeleteUserAsync(long id);

Methods with complex request objects:

public UserDto CreateUser(CreateUserRequest request);

Methods with cancellation support:

public Task<UserDto> GetUserAsync(
    long id,
    CancellationToken cancellationToken);

CancellationToken is provided automatically from HttpContext.RequestAborted and is not included in the WSDL contract.

Supported data types

Primitive types

.NET type XML Schema type
string xs:string
bool xs:boolean
byte xs:unsignedByte
sbyte xs:byte
short xs:short
ushort xs:unsignedShort
int xs:int
uint xs:unsignedInt
long xs:long
ulong xs:unsignedLong
float xs:float
double xs:double
decimal xs:decimal
DateTime xs:dateTime
DateTimeOffset xs:dateTime
TimeSpan xs:duration
Guid xs:string
char xs:string
byte[] xs:base64Binary
enum xs:string

Nullable types

Nullable value types are supported:

public DateTime? BirthDate { get; set; }

public int? Age { get; set; }

public decimal? Balance { get; set; }

SOAP requests may use xsi:nil:

<BirthDate xsi:nil="true" />

Complex types

Nested DTOs are supported:

public sealed class UserDto
{
    public long Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public AddressDto? Address { get; set; }
}

public sealed class AddressDto
{
    public string Country { get; set; } = string.Empty;

    public string City { get; set; } = string.Empty;
}

Collections and arrays

Supported collection types include:

List<T>
IList<T>
ICollection<T>
IEnumerable<T>
IReadOnlyList<T>
IReadOnlyCollection<T>
T[]

Example:

public sealed class UserDto
{
    public List<AccountDto> Accounts { get; set; } = [];
}

Dependency Injection

Controllers are created through the ASP.NET Core Dependency Injection container.

All controller dependencies must be registered:

builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IAccountService, AccountService>();

The library respects registered dependency lifetimes:

  • Singleton
  • Scoped
  • Transient

Controller discovery

By default, the library discovers public ASP.NET Core controllers.

A controller must:

  • Be a public non-abstract class
  • Inherit from ControllerBase, or be recognized as an MVC controller
  • Contain public instance methods
  • Be registered through ASP.NET Core MVC

Example:

builder.Services.AddControllers();

SOAP operation names

By default, the SOAP operation name is based on the controller method name.

public UserDto GetUserById(long id);

becomes:

GetUserById

REST route templates are ignored:

[HttpGet("{id}")]
public UserDto GetUserById(long id);

The SOAP operation is still named:

GetUserById

Duplicate method names

SOAP operation names must be unique across all exposed controllers.

The following configuration is invalid:

public sealed class UsersController : ControllerBase
{
    public object Get(long id)
    {
        return new object();
    }
}

public sealed class AccountsController : ControllerBase
{
    public object Get(long id)
    {
        return new object();
    }
}

Both methods would produce an operation named Get.

The library should reject duplicate operation names during contract generation instead of silently selecting the first method.

To avoid conflicts, use unique method names:

public UserDto GetUser(long id);

public AccountDto GetAccount(long id);

Alternatively, enable controller names in SOAP operation names if this option is supported:

options.IncludeControllerNameInOperationName = true;

The resulting operation names may look like:

Users_Get
Accounts_Get

Methods excluded from the SOAP contract

Infrastructure parameters are not included in the WSDL contract and are resolved automatically where applicable:

CancellationToken
HttpContext
HttpRequest
HttpResponse
ClaimsPrincipal

Methods inherited from ControllerBase or object are not exposed as SOAP operations.

Non-public methods are not exposed.

Static methods are not exposed.

Return types

The following return types are supported:

T
Task<T>
ValueTask<T>
void
Task
ValueTask

Examples:

public UserDto GetUser(long id);

public Task<UserDto> GetUserAsync(long id);

public void DeleteUser(long id);

public Task DeleteUserAsync(long id);

IActionResult and ActionResult

HTTP-specific result wrappers are not ideal for SOAP services.

Avoid returning:

IActionResult
ActionResult
JsonResult
ObjectResult
StatusCodeResult

Preferred:

public UserDto GetUser(long id)
{
    return new UserDto();
}

Instead of:

public IActionResult GetUser(long id)
{
    return Ok(new UserDto());
}

Generic ActionResult<T> may be supported by the runtime if it is explicitly unwrapped, but returning plain DTOs is recommended because it produces a clearer SOAP contract.

SOAP request example

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="http://mycompany.com/api/">
  <soap:Body>
    <tns:GetUserById>
      <tns:id>1001</tns:id>
    </tns:GetUserById>
  </soap:Body>
</soap:Envelope>

For SOAP 1.1, the request usually contains:

Content-Type: text/xml; charset=utf-8
SOAPAction: "http://mycompany.com/api/GetUserById"

SOAP response example

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetUserByIdResponse xmlns="http://mycompany.com/api/">
      <GetUserByIdResult>
        <Id>1001</Id>
        <Name>John Smith</Name>
      </GetUserByIdResult>
    </GetUserByIdResponse>
  </soap:Body>
</soap:Envelope>

SOAP Faults

Invalid requests are returned as SOAP Fault messages.

Typical client errors include:

  • Invalid XML
  • Missing SOAP Envelope
  • Missing SOAP Body
  • Unknown operation
  • Invalid parameter value
  • Missing required parameter
  • Invalid SOAPAction

Internal controller exceptions are returned as server-side SOAP Faults.

Exception details should be disabled in production:

builder.Services.AddRestToSoapBridge(options =>
{
    options.IncludeExceptionDetails = false;
});

Reverse proxy configuration

When the application runs behind IIS, nginx, Apache or another reverse proxy, the generated service URL may differ from the externally accessible address.

Configure the public SOAP endpoint explicitly when necessary:

builder.Services.AddRestToSoapBridge(options =>
{
    options.EndpointPath = "/soap";
    options.PublicServiceUrl =
        "https://api.mycompany.com/soap";
});

ASP.NET Core forwarded headers may also be required:

app.UseForwardedHeaders();

Configuration example

builder.Services.AddRestToSoapBridge(options =>
{
    options.EndpointPath = "/soap";
    options.ServiceName = "Service";
    options.TargetNamespace =
        "http://services.com/services/";

    options.EnableSoap11 = true;
    options.EnableSoap12 = true;

    options.ValidateSoapAction = true;
    options.IncludeExceptionDetails = false;

    options.MaxRequestBodySize =
        10 * 1024 * 1024;

    options.IncludeControllerNameInOperationName =
        true;

    options.PublicServiceUrl =
        "https://api.services.com/soap";
});

The exact available options depend on the current library version.

Limitations

  • REST route templates are ignored.
  • HTTP verbs do not affect SOAP operation names.
  • HTTP status-code semantics are not transferred directly to SOAP.
  • IActionResult and other HTTP-specific wrappers are not recommended.
  • Method overloading may produce ambiguous SOAP operations.
  • Operation names must be unique.
  • Generic methods are not supported.
  • Static controller methods are not exposed.
  • Methods with ref, out or pointer parameters are not supported.
  • File uploads through IFormFile are not supported.
  • Streaming responses are not supported.
  • Cyclic DTO references may not be supported.
  • Polymorphic serialization requires explicit handling.
  • Authentication and authorization still depend on the ASP.NET Core pipeline.

Recommendations

For predictable SOAP contracts:

  • Return DTOs instead of IActionResult.
  • Use unique controller method names.
  • Avoid overloaded controller methods.
  • Avoid exposing entity framework or database model classes directly.
  • Use dedicated request and response DTOs.
  • Keep DTO object graphs finite and acyclic.
  • Explicitly configure the public service URL behind a reverse proxy.
  • Disable exception details in production.
  • Validate generated WSDL during application startup.
  • Test the endpoint with SoapUI or a generated SOAP client.

About

A library for ASP.NET Core that automatically transforms your REST API into a SOAP server

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages