当前位置: 首页 > article >正文

.NET 高级开发 | 配置系统

配置和选项ASP.NET Core 模板项目下会有 appsettings.json、appsettings.Development.json 两个配置文件我们可以通过这两个文件配置 Web 应用的启动端口、是否使用 https 等大多数第三方框架也都支持在这两个 json 文件中配置。ASP.NET Core 程序默认支持从 json 文件、xml 文件、环境变量等多种配置源注入到内存中微服务应用一般会使用远程配置中心存储配置以便动态更新到程序中不管是什么类型的配置源只需要提供 IConfigurationBuilder 的扩展方法给开发者即可开发者不必关注配置源本身的实现细节。而 Microsoft.Extensions.Configuration.Abstractions 定义了统一的接口使用者只需要 注入 IConfiguration 服务即可动态获取配置。在实际生产环境中尤其在微服务场景下我们会有实时更新配置的需求要求服务实例使用集中式的配置中心以便修改配置后所有实例同时更新。通过配置中心管理多个服务的配置以及将实例的开发、测试等环境的配置隔离开来。在本章中笔者将会介绍这些 .NET 中的配置和选项在学会使用方法和原理之后还会介绍如何使用 SignalR 开发一个配置中心不管使用的是控制台还是例如 WPF 这类桌面程序都可以达到 ASP.NET Core 中使用配置的效果。在本小节中我们将会做两个实践项目第一个是实现从文件中读取配置并且在文件修改后能够实时更新配置到内存中第二个是实现一个配置中心能够将远程配置更新到本地。配置和选项配置(Configuration)选项(Options)实现自定义配置提供器实现配置中心读取配置配置拦截配置优先级配置(Configuration)创建一个控制台程序然后引入 Microsoft.Extensions.Configuration 类库我们可以使用 ConfigurationBuilder 类构建配置提供器然后通过扩展包从各种数据源中导入配置。目前Microsoft 官方提供的导入配置源扩展方法有以下类型• 内存键值集合• 配置文件json、xml、yaml 文件等• 环境变量、命令行参数无论是哪种数据源导入到内存中时均以字符串键值对的形式出现。从 json 文件获取配置需要引入Microsoft.Extensions.Configuration.Json包。在项目根目录下创建一个 json 文件内容如下{ test:配置 }然后从 json 中导入配置。var config new ConfigurationBuilder() .AddJsonFile(test.json) .Build(); string test config[test]; Console.WriteLine(test);如果配置文件不在根目录下则可以使用SetBasePath()来定义路径示例如下var config new ConfigurationBuilder() .SetBasePath(E:\\test\\aaa) .AddJsonFile(test.json) .Build();另外json 扩展默认会监听文件的变化如果文件做出了修改那么就会重新读取配置到内存中。config.AddJsonFile(appsettings.json, optional: true, reloadOnChange: true);而从键值对计划中导入配置示例如下var dic newDictionarystring,string() { [test]配置 }; var config newConfigurationBuilder() .AddInMemoryCollection(dic) .Build(); string test config[test];常用的导入配置的扩展方法有builder.Configuration .AddCommandLine(...) .AddEnvironmentVariables(...) .AddIniFile(...) .AddIniStream(...) .AddInMemoryCollection(...) .AddJsonFile(...) .AddJsonStream(...) .AddKeyPerFile(...) .AddUserSecrets(...) .AddXmlFile(...) .AddXmlStream(...);观察AddInMemoryCollection()的扩展方法可以看到本质是创建了一个 MemoryConfigurationSource 实例添加到 IConfigurationBuilder 中。public static IConfigurationBuilder AddInMemoryCollection(this IConfigurationBuilder configurationBuilder) { ThrowHelper.ThrowIfNull(configurationBuilder); configurationBuilder.Add(new MemoryConfigurationSource()); return configurationBuilder; }如果我们要自定义一个数据源需要实现 IConfigurationSource、IConfigurationProvider 两个接口IConfigurationSource 用于生成配置提供器IConfigurationProvider 用于通过 key 获取配置字符串。public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }public interfaceIConfigurationProvider { boolTryGet(string key,outstring?value); voidSet(string key,string?value); IChangeTokenGetReloadToken(); voidLoad(); IEnumerablestringGetChildKeys(IEnumerablestring earlierKeys,string? parentPath); }读取配置在 ASP.NET Core 项目中都会有个 appsettings.json 文件其默认内容如下{ Logging:{ LogLevel:{ Default:Information, Microsoft:Warning, Microsoft.Hosting.Lifetime:Information } } }因为配置在内存中是以键值对出现的我们可以使用:符号获取下一层子项的配置。var config new ConfigurationBuilder() .AddJsonFile(appsettings.json) .Build(); string test config[Logging:LogLevel:Default];image-20230807190141946查找Logging:LogLevel:Default时并不需要先定位Logging再往下查找LogLevel而是直接使用字符串Logging:LogLevel:Default作为 Key 从配置字典中查询对应的 Value。Logging:LogLevel:Default Information Logging:LogLevel:Microsoft Warning通过 json 配置文件我们可以很方便地构建层级结构的配置如果想在字典中存储可以使用{k1}:{k2}这种形式存。例如var dic newDictionarystring,string() { [testParent:Child1]6, [testParent:Child2]666 }; var config newConfigurationBuilder() .AddInMemoryCollection(dic) .Build().GetSection(testParent); string test config[Child1];如果你只想 获取 json 文件中LogLevel部分的配置可以使用GetSection()方法获取 IConfigurationSection 对象这样可以筛选出以当前字符串开头的所有配置那么我们下次使用时就不必提供完整的 Key 了。// json: { Logging:{ LogLevel:{ Default:Information, Microsoft.AspNetCore:Warning } }, AllowedHosts:* } // C# varconfig newConfigurationBuilder() .AddJsonFile(appsettings.json) .Build(); IConfigurationSection section config.GetSection(Logging:LogLevel); string test section[Default];但是这样读取起来很不方便我们可以使用 Microsoft.Extensions.Configuration.Binder 类库里面有大量的扩展方法可以帮助我们将配置字符串转换为强类型。// json: { test:{ Index:1 } } // C#: publicclassTest { publicintIndex{get;set;} } var config newConfigurationBuilder() .AddJsonFile(test.json) .Build(); var section config.GetSection(test); var o section.GetTest();即使源数据没有层次结构我们可以也可以使用Get()方法将配置映射为对象。public classTestOptions { publicstring A {get;set;} publicstring B {get;set;} publicstring C {get;set;} }var dic newDictionarystring,string() { [A]6, [B]66, [C]666 }; TestOptions config newConfigurationBuilder() .AddInMemoryCollection(dic) .Build().GetTestOptions();配置拦截有时从数据源中导入的配置是第三方扩展提供的有些配置无法直接修改因为所有的配置都会以键值对的形式存储在内存中那么我们可以尝试通过增加配置键值对来解决这个问题。比如 Serilog 的配置Serilog 可以在配置文件中设置打印文件日志我们需要在程序运行时确定日志存放到哪里。public staticvoidDynamicLog(thisIServiceCollection services,string customPath) { var configuration services!.BuildServiceProvider().GetRequiredServiceIConfiguration(); // 查找节点 var fileName configuration.AsEnumerable() .Where(x x.Key.StartsWith(Serilog:WriteTo) x.Key.EndsWith(Name) x.Value!.Equals(File)).FirstOrDefault(); // Serilog:WriteTo:0:Name if(!string.IsNullOrEmpty(fileName.Value)) { var key fileName.Key.Replace(Name,Args:path); var path Path.Combine(customPath,log.txt); configuration[key] path; } }配置优先级在 ASP.NET Core 中开发时会使用到appsettings.json、appsettings.Development.json配置文件这两个配置文件都有自己的 IConfigurationSource、IConfigurationProvider。运行时appsettings.Development.json 中的配置会替换 appsettings.json 的配置。其实在于配置的注入顺序例如我们可以手动注入多个 json 配置文件var configuration new ConfigurationBuilder() .AddJsonFile(path: appsettings.json) .AddJsonFile(path: appsettings.Development.json)这个 Configuration 会存在两个配置源// appsettings.json JsonConfigurationSource // appsettings.Development.json JsonConfigurationSource当查找配置时会从 Providers 中倒序查找会首先从 appsettings.Development.json 中查找配置当查找完成后立即返回。因此当我们需要使用自定义配置提供器时可以在最后才加上我们的提供器这样我们自定义的提供器优先级最高。选项(Options)在 ASP.NET Core 中很多中间件的配置是通过选项传递的。比如设置表单上传文件最大为 30MB。// 表单配置 builder.Services.ConfigureFormOptions(options { // 上传的文件最大为 30mb options.MultipartBodyLengthLimit 31_457_280; });这样做的好处是我们使用配置时可以直接使用强类型而不需要关注如何从 IConfiguration 中取出配置。如果我们要获取 TestOptions是通过IOptionsTestOptions来获取的不能直接获取 TestOptions 服务。private readonly TestModel _options; public TestController(IOptionsFormOptions options) { _options options.Value; }配置和选项的最重要差别是配置是用于整个程序以键值对的形式存储信息而选项是给特定模块提供参数使用强类型。示例项目在 Demo3.Options我们创建一个控制台项目引入以下包ItemGroup PackageReferenceIncludeMicrosoft.Extensions.ConfigurationVersion7.0.0/ PackageReferenceIncludeMicrosoft.Extensions.Configuration.JsonVersion7.0.0/ PackageReferenceIncludeMicrosoft.Extensions.DependencyInjectionVersion7.0.0/ PackageReferenceIncludeMicrosoft.Extensions.Options.ConfigurationExtensionsVersion7.0.0/ /ItemGroup添加一个 test.json 文件内容如下{ Title: 测试, Name: 测试测试 }再创建一个与之对应的模型类public classTestOptions { publicstringTitle{get;set;} publicstringName{get;set;} }选项接口主要有三个接口分别是•IOptionsTOptions•IOptionsSnapshotTOptions•IOptionsMonitorTOptions.NET 8 有了新的变化。使用示例如下static voidMain(string[] args) { var services newServiceCollection(); var configuration newConfigurationBuilder() .AddJsonFile(test.json, optional:true, reloadOnChange:true) .Build(); services.AddSingletonIConfiguration(configuration); services.AddOptionsTestOptions().Bind(configuration); // services.ConfigureTestOptions(name: , configuration); // 或者使用 Microsoft.Extensions.Options.ConfigurationExtensions 包 // services.ConfigureTestOptions(configuration); var ioc services.BuildServiceProvider(); var to1 ioc.GetRequiredServiceIOptionsTestOptions(); var to2 ioc.GetRequiredServiceIOptionsSnapshotTestOptions(); var to3 ioc.GetRequiredServiceIOptionsMonitorTestOptions(); to3.OnChange(s { Console.WriteLine($变更之前的值: {s.Name}); }); while(true) { Console.WriteLine($IOptions: {to1.Value.Name}); Console.WriteLine($IOptionsSnapshot: {to2.Value.Name}); Console.WriteLine($IOptionsMonitor: {to3.CurrentValue.Name}); Thread.Sleep(1000); } }我们可以手动修改 test.json 文件观察控制台打印。这三种方式都可以获取到选项它们之间的区别在于生命周期和文件监控等。ASP.NET Core 源代码services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions), typeof(UnnamedOptionsManager))); services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot), typeof(OptionsManager))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor), typeof(OptionsMonitor))); services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory), typeof(OptionsFactory))); services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache), typeof(OptionsCache)));IOptionsTOptions有以下特征注册为单一实例且可以注入到任何服务生存期。不支持在应用启动后读取配置数据。也就是说即使配置源可以动态更新但是IOptionsTOptions不会动态从 IConfiguration 中 最新的配置。也就是说在应用启动前就已经读取配置文件生成对象(单一实例)。当然后续如果修改了配置文件(.json)也不会影响这个对象的。文档解释通过使用IOptionsSnapshotTOptions针对请求生存期访问和缓存选项时每个请求都会计算一次选项。IOptionsSnapshot 的生命作用域是 scoped 在一个请求周期内有效而 IOptionsMonitor 是单例模式但是却可以监听配置的变更。由于 IOptionsSnapshot 每次请求都会进行更新因此配置文件变更后可以及时获得更新。IOptionsMonitor 则略有不同。IOptionsSnapshot 和 IOptionsMonitor 都可以检测到配置文件的更改但是 IOptionsSnapshot 每次请求都是一个新的对象在同一个请求上下文中同一个对象。而 IOptionsMonitor 是单例模式主要用于结合 IConfiguration 使用。但是要注意要使用IOptionsMonitorT很多写法是无效的。比如services.ConfigureTestOptions(o { o.Name new Random().Next(0, 100).ToString(); }); services.ConfigureTestOptions(o { configuration.Bind(o); });实现自定义配置提供器在本节中笔者将会介绍如何编写一个从文件导入的配置提供器并且实现随文件变化动态更新到内存中。代码示例在Demo4.Console中。引入Microsoft.Extensions.FileProviders.Physical用于监听目录、文件变化。我们将来实现读取自定义配置格式的文件创建一个 env.conf 文件内容如下A:111 B:222配置文件通过:号来分隔 key 和 value我们要实现自定义配置提供器需要先编写一个配置源需要继承 IConfigurationSource 接口其接口定义非常简单public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }创建一个 MyConfigurationSource 类型代码如下public classMyConfigurationSource:IConfigurationSource { // 配置文件的路径 publicstringPath{get;set;} // 是否实时监听此文件变化 publicboolReloadOnChange{get;set;} public IConfigurationProvider Build(IConfigurationBuilder builder) { returnnewMyConfigurationProvider(this); } }接着定义配置提供器配置提供器需要存储配置信息提供配置查询等接口。代码示例如下public classMyConfigurationProvider:IConfigurationProvider { privatereadonlyMyConfigurationSource _source; privatereadonlyIFileProvider _fileProvider; privatereadonlystring _path; privatereadonlystring _fileName; // 缓存 privatereadonlyDictionarystring,string _cache; publicMyConfigurationProvider(MyConfigurationSource source) { _source source; _cache newDictionarystring,string(); _path Directory.GetParent(_source.Path)!.FullName; _fileName Path.GetFileName(_source.Path); _fileProvider newPhysicalFileProvider(_path); if(_source.ReloadOnChange) { // 监听配置文件变化 ChangeToken.OnChange(() _fileProvider.Watch(_fileName),async()awaitReloadFileAsync()); } else { ReloadFileAsync().Wait(); } } // 重新加载配置文件到内存中 privateasyncTaskReloadFileAsync() { usingvar stream _fileProvider.GetFileInfo(_fileName).CreateReadStream(); usingvar streamReader newStreamReader(stream); _cache.Clear(); while(true) { var line await streamReader.ReadLineAsync(); if(line null)break; var kv line.Split(:)[0..2].Select(x x.Trim( )).ToArray(); _cache.Add(kv[0], kv[1]); } } publicIEnumerablestringGetChildKeys(IEnumerablestring earlierKeys,string? parentPath) _cache.Keys; publicIChangeTokenGetReloadToken()null; publicvoidLoad() { ReloadFileAsync().Wait(); } publicvoidSet(string key,string?value) { _cache[key]value!; } publicboolTryGet(string key,outstring?value) { return _cache.TryGetValue(key,outvalue); } }接着定义扩展以便支持外部依赖注入public staticclassExtensions { publicstaticIConfigurationBuilderAddEnvFile(thisIConfigurationBuilder builder,string path,bool reloadOnChange false) { var source newMyConfigurationSource() { Path path, ReloadOnChange reloadOnChange }; builder.Add(source); return builder; } }然后使用我们自定义的配置提供器static void Main() { var configuration newConfigurationBuilder() .AddEnvFile(env.conf,true) .Build(); while(true) { varvalue configuration[A]; Console.WriteLine($A {value}); Thread.Sleep(1000); } }启动程序后修改程序运行目录下的 env.conf 文件查看控制台中的输出检查控制台的输出是否与修改后的文件一致。实现配置中心在了解配置、选项的使用方法以及如何自定义配置提供器之后在本小节我们将会创建一个配置中心服务然后客户端通过 SignalR 与配置中心通讯当配置中心的配置内容被修改后自动推送到客户端中。新建一个名为 Demo3.ConfigCenter 的 API 项目和一个名为 Demo3.ConfigClient 的控制台项目。首先是实现配置中心的 Demo3.ConfigCenter 的代码创建一个保存客户端信息的模型类。/// summary /// 客户端的信息 /// /summary publicclassClientInfo { /// summary /// SignalR 连接的 id /// /summary publicstringConnectionId{get;set;} /// summary /// 应用名称 /// /summary publicstringAppName{get;set;} /// summary /// 命名空间 /// /summary publicstringNamespace{get;set;} /// summary /// 分组名称 /// /summary publicstringGroupName${AppName}-{Namespace}; /// summary /// 客户端的 IP 地址 /// /summary publicstringIpAddress{get;set;} }创建一个用于 SignalR 通讯的 Hub 服务与客户端进行实时通信。public partialclassConfigCenterHub:Hub { // 客户端连接信息 privatestaticreadonlyConcurrentDictionarystring,ClientInfo _clients new(); // 在内存中保存每个服务的配置 privatestaticreadonlyConcurrentDictionarystring,JsonObject _settings new(); privatereadonlyIHubContextConfigCenterHub _hubContext; publicConfigCenterHub(IHubContextConfigCenterHub hubContext) { _hubContext hubContext; } // 当客户端连接到服务时 publicoverrideasyncTaskOnConnectedAsync() { ClientInfo clientnInfo GetInfo(); await _hubContext.Groups.AddToGroupAsync(clientnInfo.ConnectionId, clientnInfo.GroupName); _clients[clientnInfo.GroupName] clientnInfo; } // 当客户端断开服务时 publicoverrideasyncTaskOnDisconnectedAsync(Exception? exception) { ClientInfo clientnInfo GetInfo(); await _hubContext.Groups.RemoveFromGroupAsync(clientnInfo.ConnectionId, clientnInfo.GroupName); _clients.TryRemove(clientnInfo.ConnectionId,out _); } // 获取客户端的信息 privateClientInfoGetInfo() { var feature Context.Features.GetIHttpConnectionFeature(); var httpContext Context.GetHttpContext(); ArgumentNullException.ThrowIfNull(feature); ArgumentNullException.ThrowIfNull(httpContext); // 从 header 中查询信息 var appName httpContext.Request.Headers[AppName].FirstOrDefault(); var namespaceName httpContext.Request.Headers[Namespace].FirstOrDefault(); ArgumentNullException.ThrowIfNull(appName); ArgumentNullException.ThrowIfNull(namespaceName); var groupName ${appName}-{namespaceName}; // 获取客户端通讯地址 var remoteAddress feature.RemoteIpAddress; ArgumentNullException.ThrowIfNull(remoteAddress); var remotePort feature.RemotePort; returnnewClientInfo { ConnectionId feature.ConnectionId, AppName appName, Namespace namespaceName, IpAddress${remoteAddress.MapToIPv4().ToString()}:{remotePort} }; } // 客户端自行获取配置 publicasyncTaskJsonObjectGetAsync() { ClientInfo clientnInfo GetInfo(); if(_settings.TryGetValue(clientnInfo.GroupName,outvar v)) { return v; } var dic newDictionarystring,JsonNode().ToList(); returnnewJsonObject(dic); } // 更新缓存 publicvoidUpdateCache(string appName,string namespaceName,JsonObject json) { var groupName ${appName}-{namespaceName}; _settings[groupName] json; } }然后注册 Hub 服务... ... builder.Services.AddSwaggerGen(); // 注入 SignalR builder.Services.AddSignalR(); builder.Services.AddScopedConfigCenterHub(); var app builder.Build(); ...... app.MapControllers(); // 加入 Hub 中间件 app.MapHubConfigCenterHub(/config); app.Run(http://*:5000);创建一个 ConfigController 控制器允许通过 API 修改配置中心的内容以及在修改配置后推送到对应的客户端中。[ApiController] [Route([controller])] publicclassConfigController:ControllerBase { privatereadonlyConfigCenterHub _configCenter; privatereadonlyIHubContextConfigCenterHub _hubContext; publicConfigController(IHubContextConfigCenterHub hubContext,ConfigCenterHub configCenter) { _hubContext hubContext; _configCenter configCenter; } [HttpPost(update)] publicasyncTaskstringUpdate(string appName,string namespaceName,[FromBody]JsonObject json) { var groupName ${appName}-{namespaceName}; _configCenter.UpdateCache(appName, namespaceName, json); await _hubContext.Clients.Group(groupName).SendAsync(Publish, json); return已更新配置; } }接下来就是客户端部分。在程序启动时会读取目录的 tmp_config.json 文件注入到配置中如果文件不存在则创建。然后使用框架自带的 JsonConfigurationProvider 为我们动态监听 json 文件的变化减少我们的代码量。在实时监听 Json 文件变化以及解析 json 这部分可以利用官方的 JsonConfigurationSource 来实现我们就不需要重新写一个了。然后使用 SignalR 与配置中心通讯将配置中心的内容写入到临时文件 tmp_config.json 中JsonConfigurationProvider 会自动将修改后的 json 文件加载到内存中。因为我们使用了本地配置文件获取到的配置先缓存在本地中所以当下次程序启动时或网络出现故障时程序依然可以通过本地缓存配置启动起来。这个也是很多配置中心都有实现的。在 Demo3.ConfigClient 中创建 OnlineConfigurationSource、OnlineConfigurationProvider 文件写入以下代码public classOnlineConfigurationSource:IConfigurationSource { /// summary /// 获取最新配置的 API 路径 /// /summary publicstring URL {get;init;} publicstringAppName{get;init;} publicstringNamespace{get;init;} publicIConfigurationProviderBuild(IConfigurationBuilder builder) { returnnewOnlineConfigurationProvider(this, builder); } }然后实现一个配置提供器与 Hub 服务器实时通信并更新到 tmp_config.json 文件中。public classOnlineConfigurationProvider:IConfigurationProvider,IDisposable { privateconststringTmpFiletmp_config.json; privatereadonlystring _jsonPath; privatereadonlyOnlineConfigurationSource _configurationSource; privatereadonlyJsonConfigurationSource _jsonSource; privatereadonlyIConfigurationProvider _provider; privatereadonlyHubConnection _connection; publicOnlineConfigurationProvider(OnlineConfigurationSource configurationSource,IConfigurationBuilder builder) { // 使用框架自带的 JsonConfigurationSource 动态获取 json 文件的内容 var curPath Directory.GetParent(typeof(OnlineConfigurationProvider).Assembly.Location).FullName; _jsonPath Path.Combine(curPath,TmpFile); if(!File.Exists(TmpFile))File.WriteAllText(_jsonPath,{}); _configurationSource configurationSource; _jsonSource newJsonConfigurationSource() { PathTmpFile, ReloadOnChangetrue, }; _provider _jsonSource.Build(builder); // 配置 SignalR 通讯将新的内容写入到 json 文件 _connection newHubConnectionBuilder() .WithUrl(_configurationSource.URL, options { options.Headers.Add(AppName, _configurationSource.AppName); options.Headers.Add(Namespace, _configurationSource.Namespace); }) .WithAutomaticReconnect() .Build(); _connection.OnJsonObject(Publish,async(json) { awaitSaveJsonAsync(json); }); _connection.StartAsync().Wait(); var json _connection.InvokeAsyncJsonObject(GetAsync).Result; SaveJsonAsync(json).Wait(); } privateasyncTaskSaveJsonAsync(JsonObject json) { // 每次清空文件重新写入内容 usingFileStream fs newFileStream(_jsonPath,FileMode.Truncate,FileAccess.ReadWrite); awaitSystem.Text.Json.JsonSerializer.SerializeAsync(fs, json); Console.WriteLine($已更新配置{System.Text.Json.JsonSerializer.Serialize(json)}); } privatebool _disposedValue; ~OnlineConfigurationProvider()Dispose(false); publicvoidDispose() { Dispose(true); GC.SuppressFinalize(this); } protectedvirtualvoidDispose(bool disposing) { if(!_disposedValue) { if(disposing) { _connection.DisposeAsync(); } _disposedValue true; } } publicIEnumerablestringGetChildKeys(IEnumerablestring earlierKeys,string? parentPath) _provider.GetChildKeys(earlierKeys, parentPath); publicIChangeTokenGetReloadToken() _provider.GetReloadToken(); publicvoidLoad() _provider.Load(); publicvoidSet(string key,string?value) _provider.Set(key,value); publicboolTryGet(string key,outstring?value) _provider.TryGet(key,outvalue); }编写扩展方法配置 Hub 服务。public staticclassExtensions { // 添加远程配置 publicstaticIConfigurationBuilderAddReomteConfig(thisIConfigurationBuilder builder,string url,string appName,stringnamespace) { var source newOnlineConfigurationSource() { URL url, AppName appName, Namespacenamespace }; builder.Add(source); return builder; } }使用配置中心服务internal classProgram { staticvoidMain(string[] args) { Thread.Sleep(5000); var builder newConfigurationBuilder() .AddReomteConfig(http://127.0.0.1:5000/config,myapp,dev); var config builder.Build(); while(true) { Console.WriteLine(config[Name]); Thread.Sleep(1000); } } }同时启动配置中心和客户端打开配置中心的 Swagger 地址修改并推送新的配置到客户端。image-20230917194903641

