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.
- 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
- .NET 9.0
- ASP.NET Core Web API
- Controllers registered through ASP.NET Core MVC
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
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.
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.
| .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 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" />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;
}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; } = [];
}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
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();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
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
Infrastructure parameters are not included in the WSDL contract and are resolved automatically where applicable:
CancellationToken
HttpContext
HttpRequest
HttpResponse
ClaimsPrincipalMethods inherited from ControllerBase or object are not exposed as SOAP operations.
Non-public methods are not exposed.
Static methods are not exposed.
The following return types are supported:
T
Task<T>
ValueTask<T>
void
Task
ValueTaskExamples:
public UserDto GetUser(long id);
public Task<UserDto> GetUserAsync(long id);
public void DeleteUser(long id);
public Task DeleteUserAsync(long id);HTTP-specific result wrappers are not ideal for SOAP services.
Avoid returning:
IActionResult
ActionResult
JsonResult
ObjectResult
StatusCodeResultPreferred:
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.
<?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"<?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>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;
});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();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.
- REST route templates are ignored.
- HTTP verbs do not affect SOAP operation names.
- HTTP status-code semantics are not transferred directly to SOAP.
IActionResultand 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,outor pointer parameters are not supported. - File uploads through
IFormFileare 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.
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.