diff --git a/Aspire.sln b/Aspire.sln
index 8478c71c52..f92f463b3c 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -423,12 +423,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Sql",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.Storage", "src\Aspire.Hosting.Azure.Storage\Aspire.Hosting.Azure.Storage.csproj", "{89E9F2B8-662C-4FFA-8F69-360680362653}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Qdrant", "src\Aspire.Hosting.Qdrant\Aspire.Hosting.Qdrant.csproj", "{D3CDBA75-7707-4884-908D-1BA22B8DF8E7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "qdrant", "qdrant", "{A4800EE3-F902-4B7B-AF53-01A85514E6B9}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qdrant.AppHost", "playground\Qdrant\Qdrant.AppHost\Qdrant.AppHost.csproj", "{F43586B8-FE36-490D-9EFA-82CFFB83A304}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Qdrant.ApiService", "playground\Qdrant\Qdrant.ApiService\Qdrant.ApiService.csproj", "{6B6D3953-E961-4720-B27E-9466A69BED1A}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.EventHubs", "src\Aspire.Hosting.Azure.EventHubs\Aspire.Hosting.Azure.EventHubs.csproj", "{2580B014-E7FE-48D9-BE40-E90604365F0E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Tests.SharedShim", "tests\Aspire.Hosting.Tests.SharedShim\Aspire.Hosting.Tests.SharedShim.csproj", "{74644A4D-8F61-4314-B6E8-5CE3802CD6C2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.KeyVault", "src\Aspire.Hosting.Azure.KeyVault\Aspire.Hosting.Azure.KeyVault.csproj", "{427F4D7C-8969-4015-AD1A-5EFFE921A184}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Qdrant.Client", "src\Components\Aspire.Qdrant.Client\Aspire.Qdrant.Client.csproj", "{E0E1B557-D3CF-4446-B993-E5CE719234FB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Qdrant.Client.Tests", "tests\Aspire.Qdrant.Client.Tests\Aspire.Qdrant.Client.Tests.csproj", "{A9CFA376-0C90-4231-9152-FBF14065195A}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Azure.Messaging.EventHubs.Tests", "tests\Aspire.Azure.Messaging.EventHubs.Tests\Aspire.Azure.Messaging.EventHubs.Tests.csproj", "{8191109E-130C-47F3-B84E-82070A6CD269}"
EndProject
Global
@@ -437,6 +449,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A9CFA376-0C90-4231-9152-FBF14065195A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9CFA376-0C90-4231-9152-FBF14065195A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9CFA376-0C90-4231-9152-FBF14065195A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9CFA376-0C90-4231-9152-FBF14065195A}.Release|Any CPU.Build.0 = Release|Any CPU
{B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1113,6 +1129,18 @@ Global
{89E9F2B8-662C-4FFA-8F69-360680362653}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89E9F2B8-662C-4FFA-8F69-360680362653}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89E9F2B8-662C-4FFA-8F69-360680362653}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D3CDBA75-7707-4884-908D-1BA22B8DF8E7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F43586B8-FE36-490D-9EFA-82CFFB83A304}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6B6D3953-E961-4720-B27E-9466A69BED1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6B6D3953-E961-4720-B27E-9466A69BED1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6B6D3953-E961-4720-B27E-9466A69BED1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6B6D3953-E961-4720-B27E-9466A69BED1A}.Release|Any CPU.Build.0 = Release|Any CPU
{2580B014-E7FE-48D9-BE40-E90604365F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2580B014-E7FE-48D9-BE40-E90604365F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2580B014-E7FE-48D9-BE40-E90604365F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1125,6 +1153,10 @@ Global
{427F4D7C-8969-4015-AD1A-5EFFE921A184}.Debug|Any CPU.Build.0 = Debug|Any CPU
{427F4D7C-8969-4015-AD1A-5EFFE921A184}.Release|Any CPU.ActiveCfg = Release|Any CPU
{427F4D7C-8969-4015-AD1A-5EFFE921A184}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E0E1B557-D3CF-4446-B993-E5CE719234FB}.Release|Any CPU.Build.0 = Release|Any CPU
{8191109E-130C-47F3-B84E-82070A6CD269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8191109E-130C-47F3-B84E-82070A6CD269}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8191109E-130C-47F3-B84E-82070A6CD269}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1134,6 +1166,7 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
+ {A9CFA376-0C90-4231-9152-FBF14065195A} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{B52DCF1A-465D-4E92-A68A-0EE1A9ED49DF} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
{E958BE04-81C2-434C-9E6C-CA145A2B8218} = {A68BA1A5-1604-433D-9778-DC0199831C2A}
{C1D595AD-FFFD-4E52-AAF6-8DD8C4BD67F1} = {A68BA1A5-1604-433D-9778-DC0199831C2A}
@@ -1330,9 +1363,14 @@ Global
{CB7CAE39-F041-4B20-A0C4-D6F44920A647} = {77CFE74A-32EE-400C-8930-5025E8555256}
{9FF853BD-FA56-4DA5-A50A-9867F2FAB1F0} = {77CFE74A-32EE-400C-8930-5025E8555256}
{89E9F2B8-662C-4FFA-8F69-360680362653} = {77CFE74A-32EE-400C-8930-5025E8555256}
+ {D3CDBA75-7707-4884-908D-1BA22B8DF8E7} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
+ {A4800EE3-F902-4B7B-AF53-01A85514E6B9} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {F43586B8-FE36-490D-9EFA-82CFFB83A304} = {A4800EE3-F902-4B7B-AF53-01A85514E6B9}
+ {6B6D3953-E961-4720-B27E-9466A69BED1A} = {A4800EE3-F902-4B7B-AF53-01A85514E6B9}
{2580B014-E7FE-48D9-BE40-E90604365F0E} = {77CFE74A-32EE-400C-8930-5025E8555256}
{74644A4D-8F61-4314-B6E8-5CE3802CD6C2} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{427F4D7C-8969-4015-AD1A-5EFFE921A184} = {77CFE74A-32EE-400C-8930-5025E8555256}
+ {E0E1B557-D3CF-4446-B993-E5CE719234FB} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{8191109E-130C-47F3-B84E-82070A6CD269} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 27660f7e6b..383c504e5e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -97,6 +97,7 @@
+
diff --git a/playground/Qdrant/Qdrant.ApiService/Program.cs b/playground/Qdrant/Qdrant.ApiService/Program.cs
new file mode 100644
index 0000000000..589448e7a4
--- /dev/null
+++ b/playground/Qdrant/Qdrant.ApiService/Program.cs
@@ -0,0 +1,93 @@
+using Qdrant.Client;
+using Qdrant.Client.Grpc;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add service defaults & Aspire components.
+builder.AddServiceDefaults();
+
+// Add services to the container.
+builder.Services.AddProblemDetails();
+
+builder.AddQdrantClient("qdrant");
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.UseExceptionHandler();
+
+app.MapGet("/create", async (QdrantClient client, ILogger logger) =>
+{
+ var collections = await client.ListCollectionsAsync();
+ if (collections.Any(x => x.Contains("movie_collection")))
+ {
+ await client.DeleteCollectionAsync("movie_collection");
+ }
+
+ await client.CreateCollectionAsync("movie_collection", new VectorParams { Size = 2, Distance = Distance.Cosine });
+ var collectionInfo = await client.GetCollectionInfoAsync("movie_collection");
+ logger.LogInformation(collectionInfo.ToString());
+
+ // generate some vectors
+ var data = new[]
+ {
+ new PointStruct
+ {
+ Id = 1,
+ Vectors = new [] {0.10022575f, -0.23998135f},
+ Payload =
+ {
+ ["title"] = "The Lion King"
+ }
+ },
+ new PointStruct
+ {
+ Id = 2,
+ Vectors = new [] {0.10327095f, 0.2563685f},
+ Payload =
+ {
+ ["title"] = "Inception"
+ }
+ },
+ new PointStruct
+ {
+ Id = 3,
+ Vectors = new [] {0.095857024f, -0.201278f},
+ Payload =
+ {
+ ["title"] = "Toy Story"
+ }
+ },
+ new PointStruct
+ {
+ Id = 4,
+ Vectors = new [] {0.106827796f, 0.21676421f},
+ Payload =
+ {
+ ["title"] = "Pulp Function"
+ }
+ },
+ new PointStruct
+ {
+ Id = 5,
+ Vectors = new [] {0.09568083f, -0.21177962f},
+ Payload =
+ {
+ ["title"] = "Shrek"
+ }
+ },
+ };
+ var updateResult = await client.UpsertAsync("movie_collection", data);
+
+ return updateResult.Status;
+});
+
+app.MapGet("/search", async (QdrantClient client) =>
+{
+ var results = await client.SearchAsync("movie_collection", new[] { 0.12217915f, -0.034832448f }, limit: 3);
+ return results.Select(titles => titles.Payload["title"].StringValue);
+});
+
+app.MapDefaultEndpoints();
+
+app.Run();
diff --git a/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json b/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json
new file mode 100644
index 0000000000..a728d35665
--- /dev/null
+++ b/playground/Qdrant/Qdrant.ApiService/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "",
+ "applicationUrl": "https://localhost:5450;http://localhost:5451",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "",
+ "applicationUrl": "http://localhost:5451",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj b/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj
new file mode 100644
index 0000000000..ffffb8fc49
--- /dev/null
+++ b/playground/Qdrant/Qdrant.ApiService/Qdrant.ApiService.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+ 1e49caab-af46-4c24-8011-953ec12b4069
+
+
+
+
+
+
+
+
diff --git a/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json b/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/Qdrant/Qdrant.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/Qdrant/Qdrant.ApiService/appsettings.json b/playground/Qdrant/Qdrant.ApiService/appsettings.json
new file mode 100644
index 0000000000..1e74af8c8a
--- /dev/null
+++ b/playground/Qdrant/Qdrant.ApiService/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Qdrant.Client": "Debug"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/Qdrant/Qdrant.AppHost/Directory.Build.props b/playground/Qdrant/Qdrant.AppHost/Directory.Build.props
new file mode 100644
index 0000000000..d9b2c324ac
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets b/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets
new file mode 100644
index 0000000000..29db89f209
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/playground/Qdrant/Qdrant.AppHost/Program.cs b/playground/Qdrant/Qdrant.AppHost/Program.cs
new file mode 100644
index 0000000000..d7390fb4e2
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/Program.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var qdrant = builder.AddQdrant("qdrant")
+ .WithDataVolume("qdrant_data");
+
+builder.AddProject("apiservice")
+ .WithReference(qdrant);
+
+builder.Build().Run();
diff --git a/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json b/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000000..e75f3d87e9
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/Properties/launchSettings.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:15206;http://localhost:15207",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16022",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17038",
+ "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15207",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16022",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17039",
+ "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true",
+ "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
+ }
+ }
+ }
+}
diff --git a/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj b/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj
new file mode 100644
index 0000000000..bf89e16e61
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/Qdrant.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 10c36641-05e0-4bfb-ad9d-a588431430f0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json b/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/Qdrant/Qdrant.AppHost/appsettings.json b/playground/Qdrant/Qdrant.AppHost/appsettings.json
new file mode 100644
index 0000000000..31c092aa45
--- /dev/null
+++ b/playground/Qdrant/Qdrant.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/spelling.dic b/spelling.dic
index fa2eff6f45..af6b38c5a5 100644
--- a/spelling.dic
+++ b/spelling.dic
@@ -45,6 +45,7 @@ pgadmin
postgre
postgres
protoc
+qdrant
rabbitmq
redis
rediscommander
diff --git a/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj
new file mode 100644
index 0000000000..4b29d0c93d
--- /dev/null
+++ b/src/Aspire.Hosting.Qdrant/Aspire.Hosting.Qdrant.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(NetCurrent)
+ true
+ aspire hosting qdrant
+ Qdrant vector database support for .NET Aspire.
+ $(SharedDir)QdrantLogo_256x.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs
new file mode 100644
index 0000000000..f2ca4ad989
--- /dev/null
+++ b/src/Aspire.Hosting.Qdrant/QdrantBuilderExtensions.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Qdrant;
+using Aspire.Hosting.Utils;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Qdrant resources to the application model.
+///
+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";
+
+ ///
+ /// Adds a Qdrant resource to the application. A container is used for local development.
+ ///
+ ///
+ /// This version the package defaults to the v1.8.3 tag of the qdrant/qdrant container image.
+ /// The .NET client library uses the gRPC port by default to communicate and this resource exposes that endpoint.
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency
+ /// The parameter used to provide the API Key for the Qdrant resource. If a random key will be generated as {name}-Key.
+ /// The host port of Qdrant database.
+ /// A reference to the .
+ public static IResourceBuilder AddQdrant(this IDistributedApplicationBuilder builder,
+ string name,
+ IResourceBuilder? apiKey = null,
+ int? port = null)
+ {
+ var apiKeyParameter = apiKey?.Resource ??
+ ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-Key", special: false);
+ var qdrant = new QdrantServerResource(name, apiKeyParameter);
+ return builder.AddResource(qdrant)
+ .WithImage(QdrantContainerImageTags.Image, QdrantContainerImageTags.Tag)
+ .WithHttpEndpoint(port: port, targetPort: QdrantPortGrpc, name: QdrantServerResource.PrimaryEndpointName)
+ .WithHttpEndpoint(port: port, targetPort: QdrantPortHttp, name: QdrantServerResource.RestEndpointName)
+ .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";
+ }
+ });
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a Qdrant container resource.
+ ///
+ /// The resource builder.
+ /// The name of the volume. Defaults to an auto-generated name based on the resource name.
+ /// A flag that indicates if this is a read-only volume.
+ /// The .
+ public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null, bool isReadOnly = false)
+ => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/qdrant/storage", isReadOnly);
+
+ ///
+ /// Adds a bind mount for the data folder to a Qdrant container resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// A flag that indicates if this is a read-only mount.
+ /// The .
+ public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, bool isReadOnly = false)
+ => builder.WithBindMount(source, "/qdrant/storage", isReadOnly);
+
+ ///
+ /// Add a reference to a Qdrant settings for a project.
+ ///
+ /// An for
+ /// The Qdrant server resource
+ ///
+ public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder qdrantResource)
+ {
+ builder.WithEnvironment(context =>
+ {
+ // primary endpoint (gRPC)
+ context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}"] = qdrantResource.Resource.ConnectionStringExpression;
+
+ // REST endpoint
+ context.EnvironmentVariables[$"ConnectionStrings__{qdrantResource.Resource.Name}_{QdrantServerResource.RestEndpointName}"] = qdrantResource.Resource.RestConnectionStringExpression;
+ });
+
+ return builder;
+ }
+}
diff --git a/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs b/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs
new file mode 100644
index 0000000000..9c598c5b7f
--- /dev/null
+++ b/src/Aspire.Hosting.Qdrant/QdrantContainerImageTags.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.Qdrant;
+
+internal static class QdrantContainerImageTags
+{
+ public const string Image = "qdrant/qdrant";
+ public const string Tag = "v1.8.3";
+}
diff --git a/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs b/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs
new file mode 100644
index 0000000000..fda54134a0
--- /dev/null
+++ b/src/Aspire.Hosting.Qdrant/QdrantServerResource.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a Qdrant database.
+///
+public class QdrantServerResource : ContainerResource, IResourceWithConnectionString
+{
+ internal const string PrimaryEndpointName = "http";
+ internal const string RestEndpointName = "rest";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the resource.
+ /// A that contains the API Key
+ public QdrantServerResource(string name, ParameterResource apiKey) : base(name)
+ {
+ ArgumentNullException.ThrowIfNull(apiKey);
+ ApiKeyParameter = apiKey;
+ }
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the parameter that contains the Qdrant API key.
+ ///
+ public ParameterResource ApiKeyParameter { get; }
+
+ ///
+ /// Gets the primary endpoint for the Qdrant database.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the connection string expression for the Qdrant gRPC endpoint.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create(
+ $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)};Key={ApiKeyParameter}");
+
+ ///
+ /// Gets the connection string expression for the Qdrant REST endpoint.
+ ///
+ public ReferenceExpression RestConnectionStringExpression =>
+ ReferenceExpression.Create(
+ $"Endpoint={PrimaryEndpoint.Property(EndpointProperty.Scheme)}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:6333;Key={ApiKeyParameter}");
+}
diff --git a/src/Aspire.Hosting.Qdrant/README.md b/src/Aspire.Hosting.Qdrant/README.md
new file mode 100644
index 0000000000..a25ff6dc2b
--- /dev/null
+++ b/src/Aspire.Hosting.Qdrant/README.md
@@ -0,0 +1,34 @@
+# Aspire.Hosting.Qdrant library
+
+Provides extension methods and resource definitions for a .NET Aspire AppHost to configure a Qdrant vector database resource.
+
+## Getting started
+
+### Install the package
+
+In your AppHost project, install the .NET Aspire Qdrant Hosting library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Hosting.Qdrant
+```
+
+## Usage example
+
+Then, in the _Program.cs_ file of `AppHost`, add a Qdrant resource and consume the connection using the following methods:
+
+```csharp
+var qdrant = builder.AddQdrant("qdrant");
+
+var myService = builder.AddProject()
+ .WithReference(qdrant);
+```
+
+## Additional documentation
+* https://qdrant.tech/documentation
+* https://learn.microsoft.com/azure/cosmos-db/nosql/sdk-dotnet-v3
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
+
+_Qdrant, and the Qdrant logo are trademarks or registered trademarks of Qdrant Solutions GmbH of Germany, and used with their permission._
diff --git a/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj
new file mode 100644
index 0000000000..9edc30e8d3
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/Aspire.Qdrant.Client.csproj
@@ -0,0 +1,21 @@
+
+
+
+ $(NetCurrent)
+ true
+ $(ComponentCommonPackageTags) qdrant
+ A Qdrant client that integrates with Aspire, including logging.
+ $(SharedDir)QdrantLogo_256x.png
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs
new file mode 100644
index 0000000000..e22929edaa
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Qdrant.Client;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Qdrant.Client;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for registering Qdrant-related services in an .
+///
+public static class AspireQdrantExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Qdrant:Client";
+
+ ///
+ /// Registers as a singleton in the services provided by the .
+ /// Configures logging for the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:Qdrant:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddQdrantClient(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null)
+ {
+ AddQdrant(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+ }
+
+ ///
+ /// Registers as a keyed singleton for the given in the services provided by the .
+ /// Configures logging for the .
+ ///
+ /// The to read config from and add services to.
+ /// The connection name to use to find a connection string.
+ /// An optional method that can be used for customizing the . It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:Qdrant:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddKeyedQdrantClient(
+ this IHostApplicationBuilder builder,
+ string name,
+ Action? configureSettings = null)
+ {
+ AddQdrant(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddQdrant(
+ this IHostApplicationBuilder builder,
+ string configurationSectionName,
+ Action? configureSettings,
+ string connectionName,
+ string? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var settings = new QdrantClientSettings();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ParseConnectionString(connectionString);
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (serviceKey is null)
+ {
+ builder.Services.AddSingleton(ConfigureQdrant);
+ }
+ else
+ {
+ builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureQdrant(sp));
+ }
+
+ QdrantClient ConfigureQdrant(IServiceProvider serviceProvider)
+ {
+ if (settings.Endpoint is not null)
+ {
+ return new QdrantClient(settings.Endpoint, apiKey: settings.Key, loggerFactory: serviceProvider.GetRequiredService());
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"A QdrantClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " +
+ $"{nameof(settings.Endpoint)} must be provided " +
+ $"in the '{configurationSectionName}' configuration section.");
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs b/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs
new file mode 100644
index 0000000000..914347a4b9
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.Qdrant.Client;
+
+[assembly: ConfigurationSchema("Aspire:Qdrant:Client", typeof(QdrantClientSettings))]
+
+[assembly: LoggingCategories("Qdrant.Client")]
diff --git a/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json
new file mode 100644
index 0000000000..a0b7d5db3d
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/ConfigurationSchema.json
@@ -0,0 +1,39 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "Qdrant.Client": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ },
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "Qdrant": {
+ "type": "object",
+ "properties": {
+ "Client": {
+ "type": "object",
+ "properties": {
+ "Endpoint": {
+ "type": "string",
+ "format": "uri",
+ "description": "The endpoint URI string of the Qdrant server to connect to."
+ },
+ "Key": {
+ "type": "string",
+ "description": "The API Key of the Qdrant server to connect to."
+ }
+ },
+ "description": "Provides the client configuration settings for connecting to a Qdrant server using QdrantClient."
+ }
+ }
+ }
+ }
+ }
+ },
+ "type": "object"
+}
diff --git a/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs
new file mode 100644
index 0000000000..5db43f6549
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs
@@ -0,0 +1,50 @@
+// 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;
+
+namespace Aspire.Qdrant.Client;
+
+///
+/// Provides the client configuration settings for connecting to a Qdrant server using QdrantClient.
+///
+public sealed class QdrantClientSettings
+{
+ private const string ConnectionStringEndpoint = "Endpoint";
+ private const string ConnectionStringKey = "Key";
+
+ ///
+ /// The endpoint URI string of the Qdrant server to connect to.
+ ///
+ public Uri? Endpoint { get; set; }
+
+ ///
+ /// The API Key of the Qdrant server to connect to.
+ ///
+ public string? Key { get; set; }
+
+ internal void ParseConnectionString(string? connectionString)
+ {
+ if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
+ {
+ Endpoint = uri;
+ }
+ else
+ {
+ var connectionBuilder = new DbConnectionStringBuilder
+ {
+ ConnectionString = connectionString
+ };
+
+ if (connectionBuilder.ContainsKey(ConnectionStringEndpoint) && Uri.TryCreate(connectionBuilder[ConnectionStringEndpoint].ToString(), UriKind.Absolute, out var serviceUri))
+ {
+ Endpoint = serviceUri;
+ }
+
+ if (connectionBuilder.ContainsKey(ConnectionStringKey))
+ {
+ Key = connectionBuilder[ConnectionStringKey].ToString();
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Qdrant.Client/README.md b/src/Components/Aspire.Qdrant.Client/README.md
new file mode 100644
index 0000000000..c956e2ec4b
--- /dev/null
+++ b/src/Components/Aspire.Qdrant.Client/README.md
@@ -0,0 +1,107 @@
+# Aspire.Qdrant.Client library
+
+Registers a [QdrantClient](https://github.com/qdrant/qdrant-dotnet) in the DI container for connecting to a Qdrant server.
+
+## Getting started
+
+### Prerequisites
+
+- Qdrant server and connection string for accessing the server API endpoint.
+
+### Install the package
+
+Install the .NET Aspire Qdrant Client library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Qdrant.Client
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddQdrantClient` extension method to register a `QdrantClient` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddQdrantClient("qdrant");
+```
+
+## Configuration
+
+The .NET Aspire Qdrant Client component provides multiple options to configure the server connection based on the requirements and conventions of your project.
+
+### Use a connection string
+
+When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddQdrantClient()`:
+
+```csharp
+builder.AddQdrantClient("qdrant");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "qdrant": "Endpoint=http://localhost:6334;Key=123456!@#$%"
+ }
+}
+```
+
+By default the `QdrantClient` uses the gRPC API endpoint.
+
+### Use configuration providers
+
+The .NET Aspire Qdrant Client component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `QdrantSettings` from configuration by using the `Aspire:Qdrant:Client` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "Qdrant": {
+ "Client": {
+ "Key": "123456!@#$%"
+ }
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to set the API key from code:
+
+```csharp
+builder.AddQdrantClient("qdrant", settings => settings.ApiKey = "12345!@#$%");
+```
+
+## AppHost extensions
+
+In your AppHost project, install the `Aspire.Hosting.Qdrant` library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Hosting.Qdrant
+```
+
+Then, in the _Program.cs_ file of `AppHost`, register a Qdrant server and consume the connection using the following methods:
+
+```csharp
+var qdrant = builder.AddQdrant("qdrant");
+
+var myService = builder.AddProject()
+ .WithReference(qdrant);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `qdrant`. In the _Program.cs_ file of `MyService`, the Qdrant connection can be consumed using:
+
+```csharp
+builder.AddQdrantClient("qdrant");
+```
+
+## Additional documentation
+
+* https://github.com/qdrant/qdrant-dotnet
+* https://github.com/dotnet/aspire/tree/main/src/Components/README.md
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
+
+_Qdrant, and the Qdrant logo are trademarks or registered trademarks of Qdrant Solutions GmbH of Germany, and used with their permission._
diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md
index 69ea259cef..0fe3966191 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -29,6 +29,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As
| Pomelo.EntityFrameworkCore.MySql | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| NATS.Net | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ |
| Seq | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | N/A | ✅ |
+| Qdrant.Client | ✅ | ✅ | ✅ | ✅ | ✅ | | | |
Nomenclature used in the table above:
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index 3fea50c74f..ea137b68d6 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -274,6 +274,14 @@ Aspire.Pomelo.EntityFrameworkCore.MySql:
- "db.client.connections.timeouts"
- "db.client.connections.usage"
+Aspire.Qdrant.Client:
+- Log categories:
+ "Qdrant.Client"
+- Activity source names:
+ - none (not currently supported by Qdrant.Client library)
+- Metric names:
+ - none (currently not supported by Qdrant.Client library)
+
Aspire.RabbitMQ.Client:
- Log categories:
- "RabbitMQ.Client"
diff --git a/src/Shared/QdrantLogo_256x.png b/src/Shared/QdrantLogo_256x.png
new file mode 100644
index 0000000000..d49afd6f3b
Binary files /dev/null and b/src/Shared/QdrantLogo_256x.png differ
diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
index ef1d696493..69256ba50d 100644
--- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
+++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
@@ -32,6 +32,7 @@
+
diff --git a/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs b/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs
new file mode 100644
index 0000000000..0e6cc2109a
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Qdrant/AddQdrantTests.cs
@@ -0,0 +1,255 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Sockets;
+using Aspire.Hosting.Qdrant;
+using Aspire.Hosting.Tests.Utils;
+using Aspire.Hosting.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Aspire.Hosting.Tests.Qdrant;
+
+public class AddQdrantTests
+{
+ private const int QdrantPortHttp = 6334;
+ private const int QdrantPortDashboard = 6333;
+
+ [Fact]
+ public async Task AddQdrantWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.AddQdrant("my-qdrant");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.GetContainerResources());
+ Assert.Equal("my-qdrant", containerResource.Name);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Null(containerAnnotation.Registry);
+
+ var endpoint = containerResource.Annotations.OfType()
+ .FirstOrDefault(e => e.Name == "http");
+ Assert.NotNull(endpoint);
+ Assert.Equal(QdrantPortHttp, endpoint.TargetPort);
+ Assert.False(endpoint.IsExternal);
+ Assert.Equal("http", endpoint.Name);
+ Assert.Null(endpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
+ Assert.Equal("http", endpoint.Transport);
+ Assert.Equal("http", endpoint.UriScheme);
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource);
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("QDRANT__SERVICE__API_KEY", env.Key);
+ Assert.False(string.IsNullOrEmpty(env.Value));
+ });
+ }
+
+ [Fact]
+ public void AddQdrantWithDefaultsAndDashboardAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.AddQdrant("my-qdrant");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.GetContainerResources());
+ Assert.Equal("my-qdrant", containerResource.Name);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Null(containerAnnotation.Registry);
+
+ var endpoint = containerResource.Annotations.OfType()
+ .FirstOrDefault(e => e.Name == "rest");
+
+ Assert.NotNull(endpoint);
+ Assert.Equal(QdrantPortDashboard, endpoint.TargetPort);
+ Assert.False(endpoint.IsExternal);
+ Assert.Equal("rest", endpoint.Name);
+ Assert.Null(endpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
+ Assert.Equal("http", endpoint.Transport);
+ Assert.Equal("http", endpoint.UriScheme);
+ }
+
+ [Fact]
+ public async Task AddQdrantAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.Configuration["Parameters:pass"] = "pass";
+
+ var pass = appBuilder.AddParameter("pass");
+ appBuilder.AddQdrant("my-qdrant", apiKey: pass);
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.GetContainerResources());
+ Assert.Equal("my-qdrant", containerResource.Name);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(QdrantContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(QdrantContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Null(containerAnnotation.Registry);
+
+ var endpoint = containerResource.Annotations.OfType()
+ .FirstOrDefault(e => e.Name == "http");
+ Assert.NotNull(endpoint);
+ Assert.Equal(QdrantPortHttp, endpoint.TargetPort);
+ Assert.False(endpoint.IsExternal);
+ Assert.Equal("http", endpoint.Name);
+ Assert.Null(endpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
+ Assert.Equal("http", endpoint.Transport);
+ Assert.Equal("http", endpoint.UriScheme);
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource);
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("QDRANT__SERVICE__API_KEY", env.Key);
+ Assert.Equal("pass", env.Value);
+ });
+ }
+
+ [Fact]
+ public async Task QdrantCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ appBuilder.Configuration["Parameters:pass"] = "pass";
+ var pass = appBuilder.AddParameter("pass");
+
+ var qdrant = appBuilder.AddQdrant("my-qdrant", pass)
+ .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334));
+
+ var connectionStringResource = qdrant.Resource as IResourceWithConnectionString;
+
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+ Assert.Equal($"Endpoint=http://localhost:6334;Key=pass", connectionString);
+ }
+
+ [Fact]
+ public async Task QdrantClientAppWithReferenceContainsConnectionStrings()
+ {
+ using var testProgram = CreateTestProgram();
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ appBuilder.Configuration["Parameters:pass"] = "pass";
+ var pass = appBuilder.AddParameter("pass");
+
+ var qdrant = appBuilder.AddQdrant("my-qdrant", pass)
+ .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334))
+ .WithEndpoint("rest", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333));
+
+ var projectA = appBuilder.AddProject("projecta")
+ .WithReference(qdrant);
+
+ // Call environment variable callbacks.
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource);
+
+ var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
+ Assert.Equal(2, servicesKeysCount);
+
+ Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant" && kvp.Value == "Endpoint=http://localhost:6334;Key=pass");
+ Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__my-qdrant_rest" && kvp.Value == "Endpoint=http://localhost:6333;Key=pass");
+ }
+
+ [Fact]
+ public async Task VerifyManifest()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions() { Args = new string[] { "--publisher", "manifest" } } );
+ var qdrant = appBuilder.AddQdrant("qdrant");
+
+ var serverManifest = await ManifestUtils.GetManifest(qdrant.Resource); // using this method does not get any ExecutionContext.IsPublishMode changes
+
+ var expectedManifest = $$"""
+ {
+ "type": "container.v0",
+ "connectionString": "Endpoint={qdrant.bindings.http.scheme}://{qdrant.bindings.http.host}:{qdrant.bindings.http.port};Key={qdrant-Key.value}",
+ "image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}",
+ "env": {
+ "QDRANT__SERVICE__API_KEY": "{qdrant-Key.value}",
+ "QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 6334
+ },
+ "rest": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 6333
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, serverManifest.ToString());
+ }
+
+ [Fact]
+ public async Task VerifyManifestWithParameters()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions() { Args = new string[] { "--publisher", "manifest" } });
+
+ var apiKeyParameter = appBuilder.AddParameter("QdrantApiKey");
+ var qdrant = appBuilder.AddQdrant("qdrant", apiKeyParameter);
+
+ var serverManifest = await ManifestUtils.GetManifest(qdrant.Resource); // using this method does not get any ExecutionContext.IsPublishMode changes
+
+ var expectedManifest = $$"""
+ {
+ "type": "container.v0",
+ "connectionString": "Endpoint={qdrant.bindings.http.scheme}://{qdrant.bindings.http.host}:{qdrant.bindings.http.port};Key={QdrantApiKey.value}",
+ "image": "{{QdrantContainerImageTags.Image}}:{{QdrantContainerImageTags.Tag}}",
+ "env": {
+ "QDRANT__SERVICE__API_KEY": "{QdrantApiKey.value}",
+ "QDRANT__SERVICE__ENABLE_STATIC_CONTENT": "0"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 6334
+ },
+ "rest": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 6333
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, serverManifest.ToString());
+ }
+
+ private static TestProgram CreateTestProgram(string[]? args = null) => TestProgram.Create(args);
+
+ private sealed class ProjectA : IProjectMetadata
+ {
+ public string ProjectPath => "projectA";
+
+ public LaunchSettings LaunchSettings { get; } = new();
+ }
+}
diff --git a/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj
new file mode 100644
index 0000000000..43aeb02ea8
--- /dev/null
+++ b/tests/Aspire.Qdrant.Client.Tests/Aspire.Qdrant.Client.Tests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ $(NetCurrent)
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs
new file mode 100644
index 0000000000..40c3e18400
--- /dev/null
+++ b/tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.XUnitExtensions;
+using Qdrant.Client;
+
+namespace Aspire.Qdrant.Client.Tests;
+public static class AspireQdrantHelpers
+{
+ public const string TestingEndpoint = "http://localhost:6334";
+
+ private static readonly Lazy s_canConnectToServer = new(GetCanConnect);
+ public static bool CanConnectToServer => s_canConnectToServer.Value;
+
+ public static void SkipIfCanNotConnectToServer()
+ {
+ if (!CanConnectToServer)
+ {
+ throw new SkipTestException("Unable to connect to the server.");
+ }
+ }
+
+ private static bool GetCanConnect()
+ {
+ try
+ {
+ var client = new QdrantClient(new Uri(TestingEndpoint));
+ client.ListCollectionsAsync().Wait();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex.Message);
+ return false;
+ }
+ }
+}
diff --git a/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs
new file mode 100644
index 0000000000..5761b2f0fb
--- /dev/null
+++ b/tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Aspire.Qdrant.Client.Tests;
+public class ConfigurationTests
+{
+ [Fact]
+ public void EndpointIsNullByDefault()
+ => Assert.Null(new QdrantClientSettings().Endpoint);
+}
diff --git a/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs
new file mode 100644
index 0000000000..d75c8366c1
--- /dev/null
+++ b/tests/Aspire.Qdrant.Client.Tests/ConformanceTests.cs
@@ -0,0 +1,71 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.ConformanceTests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Qdrant.Client;
+
+namespace Aspire.Qdrant.Client.Tests;
+public class ConformanceTests : ConformanceTests
+{
+ protected override bool SupportsKeyedRegistrations => true;
+
+ protected override bool CanCreateClientWithoutConnectingToServer => false;
+
+ protected override bool CanConnectToServer => AspireQdrantHelpers.CanConnectToServer;
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string[] RequiredLogCategories => Array.Empty();
+
+ protected override string ActivitySourceName => "";
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddQdrantClient("qdrant", configure);
+ }
+ else
+ {
+ builder.AddKeyedQdrantClient(key, configure);
+ }
+ }
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ => configuration.AddInMemoryCollection(new KeyValuePair[2]
+ {
+ new KeyValuePair(CreateConfigKey("Aspire:Qdrant:Client", key, "Endpoint"), "http://localhost:6334"),
+ new KeyValuePair($"ConnectionStrings:{key}","Endpoint=http://localhost:6334;Key=pass")
+ });
+
+ protected override void TriggerActivity(QdrantClient service)
+ {
+ }
+
+ protected override void SetHealthCheck(QdrantClientSettings options, bool enabled) => throw new NotImplementedException();
+
+ protected override void SetTracing(QdrantClientSettings options, bool enabled) => throw new NotImplementedException();
+
+ protected override void SetMetrics(QdrantClientSettings options, bool enabled) => throw new NotImplementedException();
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Qdrant": {
+ "Client": {
+ "Endpoint": "http://localhost:6334"
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": 3 }}}}""", "Value is \"integer\" but should be \"string\""),
+ ("""{"Aspire": { "Qdrant":{ "Client": { "Endpoint": "hello" }}}}""", "Value does not match format \"uri\"")
+ };
+}
diff --git a/tests/Aspire.Qdrant.Client.Tests/README.md b/tests/Aspire.Qdrant.Client.Tests/README.md
new file mode 100644
index 0000000000..af9e6386a6
--- /dev/null
+++ b/tests/Aspire.Qdrant.Client.Tests/README.md
@@ -0,0 +1,13 @@
+# Aspire.Qdrant.Client.Tests
+
+This project contains tests for the Aspire.Qdrant.Client project.
+
+When running tests locally until TestContainers support is enabled, you will need to have a Qdrant instance running locally.
+
+Run:
+
+```bash
+docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant:v1.8.0
+```
+
+Then run the tests to enable the connected tests.