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):
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