From 39c312dae51f86a03c41860680f0ad84b980bcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Tue, 17 Dec 2024 08:11:33 +0100 Subject: [PATCH] feat: Azure ServiceBus package Closed: #41 --- Directory.Packages.props | 4 +- HealthChecks.sln | 15 +++ .../ClientCreation.cs | 103 ++++++++++++++++++ .../ClientCreationMode.cs | 18 +++ .../DependencyInjectionExtensions.cs | 67 ++++++++++++ ...volve.HealthChecks.Azure.ServiceBus.csproj | 21 ++++ .../ServiceBusOptionsBase.cs | 25 +++++ .../ServiceBusQueueHealthCheck.cs | 65 +++++++++++ .../ServiceBusQueueOptions.cs | 20 ++++ .../ServiceBusQueueOptionsConfigure.cs | 15 +++ .../ServiceBusSubscriptionHealthCheck.cs | 68 ++++++++++++ .../ServiceBusSubscriptionOptions.cs | 15 +++ .../ServiceBusTopicHealthCheck.cs | 38 +++++++ .../ServiceBusTopicOptions.cs | 6 + .../HealthCheckArchitecture.cs | 2 + ...lve.HealthChecks.Tests.Architecture.csproj | 1 + .../Azure/ServiceBus/ServiceBusContainer.cs | 24 ++++ .../ServiceBusQueueHealthCheckTests.cs | 53 +++++++++ ...olve.HealthChecks.Tests.Integration.csproj | 2 + 19 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptionsConfigure.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionHealthCheck.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionOptions.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicHealthCheck.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicOptions.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusContainer.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusQueueHealthCheckTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b0b18c..bbf3c9f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,7 @@ + @@ -49,6 +50,7 @@ + @@ -60,4 +62,4 @@ - + \ No newline at end of file diff --git a/HealthChecks.sln b/HealthChecks.sln index 4bd8157..08f6cda 100644 --- a/HealthChecks.sln +++ b/HealthChecks.sln @@ -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 @@ -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 @@ -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} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs new file mode 100644 index 0000000..9016f4f --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreation.cs @@ -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? _serviceBusClients; + private static ConcurrentDictionary< + string, + ServiceBusAdministrationClient + >? _serviceBusAdministrationClients; + + internal static ServiceBusClient GetClient( + string name, + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : ServiceBusOptionsBase + { + if (options.Mode == ClientCreationMode.ServiceProvider) + { + return serviceProvider.GetRequiredService(); + } + + if (_serviceBusClients is null) + { + _serviceBusClients = new ConcurrentDictionary( + StringComparer.OrdinalIgnoreCase + ); + } + + return _serviceBusClients.GetOrAdd(name, _ => CreateClient(options, serviceProvider)); + } + + internal static ServiceBusAdministrationClient GetAdministrationClient( + string name, + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : ServiceBusOptionsBase + { + if (options.Mode == ClientCreationMode.ServiceProvider) + { + return serviceProvider.GetRequiredService(); + } + + if (_serviceBusAdministrationClients is null) + { + _serviceBusAdministrationClients = new ConcurrentDictionary< + string, + ServiceBusAdministrationClient + >(StringComparer.OrdinalIgnoreCase); + } + + return _serviceBusAdministrationClients.GetOrAdd( + name, + _ => CreateAdministrationClient(options, serviceProvider) + ); + } + + private static ServiceBusClient CreateClient( + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : ServiceBusOptionsBase => + options.Mode switch + { + ClientCreationMode.ServiceProvider => + serviceProvider.GetRequiredService(), + ClientCreationMode.DefaultAzureCredentials => new ServiceBusClient( + options.FullyQualifiedNamespace, + serviceProvider.GetService() ?? new DefaultAzureCredential() + ), + ClientCreationMode.ConnectionString => new ServiceBusClient(options.ConnectionString), + _ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."), + }; + + private static ServiceBusAdministrationClient CreateAdministrationClient( + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : ServiceBusOptionsBase => + options.Mode switch + { + ClientCreationMode.ServiceProvider => + serviceProvider.GetRequiredService(), + ClientCreationMode.DefaultAzureCredentials => new ServiceBusAdministrationClient( + options.FullyQualifiedNamespace, + serviceProvider.GetService() ?? new DefaultAzureCredential() + ), + ClientCreationMode.ConnectionString => new ServiceBusAdministrationClient( + options.ConnectionString + ), + _ => throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."), + }; +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs new file mode 100644 index 0000000..df84c8f --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ClientCreationMode.cs @@ -0,0 +1,18 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +using System; +using global::Azure.Messaging.ServiceBus; + +/// +/// Describes the mode to create or retrieve a . +/// +public enum ClientCreationMode +{ + /// + /// The default mode. The is loading the preregistered instance from the . + /// + ServiceProvider = 0, + + DefaultAzureCredentials, + ConnectionString, +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..fc573f1 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/DependencyInjectionExtensions.cs @@ -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; + +/// +/// Extensions methods for with custom Health Checks. +/// +public static class DependencyInjectionExtensions +{ + private static readonly string[] _defaultTags = ["storage", "azure", "servicebus"]; + + /// + /// Adds a health check for an Azure Service Bus Queue. + /// + /// The . + /// The name of the . + /// An optional action to configure. + /// A list of additional tags that can be used to filter sets of health checks. Optional. + /// The is . + /// The is . + /// The is or whitespace. + /// The is already in use. + /// The is . + public static IHealthChecksBuilder AddServiceBusQueueHealthCheck( + [NotNull] this IHealthChecksBuilder builder, + [NotNull] string name, + Action? options = null, + params string[] tags + ) + { + ArgumentNullException.ThrowIfNull(builder); + Argument.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(tags); + + if (!builder.IsServiceTypeRegistered()) + { + _ = builder + .Services.AddSingleton() + .AddSingleton() + .ConfigureOptions(); + } + + if (builder.IsNameAlreadyUsed(name)) + { + throw new ArgumentException($"Name `{name}` already in use.", nameof(name), null); + } + + if (options is not null) + { + _ = builder.Services.Configure(name, options); + } + + return builder.AddCheck( + name, + HealthStatus.Unhealthy, + _defaultTags.Union(tags, StringComparer.OrdinalIgnoreCase) + ); + } + + private sealed partial class ServiceBusQueueMarker { } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj b/src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj new file mode 100644 index 0000000..797bfc3 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/NetEvolve.HealthChecks.Azure.ServiceBus.csproj @@ -0,0 +1,21 @@ + + + + $(_ProjectTargetFrameworks) + + Contains HealthChecks for Azure Service Bus. + $(PackageTags);azure;servicebus; + + + + + + + + + + + + + + diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs new file mode 100644 index 0000000..ae4938d --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusOptionsBase.cs @@ -0,0 +1,25 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +public abstract class ServiceBusOptionsBase +{ + /// + /// Gets or sets the client creation mode, default is . + /// + public ClientCreationMode Mode { get; set; } + + /// + /// Gets or sets the azure service bus connection string. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets the fully qualified namespace. + /// + public string FullyQualifiedNamespace { get; set; } + + /// + /// The timeout to use when connecting and executing tasks against database. + /// Default is 100 milliseconds. + /// + public int Timeout { get; set; } = 100; +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs new file mode 100644 index 0000000..71cdf5f --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueHealthCheck.cs @@ -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 +{ + private readonly IServiceProvider _serviceProvider; + + public ServiceBusQueueHealthCheck( + IServiceProvider serviceProvider, + IOptionsMonitor optionsMonitor + ) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + ServiceBusQueueOptions options, + CancellationToken cancellationToken + ) => + options.EnablePeekMode + ? ExecutePeekHealthCheckAsync(name, options, cancellationToken) + : ExecuteHealthCheckAsync(name, options, cancellationToken); + + private async ValueTask 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); + } + + private async ValueTask 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); + } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs new file mode 100644 index 0000000..6300362 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptions.cs @@ -0,0 +1,20 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +using Microsoft.Extensions.Options; + +public class ServiceBusQueueOptions : ServiceBusOptionsBase +{ + /// + /// Gets or sets a value indicating whether to enable peek mode, default is false. + /// + /// + /// To enable the peek mode, the executing user requires Listen claim to work. + /// + /// + public bool EnablePeekMode { get; set; } + + /// + /// Gets or sets the queue name, which is checked if it exists. + /// + public string? QueueName { get; set; } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptionsConfigure.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptionsConfigure.cs new file mode 100644 index 0000000..75ffd1e --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusQueueOptionsConfigure.cs @@ -0,0 +1,15 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +using Microsoft.Extensions.Options; + +internal class ServiceBusQueueOptionsConfigure + : IConfigureNamedOptions, + IValidateOptions +{ + public void Configure(string? name, ServiceBusQueueOptions options) { } + + public void Configure(ServiceBusQueueOptions options) { } + + public ValidateOptionsResult Validate(string? name, ServiceBusQueueOptions options) => + ValidateOptionsResult.Success; +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionHealthCheck.cs new file mode 100644 index 0000000..6b40701 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionHealthCheck.cs @@ -0,0 +1,68 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +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 ServiceBusSubscriptionHealthCheck + : ConfigurableHealthCheckBase +{ + private readonly IServiceProvider _serviceProvider; + + public ServiceBusSubscriptionHealthCheck( + IOptionsMonitor optionsMonitor, + IServiceProvider serviceProvider + ) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + ServiceBusSubscriptionOptions options, + CancellationToken cancellationToken + ) => + options.EnablePeekMode + ? ExecutePeekHealthCheckAsync(name, options, cancellationToken) + : ExecuteHealthCheckAsync(name, options, cancellationToken); + + private async ValueTask ExecuteHealthCheckAsync( + string name, + ServiceBusSubscriptionOptions options, + CancellationToken cancellationToken + ) + { + var client = ClientCreation.GetAdministrationClient(name, options, _serviceProvider); + + var (isValid, subscription) = await client + .GetSubscriptionRuntimePropertiesAsync( + options.TopicName, + options.SubscriptionName, + cancellationToken + ) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid && subscription is not null, name); + } + + private async ValueTask ExecutePeekHealthCheckAsync( + string name, + ServiceBusSubscriptionOptions options, + CancellationToken cancellationToken + ) + { + var client = ClientCreation.GetClient(name, options, _serviceProvider); + + var receiver = client.CreateReceiver(options.TopicName, options.SubscriptionName); + + var (isValid, _) = await receiver + .ReceiveMessageAsync(cancellationToken: cancellationToken) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid, name); + } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionOptions.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionOptions.cs new file mode 100644 index 0000000..e60eb45 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusSubscriptionOptions.cs @@ -0,0 +1,15 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +public class ServiceBusSubscriptionOptions : ServiceBusOptionsBase +{ + /// + /// Gets or sets a value indicating whether to enable peek mode, default is false. + /// + /// + /// To enable the peek mode, the executing user requires Listen claim to work. + /// + /// + public bool EnablePeekMode { get; set; } + public string SubscriptionName { get; set; } + public string TopicName { get; set; } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicHealthCheck.cs new file mode 100644 index 0000000..69b791c --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicHealthCheck.cs @@ -0,0 +1,38 @@ +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 ServiceBusTopicHealthCheck + : ConfigurableHealthCheckBase +{ + private readonly IServiceProvider _serviceProvider; + + public ServiceBusTopicHealthCheck( + IOptionsMonitor optionsMonitor, + IServiceProvider serviceProvider + ) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override async ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + ServiceBusTopicOptions options, + CancellationToken cancellationToken + ) + { + var client = ClientCreation.GetAdministrationClient(name, options, _serviceProvider); + + var (isValid, _) = await client + .GetTopicAsync(options.TopicName, cancellationToken: cancellationToken) + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid, name); + } +} diff --git a/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicOptions.cs b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicOptions.cs new file mode 100644 index 0000000..c1da371 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.ServiceBus/ServiceBusTopicOptions.cs @@ -0,0 +1,6 @@ +namespace NetEvolve.HealthChecks.Azure.ServiceBus; + +public class ServiceBusTopicOptions : ServiceBusOptionsBase +{ + public string TopicName { get; set; } +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs index f9a5772..c9da8e3 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/HealthCheckArchitecture.cs @@ -7,6 +7,7 @@ using NetEvolve.HealthChecks.Apache.Kafka; using NetEvolve.HealthChecks.Azure.Blobs; using NetEvolve.HealthChecks.Azure.Queues; +using NetEvolve.HealthChecks.Azure.ServiceBus; using NetEvolve.HealthChecks.Azure.Tables; using NetEvolve.HealthChecks.ClickHouse; using NetEvolve.HealthChecks.Dapr; @@ -37,6 +38,7 @@ private static Architecture LoadArchitecture() typeof(KafkaCheck).Assembly, typeof(BlobContainerAvailableHealthCheck).Assembly, typeof(QueueClientAvailableHealthCheck).Assembly, + typeof(ServiceBusQueueHealthCheck).Assembly, typeof(TableClientAvailableHealthCheck).Assembly, typeof(ClickHouseCheck).Assembly, typeof(DaprHealthCheck).Assembly, diff --git a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj index eade4cb..581f54d 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Architecture/NetEvolve.HealthChecks.Tests.Architecture.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusContainer.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusContainer.cs new file mode 100644 index 0000000..4ba8d29 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusContainer.cs @@ -0,0 +1,24 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.Azure.ServiceBus; + +using System; +using System.Threading.Tasks; +using Testcontainers.ServiceBus; +using TestContainer = Testcontainers.ServiceBus.ServiceBusContainer; + +public sealed class ServiceBusContainer : IAsyncLifetime, IAsyncDisposable +{ + private readonly TestContainer _container = new ServiceBusBuilder() + .WithAcceptLicenseAgreement(true) + .Build(); + + public string ConnectionString => _container.GetConnectionString(); + + public async ValueTask DisposeAsync() => await _container.DisposeAsync().ConfigureAwait(false); + + public async Task InitializeAsync() => await _container.StartAsync().ConfigureAwait(false); + + async Task IAsyncLifetime.DisposeAsync() => + await _container.DisposeAsync().ConfigureAwait(false); + + public const string QueueName = "queue.1"; +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusQueueHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusQueueHealthCheckTests.cs new file mode 100644 index 0000000..ba22368 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/Azure/ServiceBus/ServiceBusQueueHealthCheckTests.cs @@ -0,0 +1,53 @@ +namespace NetEvolve.HealthChecks.Tests.Integration.Azure.ServiceBus; + +using Microsoft.Extensions.Azure; +using NetEvolve.HealthChecks.Azure.ServiceBus; + +public class ServiceBusQueueHealthCheckTests + : HealthCheckTestBase, + IClassFixture +{ + private readonly ServiceBusContainer _container; + + public ServiceBusQueueHealthCheckTests(ServiceBusContainer container) => _container = container; + + [Fact] + public async Task AddServiceBusQueueHealthCheck_UseOptions_ModeServiceProvider_ShouldReturnHealthy() => + await RunAndVerify( + healthChecks => + { + _ = healthChecks.AddServiceBusQueueHealthCheck( + "ServiceBusQueueServiceProviderHealthy", + options => options.QueueName = ServiceBusContainer.QueueName + ); + }, + serviceBuilder: services => + { + services.AddAzureClients(clients => + _ = clients.AddServiceBusAdministrationClient(_container.ConnectionString) + ); + } + ); + + [Fact] + public async Task AddServiceBusQueueHealthCheck_UseOptions_EnablePeekModeServiceProvider_ShouldReturnHealthy() => + await RunAndVerify( + healthChecks => + { + _ = healthChecks.AddServiceBusQueueHealthCheck( + "ServiceBusQueueServiceProviderHealthy", + options => + { + options.EnablePeekMode = true; + options.QueueName = ServiceBusContainer.QueueName; + } + ); + }, + serviceBuilder: services => + { + services.AddAzureClients(clients => + _ = clients.AddServiceBusClient(_container.ConnectionString) + ); + } + ); +} diff --git a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj index 5e500f6..8014b68 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Integration/NetEvolve.HealthChecks.Tests.Integration.csproj @@ -24,6 +24,7 @@ + @@ -37,6 +38,7 @@ +