From 01401f04b3c71009d68b0eb54de59da06b2a96e3 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Thu, 12 Nov 2020 21:34:40 +0100 Subject: [PATCH 01/18] Add support for repeated XML elements without a name attribute This commit adds support in Microsoft.Extensions.Configuration.Xml for repeated XML elements without requiring a Name attribute. This solves a particularly subtle bug when configuring Serilog from an XML configuration source. For a full description, see #36541 The original implementation of the XmlStreamConfigurationProvider has been modified to do the following: - Maintain a stack of encountered XML elements while traversing the XML source. This is needed to detect siblings. - When siblings are detected, automatically append an index to the generated configuration keys. This makes it work exactly the same as the JSON configuration provider with JSON arrays. Tests are updated to reflect the new behavior: - Tests that verified an exception occurs when entering duplicate keys have been removed. Duplicate keys are supported now. - Add tests that verify duplicate keys result in the correct configuration keys, with all the lower/upper case variants and with support for the special "Name" attribute handling. --- .../src/IXmlConfigurationValue.cs | 15 ++ .../src/XmlConfigurationElement.cs | 71 +++++++++ .../XmlConfigurationElementAttributeValue.cs | 34 ++++ .../src/XmlConfigurationElementContent.cs | 33 ++++ .../src/XmlStreamConfigurationProvider.cs | 129 ++++++++-------- .../tests/ConfigurationProviderXmlTest.cs | 10 ++ .../tests/XmlConfigurationTest.cs | 145 ++++++++++++++++++ 7 files changed, 370 insertions(+), 67 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs new file mode 100644 index 00000000000000..2914c209e605d7 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.Xml +{ + /// + /// Represents a configuration value that was parsed from an XML source + /// + internal interface IXmlConfigurationValue + { + string Key { get; } + string Value { get; } + string LineInfo { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs new file mode 100644 index 00000000000000..32a84aa971739c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Extensions.Configuration.Xml +{ + internal class XmlConfigurationElement + { + public string ElementName { get; } + + public string Name { get; } + + public string LineInfo { get; } + + public bool Multiple { get; set; } + + public int Index { get; set; } + + /// + /// The parent element, or null if this is the root element + /// + public XmlConfigurationElement Parent { get; } + + public XmlConfigurationElement(XmlConfigurationElement parent, string elementName, string name, string lineInfo) + { + Parent = parent; + ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); + Name = name; + LineInfo = lineInfo; + } + + public string Key + { + get + { + var tokens = new List(3); + + // the root element does not contribute to the prefix + if (Parent != null) tokens.Add(ElementName); + + // the name attribute always contributes to the prefix + if (Name != null) tokens.Add(Name); + + // the index only contributes to the prefix when there are multiple elements wih the same name + if (Multiple) tokens.Add(Index.ToString()); + + // the root element without a name attribute does not contribute to prefix at all + if (!tokens.Any()) return null; + + return string.Join(ConfigurationPath.KeyDelimiter, tokens); + } + } + + public bool IsSibling(XmlConfigurationElement xmlConfigurationElement) + { + if (xmlConfigurationElement is null) + { + throw new ArgumentNullException(nameof(xmlConfigurationElement)); + } + + return Parent != null + && xmlConfigurationElement.Parent == Parent + && string.Equals(ElementName, xmlConfigurationElement.ElementName) + && string.Equals(Name, xmlConfigurationElement.Name); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs new file mode 100644 index 00000000000000..d1a169ee376535 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.Configuration.Xml +{ + internal class XmlConfigurationElementAttributeValue : IXmlConfigurationValue + { + private readonly XmlConfigurationElement[] _elementPath; + private readonly string _attribute; + + public XmlConfigurationElementAttributeValue(Stack elementPath, string attribute, string value, string lineInfo) + { + _elementPath = elementPath?.Reverse()?.ToArray() ?? throw new ArgumentNullException(nameof(elementPath)); + _attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + Value = value ?? throw new ArgumentNullException(nameof(value)); + LineInfo = lineInfo; + } + + /// + /// Combines the path to this element with the attribute value to produce a key. + /// Note that this property cannot be computed during construction, + /// because the keys of the elements along the path may change when multiple elements with the same name are encountered + /// + public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Concat(new[] { _attribute }).Where(key => key != null)); + + public string Value { get; } + + public string LineInfo { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs new file mode 100644 index 00000000000000..2aedaea171707b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.Configuration.Xml +{ + internal class XmlConfigurationElementContent + : IXmlConfigurationValue + { + private readonly XmlConfigurationElement[] _elementPath; + + public XmlConfigurationElementContent(Stack elementPath, string content, string lineInfo) + { + Value = content ?? throw new ArgumentNullException(nameof(content)); + LineInfo = lineInfo ?? throw new ArgumentNullException(nameof(lineInfo)); + _elementPath = elementPath?.Reverse().ToArray() ?? throw new ArgumentNullException(nameof(elementPath)); + } + + /// + /// Combines the path to this element to produce a key. + /// Note that this property cannot be computed during construction, + /// because the keys of the elements along the path may change when multiple elements with the same name are encountered + /// + public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Where(key => key != null)); + + public string Value { get; } + + public string LineInfo { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index bd01128ab68136..77b40fc4c77026 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -30,7 +30,7 @@ public XmlStreamConfigurationProvider(XmlStreamConfigurationSource source) : bas /// The which was read from the stream. public static IDictionary Read(Stream stream, XmlDocumentDecryptor decryptor) { - var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + var configurationValues = new List(); var readerSettings = new XmlReaderSettings() { @@ -42,13 +42,11 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt using (XmlReader reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings)) { - var prefixStack = new Stack(); - - SkipUntilRootElement(reader); + // record all elements we encounter to check for repeated elements + var allElements = new List(); - // We process the root element individually since it doesn't contribute to prefix - ProcessAttributes(reader, prefixStack, data, AddNamePrefix); - ProcessAttributes(reader, prefixStack, data, AddAttributePair); + // keep track of the tree we followed to get where we are (breadcrumb style) + var currentPath = new Stack(); XmlNodeType preNodeType = reader.NodeType; while (reader.Read()) @@ -56,43 +54,50 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt switch (reader.NodeType) { case XmlNodeType.Element: - prefixStack.Push(reader.LocalName); - ProcessAttributes(reader, prefixStack, data, AddNamePrefix); - ProcessAttributes(reader, prefixStack, data, AddAttributePair); + XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null; + + var element = new XmlConfigurationElement(parent, reader.LocalName, GetName(reader), GetLineInfo(reader)); + + // check if this element has appeared before + XmlConfigurationElement sibling = allElements.Where(e => e.IsSibling(element)).OrderByDescending(e => e.Index).FirstOrDefault(); + if (sibling != null) + { + sibling.Multiple = element.Multiple = true; + element.Index = sibling.Index + 1; + } + + currentPath.Push(element); + allElements.Add(element); + + ProcessAttributes(reader, currentPath, configurationValues); // If current element is self-closing if (reader.IsEmptyElement) { - prefixStack.Pop(); + currentPath.Pop(); } break; case XmlNodeType.EndElement: - if (prefixStack.Any()) + if (currentPath.Any()) { // If this EndElement node comes right after an Element node, // it means there is no text/CDATA node in current element if (preNodeType == XmlNodeType.Element) { - string key = ConfigurationPath.Combine(prefixStack.Reverse()); - data[key] = string.Empty; + var configurationValue = new XmlConfigurationElementContent(currentPath, string.Empty, GetLineInfo(reader)); + configurationValues.Add(configurationValue); } - prefixStack.Pop(); + currentPath.Pop(); } break; case XmlNodeType.CDATA: case XmlNodeType.Text: { - string key = ConfigurationPath.Combine(prefixStack.Reverse()); - if (data.ContainsKey(key)) - { - throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, - GetLineInfo(reader))); - } - - data[key] = reader.Value; + var configurationValue = new XmlConfigurationElementContent(currentPath, reader.Value, GetLineInfo(reader)); + configurationValues.Add(configurationValue); break; } case XmlNodeType.XmlDeclaration: @@ -107,6 +112,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt GetLineInfo(reader))); } preNodeType = reader.NodeType; + // If this element is a self-closing element, // we pretend that we just processed an EndElement node // because a self-closing element contains an end within itself @@ -117,6 +123,19 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt } } } + + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var configurationValue in configurationValues) + { + var key = configurationValue.Key; + if (data.ContainsKey(key)) + { + throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, configurationValue.LineInfo)); + } + data[key] = configurationValue.Value; + } + return data; } @@ -129,18 +148,6 @@ public override void Load(Stream stream) Data = Read(stream, XmlDocumentDecryptor.Instance); } - private static void SkipUntilRootElement(XmlReader reader) - { - while (reader.Read()) - { - if (reader.NodeType != XmlNodeType.XmlDeclaration && - reader.NodeType != XmlNodeType.ProcessingInstruction) - { - break; - } - } - } - private static string GetLineInfo(XmlReader reader) { var lineInfo = reader as IXmlLineInfo; @@ -148,8 +155,7 @@ private static string GetLineInfo(XmlReader reader) SR.Format(SR.Msg_LineInfo, lineInfo.LineNumber, lineInfo.LinePosition); } - private static void ProcessAttributes(XmlReader reader, Stack prefixStack, IDictionary data, - Action, IDictionary, XmlWriter> act, XmlWriter writer = null) + private static void ProcessAttributes(XmlReader reader, Stack elementPath, IList data) { for (int i = 0; i < reader.AttributeCount; i++) { @@ -161,7 +167,7 @@ private static void ProcessAttributes(XmlReader reader, Stack prefixStac throw new FormatException(SR.Format(SR.Error_NamespaceIsNotSupported, GetLineInfo(reader))); } - act(reader, prefixStack, data, writer); + data.Add(new XmlConfigurationElementAttributeValue(elementPath, reader.LocalName, reader.Value, GetLineInfo(reader))); } // Go back to the element containing the attributes we just processed @@ -169,41 +175,30 @@ private static void ProcessAttributes(XmlReader reader, Stack prefixStac } // The special attribute "Name" only contributes to prefix - // This method adds a prefix if current node in reader represents a "Name" attribute - private static void AddNamePrefix(XmlReader reader, Stack prefixStack, - IDictionary data, XmlWriter writer) + // This method retrieves the Name of the element, if the attribute is present + // Unfortunately XmlReader.GetAttribute cannot be used, as it does not support looking for attributes in a case insensitive manner + private static string GetName(XmlReader reader) { - if (!string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase)) - { - return; - } + string name = null; - // If current element is not root element - if (prefixStack.Any()) - { - string lastPrefix = prefixStack.Pop(); - prefixStack.Push(ConfigurationPath.Combine(lastPrefix, reader.Value)); - } - else + while (reader.MoveToNextAttribute()) { - prefixStack.Push(reader.Value); + if (string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase)) + { + // If there is a namespace attached to current attribute + if (!string.IsNullOrEmpty(reader.NamespaceURI)) + { + throw new FormatException(SR.Format(SR.Error_NamespaceIsNotSupported, GetLineInfo(reader))); + } + name = reader.Value; + break; + } } - } - // Common attributes contribute to key-value pairs - // This method adds a key-value pair if current node in reader represents a common attribute - private static void AddAttributePair(XmlReader reader, Stack prefixStack, - IDictionary data, XmlWriter writer) - { - prefixStack.Push(reader.LocalName); - string key = ConfigurationPath.Combine(prefixStack.Reverse()); - if (data.ContainsKey(key)) - { - throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, GetLineInfo(reader))); - } + // Go back to the element containing the name we just processed + reader.MoveToElement(); - data[key] = reader.Value; - prefixStack.Pop(); + return name; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs index 270a20f858edb2..76cf74aa7cf5a6 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs @@ -20,6 +20,16 @@ public override void Combine_after_other_provider() // Disabled test due to XML handling of empty section. } + public override void Load_from_single_provider_with_duplicates_throws() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig))); + } + + public override void Load_from_single_provider_with_differing_case_duplicates_throws() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig))); + } + public override void Has_debug_view() { var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs index b9c59d93b4dc40..c1d5b4e8e4821e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs @@ -168,6 +168,32 @@ public void NameAttributeContributesToPrefix() Assert.Equal("MySql", xmlConfigSrc.Get("Data:Inventory:Provider")); } + [Fact] + public void LowercaseNameAttributeContributesToPrefix() + { + var xml = + @" + + TestConnectionString + SqlClient + + + AnotherTestConnectionString + MySql + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("DefaultConnection", xmlConfigSrc.Get("Data:DefaultConnection:Name")); + Assert.Equal("TestConnectionString", xmlConfigSrc.Get("Data:DefaultConnection:ConnectionString")); + Assert.Equal("SqlClient", xmlConfigSrc.Get("Data:DefaultConnection:Provider")); + Assert.Equal("Inventory", xmlConfigSrc.Get("Data:Inventory:Name")); + Assert.Equal("AnotherTestConnectionString", xmlConfigSrc.Get("Data:Inventory:ConnectionString")); + Assert.Equal("MySql", xmlConfigSrc.Get("Data:Inventory:Provider")); + } + [Fact] public void NameAttributeInRootElementContributesToPrefix() { @@ -193,6 +219,125 @@ public void NameAttributeInRootElementContributesToPrefix() Assert.Equal("MySql", xmlConfigSrc.Get("Data:Inventory:Provider")); } + [Fact] + public void RepeatedElementsContributeToPrefix() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:0:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:1:Provider")); + } + + [Fact] + public void RepeatedElementsUnderNameContributeToPrefix() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("Data:DefaultConnection:0:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("Data:DefaultConnection:0:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("Data:DefaultConnection:1:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("Data:DefaultConnection:1:Provider")); + } + + [Fact] + public void RepeatedElementsWithSameNameContributeToPrefix() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:Data:0:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:Data:0:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:Data:1:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:Data:1:Provider")); + } + + [Fact] + public void RepeatedElementsWithDifferentNamesContributeToPrefix() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:Data1:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:Data1:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:Data2:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:Data2:Provider")); + } + + [Fact] + public void NestedRepeatedElementsContributeToPrefix() + { + var xml = + @" + + TestConnectionString1 + TestConnectionString2 + + + TestConnectionString3 + TestConnectionString4 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString:0")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString:1")); + Assert.Equal("TestConnectionString3", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString:0")); + Assert.Equal("TestConnectionString4", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString:1")); + } + [Fact] public void SupportMixingNameAttributesAndCommonAttributes() { From f83dcfefa4929de639c5f71de4ba066d3db4c2bc Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 13 Nov 2020 01:23:00 +0100 Subject: [PATCH 02/18] Disable tests for duplicate handling in XML config, they are incompatible with new implementation --- .../tests/ConfigurationProviderXmlTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs index 76cf74aa7cf5a6..6c540628b4e9d0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs @@ -22,12 +22,12 @@ public override void Combine_after_other_provider() public override void Load_from_single_provider_with_duplicates_throws() { - AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig))); + // Disabled test due to XML handling of duplicate keys section. } public override void Load_from_single_provider_with_differing_case_duplicates_throws() { - AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig))); + // Disabled test due to XML handling of duplicate keys section. } public override void Has_debug_view() From 8de7bdaf857f14df639fac37e02a4627b6569e33 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 13 Nov 2020 08:33:12 +0100 Subject: [PATCH 03/18] Stop using the 'Name' parameter to use arrays in XML in the tests, this is no longer required --- .../tests/ConfigurationProviderXmlTest.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs index 6c540628b4e9d0..da6d968b089a36 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs @@ -43,11 +43,8 @@ public override void Has_debug_view() Key3=Value123 ({providerTag}) Key3a: 0=ArrayValue0 ({providerTag}) - Name=0 ({providerTag}) 1=ArrayValue1 ({providerTag}) - Name=1 ({providerTag}) 2=ArrayValue2 ({providerTag}) - Name=2 ({providerTag}) Section3: Section4: Key4=Value344 ({providerTag}) @@ -85,7 +82,7 @@ private void SectionToXml(StringBuilder xmlBuilder, string sectionName, TestSect { for (var i = 0; i < tuple.Value.AsArray.Length; i++) { - xmlBuilder.AppendLine($"<{tuple.Key} Name=\"{i}\">{tuple.Value.AsArray[i]}"); + xmlBuilder.AppendLine($"<{tuple.Key}>{tuple.Value.AsArray[i]}"); } } } From ca7097a0ae69a26391f9f351f8e800f3589ae4a0 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 18 Dec 2020 17:07:33 +0100 Subject: [PATCH 04/18] Rework the XML configuration provider The XML configuration provider now builds a simple in-memory model of the XML elements that it encounters Each element keeps track of its attributes, children and content. Furthermore, each element also has a reference to its siblings. Each group of siblings all share the same list. All of the above makes it possible to intelligently produce configuration keys and values, taking into account repeated XML elements. --- .../src/IXmlConfigurationValue.cs | 15 -- .../src/XmlConfigurationElement.cs | 54 ++--- .../XmlConfigurationElementAttributeValue.cs | 19 +- .../src/XmlConfigurationElementContent.cs | 33 --- .../src/XmlConfigurationElementTextContent.cs | 20 ++ .../src/XmlStreamConfigurationProvider.cs | 227 ++++++++++++++---- 6 files changed, 222 insertions(+), 146 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs delete mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs deleted file mode 100644 index 2914c209e605d7..00000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/IXmlConfigurationValue.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Extensions.Configuration.Xml -{ - /// - /// Represents a configuration value that was parsed from an XML source - /// - internal interface IXmlConfigurationValue - { - string Key { get; } - string Value { get; } - string LineInfo { get; } - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index 32a84aa971739c..0bbd21cfda9a9a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace Microsoft.Extensions.Configuration.Xml { @@ -16,55 +14,41 @@ internal class XmlConfigurationElement public string LineInfo { get; } - public bool Multiple { get; set; } - - public int Index { get; set; } + /// + /// The children of this element + /// + public List? Children { get; set; } /// - /// The parent element, or null if this is the root element + /// The siblings of this element, including itself + /// Elements are considered siblings if they share the same element name and name attribute + /// This list is shared by each sibling /// - public XmlConfigurationElement Parent { get; } + public List? Siblings { get; set; } + + public XmlConfigurationElementTextContent? TextContent { get; set; } - public XmlConfigurationElement(XmlConfigurationElement parent, string elementName, string name, string lineInfo) + public List? Attributes { get; set; } + + public XmlConfigurationElement(string elementName, string name, string lineInfo) { - Parent = parent; ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); Name = name; LineInfo = lineInfo; + Children = null; + Siblings = null; + TextContent = null; + Attributes = null; } - public string Key - { - get - { - var tokens = new List(3); - - // the root element does not contribute to the prefix - if (Parent != null) tokens.Add(ElementName); - - // the name attribute always contributes to the prefix - if (Name != null) tokens.Add(Name); - - // the index only contributes to the prefix when there are multiple elements wih the same name - if (Multiple) tokens.Add(Index.ToString()); - - // the root element without a name attribute does not contribute to prefix at all - if (!tokens.Any()) return null; - - return string.Join(ConfigurationPath.KeyDelimiter, tokens); - } - } - - public bool IsSibling(XmlConfigurationElement xmlConfigurationElement) + public bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) { if (xmlConfigurationElement is null) { throw new ArgumentNullException(nameof(xmlConfigurationElement)); } - return Parent != null - && xmlConfigurationElement.Parent == Parent - && string.Equals(ElementName, xmlConfigurationElement.ElementName) + return string.Equals(ElementName, xmlConfigurationElement.ElementName) && string.Equals(Name, xmlConfigurationElement.Name); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs index d1a169ee376535..ca119f2e2e2d45 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs @@ -2,30 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Linq; namespace Microsoft.Extensions.Configuration.Xml { - internal class XmlConfigurationElementAttributeValue : IXmlConfigurationValue + internal class XmlConfigurationElementAttributeValue { - private readonly XmlConfigurationElement[] _elementPath; - private readonly string _attribute; - - public XmlConfigurationElementAttributeValue(Stack elementPath, string attribute, string value, string lineInfo) + public XmlConfigurationElementAttributeValue(string attribute, string value, string lineInfo) { - _elementPath = elementPath?.Reverse()?.ToArray() ?? throw new ArgumentNullException(nameof(elementPath)); - _attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); + Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); Value = value ?? throw new ArgumentNullException(nameof(value)); LineInfo = lineInfo; } - /// - /// Combines the path to this element with the attribute value to produce a key. - /// Note that this property cannot be computed during construction, - /// because the keys of the elements along the path may change when multiple elements with the same name are encountered - /// - public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Concat(new[] { _attribute }).Where(key => key != null)); + public string Attribute { get; } public string Value { get; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs deleted file mode 100644 index 2aedaea171707b..00000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementContent.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Microsoft.Extensions.Configuration.Xml -{ - internal class XmlConfigurationElementContent - : IXmlConfigurationValue - { - private readonly XmlConfigurationElement[] _elementPath; - - public XmlConfigurationElementContent(Stack elementPath, string content, string lineInfo) - { - Value = content ?? throw new ArgumentNullException(nameof(content)); - LineInfo = lineInfo ?? throw new ArgumentNullException(nameof(lineInfo)); - _elementPath = elementPath?.Reverse().ToArray() ?? throw new ArgumentNullException(nameof(elementPath)); - } - - /// - /// Combines the path to this element to produce a key. - /// Note that this property cannot be computed during construction, - /// because the keys of the elements along the path may change when multiple elements with the same name are encountered - /// - public string Key => ConfigurationPath.Combine(_elementPath.Select(e => e.Key).Where(key => key != null)); - - public string Value { get; } - - public string LineInfo { get; } - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs new file mode 100644 index 00000000000000..263773515fcff5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Configuration.Xml +{ + internal class XmlConfigurationElementTextContent + { + public XmlConfigurationElementTextContent(string textContent, string lineInfo) + { + TextContent = textContent ?? throw new ArgumentNullException(nameof(textContent)); + LineInfo = lineInfo ?? throw new ArgumentNullException(nameof(lineInfo)); + } + + public string TextContent { get; } + + public string LineInfo { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 77b40fc4c77026..acfebcd80c7ca7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Xml; @@ -30,8 +31,6 @@ public XmlStreamConfigurationProvider(XmlStreamConfigurationSource source) : bas /// The which was read from the stream. public static IDictionary Read(Stream stream, XmlDocumentDecryptor decryptor) { - var configurationValues = new List(); - var readerSettings = new XmlReaderSettings() { CloseInput = false, // caller will close the stream @@ -40,66 +39,93 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt IgnoreWhitespace = true }; + XmlConfigurationElement? root = null; + using (XmlReader reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings)) { - // record all elements we encounter to check for repeated elements - var allElements = new List(); - // keep track of the tree we followed to get where we are (breadcrumb style) var currentPath = new Stack(); XmlNodeType preNodeType = reader.NodeType; + while (reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: - XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null; + { + var element = new XmlConfigurationElement(reader.LocalName, GetName(reader), GetLineInfo(reader)); - var element = new XmlConfigurationElement(parent, reader.LocalName, GetName(reader), GetLineInfo(reader)); + XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null; - // check if this element has appeared before - XmlConfigurationElement sibling = allElements.Where(e => e.IsSibling(element)).OrderByDescending(e => e.Index).FirstOrDefault(); - if (sibling != null) - { - sibling.Multiple = element.Multiple = true; - element.Index = sibling.Index + 1; - } + if (parent == null) + { + root = element; + } + else + { + if (parent.Children == null) + { + parent.Children = new List(); + } + else + { + // check if this element has appeared before, elements are considered siblings if their element names match + XmlConfigurationElement sibling = parent.Children.FirstOrDefault(e => element.IsSiblingOf(e)); - currentPath.Push(element); - allElements.Add(element); + if (sibling != null) + { + var siblings = sibling.Siblings; - ProcessAttributes(reader, currentPath, configurationValues); + // If this is the first sibling, we must initialize the siblings list + if (siblings == null) + { + siblings = sibling.Siblings = new List { sibling }; + } - // If current element is self-closing - if (reader.IsEmptyElement) - { - currentPath.Pop(); + // Add the current element to the shared siblings list and give it access to the shared list + siblings.Add(element); + element.Siblings = siblings; + } + } + + parent.Children.Add(element); + } + + currentPath.Push(element); + + ProcessAttributes(reader, element); + + // If current element is self-closing + if (reader.IsEmptyElement) + { + currentPath.Pop(); + } } break; - case XmlNodeType.EndElement: if (currentPath.Any()) { + XmlConfigurationElement parent = currentPath.Pop(); + // If this EndElement node comes right after an Element node, // it means there is no text/CDATA node in current element if (preNodeType == XmlNodeType.Element) { - var configurationValue = new XmlConfigurationElementContent(currentPath, string.Empty, GetLineInfo(reader)); - configurationValues.Add(configurationValue); + parent.TextContent = new XmlConfigurationElementTextContent(string.Empty, GetLineInfo(reader)); } - - currentPath.Pop(); } break; case XmlNodeType.CDATA: case XmlNodeType.Text: + if (currentPath.Any()) { - var configurationValue = new XmlConfigurationElementContent(currentPath, reader.Value, GetLineInfo(reader)); - configurationValues.Add(configurationValue); - break; + XmlConfigurationElement parent = currentPath.Peek(); + + parent.TextContent = new XmlConfigurationElementTextContent(reader.Value, GetLineInfo(reader)); } + break; case XmlNodeType.XmlDeclaration: case XmlNodeType.ProcessingInstruction: case XmlNodeType.Comment: @@ -108,35 +134,21 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt break; default: - throw new FormatException(SR.Format(SR.Error_UnsupportedNodeType, reader.NodeType, - GetLineInfo(reader))); + throw new FormatException(SR.Format(SR.Error_UnsupportedNodeType, reader.NodeType, GetLineInfo(reader))); } preNodeType = reader.NodeType; // If this element is a self-closing element, // we pretend that we just processed an EndElement node // because a self-closing element contains an end within itself - if (preNodeType == XmlNodeType.Element && - reader.IsEmptyElement) + if (preNodeType == XmlNodeType.Element && reader.IsEmptyElement) { preNodeType = XmlNodeType.EndElement; } } } - var data = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var configurationValue in configurationValues) - { - var key = configurationValue.Key; - if (data.ContainsKey(key)) - { - throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, configurationValue.LineInfo)); - } - data[key] = configurationValue.Value; - } - - return data; + return ProvideConfiguration(root); } /// @@ -155,8 +167,13 @@ private static string GetLineInfo(XmlReader reader) SR.Format(SR.Msg_LineInfo, lineInfo.LineNumber, lineInfo.LinePosition); } - private static void ProcessAttributes(XmlReader reader, Stack elementPath, IList data) + private static void ProcessAttributes(XmlReader reader, XmlConfigurationElement element) { + if (reader.AttributeCount > 0) + { + element.Attributes = new List(); + } + for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); @@ -167,7 +184,7 @@ private static void ProcessAttributes(XmlReader reader, Stack ProvideConfiguration(XmlConfigurationElement? root) + { + var configuration = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (root == null) + { + return configuration; + } + + var rootPrefix = new List(); + + // The root element only contributes to the prefix via its Name attribute + if (!string.IsNullOrEmpty(root.Name)) + { + rootPrefix.Add(root.Name); + } + + ProcessElementAttributes(rootPrefix, root); + ProcessElementContent(rootPrefix, root); + ProcessElementChildren(rootPrefix, root); + + return configuration; + + void ProcessElement(List prefix, XmlConfigurationElement element) + { + // Add element name to prefix + prefix.Add(element.ElementName); + + // Add value of name attribute to prefix + if (!string.IsNullOrEmpty(element.Name)) + { + prefix.Add(element.Name); + } + + // Add sibling index to prefix + if (element.Siblings != null) + { + prefix.Add(element.Siblings.IndexOf(element).ToString(CultureInfo.InvariantCulture)); + } + + ProcessElementAttributes(prefix, element); + + ProcessElementContent(prefix, element); + + ProcessElementChildren(prefix, element); + + // Remove 'Name' attribute + if (!string.IsNullOrEmpty(element.Name)) + { + prefix.RemoveAt(prefix.Count - 1); + } + + // Remove sibling index + if (element.Siblings != null) + { + prefix.RemoveAt(prefix.Count - 1); + } + + // Remove element name + prefix.RemoveAt(prefix.Count - 1); + } + + void ProcessElementAttributes(List prefix, XmlConfigurationElement element) + { + // Add attributes to configuration values + if (element.Attributes != null) + { + for (var i = 0; i < element.Attributes.Count; i++) + { + var attribute = element.Attributes[i]; + + prefix.Add(attribute.Attribute); + + AddToConfiguration(ConfigurationPath.Combine(prefix), attribute.Value, attribute.LineInfo); + + prefix.RemoveAt(prefix.Count - 1); + } + } + } + + void ProcessElementContent(List prefix, XmlConfigurationElement element) + { + // Add text content to configuration values + if (element.TextContent != null) + { + AddToConfiguration(ConfigurationPath.Combine(prefix), element.TextContent.TextContent, element.TextContent.LineInfo); + } + } + + void ProcessElementChildren(List prefix, XmlConfigurationElement element) + { + // Recursively walk through the children of this element + if (element.Children != null) + { + for (var i = 0; i < element.Children.Count; i++) + { + var child = element.Children[i]; + + ProcessElement(prefix, child); + } + } + } + + void AddToConfiguration(string key, string value, string lineInfo) + { + if (configuration.ContainsKey(key)) + { + throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, lineInfo)); + } + + configuration.Add(key, value); + } + } } } From c60119764185216b5c936ebf7b1a0268a4b0f72f Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 18 Dec 2020 17:22:56 +0100 Subject: [PATCH 05/18] Add test that verifies mixing repeated with non-repeated XML elements works as expected --- .../tests/XmlConfigurationTest.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs index c1d5b4e8e4821e..36dd5440ac6517 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs @@ -338,6 +338,40 @@ public void NestedRepeatedElementsContributeToPrefix() Assert.Equal("TestConnectionString4", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString:1")); } + [Fact] + public void SupportMixingRepeatedElementsWithNonRepeatedElements() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + + MyValue + + + TestConnectionString3 + SqlClient3 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString")); + Assert.Equal("TestConnectionString3", xmlConfigSrc.Get("DefaultConnection:2:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:0:Provider")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:1:Provider")); + Assert.Equal("SqlClient3", xmlConfigSrc.Get("DefaultConnection:2:Provider")); + Assert.Equal("MyValue", xmlConfigSrc.Get("OtherValue:Value")); + } + [Fact] public void SupportMixingNameAttributesAndCommonAttributes() { From 3dddfca44c933bb7a4d41c48b737d8517d70b2a0 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 08:48:07 +0100 Subject: [PATCH 06/18] Cleanup nullable annotations in XmlConfigurationElement Also don't explicitly initialize properties to null --- .../src/XmlConfigurationElement.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index 0bbd21cfda9a9a..b501d79ae030a0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -17,28 +17,24 @@ internal class XmlConfigurationElement /// /// The children of this element /// - public List? Children { get; set; } + public List Children { get; set; } /// /// The siblings of this element, including itself /// Elements are considered siblings if they share the same element name and name attribute /// This list is shared by each sibling /// - public List? Siblings { get; set; } + public List Siblings { get; set; } public XmlConfigurationElementTextContent? TextContent { get; set; } - public List? Attributes { get; set; } + public List Attributes { get; set; } public XmlConfigurationElement(string elementName, string name, string lineInfo) { ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); Name = name; LineInfo = lineInfo; - Children = null; - Siblings = null; - TextContent = null; - Attributes = null; } public bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) From 5b55a5f1a43f51cd02f1619f1fd78c56c19a2b7c Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 08:55:40 +0100 Subject: [PATCH 07/18] Use ordinal ignore case when detecting siblings in XML configuration --- .../src/XmlConfigurationElement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index b501d79ae030a0..f01e298beb213a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -44,8 +44,8 @@ public bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) throw new ArgumentNullException(nameof(xmlConfigurationElement)); } - return string.Equals(ElementName, xmlConfigurationElement.ElementName) - && string.Equals(Name, xmlConfigurationElement.Name); + return string.Equals(ElementName, xmlConfigurationElement.ElementName, StringComparison.OrdinalIgnoreCase) + && string.Equals(Name, xmlConfigurationElement.Name, StringComparison.OrdinalIgnoreCase); } } } From cd11a039dbb77a5314c9447ee90139cfddb78832 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 09:00:24 +0100 Subject: [PATCH 08/18] Remove empty line --- .../src/XmlStreamConfigurationProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index acfebcd80c7ca7..4202718254c7fe 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -55,7 +55,6 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt case XmlNodeType.Element: { var element = new XmlConfigurationElement(reader.LocalName, GetName(reader), GetLineInfo(reader)); - XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null; if (parent == null) From ff2336f8c6c525726f7fabee00c802265b2c4eca Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 09:01:19 +0100 Subject: [PATCH 09/18] Remove usage of Linq method '.Any()' --- .../src/XmlStreamConfigurationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 4202718254c7fe..1b048152b69a9e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -55,7 +55,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt case XmlNodeType.Element: { var element = new XmlConfigurationElement(reader.LocalName, GetName(reader), GetLineInfo(reader)); - XmlConfigurationElement parent = currentPath.Any() ? currentPath.Peek() : null; + XmlConfigurationElement parent = currentPath.Count != 0 ? currentPath.Peek() : null; if (parent == null) { From de44ff0e9bfb176600a3d04fa169463e15c8d7f5 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 09:07:43 +0100 Subject: [PATCH 10/18] Remove dependency on System.Linq in XmlStreamConfigurationProvider --- .../src/XmlConfigurationElement.cs | 16 +++++++++++++++- .../src/XmlStreamConfigurationProvider.cs | 7 +++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index f01e298beb213a..abed1d17778c89 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -37,7 +37,21 @@ public XmlConfigurationElement(string elementName, string name, string lineInfo) LineInfo = lineInfo; } - public bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) + public XmlConfigurationElement FindSiblingInChildren(XmlConfigurationElement element) + { + if (element is null) throw new ArgumentNullException(nameof(element)); + + for (int i = 0; i < Children.Count; i++) { + var child = Children[i]; + + if (child.IsSiblingOf(element)) + return child; + } + + return null; + } + + private bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) { if (xmlConfigurationElement is null) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 1b048152b69a9e..8c222cd29f85cf 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Xml; namespace Microsoft.Extensions.Configuration.Xml @@ -70,7 +69,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt else { // check if this element has appeared before, elements are considered siblings if their element names match - XmlConfigurationElement sibling = parent.Children.FirstOrDefault(e => element.IsSiblingOf(e)); + XmlConfigurationElement sibling = parent.FindSiblingInChildren(element); if (sibling != null) { @@ -103,7 +102,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt } break; case XmlNodeType.EndElement: - if (currentPath.Any()) + if (currentPath.Count != 0) { XmlConfigurationElement parent = currentPath.Pop(); @@ -118,7 +117,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt case XmlNodeType.CDATA: case XmlNodeType.Text: - if (currentPath.Any()) + if (currentPath.Count != 0) { XmlConfigurationElement parent = currentPath.Peek(); From 2f77e01a5b9a4a439517ea04eaa3b2d5fdba0946 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Fri, 5 Feb 2021 09:10:38 +0100 Subject: [PATCH 11/18] Simplify check that detects whether the current element is the root element --- .../src/XmlStreamConfigurationProvider.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 8c222cd29f85cf..5ff11c16449d34 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -54,14 +54,15 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt case XmlNodeType.Element: { var element = new XmlConfigurationElement(reader.LocalName, GetName(reader), GetLineInfo(reader)); - XmlConfigurationElement parent = currentPath.Count != 0 ? currentPath.Peek() : null; - if (parent == null) + if (currentPath.Count == 0) { root = element; } else { + var parent = currentPath.Peek(); + if (parent.Children == null) { parent.Children = new List(); From 9a1af2a055451d90aba731082971302710f02abf Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Mon, 8 Feb 2021 08:54:15 +0100 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Santiago Fernandez Madero --- .../src/XmlConfigurationElement.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index abed1d17778c89..06117f9ab49707 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -39,7 +39,8 @@ public XmlConfigurationElement(string elementName, string name, string lineInfo) public XmlConfigurationElement FindSiblingInChildren(XmlConfigurationElement element) { - if (element is null) throw new ArgumentNullException(nameof(element)); + if (element is null) + throw new ArgumentNullException(nameof(element)); for (int i = 0; i < Children.Count; i++) { var child = Children[i]; From 6f07563b55158158892b54c362db1d31e44ca878 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Mon, 8 Feb 2021 09:33:24 +0100 Subject: [PATCH 13/18] Add test for array simulation using Name attribute --- .../tests/XmlConfigurationTest.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs index 36dd5440ac6517..ee897a08bd76f4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs @@ -219,6 +219,29 @@ public void NameAttributeInRootElementContributesToPrefix() Assert.Equal("MySql", xmlConfigSrc.Get("Data:Inventory:Provider")); } + [Fact] + public void NameAttributeCanBeUsedToSimulateArrays() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:0:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:1:Provider")); + } + [Fact] public void RepeatedElementsContributeToPrefix() { From e479aa9979941a0b379f838e27676bef5b8b2fbf Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Mon, 8 Feb 2021 09:43:16 +0100 Subject: [PATCH 14/18] Add tests related to case insensitivity in XML configuration keys Verify that a duplicate key exception is thrown when keys are duplicate with different casing Verify that XML siblings are properly detected when the siblings have different casing Verify that values with keys that were originally upper case can be retrieved with their lower case counterparts --- .../tests/XmlConfigurationTest.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs index ee897a08bd76f4..4d26562a7035b5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs @@ -265,6 +265,29 @@ public void RepeatedElementsContributeToPrefix() Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:1:Provider")); } + [Fact] + public void RepeatedElementDetectionIsCaseInsensitive() + { + var xml = + @" + + TestConnectionString1 + SqlClient1 + + + TestConnectionString2 + SqlClient2 + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("TestConnectionString1", xmlConfigSrc.Get("DefaultConnection:0:ConnectionString")); + Assert.Equal("SqlClient1", xmlConfigSrc.Get("DefaultConnection:0:Provider")); + Assert.Equal("TestConnectionString2", xmlConfigSrc.Get("DefaultConnection:1:ConnectionString")); + Assert.Equal("SqlClient2", xmlConfigSrc.Get("DefaultConnection:1:Provider")); + } + [Fact] public void RepeatedElementsUnderNameContributeToPrefix() { @@ -419,6 +442,24 @@ public void SupportMixingNameAttributesAndCommonAttributes() Assert.Equal("MySql", xmlConfigSrc.Get("Data:Inventory:Provider")); } + [Fact] + public void KeysAreCaseInsensitive() + { + var xml = + @" + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + + xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)); + + Assert.Equal("DefaultConnection", xmlConfigSrc.Get("data:defaultconnection:name")); + Assert.Equal("TestConnectionString", xmlConfigSrc.Get("data:defaultconnection:connectionstring")); + Assert.Equal("SqlClient", xmlConfigSrc.Get("data:defaultconnection:provider")); + } + [Fact] public void SupportCDATAAsTextNode() { @@ -623,6 +664,30 @@ public void ThrowExceptionWhenKeyIsDuplicated() Assert.Equal(expectedMsg, exception.Message); } + [Fact] + public void ThrowExceptionWhenKeyIsDuplicatedWithDifferentCasing() + { + var xml = + @" + + + TestConnectionString + SqlClient + + + + NewProvider + + "; + var xmlConfigSrc = new XmlConfigurationProvider(new XmlConfigurationSource()); + var expectedMsg = SR.Format(SR.Error_KeyIsDuplicated, "data:defaultconnection:connectionstring", + SR.Format(SR.Msg_LineInfo, 8, 52)); + + var exception = Assert.Throws(() => xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml))); + + Assert.Equal(expectedMsg, exception.Message); + } + [Fact] public void XmlConfiguration_Throws_On_Missing_Configuration_File() { From 8901143c196c11840fec2e37bf467584321310c7 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Sun, 28 Feb 2021 12:10:22 +0100 Subject: [PATCH 15/18] Improve performance of XML configuration provider --- ...Microsoft.Extensions.Configuration.Xml.sln | 88 +++--- .../src/XmlConfigurationElement.cs | 45 +-- .../XmlConfigurationElementAttributeValue.cs | 9 +- .../src/XmlConfigurationElementTextContent.cs | 9 +- .../src/XmlStreamConfigurationProvider.cs | 269 +++++++++++++----- 5 files changed, 259 insertions(+), 161 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln b/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln index 9e53feaf15e697..9c2577618443fd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31005.135 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{8AF3BFFC-9727-4E21-8E91-11501C9F97E4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\ref\Microsoft.Extensions.Configuration.Abstractions.csproj", "{B1E05D83-5479-43B7-B25C-D8243FE6B89F}" @@ -45,7 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Drawing.Common", ".. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{78F4F9EE-7E1D-41B5-B55A-850C1282EA99}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{5B866430-6F0B-49F1-8294-7B07F766797A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{04BA3E3C-6979-4792-B19E-C797AD607F42}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{04F0A4D7-E743-4EFF-9FBD-604309FD075A}" EndProject @@ -74,42 +78,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6236136E-39F1-41AB-BB7A-0A6169D77730}" EndProject Global - GlobalSection(NestedProjects) = preSolution - {8AF3BFFC-9727-4E21-8E91-11501C9F97E4} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} - {B05F7183-70C1-4C10-9FFE-66301E0B076B} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} - {B1E05D83-5479-43B7-B25C-D8243FE6B89F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {81370E0B-BC7E-4F49-BCF1-49E0F67F75F4} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {CA8D3F2F-410D-4E32-B104-12929767D1A8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {A9B02E45-3372-48F4-8761-46F916400B6E} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {5BF7DAFC-8D2A-43A2-82F9-4BD0E3A62126} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {EEA5C768-9A81-4BDC-A9DD-30A2591D63FE} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {600CBFCA-5F97-47EE-8AE9-B6262E1A764B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {981358A2-F5ED-45CE-B037-446BB0F4E859} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {4039B868-612D-420F-BC25-481660475CA8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {1B0E1204-F3BF-4410-BAA1-256DC40EE47B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {1AE88632-F602-4B2F-A269-A7631A361FA7} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {78F4F9EE-7E1D-41B5-B55A-850C1282EA99} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {04F0A4D7-E743-4EFF-9FBD-604309FD075A} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {A3C9F01C-6D4D-413B-BADE-A8B9046F985F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {00C86D9C-1A45-49C7-91E6-24BBBF8950CB} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {74732589-C551-4DEF-B56C-B489400D9951} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {00DA7CF9-86B4-4991-B760-CB3AAB31EED0} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {9C73A2E3-B370-4B24-ACB0-0C3A9069250D} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {21E44CA2-5355-4092-9EF7-A94520EBDD40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {EFC0C2A3-1F51-4299-BE43-78284F6AC670} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {5CB1D123-853E-4FE7-9484-AAAD7330897C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {1999A9E7-2C0F-4C1D-8656-6FA5B0F32469} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {564B50A2-16A3-4AE9-ABE0-F1582F3E97C6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {03A8EBF2-F912-480F-99E1-34A9A33DD525} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {6824AD93-4154-4710-A018-81DA1FA98C40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {26C61EB7-2798-4314-B750-8CD2837D4216} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {5B866430-6F0B-49F1-8294-7B07F766797A} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {A383D45E-58AC-4FC2-AB1E-0BF8666C8623} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {8CBDDA63-8388-42AF-934E-7C60832A9B1C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {B25AD126-F78D-45CC-AD06-6F1E03D570DA} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {D80C2723-C720-4CDF-8DCF-0338799E9121} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {C77305BE-823E-487F-825E-9C26E0674CC6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -207,10 +175,10 @@ Global {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Debug|Any CPU.Build.0 = Debug|Any CPU {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Release|Any CPU.ActiveCfg = Release|Any CPU {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Release|Any CPU.Build.0 = Release|Any CPU - {5B866430-6F0B-49F1-8294-7B07F766797A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B866430-6F0B-49F1-8294-7B07F766797A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5B866430-6F0B-49F1-8294-7B07F766797A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B866430-6F0B-49F1-8294-7B07F766797A}.Release|Any CPU.Build.0 = Release|Any CPU + {04BA3E3C-6979-4792-B19E-C797AD607F42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04BA3E3C-6979-4792-B19E-C797AD607F42}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04BA3E3C-6979-4792-B19E-C797AD607F42}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04BA3E3C-6979-4792-B19E-C797AD607F42}.Release|Any CPU.Build.0 = Release|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Debug|Any CPU.Build.0 = Debug|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -255,6 +223,42 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8AF3BFFC-9727-4E21-8E91-11501C9F97E4} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} + {B1E05D83-5479-43B7-B25C-D8243FE6B89F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {9C73A2E3-B370-4B24-ACB0-0C3A9069250D} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {81370E0B-BC7E-4F49-BCF1-49E0F67F75F4} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {21E44CA2-5355-4092-9EF7-A94520EBDD40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {CA8D3F2F-410D-4E32-B104-12929767D1A8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {EFC0C2A3-1F51-4299-BE43-78284F6AC670} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {A9B02E45-3372-48F4-8761-46F916400B6E} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {5CB1D123-853E-4FE7-9484-AAAD7330897C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {B05F7183-70C1-4C10-9FFE-66301E0B076B} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} + {5BF7DAFC-8D2A-43A2-82F9-4BD0E3A62126} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {1999A9E7-2C0F-4C1D-8656-6FA5B0F32469} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {EEA5C768-9A81-4BDC-A9DD-30A2591D63FE} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {564B50A2-16A3-4AE9-ABE0-F1582F3E97C6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {600CBFCA-5F97-47EE-8AE9-B6262E1A764B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {03A8EBF2-F912-480F-99E1-34A9A33DD525} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {981358A2-F5ED-45CE-B037-446BB0F4E859} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {6824AD93-4154-4710-A018-81DA1FA98C40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {4039B868-612D-420F-BC25-481660475CA8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {26C61EB7-2798-4314-B750-8CD2837D4216} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {1B0E1204-F3BF-4410-BAA1-256DC40EE47B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {1AE88632-F602-4B2F-A269-A7631A361FA7} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {78F4F9EE-7E1D-41B5-B55A-850C1282EA99} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {04BA3E3C-6979-4792-B19E-C797AD607F42} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {04F0A4D7-E743-4EFF-9FBD-604309FD075A} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {A383D45E-58AC-4FC2-AB1E-0BF8666C8623} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {A3C9F01C-6D4D-413B-BADE-A8B9046F985F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {8CBDDA63-8388-42AF-934E-7C60832A9B1C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {00C86D9C-1A45-49C7-91E6-24BBBF8950CB} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {B25AD126-F78D-45CC-AD06-6F1E03D570DA} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {74732589-C551-4DEF-B56C-B489400D9951} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {D80C2723-C720-4CDF-8DCF-0338799E9121} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {00DA7CF9-86B4-4991-B760-CB3AAB31EED0} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {C77305BE-823E-487F-825E-9C26E0674CC6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {830494DE-07B3-4C63-9D74-4A123677D469} EndGlobalSection diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs index 06117f9ab49707..26441a447436cd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs @@ -12,55 +12,30 @@ internal class XmlConfigurationElement public string Name { get; } - public string LineInfo { get; } + /// + /// A composition of ElementName and Name, that serves as the basis for detecting siblings + /// + public string SiblingName { get; } /// /// The children of this element /// - public List Children { get; set; } + public IDictionary> ChildrenBySiblingName { get; set; } /// - /// The siblings of this element, including itself - /// Elements are considered siblings if they share the same element name and name attribute - /// This list is shared by each sibling + /// Performance optimization: do not initialize a dictionary and a list for elements with a single child /// - public List Siblings { get; set; } + public XmlConfigurationElement SingleChild { get; set; } - public XmlConfigurationElementTextContent? TextContent { get; set; } + public XmlConfigurationElementTextContent TextContent { get; set; } public List Attributes { get; set; } - public XmlConfigurationElement(string elementName, string name, string lineInfo) + public XmlConfigurationElement(string elementName, string name) { ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName)); Name = name; - LineInfo = lineInfo; - } - - public XmlConfigurationElement FindSiblingInChildren(XmlConfigurationElement element) - { - if (element is null) - throw new ArgumentNullException(nameof(element)); - - for (int i = 0; i < Children.Count; i++) { - var child = Children[i]; - - if (child.IsSiblingOf(element)) - return child; - } - - return null; - } - - private bool IsSiblingOf(XmlConfigurationElement xmlConfigurationElement) - { - if (xmlConfigurationElement is null) - { - throw new ArgumentNullException(nameof(xmlConfigurationElement)); - } - - return string.Equals(ElementName, xmlConfigurationElement.ElementName, StringComparison.OrdinalIgnoreCase) - && string.Equals(Name, xmlConfigurationElement.Name, StringComparison.OrdinalIgnoreCase); + SiblingName = string.IsNullOrEmpty(Name) ? ElementName : ElementName + ":" + Name; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs index ca119f2e2e2d45..bdea5cd8e60f5f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs @@ -7,17 +7,20 @@ namespace Microsoft.Extensions.Configuration.Xml { internal class XmlConfigurationElementAttributeValue { - public XmlConfigurationElementAttributeValue(string attribute, string value, string lineInfo) + public XmlConfigurationElementAttributeValue(string attribute, string value, int? lineNumber, int? linePosition) { Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute)); Value = value ?? throw new ArgumentNullException(nameof(value)); - LineInfo = lineInfo; + LineNumber = lineNumber; + LinePosition = linePosition; } public string Attribute { get; } public string Value { get; } - public string LineInfo { get; } + public int? LineNumber { get; } + + public int? LinePosition { get; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs index 263773515fcff5..76dab1529dddaf 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs @@ -7,14 +7,17 @@ namespace Microsoft.Extensions.Configuration.Xml { internal class XmlConfigurationElementTextContent { - public XmlConfigurationElementTextContent(string textContent, string lineInfo) + public XmlConfigurationElementTextContent(string textContent, int? linePosition, int? lineNumber) { TextContent = textContent ?? throw new ArgumentNullException(nameof(textContent)); - LineInfo = lineInfo ?? throw new ArgumentNullException(nameof(lineInfo)); + LineNumber = lineNumber; + LinePosition = linePosition; } public string TextContent { get; } - public string LineInfo { get; } + public int? LineNumber { get; } + + public int? LinePosition { get; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 5ff11c16449d34..4428ce37340af5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Text; using System.Xml; namespace Microsoft.Extensions.Configuration.Xml @@ -38,7 +39,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt IgnoreWhitespace = true }; - XmlConfigurationElement? root = null; + XmlConfigurationElement root = null; using (XmlReader reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings)) { @@ -52,54 +53,71 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt switch (reader.NodeType) { case XmlNodeType.Element: + var element = new XmlConfigurationElement(reader.LocalName, GetName(reader)); + + if (currentPath.Count == 0) + { + root = element; + } + else { - var element = new XmlConfigurationElement(reader.LocalName, GetName(reader), GetLineInfo(reader)); + var parent = currentPath.Peek(); - if (currentPath.Count == 0) + // If parent already has a dictionary of children, update the collection accordingly + if (parent.ChildrenBySiblingName != null) { - root = element; + // check if this element has appeared before, elements are considered siblings if their SiblingName properties match + if (parent.ChildrenBySiblingName.TryGetValue(element.SiblingName, out var siblings)) + { + siblings.Add(element); + } + else + { + parent.ChildrenBySiblingName.Add(element.SiblingName, new List { element }); + } } else { - var parent = currentPath.Peek(); - - if (parent.Children == null) + // Performance optimization: parents with a single child don't even initialize a dictionary + if (parent.SingleChild == null) { - parent.Children = new List(); + parent.SingleChild = element; } else { - // check if this element has appeared before, elements are considered siblings if their element names match - XmlConfigurationElement sibling = parent.FindSiblingInChildren(element); + // If we encounter a second child after assigning "SingleChild", we clear SingleChild and initialize the dictionary + var children = new Dictionary>(StringComparer.OrdinalIgnoreCase); - if (sibling != null) + // Special case: the first and second child have the same sibling name + if (string.Equals(parent.SingleChild.SiblingName, element.SiblingName, StringComparison.OrdinalIgnoreCase)) { - var siblings = sibling.Siblings; - - // If this is the first sibling, we must initialize the siblings list - if (siblings == null) + children.Add(element.SiblingName, new List { - siblings = sibling.Siblings = new List { sibling }; - } - - // Add the current element to the shared siblings list and give it access to the shared list - siblings.Add(element); - element.Siblings = siblings; + parent.SingleChild, + element + }); + } + else + { + children.Add(parent.SingleChild.SiblingName, new List { parent.SingleChild }); + children.Add(element.SiblingName, new List { element }); } + + parent.ChildrenBySiblingName = children; + parent.SingleChild = null; } - parent.Children.Add(element); } + } - currentPath.Push(element); + currentPath.Push(element); - ProcessAttributes(reader, element); + ProcessAttributes(reader, element); - // If current element is self-closing - if (reader.IsEmptyElement) - { - currentPath.Pop(); - } + // If current element is self-closing + if (reader.IsEmptyElement) + { + currentPath.Pop(); } break; case XmlNodeType.EndElement: @@ -111,7 +129,10 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt // it means there is no text/CDATA node in current element if (preNodeType == XmlNodeType.Element) { - parent.TextContent = new XmlConfigurationElementTextContent(string.Empty, GetLineInfo(reader)); + var lineInfo = reader as IXmlLineInfo; + var lineNumber = lineInfo?.LineNumber; + var linePosition = lineInfo?.LinePosition; + parent.TextContent = new XmlConfigurationElementTextContent(string.Empty, lineNumber, linePosition); } } break; @@ -120,9 +141,13 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt case XmlNodeType.Text: if (currentPath.Count != 0) { + var lineInfo = reader as IXmlLineInfo; + var lineNumber = lineInfo?.LineNumber; + var linePosition = lineInfo?.LinePosition; + XmlConfigurationElement parent = currentPath.Peek(); - parent.TextContent = new XmlConfigurationElementTextContent(reader.Value, GetLineInfo(reader)); + parent.TextContent = new XmlConfigurationElementTextContent(reader.Value, lineNumber, linePosition); } break; case XmlNodeType.XmlDeclaration: @@ -173,17 +198,22 @@ private static void ProcessAttributes(XmlReader reader, XmlConfigurationElement element.Attributes = new List(); } + var lineInfo = reader as IXmlLineInfo; + for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); + var lineNumber = lineInfo?.LineNumber; + var linePosition = lineInfo?.LinePosition; + // If there is a namespace attached to current attribute if (!string.IsNullOrEmpty(reader.NamespaceURI)) { throw new FormatException(SR.Format(SR.Error_NamespaceIsNotSupported, GetLineInfo(reader))); } - element.Attributes.Add(new XmlConfigurationElementAttributeValue(reader.LocalName, reader.Value, GetLineInfo(reader))); + element.Attributes.Add(new XmlConfigurationElementAttributeValue(reader.LocalName, reader.Value, lineNumber, linePosition)); } // Go back to the element containing the attributes we just processed @@ -217,7 +247,7 @@ private static string GetName(XmlReader reader) return name; } - private static IDictionary ProvideConfiguration(XmlConfigurationElement? root) + private static IDictionary ProvideConfiguration(XmlConfigurationElement root) { var configuration = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -226,12 +256,12 @@ private static IDictionary ProvideConfiguration(XmlConfiguration return configuration; } - var rootPrefix = new List(); + var rootPrefix = new Prefix(); // The root element only contributes to the prefix via its Name attribute if (!string.IsNullOrEmpty(root.Name)) { - rootPrefix.Add(root.Name); + rootPrefix.Push(root.Name); } ProcessElementAttributes(rootPrefix, root); @@ -240,46 +270,16 @@ private static IDictionary ProvideConfiguration(XmlConfiguration return configuration; - void ProcessElement(List prefix, XmlConfigurationElement element) + void ProcessElement(Prefix prefix, XmlConfigurationElement element) { - // Add element name to prefix - prefix.Add(element.ElementName); - - // Add value of name attribute to prefix - if (!string.IsNullOrEmpty(element.Name)) - { - prefix.Add(element.Name); - } - - // Add sibling index to prefix - if (element.Siblings != null) - { - prefix.Add(element.Siblings.IndexOf(element).ToString(CultureInfo.InvariantCulture)); - } - ProcessElementAttributes(prefix, element); ProcessElementContent(prefix, element); ProcessElementChildren(prefix, element); - - // Remove 'Name' attribute - if (!string.IsNullOrEmpty(element.Name)) - { - prefix.RemoveAt(prefix.Count - 1); - } - - // Remove sibling index - if (element.Siblings != null) - { - prefix.RemoveAt(prefix.Count - 1); - } - - // Remove element name - prefix.RemoveAt(prefix.Count - 1); } - void ProcessElementAttributes(List prefix, XmlConfigurationElement element) + void ProcessElementAttributes(Prefix prefix, XmlConfigurationElement element) { // Add attributes to configuration values if (element.Attributes != null) @@ -288,47 +288,160 @@ void ProcessElementAttributes(List prefix, XmlConfigurationElement eleme { var attribute = element.Attributes[i]; - prefix.Add(attribute.Attribute); + prefix.Push(attribute.Attribute); - AddToConfiguration(ConfigurationPath.Combine(prefix), attribute.Value, attribute.LineInfo); + AddToConfiguration(prefix.AsString, attribute.Value, attribute.LineNumber, attribute.LinePosition); - prefix.RemoveAt(prefix.Count - 1); + prefix.Pop(); } } } - void ProcessElementContent(List prefix, XmlConfigurationElement element) + void ProcessElementContent(Prefix prefix, XmlConfigurationElement element) { // Add text content to configuration values if (element.TextContent != null) { - AddToConfiguration(ConfigurationPath.Combine(prefix), element.TextContent.TextContent, element.TextContent.LineInfo); + var textContent = element.TextContent; + AddToConfiguration(prefix.AsString, textContent.TextContent, textContent.LineNumber, textContent.LinePosition); } } - void ProcessElementChildren(List prefix, XmlConfigurationElement element) + void ProcessElementChildren(Prefix prefix, XmlConfigurationElement element) { + if (element.SingleChild != null) + { + var child = element.SingleChild; + + ProcessElementChild(prefix, child, null); + + return; + } + + if (element.ChildrenBySiblingName == null) + { + return; + } + // Recursively walk through the children of this element - if (element.Children != null) + foreach (var childrenWithSameSiblingName in element.ChildrenBySiblingName.Values) { - for (var i = 0; i < element.Children.Count; i++) + if (childrenWithSameSiblingName.Count == 1) { - var child = element.Children[i]; + var child = childrenWithSameSiblingName[0]; - ProcessElement(prefix, child); + ProcessElementChild(prefix, child, null); } + else + { + // Multiple children with the same sibling name. Add the current index to the prefix + for (int i = 0; i < childrenWithSameSiblingName.Count; i++) + { + var child = childrenWithSameSiblingName[i]; + + ProcessElementChild(prefix, child, i); + } + } + } + } + + void ProcessElementChild(Prefix prefix, XmlConfigurationElement child, int? index) + { + // Add element name to prefix + prefix.Push(child.ElementName); + + // Add value of name attribute to prefix + var hasName = !string.IsNullOrEmpty(child.Name); + if (hasName) + { + prefix.Push(child.Name); + } + + // Add index to the prefix + if (index != null) + { + prefix.Push(index.Value.ToString(CultureInfo.InvariantCulture)); + } + + ProcessElement(prefix, child); + + // Remove index + if (index != null) + { + prefix.Pop(); } + + // Remove 'Name' attribute + if (hasName) + { + prefix.Pop(); + } + + // Remove element name + prefix.Pop(); } - void AddToConfiguration(string key, string value, string lineInfo) + void AddToConfiguration(string key, string value, int? lineNumber, int? linePosition) { +#if NETSTANDARD2_1 + if (!configuration.TryAdd(key, value)) + { + var lineInfo = lineNumber == null || linePosition == null + ? string.Empty + : SR.Format(SR.Msg_LineInfo, lineNumber.Value, linePosition.Value); + throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, lineInfo)); + } +#else if (configuration.ContainsKey(key)) { + var lineInfo = lineNumber == null || linePosition == null + ? string.Empty + : SR.Format(SR.Msg_LineInfo, lineNumber.Value, linePosition.Value); throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, lineInfo)); } configuration.Add(key, value); +#endif } } } + + /// + /// Helper class to build the configuration keys in a way that does not require string.Join + /// + internal class Prefix + { + private readonly StringBuilder _sb; + private readonly Stack _lengths; + + public Prefix() + { + _sb = new StringBuilder(); + _lengths = new Stack(); + } + + public string AsString => _sb.ToString(); + + public void Push(string value) + { + if (_sb.Length != 0) + { + _sb.Append(ConfigurationPath.KeyDelimiter); + _sb.Append(value); + _lengths.Push(value.Length + ConfigurationPath.KeyDelimiter.Length); + } + else + { + _sb.Append(value); + _lengths.Push(value.Length); + } + } + + public void Pop() + { + var length = _lengths.Pop(); + + _sb.Remove(_sb.Length - length, length); + } + } } From 862244ed2628626c6730dc12dcae27af81f9061a Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Mon, 1 Mar 2021 12:08:17 +0100 Subject: [PATCH 16/18] Revert accidental change of solution file --- ...Microsoft.Extensions.Configuration.Xml.sln | 88 +++++++++---------- 1 file changed, 42 insertions(+), 46 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln b/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln index 9c2577618443fd..9e53feaf15e697 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/Microsoft.Extensions.Configuration.Xml.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31005.135 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{8AF3BFFC-9727-4E21-8E91-11501C9F97E4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Configuration.Abstractions", "..\Microsoft.Extensions.Configuration.Abstractions\ref\Microsoft.Extensions.Configuration.Abstractions.csproj", "{B1E05D83-5479-43B7-B25C-D8243FE6B89F}" @@ -49,7 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Drawing.Common", ".. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\ref\System.Runtime.CompilerServices.Unsafe.csproj", "{78F4F9EE-7E1D-41B5-B55A-850C1282EA99}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{04BA3E3C-6979-4792-B19E-C797AD607F42}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Runtime.CompilerServices.Unsafe", "..\System.Runtime.CompilerServices.Unsafe\src\System.Runtime.CompilerServices.Unsafe.ilproj", "{5B866430-6F0B-49F1-8294-7B07F766797A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Security.AccessControl", "..\System.Security.AccessControl\ref\System.Security.AccessControl.csproj", "{04F0A4D7-E743-4EFF-9FBD-604309FD075A}" EndProject @@ -78,6 +74,42 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6236136E-39F1-41AB-BB7A-0A6169D77730}" EndProject Global + GlobalSection(NestedProjects) = preSolution + {8AF3BFFC-9727-4E21-8E91-11501C9F97E4} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} + {B05F7183-70C1-4C10-9FFE-66301E0B076B} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} + {B1E05D83-5479-43B7-B25C-D8243FE6B89F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {81370E0B-BC7E-4F49-BCF1-49E0F67F75F4} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {CA8D3F2F-410D-4E32-B104-12929767D1A8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {A9B02E45-3372-48F4-8761-46F916400B6E} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {5BF7DAFC-8D2A-43A2-82F9-4BD0E3A62126} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {EEA5C768-9A81-4BDC-A9DD-30A2591D63FE} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {600CBFCA-5F97-47EE-8AE9-B6262E1A764B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {981358A2-F5ED-45CE-B037-446BB0F4E859} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {4039B868-612D-420F-BC25-481660475CA8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {1B0E1204-F3BF-4410-BAA1-256DC40EE47B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {1AE88632-F602-4B2F-A269-A7631A361FA7} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {78F4F9EE-7E1D-41B5-B55A-850C1282EA99} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {04F0A4D7-E743-4EFF-9FBD-604309FD075A} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {A3C9F01C-6D4D-413B-BADE-A8B9046F985F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {00C86D9C-1A45-49C7-91E6-24BBBF8950CB} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {74732589-C551-4DEF-B56C-B489400D9951} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {00DA7CF9-86B4-4991-B760-CB3AAB31EED0} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} + {9C73A2E3-B370-4B24-ACB0-0C3A9069250D} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {21E44CA2-5355-4092-9EF7-A94520EBDD40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {EFC0C2A3-1F51-4299-BE43-78284F6AC670} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {5CB1D123-853E-4FE7-9484-AAAD7330897C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {1999A9E7-2C0F-4C1D-8656-6FA5B0F32469} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {564B50A2-16A3-4AE9-ABE0-F1582F3E97C6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {03A8EBF2-F912-480F-99E1-34A9A33DD525} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {6824AD93-4154-4710-A018-81DA1FA98C40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {26C61EB7-2798-4314-B750-8CD2837D4216} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {5B866430-6F0B-49F1-8294-7B07F766797A} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {A383D45E-58AC-4FC2-AB1E-0BF8666C8623} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {8CBDDA63-8388-42AF-934E-7C60832A9B1C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {B25AD126-F78D-45CC-AD06-6F1E03D570DA} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {D80C2723-C720-4CDF-8DCF-0338799E9121} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + {C77305BE-823E-487F-825E-9C26E0674CC6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -175,10 +207,10 @@ Global {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Debug|Any CPU.Build.0 = Debug|Any CPU {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Release|Any CPU.ActiveCfg = Release|Any CPU {78F4F9EE-7E1D-41B5-B55A-850C1282EA99}.Release|Any CPU.Build.0 = Release|Any CPU - {04BA3E3C-6979-4792-B19E-C797AD607F42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04BA3E3C-6979-4792-B19E-C797AD607F42}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04BA3E3C-6979-4792-B19E-C797AD607F42}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04BA3E3C-6979-4792-B19E-C797AD607F42}.Release|Any CPU.Build.0 = Release|Any CPU + {5B866430-6F0B-49F1-8294-7B07F766797A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B866430-6F0B-49F1-8294-7B07F766797A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B866430-6F0B-49F1-8294-7B07F766797A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B866430-6F0B-49F1-8294-7B07F766797A}.Release|Any CPU.Build.0 = Release|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Debug|Any CPU.Build.0 = Debug|Any CPU {04F0A4D7-E743-4EFF-9FBD-604309FD075A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -223,42 +255,6 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {8AF3BFFC-9727-4E21-8E91-11501C9F97E4} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} - {B1E05D83-5479-43B7-B25C-D8243FE6B89F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {9C73A2E3-B370-4B24-ACB0-0C3A9069250D} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {81370E0B-BC7E-4F49-BCF1-49E0F67F75F4} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {21E44CA2-5355-4092-9EF7-A94520EBDD40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {CA8D3F2F-410D-4E32-B104-12929767D1A8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {EFC0C2A3-1F51-4299-BE43-78284F6AC670} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {A9B02E45-3372-48F4-8761-46F916400B6E} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {5CB1D123-853E-4FE7-9484-AAAD7330897C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {B05F7183-70C1-4C10-9FFE-66301E0B076B} = {3732BEA1-D7F7-4F49-A0F8-2317280E104A} - {5BF7DAFC-8D2A-43A2-82F9-4BD0E3A62126} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {1999A9E7-2C0F-4C1D-8656-6FA5B0F32469} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {EEA5C768-9A81-4BDC-A9DD-30A2591D63FE} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {564B50A2-16A3-4AE9-ABE0-F1582F3E97C6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {600CBFCA-5F97-47EE-8AE9-B6262E1A764B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {03A8EBF2-F912-480F-99E1-34A9A33DD525} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {981358A2-F5ED-45CE-B037-446BB0F4E859} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {6824AD93-4154-4710-A018-81DA1FA98C40} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {4039B868-612D-420F-BC25-481660475CA8} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {26C61EB7-2798-4314-B750-8CD2837D4216} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {1B0E1204-F3BF-4410-BAA1-256DC40EE47B} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {1AE88632-F602-4B2F-A269-A7631A361FA7} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {78F4F9EE-7E1D-41B5-B55A-850C1282EA99} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {04BA3E3C-6979-4792-B19E-C797AD607F42} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {04F0A4D7-E743-4EFF-9FBD-604309FD075A} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {A383D45E-58AC-4FC2-AB1E-0BF8666C8623} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {A3C9F01C-6D4D-413B-BADE-A8B9046F985F} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {8CBDDA63-8388-42AF-934E-7C60832A9B1C} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {00C86D9C-1A45-49C7-91E6-24BBBF8950CB} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {B25AD126-F78D-45CC-AD06-6F1E03D570DA} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {74732589-C551-4DEF-B56C-B489400D9951} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {D80C2723-C720-4CDF-8DCF-0338799E9121} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - {00DA7CF9-86B4-4991-B760-CB3AAB31EED0} = {9B998EDD-24C1-4B8E-87F3-6148ED458015} - {C77305BE-823E-487F-825E-9C26E0674CC6} = {6236136E-39F1-41AB-BB7A-0A6169D77730} - EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {830494DE-07B3-4C63-9D74-4A123677D469} EndGlobalSection From 2c2f1b33f5c83f7d6494479f3814f17db8473986 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Wed, 10 Mar 2021 12:33:57 +0100 Subject: [PATCH 17/18] Apply suggestion from feedback: simplify children list initialization --- .../src/XmlStreamConfigurationProvider.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 4428ce37340af5..1c854a40ad187d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -67,14 +67,12 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt if (parent.ChildrenBySiblingName != null) { // check if this element has appeared before, elements are considered siblings if their SiblingName properties match - if (parent.ChildrenBySiblingName.TryGetValue(element.SiblingName, out var siblings)) + if (!parent.ChildrenBySiblingName.TryGetValue(element.SiblingName, out var siblings)) { - siblings.Add(element); - } - else - { - parent.ChildrenBySiblingName.Add(element.SiblingName, new List { element }); + siblings = new List(); + parent.ChildrenBySiblingName.Add(element.SiblingName, siblings); } + siblings.Add(element); } else { From 22c02e81f8e4eb9fed266abefe7f22a09e751a57 Mon Sep 17 00:00:00 2001 From: Alexander Moerman Date: Wed, 10 Mar 2021 12:37:01 +0100 Subject: [PATCH 18/18] Rename ProcessAttributes -> ReadAttributes when parsing the XML This avoids confusion with the ProcessXYZ helper methods that run during a different stage in the algorithm --- .../src/XmlStreamConfigurationProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs index 1c854a40ad187d..bfcd4adae3894d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs @@ -110,7 +110,7 @@ public static IDictionary Read(Stream stream, XmlDocumentDecrypt currentPath.Push(element); - ProcessAttributes(reader, element); + ReadAttributes(reader, element); // If current element is self-closing if (reader.IsEmptyElement) @@ -189,7 +189,7 @@ private static string GetLineInfo(XmlReader reader) SR.Format(SR.Msg_LineInfo, lineInfo.LineNumber, lineInfo.LinePosition); } - private static void ProcessAttributes(XmlReader reader, XmlConfigurationElement element) + private static void ReadAttributes(XmlReader reader, XmlConfigurationElement element) { if (reader.AttributeCount > 0) {