Skip to content

[API Proposal]: ValidateRequiredMembers support for Microsoft.Extensions.Options #117888

@lol768

Description

@lol768

Background and motivation

Today, if I create a record with required properties for my configuration type, and bind from a configuration section to that record type, I still need to write my own IValidateOptions to do null-checks by hand, or I need to apply System.ComponentModel.DataAnnotations attributes to every member to duplicatively mark it as required.

Consider this concrete example:

public record MyConfigurationType
{
    public required string RequiredMember { get; init; }
}
services.AddOptions<MyConfigurationType>()
    .Bind(configuration.GetSection("MyConfiguration"))
    .ValidateOnStart();
{
  "MyConfiguration": {
    "foo": "bar"
  }
}

The application will start without any errors, despite RequiredMember not being set.

I can make it work by writing:

public class MyConfigurationTypeConfigurationValidation : IValidateOptions<MyConfigurationType>
{
    public ValidateOptionsResult Validate(string name, MyConfigurationType options)
    {
        if (options.RequiredMember == null)
        {
            return ValidateOptionsResult.Fail("RequiredMember must be provided");
        }

        return ValidateOptionsResult.Success;
    }
}

and then registering this IValidateOptions, but it's cumbersome to have to do this for every property.

You basically want something like this, that is generic:

public class MyConfigurationTypeConfigurationValidation : IValidateOptions<MyConfigurationType>
{
    public ValidateOptionsResult Validate(string name, MyConfigurationType options)
    {
        var type = options.GetType();
        foreach (var property in type.GetProperties()
                     .Where(p => p.GetCustomAttributes(typeof(RequiredMemberAttribute), false).Any()))
        {
            var value = property.GetValue(options);
            if (value == null)
            {
                return ValidateOptionsResult.Fail(
                    $"The property '{property.Name}' is required but was not provided.");
            }
        }

        return ValidateOptionsResult.Success;
    }
}

API Proposal

public static OptionsBuilder<TOptions> ValidateRequiredMembers<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class

API Usage

services.AddOptions<MyConfigurationType>()
    .Bind(configuration.GetSection("MyConfiguration"))
    .ValidateRequiredMembers() // looks for RequiredMemberAttribute
    .ValidateOnStart();

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions