In this article, we are going to create a custom configuration provider that reads our configuration from the database. We’ve seen how the default configuration providers work, and now we’re going to implement our own custom one.
For the custom configuration provider, we’ll use Entity Framework Core, coupled with the SQL Server database.
First, let’s upgrade our solution to support EF Core using the database-first approach.
Initializing EF Core
We need to install two Nuget packages first:
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer -v 3.1.7
We need this package since we’ll be using SQL Server instance, and:
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -v 3.1.7
Since we are going to perform an initial creation and migration of the database through the CLI.
We need a class that will contain our key-value configuration pairs (Models folder):
public class ConfigurationEntity { [Key] public string Key { get; set; } public string Value { get; set; } }
And a DbContext
class (Models folder):
public class ConfigurationDbContext : DbContext { public ConfigurationDbContext(DbContextOptions options) : base(options) { } public DbSet<ConfigurationEntity> ConfigurationEntities { get; set; } }
We need just one DbSet
of ConfigurationEntity
which will map to our table in the database.
Now we just need to set up a connection to our server in the ConfigureServices()
method in the Startup
class:
services.AddDbContext<ConfigurationDbContext>(opts => opts.UseSqlServer(Configuration.GetConnectionString("sqlConnection")));
And of course, you need to change the connection string in the appsettings.json
file to your database. If you’re using SqlExpress, it most probably looks like this:
"ConnectionStrings": { "sqlConnection": "server=.\\SQLEXPRESS; database=CodeMazeCommerce; Integrated Security=true" },
That’s it, now we can simply add an initial migration through the Package Manager Console:
PM> Add-Migration InitialSetup
And apply that migration to the database:
Update-Database
Now our database is created and ready to be used for storing configuration data.
Implementing a Custom EF Core Provider
To start things off let’s create a folder ConfigurationProviders inside our Models folder, in order to group our classes properly.
After that, we need to actually create a configuration provider by inheriting the ConfigurationProvider
class. We’ll create our own provider class in the ConfigurationProviders
folder and name it EFConfigurationProvider
:
public class EFConfigurationProvider : ConfigurationProvider { public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction) { OptionsAction = optionsAction; } Action<DbContextOptionsBuilder> OptionsAction { get; } public override void Load() { var builder = new DbContextOptionsBuilder<ConfigurationDbContext>(); OptionsAction(builder); using (var dbContext = new ConfigurationDbContext(builder.Options)) { dbContext.Database.EnsureCreated(); Data = !dbContext.ConfigurationEntities.Any() ? CreateAndSaveDefaultValues(dbContext) : dbContext.ConfigurationEntities.ToDictionary(c => c.Key, c => c.Value); } } private static IDictionary<string, string> CreateAndSaveDefaultValues(ConfigurationDbContext dbContext) { var configValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Pages:HomePage:WelcomeMessage", "Welcome to the ProjectConfigurationDemo Home Page" }, { "Pages:HomePage:ShowWelcomeMessage", "true" }, { "Pages:HomePage:Color", "black" }, { "Pages:HomePage:UseRandomTitleColor", "true" } }; dbContext.ConfigurationEntities.AddRange(configValues .Select(kvp => new ConfigurationEntity { Key = kvp.Key, Value = kvp.Value }) .ToArray()); dbContext.SaveChanges(); return configValues; } }
This class might look a bit scary at first, but it’s not that scary.
The constructor has one argument a delegate Action<DbContextOptionsBuilder> optionsAction
. We’ll use the DbContextOptionsBuilder
class later to define a context for our database. We’ve already done it when we defined the connection string previously. We’re exposing the context options builder, in order to provide that option to our custom provider.
We’re overriding the Load()
method in order to populate our ConfigurationEntity
with the data from the database or create a few default ones if the database table is empty. That’s all there is to it.
Next, we’re going to register our configuration provider as a source. In order to do that, we need to implement the IConfigurationSource
interface. So let’s create the EFConfigurationSource
class in the ConfigurationProviders
folder:
public class EFConfigurationSource : IConfigurationSource { private readonly Action<DbContextOptionsBuilder> _optionsAction; public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction) { _optionsAction = optionsAction; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new EFConfigurationProvider(_optionsAction); } }
We just need to implement the Build()
method, which in our case initializes the configuration provided with the options that we’ve sent through the configuration source constructor.
This looks really confusing so let’s see how to add our database configuration provider to the list of the configuration sources. We’ll do it in a similar fashion as before:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .ConfigureAppConfiguration((hostingContext, configBuilder) => { var config = configBuilder.Build(); var configSource = new EFConfigurationSource(opts => opts.UseSqlServer(config.GetConnectionString("sqlConnection"))); configBuilder.Add(configSource); });
As you can see we’re building the configuration builder in order to get the IConfiguration
. We need it because our connection string is stored in the appsettings.json
file. Now we can create a configuration source with that connection string, and add it to the existing configuration sources with the configBuilder.Add()
method.
Now we want to clear the appsettings.json file a bit:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "ConnectionStrings": { "sqlConnection": "server=.\\SQLEXPRESS; database=CodeMazeCommerce; Integrated Security=true" }, "AllowedHosts": "*" }
We’ve removed the “Pages” section to make sure it’s being read from the database.
And we need to remove the AddDbContext()
method we’ve used before in the Startup class since it’s not needed anymore.
public void ConfigureServices(IServiceCollection services) { //remove!!! services.AddDbContext<ConfigurationDbContext>(opts => opts.UseSqlServer(Configuration.GetConnectionString("sqlConnection"))); ... }
Since we won’t need any migrations for this example, create a database called “CodeMazeCommerce” manually through your SQL Management Studio, or through the SQL Server Object Explorer.
That’s it, let’s run the application.
Running the Application
Now if we run the application, put a breakpoint in the Startup
class, and inspect the Configuration
object, we’ll find our configuration source:
Excellent.
If we inspect the database, we’ll see it’s populated:
Let’s continue the execution and see if our application is still working as expected:
It still works as it did previously! You can refresh the page a few times to make sure the color of the title still changes.
Conclusion
In this short article, we’ve seen how to implement our own custom configuration provider that reads the values from the database. In the next part, we’re going to learn how to protect our sensitive configuration values.
You can find other parts of this series on the ASP.NET Core Web API page.