diff --git a/SimpleSOAPClient.sln b/SimpleSOAPClient.sln index 349b00b..f36387a 100644 --- a/SimpleSOAPClient.sln +++ b/SimpleSOAPClient.sln @@ -21,7 +21,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "other", "other", "{6C152128 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "resources", "resources", "{A2159B1B-25D6-4AFA-A30A-22207B226377}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleSOAPClient", "src\SimpleSOAPClient\SimpleSOAPClient.csproj", "{BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleSOAPClient", "src\SimpleSOAPClient\SimpleSOAPClient.csproj", "{BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{63D634C9-3F19-4305-8831-D47A380C9EAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleSOAPClient.Tests", "tests\SimpleSOAPClient.Tests\SimpleSOAPClient.Tests.csproj", "{07625BFB-1644-415D-B2B8-468152E05C3C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +37,10 @@ Global {BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA}.Release|Any CPU.Build.0 = Release|Any CPU + {07625BFB-1644-415D-B2B8-468152E05C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07625BFB-1644-415D-B2B8-468152E05C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07625BFB-1644-415D-B2B8-468152E05C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07625BFB-1644-415D-B2B8-468152E05C3C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -41,6 +49,7 @@ Global {E136A35F-1EC1-4A90-9369-C278E33BA475} = {A2159B1B-25D6-4AFA-A30A-22207B226377} {6C152128-CC1B-4140-9FE1-8E86B013AB88} = {A2159B1B-25D6-4AFA-A30A-22207B226377} {BD232C77-3DEB-4CB1-8BC7-30A4C1A50DEA} = {68E461E4-64AD-4CC4-959D-613F5C84242B} + {07625BFB-1644-415D-B2B8-468152E05C3C} = {63D634C9-3F19-4305-8831-D47A380C9EAB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B2AD5ABD-A184-4781-910A-F187301694F8} diff --git a/src/SimpleSOAPClient/Helpers/XmlHelpers.cs b/src/SimpleSOAPClient/Helpers/XmlHelpers.cs index 6f0fea7..90183e1 100644 --- a/src/SimpleSOAPClient/Helpers/XmlHelpers.cs +++ b/src/SimpleSOAPClient/Helpers/XmlHelpers.cs @@ -20,13 +20,25 @@ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#endregion +#endregion + +using System.Runtime.CompilerServices; +using System.IO; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using System; + +[assembly: InternalsVisibleTo("SimpleSOAPClient.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e9d4b3865537a3" + + "5aabe1076ab836c58e47a3315970568d17b1d58b6d08a648e6333a714112adeb9481d79cd3a529" + + "ab6ee0e643b9098fa703ca085202968cb4792fc1ccc0d85fff62ed01993ed67f5cf5c2fac83622" + + "e019654eab372c6c4ecefbc8198b267ed757b30da82779857ca6861204961aa175ef48a7e79ad3" + + "b0d754bd")] namespace SimpleSOAPClient.Helpers { - using System.IO; - using System.Xml; - using System.Xml.Linq; - using System.Xml.Serialization; + + + /// /// Helper class with extensions for XML manipulation @@ -60,6 +72,15 @@ public static string ToXmlString(this T item, bool removeXmlDeclaration) NamespaceHandling = NamespaceHandling.OmitDuplicates })) { +#if NETSTANDARD2_0 || NET45 + if (Attribute.IsDefined(item.GetType(), typeof(System.Runtime.Serialization.DataContractAttribute))) + { + var serializer = new System.Runtime.Serialization.DataContractSerializer(typeof(T)); + serializer.WriteObject(xmlWriter, item); + xmlWriter.Flush(); + return textWriter.ToString(); + } +#endif new XmlSerializer(item.GetType()) .Serialize(xmlWriter, item, EmptyXmlSerializerNamespaces); return textWriter.ToString(); @@ -111,10 +132,18 @@ public static T ToObject(this string xml) { if (string.IsNullOrWhiteSpace(xml)) return default(T); - using (var textWriter = new StringReader(xml)) + using (var stringReader = new StringReader(xml)) + using (var xmlReader = XmlReader.Create(stringReader)) { - var result = (T)new XmlSerializer(typeof(T)).Deserialize(textWriter); +#if NETSTANDARD2_0 || NET45 + if (Attribute.IsDefined(typeof(T), typeof(System.Runtime.Serialization.DataContractAttribute))) + { + var serializer = new System.Runtime.Serialization.DataContractSerializer(typeof(T)); + return (T)serializer.ReadObject(xmlReader); + } +#endif + var result = (T)new XmlSerializer(typeof(T)).Deserialize(xmlReader); return result; } } diff --git a/tests/SimpleSOAPClient.Tests/DataContractModel.cs b/tests/SimpleSOAPClient.Tests/DataContractModel.cs new file mode 100644 index 0000000..22e6c39 --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/DataContractModel.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace SimpleSOAPClient.Tests +{ + [DataContract(Name = "DataContractModel", Namespace ="urn:simplesoapclient:test")] + public class DataContractModel + { + [DataMember(Order = 0)] + public string String { get; set; } + + [DataMember(Order = 1)] + public int Int { get; set; } + + [DataMember(Order = 2)] + public bool Bool { get; set; } + + [DataMember(Order = 3)] + public string[] Array { get; set; } + } +} diff --git a/tests/SimpleSOAPClient.Tests/SimpleSOAPClient.Tests.csproj b/tests/SimpleSOAPClient.Tests/SimpleSOAPClient.Tests.csproj new file mode 100644 index 0000000..3369861 --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/SimpleSOAPClient.Tests.csproj @@ -0,0 +1,24 @@ + + + + ../../tools/SimpleSOAPClient.snk + true + true + + netcoreapp2.0 + + false + + + + + + + + + + + + + + diff --git a/tests/SimpleSOAPClient.Tests/XmlHelpersTests.cs b/tests/SimpleSOAPClient.Tests/XmlHelpersTests.cs new file mode 100644 index 0000000..9ae2045 --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/XmlHelpersTests.cs @@ -0,0 +1,87 @@ +using SimpleSOAPClient.Helpers; +using System.Collections.Generic; +using System.Xml.Linq; +using Xunit; + +namespace SimpleSOAPClient.Tests +{ + public class XmlHelpersTests + { + const string SERIALIZED_XML = @"foo42trueOneTwoThree"; + const string SERIALIZED_DATACONTRACT = @"foo42trueOneTwoThree"; + + readonly XmlModel _xmlModel = new XmlModel + { + String = "foo", + Bool = true, + Int = 42, + Array = new[] { "One", "Two", "Three" } + }; + + readonly DataContractModel _dataContractModel = new DataContractModel + { + String = "foo", + Bool = true, + Int = 42, + Array = new[] { "One", "Two", "Three" } + }; + + [Fact] + public void ToObject_XmlSerializer() + { + var result = XmlHelpers.ToObject(SERIALIZED_XML); + Assert.Equal("foo", result.String); + Assert.True(result.Bool); + Assert.Equal(42, result.Int); + Assert.Equal(3, result.Array.Length); + } + + [Fact] + public void ToObject_DataContractSerializer() + { + var result = XmlHelpers.ToObject(SERIALIZED_DATACONTRACT); + Assert.Equal("foo", result.String); + Assert.True(result.Bool); + Assert.Equal(42, result.Int); + Assert.Equal(3, result.Array.Length); + } + + + [Fact] + public void ToXElement_XmlSerializer() + { + var result = XmlHelpers.ToXElement(_xmlModel); + var actualXml = result.ToString(); + Assert.Equal(SERIALIZED_XML, actualXml, new XmlEqualityComparer()); + } + + [Fact] + public void ToXElement_DataContractSerializer() + { + var result = XmlHelpers.ToXElement(_dataContractModel); + var actualXml = result.ToString(); + Assert.Equal(SERIALIZED_DATACONTRACT, actualXml, new XmlEqualityComparer()); + } + + [Fact] + public void ToXmlString_XmlSerializer() + { + var result = XmlHelpers.ToXmlString(_xmlModel); + Assert.Equal(SERIALIZED_XML, result, new XmlEqualityComparer()); + } + + [Fact] + public void ToXmlString_DataContractSerializer() + { + var result = XmlHelpers.ToXmlString(_dataContractModel); + Assert.Equal(SERIALIZED_DATACONTRACT, result, new XmlEqualityComparer()); + } + + class XmlEqualityComparer : IEqualityComparer + { + public bool Equals(string x, string y) => XmlNormalizer.DeepEqualsWithNormalization(XDocument.Parse(x), XDocument.Parse(y), null); + + public int GetHashCode(string obj) => XmlNormalizer.Normalize(XDocument.Parse(obj), null).GetHashCode(); + } + } +} diff --git a/tests/SimpleSOAPClient.Tests/XmlModel.cs b/tests/SimpleSOAPClient.Tests/XmlModel.cs new file mode 100644 index 0000000..7cc8bb5 --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/XmlModel.cs @@ -0,0 +1,20 @@ +using System.Xml.Serialization; + +namespace SimpleSOAPClient.Tests +{ + [XmlRoot(Namespace = "urn:simplesoapclient:test")] + public class XmlModel + { + [XmlElement(Order = 0)] + public string String { get; set; } + + [XmlElement(Order = 1)] + public int Int { get; set; } + + [XmlElement(Order = 2)] + public bool Bool { get; set; } + + [XmlArray(Order = 3)] + public string[] Array { get; set; } + } +} diff --git a/tests/SimpleSOAPClient.Tests/XmlNormalizer.cs b/tests/SimpleSOAPClient.Tests/XmlNormalizer.cs new file mode 100644 index 0000000..28d565a --- /dev/null +++ b/tests/SimpleSOAPClient.Tests/XmlNormalizer.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Schema; + +namespace SimpleSOAPClient.Tests +{ + // From https://blogs.msdn.microsoft.com/xmlteam/2009/02/16/equality-semantics-of-linq-to-xml-trees/ + public class XmlNormalizer + { + private static class Xsi + { + public static XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance"; + + public static XName schemaLocation = xsi + "schemaLocation"; + public static XName noNamespaceSchemaLocation = xsi + "noNamespaceSchemaLocation"; + } + + public static XDocument Normalize(XDocument source, XmlSchemaSet schema) + { + bool havePSVI = false; + // validate, throw errors, add PSVI information + if (schema != null) + { + source.Validate(schema, null, true); + havePSVI = true; + } + return new XDocument( + source.Declaration, + source.Nodes().Select(n => + { + // Remove comments, processing instructions, and text nodes that are + // children of XDocument. Only white space text nodes are allowed as + // children of a document, so we can remove all text nodes. + if (n is XComment || n is XProcessingInstruction || n is XText) + return null; + XElement e = n as XElement; + if (e != null) + return NormalizeElement(e, havePSVI); + return n; + } + ) + ); + } + + public static bool DeepEqualsWithNormalization(XDocument doc1, XDocument doc2, + XmlSchemaSet schemaSet) + { + XDocument d1 = Normalize(doc1, schemaSet); + XDocument d2 = Normalize(doc2, schemaSet); + return XNode.DeepEquals(d1, d2); + } + + private static IEnumerable NormalizeAttributes(XElement element, + bool havePSVI) + { + return element.Attributes() + .Where(a => !a.IsNamespaceDeclaration && + a.Name != Xsi.schemaLocation && + a.Name != Xsi.noNamespaceSchemaLocation) + .OrderBy(a => a.Name.NamespaceName) + .ThenBy(a => a.Name.LocalName) + .Select( + a => + { + if (havePSVI) + { + var dt = a.GetSchemaInfo().SchemaType.TypeCode; + switch (dt) + { + case XmlTypeCode.Boolean: + return new XAttribute(a.Name, (bool)a); + case XmlTypeCode.DateTime: + return new XAttribute(a.Name, (DateTime)a); + case XmlTypeCode.Decimal: + return new XAttribute(a.Name, (decimal)a); + case XmlTypeCode.Double: + return new XAttribute(a.Name, (double)a); + case XmlTypeCode.Float: + return new XAttribute(a.Name, (float)a); + case XmlTypeCode.HexBinary: + case XmlTypeCode.Language: + return new XAttribute(a.Name, + ((string)a).ToLower()); + } + } + return a; + } + ); + } + + private static XNode NormalizeNode(XNode node, bool havePSVI) + { + // trim comments and processing instructions from normalized tree + if (node is XComment || node is XProcessingInstruction) + return null; + XElement e = node as XElement; + if (e != null) + return NormalizeElement(e, havePSVI); + // Only thing left is XCData and XText, so clone them + return node; + } + + private static XElement NormalizeElement(XElement element, bool havePSVI) + { + if (havePSVI) + { + var dt = element.GetSchemaInfo(); + switch (dt.SchemaType.TypeCode) + { + case XmlTypeCode.Boolean: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + (bool)element); + case XmlTypeCode.DateTime: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + (DateTime)element); + case XmlTypeCode.Decimal: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + (decimal)element); + case XmlTypeCode.Double: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + (double)element); + case XmlTypeCode.Float: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + (float)element); + case XmlTypeCode.HexBinary: + case XmlTypeCode.Language: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + ((string)element).ToLower()); + default: + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + element.Nodes().Select(n => NormalizeNode(n, havePSVI)), + (!element.IsEmpty && !element.Nodes().OfType().Any()) ? + "" : null + ); + } + } + else + { + return new XElement(element.Name, + NormalizeAttributes(element, havePSVI), + element.Nodes().Select(n => NormalizeNode(n, havePSVI)), + (!element.IsEmpty && !element.Nodes().OfType().Any()) ? + "" : null + ); + } + } + } +}