Skip to content

Commit 48685f3

Browse files
authored
Improve Microsoft.Extensions.Configuration debugging (#86624)
* Improve Microsoft.Extensions.Configuration debugging * Clean up * Clean up * Clean up + tests
1 parent 62609dc commit 48685f3

File tree

6 files changed

+332
-2
lines changed

6 files changed

+332
-2
lines changed

src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationManager.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Diagnostics.CodeAnalysis;
89
using System.Linq;
910
using System.Threading;
@@ -16,6 +17,8 @@ namespace Microsoft.Extensions.Configuration
1617
/// ConfigurationManager is a mutable configuration object. It is both an <see cref="IConfigurationBuilder"/> and an <see cref="IConfigurationRoot"/>.
1718
/// As sources are added, it updates its current view of configuration.
1819
/// </summary>
20+
[DebuggerDisplay("{DebuggerToString(),nq}")]
21+
[DebuggerTypeProxy(typeof(ConfigurationManagerDebugView))]
1922
public sealed class ConfigurationManager : IConfigurationBuilder, IConfigurationRoot, IDisposable
2023
{
2124
// Concurrently modifying config sources or properties is not thread-safe. However, it is thread-safe to read config while modifying sources or properties.
@@ -159,6 +162,24 @@ private void DisposeRegistrations()
159162
}
160163
}
161164

165+
private string DebuggerToString()
166+
{
167+
return $"Sections = {ConfigurationSectionDebugView.FromConfiguration(this, this).Count}";
168+
}
169+
170+
private sealed class ConfigurationManagerDebugView
171+
{
172+
private readonly ConfigurationManager _current;
173+
174+
public ConfigurationManagerDebugView(ConfigurationManager current)
175+
{
176+
_current = current;
177+
}
178+
179+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
180+
public ConfigurationSectionDebugView[] Items => ConfigurationSectionDebugView.FromConfiguration(_current, _current).ToArray();
181+
}
182+
162183
private sealed class ConfigurationSources : IList<IConfigurationSource>
163184
{
164185
private readonly List<IConfigurationSource> _sources = new();

src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Threading;
78
using Microsoft.Extensions.Primitives;
89

@@ -11,6 +12,8 @@ namespace Microsoft.Extensions.Configuration
1112
/// <summary>
1213
/// The root node for a configuration.
1314
/// </summary>
15+
[DebuggerDisplay("{DebuggerToString(),nq}")]
16+
[DebuggerTypeProxy(typeof(ConfigurationRootDebugView))]
1417
public class ConfigurationRoot : IConfigurationRoot, IDisposable
1518
{
1619
private readonly IList<IConfigurationProvider> _providers;
@@ -135,5 +138,23 @@ internal static void SetConfiguration(IList<IConfigurationProvider> providers, s
135138
provider.Set(key, value);
136139
}
137140
}
141+
142+
private string DebuggerToString()
143+
{
144+
return $"Sections = {ConfigurationSectionDebugView.FromConfiguration(this, this).Count}";
145+
}
146+
147+
private sealed class ConfigurationRootDebugView
148+
{
149+
private readonly ConfigurationRoot _current;
150+
151+
public ConfigurationRootDebugView(ConfigurationRoot current)
152+
{
153+
_current = current;
154+
}
155+
156+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
157+
public ConfigurationSectionDebugView[] Items => ConfigurationSectionDebugView.FromConfiguration(_current, _current).ToArray();
158+
}
138159
}
139160
}

src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationSection.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using Microsoft.Extensions.Primitives;
78

