ServiceCollectionExtensionsTests.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846
  1. using Microsoft.Extensions.Configuration;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Microsoft.Extensions.Options;
  4. namespace Apq.Cfg.Tests;
  5. /// <summary>
  6. /// 依赖注入扩展测试
  7. /// </summary>
  8. public class ServiceCollectionExtensionsTests : IDisposable
  9. {
  10. private readonly string _testDir;
  11. public ServiceCollectionExtensionsTests()
  12. {
  13. _testDir = Path.Combine(Path.GetTempPath(), $"ApqCfgTests_{Guid.NewGuid():N}");
  14. Directory.CreateDirectory(_testDir);
  15. }
  16. public void Dispose()
  17. {
  18. if (Directory.Exists(_testDir))
  19. {
  20. Directory.Delete(_testDir, true);
  21. }
  22. }
  23. #region AddApqCfg 基本功能
  24. [Fact]
  25. public void AddApqCfg_RegistersICfgRoot()
  26. {
  27. // Arrange
  28. var jsonPath = Path.Combine(_testDir, "config.json");
  29. File.WriteAllText(jsonPath, """{"AppName": "TestApp"}""");
  30. var services = new ServiceCollection();
  31. // Act
  32. services.AddApqCfg(cfg => cfg
  33. .AddJson(jsonPath, level: 0, writeable: false));
  34. var provider = services.BuildServiceProvider();
  35. // Assert
  36. var cfgRoot = provider.GetService<ICfgRoot>();
  37. Assert.NotNull(cfgRoot);
  38. Assert.Equal("TestApp", cfgRoot.Get("AppName"));
  39. }
  40. [Fact]
  41. public void AddApqCfg_RegistersIConfigurationRoot()
  42. {
  43. // Arrange
  44. var jsonPath = Path.Combine(_testDir, "config.json");
  45. File.WriteAllText(jsonPath, """{"AppName": "TestApp"}""");
  46. var services = new ServiceCollection();
  47. // Act
  48. services.AddApqCfg(cfg => cfg
  49. .AddJson(jsonPath, level: 0, writeable: false));
  50. var provider = services.BuildServiceProvider();
  51. // Assert
  52. var msConfig = provider.GetService<IConfigurationRoot>();
  53. Assert.NotNull(msConfig);
  54. Assert.Equal("TestApp", msConfig["AppName"]);
  55. }
  56. [Fact]
  57. public void AddApqCfg_RegistersAsSingleton()
  58. {
  59. // Arrange
  60. var jsonPath = Path.Combine(_testDir, "config.json");
  61. File.WriteAllText(jsonPath, """{"Key": "Value"}""");
  62. var services = new ServiceCollection();
  63. services.AddApqCfg(cfg => cfg
  64. .AddJson(jsonPath, level: 0, writeable: false));
  65. var provider = services.BuildServiceProvider();
  66. // Act
  67. var cfg1 = provider.GetService<ICfgRoot>();
  68. var cfg2 = provider.GetService<ICfgRoot>();
  69. // Assert
  70. Assert.Same(cfg1, cfg2);
  71. }
  72. #endregion
  73. #region AddApqCfg 工厂方法
  74. [Fact]
  75. public void AddApqCfg_WithFactory_RegistersICfgRoot()
  76. {
  77. // Arrange
  78. var jsonPath = Path.Combine(_testDir, "config.json");
  79. File.WriteAllText(jsonPath, """{"FactoryTest": "Success"}""");
  80. var services = new ServiceCollection();
  81. // Act
  82. services.AddApqCfg(sp =>
  83. {
  84. return new CfgBuilder()
  85. .AddJson(jsonPath, level: 0, writeable: false)
  86. .Build();
  87. });
  88. var provider = services.BuildServiceProvider();
  89. // Assert
  90. var cfgRoot = provider.GetService<ICfgRoot>();
  91. Assert.NotNull(cfgRoot);
  92. Assert.Equal("Success", cfgRoot.Get("FactoryTest"));
  93. }
  94. #endregion
  95. #region ConfigureApqCfg 强类型配置
  96. [Fact]
  97. public void ConfigureApqCfg_BindsOptions()
  98. {
  99. // Arrange
  100. var jsonPath = Path.Combine(_testDir, "config.json");
  101. File.WriteAllText(jsonPath, """
  102. {
  103. "Database": {
  104. "Host": "localhost",
  105. "Port": 5432,
  106. "Name": "testdb"
  107. }
  108. }
  109. """);
  110. var services = new ServiceCollection();
  111. services.AddApqCfg(cfg => cfg
  112. .AddJson(jsonPath, level: 0, writeable: false));
  113. services.ConfigureApqCfg<DatabaseOptions>("Database");
  114. var provider = services.BuildServiceProvider();
  115. // Act
  116. var options = provider.GetRequiredService<IOptions<DatabaseOptions>>().Value;
  117. // Assert
  118. Assert.Equal("localhost", options.Host);
  119. Assert.Equal(5432, options.Port);
  120. Assert.Equal("testdb", options.Name);
  121. }
  122. [Fact]
  123. public void AddApqCfg_WithOptions_BindsOptions()
  124. {
  125. // Arrange
  126. var jsonPath = Path.Combine(_testDir, "config.json");
  127. File.WriteAllText(jsonPath, """
  128. {
  129. "App": {
  130. "Name": "MyApp",
  131. "Version": "2.0.0",
  132. "Debug": true
  133. }
  134. }
  135. """);
  136. var services = new ServiceCollection();
  137. services.AddApqCfg<AppOptions>(
  138. cfg => cfg.AddJson(jsonPath, level: 0, writeable: false),
  139. sectionKey: "App");
  140. var provider = services.BuildServiceProvider();
  141. // Act
  142. var options = provider.GetRequiredService<IOptions<AppOptions>>().Value;
  143. // Assert
  144. Assert.Equal("MyApp", options.Name);
  145. Assert.Equal("2.0.0", options.Version);
  146. Assert.True(options.Debug);
  147. }
  148. [Fact]
  149. public void ConfigureApqCfg_WithNestedSection_BindsCorrectly()
  150. {
  151. // Arrange
  152. var jsonPath = Path.Combine(_testDir, "config.json");
  153. File.WriteAllText(jsonPath, """
  154. {
  155. "Services": {
  156. "Api": {
  157. "Url": "https://api.example.com",
  158. "Timeout": 30
  159. }
  160. }
  161. }
  162. """);
  163. var services = new ServiceCollection();
  164. services.AddApqCfg(cfg => cfg
  165. .AddJson(jsonPath, level: 0, writeable: false));
  166. services.ConfigureApqCfg<ApiOptions>("Services:Api");
  167. var provider = services.BuildServiceProvider();
  168. // Act
  169. var options = provider.GetRequiredService<IOptions<ApiOptions>>().Value;
  170. // Assert
  171. Assert.Equal("https://api.example.com", options.Url);
  172. Assert.Equal(30, options.Timeout);
  173. }
  174. #endregion
  175. #region 类型转换测试
  176. [Fact]
  177. public void ConfigureApqCfg_ConvertsVariousTypes()
  178. {
  179. // Arrange
  180. var jsonPath = Path.Combine(_testDir, "config.json");
  181. File.WriteAllText(jsonPath, """
  182. {
  183. "TypeTest": {
  184. "IntValue": 42,
  185. "LongValue": 9223372036854775807,
  186. "BoolValue": true,
  187. "DoubleValue": 3.14159,
  188. "DecimalValue": 123.456,
  189. "GuidValue": "550e8400-e29b-41d4-a716-446655440000",
  190. "DateTimeValue": "2024-01-15T10:30:00"
  191. }
  192. }
  193. """);
  194. var services = new ServiceCollection();
  195. services.AddApqCfg(cfg => cfg
  196. .AddJson(jsonPath, level: 0, writeable: false));
  197. services.ConfigureApqCfg<TypeTestOptions>("TypeTest");
  198. var provider = services.BuildServiceProvider();
  199. // Act
  200. var options = provider.GetRequiredService<IOptions<TypeTestOptions>>().Value;
  201. // Assert
  202. Assert.Equal(42, options.IntValue);
  203. Assert.Equal(9223372036854775807L, options.LongValue);
  204. Assert.True(options.BoolValue);
  205. Assert.Equal(3.14159, options.DoubleValue, 5);
  206. Assert.Equal(123.456m, options.DecimalValue);
  207. Assert.Equal(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"), options.GuidValue);
  208. Assert.Equal(new DateTime(2024, 1, 15, 10, 30, 0), options.DateTimeValue);
  209. }
  210. [Fact]
  211. public void ConfigureApqCfg_ConvertsEnumType()
  212. {
  213. // Arrange
  214. var jsonPath = Path.Combine(_testDir, "config.json");
  215. File.WriteAllText(jsonPath, """
  216. {
  217. "Logging": {
  218. "Level": "Warning"
  219. }
  220. }
  221. """);
  222. var services = new ServiceCollection();
  223. services.AddApqCfg(cfg => cfg
  224. .AddJson(jsonPath, level: 0, writeable: false));
  225. services.ConfigureApqCfg<LoggingOptions>("Logging");
  226. var provider = services.BuildServiceProvider();
  227. // Act
  228. var options = provider.GetRequiredService<IOptions<LoggingOptions>>().Value;
  229. // Assert
  230. Assert.Equal(LogLevel.Warning, options.Level);
  231. }
  232. #endregion
  233. #region 边界情况
  234. [Fact]
  235. public void ConfigureApqCfg_NonExistentSection_ReturnsDefaultValues()
  236. {
  237. // Arrange
  238. var jsonPath = Path.Combine(_testDir, "config.json");
  239. File.WriteAllText(jsonPath, """{"Other": "Value"}""");
  240. var services = new ServiceCollection();
  241. services.AddApqCfg(cfg => cfg
  242. .AddJson(jsonPath, level: 0, writeable: false));
  243. services.ConfigureApqCfg<DatabaseOptions>("NonExistent");
  244. var provider = services.BuildServiceProvider();
  245. // Act
  246. var options = provider.GetRequiredService<IOptions<DatabaseOptions>>().Value;
  247. // Assert
  248. Assert.Null(options.Host);
  249. Assert.Equal(0, options.Port);
  250. Assert.Null(options.Name);
  251. }
  252. [Fact]
  253. public void AddApqCfg_CalledTwice_UsesFirstRegistration()
  254. {
  255. // Arrange
  256. var jsonPath1 = Path.Combine(_testDir, "config1.json");
  257. var jsonPath2 = Path.Combine(_testDir, "config2.json");
  258. File.WriteAllText(jsonPath1, """{"Source": "First"}""");
  259. File.WriteAllText(jsonPath2, """{"Source": "Second"}""");
  260. var services = new ServiceCollection();
  261. // Act - 第二次调用应该被忽略(TryAddSingleton)
  262. services.AddApqCfg(cfg => cfg.AddJson(jsonPath1, level: 0, writeable: false));
  263. services.AddApqCfg(cfg => cfg.AddJson(jsonPath2, level: 0, writeable: false));
  264. var provider = services.BuildServiceProvider();
  265. var cfgRoot = provider.GetRequiredService<ICfgRoot>();
  266. // Assert
  267. Assert.Equal("First", cfgRoot.Get("Source"));
  268. }
  269. #endregion
  270. #region IOptionsMonitor 测试
  271. [Fact]
  272. public void ConfigureApqCfg_RegistersIOptionsMonitor()
  273. {
  274. // Arrange
  275. var jsonPath = Path.Combine(_testDir, "config.json");
  276. File.WriteAllText(jsonPath, """
  277. {
  278. "Database": {
  279. "Host": "localhost",
  280. "Port": 5432
  281. }
  282. }
  283. """);
  284. var services = new ServiceCollection();
  285. services.AddApqCfg(cfg => cfg
  286. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true));
  287. services.ConfigureApqCfg<DatabaseOptions>("Database");
  288. var provider = services.BuildServiceProvider();
  289. // Act
  290. var monitor = provider.GetService<IOptionsMonitor<DatabaseOptions>>();
  291. // Assert
  292. Assert.NotNull(monitor);
  293. Assert.Equal("localhost", monitor.CurrentValue.Host);
  294. Assert.Equal(5432, monitor.CurrentValue.Port);
  295. }
  296. [Fact]
  297. public async Task IOptionsMonitor_NotifiesOnChange()
  298. {
  299. // Arrange
  300. var jsonPath = Path.Combine(_testDir, "config.json");
  301. File.WriteAllText(jsonPath, """
  302. {
  303. "Database": {
  304. "Host": "localhost",
  305. "Port": 5432
  306. }
  307. }
  308. """);
  309. var services = new ServiceCollection();
  310. services.AddApqCfg(cfg => cfg
  311. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true));
  312. services.ConfigureApqCfg<DatabaseOptions>("Database");
  313. var provider = services.BuildServiceProvider();
  314. var monitor = provider.GetRequiredService<IOptionsMonitor<DatabaseOptions>>();
  315. var cfgRoot = provider.GetRequiredService<ICfgRoot>();
  316. var changeNotified = false;
  317. string? newHost = null;
  318. monitor.OnChange((options, _) =>
  319. {
  320. changeNotified = true;
  321. newHost = options.Host;
  322. });
  323. // Act - 直接修改文件触发变更(而不是通过 Set 方法)
  324. // 因为 ConfigChanges 是通过文件监视器触发的
  325. await Task.Delay(100); // 等待文件监视器初始化
  326. File.WriteAllText(jsonPath, """
  327. {
  328. "Database": {
  329. "Host": "newhost.local",
  330. "Port": 5432
  331. }
  332. }
  333. """);
  334. // 等待变更通知(文件监视器 + 防抖)
  335. await Task.Delay(500);
  336. // Assert
  337. Assert.True(changeNotified);
  338. Assert.Equal("newhost.local", newHost);
  339. Assert.Equal("newhost.local", monitor.CurrentValue.Host);
  340. }
  341. #endregion
  342. #region IOptionsSnapshot 测试
  343. [Fact]
  344. public void ConfigureApqCfg_RegistersIOptionsSnapshot()
  345. {
  346. // Arrange
  347. var jsonPath = Path.Combine(_testDir, "config.json");
  348. File.WriteAllText(jsonPath, """
  349. {
  350. "Database": {
  351. "Host": "localhost",
  352. "Port": 5432
  353. }
  354. }
  355. """);
  356. var services = new ServiceCollection();
  357. services.AddApqCfg(cfg => cfg
  358. .AddJson(jsonPath, level: 0, writeable: false));
  359. services.ConfigureApqCfg<DatabaseOptions>("Database");
  360. var provider = services.BuildServiceProvider();
  361. // Act - IOptionsSnapshot 是 Scoped,需要创建 scope
  362. using var scope = provider.CreateScope();
  363. var snapshot = scope.ServiceProvider.GetService<IOptionsSnapshot<DatabaseOptions>>();
  364. // Assert
  365. Assert.NotNull(snapshot);
  366. Assert.Equal("localhost", snapshot.Value.Host);
  367. Assert.Equal(5432, snapshot.Value.Port);
  368. }
  369. #endregion
  370. #region 嵌套对象绑定测试
  371. [Fact]
  372. public void ConfigureApqCfg_BindsNestedObjects()
  373. {
  374. // Arrange
  375. var jsonPath = Path.Combine(_testDir, "config.json");
  376. File.WriteAllText(jsonPath, """
  377. {
  378. "App": {
  379. "Name": "TestApp",
  380. "Database": {
  381. "Host": "db.local",
  382. "Port": 3306
  383. },
  384. "Cache": {
  385. "Host": "redis.local",
  386. "Port": 6379
  387. }
  388. }
  389. }
  390. """);
  391. var services = new ServiceCollection();
  392. services.AddApqCfg(cfg => cfg
  393. .AddJson(jsonPath, level: 0, writeable: false));
  394. services.ConfigureApqCfg<AppWithNestedOptions>("App");
  395. var provider = services.BuildServiceProvider();
  396. // Act
  397. var options = provider.GetRequiredService<IOptions<AppWithNestedOptions>>().Value;
  398. // Assert
  399. Assert.Equal("TestApp", options.Name);
  400. Assert.NotNull(options.Database);
  401. Assert.Equal("db.local", options.Database.Host);
  402. Assert.Equal(3306, options.Database.Port);
  403. Assert.NotNull(options.Cache);
  404. Assert.Equal("redis.local", options.Cache.Host);
  405. Assert.Equal(6379, options.Cache.Port);
  406. }
  407. [Fact]
  408. public void ConfigureApqCfg_BindsDeeplyNestedObjects()
  409. {
  410. // Arrange
  411. var jsonPath = Path.Combine(_testDir, "config.json");
  412. File.WriteAllText(jsonPath, """
  413. {
  414. "Root": {
  415. "Level1": {
  416. "Level2": {
  417. "Value": "DeepValue"
  418. }
  419. }
  420. }
  421. }
  422. """);
  423. var services = new ServiceCollection();
  424. services.AddApqCfg(cfg => cfg
  425. .AddJson(jsonPath, level: 0, writeable: false));
  426. services.ConfigureApqCfg<DeepNestedOptions>("Root");
  427. var provider = services.BuildServiceProvider();
  428. // Act
  429. var options = provider.GetRequiredService<IOptions<DeepNestedOptions>>().Value;
  430. // Assert
  431. Assert.NotNull(options.Level1);
  432. Assert.NotNull(options.Level1.Level2);
  433. Assert.Equal("DeepValue", options.Level1.Level2.Value);
  434. }
  435. #endregion
  436. #region 集合绑定测试
  437. [Fact]
  438. public void ConfigureApqCfg_BindsStringArray()
  439. {
  440. // Arrange
  441. var jsonPath = Path.Combine(_testDir, "config.json");
  442. File.WriteAllText(jsonPath, """
  443. {
  444. "Config": {
  445. "Tags": {
  446. "0": "tag1",
  447. "1": "tag2",
  448. "2": "tag3"
  449. }
  450. }
  451. }
  452. """);
  453. var services = new ServiceCollection();
  454. services.AddApqCfg(cfg => cfg
  455. .AddJson(jsonPath, level: 0, writeable: false));
  456. services.ConfigureApqCfg<ArrayOptions>("Config");
  457. var provider = services.BuildServiceProvider();
  458. // Act
  459. var options = provider.GetRequiredService<IOptions<ArrayOptions>>().Value;
  460. // Assert
  461. Assert.NotNull(options.Tags);
  462. Assert.Collection(options.Tags,
  463. item => Assert.Equal("tag1", item),
  464. item => Assert.Equal("tag2", item),
  465. item => Assert.Equal("tag3", item));
  466. }
  467. [Fact]
  468. public void ConfigureApqCfg_BindsIntList()
  469. {
  470. // Arrange
  471. var jsonPath = Path.Combine(_testDir, "config.json");
  472. File.WriteAllText(jsonPath, """
  473. {
  474. "Config": {
  475. "Ports": {
  476. "0": 80,
  477. "1": 443,
  478. "2": 8080
  479. }
  480. }
  481. }
  482. """);
  483. var services = new ServiceCollection();
  484. services.AddApqCfg(cfg => cfg
  485. .AddJson(jsonPath, level: 0, writeable: false));
  486. services.ConfigureApqCfg<ListOptions>("Config");
  487. var provider = services.BuildServiceProvider();
  488. // Act
  489. var options = provider.GetRequiredService<IOptions<ListOptions>>().Value;
  490. // Assert
  491. Assert.NotNull(options.Ports);
  492. Assert.Collection(options.Ports,
  493. item => Assert.Equal(80, item),
  494. item => Assert.Equal(443, item),
  495. item => Assert.Equal(8080, item));
  496. }
  497. [Fact]
  498. public void ConfigureApqCfg_BindsDictionary()
  499. {
  500. // Arrange
  501. var jsonPath = Path.Combine(_testDir, "config.json");
  502. File.WriteAllText(jsonPath, """
  503. {
  504. "Config": {
  505. "Settings": {
  506. "Key1": "Value1",
  507. "Key2": "Value2"
  508. }
  509. }
  510. }
  511. """);
  512. var services = new ServiceCollection();
  513. services.AddApqCfg(cfg => cfg
  514. .AddJson(jsonPath, level: 0, writeable: false));
  515. services.ConfigureApqCfg<DictionaryOptions>("Config");
  516. var provider = services.BuildServiceProvider();
  517. // Act
  518. var options = provider.GetRequiredService<IOptions<DictionaryOptions>>().Value;
  519. // Assert
  520. Assert.NotNull(options.Settings);
  521. Assert.Collection(options.Settings.OrderBy(x => x.Key),
  522. item => { Assert.Equal("Key1", item.Key); Assert.Equal("Value1", item.Value); },
  523. item => { Assert.Equal("Key2", item.Key); Assert.Equal("Value2", item.Value); });
  524. }
  525. [Fact]
  526. public void ConfigureApqCfg_BindsComplexObjectList()
  527. {
  528. // Arrange
  529. var jsonPath = Path.Combine(_testDir, "config.json");
  530. File.WriteAllText(jsonPath, """
  531. {
  532. "Config": {
  533. "Endpoints": {
  534. "0": {
  535. "Host": "api1.local",
  536. "Port": 8001
  537. },
  538. "1": {
  539. "Host": "api2.local",
  540. "Port": 8002
  541. }
  542. }
  543. }
  544. }
  545. """);
  546. var services = new ServiceCollection();
  547. services.AddApqCfg(cfg => cfg
  548. .AddJson(jsonPath, level: 0, writeable: false));
  549. services.ConfigureApqCfg<ComplexListOptions>("Config");
  550. var provider = services.BuildServiceProvider();
  551. // Act
  552. var options = provider.GetRequiredService<IOptions<ComplexListOptions>>().Value;
  553. // Assert
  554. Assert.NotNull(options.Endpoints);
  555. Assert.Collection(options.Endpoints,
  556. item => { Assert.Equal("api1.local", item.Host); Assert.Equal(8001, item.Port); },
  557. item => { Assert.Equal("api2.local", item.Host); Assert.Equal(8002, item.Port); });
  558. }
  559. #endregion
  560. #region ConfigureApqCfg 带变更回调测试
  561. [Fact]
  562. public async Task ConfigureApqCfg_WithOnChange_InvokesCallback()
  563. {
  564. // Arrange
  565. var jsonPath = Path.Combine(_testDir, "config-callback.json");
  566. File.WriteAllText(jsonPath, """
  567. {
  568. "Database": {
  569. "Host": "localhost",
  570. "Port": 5432
  571. }
  572. }
  573. """);
  574. var services = new ServiceCollection();
  575. services.AddApqCfg(cfg => cfg
  576. .AddJson(jsonPath, level: 0, writeable: true, isPrimaryWriter: true, reloadOnChange: true));
  577. var callbackInvoked = false;
  578. string? newHost = null;
  579. services.ConfigureApqCfg<DatabaseOptions>("Database", options =>
  580. {
  581. callbackInvoked = true;
  582. newHost = options.Host;
  583. });
  584. var provider = services.BuildServiceProvider();
  585. // 触发 IOptionsMonitor 的创建和回调注册
  586. var monitor = provider.GetRequiredService<IOptionsMonitor<DatabaseOptions>>();
  587. // 解析 IDisposable 服务以确保回调被注册
  588. var disposables = provider.GetServices<IDisposable>().ToList();
  589. // Act - 直接修改文件触发变更
  590. await Task.Delay(200); // 等待文件监视器初始化
  591. File.WriteAllText(jsonPath, """
  592. {
  593. "Database": {
  594. "Host": "callback.local",
  595. "Port": 5432
  596. }
  597. }
  598. """);
  599. // 等待变更通知(文件监视器 + 防抖 + 处理)
  600. await Task.Delay(800);
  601. // Assert
  602. Assert.True(callbackInvoked);
  603. Assert.Equal("callback.local", newHost);
  604. }
  605. #endregion
  606. #region 测试用选项类
  607. public class DatabaseOptions
  608. {
  609. public string? Host { get; set; }
  610. public int Port { get; set; }
  611. public string? Name { get; set; }
  612. }
  613. public class AppOptions
  614. {
  615. public string? Name { get; set; }
  616. public string? Version { get; set; }
  617. public bool Debug { get; set; }
  618. }
  619. public class ApiOptions
  620. {
  621. public string? Url { get; set; }
  622. public int Timeout { get; set; }
  623. }
  624. public class TypeTestOptions
  625. {
  626. public int IntValue { get; set; }
  627. public long LongValue { get; set; }
  628. public bool BoolValue { get; set; }
  629. public double DoubleValue { get; set; }
  630. public decimal DecimalValue { get; set; }
  631. public Guid GuidValue { get; set; }
  632. public DateTime DateTimeValue { get; set; }
  633. }
  634. public enum LogLevel
  635. {
  636. Debug,
  637. Info,
  638. Warning,
  639. Error
  640. }
  641. public class LoggingOptions
  642. {
  643. public LogLevel Level { get; set; }
  644. }
  645. // 嵌套对象测试用类
  646. public class AppWithNestedOptions
  647. {
  648. public string? Name { get; set; }
  649. public DatabaseOptions? Database { get; set; }
  650. public CacheOptions? Cache { get; set; }
  651. }
  652. public class CacheOptions
  653. {
  654. public string? Host { get; set; }
  655. public int Port { get; set; }
  656. }
  657. public class DeepNestedOptions
  658. {
  659. public Level1Options? Level1 { get; set; }
  660. }
  661. public class Level1Options
  662. {
  663. public Level2Options? Level2 { get; set; }
  664. }
  665. public class Level2Options
  666. {
  667. public string? Value { get; set; }
  668. }
  669. // 集合测试用类
  670. public class ArrayOptions
  671. {
  672. public string[]? Tags { get; set; }
  673. }
  674. public class ListOptions
  675. {
  676. public List<int>? Ports { get; set; }
  677. }
  678. public class DictionaryOptions
  679. {
  680. public Dictionary<string, string>? Settings { get; set; }
  681. }
  682. public class ComplexListOptions
  683. {
  684. public List<EndpointOptions>? Endpoints { get; set; }
  685. }
  686. public class EndpointOptions
  687. {
  688. public string? Host { get; set; }
  689. public int Port { get; set; }
  690. }
  691. #endregion
  692. }