This article comes in 3 parts
- Calling a Secured SOAP Service from ASP.NET Core WebAPI (WSDL+ WCF)
- Securing a SOAP Client with OAuth2 in ASP.NET Core (WCF Behaviors)
- Integrating and Testing a Secured SOAP Client with Dependency Injection (This page)
You can find the companion code for this blog at GVerelst/ShiftLeftStore.
Introduction
In the previous articles, we built a working SOAP client using WSDL‑generated code and secure it using OAuth2.
Now that authentication works, we need to integrate the client into ASP.NET Core in a clean, testable way.
Audience
This guide is for .NET developers and software architects who need to integrate SOAP services into modern ASP.NET Core WebAPI projects, especially when bridging legacy systems with REST interfaces. Readers should be comfortable with C#, dependency injection, and basic HTTP concepts — no prior WCF experience required. Here are some possible scenarios:
- Integrating ERP SOAP endpoints into modern REST APIs
- Wrapping legacy partner systems behind WebAPI
- Replacing brittle XML‑manipulation code with WCF behaviors
Refactoring the code (dependency injection)
We now have working code, but there is a lot of setup to do to make it work. Here are the steps;
- First obtain the ShiftLeftSettings from a configuration. The settings must come from a secure configuration store (config file, secrets.json, Azure key vault, …).
- Use the settings in the OAuth2Client constructor
- Use the OAuth2Client in the SecurityTokenMessageInspector constructor
- Use the SecurityTokenMessageInspector in the SecurityTokenEndpointBehavior constructor
- Finally create the SecurityTokenEndpointBehavior
This screams for dependency injection! The goal is that users of the service inject the service in the class constructor (or in the methods) and just use it without bothering about all the dependencies and how exactly to use it.
Here are some reasons to choose for DI when working with WCF clients:
- WCF clients have complex dependency chains
- OAuth2 token retrieval must be centralized
- Inspectors and behaviors must be reused safely
- Configuration must be testable
Setting up the IoC container
To hide the complexity for the users of the service, the setup should be easy:
builder.Services.AddSoapServices();
We will create an extension method on the IServiceCollection interface :
public static class IServiceCollectionExtensions{ public static void AddSoapServices(this IServiceCollection services, ShiftLeftSettings settings) { // TODO: register services }}
Configuring the settings using the IOptions<T> pattern
Avoid injecting ShiftLeftSettings directly — always use IOptions<T> or IOptionsMonitor<T> to ensure proper lifetime management. It will also make your code easy to test. Using the IOptions patterns allows to write tests that don’t depend on actual config files.
It requires a small change in the classes that use the ShiftLeftSettings. They can now receive a parameter of type IOptions<ShiftLeftSettings>. Using its Value property, we can get access to the actual settings.
We start with the ShiftLeftSettings class that will hold the retrieved settings from the configuration store. If you want to change the configuration store, all you need to do is to register the right configuration provider. The rest of the code will remain the same.
namespace ShiftLeftStore.Soap.Services{ public class ShiftLeftSettings { public required string TokenEndpoint { get; set; } public required string ClientId { get; set; } public required string ClientSecret { get; set; } public required string Scope { get; set; } public required string ServiceURI { get; set; } }}
And here is the config file. Normally this file should NOT go into your source code control system for everybody to read.
Remember that there are more secure ways of storing settings, such as Azure Key Vault.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ShiftLeft": { "TokenEndpoint": "https://api.shiftleft.io/oauth/token", "ClientId": "your-client-id", "ClientSecret": "your-client-secret", "Scope": "openid", "ServiceURI": "https://demo.totalshiftleft.ai/soap" }}
Our AddSoapServices function already has one line now:
public static void AddSoapServices(this IServiceCollection services, IConfiguration configuration){ services.Configure<ShiftLeftSettings>(configuration.GetSection("ShiftLeft")); // ...}
and the constructor in OAuth2Client changes slightly:
public class OAuth2Client : IOAuth2Client{ private readonly ShiftLeftSettings _settings; // not used in this dummy implementation. public OAuth2Client(IOptions<ShiftLeftSettings> options) { _settings = options.Value; } // Todo: actual implementation to obtain the token using _settings and OAuth2 flow. public async Task<string> GetJwtTokenAsync() => await Task.FromResult(string.Empty);}
The constructor now receives IOptions<ShiftLeftSettings> parameter. We assign its Value property to _settings for later use.
Registering the other services in the IoC container now is easy:
public static void AddSoapServices(this IServiceCollection services, IConfiguration configuration){ services.Configure<ShiftLeftSettings>(configuration.GetSection("ShiftLeft")); services.AddSingleton<IOAuth2Client, OAuth2Client>(); services.AddSingleton<IClientMessageInspector, SecurityTokenMessageInspector>(); services.AddSingleton<IEndpointBehavior, SecurityTokenEndpointBehavior>(); services.AddScoped<SandboxPortType, SandboxPortTypeClient>(); services.AddScoped<IShiftLeftStoreService, ShiftLeftStoreService>();}
Line 4 tells the framework how to instantiate an IOAuth2Client object. The config has been defined on line 3, and is automatically injected when IOAuth2Client is injected.
Line 5 tells how to create a IClientMessageInspector. This class takes an IAuth2Client parameter in its constructor. All the dependencies are clear through the constructors so the IoC container knows exactly how to build the whole object tree.
Etcetera …
Notice the lifetime for the registered services:
| Service | Lifetime | Why |
|---|---|---|
| OAuth2Client | Singleton | Stateless, config‑based |
| MessageInspector | Singleton | No per‑request state |
| EndpointBehavior | Singleton | Pure configuration |
| WCF Client | Scoped | Not thread‑safe |
| StoreService | Scoped | Depends on scoped WCF client |
Remark: Don’t register the actual WCF client as a singleton. WCF clients maintain internal channel state; sharing them across threads leads to channel faults and disposed objects.

Testing the AddSoapServices method (unit test)
Testing the DI is not dependent on any external services and should be performed in a new unittest project. Use the same steps as before to create an XUnit project and call it ShiftLeftStore.Soap.Unittests.
I split unit tests and integration tests so that in the CI/CD pipeline I can run all the tests in the *.Unittests project and skip the tests in the *.Integrationtests project. Integration tests (by nature) depend on external resources that often are not accessible from the build server, so it is good practice to separate them and only execute the unit tests in the build pipeline.
public class IServiceCollectionExtensionsTests{ [Fact] public void AddSoapServices_RegistersExpectedServicesAndOptions() { // Arrange var inMemory = new Dictionary<string, string> { ["ShiftLeft:ClientId"] = "cid", ["ShiftLeft:ClientSecret"] = "secret", ["ShiftLeft:TokenEndpoint"] = "https://token", ["ShiftLeft:ServiceURI"] = "https://demo.totalshiftleft.ai/soap" }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(inMemory) .Build(); var services = new ServiceCollection(); // Act services.AddSoapServices(configuration); using var provider = services.BuildServiceProvider(); // Assert: options configured var options = provider.GetService<IOptions<ShiftLeftSettings>>(); Assert.NotNull(options); Assert.Equal("cid", options.Value.ClientId); // Assert: services registered Assert.NotNull(provider.GetService<IOAuth2Client>()); Assert.NotNull(provider.GetService<IClientMessageInspector>()); Assert.NotNull(provider.GetService<IEndpointBehavior>()); Assert.NotNull(provider.GetService<SandboxPortType>()); Assert.NotNull(provider.GetService<IShiftLeftStoreService>()); }}
Notice that we use an in-memory collection for the configuration. This doesn’t require any change in the rest of the code.
Testing the CreateProduct method again is left as an exercise for the reader. You can find the working code in my github repository.
The unit tests are not complete. We should add more cases, test for more edge cases, add negative tests. But that is not the scope of this article.
Adding the other methods for the ShiftLeftStoreService is easy now. The implementation is also in the reference project.
Now we can go back to the Web API and implement the Products controller. With all the work that we have done this will be easy.
Registering the ShiftLeftStoreService
Open Program.cs in the ShiftLeftStore project and add this:
using ShiftLeftStore.Soap.Extensions;var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddControllers();builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();builder.Services.AddSoapServices(builder.Configuration);var app = builder.Build();
Line 1 imports the namespace and line 8 registers the necessary services to use the ShiftLeftService. To use it we only need to use constructor or method injection and adapt the config file to include the ShiftLeft settings.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ShiftLeft": { "TokenEndpoint": "https://api.shiftleft.io/oauth/token", "ClientId": "your-client-id", "ClientSecret": "your-client-secret", "Scope": "openid", "ServiceURI": "https://demo.totalshiftleft.ai/soap" }}
Adding the Products controller to the ShiftLeftStore project
The ProductController will work with its own Product model class. We could be lazy in this case and use the Product class from the Soap project, but experience teaches me that later you’ll need a separate class to interface with your REST API anyway. Adding one more class and some conversions is not a lot of work and may save us work and frustrations in the future.
using SLS = ShiftLeftStore.Soap.Models;namespace ShiftLeftStore.Models{ public record struct Product(string Name, decimal Price, string Description, int Stock, string Category, string Id ) { internal Product(SLS.Product product) : this(product.Name, product.Price, product.Description, product.Stock, product.Category, product.Id) { } internal SLS.Product ToSLSProduct() => new SLS.Product(Name, Price, Description, Stock, Category, Id); }}
Add a new ProductController to the ShiftLeftStore project:

Add the IShiftLeftStoreService interface to the constructor and use it to handle the products, and use the injected service to implement the REST endpoints:
using Microsoft.AspNetCore.Mvc;using ShiftLeftStore.Models;using ShiftLeftStore.Soap.Services;namespace ShiftLeftStore.Controllers{ [Route("api/[controller]")] [ApiController] public class ProductsController(IShiftLeftStoreService _service) : ControllerBase { [HttpGet] public async Task<IEnumerable<Product>> GetAsync() => (await _service.GetProductsAsync()).Select(p => new Product(p)); [HttpGet("{id}")] public async Task<Product> GetAsync(string id) => new Product(await _service.GetProductAsync(id)); [HttpPost] public async Task<Product> PostAsync([FromBody] Product newProduct) { var prod = await _service.CreateProductAsync(newProduct.ToSLSProduct()); return new Product(prod); } [HttpPut("{id}")] public async Task<Product> PutAsync(string id, [FromBody] Product newProduct) { var prod = await _service.UpdateProductAsync(newProduct.ToSLSProduct()); return new Product(prod); } [HttpDelete("{id}")] public async Task DeleteAsync(string id) => await _service.DeleteProductAsync(id); }}
In line 9 the IShiftLeftStoreService is injected, making it available in all the methods. The implementation of the methods now is trivial.
Conclusion
Calling a secured SOAP service from ASP.NET Core doesn’t have to be painful. With a proper setup we gained:
- Centralized authentication
- Fully testable DI setup
- Clean separation between SOAP and REST
- No more manual XML manipulation
- A maintainable architecture ready for production
References
Options pattern – .NET | Microsoft Learn
How to: Inspect or Modify Messages on the Client – WCF | Microsoft Learn





















If you don’t have an Azure account yet, there are some ways to get a free test account. You can surf to
Sysprep can be used with parameters (when you know what you are doing), or just without parameters, which will pop up a little form. In the screenshot, you can see the form with the right values filled in:



