Description
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.