netcore3.0 IConfiguration配置源码解析(二)
上一篇主要讲到netcore配置的基本原理,这篇文章主要分析下netcore有哪些具体的配置源
一、 环境变量:EnvironmentVariablesConfigurationSource和EnvironmentVariablesConfigurationProvider
该配置源主要获取系统的环境变量配置,很简单的实现。
public class EnvironmentVariablesConfigurationSource : IConfigurationSource { /// <summary> /// A prefix used to filter environment variables. /// </summary> public string Prefix { get; set; } /// <summary> /// Builds the <see cref="EnvironmentVariablesConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="EnvironmentVariablesConfigurationProvider"/></returns> public IConfigurationProvider Build(IConfigurationBuilder builder) { return new EnvironmentVariablesConfigurationProvider(Prefix); } }
EnvironmentVariablesConfigurationSource类的实现很简单,有个前缀的Prefix 属性,然后构建EnvironmentVariablesConfigurationProvider
public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider { private const string MySqlServerPrefix = "MYSQLCONNSTR_"; private const string SqlAzureServerPrefix = "SQLAZURECONNSTR_"; private const string SqlServerPrefix = "SQLCONNSTR_"; private const string CustomPrefix = "CUSTOMCONNSTR_"; private const string ConnStrKeyFormat = "ConnectionStrings:{0}"; private const string ProviderKeyFormat = "ConnectionStrings:{0}_ProviderName"; private readonly string _prefix; /// <summary> /// Initializes a new instance. /// </summary> public EnvironmentVariablesConfigurationProvider() : this(string.Empty) { } /// <summary> /// Initializes a new instance with the specified prefix. /// </summary> /// <param name="prefix">A prefix used to filter the environment variables.</param> public EnvironmentVariablesConfigurationProvider(string prefix) { _prefix = prefix ?? string.Empty; } /// <summary> /// Loads the environment variables. /// </summary> public override void Load() { Load(Environment.GetEnvironmentVariables()); } internal void Load(IDictionary envVariables) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var filteredEnvVariables = envVariables .Cast<DictionaryEntry>() .SelectMany(AzureEnvToAppEnv) .Where(entry => ((string)entry.Key).StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)); foreach (var envVariable in filteredEnvVariables) { var key = ((string)envVariable.Key).Substring(_prefix.Length); data[key] = (string)envVariable.Value; } Data = data; } private static string NormalizeKey(string key) { return key.Replace("__", ConfigurationPath.KeyDelimiter); } private static IEnumerable<DictionaryEntry> AzureEnvToAppEnv(DictionaryEntry entry) { var key = (string)entry.Key; var prefix = string.Empty; var provider = string.Empty; if (key.StartsWith(MySqlServerPrefix, StringComparison.OrdinalIgnoreCase)) { prefix = MySqlServerPrefix; provider = "MySql.Data.MySqlClient"; } else if (key.StartsWith(SqlAzureServerPrefix, StringComparison.OrdinalIgnoreCase)) { prefix = SqlAzureServerPrefix; provider = "System.Data.SqlClient"; } else if (key.StartsWith(SqlServerPrefix, StringComparison.OrdinalIgnoreCase)) { prefix = SqlServerPrefix; provider = "System.Data.SqlClient"; } else if (key.StartsWith(CustomPrefix, StringComparison.OrdinalIgnoreCase)) { prefix = CustomPrefix; } else { entry.Key = NormalizeKey(key); yield return entry; yield break; } // Return the key-value pair for connection string yield return new DictionaryEntry( string.Format(ConnStrKeyFormat, NormalizeKey(key.Substring(prefix.Length))), entry.Value); if (!string.IsNullOrEmpty(provider)) { // Return the key-value pair for provider name yield return new DictionaryEntry( string.Format(ProviderKeyFormat, NormalizeKey(key.Substring(prefix.Length))), provider); } } }
EnvironmentVariablesConfigurationProvider继承了抽象类ConfigurationProvider,重写了Load方法
该方法其实调用了Environment.GetEnvironmentVariables()方法获取系统的环境变量,把环境变量里面的数据加载到Data属性中。
IConfigurationBuilder的扩展方法AddEnvironmentVariables就是添加环境变量数据源。
二、 命令行配置:CommandLineConfigurationSource和CommandLineConfigurationProvider
public class CommandLineConfigurationSource : IConfigurationSource { /// <summary> /// Gets or sets the switch mappings. /// </summary> public IDictionary<string, string> SwitchMappings { get; set; } /// <summary> /// Gets or sets the command line args. /// </summary> public IEnumerable<string> Args { get; set; } /// <summary> /// Builds the <see cref="CommandLineConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="CommandLineConfigurationProvider"/></returns> public IConfigurationProvider Build(IConfigurationBuilder builder) { return new CommandLineConfigurationProvider(Args, SwitchMappings); } }
其中属性Args就是命令行传递过来的数据,命令行参数的数据格式:
key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5
这种格式都可以把配置键值对解析出来
SwitchMappings属性是把命令行的key转换成其他key的名称
扩展方法AddCommandLine添加命令行的配置源
三、文件配置源FileConfigurationSource和FileConfigurationProvider
public abstract class FileConfigurationSource : IConfigurationSource { /// <summary> /// Used to access the contents of the file. /// </summary> public IFileProvider FileProvider { get; set; } /// <summary> /// The path to the file. /// </summary> public string Path { get; set; } /// <summary> /// Determines if loading the file is optional. /// </summary> public bool Optional { get; set; } /// <summary> /// Determines whether the source will be loaded if the underlying file changes. /// </summary> public bool ReloadOnChange { get; set; } /// <summary> /// Number of milliseconds that reload will wait before calling Load. This helps /// avoid triggering reload before a file is completely written. Default is 250. /// </summary> public int ReloadDelay { get; set; } = 250; /// <summary> /// Will be called if an uncaught exception occurs in FileConfigurationProvider.Load. /// </summary> public Action<FileLoadExceptionContext> OnLoadException { get; set; } /// <summary> /// Builds the <see cref="IConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="IConfigurationProvider"/></returns> public abstract IConfigurationProvider Build(IConfigurationBuilder builder); /// <summary> /// Called to use any default settings on the builder like the FileProvider or FileLoadExceptionHandler. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> public void EnsureDefaults(IConfigurationBuilder builder) { FileProvider = FileProvider ?? builder.GetFileProvider(); OnLoadException = OnLoadException ?? builder.GetFileLoadExceptionHandler(); } /// <summary> /// If no file provider has been set, for absolute Path, this will creates a physical file provider /// for the nearest existing directory. /// </summary> public void ResolveFileProvider() { if (FileProvider == null && !string.IsNullOrEmpty(Path) && System.IO.Path.IsPathRooted(Path)) { var directory = System.IO.Path.GetDirectoryName(Path); var pathToFile = System.IO.Path.GetFileName(Path); while (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { pathToFile = System.IO.Path.Combine(System.IO.Path.GetFileName(directory), pathToFile); directory = System.IO.Path.GetDirectoryName(directory); } if (Directory.Exists(directory)) { FileProvider = new PhysicalFileProvider(directory); Path = pathToFile; } } } }
public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable { private readonly IDisposable _changeTokenRegistration; /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public FileConfigurationProvider(FileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( () => Source.FileProvider.Watch(Source.Path), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } } /// <summary> /// The source settings for this provider. /// </summary> public FileConfigurationSource Source { get; } /// <summary> /// Generates a string representing this provider name and relevant details. /// </summary> /// <returns> The configuration name. </returns> public override string ToString() => $"{GetType().Name} for '{Source.Path}' ({(Source.Optional ? "Optional" : "Required")})"; private void Load(bool reload) { var file = Source.FileProvider?.GetFileInfo(Source.Path); if (file == null || !file.Exists) { if (Source.Optional || reload) // Always optional on reload { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } else { var error = new StringBuilder($"The configuration file '{Source.Path}' was not found and is not optional."); if (!string.IsNullOrEmpty(file?.PhysicalPath)) { error.Append($" The physical path is '{file.PhysicalPath}'."); } HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString()))); } } else { // Always create new Data on reload to drop old keys if (reload) { Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } using (var stream = file.CreateReadStream()) { try { Load(stream); } catch (Exception e) { HandleException(ExceptionDispatchInfo.Capture(e)); } } } // REVIEW: Should we raise this in the base as well / instead? OnReload(); } /// <summary> /// Loads the contents of the file at <see cref="Path"/>. /// </summary> /// <exception cref="FileNotFoundException">If Optional is <c>false</c> on the source and a /// file does not exist at specified Path.</exception> public override void Load() { Load(reload: false); } /// <summary> /// Loads this provider's data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public abstract void Load(Stream stream); private void HandleException(ExceptionDispatchInfo info) { bool ignoreException = false; if (Source.OnLoadException != null) { var exceptionContext = new FileLoadExceptionContext { Provider = this, Exception = info.SourceException }; Source.OnLoadException.Invoke(exceptionContext); ignoreException = exceptionContext.Ignore; } if (!ignoreException) { info.Throw(); } } /// <inheritdoc /> public void Dispose() => Dispose(true); /// <summary> /// Dispose the provider. /// </summary> /// <param name="disposing"><c>true</c> if invoked from <see cref="IDisposable.Dispose"/>.</param> protected virtual void Dispose(bool disposing) { _changeTokenRegistration?.Dispose(); } }
这两个都是抽象类,具体的实现后面介绍
FileConfigurationSource中有个IFileProvider类型的FileProvider属性,这又涉及到netcore的文件系统,后面再开个系列文章介绍,可以简单的知道该FileProvider属性是获取对应的配置文件
Path属性是文件的路径,Optional属性是文件是否可选的,如果是false,文件不存在时,会抛出异常。
IConfigurationBuilder有个扩展方法GetFileProvider,该方法从IConfigurationBuilder的Properties属性中获取key为“FileProviderKey”对应的值转换成IFileProvider,当我们要自定义IConfigurationBuilder的IFileProvider实现时,可以从这里扩展,
系统默认使用PhysicalFileProvider作为IFileProvider
public FileConfigurationProvider(FileConfigurationSource source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } Source = source; if (Source.ReloadOnChange && Source.FileProvider != null) { _changeTokenRegistration = ChangeToken.OnChange( () => Source.FileProvider.Watch(Source.Path), () => { Thread.Sleep(Source.ReloadDelay); Load(reload: true); }); } }
FileConfigurationProvider构造函数里面监听配置文件是否修改,如果修改了会重新加载配置
FileConfigurationProvider的Load方法会读取文件流,再调用其抽象方法Load(Stream stream)解析文件流的数据存储到Data属性中
针对File的IConfigurationBuilder扩展方法其实蛮多的,如SetFileProvider可以设置SetFileProvider的IFileProvider,SetBasePath可以设置文件的路径
四、 Json文件的配置源:JsonConfigurationSource和FileConfigurationProvider
public class JsonConfigurationSource : FileConfigurationSource { /// <summary> /// Builds the <see cref="JsonConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="JsonConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new JsonConfigurationProvider(this); } }
public class JsonConfigurationProvider : FileConfigurationProvider { /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { } /// <summary> /// Loads the JSON data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public override void Load(Stream stream) { try { Data = JsonConfigurationFileParser.Parse(stream); } catch (JsonException e) { throw new FormatException(Resources.Error_JSONParseError, e); } } }
这两个对象实现很简单,主要核心是JsonConfigurationFileParser类,用来解析流
该类用到了微软的自带json解析器JsonDocument用来解析Json对象
internal class JsonConfigurationFileParser { private JsonConfigurationFileParser() { } private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Stack<string> _context = new Stack<string>(); private string _currentPath; public static IDictionary<string, string> Parse(Stream input) => new JsonConfigurationFileParser().ParseStream(input); private IDictionary<string, string> ParseStream(Stream input) { _data.Clear(); var jsonDocumentOptions = new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; using (var reader = new StreamReader(input)) using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) { if (doc.RootElement.ValueKind != JsonValueKind.Object) { throw new FormatException(Resources.FormatError_UnsupportedJSONToken(doc.RootElement.ValueKind)); } VisitElement(doc.RootElement); } return _data; } private void VisitElement(JsonElement element) { foreach (var property in element.EnumerateObject()) { EnterContext(property.Name); VisitValue(property.Value); ExitContext(); } } private void VisitValue(JsonElement value) { switch (value.ValueKind) { case JsonValueKind.Object: VisitElement(value); break; case JsonValueKind.Array: var index = 0; foreach (var arrayElement in value.EnumerateArray()) { EnterContext(index.ToString()); VisitValue(arrayElement); ExitContext(); index++; } break; case JsonValueKind.Number: case JsonValueKind.String: case JsonValueKind.True: case JsonValueKind.False: case JsonValueKind.Null: var key = _currentPath; if (_data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key)); } _data[key] = value.ToString(); break; default: throw new FormatException(Resources.FormatError_UnsupportedJSONToken(value.ValueKind)); } } private void EnterContext(string context) { _context.Push(context); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } private void ExitContext() { _context.Pop(); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } }
JsonStreamConfigurationSource和JsonStreamConfigurationProvider
public class JsonStreamConfigurationSource : StreamConfigurationSource { /// <summary> /// Builds the <see cref="JsonStreamConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>An <see cref="JsonStreamConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) => new JsonStreamConfigurationProvider(this); }
public class JsonStreamConfigurationProvider : StreamConfigurationProvider { /// <summary> /// Constructor. /// </summary> /// <param name="source">The <see cref="JsonStreamConfigurationSource"/>.</param> public JsonStreamConfigurationProvider(JsonStreamConfigurationSource source) : base(source) { } /// <summary> /// Loads json configuration key/values from a stream into a provider. /// </summary> /// <param name="stream">The json <see cref="Stream"/> to load configuration data from.</param> public override void Load(Stream stream) { Data = JsonConfigurationFileParser.Parse(stream); } }
JsonStreamConfigurationSource继承StreamConfigurationSource,里面有个Stream类型的Stream属性,设置对应的流
JsonStreamConfigurationProvider继承StreamConfigurationProvider,重写了Load方法,其实还是通过调用JsonConfigurationFileParser类来解析流
IConfigurationBuilder的扩展方法AddJsonFile:可以指定IFileProvider、Json的文件路径、是否可选的、是否重新加载配置。扩展方法AddJsonStream传递流构建JsonStreamConfigurationSource
五、 KeyPerFileConfigurationSource和KeyPerFileConfigurationProvider
public class KeyPerFileConfigurationSource : IConfigurationSource { /// <summary> /// Constructor; /// </summary> public KeyPerFileConfigurationSource() => IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix); /// <summary> /// The FileProvider whos root "/" directory files will be used as configuration data. /// </summary> public IFileProvider FileProvider { get; set; } /// <summary> /// Files that start with this prefix will be excluded. /// Defaults to "ignore.". /// </summary> public string IgnorePrefix { get; set; } = "ignore."; /// <summary> /// Used to determine if a file should be ignored using its name. /// Defaults to using the IgnorePrefix. /// </summary> public Func<string, bool> IgnoreCondition { get; set; } /// <summary> /// If false, will throw if the directory doesn't exist. /// </summary> public bool Optional { get; set; } /// <summary> /// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="KeyPerFileConfigurationProvider"/></returns> public IConfigurationProvider Build(IConfigurationBuilder builder) => new KeyPerFileConfigurationProvider(this); }
public class KeyPerFileConfigurationProvider : ConfigurationProvider { KeyPerFileConfigurationSource Source { get; set; } /// <summary> /// Initializes a new instance. /// </summary> /// <param name="source">The settings.</param> public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source) => Source = source ?? throw new ArgumentNullException(nameof(source)); private static string NormalizeKey(string key) => key.Replace("__", ConfigurationPath.KeyDelimiter); private static string TrimNewLine(string value) => value.EndsWith(Environment.NewLine) ? value.Substring(0, value.Length - Environment.NewLine.Length) : value; /// <summary> /// Loads the docker secrets. /// </summary> public override void Load() { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (Source.FileProvider == null) { if (Source.Optional) { Data = data; return; } throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional."); } var directory = Source.FileProvider.GetDirectoryContents("/"); if (!directory.Exists && !Source.Optional) { throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional."); } foreach (var file in directory) { if (file.IsDirectory) { continue; } using (var stream = file.CreateReadStream()) using (var streamReader = new StreamReader(stream)) { if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name)) { data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd())); } } } Data = data; } private string GetDirectoryName() => Source.FileProvider?.GetFileInfo("/")?.PhysicalPath ?? "<Unknown>"; /// <summary> /// Generates a string representing this provider name and relevant details. /// </summary> /// <returns> The configuration name. </returns> public override string ToString() => $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})"; }
上面实现的是读取目录下面的文件,以文件名为key,文件内容为value的配置源
扩展方法AddKeyPerFile添加KeyPerFileConfigurationSource
六、 NewtonsoftJsonConfigurationSource和NewtonsoftJsonConfigurationProvider
public class NewtonsoftJsonConfigurationSource : FileConfigurationSource { /// <summary> /// Builds the <see cref="NewtonsoftJsonConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="NewtonsoftJsonConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new NewtonsoftJsonConfigurationProvider(this); } }
public class NewtonsoftJsonConfigurationProvider : FileConfigurationProvider { /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public NewtonsoftJsonConfigurationProvider(NewtonsoftJsonConfigurationSource source) : base(source) { } /// <summary> /// Loads the JSON data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public override void Load(Stream stream) { try { Data = NewtonsoftJsonConfigurationFileParser.Parse(stream); } catch (JsonReaderException e) { string errorLine = string.Empty; if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); IEnumerable<string> fileContent; using (var streamReader = new StreamReader(stream)) { fileContent = ReadLines(streamReader); errorLine = RetrieveErrorContext(e, fileContent); } } throw new FormatException(Resources.FormatError_JSONParseError(e.LineNumber, errorLine), e); } } private static string RetrieveErrorContext(JsonReaderException e, IEnumerable<string> fileContent) { string errorLine = null; if (e.LineNumber >= 2) { var errorContext = fileContent.Skip(e.LineNumber - 2).Take(2).ToList(); // Handle situations when the line number reported is out of bounds if (errorContext.Count() >= 2) { errorLine = errorContext[0].Trim() + Environment.NewLine + errorContext[1].Trim(); } } if (string.IsNullOrEmpty(errorLine)) { var possibleLineContent = fileContent.Skip(e.LineNumber - 1).FirstOrDefault(); errorLine = possibleLineContent ?? string.Empty; } return errorLine; } private static IEnumerable<string> ReadLines(StreamReader streamReader) { string line; do { line = streamReader.ReadLine(); yield return line; } while (line != null); } }
这两个也是Json文件的配置源,和上面的差不多,不同的是解析Json数据流的方式不一样
用到NewtonsoftJsonConfigurationFileParser类来解析json
internal class NewtonsoftJsonConfigurationFileParser { private NewtonsoftJsonConfigurationFileParser() { } private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); private readonly Stack<string> _context = new Stack<string>(); private string _currentPath; private JsonTextReader _reader; public static IDictionary<string, string> Parse(Stream input) => new NewtonsoftJsonConfigurationFileParser().ParseStream(input); private IDictionary<string, string> ParseStream(Stream input) { _data.Clear(); _reader = new JsonTextReader(new StreamReader(input)); _reader.DateParseHandling = DateParseHandling.None; var jsonConfig = JObject.Load(_reader); VisitJObject(jsonConfig); return _data; } private void VisitJObject(JObject jObject) { foreach (var property in jObject.Properties()) { EnterContext(property.Name); VisitProperty(property); ExitContext(); } } private void VisitProperty(JProperty property) { VisitToken(property.Value); } private void VisitToken(JToken token) { switch (token.Type) { case JTokenType.Object: VisitJObject(token.Value<JObject>()); break; case JTokenType.Array: VisitArray(token.Value<JArray>()); break; case JTokenType.Integer: case JTokenType.Float: case JTokenType.String: case JTokenType.Boolean: case JTokenType.Bytes: case JTokenType.Raw: case JTokenType.Null: VisitPrimitive(token.Value<JValue>()); break; default: throw new FormatException(Resources.FormatError_UnsupportedJSONToken( _reader.TokenType, _reader.Path, _reader.LineNumber, _reader.LinePosition)); } } private void VisitArray(JArray array) { for (int index = 0; index < array.Count; index++) { EnterContext(index.ToString()); VisitToken(array[index]); ExitContext(); } } private void VisitPrimitive(JValue data) { var key = _currentPath; if (_data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key)); } _data[key] = data.ToString(CultureInfo.InvariantCulture); } private void EnterContext(string context) { _context.Push(context); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } private void ExitContext() { _context.Pop(); _currentPath = ConfigurationPath.Combine(_context.Reverse()); } }
主要用到Newtonsoft.Json来解析json
对应的扩展方法:AddNewtonsoftJsonFile
NewtonsoftJsonStreamConfigurationSource和NewtonsoftJsonStreamConfigurationProvider
public class NewtonsoftJsonStreamConfigurationSource : StreamConfigurationSource { /// <summary> /// Builds the <see cref="NewtonsoftJsonStreamConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>An <see cref="NewtonsoftJsonStreamConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) => new NewtonsoftJsonStreamConfigurationProvider(this); }
public class NewtonsoftJsonStreamConfigurationProvider : StreamConfigurationProvider { /// <summary> /// Constructor. /// </summary> /// <param name="source">The source of configuration.</param> public NewtonsoftJsonStreamConfigurationProvider(NewtonsoftJsonStreamConfigurationSource source) : base(source) { } /// <summary> /// Loads json configuration key/values from a stream into a provider. /// </summary> /// <param name="stream">The json <see cref="Stream"/> to load configuration data from.</param> public override void Load(Stream stream) { Data = NewtonsoftJsonConfigurationFileParser.Parse(stream); } }
对应的扩展方法:AddNewtonsoftJsonStream
七、 UserSecrets:IConfigurationBuilder有个扩展方法AddUserSecrets,系统会自动生成机密文件,保存在本地电脑上
创建的文件名为:secrets.json
看下核心实现代码
public static IConfigurationBuilder AddUserSecrets(this IConfigurationBuilder configuration, Assembly assembly, bool optional, bool reloadOnChange) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); } var attribute = assembly.GetCustomAttribute<UserSecretsIdAttribute>(); if (attribute != null) { return AddUserSecrets(configuration, attribute.UserSecretsId, reloadOnChange); } if (!optional) { throw new InvalidOperationException(Resources.FormatError_Missing_UserSecretsIdAttribute(assembly.GetName().Name)); } return configuration; }
系统会查找指定程序集上的UserSecretsIdAttribute特性,可以设置UserSecretsId属性,该值为路径的一部分。
[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)] public class UserSecretsIdAttribute : Attribute { /// <summary> /// Initializes an instance of <see cref="UserSecretsIdAttribute" />. /// </summary> /// <param name="userSecretId">The user secrets ID.</param> public UserSecretsIdAttribute(string userSecretId) { if (string.IsNullOrEmpty(userSecretId)) { throw new ArgumentException(Resources.Common_StringNullOrEmpty, nameof(userSecretId)); } UserSecretsId = userSecretId; } /// <summary> /// The user secrets ID. /// </summary> public string UserSecretsId { get; } }
public static IConfigurationBuilder AddUserSecrets(this IConfigurationBuilder configuration, string userSecretsId, bool reloadOnChange) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } if (userSecretsId == null) { throw new ArgumentNullException(nameof(userSecretsId)); } return AddSecretsFile(configuration, PathHelper.GetSecretsPathFromSecretsId(userSecretsId), reloadOnChange); }
解析secrets.json保存的文件路径
public class PathHelper { internal const string SecretsFileName = "secrets.json"; /// <summary> /// <para> /// Returns the path to the JSON file that stores user secrets. /// </para> /// <para> /// This uses the current user profile to locate the secrets file on disk in a location outside of source control. /// </para> /// </summary> /// <param name="userSecretsId">The user secret ID.</param> /// <returns>The full path to the secret file.</returns> public static string GetSecretsPathFromSecretsId(string userSecretsId) { if (string.IsNullOrEmpty(userSecretsId)) { throw new ArgumentException(Resources.Common_StringNullOrEmpty, nameof(userSecretsId)); } var badCharIndex = userSecretsId.IndexOfAny(Path.GetInvalidFileNameChars()); if (badCharIndex != -1) { throw new InvalidOperationException( string.Format( Resources.Error_Invalid_Character_In_UserSecrets_Id, userSecretsId[badCharIndex], badCharIndex)); } const string userSecretsFallbackDir = "DOTNET_USER_SECRETS_FALLBACK_DIR"; // For backwards compat, this checks env vars first before using Env.GetFolderPath var appData = Environment.GetEnvironmentVariable("APPDATA"); var root = appData // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\ ?? Environment.GetEnvironmentVariable("HOME") // On Mac/Linux it goes to ~/.microsoft/usersecrets/ ?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? Environment.GetEnvironmentVariable(userSecretsFallbackDir); // this fallback is an escape hatch if everything else fails if (string.IsNullOrEmpty(root)) { throw new InvalidOperationException("Could not determine an appropriate location for storing user secrets. Set the " + userSecretsFallbackDir + " environment variable to a folder where user secrets should be stored."); } return !string.IsNullOrEmpty(appData) ? Path.Combine(root, "Microsoft", "UserSecrets", userSecretsId, SecretsFileName) : Path.Combine(root, ".microsoft", "usersecrets", userSecretsId, SecretsFileName); } }
右键项目有个“管理用户机密”选项,点击该选项后,vs会自动生成secrets.json的文件,可以把自己的一些配置信息写入到该文件中。该文件会保存在系统指定目录下,而不再项目目录下面,当提交项目代码时,该配置文件不会被提交,其他人也看不到,应该起到隔离的作用。
八、 Xml文件配置源:XmlConfigurationSource和XmlConfigurationProvider
解析Xml文件的配置信息
public class XmlConfigurationSource : FileConfigurationSource { /// <summary> /// Builds the <see cref="XmlConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>A <see cref="XmlConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new XmlConfigurationProvider(this); } }
public class XmlConfigurationProvider : FileConfigurationProvider { /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public XmlConfigurationProvider(XmlConfigurationSource source) : base(source) { } internal XmlDocumentDecryptor Decryptor { get; set; } = XmlDocumentDecryptor.Instance; /// <summary> /// Loads the XML data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public override void Load(Stream stream) { Data = XmlStreamConfigurationProvider.Read(stream, Decryptor); } }
XmlStreamConfigurationSource和XmlStreamConfigurationProvider
public class XmlStreamConfigurationSource : StreamConfigurationSource { /// <summary> /// Builds the <see cref="XmlStreamConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>An <see cref="XmlStreamConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) => new XmlStreamConfigurationProvider(this); }
/// <summary> /// An XML file based <see cref="IConfigurationProvider"/>. /// </summary> public class XmlStreamConfigurationProvider : StreamConfigurationProvider { private const string NameAttributeKey = "Name"; /// <summary> /// Constructor. /// </summary> /// <param name="source">The <see cref="XmlStreamConfigurationSource"/>.</param> public XmlStreamConfigurationProvider(XmlStreamConfigurationSource source) : base(source) { } /// <summary> /// Read a stream of INI values into a key/value dictionary. /// </summary> /// <param name="stream">The stream of INI data.</param> /// <param name="decryptor">The <see cref="XmlDocumentDecryptor"/> to use to decrypt.</param> /// <returns>The <see cref="IDictionary{String, String}"/> which was read from the stream.</returns> public static IDictionary<string, string> Read(Stream stream, XmlDocumentDecryptor decryptor) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var readerSettings = new XmlReaderSettings() { CloseInput = false, // caller will close the stream DtdProcessing = DtdProcessing.Prohibit, IgnoreComments = true, IgnoreWhitespace = true }; using (var reader = decryptor.CreateDecryptingXmlReader(stream, readerSettings)) { var prefixStack = new Stack<string>(); SkipUntilRootElement(reader); // We process the root element individually since it doesn't contribute to prefix ProcessAttributes(reader, prefixStack, data, AddNamePrefix); ProcessAttributes(reader, prefixStack, data, AddAttributePair); var preNodeType = reader.NodeType; while (reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: prefixStack.Push(reader.LocalName); ProcessAttributes(reader, prefixStack, data, AddNamePrefix); ProcessAttributes(reader, prefixStack, data, AddAttributePair); // If current element is self-closing if (reader.IsEmptyElement) { prefixStack.Pop(); } break; case XmlNodeType.EndElement: if (prefixStack.Any()) { // If this EndElement node comes right after an Element node, // it means there is no text/CDATA node in current element if (preNodeType == XmlNodeType.Element) { var key = ConfigurationPath.Combine(prefixStack.Reverse()); data[key] = string.Empty; } prefixStack.Pop(); } break; case XmlNodeType.CDATA: case XmlNodeType.Text: { var key = ConfigurationPath.Combine(prefixStack.Reverse()); if (data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key, GetLineInfo(reader))); } data[key] = reader.Value; break; } case XmlNodeType.XmlDeclaration: case XmlNodeType.ProcessingInstruction: case XmlNodeType.Comment: case XmlNodeType.Whitespace: // Ignore certain types of nodes break; default: throw new FormatException(Resources.FormatError_UnsupportedNodeType(reader.NodeType, GetLineInfo(reader))); } preNodeType = reader.NodeType; // If this element is a self-closing element, // we pretend that we just processed an EndElement node // because a self-closing element contains an end within itself if (preNodeType == XmlNodeType.Element && reader.IsEmptyElement) { preNodeType = XmlNodeType.EndElement; } } } return data; } /// <summary> /// Loads XML configuration key/values from a stream into a provider. /// </summary> /// <param name="stream">The <see cref="Stream"/> to load ini configuration data from.</param> public override void Load(Stream stream) { Data = Read(stream, XmlDocumentDecryptor.Instance); } private static void SkipUntilRootElement(XmlReader reader) { while (reader.Read()) { if (reader.NodeType != XmlNodeType.XmlDeclaration && reader.NodeType != XmlNodeType.ProcessingInstruction) { break; } } } private static string GetLineInfo(XmlReader reader) { var lineInfo = reader as IXmlLineInfo; return lineInfo == null ? string.Empty : Resources.FormatMsg_LineInfo(lineInfo.LineNumber, lineInfo.LinePosition); } private static void ProcessAttributes(XmlReader reader, Stack<string> prefixStack, IDictionary<string, string> data, Action<XmlReader, Stack<string>, IDictionary<string, string>, XmlWriter> act, XmlWriter writer = null) { for (int i = 0; i < reader.AttributeCount; i++) { reader.MoveToAttribute(i); // If there is a namespace attached to current attribute if (!string.IsNullOrEmpty(reader.NamespaceURI)) { throw new FormatException(Resources.FormatError_NamespaceIsNotSupported(GetLineInfo(reader))); } act(reader, prefixStack, data, writer); } // Go back to the element containing the attributes we just processed reader.MoveToElement(); } // The special attribute "Name" only contributes to prefix // This method adds a prefix if current node in reader represents a "Name" attribute private static void AddNamePrefix(XmlReader reader, Stack<string> prefixStack, IDictionary<string, string> data, XmlWriter writer) { if (!string.Equals(reader.LocalName, NameAttributeKey, StringComparison.OrdinalIgnoreCase)) { return; } // If current element is not root element if (prefixStack.Any()) { var lastPrefix = prefixStack.Pop(); prefixStack.Push(ConfigurationPath.Combine(lastPrefix, reader.Value)); } else { prefixStack.Push(reader.Value); } } // Common attributes contribute to key-value pairs // This method adds a key-value pair if current node in reader represents a common attribute private static void AddAttributePair(XmlReader reader, Stack<string> prefixStack, IDictionary<string, string> data, XmlWriter writer) { prefixStack.Push(reader.LocalName); var key = ConfigurationPath.Combine(prefixStack.Reverse()); if (data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key, GetLineInfo(reader))); } data[key] = reader.Value; prefixStack.Pop(); } }
扩展方法:AddXmlFile和AddXmlStream
九、 Ini文件配置源:IniConfigurationSource和IniConfigurationProvider
/// <summary> /// Represents an INI file as an <see cref="IConfigurationSource"/>. /// Files are simple line structures (<a href="https://en.wikipedia.org/wiki/INI_file">INI Files on Wikipedia</a>) /// </summary> /// <examples> /// [Section:Header] /// key1=value1 /// key2 = " value2 " /// ; comment /// # comment /// / comment /// </examples> public class IniConfigurationSource : FileConfigurationSource { /// <summary> /// Builds the <see cref="IniConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>An <see cref="IniConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) { EnsureDefaults(builder); return new IniConfigurationProvider(this); } }
/// <summary> /// An INI file based <see cref="ConfigurationProvider"/>. /// Files are simple line structures (<a href="https://en.wikipedia.org/wiki/INI_file">INI Files on Wikipedia</a>) /// </summary> /// <examples> /// [Section:Header] /// key1=value1 /// key2 = " value2 " /// ; comment /// # comment /// / comment /// </examples> public class IniConfigurationProvider : FileConfigurationProvider { /// <summary> /// Initializes a new instance with the specified source. /// </summary> /// <param name="source">The source settings.</param> public IniConfigurationProvider(IniConfigurationSource source) : base(source) { } /// <summary> /// Loads the INI data from a stream. /// </summary> /// <param name="stream">The stream to read.</param> public override void Load(Stream stream) => Data = IniStreamConfigurationProvider.Read(stream); }
IniStreamConfigurationSource和IniStreamConfigurationProvider
/// <summary> /// Represents an INI file as an <see cref="IConfigurationSource"/>. /// Files are simple line structures (<a href="https://en.wikipedia.org/wiki/INI_file">INI Files on Wikipedia</a>) /// </summary> /// <examples> /// [Section:Header] /// key1=value1 /// key2 = " value2 " /// ; comment /// # comment /// / comment /// </examples> public class IniStreamConfigurationSource : StreamConfigurationSource { /// <summary> /// Builds the <see cref="IniConfigurationProvider"/> for this source. /// </summary> /// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param> /// <returns>An <see cref="IniConfigurationProvider"/></returns> public override IConfigurationProvider Build(IConfigurationBuilder builder) => new IniStreamConfigurationProvider(this); }
public class IniStreamConfigurationProvider : StreamConfigurationProvider { /// <summary> /// Constructor. /// </summary> /// <param name="source">The <see cref="IniStreamConfigurationSource"/>.</param> public IniStreamConfigurationProvider(IniStreamConfigurationSource source) : base(source) { } /// <summary> /// Read a stream of INI values into a key/value dictionary. /// </summary> /// <param name="stream">The stream of INI data.</param> /// <returns>The <see cref="IDictionary{String, String}"/> which was read from the stream.</returns> public static IDictionary<string, string> Read(Stream stream) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); using (var reader = new StreamReader(stream)) { var sectionPrefix = string.Empty; while (reader.Peek() != -1) { var rawLine = reader.ReadLine(); var line = rawLine.Trim(); // Ignore blank lines if (string.IsNullOrWhiteSpace(line)) { continue; } // Ignore comments if (line[0] == ';' || line[0] == '#' || line[0] == '/') { continue; } // [Section:header] if (line[0] == '[' && line[line.Length - 1] == ']') { // remove the brackets sectionPrefix = line.Substring(1, line.Length - 2) + ConfigurationPath.KeyDelimiter; continue; } // key = value OR "value" int separator = line.IndexOf('='); if (separator < 0) { throw new FormatException(Resources.FormatError_UnrecognizedLineFormat(rawLine)); } string key = sectionPrefix + line.Substring(0, separator).Trim(); string value = line.Substring(separator + 1).Trim(); // Remove quotes if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') { value = value.Substring(1, value.Length - 2); } if (data.ContainsKey(key)) { throw new FormatException(Resources.FormatError_KeyIsDuplicated(key)); } data[key] = value; } } return data; } /// <summary> /// Loads INI configuration key/values from a stream into a provider. /// </summary> /// <param name="stream">The <see cref="Stream"/> to load ini configuration data from.</param> public override void Load(Stream stream) { Data = Read(stream); } }
扩展方法:AddIniFile
九、 Azure
AzureKeyVaultConfigurationSource和AzureKeyVaultConfigurationProvider
internal class AzureKeyVaultConfigurationSource : IConfigurationSource { private readonly AzureKeyVaultConfigurationOptions _options; public AzureKeyVaultConfigurationSource(AzureKeyVaultConfigurationOptions options) { _options = options; } /// <inheritdoc /> public IConfigurationProvider Build(IConfigurationBuilder builder) { return new AzureKeyVaultConfigurationProvider(new KeyVaultClientWrapper(_options.Client), _options.Vault, _options.Manager, _options.ReloadInterval); } }
public class AzureKeyVaultConfigurationOptions { /// <summary> /// Creates a new instance of <see cref="AzureKeyVaultConfigurationOptions"/>. /// </summary> public AzureKeyVaultConfigurationOptions() { Manager = DefaultKeyVaultSecretManager.Instance; } /// <summary> /// Creates a new instance of <see cref="AzureKeyVaultConfigurationOptions"/>. /// </summary> /// <param name="vault">Azure KeyVault uri.</param> /// <param name="clientId">The application client id.</param> /// <param name="certificate">The <see cref="X509Certificate2"/> to use for authentication.</param> public AzureKeyVaultConfigurationOptions( string vault, string clientId, X509Certificate2 certificate) : this() { KeyVaultClient.AuthenticationCallback authenticationCallback = (authority, resource, scope) => GetTokenFromClientCertificate(authority, resource, clientId, certificate); Vault = vault; Client = new KeyVaultClient(authenticationCallback); } /// <summary> /// Creates a new instance of <see cref="AzureKeyVaultConfigurationOptions"/>. /// </summary> /// <param name="vault">The Azure KeyVault uri.</param> public AzureKeyVaultConfigurationOptions(string vault) : this() { var azureServiceTokenProvider = new AzureServiceTokenProvider(); var authenticationCallback = new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback); Vault = vault; Client = new KeyVaultClient(authenticationCallback); } /// <summary> /// Creates a new instance of <see cref="AzureKeyVaultConfigurationOptions"/>. /// </summary> /// <param name="vault">The Azure KeyVault uri.</param> /// <param name="clientId">The application client id.</param> /// <param name="clientSecret">The client secret to use for authentication.</param> public AzureKeyVaultConfigurationOptions( string vault, string clientId, string clientSecret) : this() { if (clientId == null) { throw new ArgumentNullException(nameof(clientId)); } if (clientSecret == null) { throw new ArgumentNullException(nameof(clientSecret)); } KeyVaultClient.AuthenticationCallback authenticationCallback = (authority, resource, scope) => GetTokenFromClientSecret(authority, resource, clientId, clientSecret); Vault = vault; Client = new KeyVaultClient(authenticationCallback); } /// <summary> /// Gets or sets the <see cref="KeyVaultClient"/> to use for retrieving values. /// </summary> public KeyVaultClient Client { get; set; } /// <summary> /// Gets or sets the vault uri. /// </summary> public string Vault { get; set; } /// <summary> /// Gets or sets the <see cref="IKeyVaultSecretManager"/> instance used to control secret loading. /// </summary> public IKeyVaultSecretManager Manager { get; set; } /// <summary> /// Gets or sets the timespan to wait between attempts at polling the Azure KeyVault for changes. <code>null</code> to disable reloading. /// </summary> public TimeSpan? ReloadInterval { get; set; } private static async Task<string> GetTokenFromClientCertificate(string authority, string resource, string clientId, X509Certificate2 certificate) { var authContext = new AuthenticationContext(authority); var result = await authContext.AcquireTokenAsync(resource, new ClientAssertionCertificate(clientId, certificate)); return result.AccessToken; } private static async Task<string> GetTokenFromClientSecret(string authority, string resource, string clientId, string clientSecret) { var authContext = new AuthenticationContext(authority); var clientCred = new ClientCredential(clientId, clientSecret); var result = await authContext.AcquireTokenAsync(resource, clientCred); return result.AccessToken; } }
internal class AzureKeyVaultConfigurationProvider : ConfigurationProvider, IDisposable { private readonly TimeSpan? _reloadInterval; private readonly IKeyVaultClient _client; private readonly string _vault; private readonly IKeyVaultSecretManager _manager; private Dictionary<string, LoadedSecret> _loadedSecrets; private Task _pollingTask; private readonly CancellationTokenSource _cancellationToken; /// <summary> /// Creates a new instance of <see cref="AzureKeyVaultConfigurationProvider"/>. /// </summary> /// <param name="client">The <see cref="KeyVaultClient"/> to use for retrieving values.</param> /// <param name="vault">Azure KeyVault uri.</param> /// <param name="manager">The <see cref="IKeyVaultSecretManager"/> to use in managing values.</param> /// <param name="reloadInterval">The timespan to wait in between each attempt at polling the Azure KeyVault for changes. Default is null which indicates no reloading.</param> public AzureKeyVaultConfigurationProvider(IKeyVaultClient client, string vault, IKeyVaultSecretManager manager, TimeSpan? reloadInterval = null) { _client = client ?? throw new ArgumentNullException(nameof(client)); _vault = vault ?? throw new ArgumentNullException(nameof(vault)); _manager = manager ?? throw new ArgumentNullException(nameof(manager)); if (reloadInterval != null && reloadInterval.Value <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(reloadInterval), reloadInterval, nameof(reloadInterval) + " must be positive."); } _pollingTask = null; _cancellationToken = new CancellationTokenSource(); _reloadInterval = reloadInterval; } /// <summary> /// Load secrets into this provider. /// </summary> public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); private async Task PollForSecretChangesAsync() { while (!_cancellationToken.IsCancellationRequested) { await WaitForReload(); try { await LoadAsync(); } catch (Exception) { // Ignore } } } protected virtual async Task WaitForReload() { await Task.Delay(_reloadInterval.Value, _cancellationToken.Token); } private async Task LoadAsync() { var secretPage = await _client.GetSecretsAsync(_vault).ConfigureAwait(false); var allSecrets = new List<SecretItem>(secretPage.Count()); do { allSecrets.AddRange(secretPage.ToList()); secretPage = secretPage.NextPageLink != null ? await _client.GetSecretsNextAsync(secretPage.NextPageLink).ConfigureAwait(false) : null; } while (secretPage != null); var tasks = new List<Task<SecretBundle>>(); var newLoadedSecrets = new Dictionary<string, LoadedSecret>(); var oldLoadedSecrets = Interlocked.Exchange(ref _loadedSecrets, null); foreach (var secret in allSecrets) { if (!_manager.Load(secret) || secret.Attributes?.Enabled != true) { continue; } var secretId = secret.Identifier.BaseIdentifier; if (oldLoadedSecrets != null && oldLoadedSecrets.TryGetValue(secretId, out var existingSecret) && existingSecret.IsUpToDate(secret.Attributes.Updated)) { oldLoadedSecrets.Remove(secretId); newLoadedSecrets.Add(secretId, existingSecret); } else { tasks.Add(_client.GetSecretAsync(secretId)); } } await Task.WhenAll(tasks).ConfigureAwait(false); foreach (var task in tasks) { var secretBundle = task.Result; newLoadedSecrets.Add(secretBundle.SecretIdentifier.BaseIdentifier, new LoadedSecret(_manager.GetKey(secretBundle), secretBundle.Value, secretBundle.Attributes.Updated)); } _loadedSecrets = newLoadedSecrets; // Reload is needed if we are loading secrets that were not loaded before or // secret that was loaded previously is not available anymore if (tasks.Any() || oldLoadedSecrets?.Any() == true) { SetData(_loadedSecrets, fireToken: oldLoadedSecrets != null); } // schedule a polling task only if none exists and a valid delay is specified if (_pollingTask == null && _reloadInterval != null) { _pollingTask = PollForSecretChangesAsync(); } } private void SetData(Dictionary<string, LoadedSecret> loadedSecrets, bool fireToken) { var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (var secretItem in loadedSecrets) { data.Add(secretItem.Value.Key, secretItem.Value.Value); } Data = data; if (fireToken) { OnReload(); } } /// <inheritdoc/> public void Dispose() { _cancellationToken.Cancel(); } private readonly struct LoadedSecret { public LoadedSecret(string key, string value, DateTime? updated) { Key = key; Value = value; Updated = updated; } public string Key { get; } public string Value { get; } public DateTime? Updated { get; } public bool IsUpToDate(DateTime? updated) { if (updated.HasValue != Updated.HasValue) { return false; } return updated.GetValueOrDefault() == Updated.GetValueOrDefault(); } } }
扩展方法:AddAzureKeyVault
这篇主要讲了netcore下面的各种数据配置源:
环境变量、命令行、Ini文件、json文件、Xml文件等,其实就是把对应的数据解析成键值对保存到Provider的Data属性中。