Skip to content

[API Proposal]: ConfigurationBinder.GetRequiredValue #69163

Open
@MattKotsenas

Description

@MattKotsenas

Background

ConfigurationBinder has a method GetValue<T>, which is a simple way of getting a single value from configuration and converting it to the specified type (docs). The API optionally accepts a default value if the configuration key cannot be found. If the key is not found and no default is specified, default(T) is returned. If the value is found but cannot be converted to the target type, an exception is thrown.

Motivation

In some cases, silently returning default(T) can lead to subtle bugs. For example, consider an example app that requires a timeout value, which should always be specified in appsettings.json, and is retrieved like this var timeout = configuration.GetValue<TimeSpan>("Timeout"). In our example the config value is missing. Since no default is specified, the default TimeSpan is returned, which is... 00:00:00! As a result, all operations time out instantly rather that what we might expect, which is some type of exception that the configuration key wasn't found.

.NET 6 introduced a GetRequiredSection extension that performs similar validation for configuration sections, so I thought it may be appropriate to extend that convenience to the single value case as well.

Note that the proposed APIs would be on the ConfigurationBinder class in Microsoft.Extensions.Configuration.Binder and not ConfigurationExtensions in Microsoft.Extensions.Configuration.Abstractions since GetValue is exposed through Binder and isn't part of the Abstractions layer.

API Proposal

namespace Microsoft.Extensions.Configuration
{
    public partial static class ConfigurationBinder
    {
        public static T GetRequiredValue<T>(this IConfiguration configuration, string key);
        public static object GetRequiredValue(this IConfiguration configuration, Type type, string key);
    }
}

I propose that the exception if the configuration key isn't found is an InvalidOperationException, since that's what GetRequiredSection throws, but am open to other suggestions.

API Usage

Configuration:

{
    "MyTimeout": "00:00:10"
}

Comparison of usage (generic):

var endpoint = configuration.GetRequiredValue<TimeSpan>("MyTimeout"); // returns '00:00:10'

var endpoint = configuration.GetRequiredValue<TimeSpan>("MyMispelledTimeout"); // throws exception
var endpoint = configuration.GetValue<TimeSpan>("MyMispelledTimeout"); // returns '00:00:00'

Comparison of usage (non-generic):

var endpoint = configuration.GetRequiredValue(typeof(TimeSpan), "MyTimeout"); // returns '00:00:10'

var endpoint = configuration.GetRequiredValue(typeof(TimeSpan), "MyMispelledTimeout"); // throws exception
var endpoint = configuration.GetValue(typeof(TimeSpan), "MyMispelledTimeout"); // returns 'null'

Alternative Designs

The non-generic case can be simulated with a one-liner like this:

var timeout = configuration.GetValue(typeof(TimeSpan), "MyTimeout") ?? throw new InvalidOperationException();

which could then be made generic with another cast, but seems a bit unwieldy, especially if used in multiple places.

The generic version is more important in my opinion, since it's the case that can introduce confusion by coercing a missing value to the default (especially for value types). The non-generic version is proposed mostly for symmetry and reflection scenarios, and thus if it's decided that the non-generic version is not wanted, I wouldn't be opposed to dropping it.

Risks

Low as far as I can see. It's a new, opt-in API that follows an existing naming convention, and that increases type safety, so the likelihood of misuse or abuse seems low.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions