Skip to content

Commit f058b86

Browse files
authored
Refactor the build method of ServiceManager to remove internal dependency (Azure#32603)
# The core changes * In `ServiceManagerStore.cs`, refactor the build method of `ServiceManager` to remove internal dependency. The original implementation builds the `ServiceManager` from a `ServiceCollection` directly, and inject `IConfigureOptions<ServiceManagerOptions>` and `IOptionsChangeTokenSource<ServiceManagerOptions>` to support hot reload. Without internal depdencies, we have to use the `ServiceManagerBuilder` to support hot-reload. The only way of `ServiceManagerBuilder` to support hot-reload is to inject a `IConfiguration` object which provides a change token. However, the SDK and function extensions have different ways to parse configuration, for example, SDK lacks the functionality to parse identity-based connection from configuration. Therefore, we have to pass the actual configuration action via `ServiceManagerBuilder.WithOptions`, and injects an `IConfiguration` object to provide change token. To avoid errors thrown by SDK when it parses the configuration, we wraps the real `IConfiguration` object to make it looks like empty configuration but provides the true change token. * Remove the internal access of `AccessKey` and also fix the problem that access keys used in signature validation are not hot-reloadable. Without internal access, we have to pass the configuration again to get the access keys.
1 parent 6a06b6b commit f058b86

21 files changed

+363
-154
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Microsoft.Azure.SignalR.Management;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.Options;
9+
using Microsoft.Extensions.Primitives;
10+
11+
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
12+
{
13+
/// <summary>
14+
/// This configuration does nothing but provides a reload token from a real <see cref="IConfiguration"/> object.
15+
/// It aims to trigger a configuration reload of <see cref="ServiceHubContext"/> when the real configuration changes. As the <see cref="ServiceManagerBuilder"/> doesn't provide an API to inject an <see cref="IOptionsChangeTokenSource{ServiceManagerOptions}"/> and it has a different way to read configuration with function extensions, we have to inject an empty configuration via <see cref="ServiceManagerBuilder.WithConfiguration(IConfiguration)"/> to provide a reload token and does actual configuration parsing via <see cref="ServiceManagerBuilder.WithOptions(Action{ServiceManagerOptions})"/>.
16+
/// </summary>
17+
internal class EmptyConfiguration : IConfiguration
18+
{
19+
private static readonly IConfiguration EmptyImpl = new ConfigurationBuilder().AddInMemoryCollection().Build();
20+
private readonly IConfiguration _configuration;
21+
22+
public EmptyConfiguration(IConfiguration configuration)
23+
{
24+
_configuration = configuration;
25+
}
26+
27+
public string this[string key] { get => null; set { } }
28+
29+
public IEnumerable<IConfigurationSection> GetChildren() => Array.Empty<IConfigurationSection>();
30+
31+
public IChangeToken GetReloadToken() => _configuration.GetReloadToken();
32+
33+
public IConfigurationSection GetSection(string key) => EmptyImpl.GetSection(key);
34+
}
35+
}

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/IInternalServiceHubContextStore.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
using System;
55
using System.Threading.Tasks;
6-
using Microsoft.Azure.SignalR;
76
using Microsoft.Azure.SignalR.Management;
7+
using Microsoft.Extensions.Options;
88

99
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
1010
{
1111
internal interface IInternalServiceHubContextStore : IServiceHubContextStore, IAsyncDisposable, IDisposable
1212
{
13-
AccessKey[] AccessKeys { get; }
13+
IOptionsMonitor<SignatureValidationOptions> SignatureValidationOptions { get; }
1414

1515
ValueTask<ServiceHubContext<T>> GetAsync<T>(string hubName) where T : class;
1616
}

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/OptionsSetup.cs

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,72 +7,66 @@
77
using Microsoft.Azure.SignalR.Management;
88
using Microsoft.Extensions.Azure;
99
using Microsoft.Extensions.Configuration;
10-
using Microsoft.Extensions.Options;
1110
using Microsoft.Extensions.Primitives;
1211

1312
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
1413
{
15-
internal class OptionsSetup : IConfigureOptions<ServiceManagerOptions>, IOptionsChangeTokenSource<ServiceManagerOptions>
14+
internal class OptionsSetup
1615
{
1716
private readonly IConfiguration _configuration;
18-
private readonly AzureComponentFactory _azureComponentFactory;
19-
private readonly string _connectionStringKey;
17+
private readonly Action<ServiceManagerOptions> _configureServiceManagerOptions;
2018

21-
public OptionsSetup(IConfiguration configuration, AzureComponentFactory azureComponentFactory, string connectionStringKey)
19+
public OptionsSetup(IConfiguration configuration, AzureComponentFactory azureComponentFactory, string connectionStringKey, SignalROptions optionsFromCode)
2220
{
2321
if (string.IsNullOrWhiteSpace(connectionStringKey))
2422
{
2523
throw new ArgumentException($"'{nameof(connectionStringKey)}' cannot be null or whitespace", nameof(connectionStringKey));
2624
}
2725

2826
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
29-
_azureComponentFactory = azureComponentFactory;
30-
_connectionStringKey = connectionStringKey;
31-
}
32-
33-
public string Name => Options.DefaultName;
34-
35-
public void Configure(ServiceManagerOptions options)
36-
{
37-
if (_configuration.TryGetJsonObjectSerializer(out var serializer))
27+
_configureServiceManagerOptions = options =>
3828
{
39-
options.UseJsonObjectSerializer(serializer);
40-
}
29+
// Apply options from code.
30+
options.ServiceEndpoints = optionsFromCode.ServiceEndpoints?.ToArray();
31+
options.ServiceTransportType = optionsFromCode.ServiceTransportType;
32+
options.UseJsonObjectSerializer(optionsFromCode.JsonObjectSerializer);
4133

42-
if (_configuration.GetConnectionString(_connectionStringKey) != null || _configuration[_connectionStringKey] != null)
43-
{
44-
options.ConnectionString = _configuration.GetConnectionString(_connectionStringKey) ?? _configuration[_connectionStringKey];
45-
}
34+
// Apply options from configuration
35+
if (_configuration.TryGetJsonObjectSerializer(out var serializer))
36+
{
37+
options.UseJsonObjectSerializer(serializer);
38+
}
39+
if (_configuration.GetConnectionString(connectionStringKey) != null || _configuration[connectionStringKey] != null)
40+
{
41+
options.ConnectionString = _configuration.GetConnectionString(connectionStringKey) ?? _configuration[connectionStringKey];
42+
}
43+
var endpoints = _configuration.GetSection(Constants.AzureSignalREndpoints).GetEndpoints(azureComponentFactory);
4644

47-
var endpoints = _configuration.GetSection(Constants.AzureSignalREndpoints).GetEndpoints(_azureComponentFactory);
48-
49-
// when the configuration is in the style: AzureSignalRConnectionString:serviceUri = https://xxx.service.signalr.net , we see the endpoint as unnamed.
50-
if (options.ConnectionString == null && _configuration.GetSection(_connectionStringKey).TryGetEndpointFromIdentity(_azureComponentFactory, out var endpoint, isNamed: false))
51-
{
52-
endpoints = endpoints.Append(endpoint);
53-
}
54-
if (endpoints.Any())
55-
{
56-
options.ServiceEndpoints = endpoints.ToArray();
57-
}
58-
var serviceTransportTypeStr = _configuration[Constants.ServiceTransportTypeName];
59-
if (Enum.TryParse<ServiceTransportType>(serviceTransportTypeStr, out var transport))
60-
{
61-
options.ServiceTransportType = transport;
62-
}
63-
else if (!string.IsNullOrWhiteSpace(serviceTransportTypeStr))
64-
{
65-
throw new InvalidOperationException($"Invalid service transport type: {serviceTransportTypeStr}.");
66-
}
67-
//make connection more stable
68-
options.ConnectionCount = 3;
69-
options.ProductInfo = GetProductInfo();
45+
// when the configuration is in the style: AzureSignalRConnectionString:serviceUri = https://xxx.service.signalr.net , we see the endpoint as unnamed.
46+
if (options.ConnectionString == null && _configuration.GetSection(connectionStringKey).TryGetEndpointFromIdentity(azureComponentFactory, out var endpoint, isNamed: false))
47+
{
48+
endpoints = endpoints.Append(endpoint);
49+
}
50+
if (endpoints.Any())
51+
{
52+
options.ServiceEndpoints = endpoints.ToArray();
53+
}
54+
var serviceTransportTypeStr = _configuration[Constants.ServiceTransportTypeName];
55+
if (Enum.TryParse<ServiceTransportType>(serviceTransportTypeStr, out var transport))
56+
{
57+
options.ServiceTransportType = transport;
58+
}
59+
else if (!string.IsNullOrWhiteSpace(serviceTransportTypeStr))
60+
{
61+
throw new InvalidOperationException($"Invalid service transport type: {serviceTransportTypeStr}.");
62+
}
63+
//make connection more stable
64+
options.ConnectionCount = 3;
65+
options.ProductInfo = GetProductInfo();
66+
};
7067
}
7168

72-
public IChangeToken GetChangeToken()
73-
{
74-
return _configuration.GetReloadToken();
75-
}
69+
public Action<ServiceManagerOptions> Configure => _configureServiceManagerOptions;
7670

7771
private string GetProductInfo()
7872
{

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceHubContextStore.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,27 @@
33

44
using System;
55
using System.Collections.Concurrent;
6-
using System.Linq;
76
using System.Threading.Tasks;
87
using Microsoft.Azure.SignalR;
98
using Microsoft.Azure.SignalR.Management;
9+
using Microsoft.Extensions.Options;
1010

1111
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
1212
{
1313
internal sealed class ServiceHubContextStore : IInternalServiceHubContextStore
1414
{
1515
private readonly ConcurrentDictionary<string, (Lazy<Task<IServiceHubContext>> Lazy, IServiceHubContext Value)> _store = new(StringComparer.OrdinalIgnoreCase);
1616
private readonly ConcurrentDictionary<string, Lazy<Task<object>>> _stronglyTypedStore = new(StringComparer.OrdinalIgnoreCase);
17-
private readonly IServiceEndpointManager _endpointManager;
1817
private readonly ServiceManager _serviceManager;
1918

20-
public AccessKey[] AccessKeys => _endpointManager.Endpoints.Keys.Select(endpoint => endpoint.AccessKey).ToArray();
21-
2219
public IServiceManager ServiceManager => _serviceManager as IServiceManager;
2320

24-
public ServiceHubContextStore(IServiceEndpointManager endpointManager, ServiceManager serviceManager)
21+
public IOptionsMonitor<SignatureValidationOptions> SignatureValidationOptions { get; }
22+
23+
public ServiceHubContextStore(IOptionsMonitor<SignatureValidationOptions> signatureValidationOptions, ServiceManager serviceManager)
2524
{
25+
SignatureValidationOptions = signatureValidationOptions;
2626
_serviceManager = serviceManager;
27-
_endpointManager = endpointManager;
2827
}
2928

3029
public async ValueTask<ServiceHubContext<T>> GetAsync<T>(string hubName) where T : class

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/Config/ServiceManagerStore.cs

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
using System;
55
using System.Collections.Concurrent;
6-
using System.Collections.Generic;
7-
using System.Linq;
86
using System.Threading.Tasks;
97
using Microsoft.Azure.SignalR;
108
using Microsoft.Azure.SignalR.Management;
@@ -51,25 +49,23 @@ public IInternalServiceHubContextStore GetByConfigurationKey(string connectionSt
5149

5250
private IInternalServiceHubContextStore CreateHubContextStore(string connectionStringKey)
5351
{
54-
var services = new ServiceCollection()
55-
.Configure<ServiceManagerOptions>(o =>
56-
{
57-
o.ServiceTransportType = _options.ServiceTransportType;
58-
o.ServiceEndpoints = _options.ServiceEndpoints?.ToArray();
59-
o.UseJsonObjectSerializer(_options.JsonObjectSerializer);
60-
})
61-
.SetupOptions<ServiceManagerOptions, OptionsSetup>(new OptionsSetup(_configuration, _azureComponentFactory, connectionStringKey))
62-
.AddSignalRServiceManager()
63-
.AddSingleton(sp => (ServiceManager)sp.GetService<IServiceManager>())
64-
.AddSingleton(_loggerFactory)
65-
.AddSingleton<IInternalServiceHubContextStore, ServiceHubContextStore>();
66-
if (_router != null)
67-
{
68-
services.AddSingleton(_router);
69-
}
70-
services.AddSingleton(services.ToList() as IReadOnlyCollection<ServiceDescriptor>);
71-
return services.BuildServiceProvider()
72-
.GetRequiredService<IInternalServiceHubContextStore>();
52+
var serviceManagerOptionsSetup = new OptionsSetup(_configuration, _azureComponentFactory, connectionStringKey, _options);
53+
var serviceManger = new ServiceManagerBuilder()
54+
// Does the actual configuration
55+
.WithOptions(serviceManagerOptionsSetup.Configure)
56+
.WithLoggerFactory(_loggerFactory)
57+
.WithRouter(_router ?? new EndpointRouterDecorator())
58+
// Serves as a reload token provider only
59+
.WithConfiguration(new EmptyConfiguration(_configuration))
60+
.BuildServiceManager();
61+
return new ServiceCollection()
62+
.AddSingleton(serviceManger)
63+
.AddOptions()
64+
.AddSingleton<IConfigureOptions<SignatureValidationOptions>>(new SignatureValidationOptionsSetup(serviceManagerOptionsSetup.Configure))
65+
.AddSingleton<IOptionsChangeTokenSource<SignatureValidationOptions>>(new ConfigurationChangeTokenSource<SignatureValidationOptions>(_configuration))
66+
.AddSingleton<ServiceHubContextStore>()
67+
.BuildServiceProvider()
68+
.GetRequiredService<ServiceHubContextStore>();
7369
}
7470

7571
public async ValueTask DisposeAsync()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
7+
{
8+
internal class SignatureValidationOptions
9+
{
10+
public List<string> AccessKeys { get; } = new List<string>();
11+
public bool RequireValidation { get; set; } = true;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.Azure.SignalR;
8+
using Microsoft.Azure.SignalR.Management;
9+
using Microsoft.Extensions.Options;
10+
using Microsoft.Extensions.Primitives;
11+
12+
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
13+
{
14+
internal class SignatureValidationOptionsSetup : IConfigureOptions<SignatureValidationOptions>
15+
{
16+
private static readonly char[] PropertySeparator = { ';' };
17+
private static readonly char[] KeyValueSeparator = { '=' };
18+
private const string AccessKeyProperty = "AccessKey";
19+
20+
private readonly Action<ServiceManagerOptions> _configureServiceManagerOptions;
21+
22+
public SignatureValidationOptionsSetup(Action<ServiceManagerOptions> configureServiceManagerOptions)
23+
{
24+
_configureServiceManagerOptions = configureServiceManagerOptions;
25+
}
26+
27+
/// <remarks>
28+
/// We can't get the <see cref="ServiceManagerOptions"/> from <see cref="ServiceManager"/>, therefore we have to reuse the configuration action to build the options again.
29+
/// </remarks>
30+
public void Configure(SignatureValidationOptions options)
31+
{
32+
var serviceManagerOptions = new ServiceManagerOptions();
33+
_configureServiceManagerOptions(serviceManagerOptions);
34+
IEnumerable<ServiceEndpoint> endpoints = serviceManagerOptions.ServiceEndpoints ?? Array.Empty<ServiceEndpoint>();
35+
if (serviceManagerOptions.ConnectionString != null)
36+
{
37+
endpoints = endpoints.Append(new ServiceEndpoint(serviceManagerOptions.ConnectionString));
38+
}
39+
foreach (var endpoint in endpoints)
40+
{
41+
if (endpoint.ConnectionString != null && TryGetAccessKey(endpoint.ConnectionString, out var accessKey))
42+
{
43+
options.AccessKeys.Add(accessKey);
44+
}
45+
else
46+
{
47+
// Once there is one connection string without access key, the validation is not required. Currently we don't have mechanism to validate identity-based connection.
48+
options.RequireValidation = false;
49+
// Validation isn't required therefore no need to continue to get the access keys.
50+
return;
51+
}
52+
}
53+
}
54+
55+
private static bool TryGetAccessKey(string connectionString, out string accesskey)
56+
{
57+
foreach (var property in connectionString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries))
58+
{
59+
var kvp = property.Split(KeyValueSeparator, 2);
60+
if (kvp.Length != 2)
61+
{
62+
continue;
63+
}
64+
if (kvp[0].Trim().Equals(AccessKeyProperty, StringComparison.OrdinalIgnoreCase))
65+
{
66+
accesskey = kvp[1].Trim();
67+
return true;
68+
}
69+
}
70+
accesskey = null;
71+
return false;
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using Microsoft.Azure.SignalR;
54
using Microsoft.Azure.WebJobs.Host.Executors;
5+
using Microsoft.Extensions.Options;
66

77
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
88
{
99
internal class ExecutionContext
1010
{
1111
public ITriggeredFunctionExecutor Executor { get; set; }
12-
13-
public AccessKey[] AccessKeys { get; set; }
12+
public IOptionsMonitor<SignatureValidationOptions> SignatureValidationOptions { get; set; }
1413
}
1514
}

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/Executor/SignalRMethodExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ protected SignalRMethodExecutor(IRequestResolver resolver, ExecutionContext exec
2626
protected Task<FunctionResult> ExecuteWithAuthAsync(HttpRequestMessage request, ExecutionContext executor,
2727
InvocationContext context, TaskCompletionSource<object> tcs = null)
2828
{
29-
if (!Resolver.ValidateSignature(request, executor.AccessKeys))
29+
if (!Resolver.ValidateSignature(request, executor.SignatureValidationOptions))
3030
{
3131
throw new SignalRTriggerAuthorizeFailedException();
3232
}

sdk/signalr/Microsoft.Azure.WebJobs.Extensions.SignalRService/src/TriggerBindings/Resolver/IRequestResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
using System.Threading.Tasks;
66

77
using Microsoft.AspNetCore.SignalR.Protocol;
8-
using Microsoft.Azure.SignalR;
98
using Microsoft.Azure.SignalR.Serverless.Protocols;
9+
using Microsoft.Extensions.Options;
1010

1111
namespace Microsoft.Azure.WebJobs.Extensions.SignalRService
1212
{
1313
internal interface IRequestResolver
1414
{
1515
bool ValidateContentType(HttpRequestMessage request);
1616

17-
bool ValidateSignature(HttpRequestMessage request, AccessKey[] accessKeys);
17+
bool ValidateSignature(HttpRequestMessage request, IOptionsMonitor<SignatureValidationOptions> signatureValidationOptions);
1818

1919
bool TryGetInvocationContext(HttpRequestMessage request, out InvocationContext context);
2020

0 commit comments

Comments
 (0)