Add support for repeated XML elements without a name attribute (#44608)
authorAlexander Moerman <alexander.moerman@gmail.com>
Wed, 10 Mar 2021 19:55:04 +0000 (20:55 +0100)
committerGitHub <noreply@github.com>
Wed, 10 Mar 2021 19:55:04 +0000 (11:55 -0800)
* 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.

Co-authored-by: Santiago Fernandez Madero <safern@microsoft.com>
src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElement.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementAttributeValue.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlConfigurationElementTextContent.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Configuration.Xml/src/XmlStreamConfigurationProvider.cs
src/libraries/Microsoft.Extensions.Configuration.Xml/tests/ConfigurationProviderXmlTest.cs
src/libraries/Microsoft.Extensions.Configuration.Xml/tests/XmlConfigurationTest.cs

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 (file)
index 0000000..26441a4
--- /dev/null
@@ -0,0 +1,41 @@
+// 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;
+
+namespace Microsoft.Extensions.Configuration.Xml
+{
+    internal class XmlConfigurationElement
+    {
+        public string ElementName { get; }
+
+        public string Name { get; }
+
+        /// <summary>
+        /// A composition of ElementName and Name, that serves as the basis for detecting siblings
+        /// </summary>
+        public string SiblingName { get; }
+
+        /// <summary>
+        /// The children of this element
+        /// </summary>
+        public IDictionary<string, List<XmlConfigurationElement>> ChildrenBySiblingName { get; set; }
+
+        /// <summary>
+        /// Performance optimization: do not initialize a dictionary and a list for elements with a single child
+        /// </summary>
+        public XmlConfigurationElement SingleChild { get; set; }
+
+        public XmlConfigurationElementTextContent TextContent { get; set; }
+
+        public List<XmlConfigurationElementAttributeValue> Attributes { get; set; }
+
+        public XmlConfigurationElement(string elementName, string name)
+        {
+            ElementName = elementName ?? throw new ArgumentNullException(nameof(elementName));
+            Name = name;
+            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
new file mode 100644 (file)
index 0000000..bdea5cd
--- /dev/null
@@ -0,0 +1,26 @@
+// 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 XmlConfigurationElementAttributeValue
+    {
+        public XmlConfigurationElementAttributeValue(string attribute, string value, int? lineNumber, int? linePosition)
+        {
+            Attribute = attribute ?? throw new ArgumentNullException(nameof(attribute));
+            Value = value ?? throw new ArgumentNullException(nameof(value));
+            LineNumber = lineNumber;
+            LinePosition = linePosition;
+        }
+
+        public string Attribute { get; }
+
+        public string Value { 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
new file mode 100644 (file)
index 0000000..76dab15
--- /dev/null
@@ -0,0 +1,23 @@
+// 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, int? linePosition, int? lineNumber)
+        {
+            TextContent = textContent ?? throw new ArgumentNullException(nameof(textContent));
+            LineNumber = lineNumber;
+            LinePosition = linePosition;
+        }
+
+        public string TextContent { get; }
+
+        public int? LineNumber { get; }
+
+        public int? LinePosition { get; }
+    }
+}
index e1c36cd..bfcd4ad 100644 (file)
@@ -3,8 +3,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
-using System.Linq;
+using System.Text;
 using System.Xml;
 
 namespace Microsoft.Extensions.Configuration.Xml
@@ -30,8 +31,6 @@ namespace Microsoft.Extensions.Configuration.Xml
         /// <returns>The <see cref="IDictionary{String, String}"/> which was read from the stream.</returns>
         public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecryptor decryptor)
         {
-            var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
             var readerSettings = new XmlReaderSettings()
             {
                 CloseInput = false, // caller will close the stream
@@ -40,61 +39,115 @@ namespace Microsoft.Extensions.Configuration.Xml
                 IgnoreWhitespace = true
             };
 
+            XmlConfigurationElement root = null;
+
             using (XmlReader reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings))
             {
-                var prefixStack = new Stack<string>();
-
-                SkipUntilRootElement(reader);
-
-                // 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<XmlConfigurationElement>();
 
                 XmlNodeType preNodeType = reader.NodeType;
+
                 while (reader.Read())
                 {
                     switch (reader.NodeType)
                     {
                         case XmlNodeType.Element:
-                            prefixStack.Push(reader.LocalName);
-                            ProcessAttributes(reader, prefixStack, data, AddNamePrefix);
-                            ProcessAttributes(reader, prefixStack, data, AddAttributePair);
+                            var element = new XmlConfigurationElement(reader.LocalName, GetName(reader));
+
+                            if (currentPath.Count == 0)
+                            {
+                                root = element;
+                            }
+                            else
+                            {
+                                var parent = currentPath.Peek();
+
+                                // If parent already has a dictionary of children, update the collection accordingly
+                                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))
+                                    {
+                                        siblings = new List<XmlConfigurationElement>();
+                                        parent.ChildrenBySiblingName.Add(element.SiblingName, siblings);
+                                    }
+                                    siblings.Add(element);
+                                }
+                                else
+                                {
+                                    // Performance optimization: parents with a single child don't even initialize a dictionary
+                                    if (parent.SingleChild == null)
+                                    {
+                                        parent.SingleChild = element;
+                                    }
+                                    else
+                                    {
+                                        // If we encounter a second child after assigning "SingleChild", we clear SingleChild and initialize the dictionary
+                                        var children = new Dictionary<string, List<XmlConfigurationElement>>(StringComparer.OrdinalIgnoreCase);
+
+                                        // Special case: the first and second child have the same sibling name
+                                        if (string.Equals(parent.SingleChild.SiblingName, element.SiblingName, StringComparison.OrdinalIgnoreCase))
+                                        {
+                                            children.Add(element.SiblingName, new List<XmlConfigurationElement>
+                                            {
+                                                parent.SingleChild,
+                                                element
+                                            });
+                                        }
+                                        else
+                                        {
+                                            children.Add(parent.SingleChild.SiblingName, new List<XmlConfigurationElement> { parent.SingleChild });
+                                            children.Add(element.SiblingName, new List<XmlConfigurationElement> { element });
+                                        }
+
+                                        parent.ChildrenBySiblingName = children;
+                                        parent.SingleChild = null;
+                                    }
+
+                                }
+                            }
+
+                            currentPath.Push(element);
+
+                            ReadAttributes(reader, element);
 
                             // If current element is self-closing
                             if (reader.IsEmptyElement)
                             {
-                                prefixStack.Pop();
+                                currentPath.Pop();
                             }
                             break;
-
                         case XmlNodeType.EndElement:
-                            if (prefixStack.Count != 0)
+                            if (currentPath.Count != 0)
                             {
+                                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)
                                 {
-                                    string key = ConfigurationPath.Combine(prefixStack.Reverse());
-                                    data[key] = string.Empty;
+                                    var lineInfo = reader as IXmlLineInfo;
+                                    var lineNumber = lineInfo?.LineNumber;
+                                    var linePosition = lineInfo?.LinePosition;
+                                    parent.TextContent = new XmlConfigurationElementTextContent(string.Empty, lineNumber, linePosition);
                                 }
-
-                                prefixStack.Pop();
                             }
                             break;
 
                         case XmlNodeType.CDATA:
                         case XmlNodeType.Text:
+                            if (currentPath.Count != 0)
                             {
-                                string key = ConfigurationPath.Combine(prefixStack.Reverse());
-                                if (data.ContainsKey(key))
-                                {
-                                    throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key,
-                                        GetLineInfo(reader)));
-                                }
+                                var lineInfo = reader as IXmlLineInfo;
+                                var lineNumber = lineInfo?.LineNumber;
+                                var linePosition = lineInfo?.LinePosition;
 
-                                data[key] = reader.Value;
-                                break;
+                                XmlConfigurationElement parent = currentPath.Peek();
+
+                                parent.TextContent = new XmlConfigurationElementTextContent(reader.Value, lineNumber, linePosition);
                             }