相关文章:

.NET 高级开发 | 配置系统

配置和选项ASP.NET Core 模板项目下会有 appsettings.json、appsettings.Development.json 两个配置文件,我们可以通过这两个文件配置 Web 应用的启动端口、是否使用 https 等,大多数第三方框架也都支持在这两个 json 文件中配置。ASP.NET Core 程序默认…...

最开放的Gemma 4来了——谷歌:没人比我更懂“不作恶”。

2026 年 4 月 2 日,谷歌 DeepMind 发布了 Gemma 4 系列模型[1]。模型有四个规格——E2B、E4B、26B MoE、31B Dense——覆盖了从树莓派到单卡 H100 的全硬件区间,31B 在 Arena 开源榜冲到第三,26B MoE 只激活 38 亿参数就打出了同级 Dense 模型…...

OpenClaw自动化测试:Qwen3-14b_int4_awq驱动接口调试与结果验证

OpenClaw自动化测试:Qwen3-14b_int4_awq驱动接口调试与结果验证 1. 为什么选择OpenClaw做接口测试自动化 去年接手一个前后端分离项目时,我每天要手动执行上百次Postman请求来验证接口逻辑。这种重复劳动不仅消耗时间,更可怕的是容易因疲劳…...

锁定一致性与音画同步:Grok 2.0 预热释放了哪些 AI 视频商用信号?

一、 引言:AI 视频商用化进程中的“最后公里”在生成式 AI(AIGC)领域,视频生成一直被视为皇冠上的明珠。然而,从实验室的惊艳 Demo 到真正的商业化落地,开发者们始终面临着两个顽固的“幽灵”:时…...

别再手动一篇篇点了!用Python脚本5分钟搞定PubMed文献批量下载(附完整代码)

科研效率革命:Python全自动抓取PubMed文献的进阶实战指南 深夜的实验室里,咖啡杯已经见底,而你的文献列表还有47篇待下载——这个场景对每个科研工作者都不陌生。传统的手动点击下载不仅耗时耗力,还容易因网络波动或操作失误导致前…...

告别重复提问:手把手教你用Continue的YAML配置文件打造专属AI编程助手

告别重复提问:用YAML配置文件打造你的AI编程伙伴 每次打开IDE准备写代码时,你是否也厌倦了反复输入那些相似的提示词?"生成单元测试"、"添加注释"、"解释这段代码"... 这些重复性请求不仅浪费时间,…...

OBS+B站直播保姆级教程:从软件安装到弹幕互动全流程解析

OBSB站直播保姆级教程:从软件安装到弹幕互动全流程解析 第一次开直播就像第一次上台演讲,手忙脚乱是常态。记得我刚开始用OBS直播时,明明调试了好几天,开播瞬间还是把麦克风静音键当成了推流按钮。本文将带你避开所有新手坑&…...

【gis系列】从等高线到地形分析:dem生成与高程、坡度、坡向解析

1. 从等高线到DEM:数据准备与处理 搞地形分析的朋友们都知道,DEM(数字高程模型)是我们的基础粮草。但很多人卡在第一步——怎么把原始等高线数据变成可用的DEM?我当年第一次做这个的时候,踩了不少坑&#x…...

2025届学术党必备的十大AI学术助手实际效果

Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 在学术研究范畴之内,AI论文工具正一步步转变成为学者以及学生的得力帮手。这种类…...

5分钟搞定!国内最稳定的NTP校时服务器推荐(含华为云、阿里云配置指南)

国内企业级NTP服务器配置实战指南 时间同步对于现代IT系统的重要性不言而喻。无论是金融交易的时间戳、分布式系统的日志同步,还是安全证书的有效期验证,毫秒级的时间误差都可能导致严重后果。在国内网络环境下,直接使用国际公共NTP服务器往往…...

【树莓派4B】Ubuntu Mate20.04系统配置与ROS Noetic安装全攻略:从烧录到远程桌面控制

1. 树莓派4B与Ubuntu Mate20.04系统烧录 树莓派4B作为一款高性能的单板计算机,在机器人开发和嵌入式项目中广受欢迎。而Ubuntu Mate20.04系统凭借其轻量级和稳定性,成为树莓派上的理想选择。我最近在做一个自主导航项目时,就采用了这个组合方…...

