1、本文主要介绍下.net core 5.0的配置文件组件JsonProvider源码核心逻辑.
直接上调用方式代码,跟着代码一步步解析
var workDir = $"{Environment.CurrentDirectory}";
var builder = new ConfigurationBuilder()
.SetBasePath(workDir)
.AddJsonFile($"test.json", optional: true, reloadOnChange: true);
var root = builder.Build();
ok,首先看ConfigurationBuilder干了什么,源码如下:
public class ConfigurationBuilder : IConfigurationBuilder
{
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (IConfigurationSource source in Sources)
{
IConfigurationProvider provider = source.Build(this);
providers.Add(provider);
}
return new ConfigurationRoot(providers);
}
}
ok,到这里其实Builder没干啥,只是初始化了Properties和 Sources两个实例,接着看SetBasePath扩展方法干了什么
public static IConfigurationBuilder SetBasePath(this IConfigurationBuilder builder, string basePath)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (basePath == null)
{
throw new ArgumentNullException(nameof(basePath));
}
return builder.SetFileProvider(new PhysicalFileProvider(basePath));
}
简单的参数校验,且调用了builder.SetFileProvider,代码如下:
public static IConfigurationBuilder SetFileProvider(this IConfigurationBuilder builder, IFileProvider fileProvider)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Properties[FileProviderKey] = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
return builder;
}
到这里很简单,向Properties属性集合写入了PhysicalFileProvider了,并给PhysicalFileProvider传入了根目录.ok,接下去看PhysicalFileProvider的逻辑.
public PhysicalFileProvider(string root, ExclusionFilters filters)
{
//路径必须是绝对路径
if (!Path.IsPathRooted(root))
{
throw new ArgumentException("The path must be absolute.", nameof(root));
}
string fullRoot = Path.GetFullPath(root);
Root = PathUtils.EnsureTrailingSlash(fullRoot);
if (!Directory.Exists(Root))
{
throw new DirectoryNotFoundException(Root);
}
_filters = filters;
_fileWatcherFactory = () => CreateFileWatcher();
}
处理了下传入的根目录,且指定了过滤器ExclusionFilters,过滤器源码如下:
public enum ExclusionFilters
{
Sensitive = DotPrefixed | Hidden | System,
DotPrefixed = 1,
Hidden = 2,
System = 4,
None = 0
}
这个特性只要是过滤扫描文件夹下的文件时,哪些文件是不能操作,关于这个逻辑,后续不再赘述了.接着看核心代码CreateFileWatcher()
internal PhysicalFilesWatcher CreateFileWatcher()
{
string root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(Root));
FileSystemWatcher watcher = new FileSystemWatcher(root);
return new PhysicalFilesWatcher(root, watcher, _filters);
}
到这里就很简单了,很明显组件用FileSystemWatcher监控了传入的指定的根目录.说明JsonProvider支持配置变更检测.
至于为什么_fileWatcherFactory是个lamdba表达式,是因为这里做了懒加载操作,代码如下:
internal PhysicalFilesWatcher FileWatcher
{
get
{
return LazyInitializer.EnsureInitialized(
ref _fileWatcher,
ref _fileWatcherInitialized,
ref _fileWatcherLock,
_fileWatcherFactory);
}
set
{
Debug.Assert(!_fileWatcherInitialized);
_fileWatcherInitialized = true;
_fileWatcher = value;
}
}
当在PhysicalFileProvider中调用FileWatcher实例时会调用CreateFileWatcher()方法,这个在多线程中表现很好,不会重复初始化Watcher对象.
ok,到这里先不介绍FileWatcher的通知机制,接着解析源码AddJsonFile扩展方法.如下:
public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("path can not be null", nameof(path));
}
return builder.AddJsonFile(s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider();
});
}
参数校验并调用builder.AddJsonFile方法源码如下:
public static IConfigurationBuilder Add<TSource>(this IConfigurationBuilder builder, Action<TSource> configureSource) where TSource : IConfigurationSource, new()
{
var source = new TSource();
configureSource?.Invoke(source);
return builder.Add(source);
}
build.Add方法向ConfigurationBuilder实例添加了JsonConfigurationSource实例
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
Sources.Add(source);
return this;
}
ok,到这里ConfigurationBuilder实例添加了PhysicalFileProvider实例和JsonConfigurationSource实例,接着看JsonConfigurationSource实例的内容
return builder.AddJsonFile(s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ResolveFileProvider();
});
ok,到这里ConfigurationBuilder实例添加了PhysicalFileProvider实例和JsonConfigurationSource实例添加完成.说明ConfigurationBuilder实例相关属性填充完毕,下面就要调用build方法了.build代码如下:
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (IConfigurationSource source in Sources)
{
IConfigurationProvider provider = source.Build(this);
providers.Add(provider);
}
return new ConfigurationRoot(providers);
}
遍历所有的IConfigurationSource,看下source.Build干了什么,代码如下:
public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider(this);
}
接着看EnsureDefaults方法:
public void EnsureDefaults(IConfigurationBuilder builder)
{
FileProvider = FileProvider ?? builder.GetFileProvider();
OnLoadException = OnLoadException ?? builder.GetFileLoadExceptionHandler();
}
应为按照示例代码的调用方式,没有显示传Provider所以,这里从builder实例中获取刚刚写入的PhysicalFileProvider实例,并制定了文件加载异常的回调OnLoadException.
最后获得一个完整的JsonConfigurationSource实例,并根据JsonConfigurationSource实例生成JsonConfigurationProvider实例.到这里可以得出一个结论通过ConfigurationBuilder实例中的IConfigurationSource实例和IFileProvider实例,并通过调用ConfigurationBuilder实例的build方法可以得到JsonConfigurationProvider实例.下面看看JsonConfigurationProvider的代码,如下:
public class JsonConfigurationProvider : FileConfigurationProvider
{
public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }
public override void Load(Stream stream)
{
try
{
Data = JsonConfigurationFileParser.Parse(stream);
}
catch (JsonException e)
{
throw new FormatException(e.Message);
}
}
}
看base中的代码:
public FileConfigurationProvider(FileConfigurationSource source)
{
Source = source ?? throw new ArgumentNullException(nameof(source));
if (Source.ReloadOnChange && Source.FileProvider != null)
{
_changeTokenRegistration = ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() =>
{
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}
}
ok,到这里很清晰了,如果sonConfigurationSource实例中的ReloadOnChange参数设为true,那么就会开启配置文件监听(通过FileSystemWatcher类实现).接着看PhysicalFileProvider实例的Watch方法
public IChangeToken Watch(string filter)
{
if (filter == null || PathUtils.HasInvalidFilterChars(filter))
{
return NullChangeToken.Singleton;
}
filter = filter.TrimStart(_pathSeparators);
return FileWatcher.CreateFileChangeToken(filter);
}
第一步,检测传入的文件名是否服务要求.接着看FileWatcher.CreateFileChangeToken
public IChangeToken CreateFileChangeToken(string filter)
{
if (filter == null)
{
throw new ArgumentNullException(nameof(filter));
}
filter = NormalizePath(filter);
if (Path.IsPathRooted(filter) || PathUtils.PathNavigatesAboveRoot(filter))
{
return NullChangeToken.Singleton;
}
IChangeToken changeToken = GetOrAddChangeToken(filter);
// We made sure that browser/iOS/tvOS never uses FileSystemWatcher.
#pragma warning disable CA1416 // Validate platform compatibility
TryEnableFileSystemWatcher();
#pragma warning restore CA1416 // Validate platform compatibility
return changeToken;
}
看是判断文件是否服务要求,接着看GetOrAddChangeToken(filter);
private IChangeToken GetOrAddChangeToken(string pattern)
{
IChangeToken changeToken;
bool isWildCard = pattern.IndexOf('*') != -1;
if (isWildCard || IsDirectoryPath(pattern))
{
changeToken = GetOrAddWildcardChangeToken(pattern);
}
else
{
changeToken = GetOrAddFilePathChangeToken(pattern);
}
return changeToken;
}
因为这边操作的是文件所以看GetOrAddFilePathChangeToken(pattern)方法
internal IChangeToken GetOrAddFilePathChangeToken(string filePath)
{
if (!_filePathTokenLookup.TryGetValue(filePath, out ChangeTokenInfo tokenInfo))
{
var cancellationTokenSource = new CancellationTokenSource();
var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token);
tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken);
tokenInfo = _filePathTokenLookup.GetOrAdd(filePath, tokenInfo);
}
IChangeToken changeToken = tokenInfo.ChangeToken;
return changeToken;
}
ok,到这里很清晰了,在FileConfigurationProvider端注入了监听令牌,本质就是向上述代码中的_filePathTokenLookup实例写入CancellationTokenSource和CancellationChangeToken实例组合,然后在PhysicalFilesWatcher实例端通过FileSystemWatcher实例注册文件监控事件遍历_filePathTokenLookup所有的令牌根据文件名找到指定的令牌触发令牌,并修改Data集合.配置组件就是通过这种方式实现配置热重载.如果不明白请参考C#下 观察者模式的另一种实现方式IChangeToken和ChangeToken.OnChange源码如下:
private void ReportChangeForMatchedEntries(string path)
{
if (string.IsNullOrEmpty(path))
{
// System.IO.FileSystemWatcher may trigger events that are missing the file name,
// which makes it appear as if the root directory is renamed or deleted. Moving the root directory
// of the file watcher is not supported, so this type of event is ignored.
return;
}
path = NormalizePath(path);
bool matched = false;
if (_filePathTokenLookup.TryRemove(path, out ChangeTokenInfo matchInfo))
{
CancelToken(matchInfo);
matched = true;
}
foreach (KeyValuePair<string, ChangeTokenInfo> wildCardEntry in _wildcardTokenLookup)
{
PatternMatchingResult matchResult = wildCardEntry.Value.Matcher.Match(path);
if (matchResult.HasMatches &&
_wildcardTokenLookup.TryRemove(wildCardEntry.Key, out matchInfo))
{
CancelToken(matchInfo);
matched = true;
}
}
if (matched)
{
//关闭监视
TryDisableFileSystemWatcher();
}
}
private void ReportChangeForMatchedEntries(string path)
{
if (string.IsNullOrEmpty(path))
{
// System.IO.FileSystemWatcher may trigger events that are missing the file name,
// which makes it appear as if the root directory is renamed or deleted. Moving the root directory
// of the file watcher is not supported, so this type of event is ignored.
return;
}
path = NormalizePath(path);
bool matched = false;
if (_filePathTokenLookup.TryRemove(path, out ChangeTokenInfo matchInfo))
{
CancelToken(matchInfo);
matched = true;
}
foreach (KeyValuePair<string, ChangeTokenInfo> wildCardEntry in _wildcardTokenLookup)
{
PatternMatchingResult matchResult = wildCardEntry.Value.Matcher.Match(path);
if (matchResult.HasMatches &&
_wildcardTokenLookup.TryRemove(wildCardEntry.Key, out matchInfo))
{
CancelToken(matchInfo);
matched = true;
}
}
if (matched)
{
//关闭监视
TryDisableFileSystemWatcher();
}
}
通过CancelToken(matchInfo)从而触发FileConfigurationProvider实例的构造函数中注入的自定义回调,回调函数如下,
_changeTokenRegistration = ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() =>
{
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
private void Load(bool reload)
{
IFileInfo file = Source.FileProvider?.GetFileInfo(Source.Path);
if (file == null || !file.Exists)
{
//文件加载可选或者需要reload
if (Source.Optional || reload) // Always optional on reload
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
else
{
var error = new StringBuilder($"{Source.Path} not found");
if (!string.IsNullOrEmpty(file?.PhysicalPath))
{
error.Append($"{file.PhysicalPath} not expected");
}
//包装异常并抛出 因为
HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString())));
}
}
else
{
using (Stream stream = OpenRead(file))
{
try
{
Load(stream);
}
catch
{
if (reload)
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
var exception = new InvalidDataException($"{file.PhysicalPath} 加载失败");
HandleException(ExceptionDispatchInfo.Capture(exception));
}
}
}
OnReload();
}
核心方法是Load方法,其加载了配置文件,且源码如下:
public class JsonConfigurationProvider : FileConfigurationProvider
{
public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }
public override void Load(Stream stream)
{
try
{
Data = JsonConfigurationFileParser.Parse(stream);
}
catch (JsonException e)
{
throw new FormatException(e.Message);
}
}
}
调用了System.Text.Json序列化了文件的内容,并以字典的形式输出.并给ConfigurationProvider的Data属性赋值至于为什么可以通过IConfigurationRoot拿到配置值,因为如下代码:
其本质就是遍历所有的ConfigurationProvider中的Data属性,并取到相应的值.
(3)、复杂类型示例
调用代码如下:
class Program
{
static void Main(string[] args)
{
var workDir = $"{Environment.CurrentDirectory}";
var builder = new ConfigurationBuilder()
.SetBasePath(workDir)
.AddJsonFile($"test.json", optional: true, reloadOnChange: true);
var root = (ConfigurationRoot)builder.Build();
var services = new ServiceCollection();
services.Configure<MySqlDbOptions>(root.GetSection("MySqlDbOptions"));
var provider=services.BuildServiceProvider();
var mySqlDbOptions = provider.GetRequiredService<IOptions<MySqlDbOptions>>().Value;
Console.ReadKey();
}
}
class MySqlDbOptions
{
public List<ChildOptions> Childs { get; set; }
public IDictionary<string, ChildOptions> Dic { get; set; }
}
public class ChildOptions
{
public int Index { get; set; }
public string Name { get; set; }
}
json文件如下:
{
"MySqlDbOptions": {
"ConnectionName": "asdasd",
"ConnectionString": "asdasdasdas",
"Numbers": [ 1, 2, 3 ],
"Childs": [
{
"Index": 1,
"Name": "张三"
},
{
"Index": 2,
"Name": "李四"
}
],
"Dic": [
{
"Index": 1,
"Name": "张三"
},
{
"Index": 1,
"Name": "张三"
}
]
}
}
Options组件几乎兼容所有的常用集合类型包括IEnumerable,和常用的值类型.