+                            break;
                         case XmlNodeType.XmlDeclaration:
                         case XmlNodeType.ProcessingInstruction:
                         case XmlNodeType.Comment:
@@ -103,21 +156,21 @@ namespace Microsoft.Extensions.Configuration.Xml
                             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;
                     }
                 }
             }
-            return data;
+
+            return ProvideConfiguration(root);
         }
 
         /// <summary>
@@ -129,18 +182,6 @@ namespace Microsoft.Extensions.Configuration.Xml
             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,20 +189,29 @@ namespace Microsoft.Extensions.Configuration.Xml
                 SR.Format(SR.Msg_LineInfo, lineInfo.LineNumber, lineInfo.LinePosition);
         }
 
-        private static void ProcessAttributes(XmlReader reader, Stack<string> prefixStack, IDictionary<string, string> data,
-            Action<XmlReader, Stack<string>, IDictionary<string, string>, XmlWriter> act, XmlWriter writer = null)
+        private static void ReadAttributes(XmlReader reader, XmlConfigurationElement element)
         {
+            if (reader.AttributeCount > 0)
+            {
+                element.Attributes = new List<XmlConfigurationElementAttributeValue>();
+            }
+
+            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)));
                 }
 
-                act(reader, prefixStack, data, writer);
+                element.Attributes.Add(new XmlConfigurationElementAttributeValue(reader.LocalName, reader.Value, lineNumber, linePosition));
             }
 
             // Go back to the element containing the attributes we just processed
