diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index cfb4a519..bfbc4865 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -33,6 +33,7 @@ jobs:
Hosting.GoFeatureFlag.Tests,
Hosting.Golang.Tests,
Hosting.Java.Tests,
+ Hosting.k6.Tests,
Hosting.LavinMQ.Tests,
Hosting.MailPit.Tests,
Hosting.Meilisearch.Tests,
@@ -51,8 +52,8 @@ jobs:
Hosting.SqlDatabaseProjects.Tests,
Hosting.SqlServer.Extensions.Tests,
Hosting.Sqlite.Tests,
- Hosting.k6.Tests,
Hosting.Minio.Tests,
+ Hosting.SurrealDb.Tests,
# Client integration tests
EventStore.Tests,
@@ -64,6 +65,7 @@ jobs:
OllamaSharp.Tests,
RavenDB.Client.Tests,
Minio.Client.Tests,
+ SurrealDb.Tests,
]
steps:
diff --git a/CODEOWNERS b/CODEOWNERS
index b67fb7f0..9341fe0e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -35,7 +35,6 @@
/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/ @ErikEJ @jmezach
/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/ @ErikEJ @jmezach
-
# CommunityToolkit.Aspire.Hosting.Rust
/examples/rust/ @Alirexaa
@@ -85,21 +84,25 @@
/examples/postgres-ext/ @Alirexaa
# CommunityToolkit.Aspire.Hosting.DbGate
+
/src/CommunityToolkit.Aspire.Hosting.DbGate/ @Alirexaa
/tests/CommunityToolkit.Aspire.Hosting.DbGate.Tests/ @Alirexaa
/examples/dbgate/ @Alirexaa
# CommunityToolkit.Aspire.Hosting.MongoDB.Extensions
+
/src/CommunityToolkit.Aspire.Hosting.MongoDB.Extensions/ @Alirexaa
/tests/CommunityToolkit.Aspire.Hosting.MongoDB.Extensions.Tests/ @Alirexaa
/examples/mongodb-ext/ @Alirexaa
# CommunityToolkit.Aspire.Hosting.Redis.Extensions
+
/src/CommunityToolkit.Aspire.Hosting.Redis.Extensions/ @Alirexaa
/tests/CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests/ @Alirexaa
/examples/redis-ext/ @Alirexaa
# CommunityToolkit.Aspire.Hosting.SqlServer.Extensions
+
/src/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions/ @Alirexaa
/tests/CommunityToolkit.Aspire.Hosting.SqlServer.Extensions.Tests/ @Alirexaa
/examples/sqlserver-ext/ @Alirexaa
@@ -112,4 +115,13 @@
/tests/CommunityToolkit.Aspire.Hosting.Minio.Tests/ @Harold-Morgan
/src/CommunityToolkit.Aspire.Minio.Client/ @Harold-Morgan
-/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ @Harold-Morgan
\ No newline at end of file
+/tests/CommunityToolkit.Aspire.Minio.Client.Tests/ @Harold-Morgan
+
+# CommunityToolkit.Aspire.SurrealDb
+# CommunityToolkit.Aspire.Hosting.SurrealDb
+
+/examples/surrealdb/ @Odonno
+/src/CommunityToolkit.Aspire.Hosting.SurrealDb/ @Odonno
+/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/ @Odonno
+/src/CommunityToolkit.Aspire.SurrealDb/ @Odonno
+/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ @Odonno
diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx
index fdffae1d..1dd9b4a3 100644
--- a/CommunityToolkit.Aspire.slnx
+++ b/CommunityToolkit.Aspire.slnx
@@ -134,6 +134,11 @@
+
+
+
+
+
@@ -172,12 +177,14 @@
+
+
@@ -217,12 +224,14 @@
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 95f5bab4..05ffba9c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,8 +23,10 @@
+
+
@@ -59,6 +61,7 @@
+
@@ -79,14 +82,16 @@
-
+
+
+
@@ -95,6 +100,8 @@
+
+
diff --git a/README.md b/README.md
index 877d88d3..ef1e5aab 100644
--- a/README.md
+++ b/README.md
@@ -12,44 +12,46 @@ All features are contributed by you, our amazing .NET community, and maintained
This repository contains the source code for the .NET Aspire Community Toolkit, a collection of community created Integrations and extensions for [.NET Aspire](https://aka.ms/dotnet/aspire).
-| Package | Description |
-|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| - **Learn More**: [`Hosting.Azure.StaticWebApps`][swa-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields]][swa-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields-preview]][swa-nuget-preview] | A hosting integration for the [Azure Static Web Apps emulator](https://learn.microsoft.com/azure/static-web-apps/static-web-apps-cli-overview) (Note: this does not support deployment of a project to Azure Static Web Apps). |
-| - **Learn More**: [`Hosting.Golang`][golang-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields]][golang-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields-preview]][golang-nuget-preview] | A hosting integration Golang apps. |
-| - **Learn More**: [`Hosting.Java`][java-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields]][java-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields-preview]][java-nuget-preview] | An integration for running Java code in .NET Aspire either using the local JDK or using a container. |
-| - **Learn More**: [`Hosting.NodeJS.Extensions`][nodejs-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.NodeJS.Extensions][nodejs-ext-shields]][nodejs-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.NodeJS.Extensions][nodejs-ext-shields-preview]][nodejs-ext-nuget-preview] | An integration that contains some additional extensions for running Node.js applications |
-| - **Learn More**: [`Hosting.Ollama`][ollama-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields]][ollama-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields-preview]][ollama-nuget-preview] | An Aspire hosting integration leveraging the [Ollama](https://ollama.com) container with support for downloading a model on startup. |
-| - **Learn More**: [`OllamaSharp`][ollama-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields]][ollamasharp-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields-preview]][ollamasharp-nuget-preview] | An Aspire client integration for the [OllamaSharp](https://github.com/awaescher/OllamaSharp) package. |
-| - **Learn More**: [`Hosting.Meilisearch`][meilisearch-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields]][meilisearch-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields-preview]][meilisearch-nuget-preview] | An Aspire hosting integration leveraging the [Meilisearch](https://meilisearch.com) container. |
-| - **Learn More**: [`Meilisearch`][meilisearch-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields]][meilisearch-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields-preview]][meilisearch-client-nuget-preview] | An Aspire client integration for the [Meilisearch](https://github.com/meilisearch/meilisearch-dotnet) package. |
-| - **Learn More**: [`Hosting.Azure.DataApiBuilder`][dab-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields]][dab-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields-preview]][dab-nuget-preview] | A hosting integration for the [Azure Data API builder](https://learn.microsoft.com/en-us/azure/data-api-builder/overview). |
-| - **Learn More**: [`Hosting.Deno`][deno-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields]][deno-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields-preview]][deno-nuget-preview] | A hosting integration for the Deno apps. |
-| - **Learn More**: [`Hosting.SqlDatabaseProjects`][sql-database-projects-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields]][sql-database-projects-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields-preview]][sql-database-projects-nuget-preview] | A hosting integration for the SQL Databases Projects. |
-| - **Learn More**: [`Hosting.Rust`][rust-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields]][rust-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields-preview]][rust-nuget-preview] | A hosting integration for the Rust apps. |
-| - **Learn More**: [`Hosting.Bun`][bun-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields]][bun-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields-preview]][bun-nuget-preview] | A hosting integration for the Bun apps. |
-| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications |
-| - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. |
-| - **Learn More**: [`EventStore`][eventstore-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. |
-| - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. |
-| - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. |
-| - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. |
-| - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. |
-| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. |
-| - **Learn More**: [`Hosting.Azure.Dapr.Redis`][dapr-azureredis-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. |
-| - **Learn More**: [`Hosting.RavenDB`][ravendb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields]][ravendb-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields-preview]][ravendb-nuget-preview] | An Aspire integration leveraging the [RavenDB](https://ravendb.net/) container. |
-| - **Learn More**: [`RavenDB.Client`][ravendb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields]][ravendb-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields-preview]][ravendb-client-nuget-preview] | An Aspire client integration for the [RavenDB.Client](https://www.nuget.org/packages/RavenDB.client) package. |
-| - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. |
-| - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. |
-| - **Learn More**: [`Hosting.MongoDB.Extensions`][mongodb-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.MongoDB.Extensions][mongodb-ext-shields]][mongodb-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MongoDB.Extensions][mongodb-ext-shields-preview]][mongodb-ext-nuget-preview] | An integration that contains some additional extensions for hosting MongoDB container. |
-| - **Learn More**: [`Hosting.PostgreSQL.Extensions`][postgres-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.PostgreSQL.Extensions][postgres-ext-shields]][postgres-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions][postgres-ext-shields-preview]][postgres-ext-nuget-preview] | An integration that contains some additional extensions for hosting PostgreSQL container. |
-| - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. |
-| - **Learn More**: [`Hosting.SqlServer.Extensions`][sqlserver-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.SqlServer.Extensions][sqlserver-ext-shields]][sqlserver-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.SqlServer.Extensions][sqlserver-ext-shields-preview]][sqlserver-ext-nuget-preview] | An integration that contains some additional extensions for hosting SqlServer container. |
-| - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). |
-| - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. |
-| - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. |
-| - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. |
-| - **Learn More**: [`Hosting.MinIO`][minio-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields]][minio-hosting-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields-preview]][minio-hosting-nuget-preview] | An Aspire hosting integration to setup a [MinIO S3](https://min.io/) storage. |
-| - **Learn More**: [`MinIO.Client`][minio-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Minio.Client][minio-client-shields]][minio-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Client.Minio][minio-client-shields-preview]][minio-client-nuget-preview] | An Aspire client integration for the [MinIO](https://github.com/minio/minio-dotnet) package. |
+| Package | Description |
+| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| - **Learn More**: [`Hosting.Azure.StaticWebApps`][swa-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields]][swa-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.StaticWebApps][swa-shields-preview]][swa-nuget-preview] | A hosting integration for the [Azure Static Web Apps emulator](https://learn.microsoft.com/azure/static-web-apps/static-web-apps-cli-overview) (Note: this does not support deployment of a project to Azure Static Web Apps). |
+| - **Learn More**: [`Hosting.Golang`][golang-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields]][golang-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Golang][golang-shields-preview]][golang-nuget-preview] | A hosting integration Golang apps. |
+| - **Learn More**: [`Hosting.Java`][java-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields]][java-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Java][java-shields-preview]][java-nuget-preview] | An integration for running Java code in .NET Aspire either using the local JDK or using a container. |
+| - **Learn More**: [`Hosting.NodeJS.Extensions`][nodejs-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.NodeJS.Extensions][nodejs-ext-shields]][nodejs-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.NodeJS.Extensions][nodejs-ext-shields-preview]][nodejs-ext-nuget-preview] | An integration that contains some additional extensions for running Node.js applications |
+| - **Learn More**: [`Hosting.Ollama`][ollama-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields]][ollama-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Ollama][ollama-shields-preview]][ollama-nuget-preview] | An Aspire hosting integration leveraging the [Ollama](https://ollama.com) container with support for downloading a model on startup. |
+| - **Learn More**: [`OllamaSharp`][ollama-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields]][ollamasharp-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.OllamaSharp][ollamasharp-shields-preview]][ollamasharp-nuget-preview] | An Aspire client integration for the [OllamaSharp](https://github.com/awaescher/OllamaSharp) package. |
+| - **Learn More**: [`Hosting.Meilisearch`][meilisearch-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields]][meilisearch-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Meilisearch][meilisearch-shields-preview]][meilisearch-nuget-preview] | An Aspire hosting integration leveraging the [Meilisearch](https://meilisearch.com) container. |
+| - **Learn More**: [`Meilisearch`][meilisearch-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields]][meilisearch-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Meilisearch][meilisearch-client-shields-preview]][meilisearch-client-nuget-preview] | An Aspire client integration for the [Meilisearch](https://github.com/meilisearch/meilisearch-dotnet) package. |
+| - **Learn More**: [`Hosting.Azure.DataApiBuilder`][dab-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields]][dab-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder][dab-shields-preview]][dab-nuget-preview] | A hosting integration for the [Azure Data API builder](https://learn.microsoft.com/en-us/azure/data-api-builder/overview). |
+| - **Learn More**: [`Hosting.Deno`][deno-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields]][deno-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Deno][deno-shields-preview]][deno-nuget-preview] | A hosting integration for the Deno apps. |
+| - **Learn More**: [`Hosting.SqlDatabaseProjects`][sql-database-projects-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields]][sql-database-projects-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects][sql-database-projects-shields-preview]][sql-database-projects-nuget-preview] | A hosting integration for the SQL Databases Projects. |
+| - **Learn More**: [`Hosting.Rust`][rust-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields]][rust-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Rust][rust-shields-preview]][rust-nuget-preview] | A hosting integration for the Rust apps. |
+| - **Learn More**: [`Hosting.Bun`][bun-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields]][bun-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Bun][bun-shields-preview]][bun-nuget-preview] | A hosting integration for the Bun apps. |
+| - **Learn More**: [`Hosting.Python.Extensions`][python-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Python.Extensions][python-ext-shields]][python-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Python.Extensions][python-ext-shields-preview]][python-ext-nuget-preview] | An integration that contains some additional extensions for running python applications |
+| - **Learn More**: [`Hosting.EventStore`][eventstore-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields]][eventstore-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.EventStore][eventstore-shields-preview]][eventstore-nuget-preview] | An Aspire hosting integration leveraging the [EventStore](https://eventstore.com) container. |
+| - **Learn More**: [`EventStore`][eventstore-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields]][eventstore-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.EventStore][eventstore-client-shields-preview]][eventstore-client-nuget-preview] | An Aspire client integration for the [EventStore](https://github.com/EventStore/EventStore-Client-Dotnet) package. |
+| - **Learn More**: [`Hosting.ActiveMQ`][activemq-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields]][activemq-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.ActiveMQ][activemq-shields-preview]][activemq-nuget-preview] | An Aspire hosting integration leveraging the [ActiveMq](https://activemq.apache.org) container. |
+| - **Learn More**: [`Hosting.Sqlite`][sqlite-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields]][sqlite-hosting-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Sqlite][sqlite-shields-preview]][sqlite-hosting-nuget-preview] | An Aspire hosting integration to setup a SQLite database with optional SQLite Web as a dev UI. |
+| - **Learn More**: [`Microsoft.Data.Sqlite`][sqlite-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields]][sqlite-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Microsoft.Data.Sqlite][sqlite-shields-preview]][sqlite-nuget-preview] | An Aspire client integration for the Microsoft.Data.Sqlite NuGet package. |
+| - **Learn More**: [`Microsoft.EntityFrameworkCore.Sqlite`][sqlite-ef-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields]][sqlite-ef-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite][sqlite-ef-shields-preview]][sqlite-ef-nuget-preview] | An Aspire client integration for the Microsoft.EntityFrameworkCore.Sqlite NuGet package. |
+| - **Learn More**: [`Hosting.Dapr`][dapr-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields]][dapr-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Dapr][dapr-shields-preview]][dapr-nuget-preview] | An Aspire hosting integration for Dapr. |
+| - **Learn More**: [`Hosting.Azure.Dapr.Redis`][dapr-azureredis-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields]][dapr-azureredis-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis][dapr-azureredis-shields-preview]][dapr-azureredis-nuget-preview] | An extension for the Dapr hosting integration for using Dapr with Azure Redis cache. |
+| - **Learn More**: [`Hosting.RavenDB`][ravendb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields]][ravendb-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.RavenDB][ravendb-shields-preview]][ravendb-nuget-preview] | An Aspire integration leveraging the [RavenDB](https://ravendb.net/) container. |
+| - **Learn More**: [`RavenDB.Client`][ravendb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields]][ravendb-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.RavenDB.Client][ravendb-client-shields-preview]][ravendb-client-nuget-preview] | An Aspire client integration for the [RavenDB.Client](https://www.nuget.org/packages/RavenDB.client) package. |
+| - **Learn More**: [`Hosting.GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields]][go-feature-flag-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.GoFeatureFlag][go-feature-flag-shields-preview]][go-feature-flag-nuget-preview] | An Aspire hosting integration leveraging the [GoFeatureFlag](https://gofeatureflag.org/) container. |
+| - **Learn More**: [`GoFeatureFlag`][go-feature-flag-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields]][go-feature-flag-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.GoFeatureFlag][go-feature-flag-client-shields-preview]][go-feature-flag-client-nuget-preview] | An Aspire client integration for the [GoFeatureFlag](https://github.com/open-feature/dotnet-sdk-contrib/tree/main/src/OpenFeature.Contrib.Providers.GOFeatureFlag) package. |
+| - **Learn More**: [`Hosting.MongoDB.Extensions`][mongodb-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.MongoDB.Extensions][mongodb-ext-shields]][mongodb-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MongoDB.Extensions][mongodb-ext-shields-preview]][mongodb-ext-nuget-preview] | An integration that contains some additional extensions for hosting MongoDB container. |
+| - **Learn More**: [`Hosting.PostgreSQL.Extensions`][postgres-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.PostgreSQL.Extensions][postgres-ext-shields]][postgres-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions][postgres-ext-shields-preview]][postgres-ext-nuget-preview] | An integration that contains some additional extensions for hosting PostgreSQL container. |
+| - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. |
+| - **Learn More**: [`Hosting.SqlServer.Extensions`][sqlserver-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.SqlServer.Extensions][sqlserver-ext-shields]][sqlserver-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.SqlServer.Extensions][sqlserver-ext-shields-preview]][sqlserver-ext-nuget-preview] | An integration that contains some additional extensions for hosting SqlServer container. |
+| - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). |
+| - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. |
+| - **Learn More**: [`Hosting.k6`][k6-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields]][k6-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.k6][k6-shields-preview]][k6-nuget-preview] | An Aspire integration leveraging the [Grafana k6](https://k6.io/) container. |
+| - **Learn More**: [`Hosting.MySql.Extensions`][mysql-ext-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.MySql.Extensions][mysql-ext-shields]][mysql-ext-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.MySql.Extensions][mysql-ext-shields-preview]][mysql-ext-nuget-preview] | An integration that contains some additional extensions for hosting MySql container. |
+| - **Learn More**: [`Hosting.MinIO`][minio-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields]][minio-hosting-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.Minio][minio-hosting-shields-preview]][minio-hosting-nuget-preview] | An Aspire hosting integration to setup a [MinIO S3](https://min.io/) storage. |
+| - **Learn More**: [`MinIO.Client`][minio-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Minio.Client][minio-client-shields]][minio-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Client.Minio][minio-client-shields-preview]][minio-client-nuget-preview] | An Aspire client integration for the [MinIO](https://github.com/minio/minio-dotnet) package. |
+| - **Learn More**: [`Hosting.SurrealDb`][surrealdb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.Hosting.SurrealDb][surrealdb-shields]][surrealdb-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.Hosting.SurrealDb][surrealdb-shields-preview]][surrealdb-nuget-preview] | An Aspire hosting integration leveraging the [SurrealDB](https://surrealdb.com/) container. |
+| - **Learn More**: [`SurrealDb`][surrealdb-integration-docs]
- Stable π¦: [![CommunityToolkit.Aspire.SurrealDb][surrealdb-client-shields]][surrealdb-client-nuget]
- Preview π¦: [![CommunityToolkit.Aspire.SurrealDb][surrealdb-client-shields-preview]][surrealdb-client-nuget-preview] | An Aspire client integration for the [SurrealDB](https://github.com/surrealdb/surrealdb.net/) package. |
## π Getting Started
@@ -263,3 +265,12 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org)
[minio-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Minio.Client/
[minio-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Minio.Client?label=nuget%20(preview)
[minio-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Minio.Client/absoluteLatest
+[surrealdb-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-surrealdb
+[surrealdb-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.SurrealDb
+[surrealdb-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.SurrealDb/
+[surrealdb-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.SurrealDb?label=nuget%20(preview)
+[surrealdb-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.SurrealDb/absoluteLatest
+[surrealdb-client-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.SurrealDb
+[surrealdb-client-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/
+[surrealdb-client-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.SurrealDb?label=nuget%20(preview)
+[surrealdb-client-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.SurrealDb/absoluteLatest
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.csproj b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.csproj
new file mode 100644
index 00000000..2c32d9d4
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.csproj
@@ -0,0 +1,19 @@
+ο»Ώ
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/Todo.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/Todo.cs
new file mode 100644
index 00000000..c4981aa7
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/Todo.cs
@@ -0,0 +1,12 @@
+ο»Ώusing SurrealDb.Net.Models;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.Models;
+
+public class Todo : Record
+{
+ internal const string Table = "todo";
+
+ public string? Title { get; set; }
+ public DateOnly? DueBy { get; set; } = null;
+ public bool IsComplete { get; set; } = false;
+}
\ No newline at end of file
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecast.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecast.cs
new file mode 100644
index 00000000..a90521be
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecast.cs
@@ -0,0 +1,36 @@
+ο»Ώusing SurrealDb.Net.Models;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.Models;
+
+///
+/// Weather forecast model.
+///
+public class WeatherForecast : Record
+{
+ internal const string Table = "weatherForecast";
+
+ ///
+ /// Date of the weather forecast.
+ ///
+ public DateTime Date { get; set; }
+
+ ///
+ /// Country of the weather forecast.
+ ///
+ public string? Country { get; set; }
+
+ ///
+ /// Temperature in Celsius.
+ ///
+ public int TemperatureC { get; set; }
+
+ ///
+ /// Temperature in Fahrenheit.
+ ///
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+
+ ///
+ /// Summary of the weather forecast.
+ ///
+ public string? Summary { get; set; }
+}
\ No newline at end of file
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecastFaker.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecastFaker.cs
new file mode 100644
index 00000000..027fbe81
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Models/WeatherForecastFaker.cs
@@ -0,0 +1,34 @@
+ο»Ώusing Bogus;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.Models;
+
+///
+/// Faker test class to generate fake WeatherForecast objects.
+///
+public class WeatherForecastFaker : Faker
+{
+ private static readonly string[] Summaries = new[]
+ {
+ "Freezing",
+ "Bracing",
+ "Chilly",
+ "Cool",
+ "Mild",
+ "Warm",
+ "Balmy",
+ "Hot",
+ "Sweltering",
+ "Scorching"
+ };
+
+ ///
+ /// Constructor
+ ///
+ public WeatherForecastFaker()
+ {
+ RuleFor(o => o.Date, f => f.Date.Recent());
+ RuleFor(o => o.Country, f => f.Address.Country());
+ RuleFor(o => o.TemperatureC, f => f.Random.Number(-20, 55));
+ RuleFor(o => o.Summary, f => f.Random.ArrayElement(Summaries));
+ }
+}
\ No newline at end of file
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Program.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Program.cs
new file mode 100644
index 00000000..0f13ace2
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Program.cs
@@ -0,0 +1,38 @@
+using CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService.Models;
+using SurrealDb.Net;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddSurrealClient("db", settings =>
+{
+ settings.Options!.NamingPolicy = "CamelCase";
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+app.MapGroup("/api")
+ .MapSurrealEndpoints(
+ "/weatherForecast",
+ new() { EnableMutations = false }
+ )
+ .MapSurrealEndpoints("/todo");
+
+app.MapPost("/init", InitializeDbAsync);
+
+app.Run();
+
+Task InitializeDbAsync(ISurrealDbClient surrealDbClient)
+{
+ const int initialCount = 5;
+ var weatherForecasts = new WeatherForecastFaker().Generate(initialCount);
+
+ var tasks = weatherForecasts.Select(weatherForecast =>
+ surrealDbClient.Create(WeatherForecast.Table, weatherForecast)
+ );
+
+ return Task.WhenAll(tasks);
+}
\ No newline at end of file
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Properties/launchSettings.json b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000..8ce06d4e
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5109",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7086;http://localhost:5109",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/appsettings.json b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost.csproj b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost.csproj
new file mode 100644
index 00000000..009a61e3
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost.csproj
@@ -0,0 +1,20 @@
+ο»Ώ
+
+
+
+ Exe
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Program.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Program.cs
new file mode 100644
index 00000000..d83ecaca
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Program.cs
@@ -0,0 +1,14 @@
+using Projects;
+
+var builder = DistributedApplication.CreateBuilder(args);
+
+var db = builder.AddSurrealServer("surreal")
+ .WithSurrealist()
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+builder.AddProject("apiservice")
+ .WithReference(db)
+ .WaitFor(db);
+
+builder.Build().Run();
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Properties/launchSettings.json b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..c65468bf
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17138;http://localhost:15162",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21257",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22098"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15162",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19149",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20034"
+ }
+ }
+ }
+}
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/appsettings.json b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/appsettings.json
new file mode 100644
index 00000000..31c092aa
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults.csproj b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults.csproj
new file mode 100644
index 00000000..c9a4399a
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults.csproj
@@ -0,0 +1,21 @@
+ο»Ώ
+
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/Extensions.cs b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/Extensions.cs
new file mode 100644
index 00000000..b34d7625
--- /dev/null
+++ b/examples/surrealdb/CommunityToolkit.Aspire.Hosting.SurrealDb.ServiceDefaults/Extensions.cs
@@ -0,0 +1,117 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks("/health");
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj
new file mode 100644
index 00000000..fb6f6c6f
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/CommunityToolkit.Aspire.Hosting.SurrealDb.csproj
@@ -0,0 +1,18 @@
+ο»Ώ
+
+
+ hosting surrealdb
+ SurrealDB support for .NET Aspire.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Shipped.txt
new file mode 100644
index 00000000..91b0e1a4
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+ο»Ώ#nullable enable
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000..1f93c2b5
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/PublicAPI.Unshipped.txt
@@ -0,0 +1,29 @@
+ο»Ώ#nullable enable
+Aspire.Hosting.ApplicationModel.SurrealDbDatabaseResource
+Aspire.Hosting.ApplicationModel.SurrealDbDatabaseResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
+Aspire.Hosting.ApplicationModel.SurrealDbDatabaseResource.DatabaseName.get -> string!
+Aspire.Hosting.ApplicationModel.SurrealDbDatabaseResource.Parent.get -> Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource!
+Aspire.Hosting.ApplicationModel.SurrealDbDatabaseResource.SurrealDbDatabaseResource(string! name, string! databaseName, Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource! parent) -> void
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource.Databases.get -> System.Collections.Generic.IReadOnlyDictionary!
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource.NamespaceName.get -> string!
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource.Parent.get -> Aspire.Hosting.ApplicationModel.SurrealDbServerResource!
+Aspire.Hosting.ApplicationModel.SurrealDbNamespaceResource.SurrealDbNamespaceResource(string! name, string! namespaceName, Aspire.Hosting.ApplicationModel.SurrealDbServerResource! parent) -> void
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression!
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.GetConnectionStringAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.Namespaces.get -> System.Collections.Generic.IReadOnlyDictionary!
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.PasswordParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource!
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.PrimaryEndpoint.get -> Aspire.Hosting.ApplicationModel.EndpointReference!
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.SurrealDbServerResource(string! name, Aspire.Hosting.ApplicationModel.ParameterResource? userName, Aspire.Hosting.ApplicationModel.ParameterResource! password) -> void
+Aspire.Hosting.ApplicationModel.SurrealDbServerResource.UserNameParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource?
+Aspire.Hosting.ApplicationModel.SurrealistContainerResource
+Aspire.Hosting.ApplicationModel.SurrealistContainerResource.SurrealistContainerResource(string! name) -> void
+Aspire.Hosting.SurrealDbBuilderExtensions
+static Aspire.Hosting.SurrealDbBuilderExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, string? databaseName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.SurrealDbBuilderExtensions.AddNamespace(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, string? namespaceName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.SurrealDbBuilderExtensions.AddSurrealServer(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder? userName = null, Aspire.Hosting.ApplicationModel.IResourceBuilder? password = null, int? port = null, string! path = "memory", bool strictMode = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.SurrealDbBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.SurrealDbBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
+static Aspire.Hosting.SurrealDbBuilderExtensions.WithSurrealist(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Action!>? configureContainer = null, string? containerName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/README.md b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/README.md
new file mode 100644
index 00000000..8eb99b95
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/README.md
@@ -0,0 +1,35 @@
+# CommunityToolkit.Aspire.Hosting.SurrealDb library
+
+Provides extension methods and resource definitions for the .NET Aspire AppHost to support running [SurrealDB](https://surrealdb.com/) containers.
+
+## Getting started
+
+### Install the package
+
+In your AppHost project, install the .NET Aspire SurrealDB Hosting library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.SurrealDb
+```
+
+## Usage example
+
+Then, in the _Program.cs_ file of `AppHost`, add a SurrealDB resource and consume the connection using the following methods:
+
+```csharp
+var db = builder.AddSurrealServer("surreal")
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+var myService = builder.AddProject()
+ .WithReference(db);
+```
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-surrealdb
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs
new file mode 100644
index 00000000..44d43df4
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbBuilderExtensions.cs
@@ -0,0 +1,386 @@
+ο»Ώ// 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 Microsoft.Extensions.DependencyInjection;
+using SurrealDb.Net;
+using System.Text.Json;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding SurrealDB resources to the application model.
+///
+public static class SurrealDbBuilderExtensions
+{
+ private const int SurrealDbPort = 8000;
+ private const string UserEnvVarName = "SURREAL_USER";
+ private const string PasswordEnvVarName = "SURREAL_PASS";
+
+ ///
+ /// Adds a SurrealDB resource to the application model. A container is used for local development.
+ /// The default image is and the tag is .
+ ///
+ /// 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 administrator username for the SurrealDB resource.
+ /// The parameter used to provide the administrator password for the SurrealDB resource. If a random password will be generated.
+ /// The host port for the SurrealDB instance.
+ /// Sets the path for storing data. If no argument is given, the default of memory for non-persistent storage in memory is assumed.
+ /// Whether strict mode is enabled on the server.
+ /// A reference to the .
+ ///
+ ///
+ /// Add a SurrealDB container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var db = builder.AddSurrealServer("surreal")
+ /// .AddNamespace("ns")
+ /// .AddDatabase("db");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddSurrealServer(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder? userName = null,
+ IResourceBuilder? password = null,
+ int? port = null,
+ string path = "memory",
+ bool strictMode = false
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ var args = new List
+ {
+ "start",
+ path
+ };
+ if (strictMode)
+ {
+ args.Add("--strict");
+ }
+
+ // The password must be at least 8 characters long and contain characters from three of the following four sets: Uppercase letters, Lowercase letters, Base 10 digits, and Symbols
+ var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", minLower: 1, minUpper: 1, minNumeric: 1);
+
+ var surrealServer = new SurrealDbServerResource(name, userName?.Resource, passwordParameter);
+ return builder.AddResource(surrealServer)
+ .WithEndpoint(port: port, targetPort: SurrealDbPort, name: SurrealDbServerResource.PrimaryEndpointName)
+ .WithImage(SurrealDbContainerImageTags.Image, SurrealDbContainerImageTags.Tag)
+ .WithImageRegistry(SurrealDbContainerImageTags.Registry)
+ .WithEnvironment(context =>
+ {
+ context.EnvironmentVariables[UserEnvVarName] = surrealServer.UserNameReference;
+ context.EnvironmentVariables[PasswordEnvVarName] = surrealServer.PasswordParameter;
+ })
+ .WithEntrypoint("/surreal")
+ .WithArgs([.. args]);
+ }
+
+ ///
+ /// Adds a SurrealDB namespace to the application model. This is a child resource of a .
+ ///
+ /// The SurrealDB resource builders.
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The name of the namespace. If not provided, this defaults to the same value as .
+ /// A reference to the .
+ ///
+ ///
+ /// Add a SurrealDB container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var db = builder.AddSurrealServer("surreal")
+ /// .AddNamespace("ns")
+ /// .AddDatabase("db");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddNamespace(
+ this IResourceBuilder builder,
+ [ResourceName] string name,
+ string? namespaceName = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ // Use the resource name as the namespace name if it's not provided
+ namespaceName ??= name;
+
+ builder.Resource.AddNamespace(name, namespaceName);
+ var surrealServerNamespace = new SurrealDbNamespaceResource(name, namespaceName, builder.Resource);
+ return builder.ApplicationBuilder.AddResource(surrealServerNamespace);
+ }
+
+ ///
+ /// Adds a SurrealDB database to the application model. This is a child resource of a .
+ ///
+ /// The SurrealDB resource builders.
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The name of the database. If not provided, this defaults to the same value as .
+ /// A reference to the .
+ ///
+ ///
+ /// Add a SurrealDB container to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var db = builder.AddSurrealServer("surreal")
+ /// .AddNamespace("ns")
+ /// .AddDatabase("db");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddDatabase(
+ this IResourceBuilder builder,
+ [ResourceName] string name,
+ string? databaseName = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ // Use the resource name as the database name if it's not provided
+ databaseName ??= name;
+
+ builder.Resource.AddDatabase(name, databaseName);
+ var surrealServerDatabase = new SurrealDbDatabaseResource(name, databaseName, builder.Resource);
+
+ SurrealDbClient? surrealDbClient = null;
+
+ builder.ApplicationBuilder.Eventing.Subscribe(surrealServerDatabase, async (@event, ct) =>
+ {
+ var connectionString = await surrealServerDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
+ if (connectionString is null)
+ {
+ throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{surrealServerDatabase}' resource but the connection string was null.");
+ }
+
+ var options = new SurrealDbOptionsBuilder().FromConnectionString(connectionString).Build();
+ surrealDbClient = new SurrealDbClient(options);
+ });
+
+ string namespaceName = builder.Resource.Name;
+ string serverName = builder.Resource.Parent.Name;
+
+ string healthCheckKey = $"{serverName}_{namespaceName}_{name}_check";
+ builder.ApplicationBuilder.Services.AddHealthChecks().AddSurreal(_ => surrealDbClient!, healthCheckKey);
+
+ return builder.ApplicationBuilder.AddResource(surrealServerDatabase)
+ .WithHealthCheck(healthCheckKey);
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a SurrealDB resource.
+ ///
+ /// The resource builder.
+ /// The name of the volume. Defaults to an auto-generated name based on the application and resource names.
+ /// The .
+ ///
+ ///
+ /// Add a SurrealDB 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 db = builder.AddSurrealServer("surreal")
+ /// .WithDataVolume()
+ /// .AddNamespace("ns")
+ /// .AddDatabase("db");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string? name = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/data");
+ }
+
+ ///
+ /// Adds a bind mount for the data folder to a SurrealDB resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// The .
+ ///
+ ///
+ /// Add a SurrealDB 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 db = builder.AddSurrealServer("surreal")
+ /// .WithDataBindMount("./data/surreal/data")
+ /// .AddNamespace("ns")
+ /// .AddDatabase("db");
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(source);
+
+ return builder.WithBindMount(source, "/data");
+ }
+
+ ///
+ /// Adds a Surrealist UI instance for SurrealDB to the application model.
+ /// The default image is and the tag is .
+ ///
+ /// The SurrealDB server resource builder.
+ /// Callback to configure Surrealist container resource.
+ /// The name of the container (optional).
+ /// A reference to the .
+ public static IResourceBuilder WithSurrealist(
+ this IResourceBuilder builder,
+ Action>? configureContainer = null,
+ string? containerName = null
+ )
+ where T : SurrealDbServerResource
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ if (builder.ApplicationBuilder.Resources.OfType().SingleOrDefault() is { } existingSurrealistResource)
+ {
+ var builderForExistingResource = builder.ApplicationBuilder.CreateResourceBuilder(existingSurrealistResource);
+ configureContainer?.Invoke(builderForExistingResource);
+
+ return builder;
+ }
+
+ containerName ??= $"{builder.Resource.Name}-surrealist";
+
+ const string CONNECTIONS_FILE_PATH = "/usr/share/nginx/html/instance.json";
+
+ var surrealistContainer = new SurrealistContainerResource(containerName);
+ var surrealistContainerBuilder = builder.ApplicationBuilder.AddResource(surrealistContainer)
+ .WithImage(SurrealDbContainerImageTags.SurrealistImage, SurrealDbContainerImageTags.SurrealistTag)
+ .WithImageRegistry(SurrealDbContainerImageTags.SurrealistRegistry)
+ .WithHttpEndpoint(targetPort: 8080, name: "http")
+ .WithBindMount(Path.GetTempFileName(), CONNECTIONS_FILE_PATH)
+ .WithRelationship(builder.Resource, "Surrealist")
+ .ExcludeFromManifest();
+
+ builder.ApplicationBuilder.Eventing.Subscribe((e, ct) =>
+ {
+ var serverFileMount = surrealistContainer.Annotations.OfType().Single(v => v.Target == CONNECTIONS_FILE_PATH);
+ var surrealDbServerResources = builder.ApplicationBuilder.Resources.OfType().ToList();
+
+ using var stream = new FileStream(serverFileMount.Source!, FileMode.Create);
+ using var writer = new Utf8JsonWriter(stream);
+
+ // Need to grant read access to the config file on unix like systems.
+ if (!OperatingSystem.IsWindows())
+ {
+ File.SetUnixFileMode(serverFileMount.Source!, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
+ }
+
+ writer.WriteStartObject();
+
+ writer.WriteStartArray("connections");
+
+ var surrealDbNamespaceResources = builder.ApplicationBuilder.Resources.OfType().ToList();
+ var surrealDbDatabaseResources = builder.ApplicationBuilder.Resources.OfType().ToList();
+
+ foreach (var surrealInstance in surrealDbServerResources)
+ {
+ if (surrealInstance.PrimaryEndpoint.IsAllocated)
+ {
+ SurrealDbNamespaceResource? uniqueNamespace = null;
+ SurrealDbDatabaseResource? uniqueDatabase = null;
+
+ var serverNamespaces = surrealDbNamespaceResources
+ .Where(ns => ns.Parent == surrealInstance)
+ .ToList();
+
+ if (serverNamespaces.Count == 1)
+ {
+ uniqueNamespace = serverNamespaces.First();
+
+ var nsDatabases = surrealDbDatabaseResources
+ .Where(db => db.Parent == uniqueNamespace)
+ .ToList();
+
+ if (nsDatabases.Count == 1)
+ {
+ uniqueDatabase = nsDatabases.First();
+ }
+ }
+
+ var endpoint = surrealInstance.PrimaryEndpoint;
+
+ writer.WriteStartObject();
+
+ writer.WriteString("id", surrealInstance.Name);
+ writer.WriteString("name", surrealInstance.Name);
+
+ if (uniqueNamespace is not null)
+ {
+ writer.WriteString("defaultNamespace", uniqueNamespace.NamespaceName);
+ }
+ if (uniqueDatabase is not null)
+ {
+ writer.WriteString("defaultDatabase", uniqueDatabase.DatabaseName);
+ }
+
+ writer.WriteStartObject("authentication");
+ writer.WriteString("protocol", "ws");
+ // How to do host resolution?
+ writer.WriteString("hostname", $"localhost:{endpoint.Port}");
+ writer.WriteString("mode", "root");
+ if (uniqueNamespace is not null)
+ {
+ writer.WriteString("namespace", uniqueNamespace.NamespaceName);
+ }
+ if (uniqueDatabase is not null)
+ {
+ writer.WriteString("database", uniqueDatabase.DatabaseName);
+ }
+
+ writer.WriteEndObject();
+
+ writer.WriteEndObject();
+ }
+ }
+
+ writer.WriteEndArray();
+
+ writer.WriteEndObject();
+
+ return Task.CompletedTask;
+ });
+
+ configureContainer?.Invoke(surrealistContainerBuilder);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbContainerImageTags.cs
new file mode 100644
index 00000000..649cabc0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbContainerImageTags.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.
+
+namespace Aspire.Hosting;
+
+internal sealed class SurrealDbContainerImageTags
+{
+ /// docker.io
+ public const string Registry = "docker.io";
+ /// surrealdb/surrealdb
+ public const string Image = "surrealdb/surrealdb";
+ /// v2.2
+ public const string Tag = "v2.2";
+
+ /// docker.io
+ public const string SurrealistRegistry = "docker.io";
+ /// surrealdb/surrealist
+ public const string SurrealistImage = "surrealdb/surrealist";
+ /// 3.3.2
+ public const string SurrealistTag = "3.3.2";
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbDatabaseResource.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbDatabaseResource.cs
new file mode 100644
index 00000000..247a714b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbDatabaseResource.cs
@@ -0,0 +1,45 @@
+ο»Ώ// 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 SurrealDB database that is a child of a SurrealDB namespace resource.
+///
+public class SurrealDbDatabaseResource : Resource, IResourceWithParent, IResourceWithConnectionString
+{
+ ///
+ /// Gets the parent SurrealDB namespace resource.
+ ///
+ public SurrealDbNamespaceResource Parent { get; }
+
+ ///
+ /// Gets the connection string expression for the SurrealDB database.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"{Parent};Database={DatabaseName}");
+
+ ///
+ /// Gets the database name.
+ ///
+ public string DatabaseName { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the resource.
+ /// The database name.
+ /// The parent SurrealDB namespace resource.
+ public SurrealDbDatabaseResource(
+ [ResourceName] string name,
+ string databaseName,
+ SurrealDbNamespaceResource parent
+ ) : base(name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(databaseName);
+ ArgumentNullException.ThrowIfNull(parent);
+
+ DatabaseName = databaseName;
+ Parent = parent;
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbNamespaceResource.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbNamespaceResource.cs
new file mode 100644
index 00000000..22305347
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbNamespaceResource.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 SurrealDB namespace that is a child of a SurrealDB container resource.
+///
+public class SurrealDbNamespaceResource : Resource, IResourceWithParent, IResourceWithConnectionString
+{
+ ///
+ /// Gets the parent SurrealDB container resource.
+ ///
+ public SurrealDbServerResource Parent { get; }
+
+ ///
+ /// Gets the connection string expression for the SurrealDB database.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"{Parent};Namespace={NamespaceName}");
+
+ ///
+ /// Gets the namespace name.
+ ///
+ public string NamespaceName { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the resource.
+ /// The namespace name.
+ /// The parent SurrealDB server resource.
+ public SurrealDbNamespaceResource(
+ [ResourceName] string name,
+ string namespaceName,
+ SurrealDbServerResource parent
+ ) : base(name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(namespaceName);
+ ArgumentNullException.ThrowIfNull(parent);
+
+ NamespaceName = namespaceName;
+ Parent = parent;
+ }
+
+ private readonly Dictionary _databases = new(StringComparer.Ordinal);
+
+ ///
+ /// A dictionary where the key is the resource name and the value is the database name.
+ ///
+ public IReadOnlyDictionary Databases => _databases;
+
+ internal void AddDatabase(string name, string databaseName)
+ {
+ _databases.TryAdd(name, databaseName);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbServerResource.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbServerResource.cs
new file mode 100644
index 00000000..a351fb0a
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealDbServerResource.cs
@@ -0,0 +1,101 @@
+ο»Ώ// 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 SurrealDB container.
+///
+public class SurrealDbServerResource : ContainerResource, IResourceWithConnectionString
+{
+ internal const string PrimaryEndpointName = "tcp";
+
+ private const string DefaultUserName = "root";
+ private const string SchemeUri = "ws";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the resource.
+ /// A parameter that contains the SurrealDB username.
+ /// A parameter that contains the SurrealDB password.
+ public SurrealDbServerResource(
+ [ResourceName] string name,
+ ParameterResource? userName,
+ ParameterResource password
+ ) : base(name)
+ {
+ ArgumentNullException.ThrowIfNull(password);
+
+ PrimaryEndpoint = new(this, PrimaryEndpointName);
+ UserNameParameter = userName;
+ PasswordParameter = password;
+ }
+
+ ///
+ /// Gets the primary endpoint for the SurrealDB instance.
+ ///
+ public EndpointReference PrimaryEndpoint { get; }
+
+ ///
+ /// Gets the parameter that contains the SurrealDB username.
+ ///
+ public ParameterResource? UserNameParameter { get; }
+
+ internal ReferenceExpression UserNameReference =>
+ UserNameParameter is not null ?
+ ReferenceExpression.Create($"{UserNameParameter}") :
+ ReferenceExpression.Create($"{DefaultUserName}");
+
+ ///
+ /// Gets the parameter that contains the SurrealDB password.
+ ///
+ public ParameterResource PasswordParameter { get; }
+
+ private ReferenceExpression ConnectionString =>
+ ReferenceExpression.Create(
+ $"Server={SchemeUri}://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}/rpc;User={UserNameReference};Password={PasswordParameter}");
+
+ ///
+ /// Gets the connection string expression for the SurrealDB instance.
+ ///
+ public ReferenceExpression ConnectionStringExpression
+ {
+ get
+ {
+ if (this.TryGetLastAnnotation(out var connectionStringAnnotation))
+ {
+ return connectionStringAnnotation.Resource.ConnectionStringExpression;
+ }
+
+ return ConnectionString;
+ }
+ }
+
+ ///
+ /// Gets the connection string for the SurrealDB instance.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A connection string for the SurrealDB instance in the form "Server=scheme://host:port;User=username;Password=password".
+ public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default)
+ {
+ if (this.TryGetLastAnnotation(out var connectionStringAnnotation))
+ {
+ return connectionStringAnnotation.Resource.GetConnectionStringAsync(cancellationToken);
+ }
+
+ return ConnectionString.GetValueAsync(cancellationToken);
+ }
+
+ private readonly Dictionary _namespaces = new(StringComparer.Ordinal);
+
+ ///
+ /// A dictionary where the key is the resource name and the value is the namespace name.
+ ///
+ public IReadOnlyDictionary Namespaces => _namespaces;
+
+ internal void AddNamespace(string name, string namespaceName)
+ {
+ _namespaces.TryAdd(name, namespaceName);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealistContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealistContainerResource.cs
new file mode 100644
index 00000000..76412425
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.SurrealDb/SurrealistContainerResource.cs
@@ -0,0 +1,14 @@
+ο»Ώusing System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Represents a container resource for Surrealist.
+///
+/// The name of the container resource.
+public sealed class SurrealistContainerResource(string name) : ContainerResource(ThrowIfNull(name))
+{
+ private static string ThrowIfNull([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
+ => argument ?? throw new ArgumentNullException(paramName);
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/AspireSurrealDbExtensions.cs b/src/CommunityToolkit.Aspire.SurrealDb/AspireSurrealDbExtensions.cs
new file mode 100644
index 00000000..c1821ae2
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/AspireSurrealDbExtensions.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;
+using CommunityToolkit.Aspire.SurrealDb;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using SurrealDb.Net;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for registering SurrealDB-related services in an .
+///
+public static class AspireSurrealDbExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Surreal:Client";
+
+ ///
+ /// Registers in the services provided by 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:Surreal:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddSurrealClient(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(connectionName);
+ AddSurrealClient(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+ }
+
+ ///
+ /// Registers as a keyed service for the given in the services provided by 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:Surreal:Client" section.
+ /// If required ConnectionString is not provided in configuration section
+ public static void AddKeyedSurrealClient(
+ this IHostApplicationBuilder builder,
+ string name,
+ Action? configureSettings = null
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNullOrEmpty(name);
+ AddSurrealClient(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddSurrealClient(
+ this IHostApplicationBuilder builder,
+ string configurationSectionName,
+ Action? configureSettings,
+ string connectionName,
+ string? serviceKey
+ )
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ var settings = new SurrealDbClientSettings();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.Options = SurrealDbOptions.Create().FromConnectionString(connectionString).Build();
+ }
+
+ configureSettings?.Invoke(settings);
+
+ if (settings.Options is null)
+ {
+ // Ensures that when the connection information is missing, an exception isn't thrown before the host
+ // is built, so any exception can be logged with ILogger.
+ return;
+ }
+
+ if (serviceKey is null)
+ {
+ builder.Services.AddSurreal(settings.Options, settings.Lifetime);
+ }
+ else
+ {
+ builder.Services.AddKeyedSurreal(serviceKey, settings.Options, settings.Lifetime);
+ }
+
+ if (!settings.DisableHealthChecks)
+ {
+ string healthCheckName = serviceKey is null ? "surrealdb" : $"surrealdb_{connectionName}";
+
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ healthCheckName,
+ sp => new SurrealDbHealthCheck(serviceKey is null ?
+ sp.GetRequiredService() :
+ sp.GetRequiredKeyedService(serviceKey)),
+ failureStatus: null,
+ tags: null,
+ timeout: settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null
+ )
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/CommunityToolkit.Aspire.SurrealDb.csproj b/src/CommunityToolkit.Aspire.SurrealDb/CommunityToolkit.Aspire.SurrealDb.csproj
new file mode 100644
index 00000000..441df04b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/CommunityToolkit.Aspire.SurrealDb.csproj
@@ -0,0 +1,20 @@
+ο»Ώ
+
+
+ surrealdb client
+ A SurrealDB client that integrates with Aspire, including health checks, logging, and telemetry.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Shipped.txt
new file mode 100644
index 00000000..91b0e1a4
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Shipped.txt
@@ -0,0 +1 @@
+ο»Ώ#nullable enable
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Unshipped.txt
new file mode 100644
index 00000000..e76c8bc0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/PublicAPI.Unshipped.txt
@@ -0,0 +1,14 @@
+ο»Ώ#nullable enable
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.DisableHealthChecks.get -> bool
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.DisableHealthChecks.set -> void
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.HealthCheckTimeout.get -> int?
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.HealthCheckTimeout.set -> void
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.Lifetime.get -> Microsoft.Extensions.DependencyInjection.ServiceLifetime
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.Lifetime.set -> void
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.Options.get -> Microsoft.Extensions.DependencyInjection.SurrealDbOptions?
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.Options.set -> void
+CommunityToolkit.Aspire.SurrealDb.SurrealDbClientSettings.SurrealDbClientSettings() -> void
+Microsoft.Extensions.Hosting.AspireSurrealDbExtensions
+static Microsoft.Extensions.Hosting.AspireSurrealDbExtensions.AddKeyedSurrealClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! name, System.Action? configureSettings = null) -> void
+static Microsoft.Extensions.Hosting.AspireSurrealDbExtensions.AddSurrealClient(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, string! connectionName, System.Action? configureSettings = null) -> void
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/README.md b/src/CommunityToolkit.Aspire.SurrealDb/README.md
new file mode 100644
index 00000000..ad6c687b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/README.md
@@ -0,0 +1,121 @@
+# CommunityToolkit.Aspire.SurrealDb
+
+Registers a [SurrealDbClient](https://github.com/surrealdb/surrealdb.net) in the DI container for connecting to a SurrealDB instance.
+
+## Getting started
+
+### Prerequisites
+
+- SurrealDB cluster.
+
+### Install the package
+
+Install the .NET Aspire SurrealDB Client library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.SurrealDb
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddSurrealClient` extension method to register a `SurrealDbClient` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddSurrealClient("surreal");
+```
+
+## Configuration
+
+The .NET Aspire SurrealDB Client integration 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.AddSurrealClient()`:
+
+```csharp
+builder.AddSurrealClient("surreal");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "surreal": "Server=ws://127.0.0.1:8000/rpc;Namespace=test;Database=test;Username=root;Password=root"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire SurrealDB Client integration supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `SurrealDbClientSettings` from configuration by using the `Aspire:Surreal:Client` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "Surreal": {
+ "Client": {
+ "Options": {
+ "Endpoint": "ws://127.0.0.1:8000/rpc",
+ "Namespace": "test",
+ "Database": "test",
+ "Username": "root",
+ "Password": "root"
+ }
+ }
+ }
+ }
+}
+```
+
+### 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.AddSurrealDbClient("surreal", settings => settings.Options.Endpoint = "ws://localhost:8000/rpc");
+```
+
+## AppHost extensions
+
+In your AppHost project, install the `CommunityToolkit.Aspire.Hosting.SurrealDb` library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.SurrealDb
+```
+
+Then, in the _Program.cs_ file of `AppHost`, register a SurrealDB cluster and consume the connection using the following methods:
+
+```csharp
+var db = builder.AddSurrealServer("surreal")
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+var myService = builder.AddProject()
+ .WithReference(db);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `db`. In the _Program.cs_ file of `MyService`, the SurrealDB connection can be consumed using:
+
+```csharp
+builder.AddSurrealClient("db");
+```
+
+Then, in your service, inject `SurrealDbClient` and use it to interact with the SurrealDB instance:
+
+```csharp
+public class MyService(SurrealDbClient client)
+{
+ // ...
+}
+```
+
+## Additional documentation
+
+- https://github.com/surrealdb/surrealdb.net
+- https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-surrealdb
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
+
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbClientSettings.cs b/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbClientSettings.cs
new file mode 100644
index 00000000..02c07719
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbClientSettings.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 Microsoft.Extensions.DependencyInjection;
+using SurrealDb.Net;
+
+namespace CommunityToolkit.Aspire.SurrealDb;
+
+///
+/// Provides the client configuration settings for connecting to a SurrealDB server using .
+///
+public sealed class SurrealDbClientSettings
+{
+ ///
+ /// The defined options used to connect to the SurrealDB server.
+ ///
+ public SurrealDbOptions? Options { get; set; }
+
+ ///
+ /// Gets or sets the Service lifetime to register services under.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the SurrealDB health check is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a integer value that indicates the SurrealDB health check timeout in milliseconds.
+ ///
+ public int? HealthCheckTimeout { get; set; }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbHealthCheck.cs b/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbHealthCheck.cs
new file mode 100644
index 00000000..963d50f0
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.SurrealDb/SurrealDbHealthCheck.cs
@@ -0,0 +1,35 @@
+ο»Ώ// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using SurrealDb.Net;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+
+namespace CommunityToolkit.Aspire.SurrealDb;
+
+internal sealed class SurrealDbHealthCheck : IHealthCheck
+{
+ private readonly ISurrealDbClient _surrealdbClient;
+
+ public SurrealDbHealthCheck(ISurrealDbClient surrealdbClient)
+ {
+ ArgumentNullException.ThrowIfNull(surrealdbClient, nameof(surrealdbClient));
+ _surrealdbClient = surrealdbClient;
+ }
+
+ ///
+ public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ bool isHealthy = await _surrealdbClient.Health(cancellationToken).ConfigureAwait(false);
+
+ return isHealthy
+ ? HealthCheckResult.Healthy()
+ : new HealthCheckResult(context.Registration.FailureStatus);
+ }
+ catch (Exception ex)
+ {
+ return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
+ }
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AddSurrealServerTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AddSurrealServerTests.cs
new file mode 100644
index 00000000..e07e40dc
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AddSurrealServerTests.cs
@@ -0,0 +1,183 @@
+ο»Ώ// 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;
+using Aspire.Hosting.Utils;
+using System.Net.Sockets;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.Tests;
+
+public class AddSurrealServerTests
+{
+ [Fact]
+ public void AddSurrealServerAddsGeneratedPasswordParameterWithUserSecretsParameterDefaultInRunMode()
+ {
+ using var appBuilder = TestDistributedApplicationBuilder.Create();
+
+ var surrealServer = appBuilder.AddSurrealServer("surreal");
+
+ Assert.Equal("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", surrealServer.Resource.PasswordParameter.Default?.GetType().FullName);
+ }
+
+ [Fact]
+ public void AddSurrealServerDoesNotAddGeneratedPasswordParameterWithUserSecretsParameterDefaultInPublishMode()
+ {
+ using var appBuilder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var surrealServer = appBuilder.AddSurrealServer("surreal");
+
+ Assert.NotEqual("Aspire.Hosting.ApplicationModel.UserSecretsParameterDefault", surrealServer.Resource.PasswordParameter.Default?.GetType().FullName);
+ }
+
+ [Fact]
+ public async Task AddSurrealServerContainerWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ var surrealServer = appBuilder.AddSurrealServer("surreal");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("surreal", containerResource.Name);
+
+ var endpoint = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(8000, endpoint.TargetPort);
+ Assert.False(endpoint.IsExternal);
+ Assert.Equal("tcp", endpoint.Name);
+ Assert.Null(endpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
+ Assert.Equal("tcp", endpoint.Transport);
+ Assert.Equal("tcp", endpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(SurrealDbContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(SurrealDbContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(SurrealDbContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await surrealServer.Resource.GetEnvironmentVariableValuesAsync();
+
+ Assert.Collection(config,
+ env =>
+ {
+ Assert.Equal("SURREAL_USER", env.Key);
+ Assert.NotNull(env.Value);
+ },
+ env =>
+ {
+ Assert.Equal("SURREAL_PASS", env.Key);
+ Assert.NotNull(env.Value);
+ Assert.True(env.Value.Length >= 8);
+ });
+ }
+
+ [Fact]
+ public async Task SurrealServerCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.Configuration["Parameters:pass"] = "p@ssw0rd1";
+
+ var pass = appBuilder.AddParameter("pass");
+ appBuilder
+ .AddSurrealServer("surreal", null, pass)
+ .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 8000));
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType());
+ var connectionString = await connectionStringResource.GetConnectionStringAsync(default);
+
+ Assert.Equal("Server=ws://localhost:8000/rpc;User=root;Password=p@ssw0rd1", connectionString);
+ Assert.Equal("Server=ws://{surreal.bindings.tcp.host}:{surreal.bindings.tcp.port}/rpc;User=root;Password={pass.value}", connectionStringResource.ConnectionStringExpression.ValueExpression);
+ }
+
+ [Fact]
+ public async Task SurrealServerDatabaseCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.Configuration["Parameters:pass"] = "p@ssw0rd1";
+
+ var pass = appBuilder.AddParameter("pass");
+ appBuilder
+ .AddSurrealServer("surreal", null, pass)
+ .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 8000))
+ .AddNamespace("ns", "myns")
+ .AddDatabase("db", "mydb");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var surrealResource = Assert.Single(appModel.Resources.OfType());
+ var connectionStringResource = (IResourceWithConnectionString)surrealResource;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ Assert.Equal("Server=ws://localhost:8000/rpc;User=root;Password=p@ssw0rd1;Namespace=myns;Database=mydb", connectionString);
+ Assert.Equal("{ns.connectionString};Database=mydb", connectionStringResource.ConnectionStringExpression.ValueExpression);
+ }
+
+ [Fact]
+ public void ThrowsWithIdenticalChildResourceNames()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ var db = builder.AddSurrealServer("surreal1");
+ db.AddNamespace("ns").AddDatabase("db");
+
+ Assert.Throws(() => db.AddNamespace("ns").AddDatabase("db"));
+ }
+
+ [Fact]
+ public void ThrowsWithIdenticalChildResourceNamesDifferentParents()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ builder.AddSurrealServer("surreal1")
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+ var db = builder.AddSurrealServer("surreal2");
+ Assert.Throws(() => db.AddNamespace("ns").AddDatabase("db"));
+ }
+
+ [Fact]
+ public void CanAddDatabasesWithDifferentNamesOnSingleServerAndNamespace()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ var surrealNs = builder.AddSurrealServer("surreal1").AddNamespace("ns");
+
+ var db1 = surrealNs.AddDatabase("db1", "customers1");
+ var db2 = surrealNs.AddDatabase("db2", "customers2");
+
+ Assert.Equal("customers1", db1.Resource.DatabaseName);
+ Assert.Equal("customers2", db2.Resource.DatabaseName);
+
+ Assert.Equal("{ns.connectionString};Database=customers1", db1.Resource.ConnectionStringExpression.ValueExpression);
+ Assert.Equal("{ns.connectionString};Database=customers2", db2.Resource.ConnectionStringExpression.ValueExpression);
+ }
+
+ [Fact]
+ public void CanAddDatabasesWithTheSameNameOnMultipleServers()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create();
+
+ var db1 = builder.AddSurrealServer("surreal1")
+ .AddNamespace("ns1", "ns")
+ .AddDatabase("db1", "imports");
+
+ var db2 = builder.AddSurrealServer("surreal2")
+ .AddNamespace("ns2", "ns")
+ .AddDatabase("db2", "imports");
+
+ Assert.Equal("imports", db1.Resource.DatabaseName);
+ Assert.Equal("imports", db2.Resource.DatabaseName);
+
+ Assert.Equal("{ns1.connectionString};Database=imports", db1.Resource.ConnectionStringExpression.ValueExpression);
+ Assert.Equal("{ns2.connectionString};Database=imports", db2.Resource.ConnectionStringExpression.ValueExpression);
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AppHostTests.cs
new file mode 100644
index 00000000..2861ae22
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/AppHostTests.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 CommunityToolkit.Aspire.Testing;
+using Aspire.Components.Common.Tests;
+using System.Net.Http.Json;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.Tests;
+
+[RequiresDocker]
+public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture>
+{
+ [Fact]
+ public async Task SurrealResourceStartsAndRespondsOk()
+ {
+ const string resourceName = "surreal";
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1));
+
+ var tcpUri = fixture.GetEndpoint(resourceName, "tcp");
+ var baseUri = new Uri(tcpUri.AbsoluteUri.Replace("tcp://", "http://"));
+ var httpClient = new HttpClient();
+ httpClient.BaseAddress = baseUri;
+
+ var response = await httpClient.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task ApiServiceStartsAndRespondsOk()
+ {
+ const string resourceName = "apiservice";
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(1));
+ var httpClient = fixture.CreateHttpClient(resourceName);
+
+ var todoResponse = await httpClient.GetAsync("/api/todo");
+ Assert.Equal(HttpStatusCode.OK, todoResponse.StatusCode);
+
+ var initResponse = await httpClient.PostAsync("/init", null);
+ Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);
+
+ var weatherForecastResponse = await httpClient.GetAsync("/api/weatherForecast");
+ Assert.Equal(HttpStatusCode.OK, weatherForecastResponse.StatusCode);
+
+ var data = await weatherForecastResponse.Content.ReadFromJsonAsync>();
+
+ Assert.NotNull(data);
+ Assert.NotEmpty(data);
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj
new file mode 100644
index 00000000..43016c58
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests.csproj
@@ -0,0 +1,22 @@
+ο»Ώ
+
+
+ CommunityToolkit.Aspire.Hosting.SurrealDb.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/PasswordConstantDefault.cs b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/PasswordConstantDefault.cs
new file mode 100644
index 00000000..7700939c
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/PasswordConstantDefault.cs
@@ -0,0 +1,15 @@
+using Aspire.Hosting.Publishing;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.Tests;
+
+internal sealed class PasswordConstantDefault : ParameterDefault
+{
+ public override void WriteToManifest(ManifestPublishingContext context)
+ {
+ }
+
+ public override string GetDefaultValue()
+ {
+ return "password";
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbFunctionalTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbFunctionalTests.cs
new file mode 100644
index 00000000..5be12b13
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbFunctionalTests.cs
@@ -0,0 +1,279 @@
+ο»Ώ// 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;
+using Aspire.Hosting.Utils;
+using Bogus;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using SurrealDb.Net;
+using Xunit.Abstractions;
+using SurrealRecord = SurrealDb.Net.Models.Record;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.Tests;
+
+[RequiresDocker]
+public class SurrealDbFunctionalTests(ITestOutputHelper testOutputHelper)
+{
+ private const int _generatedTodoCount = 10;
+ private static readonly Todo[] _todoList;
+
+ static SurrealDbFunctionalTests()
+ {
+ _todoList = [.. new TodoFaker().Generate(_generatedTodoCount)];
+
+ int index = 0;
+ foreach (var todo in _todoList)
+ {
+ todo.Id = (Todo.Table, (++index).ToString());
+ }
+ }
+
+ [Fact]
+ public async Task VerifySurrealDbResource()
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+ var ct = cts.Token;
+ using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var surrealServer = builder.AddSurrealServer("surreal");
+
+ var db = surrealServer
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+ using var app = builder.Build();
+
+ await app.StartAsync();
+
+ await app.ResourceNotifications.WaitForResourceHealthyAsync(surrealServer.Resource.Name, ct);
+
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration[$"ConnectionStrings:{db.Resource.Name}"] = await db.Resource.ConnectionStringExpression.GetValueAsync(default);
+
+ hb.AddSurrealClient(db.Resource.Name);
+
+ using var host = hb.Build();
+
+ await host.StartAsync(ct);
+
+ var surrealDbClient = host.Services.GetRequiredService();
+
+ await CreateTestData(surrealDbClient, ct);
+ await AssertTestData(surrealDbClient, ct);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
+
+ string? volumeName = null;
+ string? bindMountPath = null;
+
+ try
+ {
+ using var builder1 = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var password1 = builder1.AddParameter("surreal-password", secret: true);
+ password1.Resource.Default = new PasswordConstantDefault();
+
+ var surrealServer1 = builder1.AddSurrealServer("surreal", path: "rocksdb://data/db.db", password: password1);
+
+ var db1 = surrealServer1
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+ if (useVolume)
+ {
+ // Use a deterministic volume name to prevent them from exhausting the machines if deletion fails
+ volumeName = VolumeNameGenerator.Generate(surrealServer1, nameof(WithDataShouldPersistStateBetweenUsages));
+
+ // if the volume already exists (because of a crashing previous run), delete it
+ DockerUtils.AttemptDeleteDockerVolume(volumeName, throwOnFailure: true);
+ surrealServer1.WithDataVolume(volumeName);
+ }
+ else
+ {
+ bindMountPath = Directory.CreateTempSubdirectory().FullName;
+ surrealServer1.WithDataBindMount(bindMountPath);
+
+ if (!OperatingSystem.IsWindows())
+ {
+ File.SetUnixFileMode(bindMountPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute);
+ }
+ }
+
+ using (var app = builder1.Build())
+ {
+ await app.StartAsync(cts.Token);
+
+ await app.ResourceNotifications.WaitForResourceHealthyAsync(surrealServer1.Resource.Name, cts.Token);
+
+ try
+ {
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration[$"ConnectionStrings:{db1.Resource.Name}"] = await db1.Resource.ConnectionStringExpression.GetValueAsync(cts.Token);
+
+ hb.AddSurrealClient(db1.Resource.Name);
+
+ using var host = hb.Build();
+ await host.StartAsync(cts.Token);
+
+ await using var surrealDbClient = host.Services.GetRequiredService();
+ await CreateTestData(surrealDbClient, cts.Token);
+ await AssertTestData(surrealDbClient, cts.Token);
+ }
+ finally
+ {
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync(cts.Token);
+ }
+ }
+
+ using var builder2 = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var password2 = builder2.AddParameter("surreal-password", secret: true);
+ password2.Resource.Default = new PasswordConstantDefault();
+
+ var surrealServer2 = builder2.AddSurrealServer("surreal", path: "rocksdb://data/db.db", password: password2);
+
+ var db2 = surrealServer2
+ .AddNamespace("ns")
+ .AddDatabase("db");
+
+ if (useVolume)
+ {
+ surrealServer2.WithDataVolume(volumeName);
+ }
+ else
+ {
+ surrealServer2.WithDataBindMount(bindMountPath!);
+ }
+
+ using (var app = builder2.Build())
+ {
+ await app.StartAsync(cts.Token);
+
+ await app.ResourceNotifications.WaitForResourceHealthyAsync(surrealServer2.Resource.Name, cts.Token);
+
+ try
+ {
+ var hb = Host.CreateApplicationBuilder();
+
+ hb.Configuration[$"ConnectionStrings:{db2.Resource.Name}"] = await db2.Resource.ConnectionStringExpression.GetValueAsync(cts.Token);
+
+ hb.AddSurrealClient(db2.Resource.Name);
+
+ using var host = hb.Build();
+ await host.StartAsync(cts.Token);
+ await using var surrealDbClient = host.Services.GetRequiredService();
+ await AssertTestData(surrealDbClient, cts.Token);
+ }
+ finally
+ {
+ // Stops the container, or the Volume would still be in use
+ await app.StopAsync(cts.Token);
+ }
+ }
+
+ }
+ finally
+ {
+ if (volumeName is not null)
+ {
+ DockerUtils.AttemptDeleteDockerVolume(volumeName);
+ }
+
+ if (bindMountPath is not null)
+ {
+ try
+ {
+ Directory.Delete(bindMountPath, recursive: true);
+ }
+ catch
+ {
+ // Don't fail test if we can't clean the temporary folder
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public async Task VerifyWaitForOnSurrealDbBlocksDependentResources()
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
+ using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
+
+ var healthCheckTcs = new TaskCompletionSource();
+ builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
+ {
+ return healthCheckTcs.Task;
+ });
+
+ var resource = builder.AddSurrealServer("resource")
+ .WithHealthCheck("blocking_check");
+
+ var dependentResource = builder.AddSurrealServer("dependentresource")
+ .WaitFor(resource);
+
+ using var app = builder.Build();
+
+ var pendingStart = app.StartAsync(cts.Token);
+
+ var rns = app.Services.GetRequiredService();
+
+ await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);
+
+ healthCheckTcs.SetResult(HealthCheckResult.Healthy());
+
+ await rns.WaitForResourceAsync(resource.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cts.Token);
+
+ await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);
+
+ await pendingStart;
+
+ await app.StopAsync();
+ }
+
+ private static async Task CreateTestData(SurrealDbClient surrealDbClient, CancellationToken ct)
+ {
+ await surrealDbClient.Insert(Todo.Table, _todoList, ct);
+ }
+
+ private static async Task AssertTestData(SurrealDbClient surrealDbClient, CancellationToken ct)
+ {
+ var records = await surrealDbClient.Select(Todo.Table);
+ Assert.Equal(_generatedTodoCount, records.Count());
+
+ var firstRecord = await surrealDbClient.Select((Todo.Table, "1"));
+ Assert.NotNull(firstRecord);
+ Assert.Equivalent(firstRecord, _todoList[0]);
+ }
+
+ private sealed class Todo : SurrealRecord
+ {
+ internal const string Table = "todo";
+
+ public string? Title { get; set; }
+ public DateOnly? DueBy { get; set; } = null;
+ public bool IsComplete { get; set; } = false;
+ }
+
+ private class TodoFaker : Faker
+ {
+ public TodoFaker()
+ {
+ RuleFor(o => o.Title, f => f.Lorem.Sentence());
+ RuleFor(o => o.DueBy, f => f.Date.SoonDateOnly());
+ RuleFor(o => o.IsComplete, f => f.Random.Bool());
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbPublicApiTests.cs
new file mode 100644
index 00000000..159e7425
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.SurrealDb.Tests/SurrealDbPublicApiTests.cs
@@ -0,0 +1,247 @@
+// 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;
+
+namespace CommunityToolkit.Aspire.Hosting.SurrealDb.Tests;
+
+public class SurrealDbPublicApiTests
+{
+ [Fact]
+ public void AddSurrealServerContainerShouldThrowWhenBuilderIsNull()
+ {
+ IDistributedApplicationBuilder builder = null!;
+ const string name = "surreal";
+
+ var action = () => builder.AddSurrealServer(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddSurrealServerContainerShouldThrowWhenNameIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder([]);
+ string name = null!;
+
+ var action = () => builder.AddSurrealServer(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddDatabaseShouldThrowWhenBuilderIsNull()
+ {
+ IResourceBuilder builder = null!;
+ const string name = "surreal";
+
+ var action = () => builder.AddDatabase(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddDatabaseShouldThrowWhenNameIsNull()
+ {
+ var builder = DistributedApplication.CreateBuilder([])
+ .AddSurrealServer("surreal")
+ .AddNamespace("ns");
+ string name = null!;
+
+ var action = () => builder.AddDatabase(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddDatabaseShouldThrowWhenNameIsEmpty()
+ {
+ var builder = DistributedApplication.CreateBuilder([])
+ .AddSurrealServer("surreal")
+ .AddNamespace("ns");
+ string name = "";
+
+ var action = () => builder.AddDatabase(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void WithDataVolumeShouldThrowWhenBuilderIsNull()
+ {
+ IResourceBuilder builder = null!;
+
+ var action = () => builder.WithDataVolume();
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void WithDataShouldThrowWhenBuilderIsNull(bool useVolume)
+ {
+ IResourceBuilder builder = null!;
+
+ Func>? action = null;
+
+ if (useVolume)
+ {
+ action = () => builder.WithDataVolume();
+ }
+ else
+ {
+ const string source = "/data";
+
+ action = () => builder.WithDataBindMount(source);
+ }
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void WithDataBindMountShouldThrowWhenSourceIsNull()
+ {
+ var builder = new DistributedApplicationBuilder([]);
+ var resourceBuilder = builder.AddSurrealServer("surreal");
+
+ string source = null!;
+
+ var action = () => resourceBuilder.WithDataBindMount(source);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(source), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerServerResourceShouldThrowWhenNameIsNull()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+ string name = null!;
+ const string key = nameof(key);
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, key, special: false);
+
+ var action = () => new SurrealDbServerResource(name, null, password);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerDatabaseResourceShouldThrowWhenNameIsNull()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string name = null!;
+ string namespaceName = "ns1";
+ string databaseName = "db1";
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var nsParent = new SurrealDbNamespaceResource("ns", namespaceName, parent);
+ var action = () => new SurrealDbDatabaseResource(name, databaseName, nsParent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerDatabaseResourceShouldThrowWhenNameIsEmpty()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string name = "";
+ string namespaceName = "ns1";
+ string databaseName = "db1";
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var nsParent = new SurrealDbNamespaceResource("ns", namespaceName, parent);
+ var action = () => new SurrealDbDatabaseResource(name, databaseName, nsParent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerNamespaceResourceShouldThrowWhenNamespaceNameIsNull()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string namespaceName = null!;
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var action = () => new SurrealDbNamespaceResource("ns", namespaceName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(namespaceName), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerNamespaceResourceShouldThrowWhenNamespaceNameIsEmpty()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string namespaceName = "";
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var action = () => new SurrealDbNamespaceResource("ns", namespaceName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(namespaceName), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerDatabaseResourceShouldThrowWhenDatabaseNameIsNull()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string name = "surreal";
+ string namespaceName = "ns";
+ string databaseName = null!;
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var nsParent = new SurrealDbNamespaceResource("ns", namespaceName, parent);
+ var action = () => new SurrealDbDatabaseResource(name, databaseName, nsParent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(databaseName), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerDatabaseResourceShouldThrowWhenDatabaseNameIsEmpty()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string name = "surreal";
+ string namespaceName = "ns";
+ string databaseName = null!;
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ var parent = new SurrealDbServerResource("surreal", null, password);
+ var nsParent = new SurrealDbNamespaceResource("ns", namespaceName, parent);
+ var action = () => new SurrealDbDatabaseResource(name, databaseName, nsParent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(databaseName), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorSurrealServerDatabaseResourceShouldThrowWhenParentIsNull()
+ {
+ var distributedApplicationBuilder = DistributedApplication.CreateBuilder([]);
+
+ string name = "surreal";
+ string databaseName = "db1";
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(distributedApplicationBuilder, "password", special: false);
+ SurrealDbNamespaceResource parent = null!;
+ var action = () => new SurrealDbDatabaseResource(name, databaseName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(parent), exception.ParamName);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/AspireSurrealClientExtensionsTest.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/AspireSurrealClientExtensionsTest.cs
new file mode 100644
index 00000000..20ab869b
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/AspireSurrealClientExtensionsTest.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.Components.Common.Tests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using SurrealDb.Net;
+
+namespace CommunityToolkit.Aspire.SurrealDb.Tests;
+
+public class AspireSurrealClientExtensionsTest(SurrealDbContainerFixture containerFixture) : IClassFixture
+{
+ private const string DefaultConnectionName = "db";
+
+ private string DefaultConnectionString =>
+ RequiresDockerAttribute.IsSupported ? containerFixture.GetConnectionString() : "Endpoint=http://localhost:27011";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ [RequiresDocker]
+ public async Task AddSurrealClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed)
+ {
+ var key = DefaultConnectionName;
+
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedSurrealClient(key, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+ else
+ {
+ builder.AddSurrealClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = false;
+ });
+ }
+
+ using var host = builder.Build();
+
+ var healthCheckService = host.Services.GetRequiredService();
+
+ var healthCheckReport = await healthCheckService.CheckHealthAsync();
+
+ var healthCheckName = useKeyed ? $"surrealdb_{key}" : "surrealdb";
+
+ Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddSurrealClient_HealthCheckShouldNotBeRegisteredWhenDisabled(bool useKeyed)
+ {
+ var builder = CreateBuilder(DefaultConnectionString);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedSurrealClient(DefaultConnectionName, settings =>
+ {
+ settings.DisableHealthChecks = true;
+ });
+ }
+ else
+ {
+ builder.AddSurrealClient(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:surreal1", "Endpoint=http://localhost:19530"),
+ new KeyValuePair("ConnectionStrings:surreal2", "Endpoint=http://localhost:19531"),
+ new KeyValuePair("ConnectionStrings:surreal3", "Endpoint=http://localhost:19532"),
+ ]);
+
+ builder.AddSurrealClient("surreal1");
+ builder.AddKeyedSurrealClient("surreal2");
+ builder.AddKeyedSurrealClient("surreal3");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("surreal2");
+ var client3 = host.Services.GetRequiredKeyedService("surreal3");
+
+ 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:surreal1", "Endpoint=http://localhost:19530"),
+ new KeyValuePair("ConnectionStrings:surreal2", "Endpoint=http://localhost:19531"),
+ ]);
+
+ builder.AddSurrealClient("surreal1");
+ builder.AddKeyedSurrealClient("surreal2");
+
+ using var host = builder.Build();
+
+ var client1 = host.Services.GetRequiredService();
+ var client2 = host.Services.GetRequiredKeyedService("surreal2");
+
+ Assert.NotSame(client1, client2);
+ }
+
+ private static HostApplicationBuilder CreateBuilder(string connectionString)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair($"ConnectionStrings:{DefaultConnectionName}", connectionString)
+ ]);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/CommunityToolkit.Aspire.SurrealDb.Tests.csproj b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/CommunityToolkit.Aspire.SurrealDb.Tests.csproj
new file mode 100644
index 00000000..4fc1af1c
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/CommunityToolkit.Aspire.SurrealDb.Tests.csproj
@@ -0,0 +1,20 @@
+ο»Ώ
+
+
+ CommunityToolkit.Aspire.SurrealDb.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConfigurationTests.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000..162d09f9
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConfigurationTests.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.
+
+namespace CommunityToolkit.Aspire.SurrealDb.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void HealthChecksEnabledByDefault() =>
+ Assert.False(new SurrealDbClientSettings().DisableHealthChecks);
+
+ [Fact]
+ public void OptionsAreNullByDefault() =>
+ Assert.Null(new SurrealDbClientSettings().Options);
+
+ [Fact]
+ public void LifetimeIsSingletonByDefault() =>
+ Assert.Equal(ServiceLifetime.Singleton, new SurrealDbClientSettings().Lifetime);
+}
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConformanceTests.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConformanceTests.cs
new file mode 100644
index 00000000..c0d49f47
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/ConformanceTests.cs
@@ -0,0 +1,106 @@
+ο»Ώ// 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 Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using SurrealDb.Net;
+
+namespace CommunityToolkit.Aspire.SurrealDb.Tests;
+
+public class ConformanceTests :
+ ConformanceTests,
+ IClassFixture
+{
+ private readonly SurrealDbContainerFixture _containerFixture;
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ protected override string ActivitySourceName => string.Empty;
+
+ protected override string[] RequiredLogCategories => [];
+
+ protected override bool CanConnectToServer => RequiresDockerAttribute.IsSupported;
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ public ConformanceTests(SurrealDbContainerFixture containerFixture)
+ {
+ _containerFixture = containerFixture;
+ }
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ {
+ var connectionString = RequiresDockerAttribute.IsSupported ?
+ $"{_containerFixture.GetConnectionString()}" :
+ "Endpoint=http://localhost:27017";
+
+ configuration.AddInMemoryCollection(
+ [
+ new KeyValuePair(CreateConfigKey("Aspire:Surreal:Client", key, "Endpoint"), GetConnectionStringKeyValue(connectionString,"Endpoint")),
+ new KeyValuePair($"ConnectionStrings:{key ?? "surreal"}", $"{connectionString}")
+ ]);
+ }
+
+ internal static string GetConnectionStringKeyValue(string connectionString, string configKey)
+ {
+ // from the connection string, extract the key value of the configKey
+ var parts = connectionString.Split(';');
+ foreach (var part in parts)
+ {
+ var keyValue = part.Split('=');
+ if (keyValue.Length == 2 && keyValue[0].Equals(configKey, StringComparison.OrdinalIgnoreCase))
+ {
+ return keyValue[1];
+ }
+ }
+ return string.Empty;
+ }
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddSurrealClient("surreal", configureSettings: configure);
+ }
+ else
+ {
+ builder.AddKeyedSurrealClient(key, configureSettings: configure);
+ }
+ }
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Surreal": {
+ "Client": {
+ "Endpoint": "http://localhost:19530"
+ }
+ }
+ }
+ }
+ """;
+
+ protected override void SetHealthCheck(SurrealDbClientSettings options, bool enabled)
+ {
+ options.DisableHealthChecks = !enabled;
+ }
+
+ protected override void SetMetrics(SurrealDbClientSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void SetTracing(SurrealDbClientSettings options, bool enabled)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override void TriggerActivity(SurrealDbClient service)
+ {
+ using var source = new CancellationTokenSource(100);
+
+ service.Version(source.Token).Wait();
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbClientPublicApiTests.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbClientPublicApiTests.cs
new file mode 100644
index 00000000..15a8e7c1
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbClientPublicApiTests.cs
@@ -0,0 +1,87 @@
+ο»Ώ// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Hosting;
+
+namespace CommunityToolkit.Aspire.SurrealDb.Tests;
+
+public class SurrealDbClientPublicApiTests
+{
+ [Fact]
+ public void AddSurrealClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ const string connectionName = "surreal";
+
+ var action = () => builder.AddSurrealClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddSurrealClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = null!;
+
+ var action = () => builder.AddSurrealClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddSurrealClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string connectionName = "";
+
+ var action = () => builder.AddSurrealClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(connectionName), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedSurrealClientShouldThrowWhenBuilderIsNull()
+ {
+ IHostApplicationBuilder builder = null!;
+
+ const string connectionName = "surreal";
+
+ var action = () => builder.AddKeyedSurrealClient(connectionName);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedSurrealClientShouldThrowWhenNameIsNull()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = null!;
+
+ var action = () => builder.AddKeyedSurrealClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddKeyedSurrealClientShouldThrowWhenNameIsEmpty()
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ string name = "";
+
+ var action = () => builder.AddKeyedSurrealClient(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs
new file mode 100644
index 00000000..560d17e7
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.SurrealDb.Tests/SurrealDbContainerFixture.cs
@@ -0,0 +1,70 @@
+ο»Ώ// 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;
+using Aspire.Hosting.Utils;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+
+namespace CommunityToolkit.Aspire.SurrealDb.Tests;
+
+public sealed class SurrealDbContainerFixture : IAsyncLifetime
+{
+ private const string _username = "root";
+ private string _password = string.Empty;
+ private const int _port = 8000;
+
+ public IContainer? Container { get; private set; }
+
+ public string GetConnectionString()
+ {
+ if (Container is null)
+ {
+ throw new InvalidOperationException("The test container was not initialized.");
+ }
+
+ var endpoint = new UriBuilder("ws", Container.Hostname, Container.GetMappedPublicPort(_port), "/rpc").ToString();
+ return $"Endpoint={endpoint};Username={_username};Password={_password}";
+ }
+
+ public async Task InitializeAsync()
+ {
+ if (RequiresDockerAttribute.IsSupported)
+ {
+ var paramGenerator = new GenerateParameterDefault
+ {
+ MinLength = 8,
+ Lower = true,
+ Upper = true,
+ Numeric = true,
+ Special = false,
+ MinLower = 1,
+ MinUpper = 1,
+ MinNumeric = 1,
+ MinSpecial = 0
+ };
+
+ _password = paramGenerator.GetDefaultValue();
+
+ Container = new ContainerBuilder()
+ .WithImage($"{SurrealDbContainerImageTags.Registry}/{SurrealDbContainerImageTags.Image}:{SurrealDbContainerImageTags.Tag}")
+ .WithPortBinding(_port, true)
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(r => r.ForPort(_port)))
+ .WithEnvironment("SURREAL_USER", _username)
+ .WithEnvironment("SURREAL_PASS", _password)
+ .WithCommand("start", "memory")
+ .Build();
+
+ await Container.StartAsync();
+ }
+ }
+
+ public async Task DisposeAsync()
+ {
+ if (Container is not null)
+ {
+ await Container.DisposeAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Aspire.Testing/DockerUtils.cs b/tests/CommunityToolkit.Aspire.Testing/DockerUtils.cs
index 83d58e49..8bacff0f 100644
--- a/tests/CommunityToolkit.Aspire.Testing/DockerUtils.cs
+++ b/tests/CommunityToolkit.Aspire.Testing/DockerUtils.cs
@@ -9,6 +9,8 @@ public sealed class DockerUtils
{
public static void AttemptDeleteDockerVolume(string volumeName, bool throwOnFailure = false)
{
+ string containerRuntime = Environment.GetEnvironmentVariable("DOTNET_ASPIRE_CONTAINER_RUNTIME") ?? "docker";
+
for (var i = 0; i < 3; i++)
{
if (i != 0)
@@ -16,7 +18,7 @@ public static void AttemptDeleteDockerVolume(string volumeName, bool throwOnFail
Thread.Sleep(1000);
}
- if (Process.Start("docker", $"volume rm {volumeName}") is { } process)
+ if (Process.Start(containerRuntime, $"volume rm {volumeName}") is { } process)
{
var exited = process.WaitForExit(TimeSpan.FromSeconds(3));
var done = exited && process.ExitCode == 0;
@@ -32,7 +34,7 @@ public static void AttemptDeleteDockerVolume(string volumeName, bool throwOnFail
if (throwOnFailure)
{
- if (Process.Start("docker", $"volume inspect {volumeName}") is { } process)
+ if (Process.Start(containerRuntime, $"volume inspect {volumeName}") is { } process)
{
var exited = process.WaitForExit(TimeSpan.FromSeconds(3));
var exitCode = process.ExitCode;