From e35b78f1e25089fde798d77ce125ea9962d2efdc Mon Sep 17 00:00:00 2001 From: lice Date: Tue, 9 Jun 2026 13:25:20 +0200 Subject: [PATCH 1/3] Added the ability to opt out of XmlNamespaceManager reuse This fixes an issue where modifying the namespaces in a MessageInspector affected other requests. --- src/SoapCore.Tests/IntegrationTests.cs | 114 ++++++++++++++++++++++++- src/SoapCore/SoapCoreOptions.cs | 6 ++ src/SoapCore/SoapEndpointMiddleware.cs | 5 ++ src/SoapCore/SoapOptions.cs | 7 ++ 4 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/SoapCore.Tests/IntegrationTests.cs b/src/SoapCore.Tests/IntegrationTests.cs index 851626fe..f483dfea 100644 --- a/src/SoapCore.Tests/IntegrationTests.cs +++ b/src/SoapCore.Tests/IntegrationTests.cs @@ -1,12 +1,21 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.ServiceModel; using System.ServiceModel.Channels; using System.Text; using System.Threading.Tasks; using System.Xml; +using System.Xml.Schema; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SoapCore; +using SoapCore.Extensibility; +using SoapCore.ServiceModel; using SoapCore.Tests.Model; using SoapCore.Tests.Utilities; @@ -359,11 +368,96 @@ public async Task HandleConcurrentCallsCorrectly() Assert.AreEqual("hello, async", r[1]); } - private ITestService CreateClient(bool caseInsensitivePath = false) + [TestMethod] + public async Task ModifyingCustomMessageNamespaceManagerDoesNotAffectOtherRequestsWithCachingDisabled() + { + var namespacePrefixOverrides = new XmlNamespaceManager(new NameTable()); + namespacePrefixOverrides.AddNamespace("s", XmlSchema.Namespace); + namespacePrefixOverrides.AddNamespace("soap12", Namespaces.SOAP12_NS); + namespacePrefixOverrides.AddNamespace("soap", Namespaces.SOAP11_ENVELOPE_NS); + namespacePrefixOverrides.AddNamespace("wsdl", Namespaces.WSDL_NS); + + using var host = CreateNamespaceIsolationTestHost(namespacePrefixOverrides); + using var httpClient = host.CreateClient(); + + var preWsdl = await LoadWsdlAsync(httpClient); + Assert.IsTrue(preWsdl.Contains(" + { + services.AddRouting(); + services.AddSoapCore(); + services.TryAddSingleton(); + services.AddSoapMessageInspector(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.UseSoapEndpoint(opt => + { + opt.Path = "/Service.asmx"; + opt.SoapSerializer = SoapSerializer.XmlSerializer; + opt.EncoderOptions = new[] + { + new SoapEncoderOptions { MessageVersion = MessageVersion.Soap11 }, + new SoapEncoderOptions { MessageVersion = MessageVersion.Soap12WSAddressing10 }, + }; + opt.XmlNamespacePrefixOverrides = namespacePrefixOverrides; + }); + }); + }); + + return new TestServer(webHostBuilder); + } + + private static async Task SendSoap12AsyncMethodAsync(TestServer host) + { + const string body = @" + + + + +"; + + using var content = new StringContent(body, Encoding.UTF8, "application/soap+xml"); + using var response = await host + .CreateRequest("/Service.asmx") + .AddHeader("SOAPAction", @"""http://tempuri.org/ITestService/AsyncMethod""") + .And(msg => msg.Content = content) + .PostAsync(); + + response.EnsureSuccessStatusCode(); + } + + private static async Task LoadWsdlAsync(HttpClient httpClient) + { + var wsdlResponse = await httpClient.GetAsync("/Service.asmx?wsdl"); + if (wsdlResponse.IsSuccessStatusCode) + { + return await wsdlResponse.Content.ReadAsStringAsync(); + } + + var content = await wsdlResponse.Content.ReadAsStringAsync(); + Assert.Fail($"Failed to load wsdl, status code: {wsdlResponse.StatusCode}, content: {content}"); + throw new InvalidOperationException("Unreachable code"); + } + + private ITestService CreateClient(bool caseInsensitivePath = false, string port = "5050") { var binding = new BasicHttpBinding(); var endpoint = new EndpointAddress(new Uri( - string.Format("http://{0}:5050/{1}.svc", "localhost", caseInsensitivePath ? "serviceci" : "Service"))); + $"http://localhost:{port}/{(caseInsensitivePath ? "serviceci" : "Service")}.svc")); var channelFactory = new ChannelFactory(binding, endpoint); var serviceClient = channelFactory.CreateChannel(); return serviceClient; @@ -400,5 +494,21 @@ private ITestService CreateSoap11Iso88591Client() var serviceClient = channelFactory.CreateChannel(); return serviceClient; } + + private class TestMessageInspectorThatModifiesNamespace : IMessageInspector2 + { + public object AfterReceiveRequest(ref Message message, ServiceDescription serviceDescription) + { + return message; + } + + public void BeforeSendReply(ref Message reply, ServiceDescription serviceDescription, object correlationState) + { + if (reply is CustomMessage msg) + { + msg.XmlNamespaceLookup.AddNamespace("xsd", XmlSchema.Namespace); + } + } + } } } diff --git a/src/SoapCore/SoapCoreOptions.cs b/src/SoapCore/SoapCoreOptions.cs index 148fd37a..86cf9054 100644 --- a/src/SoapCore/SoapCoreOptions.cs +++ b/src/SoapCore/SoapCoreOptions.cs @@ -164,6 +164,12 @@ public class SoapCoreOptions /// public bool UseLegacyWsdlNaming { get; set; } = false; + /// + /// Gets or sets a value indicating whether to reuse the same XmlNamespaceManager for all requests using the same binding and encoding, or create a new one for each request. + /// Reusing the same one can improve performance, but may cause issues if you modify the namespace manager in your code. Defaults to true. + /// + public bool ReuseXmlNamespaceManager { get; set; } + public void UseCustomSerializer() where TCustomSerializer : class, IXmlSerializationHandler { diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index fd7190bb..d450d86c 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -1175,6 +1175,11 @@ private async Task ProcessMetaFromFile(HttpContext httpContext, bool showDocumen private ConcurrentXmlNamespaceLookup GetXmlNamespaceLookup(SoapMessageEncoder messageEncoder) { + if (!_options.ReuseXmlNamespaceManager) + { + return CreateDefaultNamespaceManager(messageEncoder); + } + return _xmlNamespaceLookupsByMessageEncoder.GetOrAdd(messageEncoder?.ToString() ?? "no_encoder", _ => CreateDefaultNamespaceManager(messageEncoder)); ConcurrentXmlNamespaceLookup CreateDefaultNamespaceManager(SoapMessageEncoder messageEncoder) diff --git a/src/SoapCore/SoapOptions.cs b/src/SoapCore/SoapOptions.cs index 6feeb038..8ab46a23 100644 --- a/src/SoapCore/SoapOptions.cs +++ b/src/SoapCore/SoapOptions.cs @@ -54,6 +54,12 @@ public class SoapOptions public bool UseMicrosoftGuid { get; set; } = false; + /// + /// Gets or sets a value indicating whether to reuse the same XmlNamespaceManager for all requests using the same binding and encoding, or create a new one for each request. + /// Reusing the same one can improve performance, but may cause issues if you modify the namespace manager in your code. Defaults to true. + /// + public bool ReuseXmlNamespaceManager { get; set; } = true; + /// /// Gets or sets a value indicating whether to check to make sure that the XmlOutput doesn't contain invalid characters /// Defaults to true @@ -111,6 +117,7 @@ public static SoapOptions FromSoapCoreOptions(SoapCoreOptions opt, Type serviceT SchemeOverride = opt.SchemeOverride, XmlIgnoreOnlyForWsdl = opt.XmlIgnoreOnlyForWsdl, UseLegacyWsdlNaming = opt.UseLegacyWsdlNaming, + ReuseXmlNamespaceManager = opt.ReuseXmlNamespaceManager, }; return options; From 7c5c5191ff20b0b609a1e1237c1df05f478a4712 Mon Sep 17 00:00:00 2001 From: lice Date: Tue, 9 Jun 2026 13:31:42 +0200 Subject: [PATCH 2/3] removed some changes that are not required --- src/SoapCore.Tests/IntegrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SoapCore.Tests/IntegrationTests.cs b/src/SoapCore.Tests/IntegrationTests.cs index f483dfea..85cfb63a 100644 --- a/src/SoapCore.Tests/IntegrationTests.cs +++ b/src/SoapCore.Tests/IntegrationTests.cs @@ -453,11 +453,11 @@ private static async Task LoadWsdlAsync(HttpClient httpClient) throw new InvalidOperationException("Unreachable code"); } - private ITestService CreateClient(bool caseInsensitivePath = false, string port = "5050") + private ITestService CreateClient(bool caseInsensitivePath = false) { var binding = new BasicHttpBinding(); var endpoint = new EndpointAddress(new Uri( - $"http://localhost:{port}/{(caseInsensitivePath ? "serviceci" : "Service")}.svc")); + string.Format("http://{0}:5050/{1}.svc", "localhost", caseInsensitivePath ? "serviceci" : "Service"))); var channelFactory = new ChannelFactory(binding, endpoint); var serviceClient = channelFactory.CreateChannel(); return serviceClient; From 7b52029b4a05533ec2a49a8f5df76788ed4082d2 Mon Sep 17 00:00:00 2001 From: lice Date: Tue, 9 Jun 2026 13:38:18 +0200 Subject: [PATCH 3/3] Reuse by default Fixed test and added a case for the behavior when you don't reuse the manager (for documentation) --- src/SoapCore.Tests/IntegrationTests.cs | 20 ++++++++++++++++---- src/SoapCore/SoapCoreOptions.cs | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/SoapCore.Tests/IntegrationTests.cs b/src/SoapCore.Tests/IntegrationTests.cs index 85cfb63a..91205a19 100644 --- a/src/SoapCore.Tests/IntegrationTests.cs +++ b/src/SoapCore.Tests/IntegrationTests.cs @@ -369,7 +369,11 @@ public async Task HandleConcurrentCallsCorrectly() } [TestMethod] - public async Task ModifyingCustomMessageNamespaceManagerDoesNotAffectOtherRequestsWithCachingDisabled() + [DataRow(false, true)] + + // This case is to document behavior. If we fix the root cause of this issue, we should be able to change this expected result to true. + [DataRow(true, false)] + public async Task ModifyingCustomMessageNamespaceManagerDoesNotAffectOtherRequestsWithNamespaceManagerReuseDisabled(bool reuseXmlNamespaceManager, bool expectEquality) { var namespacePrefixOverrides = new XmlNamespaceManager(new NameTable()); namespacePrefixOverrides.AddNamespace("s", XmlSchema.Namespace); @@ -377,7 +381,7 @@ public async Task ModifyingCustomMessageNamespaceManagerDoesNotAffectOtherReques namespacePrefixOverrides.AddNamespace("soap", Namespaces.SOAP11_ENVELOPE_NS); namespacePrefixOverrides.AddNamespace("wsdl", Namespaces.WSDL_NS); - using var host = CreateNamespaceIsolationTestHost(namespacePrefixOverrides); + using var host = CreateNamespaceIsolationTestHost(namespacePrefixOverrides, reuseXmlNamespaceManager: reuseXmlNamespaceManager); using var httpClient = host.CreateClient(); var preWsdl = await LoadWsdlAsync(httpClient); @@ -386,10 +390,17 @@ public async Task ModifyingCustomMessageNamespaceManagerDoesNotAffectOtherReques await SendSoap12AsyncMethodAsync(host); var postWsdl = await LoadWsdlAsync(httpClient); - Assert.AreEqual(preWsdl, postWsdl); + if (expectEquality) + { + Assert.AreEqual(preWsdl, postWsdl); + } + else + { + Assert.AreNotEqual(preWsdl, postWsdl); + } } - private static TestServer CreateNamespaceIsolationTestHost(XmlNamespaceManager namespacePrefixOverrides) + private static TestServer CreateNamespaceIsolationTestHost(XmlNamespaceManager namespacePrefixOverrides, bool reuseXmlNamespaceManager) { var webHostBuilder = new WebHostBuilder() .ConfigureServices(services => @@ -414,6 +425,7 @@ private static TestServer CreateNamespaceIsolationTestHost(XmlNamespaceManager n new SoapEncoderOptions { MessageVersion = MessageVersion.Soap12WSAddressing10 }, }; opt.XmlNamespacePrefixOverrides = namespacePrefixOverrides; + opt.ReuseXmlNamespaceManager = reuseXmlNamespaceManager; }); }); }); diff --git a/src/SoapCore/SoapCoreOptions.cs b/src/SoapCore/SoapCoreOptions.cs index 86cf9054..ba904ccc 100644 --- a/src/SoapCore/SoapCoreOptions.cs +++ b/src/SoapCore/SoapCoreOptions.cs @@ -168,7 +168,7 @@ public class SoapCoreOptions /// Gets or sets a value indicating whether to reuse the same XmlNamespaceManager for all requests using the same binding and encoding, or create a new one for each request. /// Reusing the same one can improve performance, but may cause issues if you modify the namespace manager in your code. Defaults to true. /// - public bool ReuseXmlNamespaceManager { get; set; } + public bool ReuseXmlNamespaceManager { get; set; } = true; public void UseCustomSerializer() where TCustomSerializer : class, IXmlSerializationHandler