Skip to content

Commit a5c042a

Browse files
committed
Reverse configuration chaining
1 parent f2a8e7a commit a5c042a

File tree

3 files changed

+131
-32
lines changed

3 files changed

+131
-32
lines changed

src/DefaultBuilder/src/WebApplication.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection;
54
using Microsoft.AspNetCore.Hosting;
65
using Microsoft.AspNetCore.Hosting.Server;
76
using Microsoft.AspNetCore.Hosting.Server.Features;
@@ -20,11 +19,11 @@ namespace Microsoft.AspNetCore.Builder
2019
/// </summary>
2120
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
2221
{
22+
internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
23+
2324
private readonly IHost _host;
2425
private readonly List<EndpointDataSource> _dataSources = new();
2526

26-
internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
27-
2827
internal WebApplication(IHost host)
2928
{
3029
_host = host;

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ namespace Microsoft.AspNetCore.Builder
1515
/// </summary>
1616
public sealed class WebApplicationBuilder
1717
{
18+
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
19+
1820
private readonly HostBuilder _hostBuilder = new();
1921
private readonly BootstrapHostBuilder _bootstrapHostBuilder;
2022
private readonly WebApplicationServiceCollection _services = new();
2123
private readonly List<KeyValuePair<string, string>> _hostConfigurationValues;
22-
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
2324

2425
private WebApplication? _builtApplication;
2526

@@ -62,7 +63,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
6263
});
6364

6465
// Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application name).
65-
6666
_bootstrapHostBuilder.ConfigureHostConfiguration(config =>
6767
{
6868
if (args is { Length: > 0 })
@@ -74,7 +74,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
7474
options.ApplyHostConfiguration(config);
7575
});
7676

77-
7877
Configuration = new();
7978

8079
// Collect the hosted services separately since we want those to run after the user's hosted services
@@ -100,7 +99,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
10099
Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
101100
WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
102101

103-
Services.AddSingleton<IConfiguration>(Configuration);
102+
Services.AddSingleton<IConfiguration>(_ => Configuration);
104103
}
105104

106105
/// <summary>
@@ -148,14 +147,13 @@ public WebApplication Build()
148147
builder.AddInMemoryCollection(_hostConfigurationValues);
149148
});
150149

150+
var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
151+
151152
// Wire up the application configuration by copying the already built configuration providers over to final configuration builder.
152153
// We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources.
153154
_hostBuilder.ConfigureAppConfiguration(builder =>
154155
{
155-
foreach (var provider in ((IConfigurationRoot)Configuration).Providers)
156-
{
157-
builder.Sources.Add(new ConfigurationProviderSource(provider));
158-
}
156+
builder.Add(chainedConfigSource);
159157

160158
foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
161159
{
@@ -173,17 +171,6 @@ public WebApplication Build()
173171
// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
174172
foreach (var s in _services)
175173
{
176-
// Skip the configuration manager instance we added earlier
177-
// we're already going to wire it up to this new configuration source
178-
// after we've built the application. There's a chance the user manually added
179-
// this as well but we still need to remove it from the final configuration
180-
// to avoid cycles in the configuration graph
181-
if (s.ServiceType == typeof(IConfiguration) &&
182-
s.ImplementationInstance == Configuration)
183-
{
184-
continue;
185-
}
186-
187174
services.Add(s);
188175
}
189176

@@ -205,18 +192,23 @@ public WebApplication Build()
205192
// Drop the reference to the existing collection and set the inner collection
206193
// to the new one. This allows code that has references to the service collection to still function.
207194
_services.InnerCollection = services;
195+
196+
// Make builder.Configuration match the final configuration. To do that, we add the additional
197+
// providers in the inner _hostBuilders's Configuration to the ConfigurationManager.
198+
foreach (var provider in ((IConfigurationRoot)context.Configuration).Providers)
199+
{
200+
if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
201+
{
202+
((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider));
203+
}
204+
}
208205
});
209206

210207
// Run the other callbacks on the final host builder
211208
Host.RunDeferredCallbacks(_hostBuilder);
212209

213210
_builtApplication = new WebApplication(_hostBuilder.Build());
214211

215-
// Make builder.Configuration match the final configuration. To do that
216-
// we clear the sources and add the built configuration as a source
217-
((IConfigurationBuilder)Configuration).Sources.Clear();
218-
Configuration.AddConfiguration(_builtApplication.Configuration);
219-
220212
// Mark the service collection as read-only to prevent future modifications
221213
_services.IsReadOnly = true;
222214

@@ -301,6 +293,24 @@ public LoggingBuilder(IServiceCollection services)
301293
public IServiceCollection Services { get; }
302294
}
303295

296+
private sealed class TrackingChainedConfigurationSource : IConfigurationSource
297+
{
298+
private readonly ChainedConfigurationSource _chainedConfigurationSource = new();
299+
300+
public TrackingChainedConfigurationSource(ConfigurationManager configManager)
301+
{
302+
_chainedConfigurationSource.Configuration = configManager;
303+
}
304+
305+
public IConfigurationProvider? BuiltProvider { get; set; }
306+
307+
public IConfigurationProvider Build(IConfigurationBuilder builder)
308+
{
309+
BuiltProvider = _chainedConfigurationSource.Build(builder);
310+
return BuiltProvider;
311+
}
312+
}
313+
304314
private sealed class ConfigurationProviderSource : IConfigurationSource
305315
{
306316
private readonly IConfigurationProvider _configurationProvider;

src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
using Microsoft.Extensions.Hosting;
2424
using Microsoft.Extensions.Logging;
2525
using Microsoft.Extensions.Options;
26-
using Xunit;
2726

2827
[assembly: HostingStartup(typeof(WebApplicationTests.TestHostingStartup))]
2928

@@ -706,7 +705,7 @@ public void CanResolveIConfigurationBeforeBuildingApplication()
706705
var app = builder.Build();
707706

708707
// These are different
709-
Assert.NotSame(app.Configuration, builder.Configuration);
708+
Assert.Same(app.Configuration, builder.Configuration);
710709
}
711710

