Configuration providers are an excellent mechanism for avoiding hardcoding data in our application. The idea is that instead of having configuration data in C# code, it’s better to have it in certain external locations, such as JSON files or environment variables.
However, sometimes we may need to create a custom configuration provider. Such is the case of a student who wrote to me asking for an implementation that connects to a SQL Server database. In this sense, when running the app, a query must be made to a configuration table to have those configurations available at runtime. We also want this information to be updated every X amount of time.
That’s what we are going to do in this post.
What We Have
We’re going to implement this in an ASP.NET Core app with .NET 9. We’ll also be using Entity Framework Core. However, what we’ll be doing will be adaptable to any other data access technology, such as Dapper or ADO.NET.
I have an entity that represents the table where I’ll store the configurations:
public class Configuration
{
[Key]
public required string Key { get; set; }
public required string Value { get; set; }
}
I configure EF Core so that I can use a factory to get an instance of the DbContext:
builder.Services.AddDbContextFactory<ApplicationDbContext>(opciones => opciones.UseSqlServer("name=DefaultConnection"));
With this, we’ll see that we basically want to be able to use the IConfiguration to access a value found in the database. For example, we have the following endpoint:
app.MapGet("/get-config-data", (IConfiguration configuration) =>
{
return configuration["my-key"];
});
Custom Configuration Provider
To do this, we need to set up a custom configuration provider. First, we need to create a class that will connect to our database and take the data from the Configurations table and place it in a dictionary, which the framework will then use at runtime to retrieve values like “my-key.” We also want this data to be refreshed every X amount of time, which we can do with a Timer. In the end, this is the implementation of the class:
public class DbConfigurationProvider: ConfigurationProvider, IDisposable
{
private readonly IDbContextFactory<ApplicationDbContext> dbFactory;
private Timer timer;
public DbConfigurationProvider(IDbContextFactory<ApplicationDbContext> dbFactory)
{
this.dbFactory = dbFactory;
var interval = TimeSpan.FromSeconds(5);
timer = new System.Threading.Timer(AutoRefresh, null, interval, interval);
}
private void AutoRefresh(object? state)
{
Load();
OnReload();
}
public override void Load()
{
var context = dbFactory.CreateDbContext();
var data = new Dictionary<string, string>();
var configurations = context.Configurations.ToList();
foreach (var configuration in configurations)
{
data[configuration.Key] = configuration.Value;
}
Data = data!;
}
public void Dispose()
{
timer?.Dispose();
}
}
Note: The OnReload method is used to notify the framework that we have refreshed the configuration provider data. The Data property is the dictionary that feeds the configuration provider system.
Note 2: For the Timer, I used a short time of 5 seconds simply for testing. In real life, you may want to use a slightly longer time. Remember that 5 seconds means that every 5 seconds we will be querying the Configurations table. Of course, it’s a fast operation, as it’s a query without joins or anything like that.
Now, we configure a Source:
public class DbConfigurationSource : IConfigurationSource
{
private readonly IDbContextFactory<ApplicationDbContext> dbFactory;
public DbConfigurationSource(IDbContextFactory<ApplicationDbContext> dbFactory)
{
this.dbFactory = dbFactory;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new DbConfigurationProvider(dbFactory);
}
}
Then, we create an extension method that we’ll use in the Program class:
public static class DbConfigurationExtensions
{
public static IConfigurationBuilder AddDatabaseConfiguration(
this IConfigurationBuilder builder,
IDbContextFactory<ApplicationDbContext> dbFactory)
{
return builder.Add(new DbConfigurationSource(dbFactory));
}
}
With this, we can use the AddDatabaseConfiguration method to configure our custom configuration provider. Since we want to pass an instance of the IDbContextFactory service, we’ll create a Scope in the program class. Therefore, we’ll do the following:
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>();
builder.Configuration.AddDatabaseConfiguration(dbFactory);
}
With this, when we use the IConfiguration, the keys located in our SQL Server Configurations table will also be taken into account.
Learn More
If you want to learn more about developing Web APIs using Minimal APIs, I have two courses for you (both with a discount included):
Complete course on Minimal APIs with EF Core: https://felipe-gavilan.azurewebsites.net/api/Redireccion?curso=minimal-ef-eng
Complete course on Minimal APIs and Dapper: https://felipe-gavilan.azurewebsites.net/api/Redireccion?curso=minimal-dapper-eng
Kind regards!