| name | soap-facade |
| description | Use when wrapping the legacy SOAP web service behind an ILegacyApi interface boundary. Enables testing and future replacement. Produces the interface, real adapter, test fake, and contract tests. SOAP is an external fixed contract — we adapt to it, not change it.
|
| argument-hint | [list of SOAP methods used] [where they're called from in VB] [auth/timeout requirements] |
SOAP Facade Skill
When to Use
- Before extracting any VB event handler that makes SOAP calls.
- When App-layer services need to call legacy API operations without depending on SOAP directly.
- When you need injectable fakes for unit testing SOAP-backed workflows.
Core Principle
The SOAP web service is an external dependency with a fixed contract. The facade:
- Hides the SOAP complexity behind application-vocabulary methods.
- Enables unit testing via a fake implementation.
- Centralizes error handling, retry logic, and timeout configuration.
- Allows future replacement of SOAP with REST/gRPC without touching App/Domain code.
Discovery Step
Before designing the interface, catalog all SOAP calls:
SOAP Method Name | VB Call Site | Params Sent | Return Used | Error Handling in VB?
Check for: auth tokens, envelope headers, special retry patterns, and timeouts already present in VB.
Interface Design Guidelines
Use application vocabulary, not WSDL vocabulary
public interface ILegacyApi
{
Task<OrderConfirmation> PlaceOrderAsync(OrderDraft draft, CancellationToken ct = default);
Task<CustomerRecord> LookupCustomerAsync(string accountNumber, CancellationToken ct = default);
}
public interface ILegacyApi
{
Task<PlaceOrderResponse> PlaceOrderAsync(PlaceOrderRequest request);
}
DTO design for ILegacyApi
public sealed record OrderDraft(string AccountNumber, IReadOnlyList<OrderItem> Items, string Notes);
public sealed record OrderConfirmation(string ConfirmationNumber, DateTimeOffset PlacedAt, bool IsSuccessful);
Real SOAP Adapter Implementation
public sealed class LegacyApiSoapAdapter(
LegacySoapClient soapClient,
ILogger<LegacyApiSoapAdapter> logger) : ILegacyApi
{
public async Task<OrderConfirmation> PlaceOrderAsync(OrderDraft draft, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(draft);
if (string.IsNullOrWhiteSpace(draft.AccountNumber))
throw new ArgumentException("AccountNumber is required", nameof(draft));
try
{
var soapRequest = MapToSoapRequest(draft);
var response = await soapClient.PlaceOrderAsync(soapRequest);
return MapFromSoapResponse(response);
}
catch (CommunicationException ex)
{
logger.LogError(ex, "SOAP PlaceOrder failed for account {Account}", draft.AccountNumber);
throw new LegacyApiException("Order placement failed in legacy system", ex);
}
}
private static SoapPlaceOrderRequest MapToSoapRequest(OrderDraft draft) => new()
{
AccountNumber = draft.AccountNumber,
};
}
Test Fake Implementation
public sealed class FakeLegacyApi : ILegacyApi
{
private readonly List<OrderDraft> _placedOrders = new();
private Func<string, CustomerRecord?> _customerLookup = _ => null;
public void SetupCustomer(string accountNumber, CustomerRecord record) =>
_customerLookup = acct => acct == accountNumber ? record : null;
public void SetupThrowOnPlaceOrder(Exception ex) =>
_nextPlaceOrderException = ex;
private Exception? _nextPlaceOrderException;
public Task<OrderConfirmation> PlaceOrderAsync(OrderDraft draft, CancellationToken ct)
{
if (_nextPlaceOrderException is not null)
return Task.FromException<OrderConfirmation>(_nextPlaceOrderException);
_placedOrders.Add(draft);
return Task.FromResult(new OrderConfirmation("FAKE-001", DateTimeOffset.UtcNow, true));
}
public IReadOnlyList<OrderDraft> PlacedOrders => _placedOrders.AsReadOnly();
}
Contract/Integration Tests
[Trait("Category", "Integration")]
public class LegacyApiContractTests
{
[Fact]
public async Task LookupCustomer_KnownAccount_ReturnsExpectedData()
{
var adapter = CreateAdapterPointingToTestEndpoint();
var result = await adapter.LookupCustomerAsync("TEST-ACC-001");
Assert.NotNull(result);
Assert.Equal("Test Company", result.CompanyName);
}
}
Security Checklist