diff --git a/src/SoapCore.Tests/IntegrationTests.cs b/src/SoapCore.Tests/IntegrationTests.cs index 851626fe..91205a19 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,6 +368,103 @@ public async Task HandleConcurrentCallsCorrectly() Assert.AreEqual("hello, async", r[1]); } + [TestMethod] + [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); + namespacePrefixOverrides.AddNamespace("soap12", Namespaces.SOAP12_NS); + namespacePrefixOverrides.AddNamespace("soap", Namespaces.SOAP11_ENVELOPE_NS); + namespacePrefixOverrides.AddNamespace("wsdl", Namespaces.WSDL_NS); + + using var host = CreateNamespaceIsolationTestHost(namespacePrefixOverrides, reuseXmlNamespaceManager: reuseXmlNamespaceManager); + 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; + opt.ReuseXmlNamespaceManager = reuseXmlNamespaceManager; + }); + }); + }); + + 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) { var binding = new BasicHttpBinding(); @@ -400,5 +506,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..ba904ccc 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; } = true; + 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;