OpenGL渲染与几何内核那点事-项目实践理论补充(三-1-(2):当你的CAD代码变得“又大又乱”:从手动编译到CMake,从随性编码到单元测试))

TOC 代码仓库入口: github源码地址。gitee源码地址。 系列文章规划: …见内容管理OpenGL渲染与几何内核那点事-项目实践理论补充(一-1-(8)-番外篇:当你的 CAD 遇上“活”的零件)OpenGL渲染与几何内核那点事-项目实践理论补充(一-2-(1)-当你…...

Meshlab实战指南:从稀疏点云到纹理模型的完整流程

1. Meshlab入门:为什么选择它处理3D重建数据? 第一次接触三维建模的朋友可能会问:Meshlab到底是什么?简单来说,它是一款开源的3D网格处理软件,特别擅长处理从照片重建出来的三维数据。我在实际项目中用它处…...

Delphi FMX实战:如何优化电商App图片加载性能(附GYListView高效缓存方案)

Delphi FMX电商App图片加载性能优化实战指南 电商类App的核心体验往往取决于商品图片的加载速度和流畅度。当用户快速滑动浏览上百件商品时,任何卡顿或延迟都会直接影响转化率。作为跨平台开发框架,Delphi FMX虽然提供了强大的UI构建能力,但在…...

埃因霍温理工大学:冷冻编码器也能完美分割图像?

这项由埃因霍温理工大学领导的研究发表于2026年3月的arXiv预印本论文库,论文编号为arXiv:2603.25398v1。对于想要深入了解这项技术突破的读者,可以通过该编号查询到完整的技术论文。当前的人工智能就像一位技艺高超的多面手,能够处理各种复杂…...

钙钛矿材料的“电控开关“:罗格斯大学实现光发射强度近100%调节

这项由美国罗格斯大学物理与天文系以及英国帝国理工学院化学系联合开展的研究发表于2026年3月17日,研究成果展现了一种全新的光电器件控制方式。感兴趣深入了解的读者可以查阅完整论文获取更多技术细节。如果把发光材料比作一个可调光的台灯,那么传统方法…...

Lightricks公司推出AVControl:像搭积木一样训练音视频控制模型

这项由以色列Lightricks公司主导的研究发表于2026年3月,论文编号为arXiv:2603.24793v1。想要深入了解技术细节的读者可以通过该编号查询完整论文。当你在刷短视频时,有没有想过这样一个问题:如果有一天,你只需要用嘴巴描述一下想要…...

嵌入式C语言开发核心要点与优化策略

1. 嵌入式C语言开发的核心差异在通用计算机领域,C语言往往被视为一种"中级语言",但在嵌入式系统中,它却是当之无愧的王者。我从事嵌入式开发已有八年,从智能家居到工业控制,C语言始终是项目的主力语言。与桌…...

OpenClaw任务监控方案:千问3.5-35B-A3B-FP8执行日志分析

OpenClaw任务监控方案:千问3.5-35B-A3B-FP8执行日志分析 1. 为什么需要任务监控 当我第一次在本地部署千问3.5-35B-A3B-FP8模型并接入OpenClaw时,最让我头疼的就是任务执行过程中的"黑箱"问题。模型会突然卡住,或者返回的结果与预…...

IOSignal:面向Arduino的轻量级WebRTC信令库

1. IOSignal Arduino 客户端库技术解析IOSignal 是一个面向嵌入式 WebRTC 场景的轻量级信令协议栈,专为资源受限的 Arduino 平台设计。其核心价值不在于替代 WebRTC 数据通道,而在于以极低开销完成端到端连接建立前的关键握手环节——即信令交换&#xf…...

嵌入式事件驱动架构与lwevt库实战解析

1. 嵌入式事件驱动架构的价值与挑战 在资源受限的嵌入式系统中,传统轮询式架构常面临两大痛点:一是CPU资源被无效占用,二是实时响应能力受限。我曾在一个智能家居网关项目中,就遇到过传感器数据采集与网络通信相互阻塞的情况——当…...

OpenClaw自动化测试:Phi-3-vision-128k-instruct多模态能力边界

OpenClaw自动化测试:Phi-3-vision-128k-instruct多模态能力边界 1. 测试背景与实验设计 去年在尝试用AI处理技术文档时,我发现纯文本模型经常无法理解流程图中的逻辑关系。这促使我开始探索多模态模型的实际能力边界。最近通过OpenClaw对接Phi-3-visio…...

SpringBoot集成Sqlite3+mybatisPlus+Druid实战指南与避坑手册

1. 为什么选择Sqlite3MybatisPlusDruid组合 在轻量级应用开发中,Sqlite3因其零配置、无服务器的特性成为嵌入式数据库的首选。我去年开发一个物联网设备管理系统时,就遇到过需要本地存储设备运行数据的场景。当时尝试过H2和Derby,最终发现Sql…...

OpenClaw旅行规划:Qwen3.5-9B整合机票酒店信息生成行程表

OpenClaw旅行规划:Qwen3.5-9B整合机票酒店信息生成行程表 1. 为什么需要AI旅行规划助手 上个月计划带家人去三亚度假时,我花了整整三个晚上对比不同平台的机票和酒店价格。在十几个浏览器标签页间反复切换,手动记录价格波动,最后…...

OpenClaw跨平台控制:Qwen3-32B同步操作多台设备的配置方法

OpenClaw跨平台控制:Qwen3-32B同步操作多台设备的配置方法 1. 为什么需要分布式OpenClaw控制? 去年冬天,当我需要在三台不同操作系统的设备上同步执行数据清洗任务时,第一次意识到单机OpenClaw的局限性。每台设备需要单独登录、…...

跨平台文件同步:OpenClaw+千问3.5-9B实现智能归档

跨平台文件同步:OpenClaw千问3.5-9B实现智能归档 1. 为什么需要智能文件同步? 作为一个经常在MacBook、Windows台式机和Linux服务器之间切换的开发者,我长期被文件同步问题困扰。传统的同步工具(如rsync或Syncthing)…...

极客玩法:OpenClaw+千问3.5-35B-A3B-FP8实现智能家居控制中枢

极客玩法:OpenClaw千问3.5-35B-A3B-FP8实现智能家居控制中枢 1. 为什么需要AI控制智能家居? 去年装修新房时,我安装了全套HomeAssistant智能设备。但很快发现一个问题:不同品牌的设备需要各自APP控制,语音助手只能执…...

交流与直流接触器:原理差异与工程防护

1. 交流接触器与直流接触器的本质区别接触器作为电气控制领域的核心元件,其线圈设计直接决定了工作特性。从业十余年来,我处理过太多因误接电源导致的设备故障案例。让我们从电磁原理层面,彻底搞懂这两种接触器的差异。交流接触器线圈采用粗线…...

DS3234高精度RTC驱动库:±2ppm温补时钟与双闹钟SRAM应用

1. 项目概述Soldered DS3234 RTC 是一款面向嵌入式系统的高精度实时时钟(RTC)驱动库,专为 Soldered 公司设计的 DS3234 SPI 接口 RTC 模块开发。该库并非简单封装底层寄存器访问,而是围绕 DS3234 芯片三大核心能力构建&#xff1a…...

React + TypeScript 实战:安全高效集成 OpenAI API 的进阶指南

1. 为什么选择ReactTypeScript集成OpenAI API 在当今的前端开发领域,React和TypeScript已经成为构建现代化Web应用的首选技术栈。当我们需要集成像OpenAI API这样的AI服务时,这个组合能带来显著的优势。 TypeScript的静态类型检查可以在开发阶段就捕获许…...