Skip to content

Commit 3b75b8d

Browse files
authored
Refactor global Web PubSub connection Options setup in preparation fo… (#54209)
* Abstract Web PubSub access credential into a new class, unifies identity-based connection and key-based connection * Add a util class to create Web PubSub access object from conneciton string or identity-based connection * Unify global service access info into `WebPubSubServiceAccessOptions`.
1 parent b729f67 commit 3b75b8d

10 files changed

+722
-1
lines changed

sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Config/WebPubSubJobsBuilderExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
using System;
55
using Microsoft.Azure.WebJobs;
66
using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
7+
using Microsoft.Extensions.Azure;
78
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Options;
811

912
namespace Microsoft.Extensions.Hosting
1013
{
@@ -27,6 +30,11 @@ public static IWebJobsBuilder AddWebPubSub(this IWebJobsBuilder builder)
2730

2831
builder.AddExtension<WebPubSubConfigProvider>()
2932
.ConfigureOptions<WebPubSubFunctionsOptions>(ApplyConfiguration);
33+
34+
// Register the options setup to read from default configuration section
35+
builder.Services.AddSingleton<IConfigureOptions<WebPubSubServiceAccessOptions>, WebPubSubServiceAccessOptionsSetup>();
36+
37+
builder.Services.AddAzureClientsCore();
3038
return builder;
3139
}
3240

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub;
7+
8+
#nullable enable
9+
10+
/// <summary>
11+
/// Access information to Web PubSub service.
12+
/// </summary>
13+
internal class WebPubSubServiceAccess(Uri serviceEndpoint, WebPubSubServiceCredential credential)
14+
{
15+
public Uri ServiceEndpoint { get; } = serviceEndpoint;
16+
public WebPubSubServiceCredential Credential { get; } = credential;
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub;
5+
6+
internal class WebPubSubServiceAccessOptions
7+
{
8+
public WebPubSubServiceAccess? WebPubSubAccess { get; set; }
9+
public string? Hub { get; set; }
10+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Azure;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub
9+
{
10+
/// <summary>
11+
/// Configures <see cref="WebPubSubServiceAccessOptions"/> by reading from the default configuration section.
12+
/// </summary>
13+
internal class WebPubSubServiceAccessOptionsSetup : IConfigureOptions<WebPubSubServiceAccessOptions>
14+
{
15+
private readonly IConfiguration _configuration;
16+
private readonly AzureComponentFactory _azureComponentFactory;
17+
private readonly INameResolver _nameResolver;
18+
private readonly IOptionsMonitor<WebPubSubFunctionsOptions> _publicOptions;
19+
20+
public WebPubSubServiceAccessOptionsSetup(
21+
IConfiguration configuration,
22+
AzureComponentFactory azureComponentFactory,
23+
INameResolver nameResolver,
24+
IOptionsMonitor<WebPubSubFunctionsOptions> publicOptions)
25+
{
26+
_configuration = configuration;
27+
_azureComponentFactory = azureComponentFactory;
28+
_nameResolver = nameResolver;
29+
_publicOptions = publicOptions;
30+
}
31+
32+
public void Configure(WebPubSubServiceAccessOptions options)
33+
{
34+
var publicOptions = _publicOptions.CurrentValue;
35+
36+
// WebPubSubFunctionsOptions.ConnectionString can be set via code only. Takes the highest priority.
37+
if (!string.IsNullOrEmpty(publicOptions.ConnectionString))
38+
{
39+
options.WebPubSubAccess = WebPubSubServiceAccessUtil.CreateFromConnectionString(publicOptions.ConnectionString);
40+
}
41+
else
42+
{
43+
var defaultSection = _configuration.GetSection(Constants.WebPubSubConnectionStringName);
44+
if (WebPubSubServiceAccessUtil.CreateFromIConfiguration(defaultSection, _azureComponentFactory, out var access))
45+
{
46+
options.WebPubSubAccess = access!;
47+
}
48+
}
49+
50+
// Only configure Hub from the default config section if not already set
51+
options.Hub = publicOptions.Hub ?? _nameResolver.Resolve(Constants.HubNameStringName);
52+
}
53+
}
54+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.Extensions.Azure;
7+
using Microsoft.Extensions.Configuration;
8+
9+
namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub;
10+
11+
internal static class WebPubSubServiceAccessUtil
12+
{
13+
private const string EndpointPropertyName = "Endpoint";
14+
private const string AccessKeyPropertyName = "AccessKey";
15+
private const string PortPropertyName = "Port";
16+
private static readonly char[] KeyValueSeparator = { '=' };
17+
private static readonly char[] PropertySeparator = { ';' };
18+
19+
internal static WebPubSubServiceAccess CreateFromConnectionString(string connectionString)
20+
{
21+
if (string.IsNullOrEmpty(connectionString))
22+
{
23+
throw new ArgumentNullException(nameof(connectionString));
24+
}
25+
26+
var properties = connectionString.Split(PropertySeparator, StringSplitOptions.RemoveEmptyEntries);
27+
28+
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
29+
foreach (var property in properties)
30+
{
31+
var kvp = property.Split(KeyValueSeparator, 2);
32+
if (kvp.Length != 2)
33+
continue;
34+
35+
var key = kvp[0].Trim();
36+
if (dict.ContainsKey(key))
37+
{
38+
throw new ArgumentException($"Duplicate properties found in connection string: {key}.");
39+
}
40+
41+
dict.Add(key, kvp[1].Trim());
42+
}
43+
44+
if (!dict.TryGetValue(EndpointPropertyName, out var endpoint))
45+
{
46+
throw new ArgumentException($"Required property not found in connection string: {EndpointPropertyName}.");
47+
}
48+
endpoint = endpoint.TrimEnd('/');
49+
50+
// AccessKey is optional when connection string is disabled.
51+
dict.TryGetValue(AccessKeyPropertyName, out var accessKey);
52+
53+
int? port = null;
54+
if (dict.TryGetValue(PortPropertyName, out var rawPort))
55+
{
56+
if (int.TryParse(rawPort, out var portValue) && portValue > 0 && portValue <= 0xFFFF)
57+
{
58+
port = portValue;
59+
}
60+
else
61+
{
62+
throw new ArgumentException($"Invalid Port value: {rawPort}");
63+
}
64+
}
65+
66+
var uriBuilder = new UriBuilder(endpoint);
67+
if (port.HasValue)
68+
{
69+
uriBuilder.Port = port.Value;
70+
}
71+
72+
return new WebPubSubServiceAccess(uriBuilder.Uri, new KeyCredential(accessKey));
73+
}
74+
75+
internal static bool CreateFromIConfiguration(IConfigurationSection section, AzureComponentFactory azureComponentFactory, out WebPubSubServiceAccess? result)
76+
{
77+
if (!string.IsNullOrEmpty(section.Value))
78+
{
79+
result = CreateFromConnectionString(section.Value);
80+
return true;
81+
}
82+
else
83+
{
84+
// Check if this is an identity-based connection (has serviceUri)
85+
var serviceUri = section[Constants.ServiceUriKey];
86+
if (!string.IsNullOrEmpty(serviceUri))
87+
{
88+
var endpoint = new Uri(serviceUri);
89+
var tokenCredential = azureComponentFactory.CreateTokenCredential(section);
90+
result = new WebPubSubServiceAccess(endpoint, new IdentityCredential(tokenCredential));
91+
return true;
92+
}
93+
}
94+
result = null;
95+
return false;
96+
}
97+
98+
internal static bool CanCreateFromIConfiguration(IConfigurationSection section)
99+
{
100+
if (!string.IsNullOrEmpty(section.Value))
101+
{
102+
// Assume connection string exists.
103+
return true;
104+
}
105+
else
106+
{
107+
// Check if this is an identity-based connection (has serviceUri)
108+
var serviceUri = section[Constants.ServiceUriKey];
109+
if (!string.IsNullOrEmpty(serviceUri))
110+
{
111+
// Identity-based connection
112+
return true;
113+
}
114+
}
115+
return false;
116+
}
117+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Core;
5+
6+
namespace Microsoft.Azure.WebJobs.Extensions.WebPubSub;
7+
8+
/// <summary>
9+
/// Can be key-based credential, identity-based connection, or null (A connection string without access key provided to Web PubSub trigger)
10+
/// </summary>
11+
internal abstract class WebPubSubServiceCredential
12+
{
13+
public abstract bool CanValidateSignature { get; }
14+
}
15+
16+
internal class KeyCredential(string accessKey) : WebPubSubServiceCredential
17+
{
18+
public override bool CanValidateSignature => !string.IsNullOrEmpty(AccessKey);
19+
public string AccessKey { get; } = accessKey;
20+
}
21+
22+
internal class IdentityCredential(TokenCredential tokenCredential) : WebPubSubServiceCredential
23+
{
24+
public override bool CanValidateSignature => false;
25+
26+
public TokenCredential TokenCredential { get; } = tokenCredential;
27+
}

sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Constants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ internal static class Constants
1313
public const string HubNameStringName = "WebPubSubHub";
1414
public const string WebPubSubValidationStringName = "WebPubSubValidation";
1515

16+
// Identity-based connection configuration keys
17+
public const string ServiceUriKey = "serviceUri";
18+
1619
public const string MqttWebSocketSubprotocolValue = "mqtt";
1720

1821
public static class ContentTypes

sdk/webpubsub/Microsoft.Azure.WebJobs.Extensions.WebPubSub/src/Microsoft.Azure.WebJobs.Extensions.WebPubSub.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
@@ -25,6 +25,7 @@
2525
<ItemGroup>
2626
<PackageReference Include="Microsoft.AspNetCore.Http" />
2727
<PackageReference Include="Microsoft.Azure.WebJobs" />
28+
<PackageReference Include="Microsoft.Extensions.Azure" />
2829
</ItemGroup>
2930

3031
<ItemGroup>

0 commit comments

Comments
 (0)