Skip to content

Commit 53a6dac

Browse files
committed
Migrate IOptions<T> to IOptionsMonitor<T> for live AppConfig reload
1 parent 6b5cb6a commit 53a6dac

12 files changed

+66
-34
lines changed

src/SEBT.Portal.Infrastructure/Dependencies.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static IServiceCollection AddPortalInfrastructureServices(this IServiceCo
4040

4141
services.AddHttpClient("Smarty", (sp, client) =>
4242
{
43-
var smarty = sp.GetRequiredService<IOptions<SmartySettings>>().Value;
43+
var smarty = sp.GetRequiredService<IOptionsMonitor<SmartySettings>>().CurrentValue;
4444
var baseUrl = string.IsNullOrWhiteSpace(smarty.BaseUrl)
4545
? "https://us-street.api.smartystreets.com"
4646
: smarty.BaseUrl.TrimEnd('/');
@@ -52,16 +52,17 @@ public static IServiceCollection AddPortalInfrastructureServices(this IServiceCo
5252
services.AddTransient<PassThroughAddressUpdateService>();
5353
services.AddTransient<IAddressUpdateService>(sp =>
5454
{
55-
var smarty = sp.GetRequiredService<IOptions<SmartySettings>>().Value;
55+
var smarty = sp.GetRequiredService<IOptionsMonitor<SmartySettings>>().CurrentValue;
5656
return smarty.Enabled
5757
? sp.GetRequiredService<SmartyAddressUpdateService>()
5858
: sp.GetRequiredService<PassThroughAddressUpdateService>();
5959
});
6060
services.AddTransient<IAddressValidationService, AddressValidationServiceAdapter>();
6161
services.AddSingleton<IIdentifierHasher, IdentifierHasher>();
6262

63-
// Expose SocureSettings directly for use case injection (avoids IOptions dependency in UseCases layer)
64-
services.AddSingleton(sp => sp.GetRequiredService<IOptions<SocureSettings>>().Value);
63+
// Expose SocureSettings directly for use case injection (avoids IOptions dependency in UseCases layer).
64+
// Transient so each request gets the current value from IOptionsMonitor, supporting live AppConfig reload.
65+
services.AddTransient(sp => sp.GetRequiredService<IOptionsMonitor<SocureSettings>>().CurrentValue);
6566

6667
// Socure client — disabled, stub, or real based on configuration
6768
var socureEnabled = configuration.GetValue<bool>("Socure:Enabled");
@@ -71,7 +72,7 @@ public static IServiceCollection AddPortalInfrastructureServices(this IServiceCo
7172
services.AddTransient<HttpSocureClient>();
7273
services.AddTransient<ISocureClient>(sp =>
7374
{
74-
var settings = sp.GetRequiredService<IOptions<SocureSettings>>().Value;
75+
var settings = sp.GetRequiredService<IOptionsMonitor<SocureSettings>>().CurrentValue;
7576
if (settings.UseStub)
7677
return sp.GetRequiredService<StubSocureClient>();
7778

src/SEBT.Portal.Infrastructure/Services/HouseholdIdentifierResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ public class HouseholdIdentifierResolver : IHouseholdIdentifierResolver
2424
private readonly ILogger<HouseholdIdentifierResolver>? _logger;
2525

2626
public HouseholdIdentifierResolver(
27-
IOptions<StateHouseholdIdSettings> settings,
27+
IOptionsMonitor<StateHouseholdIdSettings> optionsMonitor,
2828
IUserRepository userRepository,
2929
IPhoneOverrideProvider phoneOverrideProvider,
3030
ILogger<HouseholdIdentifierResolver>? logger = null)
3131
{
32-
_settings = settings.Value;
32+
_settings = optionsMonitor.CurrentValue;
3333
_userRepository = userRepository;
3434
_phoneOverrideProvider = phoneOverrideProvider;
3535
_logger = logger;

src/SEBT.Portal.Infrastructure/Services/HttpSocureClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ namespace SEBT.Portal.Infrastructure.Services;
1919
/// </summary>
2020
public class HttpSocureClient(
2121
IHttpClientFactory httpClientFactory,
22-
IOptions<SocureSettings> socureSettings,
22+
IOptionsMonitor<SocureSettings> socureSettings,
2323
ILogger<HttpSocureClient> logger) : ISocureClient
2424
{
2525
private static readonly JsonSerializerOptions JsonOptions = new()
@@ -36,7 +36,7 @@ public async Task<Result<IdProofingAssessmentResult>> RunIdProofingAssessmentAsy
3636
string? idValue,
3737
CancellationToken cancellationToken = default)
3838
{
39-
var settings = socureSettings.Value;
39+
var settings = socureSettings.CurrentValue;
4040

4141
var request = BuildEvaluationRequest(userId, email, dateOfBirth, idType, idValue, settings);
4242
var jsonContent = JsonSerializer.Serialize(request, JsonOptions);

src/SEBT.Portal.Infrastructure/Services/IdProofingRequirementsService.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,25 @@ namespace SEBT.Portal.Infrastructure.Services;
1313
/// </summary>
1414
public class IdProofingRequirementsService : IIdProofingRequirementsService
1515
{
16-
private readonly IdProofingRequirementsSettings _settings;
16+
private readonly IOptionsMonitor<IdProofingRequirementsSettings> _optionsMonitor;
1717
private readonly ILogger<IdProofingRequirementsService> _logger;
1818

1919
public IdProofingRequirementsService(
20-
IOptions<IdProofingRequirementsSettings> settings,
20+
IOptionsMonitor<IdProofingRequirementsSettings> optionsMonitor,
2121
ILogger<IdProofingRequirementsService> logger)
2222
{
23-
_settings = settings.Value;
23+
_optionsMonitor = optionsMonitor;
2424
_logger = logger;
2525
}
2626

2727
/// <inheritdoc />
2828
public PiiVisibility GetPiiVisibility(UserIalLevel userIalLevel)
2929
{
30+
var settings = _optionsMonitor.CurrentValue;
3031
return new PiiVisibility(
31-
IncludeAddress: MeetsRequirement("Address", _settings.AddressView, userIalLevel),
32-
IncludeEmail: MeetsRequirement("Email", _settings.EmailView, userIalLevel),
33-
IncludePhone: MeetsRequirement("Phone", _settings.PhoneView, userIalLevel));
32+
IncludeAddress: MeetsRequirement("Address", settings.AddressView, userIalLevel),
33+
IncludeEmail: MeetsRequirement("Email", settings.EmailView, userIalLevel),
34+
IncludePhone: MeetsRequirement("Phone", settings.PhoneView, userIalLevel));
3435
}
3536

3637
private bool MeetsRequirement(string fieldName, IalLevel requirement, UserIalLevel userIalLevel)

src/SEBT.Portal.Infrastructure/Services/PassThroughAddressUpdateService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace SEBT.Portal.Infrastructure.Services;
1111
/// When Smarty is disabled, performs trimming/ZIP formatting and enforces <see cref="AddressValidationPolicySettings"/>
1212
/// (e.g. General Delivery) without calling an external API.
1313
/// </summary>
14-
public sealed class PassThroughAddressUpdateService(IOptions<AddressValidationPolicySettings> policySettings)
14+
public sealed class PassThroughAddressUpdateService(IOptionsMonitor<AddressValidationPolicySettings> policySettings)
1515
: IAddressUpdateService
1616
{
1717
public Task<Result<AddressUpdateSuccess>> ValidateAndNormalizeAsync(
@@ -20,7 +20,7 @@ public Task<Result<AddressUpdateSuccess>> ValidateAndNormalizeAsync(
2020
{
2121
cancellationToken.ThrowIfCancellationRequested();
2222

23-
var policy = policySettings.Value;
23+
var policy = policySettings.CurrentValue;
2424
var normalized = AddressNormalizationHelper.TrimToAddress(
2525
request.StreetAddress1,
2626
request.StreetAddress2,

src/SEBT.Portal.Infrastructure/Services/SmartyAddressUpdateService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ namespace SEBT.Portal.Infrastructure.Services;
1818
/// </summary>
1919
public sealed class SmartyAddressUpdateService(
2020
IHttpClientFactory httpClientFactory,
21-
IOptions<SmartySettings> smartySettings,
22-
IOptions<AddressValidationPolicySettings> policySettings,
21+
IOptionsMonitor<SmartySettings> smartySettings,
22+
IOptionsMonitor<AddressValidationPolicySettings> policySettings,
2323
ILogger<SmartyAddressUpdateService> logger) : IAddressUpdateService
2424
{
2525
private static readonly JsonSerializerOptions SmartyRequestJsonOptions = new()
@@ -31,8 +31,8 @@ public async Task<Result<AddressUpdateSuccess>> ValidateAndNormalizeAsync(
3131
AddressUpdateOperationRequest request,
3232
CancellationToken cancellationToken = default)
3333
{
34-
var policy = policySettings.Value;
35-
var settings = smartySettings.Value;
34+
var policy = policySettings.CurrentValue;
35+
var settings = smartySettings.CurrentValue;
3636

3737
var inputAddress = AddressNormalizationHelper.TrimToAddress(
3838
request.StreetAddress1,

test/SEBT.Portal.Tests/Integration/HttpSocureClientSmokeTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Text.Json;
44
using Microsoft.Extensions.Logging;
55
using Microsoft.Extensions.Options;
6+
using NSubstitute;
67
using SEBT.Portal.Core.AppSettings;
78
using SEBT.Portal.Core.Services;
89
using SEBT.Portal.Infrastructure.Services;
@@ -38,7 +39,10 @@ private HttpSocureClient CreateRealClient(SocureSettings? settingsOverride = nul
3839
var factory = new SingleClientFactory(httpClient);
3940
var logger = new TestOutputLogger<HttpSocureClient>(output);
4041

41-
return new HttpSocureClient(factory, Options.Create(settings), logger);
42+
var monitor = Substitute.For<IOptionsMonitor<SocureSettings>>();
43+
monitor.CurrentValue.Returns(settings);
44+
45+
return new HttpSocureClient(factory, monitor, logger);
4246
}
4347

4448
[Fact]

test/SEBT.Portal.Tests/Unit/Infrastructure/Services/HttpSocureClientTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ private HttpSocureClient CreateClient(HttpMessageHandler handler)
3030
var factory = Substitute.For<IHttpClientFactory>();
3131
factory.CreateClient("Socure").Returns(httpClient);
3232

33+
var monitor = Substitute.For<IOptionsMonitor<SocureSettings>>();
34+
monitor.CurrentValue.Returns(settings);
35+
3336
return new HttpSocureClient(
3437
factory,
35-
Options.Create(settings),
38+
monitor,
3639
NullLogger<HttpSocureClient>.Instance);
3740
}
3841

test/SEBT.Portal.Tests/Unit/Infrastructure/Services/PassThroughAddressUpdateServiceTests.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Options;
2+
using NSubstitute;
23
using SEBT.Portal.Core.AppSettings;
34
using SEBT.Portal.Core.Models.AddressUpdate;
45
using SEBT.Portal.Infrastructure.Services;
@@ -8,11 +9,17 @@ namespace SEBT.Portal.Tests.Unit.Infrastructure.Services;
89

910
public class PassThroughAddressUpdateServiceTests
1011
{
12+
private static PassThroughAddressUpdateService CreateService(AddressValidationPolicySettings policy)
13+
{
14+
var monitor = Substitute.For<IOptionsMonitor<AddressValidationPolicySettings>>();
15+
monitor.CurrentValue.Returns(policy);
16+
return new PassThroughAddressUpdateService(monitor);
17+
}
18+
1119
[Fact]
1220
public async Task ValidateAndNormalizeAsync_AllowsGeneralDelivery_WhenPolicyAllows()
1321
{
14-
var service = new PassThroughAddressUpdateService(
15-
Options.Create(new AddressValidationPolicySettings { AllowGeneralDelivery = true }));
22+
var service = CreateService(new AddressValidationPolicySettings { AllowGeneralDelivery = true });
1623

1724
var request = new AddressUpdateOperationRequest
1825
{
@@ -32,8 +39,7 @@ public async Task ValidateAndNormalizeAsync_AllowsGeneralDelivery_WhenPolicyAllo
3239
[Fact]
3340
public async Task ValidateAndNormalizeAsync_RejectsGeneralDelivery_WhenPolicyDisallows()
3441
{
35-
var service = new PassThroughAddressUpdateService(
36-
Options.Create(new AddressValidationPolicySettings { AllowGeneralDelivery = false }));
42+
var service = CreateService(new AddressValidationPolicySettings { AllowGeneralDelivery = false });
3743

3844
var request = new AddressUpdateOperationRequest
3945
{

test/SEBT.Portal.Tests/Unit/Infrastructure/Services/SmartyAddressUpdateServiceTests.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@ private static SmartyAddressUpdateService CreateService(
4141
var factory = Substitute.For<IHttpClientFactory>();
4242
factory.CreateClient("Smarty").Returns(httpClient);
4343

44+
var smartyMonitor = Substitute.For<IOptionsMonitor<SmartySettings>>();
45+
smartyMonitor.CurrentValue.Returns(SmartySettings);
46+
var policyMonitor = Substitute.For<IOptionsMonitor<AddressValidationPolicySettings>>();
47+
policyMonitor.CurrentValue.Returns(policy ?? AllowGeneralDelivery);
48+
4449
return new SmartyAddressUpdateService(
4550
factory,
46-
Options.Create(SmartySettings),
47-
Options.Create(policy ?? AllowGeneralDelivery),
51+
smartyMonitor,
52+
policyMonitor,
4853
NullLogger<SmartyAddressUpdateService>.Instance);
4954
}
5055

0 commit comments

Comments
 (0)