Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a type implementing both IConfiguration and IConfigurationBuilder #51770

Closed
halter73 opened this issue Apr 23, 2021 · 15 comments · Fixed by #55338
Closed

Add a type implementing both IConfiguration and IConfigurationBuilder #51770

halter73 opened this issue Apr 23, 2021 · 15 comments · Fixed by #55338
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-Configuration blocking Marks issues that we want to fast track in order to unblock other important work
Milestone

Comments

@halter73
Copy link
Member

halter73 commented Apr 23, 2021

Background and Motivation

When developing a new minimal host for ASP.NET Core (WebApplication, WebApplicationBuilder), we found that it would be useful to be able to read from configuration (e.g. appsettings.json and DOTNET_/ASPNETCORE_ environment variables) while still being able to add new configuration sources. Every time a source is added via the IConfigurationBuilder interface, the IConfiguration updates automatically and immediately.

For this reason, we've temporarily introduced a Configuration type to Microsoft.AspNetCore.Builder that does exactly this. This type belongs in Microsoft.Extensions.Configuration though.

Moving this type will be a breaking change in ASP.NET Core between preview6 and preview7, but we're okay this.

Proposed API

namespace Microsoft.Extensions.Configuration
{
+    public sealed class ConfigurationManager : IConfigurationRoot, IConfigurationBuilder, IDisposable
+    {
+        public ConfigurationManager();
+        public string? this[string key] { get; set; }
+        public IConfigurationSection GetSection(string key);
+        public void Dispose();
+    }

The members that are required to implement IConfigurationBuilder will be implemented explicitly, so members like IList<IConfigurationSource> IConfigurationBuilder.Sources don't pollute intellisense. Extension methods are generally used to add configuration sources.

Usage Examples

WebApplicationBuilder essentially already exposes this type as a public property. The problem it is trying to solve is being able to read config from appsettings.json and DOTNET_/ASPNETCORE_ while configuring the host's IServiceCollection while at the same time being able to add new configuration sources or even change the content root without having to introduce additional build stages.

    using var config = new Config();

    config.AddEnvironmentVariables(prefix: "MyCustomPrefix_");

    if (config["FileConfig"] == "enabled")
    {
        config.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true);
    } 

    string myValueFromJson = config["JsonConfigValue"];
    // ...

We have docs where we demonstrate manually building an IConfigurationBuilder, reading from the built IConfiguration, and then throwing that away to add a new config source. This is an alternative to that.

Alternative Designs

Update ConfigurationBuilder to implement IConfigurationRoot and basically become this type. It would leave the IConfigurationBuilder methods as normal methods instead of explicit interface implementations like in the proposal.

In the current implementation of Configuration in Microsoft.AspNetCore.Builder, the ChangeBasePath and ChangeFileProvider methods are internal. Consumers of WebApplicationBuilder should generally call builder.WebHost.SetContentRoot() instead. That, in turn, calls these currently-internal methods.

I was thinking an alternative might be an interface or an abstract base class we implement in ASP.NET Core. Perhaps IUpdateConfiguration: IConfigurationRoot, IConfigurationBuilder. I imagine we'd want at least one default implementation in Microsoft.Extensions.Configuration though.

All sources are now reloaded when the IConfigurationBuilder.Properties["FileProvider"] changes.

Risks

It might be confusing to developers if and when this should be used over a plain old ConfigurationBuilder. It also might not be clear if you should call ChangeBasePath or ChangeFileProvider methods instead of the SetBasePath and SetFileProvider methods extension with this new type. The answer is you should.

@maryamariyan @eerhardt @safern @tarekgh @pranavkm @davidfowl @Tratcher

@halter73 halter73 added api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Configuration labels Apr 23, 2021
@ghost
Copy link

ghost commented Apr 23, 2021

Tagging subscribers to this area: @maryamariyan, @safern
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

When developing a new minimal host for ASP.NET Core (WebApplication, WebApplicationBuilder), we found that it would be useful to be able to read from configuration (appsettings.json and DOTNET_/ASPNETCORE_) environment variables in particular while still being able to add new configuration sources. Every time the configurati

For this reason, we've temporarily introduced a Configuration type to Microsoft.AspNetCore.Builder that does exactly this. This type belongs in Microsoft.Extensions.Configuration though.

Moving this type will be a breaking change in ASP.NET Core between preview4 and preview5, but we're okay this.

Proposed API

namespace Microsoft.Extensions.Configuration
{
+    public sealed class Configuration : IConfigurationRoot, IConfigurationBuilder
+    {
+        public void ChangeBasePath(string path)
+        public void ChangeFileProvider(IFileProvider fileProvider)
+    }

Usage Examples

WebApplicationBuilder essentially already exposes this type as a public property. The problem it is trying to solve is being able to read config from appsettings.json and DOTNET_/ASPNETCORE_ while configuring the host's IServiceCollection while at the same time being able to add new configuration sources or even change the content root without having to introduce additional build stages.

