Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ public ChainedConfigurationProvider(ChainedConfigurationSource source)
/// </summary>
/// <param name="key">The key.</param>
/// <param name="value">When this method returns, contains the value.</param>
/// <returns><see langword="true"/> if a value for the specified key was found, otherwise <see langword="false"/>.</returns>
/// <returns><see langword="true"/> if the chained configuration has a non-<see langword="null"/> value for the specified key, otherwise <see langword="false"/>.</returns>
public bool TryGet(string key, out string? value)
{
value = _config[key];
return !string.IsNullOrEmpty(value);
return value is not null;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// 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.Globalization;
using System.Linq;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.Primitives;
using Xunit;

namespace Microsoft.Extensions.Configuration.Test
Expand Down Expand Up @@ -153,6 +155,253 @@ public void ChainedConfiguration_ReloadingOuterConfigurationRoot_RaisesSingleOut
Assert.Equal(0, innerNotifications);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_OverConfigurationRoot_TryGetFindsKeyWithNonNullValue(string value)
{
IConfigurationProvider provider = BuildChainedProvider(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build());

Assert.True(provider.TryGet("Key", out string? actual));
Assert.Equal(value, actual);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_OverConfigurationSection_TryGetFindsKeyWithNonNullValue(string value)
{
IConfigurationProvider provider = BuildChainedProvider(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Section:Key", value } })
.Build()
.GetSection("Section"));

Assert.True(provider.TryGet("Key", out string? actual));
Assert.Equal(value, actual);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_OverConfigurationManager_TryGetFindsKeyWithNonNullValue(string value)
{
using var inner = new ConfigurationManager();
inner.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } });

IConfigurationProvider provider = BuildChainedProvider(inner);

Assert.True(provider.TryGet("Key", out string? actual));
Assert.Equal(value, actual);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_ShadowsPrecedingProvider(string value)
{
var inner = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build();

var outer = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddConfiguration(inner)
.Build();

Assert.Equal(value, outer["Key"]);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_OverConfigurationSection_ShadowsPrecedingProvider(string value)
{
var inner = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Section:Key", value } })
.Build();

var outer = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddConfiguration(inner.GetSection("Section"))
.Build();

Assert.Equal(value, outer["Key"]);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_BindsSameValueAsAnEquivalentDirectlyAddedSource(string value)
{
var direct = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build();

var chained = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build())
.Build();

var directOptions = new OptionsWithPresetValue();
var chainedOptions = new OptionsWithPresetValue();

#pragma warning disable IL2026, IL3050 // https://github.com/dotnet/runtime/issues/126862
direct.Bind(directOptions);
chained.Bind(chainedOptions);
#pragma warning restore IL2026, IL3050

Assert.Equal(value, directOptions.Key);
Assert.Equal(value, chainedOptions.Key);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_TryGetAgreesWithGetChildKeys(string value)
{
IConfigurationProvider provider = BuildChainedProvider(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build());

// GetChildKeys enumerates the wrapped configuration, which lists a key whatever its value. TryGet has to
// agree, otherwise the provider announces a key that it then refuses to return.
Assert.Contains("Key", provider.GetChildKeys(Array.Empty<string>(), parentPath: null));
Assert.True(provider.TryGet("Key", out _));
}

public static TheoryData<Func<IConfigurationRoot, IConfiguration>> ChainedConfigurationKinds => new()
{
root => root,
root => root.GetSection("Section"),
root => new PlainConfiguration(root),
};

[Theory]
[MemberData(nameof(ChainedConfigurationKinds))]
public void ChainedConfiguration_NullValueIsNotContributed(Func<IConfigurationRoot, IConfiguration> selectConfiguration)
{
var inner = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { { "Key", null }, { "Section:Key", null } })
.Build();

var outer = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddConfiguration(selectConfiguration(inner))
.Build();

// A chained configuration is a merged unit, and a unit reports the absence of a value as null. There is
// nothing to contribute, so the preceding provider still wins.
Assert.Equal("earlier-value", outer["Key"]);
}

[Theory]
[MemberData(nameof(ChainedConfigurationKinds))]
public void ChainedConfiguration_TryGetReturnsFalseForMissingKey(Func<IConfigurationRoot, IConfiguration> selectConfiguration)
{
var inner = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Section:Key", "inner-value" } })
.Build();

IConfigurationProvider provider = BuildChainedProvider(selectConfiguration(inner));

Assert.False(provider.TryGet("MissingKey", out string? value));
Assert.Null(value);
}

[Theory]
[InlineData("")]
[InlineData("inner-value")]
public void ChainedConfiguration_SectionWithNonNullValueExists(string value)
{
var outer = new ConfigurationBuilder()
.AddConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", value } })
.Build())
.Build();

Assert.True(outer.GetSection("Key").Exists());
Assert.Equal(value, outer.GetRequiredSection("Key").Value);
}

[Fact]
public void ChainedConfiguration_EmptyValueShadowingATypedValue_FailsToBindLikeADirectlyAddedSource()
{
static IConfigurationBuilder AddEarlierProvider(IConfigurationBuilder builder)
=> builder.AddInMemoryCollection(new Dictionary<string, string> { { "Port", "9000" } });

var direct = AddEarlierProvider(new ConfigurationBuilder())
.AddInMemoryCollection(new Dictionary<string, string> { { "Port", "" } })
.Build();

var chained = AddEarlierProvider(new ConfigurationBuilder())
.AddConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Port", "" } })
.Build())
.Build();

// An empty value shadows the earlier provider, so binding it to a non-string type fails. This matches
// what an equivalent directly added source has always done.
#pragma warning disable IL2026, IL3050 // https://github.com/dotnet/runtime/issues/126862
Assert.Throws<InvalidOperationException>(() => direct.Bind(new TypedOptions()));
Assert.Throws<InvalidOperationException>(() => chained.Bind(new TypedOptions()));
#pragma warning restore IL2026, IL3050
}

[Fact]
public void ChainedConfiguration_MissingKeyDoesNotShadowPrecedingProvider()
{
var outer = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "Key", "earlier-value" } })
.AddConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { { "OtherKey", "inner-value" } })
.Build())
.Build();

Assert.Equal("earlier-value", outer["Key"]);
}

private static IConfigurationProvider BuildChainedProvider(IConfiguration configuration)
=> new ChainedConfigurationSource
{
Configuration = configuration,
ShouldDisposeConfiguration = false,
}
.Build(new ConfigurationBuilder());

private class OptionsWithPresetValue
{
public string Key { get; set; } = "preset-value";
}

private class TypedOptions
{
public int Port { get; set; }
}

private class PlainConfiguration : IConfiguration
{
private readonly IConfiguration _inner;

public PlainConfiguration(IConfiguration inner) => _inner = inner;

public string? this[string key]
{
get => _inner[key];
set => _inner[key] = value;
}

public IEnumerable<IConfigurationSection> GetChildren() => _inner.GetChildren();

public IChangeToken GetReloadToken() => _inner.GetReloadToken();

public IConfigurationSection GetSection(string key) => _inner.GetSection(key);
}

private class TestConfigurationProvider : ConfigurationProvider
{
public TestConfigurationProvider(string key, string value)
Expand Down
Loading