diff --git a/Aspire.sln b/Aspire.sln
index 597d40bc70..46422771a7 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -1,4 +1,3 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -492,16 +491,32 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Azure.WebPub
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Valkey", "src\Aspire.Hosting.Valkey\Aspire.Hosting.Valkey.csproj", "{5CB63205-24F4-4388-A41B-BAF3BEA59866}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "withdockerfile", "withdockerfile", "{A6813855-E322-41EF-B2E6-7A44949EF962}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Elasticsearch", "src\Aspire.Hosting.Elasticsearch\Aspire.Hosting.Elasticsearch.csproj", "{9357EC71-823B-433A-9993-B7CB2FA082D1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Elastic.Clients.Elasticsearch", "src\Components\Aspire.Elastic.Clients.Elasticsearch\Aspire.Elastic.Clients.Elasticsearch.csproj", "{3F7B206E-5457-458F-AA81-9449FA3C1B5C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elasticsearch", "Elasticsearch", "{6C71A90C-30AE-45D7-9347-D66F9B257CBE}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elasticsearch.ApiService", "playground\Elasticsearch\Elasticsearch.ApiService\Elasticsearch.ApiService.csproj", "{F7F57331-5DDD-444A-A620-8639FC9362B2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elasticsearch.AppHost", "playground\Elasticsearch\Elasticsearch.AppHost\Elasticsearch.AppHost.csproj", "{569B8215-BDB1-445D-A7BA-DB255700A4AC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dockerfile", "dockerfile", "{A6813855-E322-41EF-B2E6-7A44949EF962}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "withdockerfile", "withdockerfile", "{5B5DC91B-5754-4CE5-B3C0-FA1584B916A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WithDockerfile.AppHost", "playground\withdockerfile\WithDockerfile.AppHost\WithDockerfile.AppHost.csproj", "{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Containers.Tests", "tests\Aspire.Hosting.Containers.Tests\Aspire.Hosting.Containers.Tests.csproj", "{588CD2D7-EE70-43C1-8233-330854BDF53C}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Elastic.Clients.Elasticsearch.Tests", "tests\Aspire.Elastic.Clients.Elasticsearch.Tests\Aspire.Elastic.Clients.Elasticsearch.Tests.csproj", "{D5B392A4-29CD-41F9-8847-0C211C832713}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "python", "python", "{7123AB7A-A4FD-4F64-8B05-D2DD0C3E2ABC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.AppHost", "playground\python\Python.AppHost\Python.AppHost.csproj", "{173BDA6E-F175-4457-BF64-58CD184E9A81}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Elasticsearch.Tests", "tests\Aspire.Hosting.Elasticsearch.Tests\Aspire.Hosting.Elasticsearch.Tests.csproj", "{62D8C73C-DAB3-4B9E-A508-34C886C374F9}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{C424395C-1235-41A4-BF55-07880A04368C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosting", "Hosting", "{830A89EC-4029-4753-B25A-068BAE37DEC7}"
@@ -1320,6 +1335,22 @@ Global
{5CB63205-24F4-4388-A41B-BAF3BEA59866}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CB63205-24F4-4388-A41B-BAF3BEA59866}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CB63205-24F4-4388-A41B-BAF3BEA59866}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9357EC71-823B-433A-9993-B7CB2FA082D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9357EC71-823B-433A-9993-B7CB2FA082D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9357EC71-823B-433A-9993-B7CB2FA082D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9357EC71-823B-433A-9993-B7CB2FA082D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3F7B206E-5457-458F-AA81-9449FA3C1B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3F7B206E-5457-458F-AA81-9449FA3C1B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3F7B206E-5457-458F-AA81-9449FA3C1B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3F7B206E-5457-458F-AA81-9449FA3C1B5C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7F57331-5DDD-444A-A620-8639FC9362B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7F57331-5DDD-444A-A620-8639FC9362B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7F57331-5DDD-444A-A620-8639FC9362B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7F57331-5DDD-444A-A620-8639FC9362B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {569B8215-BDB1-445D-A7BA-DB255700A4AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {569B8215-BDB1-445D-A7BA-DB255700A4AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {569B8215-BDB1-445D-A7BA-DB255700A4AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {569B8215-BDB1-445D-A7BA-DB255700A4AC}.Release|Any CPU.Build.0 = Release|Any CPU
{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1328,10 +1359,18 @@ Global
{588CD2D7-EE70-43C1-8233-330854BDF53C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{588CD2D7-EE70-43C1-8233-330854BDF53C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{588CD2D7-EE70-43C1-8233-330854BDF53C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D5B392A4-29CD-41F9-8847-0C211C832713}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D5B392A4-29CD-41F9-8847-0C211C832713}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D5B392A4-29CD-41F9-8847-0C211C832713}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D5B392A4-29CD-41F9-8847-0C211C832713}.Release|Any CPU.Build.0 = Release|Any CPU
{173BDA6E-F175-4457-BF64-58CD184E9A81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{173BDA6E-F175-4457-BF64-58CD184E9A81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{173BDA6E-F175-4457-BF64-58CD184E9A81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{173BDA6E-F175-4457-BF64-58CD184E9A81}.Release|Any CPU.Build.0 = Release|Any CPU
+ {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Release|Any CPU.Build.0 = Release|Any CPU
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1BC02557-B78B-48CE-9D3C-488A6B7672F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -1591,11 +1630,18 @@ Global
{9FAE1602-2C69-4D24-8655-A164489441E8} = {C424395C-1235-41A4-BF55-07880A04368C}
{DF00FDA3-D3EC-4E07-B4EC-0EBB57A813A4} = {77CFE74A-32EE-400C-8930-5025E8555256}
{5CB63205-24F4-4388-A41B-BAF3BEA59866} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
+ {9357EC71-823B-433A-9993-B7CB2FA082D1} = {B80354C7-BE58-43F6-8928-9F3A74AB7F47}
+ {3F7B206E-5457-458F-AA81-9449FA3C1B5C} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
+ {6C71A90C-30AE-45D7-9347-D66F9B257CBE} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {F7F57331-5DDD-444A-A620-8639FC9362B2} = {6C71A90C-30AE-45D7-9347-D66F9B257CBE}
+ {569B8215-BDB1-445D-A7BA-DB255700A4AC} = {6C71A90C-30AE-45D7-9347-D66F9B257CBE}
{A6813855-E322-41EF-B2E6-7A44949EF962} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{E6BE41D3-872C-47D2-B5B1-78C37AFAEAF9} = {A6813855-E322-41EF-B2E6-7A44949EF962}
{588CD2D7-EE70-43C1-8233-330854BDF53C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
+ {D5B392A4-29CD-41F9-8847-0C211C832713} = {C424395C-1235-41A4-BF55-07880A04368C}
{7123AB7A-A4FD-4F64-8B05-D2DD0C3E2ABC} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{173BDA6E-F175-4457-BF64-58CD184E9A81} = {7123AB7A-A4FD-4F64-8B05-D2DD0C3E2ABC}
+ {62D8C73C-DAB3-4B9E-A508-34C886C374F9} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{C424395C-1235-41A4-BF55-07880A04368C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{830A89EC-4029-4753-B25A-068BAE37DEC7} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{1BC02557-B78B-48CE-9D3C-488A6B7672F4} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b55c0820e5..52a24f322f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -94,6 +94,7 @@
+
@@ -154,7 +155,7 @@
-
+
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/Elasticsearch.ApiService.csproj b/playground/Elasticsearch/Elasticsearch.ApiService/Elasticsearch.ApiService.csproj
new file mode 100644
index 0000000000..9d28cd91b4
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/Elasticsearch.ApiService.csproj
@@ -0,0 +1,12 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/Models/Person.cs b/playground/Elasticsearch/Elasticsearch.ApiService/Models/Person.cs
new file mode 100644
index 0000000000..b8fc7803bb
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/Models/Person.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 Elasticsearch.ApiService.Models;
+
+public class Person
+{
+ public required string FirstName { get; set; }
+ public required string LastName { get; set; }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs b/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs
new file mode 100644
index 0000000000..c36a82d883
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Elastic.Clients.Elasticsearch;
+using Elasticsearch.ApiService.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddElasticsearchClient("elasticsearch");
+
+var app = builder.Build();
+
+app.MapGet("/get", async (ElasticsearchClient elasticClient) =>
+{
+ var response = await elasticClient.GetAsync("people", "1");
+ return response;
+});
+
+app.MapGet("/create", async (ElasticsearchClient elasticClient) =>
+{
+ var exist = await elasticClient.Indices.ExistsAsync("people");
+ if (exist.Exists)
+ {
+ await elasticClient.Indices.DeleteAsync("people");
+ }
+
+ var person = new Person
+ {
+ FirstName = "Alireza",
+ LastName = "Baloochi"
+ };
+
+ var response = await elasticClient.IndexAsync(person, "people", "1");
+ return response;
+});
+
+app.Run();
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/Properties/launchSettings.json b/playground/Elasticsearch/Elasticsearch.ApiService/Properties/launchSettings.json
new file mode 100644
index 0000000000..c6bb44e0e5
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:4855",
+ "sslPort": 44376
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5053",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7158;http://localhost:5053",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.Development.json b/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.json b/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.json
new file mode 100644
index 0000000000..10f68b8c8b
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.props b/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.props
new file mode 100644
index 0000000000..d9b2c324ac
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.targets b/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.targets
new file mode 100644
index 0000000000..466c472e8d
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Elasticsearch.AppHost.csproj b/playground/Elasticsearch/Elasticsearch.AppHost/Elasticsearch.AppHost.csproj
new file mode 100644
index 0000000000..c707b72bac
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/Elasticsearch.AppHost.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 76c9a20b-a2cc-4874-8d26-6fbe827d11bf
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs
new file mode 100644
index 0000000000..b6d273b2ef
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs
@@ -0,0 +1,19 @@
+// 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 elasticsearch = builder.AddElasticsearch("elasticsearch")
+ .WithDataVolume();
+
+builder.AddProject("elasticsearch-apiservice")
+ .WithReference(elasticsearch);
+
+// This project is only added in playground projects to support development/debugging
+// of the dashboard. It is not required in end developer code. Comment out this code
+// to test end developer dashboard launch experience. Refer to Directory.Build.props
+// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output
+// in the artifacts dir).
+builder.AddProject(KnownResourceNames.AspireDashboard);
+
+builder.Build().Run();
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Properties/launchSettings.json b/playground/Elasticsearch/Elasticsearch.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000000..cf4b3df509
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17196;http://localhost:15122",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21146",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22194"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15122",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19117",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20104"
+ }
+ },
+ "generate-manifest": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "dotnetRunMessages": true,
+ "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
+ "applicationUrl": "http://localhost:15122",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175"
+ }
+ }
+ }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.Development.json b/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.json b/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.json
new file mode 100644
index 0000000000..31c092aa45
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/aspire-manifest.json b/playground/Elasticsearch/Elasticsearch.AppHost/aspire-manifest.json
new file mode 100644
index 0000000000..4739cf32ac
--- /dev/null
+++ b/playground/Elasticsearch/Elasticsearch.AppHost/aspire-manifest.json
@@ -0,0 +1,74 @@
+{
+ "$schema": "https://json.schemastore.org/aspire-8.0.json",
+ "resources": {
+ "elasticsearch": {
+ "type": "container.v0",
+ "connectionString": "http://elastic:{elasticsearch-password.value}@{elasticsearch.bindings.http.host}:{elasticsearch.bindings.http.port}",
+ "image": "docker.io/library/elasticsearch:8.14.0",
+ "volumes": [
+ {
+ "name": "data",
+ "target": "/usr/share/elasticsearch/data",
+ "readOnly": false
+ }
+ ],
+ "env": {
+ "discovery.type": "single-node",
+ "xpack.security.enabled": "true",
+ "ELASTIC_PASSWORD": "{elasticsearch-password.value}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 9200
+ },
+ "internal": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 9300
+ }
+ }
+ },
+ "elasticsearch-apiservice": {
+ "type": "project.v0",
+ "path": "../Elasticsearch.ApiService/Elasticsearch.ApiService.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
+ "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
+ "ConnectionStrings__elasticsearch": "{elasticsearch.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "elasticsearch-password": {
+ "type": "parameter.v0",
+ "value": "{elasticsearch-password.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string",
+ "secret": true,
+ "default": {
+ "generate": {
+ "minLength": 22
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj
new file mode 100644
index 0000000000..d50c33041c
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/Aspire.Hosting.Elasticsearch.csproj
@@ -0,0 +1,28 @@
+
+
+
+ $(NetCurrent)
+ true
+ true
+ aspire hosting elasticsearch
+ Elasticsearch support for .NET Aspire.
+ $(SharedDir)Elastic_logo.png
+
+
+
+ 80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs
new file mode 100644
index 0000000000..e33af85e63
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/ElasticsearchBuilderExtensions.cs
@@ -0,0 +1,110 @@
+// 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.Elasticsearch;
+using Aspire.Hosting.Utils;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Elasticsearch resources to the application model.
+///
+public static class ElasticsearchBuilderExtensions
+{
+ private const int ElasticsearchPort = 9200;
+ private const int ElasticsearchInternalPort = 9300;
+
+ ///
+ /// Adds an Elasticsearch container resource to the application model.
+ ///
+ ///
+ /// The default image is "elasticsearch" and the tag is "8.14.0".
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The host port to bind the underlying container to.
+ /// The parameter used to provide the superuser password for the elasticsearch. If a random password will be generated.
+ /// A reference to the .
+ ///
+ /// Add an Elasticsearch container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var elasticsearch = builder.AddElasticsearch("elasticsearch");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(elasticsearch);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder AddElasticsearch(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ IResourceBuilder? password = null,
+ int? port = null)
+ {
+ var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password");
+
+ var elasticsearch = new ElasticsearchResource(name, passwordParameter);
+
+ return builder.AddResource(elasticsearch)
+ .WithImage(ElasticsearchContainerImageTags.Image, ElasticsearchContainerImageTags.Tag)
+ .WithImageRegistry(ElasticsearchContainerImageTags.Registry)
+ .WithHttpEndpoint(targetPort: ElasticsearchPort, port: port, name: ElasticsearchResource.PrimaryEndpointName)
+ .WithEndpoint(targetPort: ElasticsearchInternalPort, port: port, name: ElasticsearchResource.InternalEndpointName)
+ .WithEnvironment("discovery.type", "single-node")
+ .WithEnvironment("xpack.security.enabled", "true")
+ .WithEnvironment(context =>
+ {
+ context.EnvironmentVariables["ELASTIC_PASSWORD"] = elasticsearch.PasswordParameter;
+ });
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a Elasticseach container resource.
+ ///
+ /// The resource builder.
+ /// The name of the volume. Defaults to an auto-generated name based on the application and resource names.
+ /// The .
+ ///
+ /// Add an Elasticsearch container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a data volume is added to the container to allow data to be persisted across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var elasticsearch = builder.AddElasticsearch("elasticsearch")
+ /// .WithDataVolume();
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(elasticsearch);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null)
+ => builder.WithVolume(name ?? VolumeNameGenerator.CreateVolumeName(builder, "data"), "/usr/share/elasticsearch/data");
+
+ ///
+ /// Adds a bind mount for the data folder to a Elasticseach container resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// The .
+ ///
+ /// Add an Elasticsearch container to the application model and reference it in a .NET project. Additionally, in this
+ /// example a bind mount is added to the container to allow data to be persisted across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var elasticsearch = builder.AddElasticsearch("elasticsearch")
+ /// .WithDataBindMount("./data/elasticsearch/data");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(elasticsearch);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source)
+ => builder.WithBindMount(source, "/usr/share/elasticsearch/data");
+
+}
diff --git a/src/Aspire.Hosting.Elasticsearch/ElasticsearchContainerImageTags.cs b/src/Aspire.Hosting.Elasticsearch/ElasticsearchContainerImageTags.cs
new file mode 100644
index 0000000000..4a43f9c7a3
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/ElasticsearchContainerImageTags.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.
+
+namespace Aspire.Hosting.Elasticsearch;
+
+internal static class ElasticsearchContainerImageTags
+{
+ public const string Registry = "docker.io";
+ public const string Image = "library/elasticsearch";
+ public const string Tag = "8.14.0";
+}
+
diff --git a/src/Aspire.Hosting.Elasticsearch/ElasticsearchResource.cs b/src/Aspire.Hosting.Elasticsearch/ElasticsearchResource.cs
new file mode 100644
index 0000000000..bac9b49d20
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/ElasticsearchResource.cs
@@ -0,0 +1,57 @@
+// 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 Elasticsearch
+///
+public class ElasticsearchResource : ContainerResource, IResourceWithConnectionString
+{
+ ///
+ /// Gets the Elasticsearch container superuser name.
+ ///
+ private const string UserName = "elastic";
+
+ // this endpoint is used for all API calls over HTTP.
+ // This includes search and aggregations, monitoring and anything else that uses a HTTP request.
+ // All client libraries will use this port to talk to Elasticsearch
+ internal const string PrimaryEndpointName = "http";
+
+ //this endpoint is a custom binary protocol used for communications between nodes in a cluster.
+ //For things like cluster updates, master elections, nodes joining/leaving, shard allocation
+ internal const string InternalEndpointName = "internal";
+
+ /// The name of the resource.
+ /// A parameter that contains the Elasticsearch superuser password.
+ public ElasticsearchResource(string name, ParameterResource password) : base(name)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+ PasswordParameter = password;
+ }
+
+ private EndpointReference? _primaryEndpoint;
+ private EndpointReference? _internalEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the Elasticsearch. This endpoint is used for all API calls over HTTP.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the internal endpoint for the Elasticsearch. This endpoint used for communications between nodes in a cluster
+ ///
+ public EndpointReference InternalEndpoint => _internalEndpoint ??= new(this, InternalEndpointName);
+
+ ///
+ /// Gets the parameter that contains the Elasticsearch superuser password.
+ ///
+ public ParameterResource PasswordParameter { get; }
+
+ ///
+ /// Gets the connection string expression for the Elasticsearch
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"http://{UserName}:{PasswordParameter}@{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
+}
+
diff --git a/src/Aspire.Hosting.Elasticsearch/PublicAPI.Shipped.txt b/src/Aspire.Hosting.Elasticsearch/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..7dc5c58110
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Aspire.Hosting.Elasticsearch/PublicAPI.Unshipped.txt b/src/Aspire.Hosting.Elasticsearch/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..9f0832cefd
--- /dev/null
+++ b/src/Aspire.Hosting.Elasticsearch/PublicAPI.Unshipped.txt
@@ -0,0 +1,10 @@
+Aspire.Hosting.ApplicationModel.ElasticsearchResource
+Aspire.Hosting.ApplicationModel.ElasticsearchResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
+Aspire.Hosting.ApplicationModel.ElasticsearchResource.ElasticsearchResource(string! name, Aspire.Hosting.ApplicationModel.ParameterResource! password) -> void
+Aspire.Hosting.ApplicationModel.ElasticsearchResource.InternalEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
+Aspire.Hosting.ApplicationModel.ElasticsearchResource.PasswordParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource!
+Aspire.Hosting.ApplicationModel.ElasticsearchResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
+Aspire.Hosting.ElasticsearchBuilderExtensions
+static Aspire.Hosting.ElasticsearchBuilderExtensions.AddElasticsearch(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder? password = null, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.ElasticsearchBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.ElasticsearchBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/Aspire.Elastic.Clients.Elasticsearch.csproj b/src/Components/Aspire.Elastic.Clients.Elasticsearch/Aspire.Elastic.Clients.Elasticsearch.csproj
new file mode 100644
index 0000000000..2684b2952a
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/Aspire.Elastic.Clients.Elasticsearch.csproj
@@ -0,0 +1,31 @@
+
+
+
+ $(NetCurrent)
+ true
+ true
+ $(ComponentCommonPackageTags) Elasticsearch
+ A Elasticsearch client that integrates with Aspire, including health checks, logging, and telemetry.
+ $(SharedDir)Elastic_logo.png
+
+ false
+
+
+
+ 80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/AspireElasticClientsElasticsearchExtensions.cs b/src/Components/Aspire.Elastic.Clients.Elasticsearch/AspireElasticClientsElasticsearchExtensions.cs
new file mode 100644
index 0000000000..1dd731cb0b
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/AspireElasticClientsElasticsearchExtensions.cs
@@ -0,0 +1,141 @@
+// 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.Elastic.Clients.Elasticsearch;
+using Elastic.Clients.Elasticsearch;
+using Elastic.Transport;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Extension methods for connecting Elasticsearch with Elastic.Clients.Elasticsearch client.
+///
+public static class AspireElasticClientsElasticsearchExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Elastic:Clients:Elasticsearch";
+ private const string ActivityNameSource = "Elastic.Transport";
+ ///
+ /// Registers instance for connecting to Elasticsearch with Elastic.Clients.Elasticsearch client.
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// An optional delegate that can be used for customizing ElasticsearchClientSettings.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddElasticsearchClient(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null,
+ Action? configureClientSettings = null
+ ) => builder.AddElasticsearchClient(DefaultConfigSectionName, configureSettings, configureClientSettings, connectionName, serviceKey: null);
+
+ ///
+ /// Registers instance for connecting to Elasticsearch with Elastic.Clients.Elasticsearch client.
+ ///
+ /// The to read config from and add services to.
+ /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// An optional delegate that can be used for customizing ElasticsearchClientSettings.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddKeyedElasticsearchClient(
+ this IHostApplicationBuilder builder,
+ string name,
+ Action? configureSettings = null,
+ Action? configureClientSettings = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ builder.AddElasticsearchClient(
+ $"{DefaultConfigSectionName}:{name}",
+ configureSettings,
+ configureClientSettings,
+ connectionName: name,
+ serviceKey: name);
+ }
+
+ private static void AddElasticsearchClient(
+ this IHostApplicationBuilder builder,
+ string configurationSectionName,
+ Action? configureSettings,
+ Action? configureClientSettings,
+ string connectionName,
+ object? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var configSection = builder.Configuration.GetSection(configurationSectionName);
+
+ ElasticClientsElasticsearchSettings settings = new();
+ configSection.Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ParseConnectionString(connectionString);
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (serviceKey is null)
+ {
+ builder.Services.AddSingleton(CreateElasticsearchClient);
+ }
+ else
+ {
+ builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => CreateElasticsearchClient(sp));
+ }
+
+ if (!settings.DisableTracing)
+ {
+ builder.Services
+ .AddOpenTelemetry()
+ .WithTracing(tracer => tracer.AddSource(ActivityNameSource));
+ }
+
+ if (!settings.DisableHealthChecks)
+ {
+ var healthCheckName = serviceKey is null ? "Elastic.Clients.Elasticsearch" : $"Elastic.Clients.Elasticsearch_{connectionName}";
+
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ healthCheckName,
+ sp => new ElasticsearchHealthCheck(serviceKey is null ?
+ sp.GetRequiredService() :
+ sp.GetRequiredKeyedService(serviceKey)),
+ failureStatus: null,
+ tags: null,
+ timeout: settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null
+ ));
+ }
+
+ ElasticsearchClient CreateElasticsearchClient(IServiceProvider serviceProvider)
+ {
+ var elasticsearchClientSettings = CreateElasticsearchClientSettings(settings, connectionName, configurationSectionName);
+
+ configureClientSettings?.Invoke(elasticsearchClientSettings);
+
+ return new ElasticsearchClient(elasticsearchClientSettings);
+ }
+ }
+
+ private static ElasticsearchClientSettings CreateElasticsearchClientSettings(
+ ElasticClientsElasticsearchSettings settings,
+ string connectionName,
+ string configurationSectionName)
+ {
+ if (settings.Endpoint is not null)
+ {
+ return new ElasticsearchClientSettings(settings.Endpoint);
+ }
+ else if (settings.CloudId is not null && settings.ApiKey is not null)
+ {
+ return new(settings.CloudId, new ApiKey(settings.ApiKey));
+ }
+
+ throw new InvalidOperationException(
+ $"A ElasticsearchClient 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.Elastic.Clients.Elasticsearch/AssemblyInfo.cs b/src/Components/Aspire.Elastic.Clients.Elasticsearch/AssemblyInfo.cs
new file mode 100644
index 0000000000..462de01119
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// 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.Elastic.Clients.Elasticsearch;
+
+[assembly: ConfigurationSchema("Aspire:Elastic:Clients:Elasticsearch", typeof(ElasticClientsElasticsearchSettings))]
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/ConfigurationSchema.json b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ConfigurationSchema.json
new file mode 100644
index 0000000000..ad738f07bc
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ConfigurationSchema.json
@@ -0,0 +1,53 @@
+{
+ "type": "object",
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "Elastic": {
+ "type": "object",
+ "properties": {
+ "Clients": {
+ "type": "object",
+ "properties": {
+ "Elasticsearch": {
+ "type": "object",
+ "properties": {
+ "ApiKey": {
+ "type": "string",
+ "description": "The API Key of the Elastic Cloud to connect to."
+ },
+ "CloudId": {
+ "type": "string",
+ "description": "The CloudId of the Elastic Cloud to connect to."
+ },
+ "DisableHealthChecks": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the Elasticsearch health check is disabled or not.",
+ "default": false
+ },
+ "DisableTracing": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.",
+ "default": false
+ },
+ "Endpoint": {
+ "type": "string",
+ "format": "uri",
+ "description": "The endpoint URI string of the Elasticsearch to connect to."
+ },
+ "HealthCheckTimeout": {
+ "type": "integer",
+ "description": "Gets or sets a integer value that indicates the Elasticsearch health check timeout in milliseconds."
+ }
+ },
+ "description": "Provides the client configuration settings for connecting to a Elasticsearch using Elastic.Clients.Elasticsearch."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticClientsElasticsearchSettings.cs b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticClientsElasticsearchSettings.cs
new file mode 100644
index 0000000000..28f5d74408
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticClientsElasticsearchSettings.cs
@@ -0,0 +1,83 @@
+// 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.Elastic.Clients.Elasticsearch;
+
+///
+/// Provides the client configuration settings for connecting to a Elasticsearch using Elastic.Clients.Elasticsearch.
+///
+public sealed class ElasticClientsElasticsearchSettings
+{
+
+ private const string ConnectionStringEndpoint = "Endpoint";
+ private const string ConnectionStringApiKey = "ApiKey";
+ private const string ConnectionStringCloudId = "CloudId";
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the Elasticsearch health check is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableTracing { get; set; }
+
+ ///
+ /// Gets or sets a integer value that indicates the Elasticsearch health check timeout in milliseconds.
+ ///
+ public int? HealthCheckTimeout { get; set; }
+
+ ///
+ /// The endpoint URI string of the Elasticsearch to connect to.
+ ///
+ public Uri? Endpoint { get; set; }
+
+ ///
+ /// The API Key of the Elastic Cloud to connect to.
+ ///
+ public string? ApiKey { get; set; }
+
+ ///
+ /// The CloudId of the Elastic Cloud to connect to.
+ ///
+ public string? CloudId { 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(ConnectionStringApiKey))
+ {
+ ApiKey = connectionBuilder[ConnectionStringApiKey].ToString();
+ }
+
+ if (connectionBuilder.ContainsKey(ConnectionStringCloudId))
+ {
+ CloudId = connectionBuilder[ConnectionStringCloudId].ToString();
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticsearchHealthCheck.cs b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticsearchHealthCheck.cs
new file mode 100644
index 0000000000..17ed4c0246
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/ElasticsearchHealthCheck.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Elastic.Clients.Elasticsearch;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+// TODO: Use health check from AspNetCore.Diagnostics.HealthChecks once following PR released:
+// https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/pull/2244
+namespace Aspire.Elastic.Clients.Elasticsearch;
+
+internal sealed class ElasticsearchHealthCheck : IHealthCheck
+{
+ private readonly ElasticsearchClient _elasticsearchClient;
+
+ public ElasticsearchHealthCheck(ElasticsearchClient elasticsearchClient)
+ {
+ ArgumentNullException.ThrowIfNull(elasticsearchClient, nameof(elasticsearchClient));
+ _elasticsearchClient = elasticsearchClient;
+ }
+
+ ///
+ public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var pingResult = await _elasticsearchClient.PingAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ bool isSuccess = pingResult.ApiCallDetails.HttpStatusCode == 200;
+
+ return isSuccess
+ ? HealthCheckResult.Healthy()
+ : new HealthCheckResult(context.Registration.FailureStatus);
+ }
+ catch (Exception ex)
+ {
+ return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
+ }
+ }
+}
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Shipped.txt b/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Shipped.txt
new file mode 100644
index 0000000000..7dc5c58110
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Unshipped.txt b/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000000..7cc78d0c12
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/PublicAPI.Unshipped.txt
@@ -0,0 +1,18 @@
+#nullable enable
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.ApiKey.get -> string?
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.ApiKey.set -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.CloudId.get -> string?
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.CloudId.set -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.DisableHealthChecks.get -> bool
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.DisableHealthChecks.set -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.DisableTracing.get -> bool
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.DisableTracing.set -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.ElasticClientsElasticsearchSettings() -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.Endpoint.get -> System.Uri?
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.Endpoint.set -> void
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.HealthCheckTimeout.get -> int?
+Aspire.Elastic.Clients.Elasticsearch.ElasticClientsElasticsearchSettings.HealthCheckTimeout.set -> void
+Microsoft.Extensions.Hosting.AspireElasticClientsElasticsearchExtensions
+static Microsoft.Extensions.Hosting.AspireElasticClientsElasticsearchExtensions.AddElasticsearchClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action? configureSettings = null, System.Action? configureClientSettings = null) -> void
+static Microsoft.Extensions.Hosting.AspireElasticClientsElasticsearchExtensions.AddKeyedElasticsearchClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action? configureSettings = null, System.Action? configureClientSettings = null) -> void
diff --git a/src/Components/Aspire.Elastic.Clients.Elasticsearch/README.md b/src/Components/Aspire.Elastic.Clients.Elasticsearch/README.md
new file mode 100644
index 0000000000..d52c07e5f7
--- /dev/null
+++ b/src/Components/Aspire.Elastic.Clients.Elasticsearch/README.md
@@ -0,0 +1,145 @@
+# Aspire.Elastic.Clients.Elasticsearch
+
+Registers a [ElasticsearchClient](https://github.com/elastic/elasticsearch-net) in the DI container for connecting to a Elasticsearch.
+
+## Getting started
+
+### Prerequisites
+
+- Elasticsearch cluster.
+- Connection string for accessing the Elasticsearch API endpoint or a CloudId and an ApiKey from [Elastic Cloud](https://www.elastic.co/cloud)
+
+### Install the package
+
+Install the .NET Aspire Elasticsearch Client library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Elastic.Clients.Elasticsearch
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddElasticsearchClient` extension method to register a `ElasticsearchClient` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddElasticsearchClient("elasticsearch");
+```
+
+## Configuration
+
+The .NET Aspire Elasticsearch 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.AddElasticsearchClient()`:
+
+```csharp
+builder.AddElasticsearchClient("elasticsearch");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "elasticsearch": "http://elastic:password@localhost:27011"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire Elasticsearch Client component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `ElasticClientsElasticsearchSettings` from configuration by using the `Aspire:Elastic:Clients:Elasticsearch` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "Elastic": {
+ "Clients": {
+ "Elasticsearch": {
+ "ConnectionString": "http://elastic:password@localhost:27011"
+ }
+ }
+ }
+ }
+}
+```
+
+### 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.AddElasticsearchClient("elasticsearch", settings => settings.ConnectionString = "http://elastic:password@localhost:27011");
+```
+
+## AppHost extensions
+
+In your AppHost project, install the `Aspire.Hosting.Elasticsearch` library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Hosting.Elasticsearch
+```
+
+Then, in the _Program.cs_ file of `AppHost`, register a Elasticsearch cluster and consume the connection using the following methods:
+
+```csharp
+var elasticsearch = builder.AddElasticsearch("elasticsearch");
+
+var myService = builder.AddProject()
+ .WithReference(elasticsearch);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `elasticsearch`. In the _Program.cs_ file of `MyService`, the Elasticsearch connection can be consumed using:
+
+```csharp
+builder.AddElasticsearchClient("elasticsearch");
+```
+
+### Use a ```CloudId``` and an ```ApiKey``` with configuration providers
+
+When using [Elastic Cloud](https://www.elastic.co/cloud) ,
+you can provide the ```CloudId``` and ```ApiKey``` in ```Aspire:Elastic:Clients:Elasticsearch:Cloud``` section
+and set ```Aspire:Elastic:Clients:Elasticsearch:UseCloud``` key to ```true```
+when calling `builder.AddElasticsearchClient()`.
+Example appsettings.json that configures the options:
+
+```csharp
+builder.AddElasticsearchClient("elasticsearch");
+```
+
+```json
+{
+ "Aspire": {
+ "Elastic": {
+ "Clients": {
+ "UseCloud" : true,
+ "Cloud": {
+ "ApiKey": "Valid ApiKey",
+ "CloudId": "Valid CloudId"
+ }
+ }
+ }
+ }
+}
+```
+
+### Use a ```CloudId``` and an ```ApiKey``` with inline delegates
+
+```csharp
+builder.AddElasticsearchClient("elasticsearch",
+settings => {
+ settings.UseCloud = true;
+ settings.Cloud.CloudId = "Valid CloudId";
+ settings.Cloud.ApiKey = "Valid ApiKey";
+});
+```
+
+## Additional documentation
+
+* https://github.com/elastic/elasticsearch-net
+* https://github.com/dotnet/aspire/tree/main/src/Components/README.md
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md
index 161529d9e3..58ad2678ae 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -32,6 +32,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As
| Seq | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | N/A | ✅ |
| Qdrant.Client | ✅ | ✅ | ✅ | ✅ | ✅ | | | |
| Milvus.Client | ✅ | ✅ | ✅ | ✅ | ✅ | | | ✅ |
+| Elastic.Clients.Elasticsearch | ✅ | ✅ | ✅ | ✅ | | ✅ | | ✅ |
Nomenclature used in the table above:
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index 33bac82863..7427be4f01 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -104,6 +104,14 @@ Aspire.Confluent.Kafka:
- "messaging.kafka.message.rx"
- "messaging.kafka.message.received"
+Aspire.Elastic.Clients.Elasticsearch:
+- Log categories:
+ - none (not currently supported by Elastic.Clients.Elasticsearch library)
+- Activity source names:
+ - "Elastic.Transport"
+- Metric names:
+ - none
+
Aspire.Microsoft.Azure.Cosmos:
- Log categories:
- "Azure-Cosmos-Operation-Request-Diagnostics"
diff --git a/src/Shared/Elastic_logo.png b/src/Shared/Elastic_logo.png
new file mode 100644
index 0000000000..c3402e9911
Binary files /dev/null and b/src/Shared/Elastic_logo.png differ
diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/Aspire.Elastic.Clients.Elasticsearch.Tests.csproj b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/Aspire.Elastic.Clients.Elasticsearch.Tests.csproj
new file mode 100644
index 0000000000..340af28f7a
--- /dev/null
+++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/Aspire.Elastic.Clients.Elasticsearch.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ $(NetCurrent)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/AspireElasticClientExtensionsTest.cs b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/AspireElasticClientExtensionsTest.cs
new file mode 100644
index 0000000000..122ce8b92f
--- /dev/null
+++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/AspireElasticClientExtensionsTest.cs
@@ -0,0 +1,178 @@
+// 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.Common.Tests;
+using Elastic.Clients.Elasticsearch;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using OpenTelemetry.Trace;
+using Xunit;
+
+namespace Aspire.Elastic.Clients.Elasticsearch.Tests;
+
+public class AspireElasticClientExtensionsTest : IClassFixture
+{
+ private const string DefaultConnectionName = "elasticsearch";
+
+ private readonly ElasticsearchContainerFixture _containerFixture;
+
+ public AspireElasticClientExtensionsTest(ElasticsearchContainerFixture containerFixture)
+ {
+ _containerFixture = containerFixture;
+ }
+
+ private string DefaultConnectionString =>
+ RequiresDockerAttribute.IsSupported ? _containerFixture.GetConnectionString() : "http://elastic:password@localhost:27011";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [RequiresDocker]
+ public async Task AddElasticsearchClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed)
+ {
+ var key = DefaultConnectionName;
+
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedElasticsearchClient(key, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+ else
+ {
+ builder.AddElasticsearchClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetRequiredService();
+
+ var healthCheckReport = await healthCheckService.CheckHealthAsync();
+
+ var healthCheckName = useKeyed ? $"Elastic.Clients.Elasticsearch_{key}" : "Elastic.Clients.Elasticsearch";
+
+ Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddElasticsearchClient_HealthCheckShouldNotBeRegisteredWhenDisabled(bool useKeyed)
+ {
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedElasticsearchClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = true;
+ });
+ }
+ else
+ {
+ builder.AddElasticsearchClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = true;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetService();
+
+ Assert.Null(healthCheckService);
+ }
+
+ [Fact]
+ public void CanAddMultipleKeyedServices()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:elasticsearch1", "http://elastic:password@localhost1:19530"),
+ new KeyValuePair("ConnectionStrings:elasticsearch2", "http://elastic:password@localhost1:19531"),
+ new KeyValuePair("ConnectionStrings:elasticsearch3", "http://elastic:password@localhost1:19532"),
+ ]);
+
+ builder.AddElasticsearchClient("elasticsearch1");
+ builder.AddKeyedElasticsearchClient("elasticsearch2");
+ builder.AddKeyedElasticsearchClient("elasticsearch3");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("elasticsearch2");
+ var client3 = host.Services.GetRequiredKeyedService("elasticsearch3");
+
+ Assert.NotSame(client1, client2);
+ Assert.NotSame(client1, client3);
+ Assert.NotSame(client2, client3);
+ }
+
+ [Fact]
+ public void CanAddClientFromEncodedConnectionString()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:elasticsearch1", "Endpoint=http://elastic:password@localhost1:19530"),
+ new KeyValuePair("ConnectionStrings:elasticsearch2", "Endpoint=http://localhost1:19531"),
+ ]);
+
+ builder.AddElasticsearchClient("elasticsearch1");
+ builder.AddKeyedElasticsearchClient("elasticsearch2");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("elasticsearch2");
+
+ Assert.NotSame(client1, client2);
+ }
+
+ [Fact]
+ [RequiresDocker]
+ public void ElasticsearchInstrumentationEndToEnd()
+ {
+ RemoteExecutor.Invoke(async (connectionString) =>
+ {
+ var builder = CreateBuilder(connectionString);
+
+ builder.AddElasticsearchClient(DefaultConnectionName);
+
+ using var notifier = new ActivityNotifier();
+ builder.Services.AddOpenTelemetry().WithTracing(builder => builder.AddProcessor(notifier));
+
+ using var host = builder.Build();
+ host.Start();
+
+ var elasticsearchClient = host.Services.GetRequiredService();
+ await elasticsearchClient.PingAsync();
+
+ var activityList = await notifier.TakeAsync(1, TimeSpan.FromSeconds(10));
+ Assert.Single(activityList);
+
+ var activity = activityList[0];
+ Assert.Equal("ping", activity.OperationName);
+ Assert.Contains(activity.Tags, kvp => kvp.Key == "db.system" && kvp.Value == "elasticsearch");
+ }, DefaultConnectionString).Dispose();
+ }
+
+ private static HostApplicationBuilder CreateBuilder(string connectionString)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString)
+ ]);
+ return builder;
+ }
+}
diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConfigurationTests.cs b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConfigurationTests.cs
new file mode 100644
index 0000000000..886594ee71
--- /dev/null
+++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConfigurationTests.cs
@@ -0,0 +1,21 @@
+// 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.Elastic.Clients.Elasticsearch.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void EndpointIsNullByDefault() =>
+ Assert.Null(new ElasticClientsElasticsearchSettings().Endpoint);
+
+ [Fact]
+ public void HealthChecksEnabledByDefault() =>
+ Assert.False(new ElasticClientsElasticsearchSettings().DisableHealthChecks);
+
+ [Fact]
+ public void TracingEnabledByDefault() =>
+ Assert.False(new ElasticClientsElasticsearchSettings().DisableTracing);
+}
diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConformanceTests.cs b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConformanceTests.cs
new file mode 100644
index 0000000000..214177623e
--- /dev/null
+++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ConformanceTests.cs
@@ -0,0 +1,107 @@
+// 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.Common.Tests;
+using Aspire.Components.ConformanceTests;
+using Elastic.Clients.Elasticsearch;
+using Elastic.Transport;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Xunit;
+
+namespace Aspire.Elastic.Clients.Elasticsearch.Tests;
+
+public class ConformanceTests : ConformanceTests, IClassFixture
+{
+ private readonly ElasticsearchContainerFixture _containerFixture;
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string ActivitySourceName => "Elastic.Transport";
+
+ protected override string[] RequiredLogCategories => Array.Empty();
+
+ protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported;
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ public ConformanceTests(ElasticsearchContainerFixture containerFixture)
+ {
+ _containerFixture = containerFixture;
+ }
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ {
+ var connectionString = RequiresDockerAttribute.IsSupported ?
+ $"{_containerFixture.GetConnectionString()}" :
+ "http://elastic:password@localhost:27017";
+
+ configuration.AddInMemoryCollection(
+ [
+ new KeyValuePair(CreateConfigKey("Aspire:Elastic:Clients:Elasticsearch", key, "Endpoint"), connectionString),
+ new KeyValuePair($"ConnectionStrings:{key}", $"Endpoint={connectionString}")
+ ]);
+ }
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ //The Testcontainers module creates a container that listens to requests over HTTPS.
+ //To communicate with the Elasticsearch instance, developers must create a ElasticsearchClientSettings instance and set the ServerCertificateValidationCallback delegate to CertificateValidations.AllowAll.
+ //Failing to do so will result in a communication failure as the .NET will reject the certificate coming from the container.
+ if (key is null)
+ {
+ builder.AddElasticsearchClient("elasticsearch", configureSettings: configure, configureClientSettings : (c)=> c.ServerCertificateValidationCallback(CertificateValidations.AllowAll));
+ }
+ else
+ {
+ builder.AddKeyedElasticsearchClient(key, configureSettings: configure, configureClientSettings: (c) => c.ServerCertificateValidationCallback(CertificateValidations.AllowAll));
+ }
+ }
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Elastic": {
+ "Clients": {
+ "Elasticsearch": {
+ "Endpoint": "http://localhost:6334",
+ "DisableHealthChecks": true,
+ "DisableTracing": false,
+ "DisableMetrics": false
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "Elastic":{ "Clients": { "Elasticsearch": { "Endpoint": 0 }}}}}""", "Value is \"integer\" but should be \"string\""),
+ ("""{"Aspire": { "Elastic":{ "Clients": { "Elasticsearch": { "Endpoint": "hello" }}}}}""", "Value does not match format \"uri\"")
+ };
+
+ protected override void SetHealthCheck(ElasticClientsElasticsearchSettings options, bool enabled)
+ {
+ options.DisableHealthChecks = !enabled;
+ options.HealthCheckTimeout = 100;
+ }
+
+ protected override void SetMetrics(ElasticClientsElasticsearchSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void SetTracing(ElasticClientsElasticsearchSettings options, bool enabled)
+ {
+ options.DisableTracing = !enabled;
+ }
+
+ protected override void TriggerActivity(ElasticsearchClient service)
+ {
+ using var source = new CancellationTokenSource(100);
+
+ service.InfoAsync(source.Token).Wait();
+ }
+}
diff --git a/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs
new file mode 100644
index 0000000000..d740b18375
--- /dev/null
+++ b/tests/Aspire.Elastic.Clients.Elasticsearch.Tests/ElasticsearchContainerFixture.cs
@@ -0,0 +1,36 @@
+// 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.Common.Tests;
+using Aspire.Hosting.Elasticsearch;
+using Testcontainers.Elasticsearch;
+using Xunit;
+
+namespace Aspire.Elastic.Clients.Elasticsearch.Tests;
+
+public sealed class ElasticsearchContainerFixture : IAsyncLifetime
+{
+ public ElasticsearchContainer? Container { get; private set; }
+
+ public string GetConnectionString() => Container?.GetConnectionString() ??
+ throw new InvalidOperationException("The test container was not initialized.");
+
+ public async Task InitializeAsync()
+ {
+ if (RequiresDockerAttribute.IsSupported)
+ {
+ Container = new ElasticsearchBuilder()
+ .WithImage($"{ElasticsearchContainerImageTags.Image}:{ElasticsearchContainerImageTags.Tag}")
+ .Build();
+ await Container.StartAsync();
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Container is not null)
+ {
+ await Container.DisposeAsync();
+ }
+ }
+}
diff --git a/tests/Aspire.Hosting.Elasticsearch.Tests/AddElasticsearchTests.cs b/tests/Aspire.Hosting.Elasticsearch.Tests/AddElasticsearchTests.cs
new file mode 100644
index 0000000000..3cba9d2701
--- /dev/null
+++ b/tests/Aspire.Hosting.Elasticsearch.Tests/AddElasticsearchTests.cs
@@ -0,0 +1,239 @@
+// 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.ApplicationModel;
+using Aspire.Hosting.Elasticsearch;
+using Aspire.Hosting.Tests.Utils;
+using Aspire.Hosting.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Aspire.Hosting.Tests.Elasticsearch;
+public class AddElasticsearchTests
+{
+ [Fact]
+ public async Task AddElasticsearchContainerWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ appBuilder.AddElasticsearch("elasticsearch");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("elasticsearch", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Equal(2, endpoints.Count());
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http");
+ Assert.Equal(9200, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("http", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("http", primaryEndpoint.Transport);
+ Assert.Equal("http", primaryEndpoint.UriScheme);
+
+ var internalEndpoint = Assert.Single(endpoints, e => e.Name == "internal");
+ Assert.Equal(9300, internalEndpoint.TargetPort);
+ Assert.False(internalEndpoint.IsExternal);
+ Assert.Equal("internal", internalEndpoint.Name);
+ Assert.Null(internalEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, internalEndpoint.Protocol);
+ Assert.Equal("tcp", internalEndpoint.Transport);
+ Assert.Equal("tcp", internalEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(ElasticsearchContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(ElasticsearchContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(ElasticsearchContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource);
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("discovery.type", env.Key);
+ Assert.Equal("single-node", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("xpack.security.enabled", env.Key);
+ Assert.Equal("true", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("ELASTIC_PASSWORD", env.Key);
+ Assert.False(string.IsNullOrEmpty(env.Value));
+ });
+ }
+
+ [Fact]
+ public async Task AddElasticsearchContainerAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.Configuration["Parameters:pass"] = "pass";
+
+ var pass = appBuilder.AddParameter("pass");
+ appBuilder.AddElasticsearch("elasticsearch",pass);
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("elasticsearch", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Equal(2, endpoints.Count());
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "http");
+ Assert.Equal(9200, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("http", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("http", primaryEndpoint.Transport);
+ Assert.Equal("http", primaryEndpoint.UriScheme);
+
+ var internalEndpoint = Assert.Single(endpoints, e => e.Name == "internal");
+ Assert.Equal(9300, internalEndpoint.TargetPort);
+ Assert.False(internalEndpoint.IsExternal);
+ Assert.Equal("internal", internalEndpoint.Name);
+ Assert.Null(internalEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, internalEndpoint.Protocol);
+ Assert.Equal("tcp", internalEndpoint.Transport);
+ Assert.Equal("tcp", internalEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(ElasticsearchContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(ElasticsearchContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(ElasticsearchContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource);
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("discovery.type", env.Key);
+ Assert.Equal("single-node", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("xpack.security.enabled", env.Key);
+ Assert.Equal("true", env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("ELASTIC_PASSWORD", env.Key);
+ Assert.Equal("pass", env.Value);
+ });
+ }
+
+ [Fact]
+ public async Task ElasticsearchCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var elasticsearch = appBuilder
+ .AddElasticsearch("elasticsearch")
+ .WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 27020));
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ Assert.Equal($"http://elastic:{elasticsearch.Resource.PasswordParameter.Value}@localhost:27020", connectionString);
+ Assert.Equal("http://elastic:{elasticsearch-password.value}@{elasticsearch.bindings.http.host}:{elasticsearch.bindings.http.port}", connectionStringResource.ConnectionStringExpression.ValueExpression);
+ }
+
+ [Fact]
+ public async Task VerifyManifestWithDefaultsAddsAnnotationMetadata()
+ {
+ using var appBuilder = TestDistributedApplicationBuilder.Create();
+
+ var elasticsearch = appBuilder.AddElasticsearch("elasticsearch");
+
+ var manifest = await ManifestUtils.GetManifest(elasticsearch.Resource);
+
+ var expectedManifest = $$"""
+ {
+ "type": "container.v0",
+ "connectionString": "http://elastic:{elasticsearch-password.value}@{elasticsearch.bindings.http.host}:{elasticsearch.bindings.http.port}",
+ "image": "{{ElasticsearchContainerImageTags.Registry}}/{{ElasticsearchContainerImageTags.Image}}:{{ElasticsearchContainerImageTags.Tag}}",
+ "env": {
+ "discovery.type": "single-node",
+ "xpack.security.enabled": "true",
+ "ELASTIC_PASSWORD": "{elasticsearch-password.value}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 9200
+ },
+ "internal": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 9300
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, manifest.ToString());
+ }
+
+ [Fact]
+ public async Task VerifyManifestWithDataVolumeAddsAnnotationMetadata()
+ {
+ using var appBuilder = TestDistributedApplicationBuilder.Create();
+
+ var elasticsearch = appBuilder.AddElasticsearch("elasticsearch")
+ .WithDataVolume("data");
+
+ var manifest = await ManifestUtils.GetManifest(elasticsearch.Resource);
+
+ var expectedManifest = $$"""
+ {
+ "type": "container.v0",
+ "connectionString": "http://elastic:{elasticsearch-password.value}@{elasticsearch.bindings.http.host}:{elasticsearch.bindings.http.port}",
+ "image": "{{ElasticsearchContainerImageTags.Registry}}/{{ElasticsearchContainerImageTags.Image}}:{{ElasticsearchContainerImageTags.Tag}}",
+ "volumes": [
+ {
+ "name": "data",
+ "target": "/usr/share/elasticsearch/data",
+ "readOnly": false
+ }
+ ],
+ "env": {
+ "discovery.type": "single-node",
+ "xpack.security.enabled": "true",
+ "ELASTIC_PASSWORD": "{elasticsearch-password.value}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http",
+ "targetPort": 9200
+ },
+ "internal": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "targetPort": 9300
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, manifest.ToString());
+ }
+}
diff --git a/tests/Aspire.Hosting.Elasticsearch.Tests/Aspire.Hosting.Elasticsearch.Tests.csproj b/tests/Aspire.Hosting.Elasticsearch.Tests/Aspire.Hosting.Elasticsearch.Tests.csproj
new file mode 100644
index 0000000000..9940a49c83
--- /dev/null
+++ b/tests/Aspire.Hosting.Elasticsearch.Tests/Aspire.Hosting.Elasticsearch.Tests.csproj
@@ -0,0 +1,13 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs
new file mode 100644
index 0000000000..6e1f355c53
--- /dev/null
+++ b/tests/Aspire.Hosting.Elasticsearch.Tests/ElasticsearchFunctionalTests.cs
@@ -0,0 +1,280 @@
+// 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.Common.Tests;
+using Aspire.Hosting.Utils;
+using Elastic.Clients.Elasticsearch;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Polly;
+using Xunit;
+
+namespace Aspire.Hosting.Elasticsearch.Tests;
+
+public class ElasticsearchFunctionalTests
+{
+ [Fact]
+ [RequiresDocker]
+ public async Task VerifyElasticsearchResource()
+ {
+ var builder = CreateDistributedApplicationBuilder();
+
+ var elasticsearch = builder.AddElasticsearch("elasticsearch");
+
+ using var app = builder.Build();
+
+ await app.StartAsync();
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{elasticsearch.Resource.Name}"] = await elasticsearch.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)
+ });
+
+ hb.AddElasticsearchClient(elasticsearch.Resource.Name);
+
+ using var host = hb.Build();
+
+ await host.StartAsync();
+
+ var elasticsearchClient = host.Services.GetRequiredService();
+
+ var person = new Person
+ {
+ Id = 1,
+ FirstName = "Alireza",
+ LastName = "Baloochi"
+ };
+
+ var pipeline = new ResiliencePipelineBuilder()
+ .AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(3) })
+ .AddTimeout(TimeSpan.FromSeconds(5))
+ .Build();
+
+ await pipeline.ExecuteAsync(
+ async token =>
+ {
+ var indexResponse = await elasticsearchClient.IndexAsync(person, "people", "1",CancellationToken.None);
+
+ var getResponse = await elasticsearchClient.GetAsync("people", "1", CancellationToken.None);
+
+ Assert.True(indexResponse.IsSuccess());
+ Assert.True(getResponse.IsSuccess());
+ Assert.NotNull(getResponse.Source);
+ Assert.Equal(person.Id, getResponse.Source?.Id);
+ });
+ }
+
+ [Fact]
+ [RequiresDocker]
+ public async Task WithDataVolumeShouldPersistStateBetweenUsages()
+ {
+ var builder1 = CreateDistributedApplicationBuilder();
+ var elasticsearch1 = builder1.AddElasticsearch("elasticsearch");
+
+ // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
+ var volumeName = VolumeNameGenerator.CreateVolumeName(elasticsearch1, nameof(WithDataVolumeShouldPersistStateBetweenUsages));
+ elasticsearch1.WithDataVolume(volumeName);
+
+ var person = new Person
+ {
+ Id = 1,
+ FirstName = "Alireza",
+ LastName = "Baloochi"
+ };
+
+ var pipeline = new ResiliencePipelineBuilder()
+ .AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(3) })
+ .AddTimeout(TimeSpan.FromSeconds(5))
+ .Build();
+
+ using (var app = builder1.Build())
+ {
+ await app.StartAsync();
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{elasticsearch1.Resource.Name}"] = await elasticsearch1.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)
+ });
+
+ hb.AddElasticsearchClient(elasticsearch1.Resource.Name);
+
+ using (var host = hb.Build())
+ {
+ await host.StartAsync();
+
+ await pipeline.ExecuteAsync(
+ async token =>
+ {
+ var elasticsearchClient = host.Services.GetRequiredService();
+
+ var indexResponse = await elasticsearchClient.IndexAsync(person, "people", "1", CancellationToken.None);
+
+ Assert.True(indexResponse.IsSuccess());
+ });
+ }
+
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+
+ var builder2 = CreateDistributedApplicationBuilder();
+ var elasticsearch2 = builder2.AddElasticsearch("elasticsearch").WithDataVolume(volumeName);
+
+ using (var app = builder2.Build())
+ {
+ await app.StartAsync();
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{elasticsearch2.Resource.Name}"] = await elasticsearch2.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)
+ });
+
+ hb.AddElasticsearchClient(elasticsearch2.Resource.Name);
+
+ using (var host = hb.Build())
+ {
+ await host.StartAsync();
+ await pipeline.ExecuteAsync(
+ async token =>
+ {
+ var elasticsearchClient = host.Services.GetRequiredService();
+
+ var getResponse = await elasticsearchClient.GetAsync("people", "1", CancellationToken.None);
+
+ Assert.True(getResponse.IsSuccess());
+ Assert.NotNull(getResponse.Source);
+ Assert.Equal(person.Id, getResponse.Source?.Id);
+ });
+
+ }
+
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+
+ DockerUtils.AttemptDeleteDockerVolume(volumeName);
+ }
+
+ [Fact]
+ [RequiresDocker]
+ public async Task WithDataBindMountShouldPersistStateBetweenUsages()
+ {
+ var bindMountPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+ if (!Directory.Exists(bindMountPath))
+ {
+ Directory.CreateDirectory(bindMountPath);
+ }
+
+ var builder1 = CreateDistributedApplicationBuilder();
+ var elasticsearch1 = builder1.AddElasticsearch("elasticsearch").WithDataBindMount(bindMountPath);
+
+ var person = new Person
+ {
+ Id = 1,
+ FirstName = "Alireza",
+ LastName = "Baloochi"
+ };
+
+ var pipeline = new ResiliencePipelineBuilder()
+ .AddRetry(new() { MaxRetryAttempts = 10, Delay = TimeSpan.FromSeconds(3) })
+ .AddTimeout(TimeSpan.FromSeconds(5))
+ .Build();
+
+ using (var app = builder1.Build())
+ {
+ await app.StartAsync();
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{elasticsearch1.Resource.Name}"] = await elasticsearch1.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)
+ });
+
+ hb.AddElasticsearchClient(elasticsearch1.Resource.Name);
+
+ using (var host = hb.Build())
+ {
+ await host.StartAsync();
+
+ await pipeline.ExecuteAsync(
+ async token =>
+ {
+ var elasticsearchClient = host.Services.GetRequiredService();
+
+ var indexResponse = await elasticsearchClient.IndexAsync(person, "people", "1", CancellationToken.None);
+
+ Assert.True(indexResponse.IsSuccess());
+ });
+ }
+
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+
+ var builder2 = CreateDistributedApplicationBuilder();
+ var elasticsearch2 = builder2.AddElasticsearch("elasticsearch").WithDataBindMount(bindMountPath);
+
+ using (var app = builder2.Build())
+ {
+ await app.StartAsync();
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration.AddInMemoryCollection(new Dictionary
+ {
+ [$"ConnectionStrings:{elasticsearch2.Resource.Name}"] = await elasticsearch2.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None)
+ });
+
+ hb.AddElasticsearchClient(elasticsearch2.Resource.Name);
+
+ using (var host = hb.Build())
+ {
+ await host.StartAsync();
+ await pipeline.ExecuteAsync(
+ async token =>
+ {
+ var elasticsearchClient = host.Services.GetRequiredService();
+
+ var getResponse = await elasticsearchClient.GetAsync("people", "1", CancellationToken.None);
+
+ Assert.True(getResponse.IsSuccess());
+ Assert.NotNull(getResponse.Source);
+ Assert.Equal(person.Id, getResponse.Source?.Id);
+ });
+
+ }
+
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync();
+ }
+
+ try
+ {
+ File.Delete(bindMountPath);
+ }
+ catch
+ {
+ // Don't fail test if we can't clean the temporary folder
+ }
+ }
+
+ private static TestDistributedApplicationBuilder CreateDistributedApplicationBuilder() =>
+ TestDistributedApplicationBuilder.CreateWithTestContainerRegistry();
+
+ private sealed class Person
+ {
+ public int Id { get; set; }
+ public required string FirstName { get; set; }
+ public required string LastName { get; set; }
+ }
+
+}
diff --git a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
index 040b1d4a9a..e52492fb2d 100644
--- a/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
+++ b/tests/Aspire.Hosting.Tests/ManifestGenerationTests.cs
@@ -263,7 +263,7 @@ public void PublishingRedisResourceAsContainerResultsInConnectionStringProperty(
Assert.Equal("container.v0", container.GetProperty("type").GetString());
Assert.Equal("{rediscontainer.bindings.tcp.host}:{rediscontainer.bindings.tcp.port}", container.GetProperty("connectionString").GetString());
}
-
+
[Fact]
public void EnsureAllPostgresManifestTypesHaveVersion0Suffix()
{
diff --git a/tests/Shared/RepoTesting/Directory.Packages.Helix.props b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
index c869279784..71109ca390 100644
--- a/tests/Shared/RepoTesting/Directory.Packages.Helix.props
+++ b/tests/Shared/RepoTesting/Directory.Packages.Helix.props
@@ -12,6 +12,7 @@
+
@@ -31,6 +32,7 @@
+