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 BinderOption to allow callers to specify that configuration binding should throw an exception upon any failure. #36015

Closed
BlacKCaT27 opened this issue Dec 6, 2019 · 22 comments · Fixed by #53852
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-Configuration help wanted [up-for-grabs] Good issue for external contributors
Milestone

Comments

@BlacKCaT27
Copy link

BlacKCaT27 commented Dec 6, 2019

UPDATED PROPOSAL

Background and Motivation

The configuration binding system provided by Microsoft.Extensions.Configuration currently does not throw any kind of exception should there be any issue binding configuration data to a model object. Scenarios such as a key missing from a configuration provider, or a model object not being updated to provide access to a new configuration key can often result in difficult to diagnose run-time errors, some of which might not be caught until a deployment, as one of the few differences between testing and production environments are configuration values.

There would be value in providing users of the configuration system a mechanism to make model binding in either direction an exception-throwing event to make it more immediately apparent when issues arise due to configuration mismatches.

Proposed API

Create a new exception Microsoft.Extensions.Configuration.ConfigurationException. As much as possible, this exception should be designed to include as much information as possible based on the context in which it is throw. Given the proposals below, the exception should, at a minimum, always include the name of the configuration key and (when possible)/or (when not) the model property that was being bound at the time the exception was thrown.

Add two new properties to the Microsoft.Extensions.Configuration.BinderOptions class:

public class BinderOptions
    {
        /// <summary>
        /// When false (the default), the binder will only attempt to set public properties.
        /// If true, the binder will attempt to set all non read-only properties.
        /// </summary>
        public bool BindNonPublicProperties { get; set; }

+       /// <summary>
+       /// If set to true, the configuration system would throw a `ConfigurationException` if, during configuration binding, a 
+       /// configuration key is found for which the provided model object does not have an appropriate property which matches
+       /// the keys name. Note: appropriate property here refers to a property that is eligible to be bound to (for example, a
+       /// private property would not be considered an `eligible` target property unless 
+       /// </summary>
+       public bool ErrorOnPropertyMissing { get; set; }

+       /// <summary>
+       /// If set to true, the configuration system would throw a `ConfigurationException` if, during configuration binding, a 
+       /// property is found on a target model object which does not have a corresponding configuration key provided by any 
+       /// configuration providers. Default value is false.
+       /// </summary>
+       public bool ErrorOnKeyMissing { get; set; }
    }

Usage Examples

Below is an example when BinderOptions is being used alongside the Microsoft.Extensions.Options package. The usage would be the same for any other location where BinderOptions can be provided as an argument.

            services.AddOptions<RequestHandlingOptions>().Bind(Configuration.GetSection("RequestHandlingOptions"), options =>
            {
                options.BindNonPublicProperties = true;
                options.ErrorOnKeyMissing = true;
                options.ErrorOnPropertyMissing = true;
            });

Alternative Designs

I had considered including a 3rd flag as a new property: BinderOptions.ErrorOnConfigurationMismatch which would perhaps throw an exception in both cases proposed above, and/or additionally in other cases (such as model property count/configuration key count mismatch, which may or may not imply the above scenarios in some cases). Ultimately though I felt a simpler approach was best to start.

Or instead of adding the two properties above, add just one properties that controls throwing on all mismatches instead.

public class BinderOptions
    {
        /// <summary>
        /// When false (the default), the binder will only attempt to set public properties.
        /// If true, the binder will attempt to set all non read-only properties.
        /// </summary>
        public bool BindNonPublicProperties { get; set; }

+       /// <summary>
+       /// If set to true, the configuration system would throw a `ConfigurationException` if, during configuration binding, a 
+       /// configuration key is found for which the provided model object does not have an appropriate property which matches
+       /// the keys name; or if a configuration property is found without a matching key. 
+       /// </summary>
+       public bool ErrorOnConfigurationMismatch { get; set; }
}

Risks

As these flags would be disabled by default, the existing behavior of the configuration binding process would not change. However, developers should be encouraged to thoroughly test their configuration management systems when enabling these flags, given the environment-dependent nature of configuration management and the potential impact of the change should there be any issues with a systems configuration.

ORIGINAL POST

