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;