Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get all registered IOptions [duplicate]

Following this post: https://blog.bredvid.no/validating-configuration-in-asp-net-core-e9825bd15f10

I m now available to validate the Settings when a service need it. What I would like to do is to validate directly when the server starts in program.cs.

I m not sure how to do it? Is there a way to get the list of services injected in DI then verify if the type is assignable from IOption and register it?

Here is how I add to DI the Settings:

    //App settings
    services.ConfigureAndValidate<AuthenticationSettings>(Configuration);
    services.ConfigureAndValidate<SmtpSettings>(Configuration);

The extension code:

public static class IServiceCollectionExtensions
    {
        public static IServiceCollection ConfigureAndValidate<T>(
            this IServiceCollection serviceCollection,
            IConfiguration config,
            string section = null
        ) where T : class
        {
            var configType = typeof(T).Name;
            if (string.IsNullOrEmpty(section)) { 
                section = configType;
            }

            return serviceCollection
                .Configure<T>(config.GetSection(section))
                .PostConfigure<T>(settings =>
                {
                    var configErrors = settings.ValidationErrors().ToArray();
                    if (configErrors.Any())
                    {
                        var aggrErrors = string.Join(",", configErrors);
                        var count = configErrors.Length;
                        throw new ApplicationException($"Found {count} configuration error(s) in {configType}: {aggrErrors}");
                    }
                });
        }

        private static IEnumerable<string> ValidationErrors(this object obj)
        {
            var context = new ValidationContext(obj, serviceProvider: null, items: null);
            var results = new List<ValidationResult>();
            Validator.TryValidateObject(obj, context, results, true);
            foreach (var validationResult in results)
            {
                yield return validationResult.ErrorMessage;
            }
        }
    }

Here is my current launcher:

public class Program
{
    public static async Task Main(string[] args)
    {
        var webHost = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config.AddEnvironmentVariables();

                var env = hostingContext.HostingEnvironment;

                config.SetBasePath(env.ContentRootPath)
                      .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
            })
            .ConfigureLogging((hostingContext, logging) =>
            {
                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
            })
            .UseStartup<Startup>()
            .Build();

        using (var scope = webHost.Services.CreateScope())
        {
            var services = scope.ServiceProvider;

            /// <---- BEGIN / AN IDEA OF WHAT I WOULD LIKE TO DO ---->
            /// <---- THIS CODE IS NOT WORKING ---->
            var allServices = services.GetAllServices();
            if (allServices != null)
            {
                foreach (var service in allServices )
                {
                    if (service.ServiceType.IsAssignableFrom(IOptions))
                    {
                       services.GetRequiredService<service.ServiceType>()
                    }
                }
            }
            /// <---- END ---->
        }

        await webHost.RunAsync();
    }
}

Let me know if you have any suggestions in comment.

Thanks for your help.


EDIT 1: Thanks Steven for your help, with your answer, it helped me to continue to find an answer, but things are still missing.

now, all my settings inherit from ISettings, like:

public class AuthenticationSettings : ISettings
{
    [Required]
    public string Issuer { get; set; }
    [Required]
    public string Audience { get; set; }
    [Required]
    public string SecretKey { get; set; }
    [Required]
    public int ExpirationDurationInDays { get; set; }
}

I update Program.cs like:

using Autofac;
using Autofac.Core;



var options = services.GetService<ILifetimeScope>()
   .ComponentRegistry
   .Registrations.SelectMany(e => e.Services)
   .Select(s => s as TypedService)
   .Where(s => s.ServiceType.IsGenericType && s.ServiceType.GetGenericTypeDefinition() == typeof(IConfigureOptions<>))
   .Select(s => s.ServiceType.GetGenericArguments()[0])
   .Where(s => typeof(ISettings).IsAssignableFrom(s))
   .ToList();

so now I need to instantiate each option in options and get the Value. I m still working on it. let me know if you have any suggestion or the solution :)

like image 924
Cedric Arnould Avatar asked Jan 08 '19 16:01

Cedric Arnould


Video Answer


2 Answers

You can get a list of configured option types by iterating the IServiceCollection instance:

var configuredOptionTypes =
    from descriptor in services
    let serviceType = descriptor.ServiceType
    where serviceType.IsGenericType
    where serviceType.GetGenericTypeDefinition() == typeof(IConfigureNamedOptions<>)
    let optionType = serviceType.GetGenericArguments()[0]
    select optionType;
like image 104
Steven Avatar answered Oct 06 '22 02:10

Steven


Following suggestions from Steven, here is my solution: My settings validator service

    public SettingsValidator(
        IServiceProvider services,
        ILifetimeScope scope
    )
    {
        var types = scope.ComponentRegistry.Registrations
            .SelectMany(e => e.Services)
            .Select(s => s as TypedService)
            .Where(s => s.ServiceType.IsAssignableToGenericType(typeof(IConfigureOptions<>)))
            .Select(s => s.ServiceType.GetGenericArguments()[0])
            .Where(s => typeof(ISettings).IsAssignableFrom(s))
            .ToList();

        foreach (var t in types)
        {
            var option = services.GetService(typeof(IOptions<>).MakeGenericType(new Type[] { t }));
            option.GetPropertyValue("Value");
        }
    }

in startup:

        builder.RegisterType<SettingsValidator>();

example of Settings

public class AzureStorageSettings : ISettings
{
    [Required]
    public string ConnectionString { get; set; }
    [Required]
    public string Container { get; set; }
    [Required]
    public string Path { get; set; }
}

extensions

public static class TypeExtensions
{
    public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
    {
        foreach (var it in givenType.GetInterfaces())
        {
            if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType)
                return true;
        }

        if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
            return true;

        Type baseType = givenType.BaseType;
        if (baseType == null) return false;

        return IsAssignableToGenericType(baseType, genericType);
    }
}

in program.cs

using (var scope = webHost.Services.CreateScope())
        {
            var services = scope.ServiceProvider;
            var logger = services.GetRequiredService<ILogger<Program>>();
            try
            {
                logger.LogInformation("Starting settings validation.");
                services.GetRequiredService<SettingsValidator>();
                logger.LogInformation("The settings have been validated.");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "An error occurred while validating the settings.");
            }
        }

Let me know if it works for you too :)

like image 22
Cedric Arnould Avatar answered Oct 06 '22 01:10

Cedric Arnould