Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 5.4.0
- Added
- Introduced a new way to connect to Service Bus using Azure Entra authentication.
## 5.3.1
- Changed
- Fix usage of sent counter on receiver instead of received counter
Expand Down
52 changes: 42 additions & 10 deletions src/Ev.ServiceBus.Abstractions/Configuration/ConnectionSettings.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Azure.Core;
using Azure.Messaging.ServiceBus;
using System;

namespace Ev.ServiceBus.Abstractions;

Expand All @@ -12,9 +13,23 @@ internal ConnectionSettings(string connectionString, ServiceBusClientOptions opt
Endpoint = GetEndpointFromConnectionString(connectionString);
}

internal ConnectionSettings(string fullyQualifiedNamespace, TokenCredential credentials, ServiceBusClientOptions options)
{
Options = options;
FullyQualifiedNamespace = fullyQualifiedNamespace;
Credentials = credentials;
Endpoint = GetEndpointFromFullyQualifiedNamespace(fullyQualifiedNamespace);
}

public string Endpoint { get; }
public string ConnectionString { get; }
public ServiceBusClientOptions Options { get; }

public string? ConnectionString { get; }

public ServiceBusClientOptions? Options { get; }

public string? FullyQualifiedNamespace { get; }

public TokenCredential? Credentials { get; }

private string GetEndpointFromConnectionString(string connectionString)
{
Expand Down Expand Up @@ -43,17 +58,34 @@ private string GetEndpointFromConnectionString(string connectionString)
return string.Empty;
}

public override int GetHashCode()
private string GetEndpointFromFullyQualifiedNamespace(string fullyQualifiedNamespace)
{
return Endpoint.GetHashCode();
return $"sb://{fullyQualifiedNamespace}/";
}

private bool Equals(ConnectionSettings other) =>
string.Equals(Endpoint, other.Endpoint, StringComparison.Ordinal)
&& string.Equals(ConnectionString, other.ConnectionString, StringComparison.Ordinal)
&& Options != null
&& Options.Equals(other.Options)
&& string.Equals(FullyQualifiedNamespace, other.FullyQualifiedNamespace, StringComparison.Ordinal)
&& Equals(Credentials, other.Credentials);

public override bool Equals(object? obj)
{
if (obj is not ConnectionSettings settings)
{
return false;
}
return Endpoint.Equals(settings.Endpoint);
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((ConnectionSettings)obj);
}

public override int GetHashCode()
{
return HashCode.Combine(
Endpoint,
ConnectionString,
Options,
FullyQualifiedNamespace,
Credentials);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Azure.Messaging.ServiceBus;
using Azure.Core;
using Azure.Messaging.ServiceBus;

// ReSharper disable once CheckNamespace
namespace Ev.ServiceBus.Abstractions;
Expand All @@ -23,4 +24,25 @@ public static TOptions WithConnection<TOptions>(
options.ConnectionSettings = new ConnectionSettings(connectionString, connectionOptions);
return options;
}

/// <summary>
/// Sets the connection to use for this resource using Azure Entra ID.
/// If no connection is set then the default connection will be used.
/// </summary>
/// <param name="options"></param>
/// <param name="fullyQualifiedNamespace"></param>
/// <param name="credentials"></param>
/// <param name="connectionOptions"></param>
/// <typeparam name="TOptions"></typeparam>
/// <returns></returns>
public static TOptions WithConnection<TOptions>(
this TOptions options,
string fullyQualifiedNamespace,
TokenCredential credentials,
ServiceBusClientOptions connectionOptions)
where TOptions : ClientOptions
{
options.ConnectionSettings = new ConnectionSettings(fullyQualifiedNamespace, credentials, connectionOptions);
return options;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Azure.Messaging.ServiceBus;
using Azure.Core;
using Azure.Messaging.ServiceBus;

namespace Ev.ServiceBus.Abstractions;

Expand Down Expand Up @@ -35,6 +36,11 @@ public void WithConnection(string connectionString, ServiceBusClientOptions opti
ConnectionSettings = new ConnectionSettings(connectionString, options);
}

public void WithConnection(string fullyQualifiedNamespace, TokenCredential tokenCredential, ServiceBusClientOptions options)
{
ConnectionSettings = new ConnectionSettings(fullyQualifiedNamespace, tokenCredential, options);
}

public void WithIsolation(IsolationBehavior behavior, string? isolationKey = null, string? applicationName = null)
{
IsolationSettings = new IsolationSettings(behavior, isolationKey, applicationName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.17.0" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
</ItemGroup>

Expand Down
9 changes: 7 additions & 2 deletions src/Ev.ServiceBus.HealthChecks/ConnectionSettingsComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@ public bool Equals(ConnectionSettings? x, ConnectionSettings? y)
return false;
}

return x.ConnectionString == y.ConnectionString;
return x.ConnectionString == y.ConnectionString
&& x.FullyQualifiedNamespace == y.FullyQualifiedNamespace
&& x.Credentials == y.Credentials;
}

public int GetHashCode(ConnectionSettings? obj)
{
return obj?.ConnectionString?.GetHashCode() ?? 0;
return HashCode.Combine(
obj?.ConnectionString,
obj?.FullyQualifiedNamespace,
obj?.Credentials);
}
}
23 changes: 18 additions & 5 deletions src/Ev.ServiceBus.HealthChecks/RegistrationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,21 @@ public void Configure(HealthCheckServiceOptions options)
return;
}

