- Modern Web Development with ASP.NET Core 3
- Ricardo Peres
- 2642字
- 2021-06-18 18:35:55
Providers
The available Microsoft configuration providers (and their NuGet packages) are as follows:
- JavaScript Object Notation (JSON) files: Microsoft.Extensions.Configuration.Json
- XML files: Microsoft.Extensions.Configuration.Xml
- Initialization (INI) files: Microsoft.Extensions.Configuration.Ini
- User secrets: Microsoft.Extensions.Configuration.UserSecrets
- Azure Key Vault: Microsoft.Extensions.Configuration.AzureKeyVault
- Environment variables: Microsoft.Extensions.Configuration.EnvironmentVariables
- Command line: Microsoft.Extensions.Configuration.CommandLine
- Memory: Microsoft.Extensions.Configuration
- Docker secrets: Microsoft.Extensions.Configuration.DockerSecrets
When you reference these packages, you automatically make their extensions available. So, for example, if you want to add the JSON provider, you have two options, detailed next.
You can add a JsonConfigurationSource directly, like this:
var jsonSource = new JsonConfigurationSource {
Path = "appsettings.json" }; builder.Add(jsonSource);
Alternatively, you can use the AddJsonFile extension method, like this:
builder.AddJsonFile("appsettings.json");
Most likely, the extension methods are what you need. As I said, you can have any number of providers at the same time, as illustrated in the following code snippet:
builder .AddJsonFile("appsettings.json") .AddEnvironmentVariables() .AddXmlFile("web.config");
You just need to keep in mind that if two providers return the same configuration setting, the order by which they were added matters; the result you get will come from the last provider added, as it will override the previous ones. So, for example, imagine you are adding two JSON configuration files, one that is common across all environments (development, staging, and production), and another for a specific environment; in this case, you would likely have the following:
builder .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json");
Each provider will, of course, feature different properties for setting up; all file-based providers will require, for instance, a file path, but that doesn't make sense when we're talking about environment variables.
File-based providers
Both JSON, XML, and INI configuration sources are based on files. Therefore, their classes inherit from the FileConfigurationSource abstract base class. This class offers the following configuration properties:
- Path: The actual, fully qualified physical path where the file is to be found; this is a required setting.
- Optional: A Boolean flag for specifying whether the absence of the file causes a runtime error (false) or not (true); the default is false.
- ReloadOnChange: Here, you decide whether to automatically detect changes to the source file (true) or not (false); the default is false.
- ReloadDelay: The delay, in milliseconds, before reloading the file in the event that a change was detected (ReloadOnChange set to true); the default is 250 milliseconds.
- OnLoadException: A delegate to be called should an error occur while parsing the source file; this is empty by default.
- FileProvider: The file provider that actually retrieves the file; the default is an instance of PhysicalFileProvider, set with the folder of the Path property.
All of the extension methods allow you to supply values for each of these properties, except OnLoadException. You are also free to specify your own concrete implementation of IFileProvider, which you should do if you have specific needs, such as getting files from inside a ZIP file. ConfigurationBuilder has an extension method, SetBasePath, that sets a default PhysicalFileProvider pointing to a folder on your filesystem so that you can pass relative file paths to the configuration source's Path property.
If you set ReloadOnChange to true, .NET Core will start an operating system-specific file that monitors a watch on the source file; because these things come with a cost, try not to have many watches.
A typical example would be as follows:
builder .SetBasePath(@"C:\Configuration") .AddJsonFile(path: "appsettings.json", optional: false,
reloadOnChange: true) .AddJsonFile(path: $"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: true);
This would result in the appsettings.json file being loaded from the C:\Configuration folder (and throwing an exception if it is not present), and then loading appsettings.Development.json (this time, ignoring it if the file doesn't exist). Whenever there's a change in either file, they are reloaded and the configuration is updated.
If, however, we wanted to add an error handler, we need to add the configuration source manually, as follows:
var jsonSource = new JsonConfigurationSource { Path = "filename.json" }; jsonSource.OnLoadException = (x) => { if (x.Exception is FileNotFoundException ex) { Console.Out.WriteLine($"File {ex.FileName} not found"); x.Ignore = true; } }; builder.Add(jsonSource);
This way, we can prevent certain errors from crashing our application.
All file-based providers are added by an extension method with the name AddxxxFile, where xxx is the actual type—Json, Xml, or Ini—and always takes the same parameters (path, optional, and reloadOnChange).
JSON provider
We typically add a JSON configuration file using the AddJsonFile extension method. The JSON provider will load a file containing JSON contents and make its structure available for configuration, using dotted notation. A typical example is shown in the following code snippet:
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)mssqllocaldb;
Database=aspnetcore" } }
Any valid JSON content will work. As of now, it is not possible to specify a schema. Sections are just sub-elements of the JSON content.
An example of code used to load a configuration value would be as follows:
var defaultConnection = cfg["ConnectionStrings:DefaultConnection"];
XML provider
XML is becoming less and less common, with JSON, inversely, becoming increasingly popular; however, there are still good reasons to use XML. So, we add an XML file using the AddXmlFile extension method, and as far as configuration is concerned, we need to wrap our XML contents in a settings node; the XML declaration is optional. Refer to the following example:
<settings Flag="2"> <MySettings> <Option>10</Option> </MySettings> </settings>
Again, as of now, it is not possible to specify a validating schema. With this provider, sections are implemented as sub-elements.
Two examples of this are as follows:
var flag = cfg["Flag"]; var option = cfg["MySettings:Option"];
INI provider
INI files are a thing of the past, but, for historical reasons, Microsoft is still supporting them (actually, Linux also makes use of INI files too). In case you're not familiar with its syntax, this is what it looks like:
[SectionA] Option1=Value1 Option2=Value2 [SectionB] Option1=Value3
You add INI files to the configuration through the AddIniFile extension method.
Sections in INI files just map to the intrinsic sections provided by the INI file specification.
A single example is as follows:
var optionB2 = cfg["SectionB:Option1"];
Other providers
Besides file-based providers, there are other ways to store and retrieve configuration information. Here, we list the currently available options in .NET Core.
User secrets
.NET Core introduced user secrets as a means of storing sensitive information per user. The benefit of this is that it is kept in a secure manner, out of configuration files, and is not visible by other users. A user secrets store is identified (for a given user) by userSecretsId, which the Visual Studio template initializes as a mix of a string and a globally unique identifier (GUID), such as aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377.
Secrets in a store can be listed, added, or removed through the dotnet executable, as illustrated in the following code snippet:
dotnet user-secrets list --lists all the values in the
store dotnet user-secrets set "key" "value" --set "key" to be "value" dotnet user-secrets remove "key" --remove entry for "key" dotnet user-secrets clear --remove all entries
You will need the Microsoft.Extensions.SecretManager.Tools package. The dotnet user-secrets command will only work when in the presence of a project file that specifies the userSecretsId store ID. The AddUserSecrets extension method is what we use to add user secrets to the configuration, and it will either pick up this userSecretsIdsetting automatically, or you can provide your own at runtime, as follows:
builder.AddUserSecrets(userSecretdId: "[User Secrets Id]");
Another option is to get the user secrets ID from an assembly, in which case this needs to be decorated with the UserSecretsIdAttribute attribute, as follows:
[assembly: UserSecretsId("aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377")
In this case, the way to load it is demonstrated in the following code snippet:
builder.AddUserSecrets<Startup>();
Yet another way to specify user secrets (in ASP.NET Core 2.x) is through the .csproj file, by using a UserSecretsId element, as illustrated in the following code snippet:
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>9094c8e7-0000-0000-0000-c26798dc18d2</UserSecretsId>
</PropertyGroup>
Regardless of how you specify the user secrets ID, as with all the other providers, the way to load a value is as follows:
var value = cfg["key"];
In case you are interested, you can read more about .NET Core user secrets here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets
Azure Key Vault
Azure Key Vault is an Azure service that you can leverage for enterprise-level secure key-value storage. The full description is outside the scope of this book, but you can read about it here: https://azure.microsoft.com/en-us/services/key-vault. Suffice to say that you add the Azure Key Vault provider through the AddAzureKeyVault extension method, as depicted in this line of code:
builder.AddAzureKeyVault(vault: "https://[Vault].vault.azure.net/",
clientId: "[Client ID]", clientSecret: "[Client Secret]");
After this, all are added to the configuration object, and you can retrieve them in the usual way.
Command line
Another very popular way to get configuration settings is the command line. Executables regularly expect information to be passed in the command line, so as to dictate what should be done or to control how it should happen.
The extension method to use is AddCommandLine, and it expects a required and an optional parameter, as follows:
builder.AddCommandLine(args: Environment.GetCommandLineArgs().Skip(1).ToArray());
The args parameter will typically come from Environment.GetCommandLineArgs(), and we take the first parameter out, as this is the entry assembly's name. If we are building our configuration object in Program.Main, we can use its args parameter too.
Now, there are several ways to specify parameters. One way is illustrated in the following code snippet:
Key1=Value1 --Key2=Value2 /Key3=Value3 --Key4 Value4 /Key5 Value5
Here is another example:
dotnet run MyProject Key1=Value1 --Key2=Value2 /Key3=Value3 --Key4 Value4 /Key5 Value5
If the value has spaces in it, you need to wrap it in quotes ("). You can't use - (single dash), as this would be interpreted as a parameter to dotnet instead.
The optional parameter to AddCommandLine, switchMappings, is a dictionary that can be used to create new keys that will duplicate those from the command line, as follows:
var switchMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "--Key1", "AnotherKey" } }; builder.AddCommandLine(
args: Environment.GetCommandLineArgs().Skip(1).ToArray(),
switchMappings: switchMappings);
These keys can even have special characters in them—for example, --a:key and /some.key are valid keys.
Again, use the same syntax to retrieve their values.
Environment variables
Environment variables exist in all operating systems and can also be regarded as a source of configuration. Many tools out there, such as Docker, rely on environment variables for getting their operating context.
Adding environment variables to a .NET Core configuration is straightforward; you just need to call AddEnvironmentVariables. By default, this will bring all the existing environment variables into the configuration, but we can also specify a prefix, and filter out all variables that do not start with it, as follows:
builder.AddEnvironmentVariables(prefix: "ASPNET_");
So, this will add both ASPNET_TargetHost and ASPNET_TargetPort, but not PATH or COMPUTERNAME.
Sections are supported if you separate names with double underscores (for example, __). For example, say you have this environment variable:
ASPNETCORE__ADMINGROUP__USERS=rjperes,pm
You could access the ADMINGROUP section like this:
var group = cfg
.GetSection("ASPNETCORE")
.GetSection("ADMINGROUP");
var users = group["USERS"];
Memory
The memory provider is a convenient way of specifying values dynamically at runtime and for using dictionary objects. We add the provider with the AddInMemoryCollection extension method, as follows:
var properties = new Dictionary<string, string> { { "key", "value" } }; builder.AddInMemoryCollection(properties);
The advantage of this approach is that it is easy to populate a dictionary with whatever values we want, particularly in unit tests.
Docker
The ability to have secrets coming from Docker-stored files is relatively new in .NET Core. Basically, it will try to load text files in a specific directory inside a Docker instance as the values where the key is the filename itself. This is an actual feature of Docker, about which you can read more here: https://docs.docker.com/engine/swarm/secrets
The AddDockerSecrets extension method takes two optional parameters—the user secrets directory and whether or not this directory itself is optional; in other words, just ignore it if it's not there. This is illustrated in the following code snippet:
builder.AddDockerSecrets(secretsPath: "/var/lib/secrets", optional: true);
It is possible to specify these two parameters plus an ignore prefix and a delegate for filtering out files by their names if we use the overload that takes a configuration object, as illustrated in the following code block:
builder.AddDockerSecrets(opt => { opt.SecretsDirectory = "/var/lib/secrets"; opt.Optional = true; opt.IgnorePrefix = "ignore."; opt.IgnoreCondition = (filename) => !filename.Contains($".{env.EnvironmentName}."); });
Here, we are filtering out both files starting with ignore., as well as those that do not contain the current environment name (for example, .Development.). Pretty cool!
Default providers
The ASP.NET Core code included in the default application templates (WebHostBuilder.CreateDefaultBuilder) registers the following providers:
- JSON
- Environment
- Command line
- User secrets
Of course, you can add new providers to the configuration builder to match your needs. Next, we will see how we can create a custom provider for specific configuration needs.
Creating a custom provider
Although we have several options for storing configuration values, you may have your own specific needs. For example, if you are using Windows, you might want to store your configuration settings in the Registry. For that, you need a custom provider. Let's see how we can build one.
First, you need to add the Microsoft.Win32.Registry NuGet package to your project. Then, we start by implementing IConfigurationSource, as follows:
public sealed class RegistryConfigurationSource : IConfigurationSource { public RegistryHive Hive { get; set; } = RegistryHive.CurrentUser; public IConfigurationProvider Build(IConfigurationBuilder builder) { return new RegistryConfigurationProvider(this); } }
As you can see from the preceding code block, the only configurable property is Hive, by means of which you can specify a specific Registry hive, with CurrentUser (HKEY_CURRENT_USER) being the default.
Next, we need an IConfigurationProvider implementation. Let's inherit from the ConfigurationProvider class, as this takes care of some of the basic implementations, such as reloading (which we do not support as we go directly to the source). The code can be seen here:
public sealed class RegistryConfigurationProvider : ConfigurationProvider { private readonly RegistryConfigurationSource _configurationSource; public RegistryConfigurationProvider(
RegistryConfigurationSource configurationSource) { _configurationSource = configurationSource; } private RegistryKey GetRegistryKey(string key) { RegistryKey regKey; switch (_configurationSource.Hive) { case RegistryHive.ClassesRoot: regKey = Registry.ClassesRoot; break; case RegistryHive.CurrentConfig: regKey = Registry.CurrentConfig; break; case RegistryHive.CurrentUser: regKey = Registry.CurrentUser; break; case RegistryHive.LocalMachine: regKey = Registry.LocalMachine; break; case RegistryHive.PerformanceData: regKey = Registry.PerformanceData; break; case RegistryHive.Users: regKey = Registry.Users; break; default: throw new InvalidOperationException($"Supplied hive
{_configurationSource.Hive} is invalid."); } var parts = key.Split('\\'); var subKey = string.Join("", parts.Where(
(x, i) => i < parts.Length - 1)); return regKey.OpenSubKey(subKey); } public override bool TryGet(string key, out string value) { var regKey = this.GetRegistryKey(key); var parts = key.Split('\\'); var name = parts.Last(); var regValue = regKey.GetValue(name); value = regValue?.ToString(); return regValue != null; } public override void Set(string key, string value) { var regKey = this.GetRegistryKey(key); var parts = key.Split(''); var name = parts.Last(); regKey.SetValue(name, value); } }
This provider class leverages the Registry API to retrieve values from the Windows Registry, which, of course, will not work on non-Windows machines. The TryGet and Set methods, defined in the ConfigurationProvider class, both delegate to the private GetRegistryKey method, which retrieves a key-value pair from the Registry.
Finally, let's add a friendly extension method to make registration simpler, as follows:
public static class RegistryConfigurationExtensions { public static IConfigurationBuilder AddRegistry(
this IConfigurationBuilder builder, RegistryHive hive = RegistryHive.CurrentUser) { return builder.Add(new RegistryConfigurationSource { Hive = hive }); } }
Now, you can use this provider, as follows:
builder .AddJsonFile("appsettings.json") .AddRegistry(RegistryHive.LocalMachine);
Nice and easy, don't you think? Now, let's see how we can use the configuration files for the providers that we registered.
- Instant Zepto.js
- 碼上行動:零基礎學會Python編程(ChatGPT版)
- Cassandra Design Patterns(Second Edition)
- Effective Python Penetration Testing
- 微信公眾平臺開發:從零基礎到ThinkPHP5高性能框架實踐
- Expert Android Programming
- Java編程技術與項目實戰(第2版)
- Julia 1.0 Programming Complete Reference Guide
- PrimeFaces Blueprints
- 零基礎看圖學ScratchJr:少兒趣味編程(全彩大字版)
- Python面試通關寶典
- HTML5與CSS3權威指南
- Node.js實戰:分布式系統中的后端服務開發
- 計算機系統解密:從理解計算機到編寫高效代碼
- Java EE實用教程