    var config = new Configuration();

    config.AddEnvironmentVariables(prefix: "MyCustomPrefix_");

    if (config["FileConfig"] == "enabled")
    {
        config.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true);
    } 

Alternative Designs

In the current implementation of Configuration in Microsoft.AspNetCore.Builder, the ChangeBasePath and ChangeFileProvider methods are internal. For WebApplicationBuilder, we would prefer builder.WebHost.SetContentRoot() was called instead, but that in turn call these currently-internal methods.

I was thinking an alternative might be an interface or an abstract base class we implement in ASP.NET Core. Perhaps IUpdateConfiguration: IConfigurationRoot, IConfigurationBuilder. I imagine we'd want at least one default implementation in Microsoft.Extensions.Configuration though.

Risks

It might be confusing to developers if and when this should be used over a plain old ConfigurationBuilder. It also might not be clear if you should call ChangeBasePath or ChangeFileProvider methods instead of the SetBasePath and SetFileProvider methods with this new type. The answer is you should.

Author: halter73
Assignees: -
Labels:

api-suggestion, area-Extensions-Configuration

Milestone: -

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Apr 23, 2021
@pakrym
Copy link
Contributor

pakrym commented Apr 24, 2021

How does reloading work when one config source is based on another one? I.e. appconfig.json contains a KeyVault uri and we reload app config.

Example:

var config = new Configuration();
config.AddEnvironmentVariables(prefix: "MyCustomPrefix_"); 
config.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true);
config.AddKeyVault(config["kvuri");

@pakrym
Copy link
Contributor

pakrym commented Apr 24, 2021

Can we turn the ConfigurationBuilder into this type? Just make the IConfiguration members lazy?

@halter73
Copy link
Member Author

That's a good point. If "kvuri" comes from a reloadable source, the keyvault source wouldn't be updated when "kvuri" updates after startup.

Just make the IConfiguration members lazy?

I don't think it's possible to make the above example "just work" though. How does AddKeyVault know that it's argument came from config? If AddKeyVault is called in Program.Main, we cannot just rerun it.

Maybe we can add a helper method that let's you register a callback that reruns on reload and re-adds dependent config sources.

@pakrym
Copy link
Contributor

pakrym commented Apr 24, 2021

I don't think it's possible to make the above example "just work" though. How does AddKeyVault know that it's argument came from config? If AddKeyVault is called in Program.Main, we cannot just rerun it.

Yeah, that comment was unrelated to the first one. I'm just proposing to reuse the existing type instead of adding a new one.

@davidfowl
Copy link
Member

davidfowl commented Apr 24, 2021

Can we turn the ConfigurationBuilder into this type? Just make the IConfiguration members lazy?

That's a very interesting idea. I think it would work really well actually.

@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Apr 26, 2021
@maryamariyan maryamariyan added this to the 6.0.0 milestone Apr 26, 2021
@KevinCathcart
Copy link
Contributor

In terms of overall value, I see the basic concept as a good idea. As proof, I offer that implementation linked to in the first post is the second implementation of this in ASP.NET Core.

However I do have real concerns with both the API as proposed here, and especially with the "reference implementation" of this type shipped by ASP.NET Core in Preview 4

For the API

I object to the new methods, they are not necessary.

First of all, SetBasePath is just a helper over SetFileProvider, and all that really does is set a property in the builder's Property dictionary.

But the implementation's Property IDictionary<,> can be built to monitor values being set just like sources list does so these are simply not needed. Since any change to these properties could result in a configuration source provider generating a configuration source with different config values, you really want to trigger rebuild on that anyway.

For the implementation

  1. It would probably be better to actually implement the IConfigurationBuilder directly rather than delegate to the existing ConfigurationBuilder class. It is only 24 lines of nearly trivial non-whitespace non-comment code. Your current implementation would probably come within one or two lines of breaking-even with this change, and it can completely avoid concerns like the sources lists getting out of sync. (Of course, if this does become just enhancing the existing ConfigurationBuilder, then you get this for free!)

  2. You really do want the build/rebuilds to be lazy. Building a new ConfigurationRoot after each change even if not queried between the changes is not great, since the load action of the configuration provider can potentially be expensive (e.g. network IO). Having to do that 3 times because you just added three more file sources seems silly. Being lazy, and rebuilding only when you try to fetch a configuration value or the providers property is much better.

Even better would be to also cache the providers, and only build the new sources when ones are added. However for property changes, you would need to do a full rebuild. Only problem is that that approach would prevent delegating to the existing ConfigurationRoot class. I'd suggest NOT doing this approach at first, as it will make proper ChangeToken handling rather complicated. The lazy building/rebuilding is probably good enough, unless people complain.

  1. In the real implementation, GetSections and GetChildren should wrap this hybrid type. This will mean that if you are holding onto an IConfigurationSection, you will see updates from newly added sources. If implemented in the main Microsoft.Extensions.Configuration assembly, these would both be literal one liners:
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);