Is your feature request related to a problem? Please describe.

Currently, when using configuration binding, in the event that a property does not get bound (either because there's a missing entry in the configuration file or a missing property on the class being bound to), the binder simply defaults to null for that property. This results in run-time null exceptions if there is a configuration issue.

Describe the solution you'd like

I propose we add an optional setting in the new BinderOptions class which allow callers to specify that in the event of a failure to properly bind every configuration option, to throw an exception rather than passively set property values to null.

An optional extension of this idea would be to also allow callers to specify custom validators for specific configuration values. For example, being able to annotate the POCO that a configuration file is being bound to such that something like a Connection String could have a custom validator called on it to make sure all the required arguments are contained in the string, the database is reachable, etc.

Describe alternatives you've considered

The alternative is to use something like an IStartupFilter to manually check all your configuration parameters validity during application start-up. This is tedious and error-prone, and results in application level code having to validate behaviors provided by framework level code, mixing concerns.

Additional context

I believe this feature will add a lot of value at relatively low cost. It's much easier to find misconfiguration errors if start-up fails because of app misconfiguration, rather than having to track down unexpected null exceptions thrown from some piece of code using configuration values.

It looks like there's been some work/discussion on this front in other forms (issue dotnet/extensions#459 and issue dotnet/extensions#763), but those are more focused on the larger problems of IOption and custom validators. Perhaps this issue can focus solely on establishing a starting point via making Bind() throw on error an option.

I'd be more than happy to put up an initial PR to explore this further.

@BlacKCaT27 BlacKCaT27 changed the title Add BinderOption to allow callers to specify that configuration binding should thrown an exception upon any failure. Add BinderOption to allow callers to specify that configuration binding should throw an exception upon any failure. Dec 6, 2019
@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Dec 12, 2019

@Pilchie I'm just a fan of .net core who wants to make a contribution, not privy to the teams plans, but I don't want to put up a PR that has no chance of being merged if what I submit goes against whatever plan your team has on this topic. Is there already something in the works on this front?

@analogrelay analogrelay transferred this issue from dotnet/extensions May 7, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Meta untriaged New issue has not been triaged by the area owner labels May 7, 2020
@analogrelay analogrelay added this to the Future milestone May 7, 2020
@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Jul 1, 2020
@maryamariyan
Copy link
Member

@Pilchie I'm just a fan of .net core who wants to make a contribution, not privy to the teams plans, but I don't want to put up a PR that has no chance of being merged if what I submit goes against whatever plan your team has on this topic. Is there already something in the works on this front?

@BlacKCaT27 thanks for the issue. We usually stay away from throwing first hand exceptions, but since this is configurable and disabled by default this feature makes sense.
cc: @davidfowl

To satisfy this issue there needs to be a new API proposal introduced in configuration binding.

The best next step on this would be to prepare the API proposal, usage, etc. in the issue page here (following https://github.com/dotnet/runtime/blob/master/docs/project/api-review-process.md).

@maryamariyan maryamariyan added this to Triaged in ML, Extensions, Globalization, etc, POD. via automation Oct 1, 2020
@davidfowl
Copy link
Member

It's much easier to find misconfiguration errors if start-up fails because of app misconfiguration, rather than having to track down unexpected null exceptions thrown from some piece of code using configuration values.

It depends on how you bind, it won't fail at startup, it'll fail when you try to resolve the options.

@maryamariyan
Copy link
Member

maryamariyan commented Oct 6, 2020

Related to closed dupe: #36129 and #36502

@BlacKCaT27
Copy link
Author

Thank you very much for the update. I'll try to get a proposal together this weekend.

@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Oct 11, 2020

Background and Motivation

The configuration binding system provided by Microsoft.Extensions.Configuration currently does not throw any kind of exception should there be any issue binding configuration data to a model object. Scenarios such as a key missing from a configuration provider, or a model object not being updated to provide access to a new configuration key can often result in difficult to diagnose run-time errors, some of which might not be caught until a deployment, as one of the few differences between testing and production environments are configuration values.

There would be value in providing users of the configuration system a mechanism to make model binding in either direction an exception-throwing event to make it more immediately apparent when issues arise due to configuration mismatches.

Proposed API

Create a new exception Microsoft.Extensions.Configuration.ConfigurationException. As much as possible, this exception should be designed to include as much information as possible based on the context in which it is throw. Given the proposals below, the exception should, at a minimum, always include the name of the configuration key and (when possible)/or (when not) the model property that was being bound at the time the exception was thrown.

Add two new properties to the Microsoft.Extensions.Configuration.BinderOptions class:

BinderOptions.ErrorOnKeyMissing - boolean - If set to true, the configuration system would throw a ConfigurationException if, during configuration binding, a property is found on a target model object which does not have a corresponding configuration key provided by any configuration providers. Default value is false.

BinderOptions.ErrorOnPropertyMissing - boolean - If set to true, the configuration system would throw a ConfigurationException if, during configuration binding, a configuration key is found for which the provided model object does not have an appropriate property which matches the keys name. Note: appropriate property here refers to a property that is eligible to be bound to (for example, a private property would not be considered an eligible target property unless BinderOptions.BindNonPublicProperties were set to true).

Usage Examples

Below is an example when BinderOptions is being used alongside the Microsoft.Extensions.Options package. The usage would be the same for any other location where BinderOptions can be provided as an argument.

            services.AddOptions<RequestHandlingOptions>().Bind(Configuration.GetSection("RequestHandlingOptions"), options =>
            {
                options.BindNonPublicProperties = true;
                options.ErrorOnKeyMissing = true;
                options.ErrorOnPropertyMissing = true;
            });

Alternative Designs

I had considered including a 3rd flag as a new property: BinderOptions.ErrorOnConfigurationMismatch which would perhaps throw an exception in both cases proposed above, and/or additionally in other cases (such as model property count/configuration key count mismatch, which may or may not imply the above scenarios in some cases). Ultimately though I felt a simpler approach was best to start.

Risks

As these flags would be disabled by default, the existing behavior of the configuration binding process would not change. However, developers should be encouraged to thoroughly test their configuration management systems when enabling these flags, given the environment-dependent nature of configuration management and the potential impact of the change should there be any issues with a systems configuration.

@safern
Copy link
Member

safern commented Oct 15, 2020

@BlacKCaT27 thanks for putting this together. I moved it to the issue description as that is where the reviewers look at the proposal for and also formatted a little bit to use the diff tool in markdown so that it is clearer what APIs are being proposed.

I do have a question, in my opinion, I think it would just be better to have just one property that controls all binding errors, it is either, you want errors or not when binding, it doesn't matter if it is a key or a property, what do you think?

@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Oct 15, 2020 via email

@safern
Copy link
Member

safern commented Oct 15, 2020

Thanks, @BlacKCaT27 -- yeah I think having more control is sometimes good, but I would expect both properties to be set to true whenever someone wants error, I don't see a clear case of just setting one to true. I'll leave it as open question on the proposal so that when the design review goes ahead we make a decision.

@safern safern added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed feature-request labels Oct 15, 2020
@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 Jan 21, 2021
@terrajobst
Copy link
Member

terrajobst commented Jan 21, 2021

Video

  • ErrorOnPropertyMissing makes total sense. The default should be false so that it's not a breaking change. But the name is a bit ambiguous. We'd prefer ErrorOnUnknownConfiguration.
  • ErrorOnKeyMissing seems a bit strange because options are mostly, well, optional. A better design might be to add an attribute that the user would put on their strongly typed option type property, akin to how serializers work. This design assumes the binder doesn't initialize unspecified properties to their default value.
namespace Microsoft.Extensions.Configuration
{
    public class BinderOptions
    {
        public bool ErrorOnUnknownConfiguration{ get; set; }
    }
}

@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Jan 21, 2021

The thought behind ErrorOnKeyMissing was that there are cases where the default value of a configuration property will never be valid (e.g. connection strings), so you'd want the system to blow up if none of your configuration providers gave you what you were expecting.

That said, I see your point about this being a pretty heavy-handed approach when not all configuration properties necessarily need overrides. I love the idea of some sort of [ConfigurationRequired] attribute for config POCO properties. Is that something that could be worked into this change in lieu of ErrorOnKeyMissing?

Edit: Totally on board with the name change for PropertyMissing, btw. I agree it was too ambiguous.

@safern
Copy link
Member

safern commented Jan 30, 2021

I love the idea of some sort of [ConfigurationRequired] attribute for config POCO properties. Is that something that could be worked into this change in lieu of ErrorOnKeyMissing?

@BlacKCaT27 could you propose that as a separate issue? Would that be tight to this new property? I mean, would we throw if we find ConfigurationRequired only when ErrorOnUnknownConfiguration == true or would we throw always?

@BlacKCaT27
Copy link
Author

Great question. I'm personally leaning towards "throw always" and use the attribute in lieu of ErrorOnUnknownConfiguration. I'm struggling to see what value giving it a config flag would add. Either you use the attribute, and an exception is thrown if there's missing config data, or you don't and the check never throws for that property.

I can submit this as a separate issue.

@safern
Copy link
Member

safern commented Feb 1, 2021

I'm personally leaning towards "throw always" and use the attribute in lieu of ErrorOnUnknownConfiguration. I'm struggling to see what value giving it a config flag would add. Either you use the attribute...

The thing that it's making me doubt about that approach is that if I want the system to throw about any unknown configuration, I would have to add the attribute to all the keys? Maybe we could come up with a behavior where if you add it to the class it should throw for all the keys under that class, but if you have a subclass then you would end up annotating all classes, so in that case a switch would be simpler. So I think having the switch for an all or nothing behavior is helpful and then the attribute for a granular situation?

@BlacKCaT27
Copy link
Author

True, though I think at that point it depends more on the use case. I could see some scenarios where there's only a couple fields where I want this attribute (so an unmatched configuration throws, like a connection string) but for others I don't care.

What if, like you suggest, a switch for all-or-nothing behavior gets added, but then the attribute can be used to configure the systems reaction. Like, instead of the switch saying "do I or do I not throw when there's a problem", the switch becomes "check for required configuration attributes" and then the attribute can have a parameter list that describes what should happen on a failure. That way you could make the attribute more useful by giving it optional params like a default value to assign alongside a flag dictating whether or not to throw.

[ConfigurationRequired(default: "myDefaultValue", throwOnError: false)]
[ConfigurationRequired(throwOnError: true)]

Going this route you could then safely default the switch to On, since it would be inert until someone used ConfigurationRequiredAttribute. Then setting the switch to Off could be used like a debug control if a developer needs to disable it for any reason.

@safern
Copy link
Member

safern commented Feb 1, 2021

While I understand the intent of what you describe above I think not depending on attributes and being able to bind with validation for missing Properties by just turning on a switch is a really good thing to have. Then the attributes could be a separate configuration that doesn't depend on the switch where you can granularly fail for missing properties/keys and also configure the default value for a required configuration.

Don't you think it is still valuable for complicated configurations to instead of having to throw an attribute on all properties, to just turn on a switch to get missing properties validation?

@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Feb 1, 2021 via email

@safern
Copy link
Member

safern commented Feb 1, 2021

Having a global switch to start is a good approach. It may even provide us
more insight into how best to introduce an attribute later.

Awesome. Thanks for the discussion. Feel free to open a separate issue for the attribute addition where we can discuss the design of it.

Are you interested on tackling this issue?

@safern safern added the help wanted [up-for-grabs] Good issue for external contributors label Feb 1, 2021
@BlacKCaT27
Copy link
Author

BlacKCaT27 commented Feb 2, 2021 via email

@SteveDunn
Copy link
Contributor

I'd be happy to take this one on. My last PR (#52514) was closed as there was not enough information provided before being marked up-for-grabs, and hence the .net team will be doing it themselves at some point.
So it'd make sense for me to start another easy-ish task from a somewhat familiar area of the repo.

@maryamariyan
Copy link
Member

@SteveDunn sounds good.

@SteveDunn
Copy link
Contributor

I created a PR for this: #53852

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 12, 2021
ML, Extensions, Globalization, etc, POD. automation moved this from Future to Done Jun 17, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jun 17, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Jul 17, 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 help wanted [up-for-grabs] Good issue for external contributors
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants