runtime icon indicating copy to clipboard operation
runtime copied to clipboard

XML attribute is read as a null IConfigurationSection.Value starting in 6.0.0

Open emz00 opened this issue 3 years ago • 4 comments

Description

I have an XML configuration file like this:

<configuration>
    <Level1>
        <Level2 Key1="Value1" />
        <Level2 Key2="Value2" />
    </Level1>
</configuration>

that I read using Microsoft.Extensions.Configuration.IConfigurationSection:

foreach (var configItem in configRoot.GetSection("Level1:Level2").GetChildren())
    Console.WriteLine("Path={0}, key={1}, value={2}", configItem.Path, configItem.Key, configItem.Value ?? "<null>");

Up to v5.0.0 it returned the expected keys and values, but from v6.0.0 returns "0" and "1" for keys and nulls for values.

Reproduction Steps

Run WeirdXmlConfigIssue.zip

Expected behavior

M.E.Configuration version 6.0.0.0
Path=Level1:Level2:Key1, key=Key1, value=Value1
Path=Level1:Level2:Key2, key=Key2, value=Value2

Actual behavior

M.E.Configuration version 6.0.0.0
Path=Level1:Level2:0, key=0, value=<null>
Path=Level1:Level2:1, key=1, value=<null>

Regression?

Yes. If you edit the project file to reference version 5.0.0 of Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Xml the output is

M.E.Configuration version 5.0.0.0
Path=Level1:Level2:Key1, key=Key1, value=Value1
Path=Level1:Level2:Key2, key=Key2, value=Value2

Known Workarounds

Don't use XML attributes at all. This works:

<configuration>
    <Level1>
        <Level2>
            <Key1>Value1</Key1>
            <Key2>Value2</Key2>
        </Level2>
    </Level1>
</configuration>

But it requires repeating each key, which is annoying when they're long.

A mix of the two does not work, however. This:

<configuration>
    <Level1>
        <Level2>
            <Key1>Value1</Key1>
            <Key2>Value2</Key2>
        </Level2>
        <Level2 Key3="Value3" />
    </Level1>
</configuration>

still outputs

M.E.Configuration version 6.0.0.0
Path=Level1:Level2:0, key=0, value=<null>
Path=Level1:Level2:1, key=1, value=<null>

Configuration

.NET 6.0.5, Windows 7 x64

Other information

https://docs.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#xml-configuration-provider mentions

In .NET 5 and earlier versions, add the name attribute to distinguish repeating elements that use the same element name. In .NET 6 and later versions, the XML configuration provider automatically indexes repeating elements. That means you don't have to specify the name attribute, except if you want the "0" index in the key and there's only one element.