@@ -169,41 +219,227 @@ namespace Microsoft.Extensions.Configuration.Xml
         }
 
         // 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<string> prefixStack,
-            IDictionary<string, string> 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))
+            string name = null;
+
+            while (reader.MoveToNextAttribute())
             {
-                return;
+                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;
+                }
             }
 
-            // If current element is not root element
-            if (prefixStack.Count != 0)
+            // Go back to the element containing the name we just processed
+            reader.MoveToElement();
+
+            return name;
+        }
+
+        private static IDictionary<string, string> ProvideConfiguration(XmlConfigurationElement root)
+        {
+            var configuration = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+            if (root == null)
             {
-                string lastPrefix = prefixStack.Pop();
-                prefixStack.Push(ConfigurationPath.Combine(lastPrefix, reader.Value));
+                return configuration;
             }
-            else
+
+            var rootPrefix = new Prefix();
+
+            // The root element only contributes to the prefix via its Name attribute
+            if (!string.IsNullOrEmpty(root.Name))
             {
-                prefixStack.Push(reader.Value);
+                rootPrefix.Push(root.Name);
+            }
+
+            ProcessElementAttributes(rootPrefix, root);
+            ProcessElementContent(rootPrefix, root);
+            ProcessElementChildren(rootPrefix, root);
+
+            return configuration;
+
+            void ProcessElement(Prefix prefix, XmlConfigurationElement element)
+            {
+                ProcessElementAttributes(prefix, element);
+
+                ProcessElementContent(prefix, element);
+
+                ProcessElementChildren(prefix, element);
+            }
+
+            void ProcessElementAttributes(Prefix 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.Push(attribute.Attribute);
+
+                        AddToConfiguration(prefix.AsString, attribute.Value, attribute.LineNumber, attribute.LinePosition);
+
+                        prefix.Pop();
+                    }
+                }
+            }
+
+            void ProcessElementContent(Prefix prefix, XmlConfigurationElement element)
+            {
+                // Add text content to configuration values
+                if (element.TextContent != null)
+                {
+                    var textContent = element.TextContent;
+                    AddToConfiguration(prefix.AsString, textContent.TextContent, textContent.LineNumber, textContent.LinePosition);
+                }
+            }
+
+            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
+                foreach (var childrenWithSameSiblingName in element.ChildrenBySiblingName.Values)
+                {
+                    if (childrenWithSameSiblingName.Count == 1)
+                    {
+                        var child = childrenWithSameSiblingName[0];
+
+                        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, 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
             }
         }
+    }
 
-        // 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<string> prefixStack,
-            IDictionary<string, string> data, XmlWriter writer)
+    /// <summary>
+    /// Helper class to build the configuration keys in a way that does not require string.Join
+    /// </summary>
+    internal class Prefix
+    {
+        private readonly StringBuilder _sb;
+        private readonly Stack<int> _lengths;
+
+        public Prefix()
         {
-            prefixStack.Push(reader.LocalName);
-            string key = ConfigurationPath.Combine(prefixStack.Reverse());
-            if (data.ContainsKey(key))
+            _sb = new StringBuilder();
+            _lengths = new Stack<int>();
+        }
+
+        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
             {
-                throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key, GetLineInfo(reader)));
+                _sb.Append(value);
+                _lengths.Push(value.Length);
             }
+        }
+
+        public void Pop()
+        {
+            var length = _lengths.Pop();
 
-            data[key] = reader.Value;
-            prefixStack.Pop();
+            _sb.Remove(_sb.Length - length, length);
         }
     }
 }
index 270a20f..da6d968 100644 (file)
@@ -20,6 +20,16 @@ namespace Microsoft.Extensions.Configuration.Xml.Test
             // Disabled test due to XML handling of empty section.
         }
 
+        public override void Load_from_single_provider_with_duplicates_throws()
+        {
+            // Disabled test due to XML handling of duplicate keys section.
+        }
+
+        public override void Load_from_single_provider_with_differing_case_duplicates_throws()
+        {
+            // Disabled test due to XML handling of duplicate keys section.
+        }
+
         public override void Has_debug_view()
         {
             var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig));
@@ -33,11 +43,8 @@ Section1:
     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})
@@ -75,7 +82,7 @@ Section3:
                 {
                     for (var i = 0; i < tuple.Value.AsArray.Length; i++)
                     {
-                        xmlBuilder.AppendLine($"<{tuple.Key} Name=\"{i}\">{tuple.Value.AsArray[i]}</{tuple.Key}>");
+                        xmlBuilder.AppendLine($"<{tuple.Key}>{tuple.Value.AsArray[i]}</{tuple.Key}>");
                     }
                 }
             }