The second one is an internal static shared helper that works for any IConfigurationRoot, used to share the implementation of GetChildren between RootConfiguration and ConfigurationSection.

  1. You need to implement the Reload token properly. It needs to be a separate token that triggers whenever a (lazy) rebuild is triggered, as well as whenever the delegated ConfigurationRoot's token fires. Look at how the existing ConfigurationRoot class handles this. Its token is triggered when any provider's is triggered, or when you call it's reload method. The only real difference here is that we have a single ConfigurationRoot, as opposed to multiple providers, and there are a few minor twists stemming from laziness.

  2. Having the build method returning this is dangerous. It needs to return something that is thread safe because the result becomes a DI singleton, and this combined builder is not thread-safe. Returning this._configuration is also not good, since that instance could get disposed. Returning a freshly build ConfigurationRoot is the safe option.

I've posted a gist with my proposed alternative reference implementation that addresses all the above points. (It is a derivative of the one I'm criticizing. This implementation is offered under the terms of the .NET CLA, which you can verify I have signed by looking at my pull request(s) in this repo.).

Hope this helps.

@halter73
Copy link
Member Author

I object to the new methods, they are not necessary.

Agreed. I need to update this proposal. @pakrym pointed out many the same things. Thanks for the gist! The reference implementation will need to be rewritten.

halter73 added a commit to dotnet/aspnetcore that referenced this issue May 14, 2021
halter73 added a commit to dotnet/aspnetcore that referenced this issue May 18, 2021
halter73 added a commit to dotnet/aspnetcore that referenced this issue May 20, 2021
halter73 added a commit to dotnet/aspnetcore that referenced this issue May 21, 2021
halter73 added a commit to dotnet/aspnetcore that referenced this issue May 26, 2021
@halter73 halter73 added blocking Marks issues that we want to fast track in order to unblock other important work api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jul 3, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 8, 2021
@am11
Copy link
Member

am11 commented Jul 9, 2021

If renaming type is possible, perhaps MutableConfiguration would be better than Config?

@davidfowl
Copy link
Member

Yea, I'm not a fan of Config, plus it violates the naming guidelines 😄

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Jul 12, 2021
@terrajobst
Copy link
Member

  • @bartonjs @halter73 and me had a quick sync and concluded that a single term is problematic, especially because Microsoft.Extensions.Configuration is added as a global using
  • Most of the good compound names are taken, so we went with ConfigurationManager as that was the least gross name we could think off
namespace Microsoft.Extensions.Configuration
{
    public sealed class ConfigurationManager : IConfigurationRoot, IConfigurationBuilder, IDisposable
    {
        public ConfigurationManager();
        public string? this[string key] { get; set; }
        public IConfigurationSection GetSection(string key);
        public void Dispose();
    }
}

@davidfowl
Copy link
Member

What happens when System.Configuration is in the same project?

@eerhardt
Copy link
Member

One other alternate design possibility: implement IConfigurationBuilder on ConfigurationRoot.

namespace Microsoft.Extensions.Configuration
{
    public sealed class ConfigurationRoot : IConfigurationRoot, IConfigurationBuilder, IDisposable
    {
        public ConfigurationRoot();
        public string? this[string key] { get; set; }
        public IConfigurationSection GetSection(string key);
        public void Dispose();
    }
}
    using var config = new ConfigurationRoot();

    config.AddEnvironmentVariables(prefix: "MyCustomPrefix_");

    if (config["FileConfig"] == "enabled")
    {
        config.AddJsonFile("MyConfig.json", optional: true, reloadOnChange: true);
    } 

    string myValueFromJson = config["JsonConfigValue"];
    // ...

@davidfowl
Copy link
Member

That feels slightly risky as it changes the behavior quite a bit and introduces some potentially undesirable side effects

@eerhardt
Copy link
Member

as it changes the behavior quite a bit and introduces some potentially undesirable side effects

Can you elaborate on that? My thinking is ConfigurationRoot would have 2 modes, depending on how it was constructed. It has an existing ctor that takes a list of config providers and we would add a new parameterless ctor. If you called the existing ctor, you wouldn't be able to add more builders to it - it is "frozen" or "locked", like it is today. But if created with the new parameterless ctor, we would give it the behavior desired here.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 13, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Aug 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-Configuration blocking Marks issues that we want to fast track in order to unblock other important work
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants