Skip to content

Commit 9d1fb4f

Browse files
samsadsamSamSadfa
authored andcommitted
Give the users the ability to have control over ConfigurationClient instance(s) used by the provider (#598)
* Introduced a new `AzureAppConfigurationClientFactory` class to handle the creation of `ConfigurationClient` instances * remove clients dictionary since we will not have hits and clients are already stored in ConfigurationClientManager * revert * add license + remove unused usings * ran dotnet format * add capability of fallback to different stores * add explicit type * address comments * remove scheme validation --------- Co-authored-by: Sami Sadfa <samisadfa@microsoft.com>
1 parent 4b5e523 commit 9d1fb4f

File tree

6 files changed

+128
-79
lines changed

6 files changed

+128
-79
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Azure.Core;
5+
using Azure.Data.AppConfiguration;
6+
using Microsoft.Extensions.Azure;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
11+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
12+
{
13+
internal class AzureAppConfigurationClientFactory : IAzureClientFactory<ConfigurationClient>
14+
{
15+
private readonly ConfigurationClientOptions _clientOptions;
16+
17+
private readonly TokenCredential _credential;
18+
private readonly IEnumerable<string> _connectionStrings;
19+
20+
public AzureAppConfigurationClientFactory(
21+
IEnumerable<string> connectionStrings,
22+
ConfigurationClientOptions clientOptions)
23+
{
24+
if (connectionStrings == null || !connectionStrings.Any())
25+
{
26+
throw new ArgumentNullException(nameof(connectionStrings));
27+
}
28+
29+
_connectionStrings = connectionStrings;
30+
31+
_clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
32+
}
33+
34+
public AzureAppConfigurationClientFactory(
35+
TokenCredential credential,
36+
ConfigurationClientOptions clientOptions)
37+
{
38+
_credential = credential ?? throw new ArgumentNullException(nameof(credential));
39+
_clientOptions = clientOptions ?? throw new ArgumentNullException(nameof(clientOptions));
40+
}
41+
42+
public ConfigurationClient CreateClient(string endpoint)
43+
{
44+
if (string.IsNullOrEmpty(endpoint))
45+
{
46+
throw new ArgumentNullException(nameof(endpoint));
47+
}
48+
49+
if (!Uri.TryCreate(endpoint, UriKind.Absolute, out Uri uriResult))
50+
{
51+
throw new ArgumentException("Invalid host URI.");
52+
}
53+
54+
if (_credential != null)
55+
{
56+
return new ConfigurationClient(uriResult, _credential, _clientOptions);
57+
}
58+
59+
string connectionString = _connectionStrings.FirstOrDefault(cs => ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection) == endpoint);
60+
61+
//
62+
// falback to the first connection string
63+
if (connectionString == null)
64+
{
65+
string id = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.IdSection);
66+
string secret = ConnectionStringUtils.Parse(_connectionStrings.First(), ConnectionStringUtils.SecretSection);
67+
68+
connectionString = ConnectionStringUtils.Build(uriResult, id, secret);
69+
}
70+
71+
return new ConfigurationClient(connectionString, _clientOptions);
72+
}
73+
}
74+
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//
44
using Azure.Core;
55
using Azure.Data.AppConfiguration;
6+
using Microsoft.Extensions.Azure;
67
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
78
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
89
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
@@ -131,6 +132,11 @@ internal IEnumerable<IKeyValueAdapter> Adapters
131132
/// </summary>
132133
internal StartupOptions Startup { get; set; } = new StartupOptions();
133134

135+
/// <summary>
136+
/// Client factory that is responsible for creating instances of ConfigurationClient.
137+
/// </summary>
138+
internal IAzureClientFactory<ConfigurationClient> ClientFactory { get; private set; }
139+
134140
/// <summary>
135141
/// Initializes a new instance of the <see cref="AzureAppConfigurationOptions"/> class.
136142
/// </summary>
@@ -144,6 +150,17 @@ public AzureAppConfigurationOptions()
144150
};
145151
}
146152

153+
/// <summary>
154+
/// Sets the client factory used to create ConfigurationClient instances.
155+
/// </summary>
156+
/// <param name="factory">The client factory.</param>
157+
/// <returns>The current <see cref="AzureAppConfigurationOptions"/> instance.</returns>
158+
public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory<ConfigurationClient> factory)
159+
{
160+
ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory));
161+
return this;
162+
}
163+
147164
/// <summary>
148165
/// Specify what key-values to include in the configuration provider.
149166
/// <see cref="Select"/> can be called multiple times to include multiple sets of key-values.

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33
//
4+
using Azure.Data.AppConfiguration;
5+
using Microsoft.Extensions.Azure;
46
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
59

610
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
711
{
@@ -29,35 +33,33 @@ public IConfigurationProvider Build(IConfigurationBuilder builder)
2933
try
3034
{
3135
AzureAppConfigurationOptions options = _optionsProvider();
32-
IConfigurationClientManager clientManager;
3336

3437
if (options.ClientManager != null)
3538
{
36-
clientManager = options.ClientManager;
39+
return new AzureAppConfigurationProvider(options.ClientManager, options, _optional);
3740
}
38-
else if (options.ConnectionStrings != null)
41+
42+
IEnumerable<Uri> endpoints;
43+
IAzureClientFactory<ConfigurationClient> clientFactory = options.ClientFactory;
44+
45+
if (options.ConnectionStrings != null)
3946
{
40-
clientManager = new ConfigurationClientManager(
41-
options.ConnectionStrings,
42-
options.ClientOptions,
43-
options.ReplicaDiscoveryEnabled,
44-
options.LoadBalancingEnabled);
47+
endpoints = options.ConnectionStrings.Select(cs => new Uri(ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection)));
48+
49+
clientFactory ??= new AzureAppConfigurationClientFactory(options.ConnectionStrings, options.ClientOptions);
4550
}
4651
else if (options.Endpoints != null && options.Credential != null)
4752
{
48-
clientManager = new ConfigurationClientManager(
49-
options.Endpoints,
50-
options.Credential,
51-
options.ClientOptions,
52-
options.ReplicaDiscoveryEnabled,
53-
options.LoadBalancingEnabled);
53+
endpoints = options.Endpoints;
54+
55+
clientFactory ??= new AzureAppConfigurationClientFactory(options.Credential, options.ClientOptions);
5456
}
5557
else
5658
{
5759
throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} to specify how to connect to Azure App Configuration.");
5860
}
5961

60-
provider = new AzureAppConfigurationProvider(clientManager, options, _optional);
62+
provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional);
6163
}
6264
catch (InvalidOperationException ex) // InvalidOperationException is thrown when any problems are found while configuring AzureAppConfigurationOptions or when SDK fails to create a configurationClient.
6365
{

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ConfigurationClientManager.cs

Lines changed: 10 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
// Licensed under the MIT license.
33
//
44

5-
using Azure.Core;
65
using Azure.Data.AppConfiguration;
76
using DnsClient;
87
using DnsClient.Protocol;
8+
using Microsoft.Extensions.Azure;
99
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
1010
using System;
1111
using System.Collections.Generic;
@@ -26,12 +26,11 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
2626
/// </remarks>
2727
internal class ConfigurationClientManager : IConfigurationClientManager, IDisposable
2828
{
29+
private readonly IAzureClientFactory<ConfigurationClient> _clientFactory;
2930
private readonly IList<ConfigurationClientWrapper> _clients;
31+
3032
private readonly Uri _endpoint;
31-
private readonly string _secret;
32-
private readonly string _id;
33-
private readonly TokenCredential _credential;
34-
private readonly ConfigurationClientOptions _clientOptions;
33+
3534
private readonly bool _replicaDiscoveryEnabled;
3635
private readonly SrvLookupClient _srvLookupClient;
3736
private readonly string _validDomain;
@@ -52,61 +51,20 @@ internal class ConfigurationClientManager : IConfigurationClientManager, IDispos
5251
internal int RefreshClientsCalled { get; set; } = 0;
5352

5453
public ConfigurationClientManager(
55-
IEnumerable<string> connectionStrings,
56-
ConfigurationClientOptions clientOptions,
57-
bool replicaDiscoveryEnabled,
58-
bool loadBalancingEnabled)
59-
{
60-
if (connectionStrings == null || !connectionStrings.Any())
61-
{
62-
throw new ArgumentNullException(nameof(connectionStrings));
63-
}
64-
65-
string connectionString = connectionStrings.First();
66-
_endpoint = new Uri(ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.EndpointSection));
67-
_secret = ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.SecretSection);
68-
_id = ConnectionStringUtils.Parse(connectionString, ConnectionStringUtils.IdSection);
69-
_clientOptions = clientOptions;
70-
_replicaDiscoveryEnabled = replicaDiscoveryEnabled;
71-
72-
// If load balancing is enabled, shuffle the passed in connection strings to randomize the endpoint used on startup
73-
if (loadBalancingEnabled)
74-
{
75-
connectionStrings = connectionStrings.ToList().Shuffle();
76-
}
77-
78-
_validDomain = GetValidDomain(_endpoint);
79-
_srvLookupClient = new SrvLookupClient();
80-
81-
_clients = connectionStrings
82-
.Select(cs =>
83-
{
84-
var endpoint = new Uri(ConnectionStringUtils.Parse(cs, ConnectionStringUtils.EndpointSection));
85-
return new ConfigurationClientWrapper(endpoint, new ConfigurationClient(cs, _clientOptions));
86-
})
87-
.ToList();
88-
}
89-
90-
public ConfigurationClientManager(
54+
IAzureClientFactory<ConfigurationClient> clientFactory,
9155
IEnumerable<Uri> endpoints,
92-
TokenCredential credential,
93-
ConfigurationClientOptions clientOptions,
9456
bool replicaDiscoveryEnabled,
9557
bool loadBalancingEnabled)
9658
{
59+
_clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory));
60+
9761
if (endpoints == null || !endpoints.Any())
9862
{
9963
throw new ArgumentNullException(nameof(endpoints));
10064
}
10165

102-
if (credential == null)
103-
{
104-
throw new ArgumentNullException(nameof(credential));
105-
}
106-
10766
_endpoint = endpoints.First();
108-
_credential = credential;
109-
_clientOptions = clientOptions;
67+
11068
_replicaDiscoveryEnabled = replicaDiscoveryEnabled;
11169

11270
// If load balancing is enabled, shuffle the passed in endpoints to randomize the endpoint used on startup
@@ -119,7 +77,7 @@ public ConfigurationClientManager(
11977
_srvLookupClient = new SrvLookupClient();
12078

12179
_clients = endpoints
122-
.Select(endpoint => new ConfigurationClientWrapper(endpoint, new ConfigurationClient(endpoint, _credential, _clientOptions)))
80+
.Select(endpoint => new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)))
12381
.ToList();
12482
}
12583

@@ -289,9 +247,7 @@ private async Task RefreshFallbackClients(CancellationToken cancellationToken)
289247
{
290248
var targetEndpoint = new Uri($"https://{host}");
291249

292-
var configClient = _credential == null
293-
? new ConfigurationClient(ConnectionStringUtils.Build(targetEndpoint, _id, _secret), _clientOptions)
294-
: new ConfigurationClient(targetEndpoint, _credential, _clientOptions);
250+
ConfigurationClient configClient = _clientFactory.CreateClient(targetEndpoint.AbsoluteUri);
295251

296252
newDynamicClients.Add(new ConfigurationClientWrapper(targetEndpoint, configClient));
297253
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.7.0" />
2020
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
2121
<PackageReference Include="DnsClient" Version="1.7.0" />
22+
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.7.6" />
2223
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
2324
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
2425
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />

tests/Tests.AzureAppConfiguration/FailoverTests.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,11 @@ public void FailOverTests_AutoFailover()
268268
[Fact]
269269
public void FailOverTests_ValidateEndpoints()
270270
{
271+
var clientFactory = new AzureAppConfigurationClientFactory(new DefaultAzureCredential(), new ConfigurationClientOptions());
272+
271273
var configClientManager = new ConfigurationClientManager(
274+
clientFactory,
272275
new[] { new Uri("https://foobar.azconfig.io") },
273-
new DefaultAzureCredential(),
274-
new ConfigurationClientOptions(),
275276
true,
276277
false);
277278

@@ -285,9 +286,8 @@ public void FailOverTests_ValidateEndpoints()
285286
Assert.False(configClientManager.IsValidEndpoint("azure.azconfig.bad.io"));
286287

287288
var configClientManager2 = new ConfigurationClientManager(
289+
clientFactory,
288290
new[] { new Uri("https://foobar.appconfig.azure.com") },
289-
new DefaultAzureCredential(),
290-
new ConfigurationClientOptions(),
291291
true,
292292
false);
293293

@@ -301,19 +301,17 @@ public void FailOverTests_ValidateEndpoints()
301301
Assert.False(configClientManager2.IsValidEndpoint("azure.appconfigbad.azure.com"));
302302

303303
var configClientManager3 = new ConfigurationClientManager(
304+
clientFactory,
304305
new[] { new Uri("https://foobar.azconfig-test.io") },
305-
new DefaultAzureCredential(),
306-
new ConfigurationClientOptions(),
307306
true,
308307
false);
309308

310309
Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig-test.io"));
311310
Assert.False(configClientManager3.IsValidEndpoint("azure.azconfig.io"));
312311

313312
var configClientManager4 = new ConfigurationClientManager(
313+
clientFactory,
314314
new[] { new Uri("https://foobar.z1.appconfig-test.azure.com") },
315-
new DefaultAzureCredential(),
316-
new ConfigurationClientOptions(),
317315
true,
318316
false);
319317

@@ -325,10 +323,11 @@ public void FailOverTests_ValidateEndpoints()
325323
[Fact]
326324
public void FailOverTests_GetNoDynamicClient()
327325
{
326+
var clientFactory = new AzureAppConfigurationClientFactory(new DefaultAzureCredential(), new ConfigurationClientOptions());
327+
328328
var configClientManager = new ConfigurationClientManager(
329+
clientFactory,
329330
new[] { new Uri("https://azure.azconfig.io") },
330-
new DefaultAzureCredential(),
331-
new ConfigurationClientOptions(),
332331
true,
333332
false);
334333

0 commit comments

Comments
 (0)