Search code examples
.net-coreconfigurationprovider.net-7.0

Loading Configuration with custom configuration provider always returns nulls


Hi I need to implement custom configuration provider that self updates it's still not dynamic enough but it will suffice for now.

Here is the example that I've been using:<br>
https://medium.com/@gokerakce/implement-a-custom-configuration-provider-in-net-7-c0a195dcd05f


Here is my code:

namespace Domain.Entities;

public class SettingsOptions  {
    public string DcaID { get; init; }
    public string UserID { get; init; }
    public string BookID { get; init; }
    public string Environment { get; init; }
    public string DcaName { get; init; }
    public bool IsCronActive { get; init; }
    public int Interval { get; init; }
}

[Table("config")]
public class Settings {
    public Settings(string key, string value) {
        Key = key;
        Value = value;
    }
    [Key] public string Key { get; set; }
    public string Value { get; set; }
}

Provider Source:

public class EntityConfigurationSource : IConfigurationSource {
        public required Action<DbContextOptionsBuilder> OptionsAction { get; init; }
    
        public bool ReloadOnChange { get; init; }
    
        public int ReloadDelay { get; init; } = 10;
    
        public int PeriodInSeconds { get; init; } = 5;
    
        public IConfigurationProvider Build(IConfigurationBuilder builder) {
            return new EntityConfigurationProvider(this);
        }
    }

Here's how I Provide it:

namespace Infrastructure.Common.Providers.EntityConfigurationSource;

public class EntityConfigurationProvider : ConfigurationProvider, IDisposable {
    private readonly EntityConfigurationSource _source;
    private readonly Timer? _timer;

    public EntityConfigurationProvider(EntityConfigurationSource source) {
        _source = source;

        if (_source.ReloadOnChange) {
            _timer = new Timer
            (
                callback: ReloadSettings,
                dueTime: TimeSpan.FromSeconds(_source.ReloadDelay),
                period: TimeSpan.FromSeconds(_source.PeriodInSeconds),
                state: null
            );
        }
    }

    public override void Load() {
        var builder = new DbContextOptionsBuilder<EntityConfigurationDbContext>();
        _source.OptionsAction(builder);

        using (var dbContext = new EntityConfigurationDbContext(builder.Options)) {
            Data = dbContext.Settings.Any()
                ? dbContext.Settings.ToDictionary<Settings, string, string?>(c => c.Key, c => c.Value,
                    StringComparer.OrdinalIgnoreCase)
                : CreateAndSaveDefaultValues(dbContext);
        }
    }

    private void ReloadSettings(object? state) {
        Load();
        OnReload();
    }

    static IDictionary<string, string?> CreateAndSaveDefaultValues(EntityConfigurationDbContext context) {
        // setting default values for now, until on release than it would be empty or zeroed inputs.
        // after running the service the deploy script should initiate it with concrete values instead of mocks.
        var settings = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase) {
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.DcaID)}"] = "123",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.UserID)}"] = "123",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.BookID)}"] = "123",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.DcaName)}"] = "xxx",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.Environment)}"] = "Development",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.Interval)}"] = "10",
            [$"{nameof(SettingsOptions)}:{nameof(SettingsOptions.IsCronActive)}"] = "true",
        };
        context.Settings.AddRange(
            settings.Select(kvp => new Settings(kvp.Key, kvp.Value!)).ToArray());

        context.SaveChanges();
        return settings;
    }

    public void Dispose() {
        _timer?.Dispose();
    }
}

Here's the DbContext

namespace Infrastructure.Common.Providers.EntityConfigurationSource;

public class EntityConfigurationDbContext : DbContext, IEntityConfigurationContext {
    public DbSet<Settings> Settings => Set<Settings>();

    public EntityConfigurationDbContext(DbContextOptions<EntityConfigurationDbContext> options) : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        base.OnModelCreating(modelBuilder);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
        optionsBuilder.LogTo(
            action: Console.WriteLine,
            minimumLevel: LogLevel.Information
        );
        base.OnConfiguring(optionsBuilder);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) {
        return await base.SaveChangesAsync(cancellationToken);
    }
}

Usage:

   public CreateUploadFileListCommandHandler(
      ILogger<CreateUploadFileListCommandHandler> logger,
      IOptionsMonitor<SettingsOptions> settingsOptions) {
      _logger = logger;
      _settingsOptions = settingsOptions; 
   }
    

Here's how I attach it to configuration pipeline:

 public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration, IConfigurationBuilder configurationBuilder) {

}
    namespace Infrastructure.Common.Helpers;
    
    public static class ConfigurationProviderExtensions {
        public static IConfigurationBuilder AddCustomDbConfiguration(this IConfigurationBuilder builder,
            Action<DbContextOptionsBuilder> optionsAction) {
            return builder.Add(new EntityConfigurationSource {
                OptionsAction = optionsAction,
                ReloadOnChange = true,
                PeriodInSeconds = 60, // for debug purposes
            });
        }
    }

Program.cs
var builder = WebApplication.CreateBuilder(args);

var configuration = new ConfigurationBuilder()
    .SetBasePath(builder.Environment.ContentRootPath)
    .AddEnvironmentVariables()
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
    .AddUserSecrets<Program>()
    .Build();

// Add services to the container.
builder.Services
    .AddInfrastructure(configuration, builder.Configuration)

Here is the result: (seems that there's a bug when adding an image here):

enter image description here


Solution

  • This inside Program.cs file loaded the data inside the model:

    var configuration = new ConfigurationBuilder()
        .SetBasePath(builder.Environment.ContentRootPath)
        .AddEnvironmentVariables()
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true)
        .AddUserSecrets<Program>()
        .Build();
    
    // this line
    builder.Services.Configure<SettingsOptions>(builder.Configuration.GetSection(nameof(SettingsOptions)));
    

    Seems like something inside configuration