JSON Configuration Transformation
Problem
Microsoft has changed how configuration works for net core applications (https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1) . In the past we would use a XML transformation to adjust our settings on the fly and/or at deployment time. With the migration to net core, we have to address how we want to work with deployments and our local environments.
Background
Web Applications (Web Api and/or MVC)
Microsoft suggests the use of CreateDefaultBuilder
. When invoked the class provides the default configuration in the following order:
ChainedConfigurationProvider
: Adds an existingIConfiguration
as a source. In the default configuration case, adds the host configuration and setting it as the first source for the app configuration.appsettings.json
using the JSON configuration provider.appsettings.Environment.json
using the JSON configuration provider.- App secrets when the app runs in the Development environment.
- Environment variables using the Environment Variables configuration provider.
- Command-line arguments using the Command-line configuration provider
This gives us the ability to derive settings via inheritance. For example you can force a settings change using the command line without modifying the appsettings.json
file. This pattern also allows for customized environments that are developer specific. For example a developer wants to use SQL server 2019 on a Linux container, and they have mapped the port to 1433 instead of port 1432, and/or a developer has a named instance version of SQL server.
Console Applications
Console applications can also benefit from this pattern, however this is done by including the providers for configuration as packages. For example:
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> </ItemGroup>
Recommendation
Though we can use environment variables, we are choosing to exclude that usage for the ODS/API as a default implementation; however, environment variables are available and can be utilized as a deployment mechanism. As a solution for development and we will favor the usage a specific locals settings file as follows:
This pattern should be used for all of the ODS/API tools.
appsettings.json
In this main file, we will have the settings setup for initdev and this file will be checked in. This file will be added to the .gitignore
file so that changes are not accidentally checked in. Test assemblies also will have their own specific appsettings.json file.
appsettings.local.json
This file is for customization for development overrides. The file also will be to the .gitignore
file so that it is not accidentally checked in. Power shell scripts will need to be updated to address the overrides used by the TestHarness etc. We will include a checked in appsettings.local.json.example that users can copy for usage. Documentation will be updated to include how to use this solution.
Connection Strings
The appsettings.json file has a predefined connection strings section. We have a couple of options to address the use case of multiple database support:
Option 1: ConnectionStringsByEngine
Define a new section named ConnectionStringsByEngine that would be a dictionary of connection strings keyed off the database engine. For example:
{ "ConnectionStringsByEngine": { "sqlServer": { "EdFi_Admin": "Server=(local); Database=EdFi_Admin; Trusted_Connection=True; Application Name=EdFi.Ods.WebApi;", "EdFi_Security": "Server=(local); Database=EdFi_Security; Trusted_Connection=True; Persist Security Info=True; Application Name=EdFi.Ods.WebApi;", "EdFi_Ods": "Server=(local); Database=EdFi_{0}; Trusted_Connection=True; Application Name=EdFi.Ods.WebApi;" }, "postgreSql": { "EdFi_Admin": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_Admin; Application Name=EdFi.Ods.WebApi;", "EdFi_Security": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_Security; Application Name=EdFi.Ods.WebApi;", "EdFi_Ods": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_{0}; Application Name=EdFi.Ods.WebApi;" } } }
We would need to modify the ConfigConnectionStringsProvider to build the dictionary based on DatabaseEngine.
The benefit of this solution, this makes extension more flexible without any future changes to the connection strings provider.
Option 2. Separate Sections
Define two sections for the connections strings. SqlServerConnectionStrings, and PostgreSqlConnectionStrings. For example:
{ "SqlServerConnectionStrings": { "EdFi_Admin": "Server=(local); Database=EdFi_Admin; Trusted_Connection=True; Application Name=EdFi.Ods.WebApi;", "EdFi_Security": "Server=(local); Database=EdFi_Security; Trusted_Connection=True; Persist Security Info=True; Application Name=EdFi.Ods.WebApi;", "EdFi_Ods": "Server=(local); Database=EdFi_{0}; Trusted_Connection=True; Application Name=EdFi.Ods.WebApi;" }, "PostgreSqlConnectionStrings": { "EdFi_Admin": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_Admin; Application Name=EdFi.Ods.WebApi;", "EdFi_Security": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_Security; Application Name=EdFi.Ods.WebApi;", "EdFi_Ods": "Host=localhost; Port=5432; Username=postgres; Database=EdFi_{0}; Application Name=EdFi.Ods.WebApi;" } }
The disadvantage to this solution is that we would need to modify ConfigConnectionStringsProvider whenever there is an extension to database engines.
Discussion/Decision 8/18/2020
After a discussion, we have decided to do the following:
- appsettings.json is required, and will have connection string keys with empty values, along with a production deployment setup.
- For Octopus, we can poke the file, with values we would like address via replacements.
- Connection strings will be generated via initdev process and stored in appsettings.local.json
- Will not go with option 1 or 2 but instead will use the standard ConnectionStrings section, but the generated connection string will be based off of engine.
- Engine will be written to appsettings.local.json
- Introduce an appsettings.edfi.json that will be checked that has the "developer setup". This file will contain enabled features and extensions for the developer experience.
Deployment Automation
Deployment automation processes will need to create the correct settings config file during deployment, instead of using the environment file(s) checked into source control. The deployment PowerShell scripts* will need to be updated to support generating the environment-specific config file (* or other replacement for PowerShell, for example Python).
In PowerShell, reading a JSON file into a proper object is more difficult than expected. It is easier to create a file from scratch, rather than opening and modifying a template. Example:
function New-ProductionAppSettingsFile { [CmdletBinding()] param ( [hashtable] [Parameter(Mandatory=$true)] $Config ) $connectionString = New-ConnectionString -ConnectionInfo $Config.DbConnectionInfo $prodSettings = @{ SqlServerConnectionStrings = @{ EdFi_Admin = $connectionString # Etc. } } $prodSettingsFile = Join-Path -Path $Config.PackageDirectory -ChildPath "appsettings.Production.json" $prodSettings | ConvertTo-Json | Out-File -FilePath $prodSettingsFile -NoNewline -Encoding UTF8 }
Function New-ConnectionString
comes from the EdFI.Installer.AppCommon package of PowerShell scripts: Configuration.psm1.