diff --git a/src/SoapCore.Tests/MessageContract/Issue1161Service.cs b/src/SoapCore.Tests/MessageContract/Issue1161Service.cs new file mode 100644 index 00000000..e882cb5a --- /dev/null +++ b/src/SoapCore.Tests/MessageContract/Issue1161Service.cs @@ -0,0 +1,9 @@ +using SoapCore.Tests.MessageContract.Models; + +namespace SoapCore.Tests.MessageContract +{ + public class Issue1161Service : IServiceIssue1161 + { + public Issue1161MessageContract Process(Issue1161MessageContract rq) => rq; + } +} diff --git a/src/SoapCore.Tests/MessageContract/Issue1161Tests.cs b/src/SoapCore.Tests/MessageContract/Issue1161Tests.cs new file mode 100644 index 00000000..63fef2f0 --- /dev/null +++ b/src/SoapCore.Tests/MessageContract/Issue1161Tests.cs @@ -0,0 +1,97 @@ +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SoapCore.Tests.MessageContract.Models; + +namespace SoapCore.Tests.MessageContract +{ + // Tests for https://github.com/DigDes/SoapCore/issues/1161. + // When an endpoint uses SoapSerializer.XmlSerializer, response headers must + // also use XmlSerializer. DataContractSerializer would either produce the + // wrong XML, or throw InvalidDataContractException if the header type contains + // an [XmlAnyAttribute] XmlAttribute[] member with values. + [TestClass] + public class Issue1161Tests + { + private const string DefaultNamespacedHeaderRequest = @" + + + + SomeSender + + + + + someBody + + + "; + + private const string PrefixedNamespacedHeaderRequest = @" + + + + SomeSender + + + + + someBody + + + "; + + [TestMethod] + public async Task Issue1161_DefaultNamespaceHeader_DoesNotEmitDataContractSerializerNamespaces() + { + var response = await PostSoapAsync(DefaultNamespacedHeaderRequest); + + // On an XmlSerializer endpoint, response headers must use XmlSerializer. + // DataContractSerializer would add unwanted namespaces and an extra + // element. Neither should appear in the output. + StringAssert.Contains(response, "SomeSender"); + StringAssert.DoesNotMatch(response, new System.Text.RegularExpressions.Regex("schemas\\.datacontract\\.org/2004/07")); + StringAssert.DoesNotMatch(response, new System.Text.RegularExpressions.Regex("Serialization/Arrays")); + StringAssert.DoesNotMatch(response, new System.Text.RegularExpressions.Regex("SomeSender"); + StringAssert.DoesNotMatch(response, new System.Text.RegularExpressions.Regex("InvalidDataContractException")); + StringAssert.DoesNotMatch(response, new System.Text.RegularExpressions.Regex("schemas\\.datacontract\\.org/2004/07")); + } + + private static async Task PostSoapAsync(string body) + { + using var host = CreateTestHost(); + using var content = new StringContent(body, Encoding.UTF8, "text/xml"); + using var res = await host.CreateRequest("/Service.asmx") + .AddHeader("SOAPAction", "\"http://tempuri.org/IServiceIssue1161/Process\"") + .And(msg => msg.Content = content) + .PostAsync(); + + res.EnsureSuccessStatusCode(); + return await res.Content.ReadAsStringAsync(); + } + + private static TestServer CreateTestHost() + { + var webHostBuilder = new WebHostBuilder() + .UseStartup() + .ConfigureServices(services => services.AddSingleton(new StartupConfiguration(typeof(Issue1161Service)))); + return new TestServer(webHostBuilder); + } + } +} diff --git a/src/SoapCore.Tests/MessageContract/Models/IServiceIssue1161.cs b/src/SoapCore.Tests/MessageContract/Models/IServiceIssue1161.cs new file mode 100644 index 00000000..454cdb14 --- /dev/null +++ b/src/SoapCore.Tests/MessageContract/Models/IServiceIssue1161.cs @@ -0,0 +1,13 @@ +using System.ServiceModel; +using System.Xml.Serialization; + +namespace SoapCore.Tests.MessageContract.Models +{ + [ServiceContract(Namespace = "http://tempuri.org/")] + public interface IServiceIssue1161 + { + [OperationContract] + [XmlSerializerFormat(SupportFaults = true)] + Issue1161MessageContract Process(Issue1161MessageContract rq); + } +} diff --git a/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageContract.cs b/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageContract.cs new file mode 100644 index 00000000..ed9cc77a --- /dev/null +++ b/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageContract.cs @@ -0,0 +1,14 @@ +using System.ServiceModel; + +namespace SoapCore.Tests.MessageContract.Models +{ + [MessageContract] + public class Issue1161MessageContract + { + [MessageHeader(Name = "MessageHeader", Namespace = "http://www.ebxml.org/namespaces/messageHeader")] + public Issue1161MessageHeader Header { get; set; } + + [MessageBodyMember] + public string BodyMessage { get; set; } + } +} diff --git a/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageHeader.cs b/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageHeader.cs new file mode 100644 index 00000000..ebc34c66 --- /dev/null +++ b/src/SoapCore.Tests/MessageContract/Models/Issue1161MessageHeader.cs @@ -0,0 +1,18 @@ +using System.Xml; +using System.Xml.Serialization; + +namespace SoapCore.Tests.MessageContract.Models +{ + // Test type for issue #1161. Contains an [XmlAnyAttribute] XmlAttribute[] member. + // When the endpoint uses SoapSerializer.XmlSerializer, the response must use XmlSerializer too. + // DataContractSerializer cannot serialize XmlAttribute[] and throws InvalidDataContractException. + [XmlType(AnonymousType = true, Namespace = "http://www.ebxml.org/namespaces/messageHeader")] + public class Issue1161MessageHeader + { + [XmlElement(Order = 0)] + public string From { get; set; } + + [XmlAnyAttribute] + public XmlAttribute[] SomeOtherAttributes { get; set; } + } +} diff --git a/src/SoapCore/SoapEndpointMiddleware.cs b/src/SoapCore/SoapEndpointMiddleware.cs index 69f98e7b..5ebf5702 100644 --- a/src/SoapCore/SoapEndpointMiddleware.cs +++ b/src/SoapCore/SoapEndpointMiddleware.cs @@ -623,7 +623,27 @@ private Message CreateResponseMessage( foreach (var messageHeaderMember in messageHeaderMembers) { var messageHeaderAttribute = messageHeaderMember.Attribute; - responseMessage.Headers.Add(MessageHeader.CreateHeader(messageHeaderAttribute.Name ?? messageHeaderMember.Member.Name, messageHeaderAttribute.Namespace ?? operation.Contract.Namespace, messageHeaderMember.Member.GetPropertyOrFieldValue(responseObject), messageHeaderAttribute.MustUnderstand)); + var headerName = messageHeaderAttribute.Name ?? messageHeaderMember.Member.Name; + var headerNamespace = messageHeaderAttribute.Namespace ?? operation.Contract.Namespace; + var headerValue = messageHeaderMember.Member.GetPropertyOrFieldValue(responseObject); + + // When the endpoint is configured to use XmlSerializer, MessageHeader.CreateHeader + // (which always wraps with DataContractSerializer) produces wrong-shaped XML and + // throws InvalidDataContractException on values containing XmlAttribute[] members + // (e.g. [XmlAnyAttribute]). See issue #1161. + if (_options.SoapSerializer == SoapSerializer.XmlSerializer) + { + responseMessage.Headers.Add(new XmlSerializerMessageHeader( + headerName, + headerNamespace, + headerValue, + messageHeaderMember.Member.GetPropertyOrFieldType(), + messageHeaderAttribute.MustUnderstand)); + } + else + { + responseMessage.Headers.Add(MessageHeader.CreateHeader(headerName, headerNamespace, headerValue, messageHeaderAttribute.MustUnderstand)); + } } } diff --git a/src/SoapCore/XmlSerializerMessageHeader.cs b/src/SoapCore/XmlSerializerMessageHeader.cs new file mode 100644 index 00000000..55447986 --- /dev/null +++ b/src/SoapCore/XmlSerializerMessageHeader.cs @@ -0,0 +1,75 @@ +using System; +using System.ServiceModel.Channels; +using System.Xml; +using System.Xml.Serialization; + +namespace SoapCore +{ + public class XmlSerializerMessageHeader : MessageHeader + { + private const string Xmlns = "xmlns"; + private readonly object _value; + private readonly Type _valueType; + private readonly bool _mustUnderstand; + + public XmlSerializerMessageHeader(string name, string ns, object value, Type valueType, bool mustUnderstand) + { + Name = name; + Namespace = ns; + _value = value; + _valueType = valueType; + _mustUnderstand = mustUnderstand; + } + + public override string Name { get; } + public override string Namespace { get; } + public override bool MustUnderstand => _mustUnderstand; + + protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion) + { + if (_value == null) + { + return; + } + + // XmlSerializer always writes a root element, but the surrounding MessageHeader element + // is already opened by OnWriteStartHeader. Serialize into a temporary XmlDocument so + // only its inner content can then be copied to the actual header writer. + var serializer = CachedXmlSerializer.GetXmlSerializer(_valueType, Name, Namespace); + var doc = new XmlDocument(); + using (var docWriter = doc.CreateNavigator().AppendChild()) + { + var namespaces = new XmlSerializerNamespaces(); + namespaces.Add(string.Empty, Namespace); + serializer.Serialize(docWriter, _value, namespaces); + } + + // Copy the root's attributes onto the header element. + foreach (XmlAttribute attr in doc.DocumentElement.Attributes) + { + // xmlns="..." default-namespace declaration - skip if it matches the header's + // own namespace (already declared by OnWriteStartHeader); otherwise emit as xmlns. + if (attr.Prefix.Length == 0 && attr.LocalName == Xmlns) + { + if (attr.Value == Namespace) + { + continue; + } + + writer.WriteAttributeString(Xmlns, attr.Value); + continue; + } + + // Other attributes (including xmlns:prefix="..." declarations XmlSerializer produced) + // XmlDictionaryWriter recognizes the xmlns NamespaceURI and emits them as namespace declarations when needed. + writer.WriteAttributeString(attr.Prefix, attr.LocalName, attr.NamespaceURI, attr.Value); + } + + // Copy the root's children into the header element. + foreach (XmlNode child in doc.DocumentElement.ChildNodes) + { + child.WriteTo(writer); + } + } + } +}