var commonConnectionString = _serviceBusOptions.Value.Settings.ConnectionSettings?.ConnectionString;
var commonSettings = _serviceBusOptions.Value.Settings.ConnectionSettings;
var commonConnectionString = commonSettings?.ConnectionString;
var commonFullyQualifiedNamespace = commonSettings?.FullyQualifiedNamespace;
var commonCredentials = commonSettings?.Credentials;

var resources = _serviceBusOptions.Value.Receivers.Union(_serviceBusOptions.Value.Senders).Distinct()
.ToArray();

foreach (var resourceGroup in resources.GroupBy(o => o.ConnectionSettings, new ConnectionSettingsComparer()))
{
var connectionString = resourceGroup.Key?.ConnectionString ?? commonConnectionString;
if (connectionString == null)
var fullyQualifiedNamespace = resourceGroup.Key?.FullyQualifiedNamespace ?? commonFullyQualifiedNamespace;
var credentials = resourceGroup.Key?.Credentials ?? commonCredentials;

if (connectionString == null && fullyQualifiedNamespace == null && credentials == null)
{
continue;
}
Expand All @@ -46,7 +53,9 @@ public void Configure(HealthCheckServiceOptions options)
options.Registrations.Add(new HealthCheckRegistration($"Queue:{group.Key}",
sp => (IHealthCheck) new AzureServiceBusQueueHealthCheck(new AzureServiceBusQueueHealthCheckOptions(group.Key)
{
ConnectionString = connectionString
ConnectionString = connectionString,
FullyQualifiedNamespace = fullyQualifiedNamespace,
Credential = credentials
}),
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
}
Expand All @@ -58,7 +67,9 @@ public void Configure(HealthCheckServiceOptions options)
options.Registrations.Add(new HealthCheckRegistration($"Topic:{group.Key}",
sp => (IHealthCheck) new AzureServiceBusTopicHealthCheck(new AzureServiceBusTopicHealthCheckOptions(group.Key)
{
ConnectionString = connectionString
ConnectionString = connectionString,
FullyQualifiedNamespace = fullyQualifiedNamespace,
Credential = credentials,
}),
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
}
Expand All @@ -73,7 +84,9 @@ public void Configure(HealthCheckServiceOptions options)
options.Registrations.Add(new HealthCheckRegistration($"Subscription:{group.Key.TopicName}/Subscriptions/{group.Key.SubscriptionName}",
sp => (IHealthCheck) new AzureServiceBusSubscriptionHealthCheck(new AzureServiceBusSubscriptionHealthCheckHealthCheckOptions(group.Key.TopicName, group.Key.SubscriptionName)
{
ConnectionString = connectionString
ConnectionString = connectionString,
FullyQualifiedNamespace = fullyQualifiedNamespace,
Credential = credentials
}),
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
}
Expand Down
14 changes: 14 additions & 0 deletions src/Ev.ServiceBus/Dispatch/DispatchRegistrationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ public void CustomizeConnection(
_options.WithConnection(connectionString, options);
}

/// <summary>
/// Sets a specific connection using Azure Entra authorization for the underlying resource.
/// </summary>
/// <param name="fullyQualifiedNamespace"></param>
/// <param name="credentials"></param>
/// <param name="options"></param>
public void CustomizeConnection(
string fullyQualifiedNamespace,
Azure.Core.TokenCredential credentials,
ServiceBusClientOptions options)
{
_options.WithConnection(fullyQualifiedNamespace, credentials, options);
}

/// <summary>
/// Registers a class as a payload to serialize and send through the current resource.
/// </summary>
Expand Down
18 changes: 14 additions & 4 deletions src/Ev.ServiceBus/Management/Factories/ClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Azure.Messaging.ServiceBus;
using Ev.ServiceBus.Abstractions;

Expand All @@ -7,11 +8,20 @@ public class ClientFactory : IClientFactory
{
public ServiceBusClient Create(ConnectionSettings connectionSettings)
{
if (connectionSettings.Options != null)
if (!string.IsNullOrWhiteSpace(connectionSettings.ConnectionString))
{
return new ServiceBusClient(connectionSettings.ConnectionString, connectionSettings.Options);
return connectionSettings.Options is not null
? new ServiceBusClient(connectionSettings.ConnectionString, connectionSettings.Options)
: new ServiceBusClient(connectionSettings.ConnectionString);
}

return new ServiceBusClient(connectionSettings.ConnectionString);
if(connectionSettings.Credentials is not null && !string.IsNullOrWhiteSpace(connectionSettings.FullyQualifiedNamespace))
{
return connectionSettings.Options is not null
? new ServiceBusClient(connectionSettings.FullyQualifiedNamespace, connectionSettings.Credentials, connectionSettings.Options)
: new ServiceBusClient(connectionSettings.FullyQualifiedNamespace, connectionSettings.Credentials);
}

throw new InvalidOperationException("Insufficient connection settings: provide either a connection string or both FullyQualifiedNamespace and Credentials.");
}
}
}
33 changes: 33 additions & 0 deletions tests/Ev.ServiceBus.HealthChecks.UnitTests/ComplexTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Messaging.ServiceBus;
using Ev.ServiceBus.Reception;
using Ev.ServiceBus.UnitTests.Helpers;
Expand Down Expand Up @@ -166,6 +167,38 @@ public void MultipleRegistrationsGoesWell_case4()
reg => { reg.Name.Should().Be("Subscription:topic/Subscriptions/subscription3"); });
}

[Fact]
public void HealthCheckWorksWithEntraAuthorization()
{
var services = new ServiceCollection();

services.AddServiceBus(settings =>
{
settings.WithConnection("fullyQualifiedNamespace", new DefaultAzureCredential(), new ServiceBusClientOptions());
});

services.AddHealthChecks().AddEvServiceBusChecks();

services.RegisterServiceBusReception().FromSubscription("topic", "subscription", builder =>
{
builder.RegisterReception<RelationCreated, RelationCreatedHandler>();
});
services.RegisterServiceBusReception().FromQueue("queue", builder =>
{
builder.RegisterReception<RelationCreated, RelationCreatedHandler>();
});

var provider = services.BuildServiceProvider();

var healthOptions = provider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

healthOptions.Value.Registrations.Count.Should().Be(2);
healthOptions.Value.Registrations.Should().SatisfyRespectively(
reg => { reg.Name.Should().Be("Queue:queue"); },
reg => { reg.Name.Should().Be("Subscription:topic/Subscriptions/subscription"); }
);
}

public class RelationCreated { }

public class RelationCreatedHandler : IMessageReceptionHandler<RelationCreated>
Expand Down
Loading