89
namespace Microsoft.Extensions.Configuration
910
{
1011
/// <summary>
1112
/// Represents a section of application configuration values.
1213
/// </summary>
14+
[DebuggerDisplay("{DebuggerToString(),nq}")]
15+
[DebuggerTypeProxy(typeof(ConfigurationSectionDebugView))]
1316
public class ConfigurationSection : IConfigurationSection
1417
{
1518
private readonly IConfigurationRoot _root;
@@ -96,5 +99,43 @@ public string? this[string key]
9699
/// </summary>
97100
/// <returns>The <see cref="IChangeToken"/>.</returns>
98101
public IChangeToken GetReloadToken() => _root.GetReloadToken();
102+
103+
private string DebuggerToString()
104+
{
105+
var s = $"Path = {Path}";
106+
var childCount = Configuration.ConfigurationSectionDebugView.FromConfiguration(this, _root).Count;
107+
if (childCount > 0)
108+
{
109+
s += $", Sections = {childCount}";
110+
}
111+
if (Value is not null)
112+
{
113+
s += $", Value = {Value}";
114+
IConfigurationProvider? provider = Configuration.ConfigurationSectionDebugView.GetValueProvider(_root, Path);
115+
if (provider != null)
116+
{
117+
s += $", Provider = {provider}";
118+
}
119+
}
120+
return s;
121+
}
122+
123+
private sealed class ConfigurationSectionDebugView
124+
{
125+
private readonly ConfigurationSection _current;
126+
private readonly IConfigurationProvider? _provider;
127+
128+
public ConfigurationSectionDebugView(ConfigurationSection current)
129+
{
130+
_current = current;
131+
_provider = Configuration.ConfigurationSectionDebugView.GetValueProvider(_current._root, _current.Path);
132+
}
133+
134+
public string Path => _current.Path;
135+
public string Key => _current.Key;
136+
public string? Value => _current.Value;
137+
public IConfigurationProvider? Provider => _provider;
138+
public List<Configuration.ConfigurationSectionDebugView> Sections => Configuration.ConfigurationSectionDebugView.FromConfiguration(_current, _current._root);
139+
}
99140
}
100141
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
9+
namespace Microsoft.Extensions.Configuration
10+
{
11+
internal sealed class ConfigurationSectionDebugView
12+
{
13+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
14+
private readonly IConfigurationSection _section;
15+
16+
public ConfigurationSectionDebugView(IConfigurationSection section, string path, IConfigurationProvider? provider)
17+
{
18+
_section = section;
19+
Path = path;
20+
Provider = provider;
21+
}
22+
23+
public string Path { get; }
24+
public string Key => _section.Key;
25+
public string FullPath => _section.Path;
26+
public string? Value => _section.Value;
27+
public IConfigurationProvider? Provider { get; }
28+
29+
public override string ToString()
30+
{
31+
var s = $"Path = {Path}";
32+
if (Value is not null)
33+
{
34+
s += $", Value = {Value}";
35+
}
36+
if (Provider is not null)
37+
{
38+
s += $", Provider = {Provider}";
39+
}
40+
return s;
41+
}
42+
43+
internal static List<ConfigurationSectionDebugView> FromConfiguration(IConfiguration current, IConfigurationRoot root)
44+
{
45+
var data = new List<ConfigurationSectionDebugView>();
46+
47+
var stack = new Stack<IConfiguration>();
48+
stack.Push(current);
49+
int prefixLength = (current is IConfigurationSection rootSection) ? rootSection.Path.Length + 1 : 0;
50+
while (stack.Count > 0)
51+
{
52+
IConfiguration config = stack.Pop();
53+
// Don't include the sections value if we are removing paths, since it will be an empty key
54+
if (config is IConfigurationSection section && config != current)
55+
{
56+
IConfigurationProvider? provider = GetValueProvider(root, section.Path);
57+
string path = section.Path.Substring(prefixLength);
58+
59+
data.Add(new ConfigurationSectionDebugView(section, path, provider));
60+
}
61+
foreach (IConfigurationSection child in config.GetChildren())
62+
{
63+
stack.Push(child);
64+
}
65+
}
66+
67+
data.Sort((i1, i2) => ConfigurationKeyComparer.Instance.Compare(i1.Path, i2.Path));
68+
return data;
69+
}
70+
71+
internal static IConfigurationProvider? GetValueProvider(IConfigurationRoot root, string key)
72+
{
73+
foreach (IConfigurationProvider provider in root.Providers.Reverse())
74+
{
75+
if (provider.TryGet(key, out _))
76+
{
77+
return provider;
78+
}
79+
}
80+
81+
return null;
82+
}
83+
}
84+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using Microsoft.Extensions.Configuration.Memory;
6+
using Xunit;
7+
8+
namespace Microsoft.Extensions.Configuration.Test
9+
{
10+
public class ConfigurationSectionDebugViewTest
11+
{
12+
[Fact]
13+
public void FromConfiguration_Root()
14+
{
15+
var config = new ConfigurationManager();
16+
17+
config.AddInMemoryCollection(new Dictionary<string, string>
18+
{
19+
{"Mem1:", "NoKeyValue1"},
20+
{"Mem1:KeyInMem1", "ValueInMem1"},
21+
{"Mem1:KeyInMem1:Deep1", "ValueDeep1"}
22+
});
23+
24+
var items = ConfigurationSectionDebugView.FromConfiguration(config, config);
25+
26+
Assert.Collection(items,
27+
i =>
28+
{
29+
Assert.Equal("Mem1", i.Path);
30+
Assert.Null(i.Value);
31+
Assert.Null(i.Provider);
32+
},
33+
i =>
34+
{
35+
Assert.Equal("Mem1:", i.Path);
36+
Assert.Equal("NoKeyValue1", i.Value);
37+
Assert.IsType<MemoryConfigurationProvider>(i.Provider);
38+
},
39+
i =>
40+
{
41+
Assert.Equal("Mem1:KeyInMem1", i.Path);
42+
Assert.Equal("ValueInMem1", i.Value);
43+
Assert.IsType<MemoryConfigurationProvider>(i.Provider);
44+
},
45+
i =>
46+
{
47+
Assert.Equal("Mem1:KeyInMem1:Deep1", i.Path);
48+
Assert.Equal("ValueDeep1", i.Value);
49+
Assert.IsType<MemoryConfigurationProvider>(i.Provider);
50+
});
51+
}
52+
53+
[Fact]
54+
public void FromConfiguration_Section()
55+
{
56+
var config = new ConfigurationManager();
57+
58+
config.AddInMemoryCollection(new Dictionary<string, string>
59+
{
60+
{"Mem1:", "NoKeyValue1"},
61+
{"Mem1:KeyInMem1", "ValueInMem1"},
62+
{"Mem1:KeyInMem1:", "NoKeyValue2"},
63+
{"Mem1:KeyInMem1:Deep1", "ValueDeep1"},
64+
{"Mem1:KeyInMem2", "ValueInMem1"}
65+
});
66+
67+
var section = config.GetSection("Mem1:KeyInMem1");
68+
69+
var items = ConfigurationSectionDebugView.FromConfiguration(section, config);
70+
71+
Assert.Collection(items,
72+
i =>
73+
{
74+
Assert.Equal("", i.Path);
75+
Assert.Equal("NoKeyValue2", i.Value);
76+
Assert.IsType<MemoryConfigurationProvider>(i.Provider);
77+
},
78+
i =>
79+
{
80+
Assert.Equal("Deep1", i.Path);
81+
Assert.Equal("ValueDeep1", i.Value);
82+
Assert.IsType<MemoryConfigurationProvider>(i.Provider);
83+
});
84+
}
85+
86+
[Fact]
87+
public void FromConfiguration_MultipleProviders()
88+
{
89+
var provider1 = new TestMemorySourceProvider(new Dictionary<string, string>
90+
{
91+
{"Mem1:", "NoKeyValue1"},
92+
{"Key1", "ValueInMem1"}
93+
});
94+
var provider2 = new TestMemorySourceProvider(new Dictionary<string, string>
95+
{
96+
{"Mem2:", "NoKeyValue2"},
97+
{"Key2", "ValueInMem2"}
98+
});
99+
100+
var config = new ConfigurationManager();
101+
config.Sources.Add(provider1);
102+
config.Sources.Add(provider2);
103+
104+
var items = ConfigurationSectionDebugView.FromConfiguration(config, config);
105+
106+
Assert.Collection(items,
107+
i =>
108+
{
109+
Assert.Equal("Key1", i.Path);
110+
Assert.Equal("Key1", i.Key);
111+
Assert.Equal("ValueInMem1", i.Value);
112+
Assert.Equal(provider1, i.Provider);
113+
},
114+
i =>
115+
{
116+
Assert.Equal("Key2", i.Path);
117+
Assert.Equal("Key2", i.Key);
118+
Assert.Equal("ValueInMem2", i.Value);
119+
Assert.Equal(provider2, i.Provider);
120+
},
121+
i =>
122+
{
123+
Assert.Equal("Mem1", i.Path);
124+
Assert.Equal("Mem1", i.Key);
125+
Assert.Null(i.Value);
126+
Assert.Null(i.Provider);
127+
},
128+
i =>
129+
{
130+
Assert.Equal("Mem1:", i.Path);
131+
Assert.Equal(string.Empty, i.Key);
132+
Assert.Equal("NoKeyValue1", i.Value);
133+
Assert.Equal(provider1, i.Provider);
134+
},
135+
i =>
136+
{
137+
Assert.Equal("Mem2", i.Path);
138+
Assert.Equal("Mem2", i.Key);
139+
Assert.Null(i.Value);
140+
Assert.Null(i.Provider);
141+
},
142+
i =>
143+
{
144+
Assert.Equal("Mem2:", i.Path);
145+
Assert.Equal(string.Empty, i.Key);
146+
Assert.Equal("NoKeyValue2", i.Value);
147+
Assert.Equal(provider2, i.Provider);
148+
});
149+
}
150+
151+
public class TestMemorySourceProvider : MemoryConfigurationProvider, IConfigurationSource
152+
{
153+
public TestMemorySourceProvider(Dictionary<string, string> initialData)
154+
: base(new MemoryConfigurationSource { InitialData = initialData })
155+
{ }
156+
157+
public IConfigurationProvider Build(IConfigurationBuilder builder)
158+
{
159+
return this;
160+
}
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)