The idea is to create a strongly typed application configuration by constructing concrete class and get the values populated from the database so that it is easier to change. This would allow admin to directly modify DB values for configuration.
So, let's start with what we need.
Application setting classes as needed
Classes for application setting. It is just a POCO implementation.
/// <summary>
/// Entire application settings
/// </summary>
public class Setting
{
/// <summary>
/// Gets or sets the setting version.
/// </summary>
/// <value>
/// The setting version.
/// </value>
public int SettingVersion { get; set; }
/// <summary>
/// Gets or sets the name of the application.
/// </summary>
/// <value>
/// The name of the application.
/// </value>
public string ApplicationName { get; set; }
/// <summary>
/// Gets or sets the LDAP.
/// </summary>
/// <value>
/// The LDAP.
/// </value>
public LdapSetting Ldap { get; set; }
}
/// <summary>
/// LDAP settings
/// </summary>
public class LdapSetting
{
/// <summary>
/// Gets or sets the domain host.
/// </summary>
/// <value>
/// The domain host.
/// </value>
public string DomainHost { get; set; }
/// <summary>
/// Gets or sets the name of the domain.
/// </summary>
/// <value>
/// The name of the domain.
/// </value>
public string DomainName { get; set; }
}
DB Model to hold above values
The entire thing would be saved as a key and value so we can create a simple DB model for same.
public class ConfigurationValue
{
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>
/// The identifier.
/// </value>
public int Id { get; set; }
/// <summary>
/// Gets or sets the key.
/// </summary>
/// <value>
/// The key.
/// </value>
public string Key { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
public string Value { get; set; }
}
public class MyAppContext
: DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="GemsContext"/> class.
/// </summary>
/// <param name="option">Database context options.</param>
public MyAppContext(DbContextOptions option)
: base(option)
{
}
/// <summary>
/// Gets or sets the configurations.
/// </summary>
/// <value>
/// The configurations.
/// </value>
public DbSet<ConfigurationValue> Configurations { get; set; }
}
Configuration Provider
The next thing is to create a provider for custom configuration to match with our requirement. This would basically read information from DB and set the values to class. Also, we can create default values if it does not exists.
EFConfigProvider
/// <summary>
/// EF configuration provider for the application.
/// </summary>
/// <seealso cref="ConfigurationProvider" />
public class EFConfigProvider
: ConfigurationProvider
{
/// <summary>
/// Gets or sets the name of the hosting environment.
/// </summary>
/// <value>
/// The name of the hosting environment.
/// </value>
private readonly string EnvironmentName;
/// <summary>
/// The default values for settings
/// </summary>
private Lazy<Dictionary<string, string>> DefaultValues;
/// <summary>
/// Initializes a new instance of the <see cref="EFConfigProvider"/> class.
/// </summary>
/// <param name="dbContextBuilderAction">The database context builder action.</param>
/// <param name="environmentName">Name of the hosting environment.</param>
public EFConfigProvider(Action<DbContextOptionsBuilder> dbContextBuilderAction, string environmentName)
{
DbContextBuilderAction = dbContextBuilderAction;
EnvironmentName = environmentName;
InitDefaultValues(environmentName);
}
/// <summary>
/// Gets the database context builder action.
/// </summary>
/// <value>
/// The database context builder action.
/// </value>
private Action<DbContextOptionsBuilder> DbContextBuilderAction { get; }
/// <summary>
/// Loads (or reloads) the data for this provider.
/// </summary>
public override void Load()
{
var builder = new DbContextOptionsBuilder<MyAppContext>();
DbContextBuilderAction(builder);
using (var dbContext = new MyAppContext(builder.Options))
{
dbContext.Database.EnsureCreated();
Data = !dbContext.Configurations.Any()
? CreateAndSaveDefaultValues(dbContext)
: EnsureAllSync(dbContext.Configurations.ToDictionary(c => c.Key, c => c.Value), dbContext);
}
}
/// <summary>
/// Creates the and save default values.
/// </summary>
/// <param name="dbContext">The database context.</param>
/// <returns>Data feeds for IOption.</returns>
private IDictionary<string, string> CreateAndSaveDefaultValues(
MyAppContext dbContext)
{
AddInDb(DefaultValues.Value, dbContext);
return DefaultValues.Value;
}
private void AddInDb(IDictionary<string, string> configurations, MyAppContext dbContext)
{
dbContext.Configurations.AddRange(configurations
.Select(kvp => new ConfigurationValue { Key = kvp.Key, Value = kvp.Value }));
dbContext.SaveChanges();
}
/// <summary>
/// Ensures all values are present.
/// </summary>
/// <param name="dbValues">The database values.</param>
/// <param name="dbContext">The database context.</param>
/// <returns>Synced dictionary.</returns>
private IDictionary<string, string> EnsureAllSync(IDictionary<string, string> dbValues, MyAppContext dbContext)
{
// Find item which does not exist in DB values, for anything added later stage.
var newItems = DefaultValues.Value.Where(item => !dbValues.Keys.Contains(item.Key))
.ToDictionary(item => item.Key, item => item.Value);
if (newItems.Any())
{
AddInDb(newItems, dbContext);
return dbValues
.Concat(newItems.Select(item => new KeyValuePair<string, string>(item.Key, item.Value)))
.ToDictionary(item => item.Key, item => item.Value);
}
return dbValues;
}
/// <summary>
/// Initializes the default values.
/// </summary>
/// <param name="environmentName">Name of the environment.</param>
private void InitDefaultValues(string environmentName)
{
// Can be done based on environment name.
DefaultValues = new Lazy<Dictionary<string, string>>(() =>
{
return new Dictionary<string, string>
{
{ nameof(Setting.SettingVersion), "1" },
{ nameof(Setting.ApplicationName), $"App - {environmentName}" },
{ $"{nameof(Setting.Ldap)}:{nameof(LdapSetting.DomainHost)}", "NA" },
{ $"{nameof(Setting.Ldap)}:{nameof(LdapSetting.DomainName)}", "NA" }
};
});
}
}
- EnvironmentName is just to know which hosting environment configuration belongs to like Development, Testing or whatever you might have configured.
- EnsureAllSync is for handling any new addition of configuration/properties added at a later stage. This would add into the database automatically.
- InitDefaultValues is to generate default values if records are not available in DB. Also, we can identify the environment and can have a different set of values based on environment. I would recommend using enum in that situation.
- You might be a bit skeptic about how the default values are used. It is basically required by dotnet itself to read as IDictionary, in which the configurations are put by using : deliminator. So, the text name has to be used based on the class property. I have used nameof operator to keep it strongly typed, magic strings are always a problem in refactoring.
Configuration Source
Now, the custom provider needs to be added into configuration source i:e IConfigurationSource, which would simply create an instance of the provider.
/// <summary>
/// Application Configuration source.
/// </summary>
/// <seealso cref="IConfigurationSource" />
public class EFConfigSource
: IConfigurationSource
{
/// <summary>
/// The database context builder action
/// </summary>
private readonly Action<DbContextOptionsBuilder> DbContextBuilderAction;
/// <summary>
/// The hosting environment name
/// </summary>
private readonly string EnvironmentName;
/// <summary>
/// Initializes a new instance of the <see cref="EFConfigSource"/> class.
/// </summary>
/// <param name="optionsAction">The options action.</param>
/// <param name="environmentName">Name of the hosting environment.</param>
public EFConfigSource(Action<DbContextOptionsBuilder> optionsAction, string environmentName)
{
DbContextBuilderAction = optionsAction;
EnvironmentName = environmentName;
}
/// <summary>
/// Builds the <see cref="T:Microsoft.Extensions.Configuration.IConfigurationProvider" /> for this source.
/// </summary>
/// <param name="builder">The <see cref="T:Microsoft.Extensions.Configuration.IConfigurationBuilder" />.</param>
/// <returns>
/// An <see cref="T:Microsoft.Extensions.Configuration.IConfigurationProvider" />
/// </returns>
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new EFConfigProvider(DbContextBuilderAction, EnvironmentName);
}
}
Final configuration on Startup.cs with help of some extension class
By doing above we have done most of the task, just we need to create extension class and register on startup.cs
Extension configuration that would be used on StartUp.cs
/// <summary>
/// EF configuration helper.
/// </summary>
public static class ExtensionEfConfig
{
/// <summary>
/// Adds the entity framework configuration.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="setup">The setup.</param>
/// <param name="environmentName">Name of the environment.</param>
/// <returns><see cref="IConfigurationBuilder"/> after creation of EF config provider.</returns>
public static IConfigurationBuilder AddEntityFrameworkConfig(
this IConfigurationBuilder builder, Action<DbContextOptionsBuilder> setup, string environmentName)
{
return builder.Add(new EFConfigSource(setup, environmentName));
}
/// <summary>
/// Adds the configurations.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="configuration">The configuration root.</param>
public static void AddConfigurations(this IServiceCollection services, IConfigurationRoot configuration)
{
services.Configure<Setting>(configuration);
services.Configure<LdapSetting>(configuration.GetSection(nameof(Setting.Ldap)));
}
}
In this, I have used two functions. The first one is to construct configuration builder and the second one is to get the class registered through IOptions.
You can see the both dependencies are registered by the master class and child class by getting a subsection of configuration.
Startup.cs
private readonly string SqlConnectionString;
public Startup(IHostingEnvironment env)
{
SqlConnectionString = $"SqlDatabase{env.EnvironmentName}";
Func<IConfigurationBuilder> getConfig = () =>
new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
var defaultConfig = getConfig().Build();
Configuration = getConfig()
.AddEntityFrameworkConfig(options =>
options.UseSqlServer(defaultConfig.GetConnectionString(SqlConnectionString)), env.EnvironmentName
).Build();
}
services.AddConfigurations(Configuration);
- The defaultConfig would get information from existing configurations available, and the second one is final one which would include DB values as well. Created a helper anonymous function for same.
- Now, DI for configuration is as simple as calling a function.
Usage
The last thing is how to use, we can have DI with IOptions to consume what we started with simple POCOs.
public class HomeController
: Controller
{
public HomeController(IOptions<LdapSetting> ldapSetting, IOptions<Setting> wholeSettings)
{
Debugger.Break();
}
}
Whole code base can be found on:
Comments
Post a Comment