Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/SoapCore.Tests/MessageContract/Issue1161Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using SoapCore.Tests.MessageContract.Models;

namespace SoapCore.Tests.MessageContract
{
public class Issue1161Service : IServiceIssue1161
{
public Issue1161MessageContract Process(Issue1161MessageContract rq) => rq;
}
}
97 changes: 97 additions & 0 deletions src/SoapCore.Tests/MessageContract/Issue1161Tests.cs
Original file line number Diff line number Diff line change
@@ -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 = @"<?xml version=""1.0"" encoding=""utf-8""?>
<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
<s:Header>
<MessageHeader xmlns=""http://www.ebxml.org/namespaces/messageHeader"">
<From>SomeSender</From>
</MessageHeader>
</s:Header>
<s:Body>
<Process xmlns=""http://tempuri.org/"">
<BodyMessage>someBody</BodyMessage>
</Process>
</s:Body>
</s:Envelope>";

private const string PrefixedNamespacedHeaderRequest = @"<?xml version=""1.0"" encoding=""utf-8""?>
<s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"">
<s:Header>
<h:MessageHeader xmlns:h=""http://www.ebxml.org/namespaces/messageHeader""
xmlns=""http://www.ebxml.org/namespaces/messageHeader"">
<From>SomeSender</From>
</h:MessageHeader>
</s:Header>
<s:Body>
<Process xmlns=""http://tempuri.org/"">
<BodyMessage>someBody</BodyMessage>
</Process>
</s:Body>
</s:Envelope>";

[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
// <SomeOtherAttributes /> element. Neither should appear in the output.
StringAssert.Contains(response, "<From>SomeSender</From>");
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("<SomeOtherAttributes"));
}

[TestMethod]
public async Task Issue1161_PrefixedNamespaceHeader_DoesNotThrowInvalidDataContractException()
{
// A namespace-prefixed inbound header (xmlns:h="...") populates the
// [XmlAnyAttribute] XmlAttribute[] member during deserialization.
// The response must serialize successfully (no InvalidDataContractException).
var response = await PostSoapAsync(PrefixedNamespacedHeaderRequest);

StringAssert.Contains(response, "<From>SomeSender</From>");
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<string> 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<Startup>()
.ConfigureServices(services => services.AddSingleton<IStartupConfiguration>(new StartupConfiguration(typeof(Issue1161Service))));
return new TestServer(webHostBuilder);
}
}
}
13 changes: 13 additions & 0 deletions src/SoapCore.Tests/MessageContract/Models/IServiceIssue1161.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
22 changes: 21 additions & 1 deletion src/SoapCore/SoapEndpointMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}

Expand Down
75 changes: 75 additions & 0 deletions src/SoapCore/XmlSerializerMessageHeader.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}