Treat empty string value from chained configuration as present - #131480
Conversation
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Updates ChainedConfigurationProvider.TryGet so that an empty string ("") from the chained configuration is treated as a present value (only null remains “not found”). This brings chained configuration behavior in line with the provider model where “found” is independent of “non-empty”.
Changes:
- Change
ChainedConfigurationProvider.TryGetto returntruewhen the underlying configuration indexer returns a non-nullvalue (including empty string). - Add tests covering empty-string shadowing behavior and parity with directly-added in-memory sources, plus a test pinning existing
nullbehavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.Configuration/src/ChainedConfigurationProvider.cs | Adjusts TryGet presence semantics from “non-empty” to “non-null” and updates its XML doc. |
| src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs | Adds coverage for empty-string presence/shadowing and null non-contribution scenarios. |
Comments suppressed due to low confidence (2)
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:313
TryGetmay assignnullto theoutparameter on failure; usingout string valuehere can trigger nullability warnings. Useout string?to match the contract.
Assert.False(provider.TryGet("MissingKey", out string value));
Assert.Null(value);
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:396
IConfiguration.this[string]is nullable (string?). Implementing it as non-nullablestringcan cause nullability mismatch warnings (and makes the wrapper incorrect for missing keys). Make the indexerstring?to match the interface contract.
public string this[string key]
{
get => _inner[key];
set => _inner[key] = value;
}
|
Added When you commit this breaking change:
Tagging @dotnet/compat for awareness of the breaking change. |
|
@rosebyte I have marked this PR with the breaking change label. It is better to have breaking change doc for that and can be linked to the .NET 10 breaking change doc. |
|
One thing worth deciding before merge: the PR says Since the core scenario in #65594 (respecting
Either way is fine, just want to make sure the null limitation stays tracked rather than being closed out silently. |
|
Building on the
Suggested implementation: public bool TryGet(string key, out string? value) => _config switch
{
IConfigurationRoot root => root.TryGetConfiguration(key, out value),
ConfigurationSection section => section.TryGetValue(key, out value),
_ => (value = _config[key]) is not null
};
Points to address if we take this:
|
|
On reading the wrapped configuration's providers: Eric suggested the same in the issue, but I do not think we should take it. There is a problem with the layering. A provider bids. I have further concerns about bypassing the
So we can keep the PR as merely related to the issue, for clarity, but once it is merged I do not see anything else actionable there, and I would propose closing it: what it otherwise suggests would likely make the configuration extensions more inconsistent rather than less. |
|
Last but not least, the indexer of the ConfigurationManager does not merely sweep, it sweeps a list it has pinned. That pin is what keeps a read correct while // We cannot track the duration of the reference to the providers if this property is used.
// If a configuration source is removed after this is accessed but before it's completely enumerated,
// this may allow access to a disposed provider.
IEnumerable<IConfigurationProvider> IConfigurationRoot.Providers => _providerManager.NonReferenceCountedProviders;and copes by swallowing |
|
Thanks for the detailed analysis. Your disposal point is correct and I verified it against the code: I think there is a cleaner path that addresses the null case without the problems you raised, and it is the one the framework code already hints at. In
I think this also answers your three concerns, because the behavior stops being a wrapper reverse-engineering The reason I would not fold this into the current PR: it is a separate observable breaking change of the same class as the empty-string one (a wrapped present-null would begin to shadow lower providers with null), and it needs its own breaking-change doc and notification. It is also a larger change touching the in-box configuration types and likely an API-review discussion. Given that, does it make sense to land the empty-string fix here for .NET 11 and keep #65594 open, then do the null work as its own change in .NET 12? That keeps this PR small and low risk, avoids a second break in the same release, and gives the abstraction-level change room for proper review. If you agree, I would keep this PR as "related to" #65594 rather than closing the issue, so the null case stays tracked. |
|
I don't see how ConfigurationSection could help us here since IConfiguration returns IConfigurationSection and IConfigurationSection doesn't have TryGetValue method. Anyway, let's merge this PR as we all agree on it and it aligns with the changes done in net10.0 without compromises, and continue this discussion in the issue, what do you think? |
|
You are right that What I have in mind does not touch the interface:
Either way it is a separate breaking change and a design discussion, so I fully agree: let us merge this PR as is since we all agree it matches the .NET 10 empty/null direction, and continue the null case in #65594. I will keep the PR as "related to" the issue rather than closing it so the null work stays tracked, and we can take up the approach there for .NET 12. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:167
- Nullability mismatch: IConfigurationProvider.TryGet has an
out string?parameter, but this test passesout string. With nullable warnings enabled (and treated as errors in this repo), this can fail the build. Useout string?instead.
Assert.True(provider.TryGet("Key", out string actual));
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:181
- Nullability mismatch: IConfigurationProvider.TryGet has an
out string?parameter, but this test passesout string. Switch the out variable tostring?to avoid nullable warnings-as-errors.
Assert.True(provider.TryGet("Key", out string actual));
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:195
- Nullability mismatch: IConfigurationProvider.TryGet has an
out string?parameter, but this test passesout string. Useout string?to keep the test build clean under nullable warnings.
Assert.True(provider.TryGet("Key", out string actual));
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:314
- Nullability mismatch: TryGet's out parameter is
string?, and this test expects it to be null when the key is missing. Declare the out variable asstring?so the call andAssert.Nulldon't produce nullable warnings (which are typically treated as errors).
Assert.False(provider.TryGet("MissingKey", out string value));
Assert.Null(value);
}
src/libraries/Microsoft.Extensions.Configuration/tests/ChainedConfigurationProviderTests.cs:396
- PlainConfiguration implements IConfiguration, whose indexer returns
string?. This implementation declares a non-nullablestringindexer, which causes nullability-mismatch warnings (and can fail the build if warnings are treated as errors). Match the interface nullability (string?).
public string this[string key]
{
get => _inner[key];
set => _inner[key] = value;
}
|
Created dotnet/docs#55653 to track the compatibility article and linked it to this PR. The @rosebyte, please email dotnet/docs#55653 to the .NET Breaking Change Notifications alias ( Note This comment was drafted with AI assistance from GitHub Copilot. |
Related to #65594
Summary
ChainedConfigurationProvider.TryGetused!string.IsNullOrEmpty(value), so a key present in the chained configuration with an empty value was reported as not found. It therefore failed to shadow values from providers registered earlier in the outer builder, and a chained configuration disagreed with a plainConfigurationBuildergiven the same data.Continuation of the .NET 10 work
.NET 10 converged the interpretations of
""andnullin #116677 ("Support Null configuration", fixing #116700 and #36510). Before that pass the two were used more or less interchangeably to mean "nothing here". That PR separated them:nullrather than converting it to"";nulland onto"";nullas a value to bind rather than a value to skip;ConfigurationSection.TryGetValuewas added so callers can tell an absent key from an explicit null.The rule that fell out of it is that an empty string is a value and
nullis an absence.ChainedConfigurationProviderwas not part of that pass and still conflates the two. This PR applies the same rule to it.Null is deliberately left alone
The null rows still disagree, and this PR does not change that.
The wrapped
IConfigurationexposes only an indexer, which cannot tell a null value apart from a missing key. Closing the gap would mean downcasting to the in-box configuration types and walking their providers, which turns an implementation detail into a contract: it would commitConfigurationRootandConfigurationManagerto never growing lookup logic of their own. That is too high a price for the remaining sliver.There is also a reading on which the current behaviour is right. A chained
IConfigurationis a merged unit rather than a single provider, and a unit reports the absence of a value as null, so "null means nothing to contribute" is defensible on its own terms.ChainedConfiguration_NullValueIsNotContributedpins it so the limit reads as deliberate rather than as an oversight.Breaking change
This is a behavioural breaking change, and it is the counterpart to the .NET 10 one documented as Null values preserved in configuration (dotnet/docs#46890). That change stopped providers treating null as missing; this one stops chained configuration treating empty as missing.
Requesting the
breaking-changelabel, and a docs issue to sit alongside the .NET 10 article.