Skip to content

Commit

Permalink
feat: Azure ServiceBus package
Browse files Browse the repository at this point in the history
Closed: #41
  • Loading branch information
samtrion committed Dec 17, 2024
1 parent 997c5ae commit 39c312d
Show file tree
Hide file tree
Showing 19 changed files with 561 additions and 1 deletion.
4 changes: 3 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<ItemGroup>
<PackageVersion Include="Azure.Data.Tables" Version="12.9.1" />
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.18.2" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageVersion Include="Azure.Storage.Queues" Version="12.21.0" />
<PackageVersion Include="ClickHouse.Client" Version="7.9.1" />
Expand Down Expand Up @@ -49,6 +50,7 @@
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.1.0" />
<PackageVersion Include="Testcontainers.Redis" Version="4.1.0" />
<PackageVersion Include="Testcontainers.Redpanda" Version="4.1.0" />
<PackageVersion Include="Testcontainers.ServiceBus" Version="4.1.0" />
<PackageVersion Include="TngTech.ArchUnitNET.xUnit" Version="0.11.1" />
<PackageVersion Include="Verify.Xunit" Version="28.5.0" />
<PackageVersion Include="xunit" Version="2.9.2" />
Expand All @@ -60,4 +62,4 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.10" />
</ItemGroup>
</Project>
</Project>
15 changes: 15 additions & 0 deletions HealthChecks.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.Test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.Tests.Architecture", "tests\NetEvolve.HealthChecks.Tests.Architecture\NetEvolve.HealthChecks.Tests.Architecture.csproj", "{17BCA132-1FBB-46C1-B6A1-60F64969383D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEvolve.HealthChecks.Azure.ServiceBus", "src\NetEvolve.HealthChecks.Azure.ServiceBus\NetEvolve.HealthChecks.Azure.ServiceBus.csproj", "{6133570F-FF54-480D-8979-B0787022EA6A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -315,6 +317,18 @@ Global
{17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x64.Build.0 = Release|Any CPU
{17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x86.ActiveCfg = Release|Any CPU
{17BCA132-1FBB-46C1-B6A1-60F64969383D}.Release|x86.Build.0 = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|x64.ActiveCfg = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|x64.Build.0 = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|x86.ActiveCfg = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Debug|x86.Build.0 = Debug|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|Any CPU.Build.0 = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|x64.ActiveCfg = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|x64.Build.0 = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|x86.ActiveCfg = Release|Any CPU
{6133570F-FF54-480D-8979-B0787022EA6A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -340,6 +354,7 @@ Global
{66406BE8-0281-4C95-B90B-20CAE4516A16} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827}
{2B089420-E791-44E7-B471-F6F527B33E1C} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827}
{17BCA132-1FBB-46C1-B6A1-60F64969383D} = {E412EC77-2022-4A1D-AAC1-FDF1A4A45827}
{6133570F-FF54-480D-8979-B0787022EA6A} = {EF615D18-42E2-48A4-8EBA-E652DC574C56}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {28B4CC2B-39E8-49C0-9687-78121BD83A53}
Expand Down
103 changes: 103 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using global::Azure.Core;
using global::Azure.Identity;
using global::Azure.Messaging.ServiceBus;
using global::Azure.Messaging.ServiceBus.Administration;
using Microsoft.Extensions.DependencyInjection;

internal static class ClientCreation
{
private static ConcurrentDictionary<string, ServiceBusClient>? _serviceBusClients;
private static ConcurrentDictionary<
string,
ServiceBusAdministrationClient
>? _serviceBusAdministrationClients;

internal static ServiceBusClient GetClient<TOptions>(
string name,
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : ServiceBusOptionsBase
{
if (options.Mode == ClientCreationMode.ServiceProvider)
{
return serviceProvider.GetRequiredService<ServiceBusClient>();
}

if (_serviceBusClients is null)
{
_serviceBusClients = new ConcurrentDictionary<string, ServiceBusClient>(
StringComparer.OrdinalIgnoreCase
);

Check warning on line 36 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L34-L36

Added lines #L34 - L36 were not covered by tests
}

return _serviceBusClients.GetOrAdd(name, _ => CreateClient(options, serviceProvider));

Check warning on line 39 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L39

Added line #L39 was not covered by tests
}

internal static ServiceBusAdministrationClient GetAdministrationClient<TOptions>(
string name,
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : ServiceBusOptionsBase
{
if (options.Mode == ClientCreationMode.ServiceProvider)
{
return serviceProvider.GetRequiredService<ServiceBusAdministrationClient>();
}

if (_serviceBusAdministrationClients is null)
{
_serviceBusAdministrationClients = new ConcurrentDictionary<
string,
ServiceBusAdministrationClient
>(StringComparer.OrdinalIgnoreCase);

Check warning on line 59 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L56-L59

Added lines #L56 - L59 were not covered by tests
}

return _serviceBusAdministrationClients.GetOrAdd(
name,
_ => CreateAdministrationClient(options, serviceProvider)
);

Check warning on line 65 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L62-L65

Added lines #L62 - L65 were not covered by tests
}

private static ServiceBusClient CreateClient<TOptions>(
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : ServiceBusOptionsBase =>
options.Mode switch
{
ClientCreationMode.ServiceProvider =>
serviceProvider.GetRequiredService<ServiceBusClient>(),

Check warning on line 76 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L74-L76

Added lines #L74 - L76 were not covered by tests
ClientCreationMode.DefaultAzureCredentials => new ServiceBusClient(
options.FullyQualifiedNamespace,
serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential()
),
ClientCreationMode.ConnectionString => new ServiceBusClient(options.ConnectionString),
_ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."),
};

Check warning on line 83 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L78-L83

Added lines #L78 - L83 were not covered by tests

private static ServiceBusAdministrationClient CreateAdministrationClient<TOptions>(
TOptions options,
IServiceProvider serviceProvider
)
where TOptions : ServiceBusOptionsBase =>
options.Mode switch
{
ClientCreationMode.ServiceProvider =>
serviceProvider.GetRequiredService<ServiceBusAdministrationClient>(),

Check warning on line 93 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L91-L93

Added lines #L91 - L93 were not covered by tests
ClientCreationMode.DefaultAzureCredentials => new ServiceBusAdministrationClient(
options.FullyQualifiedNamespace,
serviceProvider.GetService<TokenCredential>() ?? new DefaultAzureCredential()
),
ClientCreationMode.ConnectionString => new ServiceBusAdministrationClient(
options.ConnectionString
),
_ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."),
};

Check warning on line 102 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs#L95-L102

Added lines #L95 - L102 were not covered by tests
}
18 changes: 18 additions & 0 deletions src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

using System;
using global::Azure.Messaging.ServiceBus;

/// <summary>
/// Describes the mode to create or retrieve a <see cref="ServiceBusClient"/>.
/// </summary>
public enum ClientCreationMode
{
/// <summary>
/// The default mode. The <see cref="ServiceBusClient"/> is loading the preregistered instance from the <see cref="IServiceProvider"/>.
/// </summary>
ServiceProvider = 0,

DefaultAzureCredentials,

Check warning on line 16 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / CodeQL / Run CodeQL

Missing XML comment for publicly visible type or member 'ClientCreationMode.DefaultAzureCredentials'

Check warning on line 16 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / Tests / Testing .NET solution

Missing XML comment for publicly visible type or member 'ClientCreationMode.DefaultAzureCredentials'
ConnectionString,

Check warning on line 17 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / CodeQL / Run CodeQL

Missing XML comment for publicly visible type or member 'ClientCreationMode.ConnectionString'

Check warning on line 17 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / Tests / Testing .NET solution

Missing XML comment for publicly visible type or member 'ClientCreationMode.ConnectionString'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using NetEvolve.Arguments;
using NetEvolve.HealthChecks.Abstractions;

/// <summary>
/// Extensions methods for <see cref="IHealthChecksBuilder"/> with custom Health Checks.
/// </summary>
public static class DependencyInjectionExtensions
{
private static readonly string[] _defaultTags = ["storage", "azure", "servicebus"];

/// <summary>
/// Adds a health check for an Azure Service Bus Queue.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The name of the <see cref="ServiceBusQueueHealthCheck"/>.</param>
/// <param name="options">An optional action to configure.</param>
/// <param name="tags">A list of additional tags that can be used to filter sets of health checks. Optional.</param>
/// <exception cref="ArgumentNullException">The <paramref name="builder"/> is <see langword="null" />.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="name"/> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">The <paramref name="name"/> is <see langword="null" /> or <c>whitespace</c>.</exception>
/// <exception cref="ArgumentException">The <paramref name="name"/> is already in use.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="tags"/> is <see langword="null" />.</exception>
public static IHealthChecksBuilder AddServiceBusQueueHealthCheck(
[NotNull] this IHealthChecksBuilder builder,
[NotNull] string name,
Action<ServiceBusQueueOptions>? options = null,
params string[] tags
)
{
ArgumentNullException.ThrowIfNull(builder);
Argument.ThrowIfNullOrEmpty(name);
ArgumentNullException.ThrowIfNull(tags);

if (!builder.IsServiceTypeRegistered<ServiceBusQueueMarker>())
{
_ = builder
.Services.AddSingleton<ServiceBusQueueMarker>()
.AddSingleton<ServiceBusQueueHealthCheck>()
.ConfigureOptions<ServiceBusQueueOptionsConfigure>();
}

if (builder.IsNameAlreadyUsed<ServiceBusQueueHealthCheck>(name))
{
throw new ArgumentException($"Name `{name}` already in use.", nameof(name), null);

Check warning on line 51 in src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs#L51

Added line #L51 was not covered by tests
}

if (options is not null)
{
_ = builder.Services.Configure(name, options);
}

return builder.AddCheck<ServiceBusQueueHealthCheck>(
name,
HealthStatus.Unhealthy,
_defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase)
);
}

private sealed partial class ServiceBusQueueMarker { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(_ProjectTargetFrameworks)</TargetFrameworks>

<Description>Contains HealthChecks for Azure Service Bus.</Description>
<PackageTags>$(PackageTags);azure;servicebus;</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Messaging.ServiceBus" />
<PackageReference Include="NetEvolve.Arguments" />
<PackageReference Include="NetEvolve.Extensions.Tasks" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\NetEvolve.HealthChecks.Abstractions\NetEvolve.HealthChecks.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

public abstract class ServiceBusOptionsBase

Check warning on line 3 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / CodeQL / Run CodeQL

Missing XML comment for publicly visible type or member 'ServiceBusOptionsBase'

Check warning on line 3 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / Tests / Testing .NET solution

Missing XML comment for publicly visible type or member 'ServiceBusOptionsBase'
{
/// <summary>
/// Gets or sets the client creation mode, default is <see cref="ClientCreationMode.ServiceProvider"/>.
/// </summary>
public ClientCreationMode Mode { get; set; }

/// <summary>
/// Gets or sets the azure service bus connection string.
/// </summary>
public string? ConnectionString { get; set; }

Check warning on line 13 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs#L13

Added line #L13 was not covered by tests

/// <summary>
/// Gets or sets the fully qualified namespace.
/// </summary>
public string FullyQualifiedNamespace { get; set; }

Check warning on line 18 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / CodeQL / Run CodeQL

Non-nullable property 'FullyQualifiedNamespace' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 18 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / Tests / Testing .NET solution

Non-nullable property 'FullyQualifiedNamespace' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 18 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs#L18

Added line #L18 was not covered by tests

/// <summary>
/// The timeout to use when connecting and executing tasks against database.
/// Default is 100 milliseconds.
/// </summary>
public int Timeout { get; set; } = 100;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using NetEvolve.Extensions.Tasks;
using NetEvolve.HealthChecks.Abstractions;

internal sealed class ServiceBusQueueHealthCheck
: ConfigurableHealthCheckBase<ServiceBusQueueOptions>
{
private readonly IServiceProvider _serviceProvider;

public ServiceBusQueueHealthCheck(
IServiceProvider serviceProvider,
IOptionsMonitor<ServiceBusQueueOptions> optionsMonitor
)
: base(optionsMonitor) => _serviceProvider = serviceProvider;

protected override ValueTask<HealthCheckResult> ExecuteHealthCheckAsync(
string name,
HealthStatus failureStatus,
ServiceBusQueueOptions options,
CancellationToken cancellationToken
) =>
options.EnablePeekMode
? ExecutePeekHealthCheckAsync(name, options, cancellationToken)
: ExecuteHealthCheckAsync(name, options, cancellationToken);

private async ValueTask<HealthCheckResult> ExecuteHealthCheckAsync(
string name,
ServiceBusQueueOptions options,
CancellationToken cancellationToken
)
{
var client = ClientCreation.GetAdministrationClient(name, options, _serviceProvider);

var (isValid, queue) = await client
.GetQueueAsync(options.QueueName, cancellationToken: cancellationToken)
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

return HealthCheckState(isValid && queue is not null, name);
}

Check warning on line 46 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs

View check run for this annotation

Codecov / codecov/patch

src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs#L46

Added line #L46 was not covered by tests

private async ValueTask<HealthCheckResult> ExecutePeekHealthCheckAsync(
string name,
ServiceBusQueueOptions options,
CancellationToken cancellationToken
)
{
var client = ClientCreation.GetClient(name, options, _serviceProvider);

var receiver = client.CreateReceiver(options.QueueName);

var (isValid, _) = await receiver
.ReceiveMessageAsync(cancellationToken: cancellationToken)
.WithTimeoutAsync(options.Timeout, cancellationToken)
.ConfigureAwait(false);

return HealthCheckState(isValid, name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace NetEvolve.HealthChecks.Azure.ServiceBus;

using Microsoft.Extensions.Options;

public class ServiceBusQueueOptions : ServiceBusOptionsBase

Check warning on line 5 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / CodeQL / Run CodeQL

Missing XML comment for publicly visible type or member 'ServiceBusQueueOptions'

Check warning on line 5 in src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs

View workflow job for this annotation

GitHub Actions / Build & Tests / Tests / Testing .NET solution

Missing XML comment for publicly visible type or member 'ServiceBusQueueOptions'
{
/// <summary>
/// Gets or sets a value indicating whether to enable peek mode, default is <c>false</c>.
/// </summary>
/// <remarks>
/// To enable the peek mode, the executing user requires Listen claim to work.
/// </remarks>
/// <seealso href="https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#azure-service-bus-data-sender"/>
public bool EnablePeekMode { get; set; }

/// <summary>
/// Gets or sets the queue name, which is checked if it exists.
/// </summary>
public string? QueueName { get; set; }
}
Loading

0 comments on commit 39c312d

Please sign in to comment.