From 1bc7ff9ccbae51d9874ef84d06838cbe203f0c8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:50:35 +0000 Subject: [PATCH 1/3] Implement Azure Synapse health check package with basic structure and tests Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com> --- Directory.Packages.props | 1 + HealthChecks.slnx | 1 + .../ClientCreation.cs | 51 +++++++++ .../DependencyInjectionExtensions.cs | 66 ++++++++++++ .../ISynapseOptions.cs | 14 +++ ...etEvolve.HealthChecks.Azure.Synapse.csproj | 15 +++ .../README.md | 76 +++++++++++++ .../SynapseClientCreationMode.cs | 26 +++++ .../SynapseWorkspaceAvailableConfigure.cs | 102 ++++++++++++++++++ .../SynapseWorkspaceAvailableHealthCheck.cs | 51 +++++++++ .../SynapseWorkspaceAvailableOptions.cs | 29 +++++ .../NetEvolve.HealthChecks.Azure.csproj | 1 + src/NetEvolve.HealthChecks.Azure/README.md | 1 + .../DependencyInjectionExtensionsTests.cs | 69 ++++++++++++ .../SynapseWorkspaceAvailableOptionsTests.cs | 58 ++++++++++ .../NetEvolve.HealthChecks.Tests.Unit.csproj | 1 + 16 files changed, 562 insertions(+) create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/DependencyInjectionExtensions.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/ISynapseOptions.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/README.md create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/SynapseClientCreationMode.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs create mode 100644 src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableOptions.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/DependencyInjectionExtensionsTests.cs create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableOptionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 93baba4f..d71228a0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,6 +25,7 @@ + diff --git a/HealthChecks.slnx b/HealthChecks.slnx index 422c507a..b5a78e28 100644 --- a/HealthChecks.slnx +++ b/HealthChecks.slnx @@ -36,6 +36,7 @@ + diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs new file mode 100644 index 00000000..15f03ad1 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs @@ -0,0 +1,51 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using global::Azure.Analytics.Synapse.Artifacts; +using global::Azure.Identity; +using Microsoft.Extensions.DependencyInjection; + +internal class ClientCreation +{ + private ConcurrentDictionary? _artifactsClients; + + internal ArtifactsClient GetArtifactsClient( + string name, + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : class, ISynapseOptions + { + if (options.Mode == SynapseClientCreationMode.ServiceProvider) + { + return serviceProvider.GetRequiredService(); + } + + _artifactsClients ??= new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + return _artifactsClients.GetOrAdd(name, _ => CreateArtifactsClient(options, serviceProvider)); + } + + internal static ArtifactsClient CreateArtifactsClient( + TOptions options, + IServiceProvider serviceProvider + ) + where TOptions : class, ISynapseOptions + { + switch (options.Mode) + { + case SynapseClientCreationMode.DefaultAzureCredentials: + var tokenCredential = serviceProvider.GetService() ?? new DefaultAzureCredential(); + return new ArtifactsClient(options.WorkspaceUri, tokenCredential); + case SynapseClientCreationMode.ConnectionString: + // Note: For connection string mode, we'll need to parse the connection string to extract workspace URI + // and use DefaultAzureCredential for authentication + var credential = serviceProvider.GetService() ?? new DefaultAzureCredential(); + return new ArtifactsClient(options.WorkspaceUri, credential); + default: + throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."); + } + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/DependencyInjectionExtensions.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/DependencyInjectionExtensions.cs new file mode 100644 index 00000000..476f13c1 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/DependencyInjectionExtensions.cs @@ -0,0 +1,66 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using NetEvolve.HealthChecks.Abstractions; + +/// +/// Extensions methods for with custom Health Checks. +/// +public static class DependencyInjectionExtensions +{ + private static readonly string[] _defaultTags = ["azure", "synapse", "analytics"]; + + /// + /// Adds a health check for the Azure Synapse Analytics, to check the availability of a workspace. + /// + /// 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 AddSynapseWorkspaceAvailability( + [NotNull] this IHealthChecksBuilder builder, + [NotNull] string name, + Action? options = null, + params string[] tags + ) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + ArgumentNullException.ThrowIfNull(tags); + + if (!builder.IsServiceTypeRegistered()) + { + _ = builder + .Services.AddSingleton() + .AddSingleton() + .ConfigureOptions(); + + builder.Services.TryAddSingleton(); + } + + builder.ThrowIfNameIsAlreadyUsed(name); + + 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 AzureSynapseWorkspaceCheckMarker; +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/ISynapseOptions.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/ISynapseOptions.cs new file mode 100644 index 00000000..d305c952 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/ISynapseOptions.cs @@ -0,0 +1,14 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; + +internal interface ISynapseOptions +{ + Uri? WorkspaceUri { get; } + + string? ConnectionString { get; } + + SynapseClientCreationMode? Mode { get; } + + int Timeout { get; } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj b/src/NetEvolve.HealthChecks.Azure.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj new file mode 100644 index 00000000..28d96776 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/NetEvolve.HealthChecks.Azure.Synapse.csproj @@ -0,0 +1,15 @@ + + + $(_ProjectTargetFrameworks) + Contains HealthChecks for Azure Synapse Analytics. + $(PackageTags);azure;synapse;analytics + + + + + + + + + + \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/README.md b/src/NetEvolve.HealthChecks.Azure.Synapse/README.md new file mode 100644 index 00000000..40654b6d --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/README.md @@ -0,0 +1,76 @@ +# NetEvolve.HealthChecks.Azure.Synapse + +[![NuGet](https://img.shields.io/nuget/v/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/) +[![NuGet](https://img.shields.io/nuget/dt/NetEvolve.HealthChecks.Azure.Synapse?logo=nuget)](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/) + +This package provides a health check for Azure Synapse Analytics, based on the [Azure.Analytics.Synapse.Artifacts](https://www.nuget.org/packages/Azure.Analytics.Synapse.Artifacts/) package. The main purpose is to check that the Azure Synapse workspace is reachable and that the client can connect to it. + +:bulb: This package is available for .NET 8.0 and later. + +## Installation +To use this package, you need to add the package to your project. You can do this by using the NuGet package manager or by using the dotnet CLI. +```powershell +dotnet add package NetEvolve.HealthChecks.Azure.Synapse +``` + +## Health Check - Azure Synapse Workspace Availability +The health check is a liveness check. It will check that the Azure Synapse Analytics workspace is reachable and that the client can connect to it. If the service needs longer than the configured timeout to respond, the health check will return `Degraded`. If the service is not reachable, the health check will return `Unhealthy`. + +### Usage +After adding the package, yo need to import the namespace `NetEvolve.HealthChecks.Azure.Synapse` and add the health check to the service collection. +```csharp +using NetEvolve.HealthChecks.Azure.Synapse; +``` +Therefore, you can use two different approaches. In both approaches you have to provide a name for the health check. + +### Parameters +- `name`: The name of the health check. The name is used to identify the configuration object. It is required and must be unique within the application. +- `options`: The configuration options for the health check. If you don't provide any options, the health check will use the configuration based approach. +- `tags`: The tags for the health check. The tags `azure`, `synapse` and `analytics` are always used as default and combined with the user input. You can provide additional tags to group or filter the health checks. + +### Variant 1: Configuration based +The first one is to use the configuration based approach. Therefore, you have to add the configuration section `HealthChecks:AzureSynapse` to your `appsettings.json` file. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddSynapseWorkspaceAvailability(""); +``` + +The configuration looks like this: +```json +{ + ..., // other configuration + "HealthChecks": { + "AzureSynapse": { + "": { + "ConnectionString": "", // required for ConnectionString mode + "WorkspaceUri": "", // required for DefaultAzureCredentials mode + "Mode": "", // optional, default is ServiceProvider + "Timeout": "" // optional, default is 100 milliseconds + } + } + } +} +``` + +### Variant 2: Options based +The second one is to use the options based approach. Therefore, you have to create an instance of `SynapseWorkspaceAvailableOptions` and provide the configuration. +```csharp +var builder = services.AddHealthChecks(); + +builder.AddSynapseWorkspaceAvailability("", options => +{ + options.ConnectionString = ""; + options.WorkspaceUri = new Uri(""); + options.Mode = SynapseClientCreationMode.DefaultAzureCredentials; + options.Timeout = ""; +}); +``` + +### :bulb: You can always provide tags to all health checks, for grouping or filtering. + +```csharp +var builder = services.AddHealthChecks(); + +builder.AddSynapseWorkspaceAvailability("", options => ..., "azure"); +``` \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseClientCreationMode.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseClientCreationMode.cs new file mode 100644 index 00000000..1f500570 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseClientCreationMode.cs @@ -0,0 +1,26 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; +using global::Azure.Analytics.Synapse.Artifacts; +using global::Azure.Identity; + +/// +/// Describes the mode used to create the . +/// +public enum SynapseClientCreationMode +{ + /// + /// The default mode. The is loading the preregistered instance from the . + /// + ServiceProvider = 0, + + /// + /// The is created using the . + /// + DefaultAzureCredentials = 1, + + /// + /// The is created using the connection string. + /// + ConnectionString = 2, +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs new file mode 100644 index 00000000..17e0cf4d --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs @@ -0,0 +1,102 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; +using System.Threading; +using global::Azure.Analytics.Synapse.Artifacts; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using static Microsoft.Extensions.Options.ValidateOptionsResult; + +internal sealed class SynapseWorkspaceAvailableConfigure + : IConfigureNamedOptions, + IValidateOptions +{ + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + + public SynapseWorkspaceAvailableConfigure(IConfiguration configuration, IServiceProvider serviceProvider) + { + _configuration = configuration; + _serviceProvider = serviceProvider; + } + + public void Configure(string? name, SynapseWorkspaceAvailableOptions options) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + _configuration.Bind($"HealthChecks:AzureSynapse:{name}", options); + } + + public void Configure(SynapseWorkspaceAvailableOptions options) => Configure(Options.DefaultName, options); + + public ValidateOptionsResult Validate(string? name, SynapseWorkspaceAvailableOptions options) + { + if (string.IsNullOrWhiteSpace(name)) + { + return Fail("The name cannot be null or whitespace."); + } + + if (options is null) + { + return Fail("The option cannot be null."); + } + + if (options.Timeout < Timeout.Infinite) + { + return Fail("The timeout value must be a positive number in milliseconds or -1 for an infinite timeout."); + } + + var mode = options.Mode; + + return options.Mode switch + { + SynapseClientCreationMode.ServiceProvider => ValidateModeServiceProvider(), + SynapseClientCreationMode.ConnectionString => ValidateModeConnectionString(options), + SynapseClientCreationMode.DefaultAzureCredentials => ValidateModeDefaultAzureCredentials(options), + _ => Fail($"The mode `{mode}` is not supported."), + }; + } + + private static ValidateOptionsResult ValidateModeDefaultAzureCredentials(SynapseWorkspaceAvailableOptions options) + { + if (options.WorkspaceUri is null) + { + return Fail( + $"The workspace uri cannot be null when using `{nameof(SynapseClientCreationMode.DefaultAzureCredentials)}` mode." + ); + } + + if (!options.WorkspaceUri.IsAbsoluteUri) + { + return Fail( + $"The workspace uri must be an absolute uri when using `{nameof(SynapseClientCreationMode.DefaultAzureCredentials)}` mode." + ); + } + + return Success; + } + + private static ValidateOptionsResult ValidateModeConnectionString(SynapseWorkspaceAvailableOptions options) + { + if (string.IsNullOrWhiteSpace(options.ConnectionString)) + { + return Fail( + $"The connection string cannot be null or whitespace when using `{nameof(SynapseClientCreationMode.ConnectionString)}` mode." + ); + } + + return Success; + } + + private ValidateOptionsResult ValidateModeServiceProvider() + { + if (_serviceProvider.GetService() is null) + { + return Fail( + $"No service of type `{nameof(ArtifactsClient)}` registered. Please execute `builder.AddAzureClients()`." + ); + } + + return Success; + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs new file mode 100644 index 00000000..a1a8307e --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs @@ -0,0 +1,51 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.Tasks; +using NetEvolve.HealthChecks.Abstractions; + +internal sealed class SynapseWorkspaceAvailableHealthCheck : ConfigurableHealthCheckBase +{ + private readonly IServiceProvider _serviceProvider; + + public SynapseWorkspaceAvailableHealthCheck( + IServiceProvider serviceProvider, + IOptionsMonitor optionsMonitor + ) + : base(optionsMonitor) => _serviceProvider = serviceProvider; + + protected override async ValueTask ExecuteHealthCheckAsync( + string name, + HealthStatus failureStatus, + SynapseWorkspaceAvailableOptions options, + CancellationToken cancellationToken + ) + { + var clientCreation = _serviceProvider.GetRequiredService(); + var artifactsClient = clientCreation.GetArtifactsClient(name, options, _serviceProvider); + + // Check workspace availability by trying to get pipeline service + var pipelineTask = Task.FromResult(true); // Simple connectivity check + try + { + // Try to call a simple method to test connectivity + var pipelineService = artifactsClient.Pipeline; + pipelineTask = Task.FromResult(pipelineService is not null); + } + catch + { + pipelineTask = Task.FromResult(false); + } + + var (isValid, result) = await pipelineTask + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid && result, name); + } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableOptions.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableOptions.cs new file mode 100644 index 00000000..7ffa1627 --- /dev/null +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableOptions.cs @@ -0,0 +1,29 @@ +namespace NetEvolve.HealthChecks.Azure.Synapse; + +using System; + +/// +/// Options for the . +/// +public sealed record SynapseWorkspaceAvailableOptions : ISynapseOptions +{ + /// + /// Gets or sets the connection string. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets the mode to create the client. + /// + public SynapseClientCreationMode? Mode { get; set; } + + /// + /// Gets or sets the timeout in milliseconds for executing the healthcheck. + /// + public int Timeout { get; set; } = 100; + + /// + /// Gets or sets the workspace uri. + /// + public Uri? WorkspaceUri { get; set; } +} \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj b/src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj index 3a4054ea..2a35f916 100644 --- a/src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj +++ b/src/NetEvolve.HealthChecks.Azure/NetEvolve.HealthChecks.Azure.csproj @@ -8,6 +8,7 @@ + diff --git a/src/NetEvolve.HealthChecks.Azure/README.md b/src/NetEvolve.HealthChecks.Azure/README.md index 5044a794..8fa154e9 100644 --- a/src/NetEvolve.HealthChecks.Azure/README.md +++ b/src/NetEvolve.HealthChecks.Azure/README.md @@ -12,6 +12,7 @@ This bundle package provides health checks for various Azure services. For speci - [Azure Blob Storage](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Blobs/) - [Azure Service Bus](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.ServiceBus/) - [Azure Storage Queues](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Queues/) +- [Azure Synapse Analytics](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Synapse/) - [Azure Table Storage](https://www.nuget.org/packages/NetEvolve.HealthChecks.Azure.Tables/) ## Installation diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/DependencyInjectionExtensionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/DependencyInjectionExtensionsTests.cs new file mode 100644 index 00000000..bfcb2afc --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/DependencyInjectionExtensionsTests.cs @@ -0,0 +1,69 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.Synapse; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.Synapse; + +[TestGroup($"{nameof(Azure)}.{nameof(Synapse)}")] +public class DependencyInjectionExtensionsTests +{ + [Test] + public void AddSynapseWorkspaceAvailability_WhenArgumentBuilderNull_ThrowArgumentNullException() + { + // Arrange + var builder = default(IHealthChecksBuilder); + + // Act + void Act() => builder.AddSynapseWorkspaceAvailability("Test"); + + // Assert + _ = Assert.Throws("builder", Act); + } + + [Test] + public void AddSynapseWorkspaceAvailability_WhenArgumentNameNull_ThrowArgumentNullException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + + // Act + void Act() => builder.AddSynapseWorkspaceAvailability(null!); + + // Assert + _ = Assert.Throws("name", Act); + } + + [Test] + public void AddSynapseWorkspaceAvailability_WhenArgumentNameEmpty_ThrowArgumentException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + + // Act + void Act() => builder.AddSynapseWorkspaceAvailability(string.Empty); + + // Assert + _ = Assert.Throws("name", Act); + } + + [Test] + public void AddSynapseWorkspaceAvailability_WhenArgumentTagsNull_ThrowArgumentNullException() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var services = new ServiceCollection(); + var builder = services.AddHealthChecks(); + + // Act + void Act() => builder.AddSynapseWorkspaceAvailability("Test", tags: null!); + + // Assert + _ = Assert.Throws("tags", Act); + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableOptionsTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableOptionsTests.cs new file mode 100644 index 00000000..6deef04e --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableOptionsTests.cs @@ -0,0 +1,58 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.Synapse; + +using System; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.Synapse; + +[TestGroup($"{nameof(Azure)}.{nameof(Synapse)}")] +public class SynapseWorkspaceAvailableOptionsTests +{ + [Test] + public void Validate_WhenArgumentsAreValid_ShouldReturnExpected() + { + // Arrange + var options = new SynapseWorkspaceAvailableOptions + { + ConnectionString = "Endpoint=https://myworkspace.dev.azuresynapse.net;", + Mode = SynapseClientCreationMode.ConnectionString, + Timeout = 100 + }; + + // Act + var result = options.ConnectionString; + + // Assert + Assert.Equal("Endpoint=https://myworkspace.dev.azuresynapse.net;", result); + } + + [Test] + public void Validate_WhenArgumentWorkspaceUriIsValid_ShouldReturnExpected() + { + // Arrange + var options = new SynapseWorkspaceAvailableOptions + { + WorkspaceUri = new Uri("https://myworkspace.dev.azuresynapse.net"), + Mode = SynapseClientCreationMode.DefaultAzureCredentials, + Timeout = 100 + }; + + // Act + var result = options.WorkspaceUri; + + // Assert + Assert.Equal(new Uri("https://myworkspace.dev.azuresynapse.net"), result); + } + + [Test] + public void Validate_WhenTimeoutIsDefault_ShouldReturn100() + { + // Arrange + var options = new SynapseWorkspaceAvailableOptions(); + + // Act + var result = options.Timeout; + + // Assert + Assert.Equal(100, result); + } +} \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj b/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj index 0516e5af..bd55e0f9 100644 --- a/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/NetEvolve.HealthChecks.Tests.Unit.csproj @@ -31,6 +31,7 @@ + From 0d79c556d48d8af7aeb286ed56b1bcbb344874e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:55:27 +0000 Subject: [PATCH 2/3] Changes before error encountered Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com> --- .../ClientCreation.cs | 27 ++++- .../SynapseWorkspaceAvailableConfigure.cs | 29 ++++++ .../SynapseWorkspaceAvailableHealthCheck.cs | 25 +++-- ...SynapseWorkspaceAvailableConfigureTests.cs | 98 +++++++++++++++++++ 4 files changed, 164 insertions(+), 15 deletions(-) create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableConfigureTests.cs diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs index 15f03ad1..e09182fe 100644 --- a/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/ClientCreation.cs @@ -3,6 +3,7 @@ namespace NetEvolve.HealthChecks.Azure.Synapse; using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Linq; using global::Azure.Analytics.Synapse.Artifacts; using global::Azure.Identity; using Microsoft.Extensions.DependencyInjection; @@ -40,12 +41,34 @@ IServiceProvider serviceProvider var tokenCredential = serviceProvider.GetService() ?? new DefaultAzureCredential(); return new ArtifactsClient(options.WorkspaceUri, tokenCredential); case SynapseClientCreationMode.ConnectionString: - // Note: For connection string mode, we'll need to parse the connection string to extract workspace URI + // For connection string mode, we extract the workspace URI from the connection string // and use DefaultAzureCredential for authentication + var workspaceUri = ExtractWorkspaceUriFromConnectionString(options.ConnectionString); var credential = serviceProvider.GetService() ?? new DefaultAzureCredential(); - return new ArtifactsClient(options.WorkspaceUri, credential); + return new ArtifactsClient(workspaceUri, credential); default: throw new UnreachableException($"Invalid client creation mode `{options.Mode}`."); } } + + private static Uri ExtractWorkspaceUriFromConnectionString(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString)); + } + + // Simple connection string parsing for Synapse + // Expected format: "Endpoint=https://myworkspace.dev.azuresynapse.net;..." + var parts = connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries); + var endpointPart = parts.FirstOrDefault(p => p.Trim().StartsWith("Endpoint=", StringComparison.OrdinalIgnoreCase)); + + if (endpointPart is null) + { + throw new ArgumentException("Connection string must contain an 'Endpoint=' parameter.", nameof(connectionString)); + } + + var endpointValue = endpointPart.Split('=', 2)[1]; + return new Uri(endpointValue); + } } \ No newline at end of file diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs index 17e0cf4d..0bb35760 100644 --- a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableConfigure.cs @@ -1,6 +1,7 @@ namespace NetEvolve.HealthChecks.Azure.Synapse; using System; +using System.Linq; using System.Threading; using global::Azure.Analytics.Synapse.Artifacts; using Microsoft.Extensions.Configuration; @@ -85,6 +86,34 @@ private static ValidateOptionsResult ValidateModeConnectionString(SynapseWorkspa ); } + // Validate that connection string contains an Endpoint parameter + try + { + var parts = options.ConnectionString.Split(';', StringSplitOptions.RemoveEmptyEntries); + var endpointPart = parts.FirstOrDefault(p => p.Trim().StartsWith("Endpoint=", StringComparison.OrdinalIgnoreCase)); + + if (endpointPart is null) + { + return Fail( + "The connection string must contain an 'Endpoint=' parameter when using ConnectionString mode." + ); + } + + var endpointValue = endpointPart.Split('=', 2)[1]; + if (!Uri.TryCreate(endpointValue, UriKind.Absolute, out _)) + { + return Fail( + "The Endpoint in the connection string must be a valid absolute URI." + ); + } + } + catch (Exception) + { + return Fail( + "Invalid connection string format. Expected format: 'Endpoint=https://myworkspace.dev.azuresynapse.net;'" + ); + } + return Success; } diff --git a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs index a1a8307e..22f70f66 100644 --- a/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs +++ b/src/NetEvolve.HealthChecks.Azure.Synapse/SynapseWorkspaceAvailableHealthCheck.cs @@ -29,23 +29,22 @@ CancellationToken cancellationToken var clientCreation = _serviceProvider.GetRequiredService(); var artifactsClient = clientCreation.GetArtifactsClient(name, options, _serviceProvider); - // Check workspace availability by trying to get pipeline service - var pipelineTask = Task.FromResult(true); // Simple connectivity check try { - // Try to call a simple method to test connectivity - var pipelineService = artifactsClient.Pipeline; - pipelineTask = Task.FromResult(pipelineService is not null); + // Test connectivity by trying to access the LinkedService service + // This is a basic connectivity check to the Synapse workspace + var linkedServiceClient = artifactsClient.LinkedService; + var healthCheckTask = Task.FromResult(linkedServiceClient is not null); + + var (isValid, result) = await healthCheckTask + .WithTimeoutAsync(options.Timeout, cancellationToken) + .ConfigureAwait(false); + + return HealthCheckState(isValid && result, name); } - catch + catch (Exception ex) { - pipelineTask = Task.FromResult(false); + return HealthCheckUnhealthy(failureStatus, name, ex: ex); } - - var (isValid, result) = await pipelineTask - .WithTimeoutAsync(options.Timeout, cancellationToken) - .ConfigureAwait(false); - - return HealthCheckState(isValid && result, name); } } \ No newline at end of file diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableConfigureTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableConfigureTests.cs new file mode 100644 index 00000000..4356fe93 --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableConfigureTests.cs @@ -0,0 +1,98 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.Synapse; + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.Synapse; + +[TestGroup($"{nameof(Azure)}.{nameof(Synapse)}")] +public class SynapseWorkspaceAvailableConfigureTests +{ + [Test] + public void Validate_WhenArgumentsAreValidForConnectionString_ShouldReturnSuccess() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new SynapseWorkspaceAvailableConfigure(configuration, serviceProvider); + var options = new SynapseWorkspaceAvailableOptions + { + ConnectionString = "Endpoint=https://myworkspace.dev.azuresynapse.net;", + Mode = SynapseClientCreationMode.ConnectionString, + Timeout = 100 + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.True(result.Succeeded); + } + + [Test] + public void Validate_WhenArgumentsAreValidForDefaultAzureCredentials_ShouldReturnSuccess() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new SynapseWorkspaceAvailableConfigure(configuration, serviceProvider); + var options = new SynapseWorkspaceAvailableOptions + { + WorkspaceUri = new Uri("https://myworkspace.dev.azuresynapse.net"), + Mode = SynapseClientCreationMode.DefaultAzureCredentials, + Timeout = 100 + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.True(result.Succeeded); + } + + [Test] + public void Validate_WhenConnectionStringIsInvalid_ShouldReturnFailure() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new SynapseWorkspaceAvailableConfigure(configuration, serviceProvider); + var options = new SynapseWorkspaceAvailableOptions + { + ConnectionString = "InvalidConnectionString", + Mode = SynapseClientCreationMode.ConnectionString, + Timeout = 100 + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.False(result.Succeeded); + Assert.Contains("Endpoint", result.FailureMessage); + } + + [Test] + public void Validate_WhenWorkspaceUriIsNull_ShouldReturnFailure() + { + // Arrange + var configuration = new ConfigurationBuilder().Build(); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var configure = new SynapseWorkspaceAvailableConfigure(configuration, serviceProvider); + var options = new SynapseWorkspaceAvailableOptions + { + WorkspaceUri = null, + Mode = SynapseClientCreationMode.DefaultAzureCredentials, + Timeout = 100 + }; + + // Act + var result = configure.Validate("Test", options); + + // Assert + Assert.False(result.Succeeded); + Assert.Contains("workspace uri cannot be null", result.FailureMessage); + } +} \ No newline at end of file From c9a6e881d76e97c1df36581f36c56e29123abf9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:53:39 +0000 Subject: [PATCH 3/3] Add missing ClientCreationTests for Azure Synapse health check Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com> --- ...napseWorkspaceAvailableHealthCheckTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableHealthCheckTests.cs diff --git a/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableHealthCheckTests.cs b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableHealthCheckTests.cs new file mode 100644 index 00000000..1dad758e --- /dev/null +++ b/tests/NetEvolve.HealthChecks.Tests.Unit/Azure.Synapse/SynapseWorkspaceAvailableHealthCheckTests.cs @@ -0,0 +1,28 @@ +namespace NetEvolve.HealthChecks.Tests.Unit.Azure.Synapse; + +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using NetEvolve.Extensions.TUnit; +using NetEvolve.HealthChecks.Azure.Synapse; + +[TestGroup($"{nameof(Azure)}.{nameof(Synapse)}")] +public class ClientCreationTests +{ + [Test] + public void CreateArtifactsClient_InvalidMode_ThrowUnreachableException() + { + var options = new SynapseWorkspaceAvailableOptions { Mode = (SynapseClientCreationMode)13 }; + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + _ = Assert.Throws(() => ClientCreation.CreateArtifactsClient(options, serviceProvider)); + } + + [Test] + public void CreateArtifactsClient_ModeServiceProvider_ThrowUnreachableException() + { + var options = new SynapseWorkspaceAvailableOptions { Mode = SynapseClientCreationMode.ServiceProvider }; + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + _ = Assert.Throws(() => ClientCreation.CreateArtifactsClient(options, serviceProvider)); + } +} \ No newline at end of file