#pragma warning disable ASPIRECONNECTIONSTRINGS001
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Data.Common;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Qdrant;
using Aspire.Qdrant.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Qdrant.Client;
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for adding Qdrant resources to the application model.
/// </summary>
public static class QdrantBuilderExtensions
{
private const int QdrantPortGrpc = 6334;
private const int QdrantPortHttp = 6333;
private const string ApiKeyEnvVarName = "QDRANT__SERVICE__API_KEY";
private const string EnableStaticContentEnvVarName = "QDRANT__SERVICE__ENABLE_STATIC_CONTENT";
/// <summary>
/// Adds a Qdrant resource to the application. A container is used for local development.
/// </summary>
/// <remarks>
/// The .NET client library uses the gRPC port by default to communicate and this resource exposes that endpoint.
/// This version of the package defaults to the <inheritdoc cref="QdrantContainerImageTags.Tag"/> tag of the <inheritdoc cref="QdrantContainerImageTags.Image"/> container image.
/// </remarks>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency</param>
/// <param name="apiKey">The parameter used to provide the API Key for the Qdrant resource. If <see langword="null"/> a random key will be generated as {name}-Key.</param>
/// <param name="grpcPort">The host port of gRPC endpoint of Qdrant database.</param>
/// <param name="httpPort">The host port of HTTP endpoint of Qdrant database.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{QdrantServerResource}"/>.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<QdrantServerResource> AddQdrant(this IDistributedApplicationBuilder builder,
string name,
IResourceBuilder<ParameterResource>? apiKey = null,
int? grpcPort = null,
int? httpPort = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(name);
var apiKeyParameter = apiKey?.Resource ??
ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false);
var qdrant = new QdrantServerResource(name, apiKeyParameter);
QdrantClient? qdrantClient = null;
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(qdrant, async (@event, ct) =>
{
var connectionString = await qdrant.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false)
?? throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{qdrant.Name}' resource but the connection string was null.");
qdrantClient = CreateQdrantClient(connectionString);
});
var healthCheckKey = $"{name}_check";
builder.Services.AddHealthChecks()
.Add(new HealthCheckRegistration(
healthCheckKey,
sp => new QdrantHealthCheck(qdrantClient ?? throw new InvalidOperationException("Qdrant Client is unavailable")),
failureStatus: default,
tags: default,
timeout: default));
return builder.AddResource(qdrant)
.WithImage(QdrantContainerImageTags.Image, QdrantContainerImageTags.Tag)
.WithImageRegistry(QdrantContainerImageTags.Registry)
.WithIconName("DatabaseSearch")
.WithHttpEndpoint(port: grpcPort, targetPort: QdrantPortGrpc, name: QdrantServerResource.PrimaryEndpointName)
.WithEndpoint(QdrantServerResource.PrimaryEndpointName, endpoint =>
{
endpoint.Transport = "http2";
})
.WithHttpEndpoint(port: httpPort, targetPort: QdrantPortHttp, name: QdrantServerResource.HttpEndpointName)
.WithEnvironment(context =>
{
context.EnvironmentVariables[ApiKeyEnvVarName] = qdrant.ApiKeyParameter;
// If in Publish mode, disable static content, which disables the Dashboard Web UI
// https://github.com/qdrant/qdrant/blob/acb04d5f0d22b46a756b31c0fc507336a0451c15/src/settings.rs#L36-L40
if (builder.ExecutionContext.IsPublishMode)
{
context.EnvironmentVariables[EnableStaticContentEnvVarName] = "0";
}
})
.WithHealthCheck(healthCheckKey)
.WithUrlForEndpoint(QdrantServerResource.PrimaryEndpointName, c =>
{
c.DisplayText = "Qdrant (GRPC)";
// https://github.com/microsoft/aspire/issues/8809
c.DisplayLocation = UrlDisplayLocation.DetailsOnly;
})
.WithUrlForEndpoint(QdrantServerResource.HttpEndpointName, c => c.DisplayText = "Qdrant (HTTP)")
.WithUrlForEndpoint(QdrantServerResource.HttpEndpointName, e => new ResourceUrlAnnotation() { Url = "/dashboard", DisplayText = "Manage", DisplayOrder = 1 });
}
/// <summary>
/// Adds a named volume for the data folder to a Qdrant container resource.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="name">The name of the volume. Defaults to an auto-generated name based on the resource name.</param>
/// <param name="isReadOnly">A flag that indicates if this is a read-only volume.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<QdrantServerResource> WithDataVolume(this IResourceBuilder<QdrantServerResource> builder, string? name = null, bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/qdrant/storage",
isReadOnly);
}
/// <summary>
/// Adds a bind mount for the data folder to a Qdrant container resource.
/// </summary>
/// <param name="builder">The resource builder.</param>
/// <param name="source">The source directory on the host to mount into the container.</param>
/// <param name="isReadOnly">A flag that indicates if this is a read-only mount.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<QdrantServerResource> WithDataBindMount(this IResourceBuilder<QdrantServerResource> builder, string source, bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrEmpty(source);
return builder.WithBindMount(source, "/qdrant/storage", isReadOnly);
}
/// <summary>
/// Adds a reference to a Qdrant server to the resource.
/// </summary>
/// <remarks>
/// This overload is not available in polyglot app hosts. Use the overload that accepts an explicit connection name instead.
/// </remarks>
/// <param name="builder">The resource builder for the destination resource.</param>
/// <param name="qdrantResource">The Qdrant server resource.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExportIgnore(Reason = "Use the overload that accepts an explicit connection name when calling this API from polyglot AppHosts.")]
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<QdrantServerResource> qdrantResource)
where TDestination : IResourceWithEnvironment
{
return WithReference(builder, qdrantResource, connectionName: null);
}
/// <summary>
/// Adds a reference to a Qdrant server to the resource.
/// </summary>
/// <param name="builder">The resource builder for the destination resource.</param>
/// <param name="qdrantResource">The Qdrant server resource.</param>
/// <param name="connectionName">
/// An override of the source resource's logical connection name. Physical environment-variable names are derived from this value when it is not <see langword="null"/>,
/// unless the source resource specifies <see cref="IResourceWithConnectionString.ConnectionStringEnvironmentVariable"/>, in which case that explicit physical name is preserved
/// for the gRPC connection and used with an <c>_http</c> suffix for the HTTP connection.
/// </param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
[AspireExportIgnore(Reason = "Polyglot AppHosts use the generic withReference export.")]
public static IResourceBuilder<TDestination> WithReference<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<QdrantServerResource> qdrantResource, string? connectionName = null)
where TDestination : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(qdrantResource);
// Add standard references and environment variables
ResourceBuilderExtensions.WithReference(builder, qdrantResource, connectionName);
var resource = (IResourceWithConnectionString)qdrantResource.Resource;
connectionName ??= resource.Name;
var connectionStringNames = ConnectionStringEnvironmentVariableNames.Create(resource, connectionName);
var httpLogicalName = $"{connectionName}_{QdrantServerResource.HttpEndpointName}";
var httpConnectionStringNames = connectionStringNames.IsExplicit
? new ConnectionStringEnvironmentVariableNames(
httpLogicalName,
$"{connectionStringNames.OriginalName}_{QdrantServerResource.HttpEndpointName}",
$"{connectionStringNames.PortableName}_{QdrantServerResource.HttpEndpointName}",
isExplicit: true)
: ConnectionStringEnvironmentVariableNames.Create(resource, httpLogicalName);
var httpConnectionStringExpression = qdrantResource.Resource.HttpConnectionStringExpression;
var httpReference = new ConnectionStringReference(
resource,
optional: false,
httpConnectionStringNames,
nameof(QdrantServerResource.HttpConnectionStringExpression),
httpConnectionStringExpression);
// Determine what to inject based on the annotation on the destination resource
var injectionAnnotation = builder.Resource.TryGetLastAnnotation<ReferenceEnvironmentInjectionAnnotation>(out var annotation) ? annotation : null;
var flags = injectionAnnotation?.Flags ?? ReferenceEnvironmentInjectionFlags.All;
if (flags.HasFlag(ReferenceEnvironmentInjectionFlags.ConnectionString))
{
builder.WithEnvironment(context =>
{
ResourceBuilderExtensions.ValidateConnectionStringReference(context, httpReference);
context.EnvironmentVariables[httpConnectionStringNames.OriginalName] = httpReference;
if (!string.Equals(httpConnectionStringNames.OriginalName, httpConnectionStringNames.PortableName, StringComparison.OrdinalIgnoreCase))
{
context.EnvironmentVariables[httpConnectionStringNames.PortableName] = httpReference;
}
});
}
return builder;
}
private static QdrantClient CreateQdrantClient(string? connectionString)
{
if (connectionString is null)
{
throw new InvalidOperationException("Connection string is unavailable");
}
Uri? endpoint = null;
string? key = null;
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
{
endpoint = uri;
}
else
{
var connectionBuilder = new DbConnectionStringBuilder
{
ConnectionString = connectionString
};
if (connectionBuilder.TryGetValue("Endpoint", out var endpointValue) && Uri.TryCreate(endpointValue.ToString(), UriKind.Absolute, out var serviceUri))
{
endpoint = serviceUri;
}
if (connectionBuilder.TryGetValue("Key", out var keyValue))
{
key = keyValue.ToString();
}
}
if (endpoint is null)
{
throw new InvalidOperationException("Endpoint is unavailable");
}
var client = new QdrantClient(endpoint, key);
return client;
}
}