amantinband/clean-architecture

Using options pattern in a different way

amirhessampourhossein opened this issue ยท 4 comments

Currently, this is how your JwtSettings is configured ๐Ÿ‘‡

var jwtSettings = new JwtSettings();
configuration.Bind(JwtSettings.Section, jwtSettings);
services.AddSingleton(Options.Create(jwtSettings));

What's your opinion about the approach below ๐Ÿ‘‡

services.AddOptions<JwtSettings>().BindConfiguration(JwtSettings.Section);

we could also add validation in two different ways :

  • Data Annotations (built-in)
services.AddOptions<JwtSettings>()
    .BindConfiguration(JwtSettings.Section)
    .ValidateDataAnnotations()
    .ValidateOnStart();
  • Fluent Validation
public class FluentValidateOptions<TOptions>
    : IValidateOptions<TOptions>
    where TOptions : class
{
    private readonly IServiceProvider _serviceProvider;
    private readonly string? _name;

    public FluentValidateOptions(IServiceProvider serviceProvider, string? name)
    {
        _serviceProvider = serviceProvider;
        _name = name;
    }

    public ValidateOptionsResult Validate(string? name, TOptions options)
    {
        if (_name is not null && _name != name)
            return ValidateOptionsResult.Skip;

        ArgumentNullException.ThrowIfNull(options);

        using var scope = _serviceProvider.CreateScope();

        var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();

        var result = validator.Validate(options);

        if (result.IsValid)
            return ValidateOptionsResult.Success;

        var errors = result.Errors
            .Select(x => $"Validation failed for {x.PropertyName} with the error: {x.ErrorMessage}")
            .ToList();

        return ValidateOptionsResult.Fail(errors);
    }
public static OptionsBuilder<TOptions> ValidateFluentValidation<TOptions>(
        this OptionsBuilder<TOptions> optionsBuilder)
        where TOptions : class
    {
        optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
            serviceProvider => new FluentValidateOptions<TOptions>(
                serviceProvider,
                optionsBuilder.Name
                ));
        return optionsBuilder;
    }
builder.Services.AddOptions<JwtSettings>()
    .BindConfiguration(JwtSettings.ConfigurationSectionName)
    .ValidateFluentValidation()
    .ValidateOnStart();

Generally I go with what you suggested, but over here we need to both register the settings as singleton and use it a few lines later:
image

Regarding validation, not sure I want to add this to the template, but let me think about it. Keeping the issue open for now

What about separating the second part and putting it in another class that implements IConfigureOptions<JwtBearerOptions> ?

Yeah that's a good option as well. Feel free to create a PR for this if you'd like :)

Thanks Amir! ๐Ÿซถ