712711
[Fact]
@@ -723,7 +722,28 @@ public void ManuallyAddingConfigurationAsServiceWorks()
723722
var app = builder.Build();
724723

725724
// These are different
726-
Assert.NotSame(app.Configuration, builder.Configuration);
725+
Assert.Same(app.Configuration, builder.Configuration);
726+
}
727+
728+
[Fact]
729+
public void AddingMemoryStreamBackedConfigurationWorks()
730+
{
731+
var builder = WebApplication.CreateBuilder();
732+
733+
var jsonConfig = @"{ ""foo"": ""bar"" }";
734+
using var ms = new MemoryStream();
735+
using var sw = new StreamWriter(ms);
736+
sw.WriteLine(jsonConfig);
737+
sw.Flush();
738+
739+
ms.Position = 0;
740+
builder.Configuration.AddJsonStream(ms);
741+
742+
Assert.Equal("bar", builder.Configuration["foo"]);
743+
744+
var app = builder.Build();
745+
746+
Assert.Equal("bar", app.Configuration["foo"]);
727747
}
728748

729749
[Fact]
@@ -1495,6 +1515,22 @@ public void ClearingConfigurationDoesNotAffectHostConfiguration()
14951515
Assert.Equal(Path.GetTempPath(), hostEnv.ContentRootPath);
14961516
}
14971517

1518+
[Fact]
1519+
public void ConfigurationGetDebugViewWorks()
1520+
{
1521+
var builder = WebApplication.CreateBuilder();
1522+
1523+
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string>
1524+
{
1525+
["foo"] = "bar",
1526+
});
1527+
1528+
var app = builder.Build();
1529+
1530+
// Make sure we don't lose "MemoryConfigurationProvider" from GetDebugView() when wrapping the provider.
1531+
Assert.Contains("foo=bar (MemoryConfigurationProvider)", ((IConfigurationRoot)app.Configuration).GetDebugView());
1532+
}
1533+
14981534
[Fact]
14991535
public void ConfigurationCanBeReloaded()
15001536
{
@@ -1524,23 +1560,77 @@ public void ConfigurationSourcesAreBuiltOnce()
15241560
Assert.Equal(1, configSource.ProvidersBuilt);
15251561
}
15261562

1563+
[Fact]
1564+
public void ConfigurationProvidersAreLoadedOnceAfterBuild()
1565+
{
1566+
var builder = WebApplication.CreateBuilder();
1567+
1568+
var configSource = new RandomConfigurationSource();
1569+
((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource);
1570+
1571+
using var app = builder.Build();
1572+
1573+
Assert.Equal(1, configSource.ProvidersLoaded);
1574+
}
1575+
1576+
[Fact]
1577+
public void ConfigurationProvidersAreDisposedWithWebApplication()
1578+
{
1579+
var builder = WebApplication.CreateBuilder();
1580+
1581+
var configSource = new RandomConfigurationSource();
1582+
((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource);
1583+
1584+
{
1585+
using var app = builder.Build();
1586+
1587+
Assert.Equal(0, configSource.ProvidersDisposed);
1588+
}
1589+
1590+
Assert.Equal(1, configSource.ProvidersDisposed);
1591+
}
1592+
1593+
[Fact]
1594+
public void ConfigurationProviderTypesArePreserved()
1595+
{
1596+
var builder = WebApplication.CreateBuilder();
1597+
1598+
((IConfigurationBuilder)builder.Configuration).Sources.Add(new RandomConfigurationSource());
1599+
1600+
var app = builder.Build();
1601+
1602+
Assert.Single(((IConfigurationRoot)app.Configuration).Providers.OfType<RandomConfigurationProvider>());
1603+
}
1604+
15271605
public class RandomConfigurationSource : IConfigurationSource
15281606
{
15291607
public int ProvidersBuilt { get; set; }
1608+
public int ProvidersLoaded { get; set; }
1609+
public int ProvidersDisposed { get; set; }
15301610

15311611
public IConfigurationProvider Build(IConfigurationBuilder builder)
15321612
{
15331613
ProvidersBuilt++;
1534-
return new RandomConfigurationProvider();
1614+
return new RandomConfigurationProvider(this);
15351615
}
15361616
}
15371617

1538-
public class RandomConfigurationProvider : ConfigurationProvider
1618+
public class RandomConfigurationProvider : ConfigurationProvider, IDisposable
15391619
{
1620+
private readonly RandomConfigurationSource _source;
1621+
1622+
public RandomConfigurationProvider(RandomConfigurationSource source)
1623+
{
1624+
_source = source;
1625+
}
1626+
15401627
public override void Load()
15411628
{
1629+
_source.ProvidersLoaded++;
15421630
Data["Random"] = Guid.NewGuid().ToString();
15431631
}
1632+
1633+
public void Dispose() => _source.ProvidersDisposed++;
15441634
}
15451635

15461636
class ThrowingStartupFilter : IStartupFilter

0 commit comments

Comments
 (0)