I suspect this is related to that, but it's certainly not obvious from the docs that this would happen (the value would become null). If this is intentional it's a breaking change (not listed on https://docs.microsoft.com/en-us/dotnet/core/compatibility/6.0 as far as I can see), and a pretty bad one.

It works when there is only one item in the list:

<configuration>
    <Level1>
        <Level2 OnlyKey="OnlyValue" />
    </Level1>
</configuration>

outputs

M.E.Configuration version 6.0.0.0
Path=Level1:Level2:OnlyKey, key=OnlyKey, value=OnlyValue

as expected.

emz00 avatar Jun 14 '22 15:06 emz00

Tagging subscribers to this area: @dotnet/area-extensions-configuration See info in area-owners.md if you want to be subscribed.

Issue Details

Description

I have a configuration file like this:

<configuration>
    <Level1>
        <Level2 Key1="Value1" />
        <Level2 Key2="Value2" />
    </Level1>
</configuration>

that I read using Microsoft.Extensions.Configuration.IConfigurationSection:

foreach (var configItem in configRoot.GetSection("Level1:Level2").GetChildren())
    Console.WriteLine("Path={0}, key={1}, value={2}", configItem.Path, configItem.Key, configItem.Value ?? "<null>");

Up to v5.0.0 it returned the expected keys and values, but from v6.0.0 returns "0" and "1" for keys and nulls for values.

Reproduction Steps

Run WeirdXmlConfigIssue.zip

Expected behavior

M.E.Configuration version 6.0.0.0 Path=Level1:Level2:Key1, key=Key1, value=Value1 Path=Level1:Level2:Key2, key=Key2, value=Value2

Actual behavior

M.E.Configuration version 6.0.0.0 Path=Level1:Level2:0, key=0, value= Path=Level1:Level2:1, key=1, value=

Regression?

Yes. If you edit the project file to reference version 5.0.0 of Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Xml the output is

M.E.Configuration version 5.0.0.0 Path=Level1:Level2:Key1, key=Key1, value=Value1 Path=Level1:Level2:Key2, key=Key2, value=Value2

Known Workarounds

Don't use XML attributes at all. This works:

<configuration>
    <Level1>
        <Level2>
            <Key1>Value1</Key1>
            <Key2>Value2</Key2>
        </Level2>
    </Level1>
</configuration>

But it requires repeating each key, which is annoying when they're long.

A mix of the two does not work, however. This:

<configuration>
    <Level1>
        <Level2>
            <Key1>Value1</Key1>
            <Key2>Value2</Key2>
        </Level2>
        <Level2 Key3="Value3" />
    </Level1>
</configuration>

still outputs

M.E.Configuration version 6.0.0.0 Path=Level1:Level2:0, key=0, value= Path=Level1:Level2:1, key=1, value=

Configuration

.NET 6.0.5, Windows 7 x64

Other information

https://docs.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#xml-configuration-provider mentions

In .NET 5 and earlier versions, add the name attribute to distinguish repeating elements that use the same element name. In .NET 6 and later versions, the XML configuration provider automatically indexes repeating elements. That means you don't have to specify the name attribute, except if you want the "0" index in the key and there's only one element.

I suspect this is related to that, but it's certainly not obvious from the docs that this would happen (the value would become null). If this is intentional it's a breaking change (not listed on https://docs.microsoft.com/en-us/dotnet/core/compatibility/6.0 as far as I can see), and a pretty bad one.

Author: loop-evgeny
Assignees: -
Labels:

area-Extensions-Configuration

Milestone: -

msftbot[bot] avatar Jun 14 '22 15:06 msftbot[bot]

Any chance of this being fixed in 6.0.x?

emz00 avatar Jun 16 '22 20:06 emz00

Since this is a regression from 5.0, we should backport this to 6.0.x.

eerhardt avatar Jun 21 '22 21:06 eerhardt

Any estimate on when this might get fixed? We want to upgrade to .NET 6 and need to decide whether to work around it by transforming all XML attributes to elements or just wait for the fix.

emz00 avatar Jul 29 '22 07:07 emz00

This looks like a breaking change due to https://github.com/dotnet/runtime/pull/44608. The issue is that the repeated elements:

    <Level1>
        <Level2 Key1="Value1" />
        <Level2 Key2="Value2" />
    </Level1>

are not "named", and so now they are getting an "index" injected into them. If you look at the configItem in var configItem in configRoot.GetSection("Level1:Level2").GetChildren(), you'll see that its path is Level1:Level2:0 and not Level1:Level2:Key1 as it was before #44608. This is due to the 0 and 1 indices getting injected into the path. The full paths of the configuration:

Before Item1 Level1:Level2:Key1:Value1
Before Item2 Level1:Level2:Key2:Value2
After Item1 Level1:Level2:0:Key1:Value1
After Item2 Level1:Level2:1:Key2:Value2

This was a breaking change that should have been documented in 6.0, but it doesn't appear that was the case. From the PR description:

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.

cc @amoerie - in case there is anything I am missing here.

I believe this is "by design" and we just need to add a breaking change documentation for it. See https://github.com/dotnet/runtime/pull/44608 for more information on why this was done.

eerhardt avatar Aug 10 '22 00:08 eerhardt

It looks like this is a use case that I was not aware of, and that was not present in the unit tests. (Because I distinctly remember not removing any in my pull request)

I think I agree with @eerhardt that this is by design, albeit accidentally. The fact that this worked under .NET 5 also seemed accidental, since it was not covered by any test.

My 2 cents on this issue: I would investigate the effort to simply update the XML files. If that is too much trouble, nothing stops you from using a custom ConfigurationProvider, which is what I did before .NET 6 was released.

amoerie avatar Aug 10 '22 08:08 amoerie

Thinking about it more:

    <Level1>
        <Level2 Key1="Value1" />
        <Level2 Key2="Value2" />
    </Level1>

Is kind of odd before #44608. You need to ensure you use different attribute values in the Level2 elements. If both elements had the same attribute, you'd get an exception:

    <Level1>
        <Level2 Key1="Value1" />
        <Level2 Key1="Value2" Key2="Value2" />
    </Level1>

You can update your XML to collapse the 2 attributes into the same element, and get the original behavior:

    <Level1>
        <Level2 Key1="Value1" Key2="Value2" />
    </Level1>

@tarekgh @maryamariyan - do you think this deserves a breaking change doc? Or is this enough of an edge case that it doesn't justify documenting this change separately from this issue?

eerhardt avatar Aug 10 '22 15:08 eerhardt

It will be good if we file a breaking change issue to have it documented and can point users to such case. I marked https://github.com/dotnet/runtime/pull/44608 with the breaking change label.

tarekgh avatar Aug 10 '22 16:08 tarekgh

I have created https://github.com/dotnet/docs/issues/30660 to document this breaking change.

I'm going to close this issue since this is now by design behavior.

eerhardt avatar Aug 12 '22 18:08 eerhardt

You can update your XML to collapse the 2 attributes into the same element, and get the original behavior:

    <Level1>
        <Level2 Key1="Value1" Key2="Value2" />
    </Level1>

Thanks, this works as a workaround. It's still a bit of a gotcha, of course, and definitely needs to be documented.

I see that https://github.com/dotnet/runtime/pull/44608 was prompted by Serilog and it's actually with Serilog overrides that I ran into this issue, too.

emz00 avatar Aug 15 '22 12:08 emz00