index b9c59d9..4d26562 100644 (file)
@@ -169,6 +169,32 @@ namespace Microsoft.Extensions.Configuration.Xml.Test
         }
 
         [Fact]
+        public void LowercaseNameAttributeContributesToPrefix()
+        {
+            var xml =
+                @"<settings>
+                    <Data name='DefaultConnection'>
+                        <ConnectionString>TestConnectionString</ConnectionString>
+                        <Provider>SqlClient</Provider>
+                    </Data>
+                    <Data name='Inventory'>
+                        <ConnectionString>AnotherTestConnectionString</ConnectionString>
+                        <Provider>MySql</Provider>
+                    </Data>
+                </settings>";
+            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()
         {
             var xml =
@@ -194,6 +220,205 @@ namespace Microsoft.Extensions.Configuration.Xml.Test
         }
 
         [Fact]
+        public void NameAttributeCanBeUsedToSimulateArrays()
+        {
+            var xml =
+              @"<settings>
+                  <DefaultConnection Name='0'>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <DefaultConnection Name='1'>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </DefaultConnection>
+                </settings>";
+            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()
+        {
+            var xml =
+              @"<settings>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </DefaultConnection>
+                </settings>";
+            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 RepeatedElementDetectionIsCaseInsensitive()
+        {
+            var xml =
+              @"<settings>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <defaultconnection>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </defaultconnection>
+                </settings>";
+            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 =
+              @"<settings Name='Data'>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </DefaultConnection>
+                </settings>";
+            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 =
+              @"<settings>
+                  <DefaultConnection Name='Data'>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <DefaultConnection Name='Data'>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </DefaultConnection>
+                </settings>";
+            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 =
+              @"<settings>
+                  <DefaultConnection Name='Data1'>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <Provider>SqlClient1</Provider>
+                  </DefaultConnection>
+                  <DefaultConnection Name='Data2'>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                      <Provider>SqlClient2</Provider>
+                  </DefaultConnection>
+                </settings>";
+            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 =
+              @"<settings>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString1</ConnectionString>
+                      <ConnectionString>TestConnectionString2</ConnectionString>
+                  </DefaultConnection>
+                  <DefaultConnection>
+                      <ConnectionString>TestConnectionString3</ConnectionString>
+                      <ConnectionString>TestConnectionString4</ConnectionString>
+                  </DefaultConnection>
+                </settings>";
+            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 SupportMixingRepeatedElementsWithNonRepeatedElements()
+        {
+            var xml =
+              @"<settings>
+                    <DefaultConnection>
+                        <ConnectionString>TestConnectionString1</ConnectionString>
+                        <Provider>SqlClient1</Provider>
+                    </DefaultConnection>
+                    <DefaultConnection>
+                        <ConnectionString>TestConnectionString2</ConnectionString>
+                        <Provider>SqlClient2</Provider>
+                    </DefaultConnection>
+                    <OtherValue>
+                        <Value>MyValue</Value>
+                    </OtherValue>
+                    <DefaultConnection>
+                        <ConnectionString>TestConnectionString3</ConnectionString>
+                        <Provider>SqlClient3</Provider>
+                    </DefaultConnection>
+                </settings>";
+            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()
         {
             var xml =
@@ -218,6 +443,24 @@ namespace Microsoft.Extensions.Configuration.Xml.Test
         }
 
         [Fact]
+        public void KeysAreCaseInsensitive()
+        {
+            var xml =
+                @"<settings>
+                    <Data Name='DefaultConnection'
+                          ConnectionString='TestConnectionString'
+                          Provider='SqlClient' />
+                </settings>";
+            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()
         {
             var xml =
@@ -422,6 +665,30 @@ namespace Microsoft.Extensions.Configuration.Xml.Test
         }
 
         [Fact]
+        public void ThrowExceptionWhenKeyIsDuplicatedWithDifferentCasing()
+        {
+            var xml =
+                @"<settings>
+                    <Data>
+                        <DefaultConnection>
+                            <ConnectionString>TestConnectionString</ConnectionString>
+                            <Provider>SqlClient</Provider>
+                        </DefaultConnection>
+                    </Data>
+                    <data name='defaultconnection' connectionstring='NewConnectionString'>
+                        <provider>NewProvider</provider>
+                    </data>
+                </settings>";
+            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<FormatException>(() => xmlConfigSrc.Load(TestStreamHelpers.StringToStream(xml)));
+
+            Assert.Equal(expectedMsg, exception.Message);
+        }
+
+        [Fact]
         public void XmlConfiguration_Throws_On_Missing_Configuration_File()
         {
             var ex = Assert.Throws<FileNotFoundException>(() => new ConfigurationBuilder().AddXmlFile("NotExistingConfig.xml", optional: false).Build());