diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e178e4885..3fd8a95c8 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -39,6 +39,7 @@ jobs:
Hosting.Meilisearch.Tests,
Hosting.MongoDB.Extensions.Tests,
Hosting.MySql.Extensions.Tests,
+ Hosting.Neon.Tests,
Hosting.Ngrok.Tests,
Hosting.NodeJS.Extensions.Tests,
Hosting.Ollama.Tests,
diff --git a/CommunityToolkit.Aspire.slnx b/CommunityToolkit.Aspire.slnx
index b27af11f5..1baa6c388 100644
--- a/CommunityToolkit.Aspire.slnx
+++ b/CommunityToolkit.Aspire.slnx
@@ -92,6 +92,11 @@
+
+
+
+
+
@@ -177,6 +182,7 @@
+
@@ -198,6 +204,7 @@
+
@@ -228,6 +235,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9222db74c..515964304 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,6 +18,7 @@
+
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/CommunityToolkit.Aspire.Hosting.Neon.ApiService.csproj b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/CommunityToolkit.Aspire.Hosting.Neon.ApiService.csproj
new file mode 100644
index 000000000..6f3311664
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/CommunityToolkit.Aspire.Hosting.Neon.ApiService.csproj
@@ -0,0 +1,13 @@
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Program.cs b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Program.cs
new file mode 100644
index 000000000..ea500633b
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Program.cs
@@ -0,0 +1,24 @@
+using Npgsql;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddNeonDataSource("neondb");
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+app.MapGet("/", () => "Neon API Service");
+
+app.MapGet("/test", async (NpgsqlDataSource dataSource) =>
+{
+ await using var connection = await dataSource.OpenConnectionAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "SELECT version()";
+ var version = await command.ExecuteScalarAsync();
+ return new { version, message = "Connected to Neon successfully!" };
+});
+
+app.Run();
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Properties/launchSettings.json b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Properties/launchSettings.json
new file mode 100644
index 000000000..df85fcaf4
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/appsettings.json b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/CommunityToolkit.Aspire.Hosting.Neon.AppHost.csproj b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/CommunityToolkit.Aspire.Hosting.Neon.AppHost.csproj
new file mode 100644
index 000000000..bf2d8023d
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/CommunityToolkit.Aspire.Hosting.Neon.AppHost.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+ Exe
+ enable
+ enable
+ true
+ neon-example-apphost-secrets
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/Program.cs b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/Program.cs
new file mode 100644
index 000000000..5a6dcac9a
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/Program.cs
@@ -0,0 +1,15 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+// Add Neon project resource
+var neon = builder.AddNeonProject("neon");
+
+// Add a Neon database
+var neonDb = neon.AddDatabase("neondb");
+
+// Reference the Neon database in a project
+builder.AddProject("apiservice")
+ .WithReference(neonDb)
+ .WaitFor(neonDb)
+ .WithHttpHealthCheck("/health");
+
+builder.Build().Run();
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/Properties/launchSettings.json b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..0f18f759d
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.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:17052;http://localhost:15125",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21025",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22191"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15125",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19091",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20167"
+ }
+ }
+ }
+}
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults.csproj b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults.csproj
new file mode 100644
index 000000000..6f00332d1
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ Library
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults/Extensions.cs b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.ServiceDefaults/Extensions.cs
new file mode 100644
index 000000000..b34d76254
--- /dev/null
+++ b/examples/neon/CommunityToolkit.Aspire.Hosting.Neon.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.Neon/CommunityToolkit.Aspire.Hosting.Neon.csproj b/src/CommunityToolkit.Aspire.Hosting.Neon/CommunityToolkit.Aspire.Hosting.Neon.csproj
new file mode 100644
index 000000000..3580f5d70
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/CommunityToolkit.Aspire.Hosting.Neon.csproj
@@ -0,0 +1,12 @@
+
+
+
+ hosting neon postgresql
+ Neon support for .NET Aspire.
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/NeonBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonBuilderExtensions.cs
new file mode 100644
index 000000000..a2a0eeb9b
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonBuilderExtensions.cs
@@ -0,0 +1,169 @@
+using CommunityToolkit.Aspire.Hosting.Neon;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Neon resources to the application model.
+///
+public static class NeonBuilderExtensions
+{
+ private const int NeonPort = 5432;
+ private const string DefaultUserName = "postgres";
+
+ ///
+ /// Adds a Neon project resource to the application model.
+ /// 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 user name for the Neon project. If a default user name will be used.
+ /// The parameter used to provide the password for the Neon project. If a random password will be generated.
+ /// The host port to bind the underlying container to.
+ /// A reference to the .
+ ///
+ ///
+ /// Add a Neon project to the application model and reference it in a .NET project.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var neon = builder.AddNeonProject("neon");
+ /// var db = neon.AddDatabase("neondb");
+ ///
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddNeonProject(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder? userName = null,
+ IResourceBuilder? password = null,
+ int? port = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ var userNameParameter = userName?.Resource;
+ if (userNameParameter == null)
+ {
+ var userNameBuilder = builder.AddParameter($"{name}-username", secret: false);
+ builder.Configuration[$"Parameters:{name}-username"] = DefaultUserName;
+ userNameParameter = userNameBuilder.Resource;
+ }
+
+ var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password");
+
+ var neonProject = new NeonProjectResource(name, userNameParameter, passwordParameter);
+
+ return builder.AddResource(neonProject)
+ .WithImage(NeonContainerImageTags.Image, NeonContainerImageTags.Tag)
+ .WithImageRegistry(NeonContainerImageTags.Registry)
+ .WithEndpoint(targetPort: NeonPort, port: port, name: NeonProjectResource.PrimaryEndpointName)
+ .WithEnvironment(context =>
+ {
+ context.EnvironmentVariables["POSTGRES_USER"] = neonProject.UserNameParameter;
+ context.EnvironmentVariables["POSTGRES_PASSWORD"] = neonProject.PasswordParameter;
+ });
+ }
+
+ ///
+ /// Adds a Neon database resource to the application model.
+ ///
+ /// The Neon project resource builder.
+ /// 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 Neon project with a database to the application model.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var neon = builder.AddNeonProject("neon")
+ /// .AddDatabase("neondb");
+ ///
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(neon);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder AddDatabase(
+ this IResourceBuilder builder,
+ [ResourceName] string name,
+ string? databaseName = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ var databaseResource = new NeonDatabaseResource(name, databaseName ?? name, builder.Resource);
+
+ return builder.ApplicationBuilder.AddResource(databaseResource);
+ }
+
+ ///
+ /// Adds a named volume for the data folder to a Neon project 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 Neon project to the application model with a data volume to persist data across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var neon = builder.AddNeonProject("neon")
+ /// .WithDataVolume();
+ /// var db = neon.AddDatabase("neondb");
+ ///
+ /// 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"), "/var/lib/postgresql/data");
+ }
+
+ ///
+ /// Adds a bind mount for the data folder to a Neon project resource.
+ ///
+ /// The resource builder.
+ /// The source directory on the host to mount into the container.
+ /// The .
+ ///
+ ///
+ /// Add a Neon project to the application model with a data bind mount to persist data across container restarts.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// var neon = builder.AddNeonProject("neon")
+ /// .WithDataBindMount("./data/neon");
+ /// var db = neon.AddDatabase("neondb");
+ ///
+ /// var api = builder.AddProject<Projects.Api>("api")
+ /// .WithReference(db);
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ ///
+ public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(source);
+
+ return builder.WithBindMount(source, "/var/lib/postgresql/data");
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/NeonContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonContainerImageTags.cs
new file mode 100644
index 000000000..ed7c0e4f4
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonContainerImageTags.cs
@@ -0,0 +1,11 @@
+namespace CommunityToolkit.Aspire.Hosting.Neon;
+
+internal static class NeonContainerImageTags
+{
+ /// docker.io
+ public const string Registry = "docker.io";
+ /// neondatabase/neon
+ public const string Image = "neondatabase/neon";
+ /// latest
+ public const string Tag = "latest";
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/NeonDatabaseResource.cs b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonDatabaseResource.cs
new file mode 100644
index 000000000..33f859f4c
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonDatabaseResource.cs
@@ -0,0 +1,35 @@
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a Neon database.
+///
+public class NeonDatabaseResource : Resource, IResourceWithConnectionString, IResourceWithParent
+{
+ /// The name of the resource.
+ /// The database name.
+ /// The Neon project resource associated with this database.
+ public NeonDatabaseResource(string name, string databaseName, NeonProjectResource parent) : base(name)
+ {
+ ArgumentNullException.ThrowIfNull(parent);
+ ArgumentException.ThrowIfNullOrEmpty(databaseName);
+
+ Parent = parent;
+ DatabaseName = databaseName;
+ }
+
+ ///
+ /// Gets the parent Neon project resource.
+ ///
+ public NeonProjectResource Parent { get; }
+
+ ///
+ /// Gets the database name.
+ ///
+ public string DatabaseName { get; }
+
+ ///
+ /// Gets the connection string expression for the Neon database.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"{Parent.ConnectionStringExpression};Database={DatabaseName}");
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/NeonProjectResource.cs b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonProjectResource.cs
new file mode 100644
index 000000000..744530a65
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/NeonProjectResource.cs
@@ -0,0 +1,44 @@
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a Neon project.
+///
+public class NeonProjectResource : ContainerResource, IResourceWithConnectionString
+{
+ internal const string PrimaryEndpointName = "tcp";
+
+ /// The name of the resource.
+ /// A parameter that contains the Neon user name.
+ /// A parameter that contains the Neon password.
+ public NeonProjectResource(string name, ParameterResource userName, ParameterResource password) : base(name)
+ {
+ ArgumentNullException.ThrowIfNull(userName);
+ ArgumentNullException.ThrowIfNull(password);
+
+ UserNameParameter = userName;
+ PasswordParameter = password;
+ }
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the Neon project.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the parameter that contains the Neon user name.
+ ///
+ public ParameterResource UserNameParameter { get; }
+
+ ///
+ /// Gets the parameter that contains the Neon password.
+ ///
+ public ParameterResource PasswordParameter { get; }
+
+ ///
+ /// Gets the connection string expression for the Neon project.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create($"Host={PrimaryEndpoint.Property(EndpointProperty.Host)};Port={PrimaryEndpoint.Property(EndpointProperty.Port)};Username={UserNameParameter};Password={PasswordParameter}");
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/README.md b/src/CommunityToolkit.Aspire.Hosting.Neon/README.md
new file mode 100644
index 000000000..58b09d5ae
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/README.md
@@ -0,0 +1,69 @@
+# CommunityToolkit.Aspire.Hosting.Neon library
+
+This integration provides support for [Neon](https://neon.tech/), a serverless PostgreSQL-compatible database, in .NET Aspire applications.
+
+## Getting Started
+
+### Install the package
+
+In your AppHost project, install the package using the following command:
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Hosting.Neon
+```
+
+### Example usage
+
+In the _Program.cs_ file of your AppHost project, add a Neon project and database:
+
+```csharp
+var builder = DistributedApplication.CreateBuilder(args);
+
+// Add Neon project resource
+var neon = builder.AddNeonProject("neon");
+
+// Add a Neon database
+var neonDb = neon.AddDatabase("neondb");
+
+// Reference the Neon database in a project
+var exampleProject = builder.AddProject()
+ .WithReference(neonDb);
+
+builder.Build().Run();
+```
+
+## Additional Configuration
+
+### Data Persistence
+
+To persist data across container restarts, you can use either a volume or bind mount:
+
+```csharp
+var neon = builder.AddNeonProject("neon")
+ .WithDataVolume(); // or .WithDataBindMount("./data/neon")
+```
+
+### Custom Credentials
+
+You can provide custom credentials for the Neon project:
+
+```csharp
+var userName = builder.AddParameter("neon-user");
+var password = builder.AddParameter("neon-password", secret: true);
+
+var neon = builder.AddNeonProject("neon", userName, password);
+```
+
+## About Neon Features
+
+This integration provides a PostgreSQL-compatible container for local development. For production scenarios using Neon's cloud service, you can use the Neon connection string directly with the standard [Aspire PostgreSQL](https://learn.microsoft.com/dotnet/aspire/database/postgresql-component) or [Aspire Npgsql](https://learn.microsoft.com/dotnet/aspire/database/postgresql-entity-framework-component) integrations.
+
+Neon's unique features like [database branching](https://neon.tech/docs/introduction/branching) are available when using Neon's cloud service and can be managed through their API or CLI alongside this integration.
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-neon
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/src/CommunityToolkit.Aspire.Hosting.Neon/api/CommunityToolkit.Aspire.Hosting.Neon.cs b/src/CommunityToolkit.Aspire.Hosting.Neon/api/CommunityToolkit.Aspire.Hosting.Neon.cs
new file mode 100644
index 000000000..38fb7bdbc
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Neon/api/CommunityToolkit.Aspire.Hosting.Neon.cs
@@ -0,0 +1,48 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+namespace Aspire.Hosting
+{
+ public static partial class NeonBuilderExtensions
+ {
+ public static ApplicationModel.IResourceBuilder AddDatabase(this ApplicationModel.IResourceBuilder builder, string name, string? databaseName = null) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder AddNeonProject(this IDistributedApplicationBuilder builder, string name, ApplicationModel.IResourceBuilder? userName = null, ApplicationModel.IResourceBuilder? password = null, int? port = null) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder WithDataBindMount(this ApplicationModel.IResourceBuilder builder, string source) { throw null; }
+
+ public static ApplicationModel.IResourceBuilder WithDataVolume(this ApplicationModel.IResourceBuilder builder, string? name = null) { throw null; }
+ }
+}
+
+namespace Aspire.Hosting.ApplicationModel
+{
+ public partial class NeonDatabaseResource : Resource, IResourceWithConnectionString, IResourceWithParent, IResource, IManifestExpressionProvider, IValueProvider, IValueWithReferences
+ {
+ public NeonDatabaseResource(string name, string databaseName, NeonProjectResource parent) : base(default!) { }
+
+ public ReferenceExpression ConnectionStringExpression { get { throw null; } }
+
+ public string DatabaseName { get { throw null; } }
+
+ public NeonProjectResource Parent { get { throw null; } }
+ }
+
+ public partial class NeonProjectResource : ContainerResource, IResourceWithConnectionString, IResource, IManifestExpressionProvider, IValueProvider, IValueWithReferences
+ {
+ public NeonProjectResource(string name, ParameterResource userName, ParameterResource password) : base(default!, default) { }
+
+ public ReferenceExpression ConnectionStringExpression { get { throw null; } }
+
+ public ParameterResource PasswordParameter { get { throw null; } }
+
+ public EndpointReference PrimaryEndpoint { get { throw null; } }
+
+ public ParameterResource UserNameParameter { get { throw null; } }
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Neon/CommunityToolkit.Aspire.Neon.csproj b/src/CommunityToolkit.Aspire.Neon/CommunityToolkit.Aspire.Neon.csproj
new file mode 100644
index 000000000..4a2922e19
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Neon/CommunityToolkit.Aspire.Neon.csproj
@@ -0,0 +1,12 @@
+
+
+
+ neon postgresql npgsql
+ Neon client support for .NET Aspire.
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Aspire.Neon/NeonExtensions.cs b/src/CommunityToolkit.Aspire.Neon/NeonExtensions.cs
new file mode 100644
index 000000000..7cd177747
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Neon/NeonExtensions.cs
@@ -0,0 +1,71 @@
+using CommunityToolkit.Aspire.Neon;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Npgsql;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Provides extension methods for adding Neon database client to an .
+///
+public static class NeonExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Neon";
+
+ ///
+ /// Registers an in the DI container for connecting to a Neon database.
+ ///
+ /// The to read config from and add services to.
+ /// The name of the connection string in the configuration.
+ /// An optional delegate that can be used for customizing the .
+ /// An optional delegate that can be used for customizing the .
+ ///
+ ///
+ /// Add a Neon data source to the service collection.
+ ///
+ /// var builder = WebApplication.CreateBuilder(args);
+ ///
+ /// builder.AddNeonDataSource("neondb");
+ ///
+ /// var app = builder.Build();
+ ///
+ ///
+ ///
+ public static void AddNeonDataSource(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null,
+ Action? configureDataSourceBuilder = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(connectionName);
+
+ var configSectionPath = $"{DefaultConfigSectionName}:{connectionName}";
+ var configSection = builder.Configuration.GetSection(configSectionPath);
+
+ var settings = new NeonSettings();
+ configSection.Bind(settings);
+ configureSettings?.Invoke(settings);
+
+ if (string.IsNullOrEmpty(settings.ConnectionString))
+ {
+ settings.ConnectionString = builder.Configuration.GetConnectionString(connectionName);
+ }
+
+ if (string.IsNullOrEmpty(settings.ConnectionString))
+ {
+ throw new InvalidOperationException($"Connection string '{connectionName}' not found.");
+ }
+
+ builder.AddNpgsqlDataSource(
+ connectionName,
+ configureSettings: npgsqlSettings =>
+ {
+ npgsqlSettings.ConnectionString = settings.ConnectionString;
+ npgsqlSettings.DisableHealthChecks = settings.DisableHealthChecks;
+ npgsqlSettings.DisableTracing = settings.DisableTracing;
+ npgsqlSettings.DisableMetrics = settings.DisableMetrics;
+ },
+ configureDataSourceBuilder: configureDataSourceBuilder);
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Neon/NeonSettings.cs b/src/CommunityToolkit.Aspire.Neon/NeonSettings.cs
new file mode 100644
index 000000000..f64a33fc2
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Neon/NeonSettings.cs
@@ -0,0 +1,36 @@
+namespace CommunityToolkit.Aspire.Neon;
+
+///
+/// Provides the client configuration settings for connecting to a Neon database.
+///
+public sealed class NeonSettings
+{
+ ///
+ /// Gets or sets the connection string to use for connecting to the Neon database.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the database health check is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableHealthChecks { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableTracing { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are disabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool DisableMetrics { get; set; }
+}
diff --git a/src/CommunityToolkit.Aspire.Neon/README.md b/src/CommunityToolkit.Aspire.Neon/README.md
new file mode 100644
index 000000000..2116f39c5
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Neon/README.md
@@ -0,0 +1,87 @@
+# CommunityToolkit.Aspire.Neon library
+
+This integration provides PostgreSQL client support for .NET Aspire applications, compatible with both local PostgreSQL containers and Neon's serverless PostgreSQL service. It leverages the Npgsql library for connectivity.
+
+## Getting Started
+
+### Install the package
+
+In your client project, install the package using the following command:
+
+```dotnetcli
+dotnet add package CommunityToolkit.Aspire.Neon
+```
+
+### Example usage
+
+In the _Program.cs_ file of your client project, add the Neon data source:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// Add Neon data source
+builder.AddNeonDataSource("neondb");
+
+var app = builder.Build();
+```
+
+You can then retrieve the `NpgsqlDataSource` instance using dependency injection:
+
+```csharp
+public class MyService
+{
+ private readonly NpgsqlDataSource _dataSource;
+
+ public MyService(NpgsqlDataSource dataSource)
+ {
+ _dataSource = dataSource;
+ }
+
+ public async Task GetCountAsync()
+ {
+ await using var connection = await _dataSource.OpenConnectionAsync();
+ await using var command = connection.CreateCommand();
+ command.CommandText = "SELECT COUNT(*) FROM users";
+ return Convert.ToInt32(await command.ExecuteScalarAsync());
+ }
+}
+```
+
+## Configuration
+
+The Neon client integration supports the following configuration options:
+
+```json
+{
+ "Aspire": {
+ "Neon": {
+ "neondb": {
+ "ConnectionString": "Host=myhost;Database=mydb;Username=myuser;Password=mypass",
+ "DisableHealthChecks": false,
+ "DisableTracing": false,
+ "DisableMetrics": false
+ }
+ }
+ }
+}
+```
+
+## Using with Neon Cloud Service
+
+For production scenarios using Neon's cloud service, provide your Neon connection string in the configuration:
+
+```json
+{
+ "ConnectionStrings": {
+ "neondb": "postgresql://user:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb?sslmode=require"
+ }
+}
+```
+
+## Additional Information
+
+https://learn.microsoft.com/dotnet/aspire/community-toolkit/neon
+
+## Feedback & contributing
+
+https://github.com/CommunityToolkit/Aspire
diff --git a/src/CommunityToolkit.Aspire.Neon/api/CommunityToolkit.Aspire.Neon.cs b/src/CommunityToolkit.Aspire.Neon/api/CommunityToolkit.Aspire.Neon.cs
new file mode 100644
index 000000000..7da7577f2
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Neon/api/CommunityToolkit.Aspire.Neon.cs
@@ -0,0 +1,29 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+namespace CommunityToolkit.Aspire.Neon
+{
+ public sealed partial class NeonSettings
+ {
+ public string? ConnectionString { get { throw null; } set { } }
+
+ public bool DisableHealthChecks { get { throw null; } set { } }
+
+ public bool DisableMetrics { get { throw null; } set { } }
+
+ public bool DisableTracing { get { throw null; } set { } }
+ }
+}
+
+namespace Microsoft.Extensions.Hosting
+{
+ public static partial class NeonExtensions
+ {
+ public static void AddNeonDataSource(this IHostApplicationBuilder builder, string connectionName, System.Action? configureSettings = null, System.Action? configureDataSourceBuilder = null) { }
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/AddNeonTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/AddNeonTests.cs
new file mode 100644
index 000000000..b4fbd3109
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/AddNeonTests.cs
@@ -0,0 +1,159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Sockets;
+using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Neon.Tests;
+
+public class AddNeonTests
+{
+ [Fact]
+ public async Task AddNeonProjectWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ var neon = appBuilder.AddNeonProject("neon");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("neon", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Single(endpoints);
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "tcp");
+ Assert.Equal(5432, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("tcp", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("tcp", primaryEndpoint.Transport);
+ Assert.Equal("tcp", primaryEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(NeonContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(NeonContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(NeonContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await neon.Resource.GetEnvironmentVariableValuesAsync();
+
+ Assert.Equal(2, config.Count());
+ Assert.Contains(config, c => c.Key == "POSTGRES_USER");
+ Assert.Contains(config, c => c.Key == "POSTGRES_PASSWORD");
+ }
+
+ [Fact]
+ public async Task AddNeonProjectAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var userName = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(appBuilder, $"userName", special: false);
+ var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(appBuilder, $"password");
+
+ appBuilder.Configuration["Parameters:userName"] = await userName.GetValueAsync(default);
+ appBuilder.Configuration["Parameters:password"] = await password.GetValueAsync(default);
+
+ var userNameParameter = appBuilder.AddParameter(userName.Name);
+ var passwordParameter = appBuilder.AddParameter(password.Name);
+ var neon = appBuilder.AddNeonProject("neon", userNameParameter, passwordParameter);
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("neon", containerResource.Name);
+
+ var endpoints = containerResource.Annotations.OfType();
+ Assert.Single(endpoints);
+
+ var primaryEndpoint = Assert.Single(endpoints, e => e.Name == "tcp");
+ Assert.Equal(5432, primaryEndpoint.TargetPort);
+ Assert.False(primaryEndpoint.IsExternal);
+ Assert.Equal("tcp", primaryEndpoint.Name);
+ Assert.Null(primaryEndpoint.Port);
+ Assert.Equal(ProtocolType.Tcp, primaryEndpoint.Protocol);
+ Assert.Equal("tcp", primaryEndpoint.Transport);
+ Assert.Equal("tcp", primaryEndpoint.UriScheme);
+
+ var containerAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(NeonContainerImageTags.Tag, containerAnnotation.Tag);
+ Assert.Equal(NeonContainerImageTags.Image, containerAnnotation.Image);
+ Assert.Equal(NeonContainerImageTags.Registry, containerAnnotation.Registry);
+
+ var config = await neon.Resource.GetEnvironmentVariableValuesAsync();
+
+ Assert.Equal(2, config.Count());
+ Assert.Contains(config, c => c.Key == "POSTGRES_USER");
+ Assert.Contains(config, c => c.Key == "POSTGRES_PASSWORD");
+ }
+
+ [Fact]
+ public async Task NeonCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var neon = appBuilder
+ .AddNeonProject("neon")
+ .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432));
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ var expectedUserName = await neon.Resource.UserNameParameter.GetValueAsync(default);
+ var expectedPassword = await neon.Resource.PasswordParameter.GetValueAsync(default);
+
+ Assert.Equal($"Host=localhost;Port=5432;Username={expectedUserName};Password={expectedPassword}", connectionString);
+ }
+
+ [Fact]
+ public void AddDatabaseAddsNeonDatabaseResource()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var neon = appBuilder.AddNeonProject("neon");
+
+ var db = neon.AddDatabase("mydb", "actual-db-name");
+
+ using var app = appBuilder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ var dbResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("mydb", dbResource.Name);
+ Assert.Equal("actual-db-name", dbResource.DatabaseName);
+ Assert.Equal(neon.Resource, dbResource.Parent);
+ }
+
+ [Fact]
+ public async Task NeonDatabaseCreatesConnectionString()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var neon = appBuilder
+ .AddNeonProject("neon")
+ .WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432));
+
+ var db = neon.AddDatabase("mydb");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var connectionStringResource = Assert.Single(appModel.Resources.OfType()) as IResourceWithConnectionString;
+ var connectionString = await connectionStringResource.GetConnectionStringAsync();
+
+ var expectedUserName = await neon.Resource.UserNameParameter.GetValueAsync(default);
+ var expectedPassword = await neon.Resource.PasswordParameter.GetValueAsync(default);
+
+ Assert.Contains("Host=localhost", connectionString);
+ Assert.Contains("Port=5432", connectionString);
+ Assert.Contains($"Username={expectedUserName}", connectionString);
+ Assert.Contains($"Password={expectedPassword}", connectionString);
+ Assert.Contains("Database=mydb", connectionString);
+ }
+}
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/CommunityToolkit.Aspire.Hosting.Neon.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/CommunityToolkit.Aspire.Hosting.Neon.Tests.csproj
new file mode 100644
index 000000000..e5ebb55a5
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/CommunityToolkit.Aspire.Hosting.Neon.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/NeonPublicApiTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/NeonPublicApiTests.cs
new file mode 100644
index 000000000..69f23f4a9
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Neon.Tests/NeonPublicApiTests.cs
@@ -0,0 +1,24 @@
+// 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 CommunityToolkit.Aspire.Testing;
+
+namespace CommunityToolkit.Aspire.Hosting.Neon.Tests;
+
+[RequiresDocker]
+public class NeonPublicApiTests(AspireIntegrationTestFixture fixture) : IClassFixture>
+{
+ [Fact]
+ public async Task ResourceStartsAndRespondsOk()
+ {
+ var appName = "neon";
+ var httpClient = fixture.CreateHttpClient(appName);
+
+ await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(appName).WaitAsync(TimeSpan.FromMinutes(5));
+
+ var response = await httpClient.GetAsync("/");
+
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+}