diff --git a/Aspire.sln b/Aspire.sln
index aea73c665fa..a227dfa8483 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -159,6 +159,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector.Tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.IntegrationServiceA", "tests\testproject\TestProject.IntegrationServiceA\TestProject.IntegrationServiceA.csproj", "{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nats", "nats", "{91F22EEA-EB23-425A-9B32-9438A0809F4B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nats.ApiService", "playground\nats\Nats.ApiService\Nats.ApiService.csproj", "{74F89D52-645B-4283-98AF-D04DA794EE37}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nats.AppHost", "playground\nats\Nats.AppHost\Nats.AppHost.csproj", "{A0BB6F6B-9543-4525-81D6-95DC07BD3D02}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.NATS.Net", "src\Components\Aspire.NATS.Net\Aspire.NATS.Net.csproj", "{303E6308-740C-4673-96D2-0E81934C9E69}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{2136E31D-2CBB-41BB-8618-716FF8E46E9E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationSchemaGenerator", "src\Tools\ConfigurationSchemaGenerator\ConfigurationSchemaGenerator.csproj", "{39FA2A64-012F-4EB9-A14F-E8AC54C975F6}"
@@ -237,7 +245,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostgresEndToEnd.ApiService
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mysql", "mysql", "{621991F1-854A-4743-835B-10CAF11A0CFF}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlDb.AppHost", "Playground\mysql\MySqlDb.AppHost\MySqlDb.AppHost.csproj", "{7E2AD00B-60E0-46C2-8640-7217D678F312}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlDb.AppHost", "playground\mysql\MySqlDb.AppHost\MySqlDb.AppHost.csproj", "{7E2AD00B-60E0-46C2-8640-7217D678F312}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySql.ApiService", "playground\mysql\MySql.ApiService\MySql.ApiService.csproj", "{F699F3AD-2AD9-454B-BA40-82AC3D6250FE}"
EndProject
@@ -265,8 +273,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureStorageEndToEnd.AppHos
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureStorageEndToEnd.ApiService", "playground\AzureStorageEndToEnd\AzureStorageEndToEnd.ApiService\AzureStorageEndToEnd.ApiService.csproj", "{921CB408-5E37-4354-B4CF-EAE517F633DC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.NATS.Net.Tests", "tests\Aspire.NATS.Net.Tests\Aspire.NATS.Net.Tests.csproj", "{C774BE00-EE93-4148-B866-8F0F2BA1E473}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Dashboard.Components.Tests", "tests\Aspire.Dashboard.Components.Tests\Aspire.Dashboard.Components.Tests.csproj", "{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nats.Backend", "playground\nats\Nats.Backend\Nats.Backend.csproj", "{C4833DEC-0A4F-4504-B8D0-06C60B84119C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nats.Common", "playground\nats\Nats.Common\Nats.Common.csproj", "{9CA94707-E801-444F-A582-D5BD0104CF9B}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{A7C6452C-FEDB-4883-9AE7-29892D260AA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Testing", "src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj", "{3216CF59-84B0-46FF-8572-D0AFB0155423}"
@@ -581,6 +595,18 @@ Global
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {74F89D52-645B-4283-98AF-D04DA794EE37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {74F89D52-645B-4283-98AF-D04DA794EE37}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {74F89D52-645B-4283-98AF-D04DA794EE37}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {74F89D52-645B-4283-98AF-D04DA794EE37}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0BB6F6B-9543-4525-81D6-95DC07BD3D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0BB6F6B-9543-4525-81D6-95DC07BD3D02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0BB6F6B-9543-4525-81D6-95DC07BD3D02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0BB6F6B-9543-4525-81D6-95DC07BD3D02}.Release|Any CPU.Build.0 = Release|Any CPU
+ {303E6308-740C-4673-96D2-0E81934C9E69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {303E6308-740C-4673-96D2-0E81934C9E69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {303E6308-740C-4673-96D2-0E81934C9E69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {303E6308-740C-4673-96D2-0E81934C9E69}.Release|Any CPU.Build.0 = Release|Any CPU
{39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -745,10 +771,22 @@ Global
{921CB408-5E37-4354-B4CF-EAE517F633DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{921CB408-5E37-4354-B4CF-EAE517F633DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{921CB408-5E37-4354-B4CF-EAE517F633DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C774BE00-EE93-4148-B866-8F0F2BA1E473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C774BE00-EE93-4148-B866-8F0F2BA1E473}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C774BE00-EE93-4148-B866-8F0F2BA1E473}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C774BE00-EE93-4148-B866-8F0F2BA1E473}.Release|Any CPU.Build.0 = Release|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4833DEC-0A4F-4504-B8D0-06C60B84119C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4833DEC-0A4F-4504-B8D0-06C60B84119C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4833DEC-0A4F-4504-B8D0-06C60B84119C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4833DEC-0A4F-4504-B8D0-06C60B84119C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9CA94707-E801-444F-A582-D5BD0104CF9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9CA94707-E801-444F-A582-D5BD0104CF9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9CA94707-E801-444F-A582-D5BD0104CF9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9CA94707-E801-444F-A582-D5BD0104CF9B}.Release|Any CPU.Build.0 = Release|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3216CF59-84B0-46FF-8572-D0AFB0155423}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -891,6 +929,10 @@ Global
{CA283D7F-EB95-4353-B196-C409965D2B42} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{C8079F06-304F-49B1-A0C1-45AA3782A923} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8} = {975F6F41-B455-451D-A312-098DE4A167B6}
+ {91F22EEA-EB23-425A-9B32-9438A0809F4B} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {74F89D52-645B-4283-98AF-D04DA794EE37} = {91F22EEA-EB23-425A-9B32-9438A0809F4B}
+ {A0BB6F6B-9543-4525-81D6-95DC07BD3D02} = {91F22EEA-EB23-425A-9B32-9438A0809F4B}
+ {303E6308-740C-4673-96D2-0E81934C9E69} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{39FA2A64-012F-4EB9-A14F-E8AC54C975F6} = {2136E31D-2CBB-41BB-8618-716FF8E46E9E}
{20A5A907-A135-4735-B4BF-E13514F360E3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{E592E447-BA3C-44FA-86C1-EBEDC864A644} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
@@ -943,7 +985,10 @@ Global
{8AA07A14-A4A7-45EC-B0F6-4690B516B16D} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{157A434E-E3CA-4080-96CF-903CC3DF66E9} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D}
{921CB408-5E37-4354-B4CF-EAE517F633DC} = {8AA07A14-A4A7-45EC-B0F6-4690B516B16D}
+ {C774BE00-EE93-4148-B866-8F0F2BA1E473} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{0870A667-FB0C-4758-AEAF-9E5F092AD7C1} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
+ {C4833DEC-0A4F-4504-B8D0-06C60B84119C} = {91F22EEA-EB23-425A-9B32-9438A0809F4B}
+ {9CA94707-E801-444F-A582-D5BD0104CF9B} = {91F22EEA-EB23-425A-9B32-9438A0809F4B}
{3216CF59-84B0-46FF-8572-D0AFB0155423} = {A7C6452C-FEDB-4883-9AE7-29892D260AA3}
{BE46B5B3-DFD4-4565-A2CD-7D95C623B03D} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{8B1802BC-6CB0-4027-850C-2AED42A82C9E} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b0439754dd1..886ee2e7049 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -90,6 +90,7 @@
+
diff --git a/playground/nats/Nats.ApiService/Nats.ApiService.csproj b/playground/nats/Nats.ApiService/Nats.ApiService.csproj
new file mode 100644
index 00000000000..9458666178c
--- /dev/null
+++ b/playground/nats/Nats.ApiService/Nats.ApiService.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/nats/Nats.ApiService/Nats.ApiService.http b/playground/nats/Nats.ApiService/Nats.ApiService.http
new file mode 100644
index 00000000000..df2adc01f55
--- /dev/null
+++ b/playground/nats/Nats.ApiService/Nats.ApiService.http
@@ -0,0 +1,51 @@
+@HostAddress = http://localhost:5156
+
+POST {{HostAddress}}/ping/
+
+###
+
+POST {{HostAddress}}/stream/
+Content-Type: application/json
+
+{
+ "name": "EVENTS",
+ "description": "Events Description",
+ "subjects": ["events.>"],
+ "max_msgs": 10
+}
+
+###
+
+GET {{HostAddress}}/stream/EVENTS
+Accept: application/json
+
+###
+
+POST {{HostAddress}}/publish/
+Content-Type: application/json
+
+{
+ "subject": "events.mouse.click",
+ "name": "Mouse Event",
+ "description": "Mouse Event Description",
+ "priority": 0.1
+}
+
+###
+
+POST {{HostAddress}}/publish/
+Content-Type: application/json
+
+{
+ "subject": "events.key.a",
+ "name": "Key Event",
+ "description": "Key Event Description",
+ "priority": 0.5
+}
+
+###
+
+GET {{HostAddress}}/consume/EVENTS
+Accept: application/json
+
+###
diff --git a/playground/nats/Nats.ApiService/Program.cs b/playground/nats/Nats.ApiService/Program.cs
new file mode 100644
index 00000000000..f08be8be0cc
--- /dev/null
+++ b/playground/nats/Nats.ApiService/Program.cs
@@ -0,0 +1,85 @@
+using NATS.Client.Core;
+using NATS.Client.JetStream;
+using NATS.Client.JetStream.Models;
+using Nats.Common;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.AddServiceDefaults();
+
+builder.AddNatsClient("nats", configureOptions: opts =>
+{
+ var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
+ return opts with { SerializerRegistry = jsonRegistry };
+});
+
+builder.AddNatsJetStream();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.MapGet("/ping", async (INatsConnection nats) =>
+{
+ var rtt = await nats.PingAsync();
+ return Results.Json(new { rtt, nats.ServerInfo });
+});
+
+app.MapPost("/stream", async (StreamConfig config, INatsJSContext jetStream) =>
+{
+ var stream = await jetStream.CreateStreamAsync(config);
+ var name = stream.Info.Config.Name;
+ return Results.Created($"/stream/{name}", name);
+});
+
+app.MapGet("/stream/{name}", async (string name, INatsJSContext jetStream) =>
+{
+ var stream = await jetStream.GetStreamAsync(name);
+ return Results.Ok(stream.Info);
+});
+
+app.MapPost("/publish/", async (AppEvent @event, INatsJSContext jetStream) =>
+{
+ try
+ {
+ var ack = await jetStream.PublishAsync(@event.Subject, @event);
+ ack.EnsureSuccess();
+ }
+ catch (NatsJSPublishNoResponseException)
+ {
+ return Results.Problem("Make sure the stream is created before publishing.");
+ }
+
+ return Results.Created();
+});
+
+app.MapGet("/consume/{name}", async (string name, INatsJSContext jetStream) =>
+{
+ var stream = await jetStream.GetStreamAsync(name);
+ var consumer = await stream.CreateOrderedConsumerAsync();
+
+ var events = new List();
+ await foreach(var msg in consumer.ConsumeAsync())
+ {
+ events.Add(msg.Data!);
+
+ if (msg.Metadata?.NumPending == 0)
+ {
+ break;
+ }
+ }
+
+ return Results.Ok(events);
+});
+
+app.Run();
diff --git a/playground/nats/Nats.ApiService/Properties/launchSettings.json b/playground/nats/Nats.ApiService/Properties/launchSettings.json
new file mode 100644
index 00000000000..352240ccf9e
--- /dev/null
+++ b/playground/nats/Nats.ApiService/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:12538",
+ "sslPort": 44308
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5156",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7060;http://localhost:5156",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/nats/Nats.ApiService/appsettings.Development.json b/playground/nats/Nats.ApiService/appsettings.Development.json
new file mode 100644
index 00000000000..0c208ae9181
--- /dev/null
+++ b/playground/nats/Nats.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/nats/Nats.ApiService/appsettings.json b/playground/nats/Nats.ApiService/appsettings.json
new file mode 100644
index 00000000000..10f68b8c8b4
--- /dev/null
+++ b/playground/nats/Nats.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/nats/Nats.AppHost/Directory.Build.props b/playground/nats/Nats.AppHost/Directory.Build.props
new file mode 100644
index 00000000000..b9b39c05e81
--- /dev/null
+++ b/playground/nats/Nats.AppHost/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/playground/nats/Nats.AppHost/Directory.Build.targets b/playground/nats/Nats.AppHost/Directory.Build.targets
new file mode 100644
index 00000000000..b7ba77268f8
--- /dev/null
+++ b/playground/nats/Nats.AppHost/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/playground/nats/Nats.AppHost/Nats.AppHost.csproj b/playground/nats/Nats.AppHost/Nats.AppHost.csproj
new file mode 100644
index 00000000000..c3170c453a6
--- /dev/null
+++ b/playground/nats/Nats.AppHost/Nats.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ Nats.AppHosting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/nats/Nats.AppHost/Program.cs b/playground/nats/Nats.AppHost/Program.cs
new file mode 100644
index 00000000000..100b340df3d
--- /dev/null
+++ b/playground/nats/Nats.AppHost/Program.cs
@@ -0,0 +1,12 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var nats = builder.AddNats("nats")
+ .WithJetStream();
+
+builder.AddProject("api")
+ .WithReference(nats);
+
+builder.AddProject("backend")
+ .WithReference(nats);
+
+builder.Build().Run();
diff --git a/playground/nats/Nats.AppHost/Properties/launchSettings.json b/playground/nats/Nats.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000000..8f20c37b3fd
--- /dev/null
+++ b/playground/nats/Nats.AppHost/Properties/launchSettings.json
@@ -0,0 +1,15 @@
+{
+ "profiles": {
+ "run": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "dotnetRunMessages": true,
+ "applicationUrl": "http://localhost:18888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ },
+ "$schema": "http://json.schemastore.org/launchsettings.json"
+}
diff --git a/playground/nats/Nats.AppHost/aspire-manifest.json b/playground/nats/Nats.AppHost/aspire-manifest.json
new file mode 100644
index 00000000000..f1a6652ff3d
--- /dev/null
+++ b/playground/nats/Nats.AppHost/aspire-manifest.json
@@ -0,0 +1,62 @@
+{
+ "resources": {
+ "nats": {
+ "type": "container.v0",
+ "connectionString": "nats://{nats.bindings.tcp.host}:{nats.bindings.tcp.port}",
+ "image": "nats:2",
+ "args": [
+ "-js"
+ ],
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "containerPort": 4222
+ }
+ }
+ },
+ "api": {
+ "type": "project.v0",
+ "path": "../Nats.ApiService/Nats.ApiService.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "ConnectionStrings__nats": "{nats.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ },
+ "backend": {
+ "type": "project.v0",
+ "path": "../Nats.Backend/Nats.Backend.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "ConnectionStrings__nats": "{nats.connectionString}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/nats/Nats.Backend/Nats.Backend.csproj b/playground/nats/Nats.Backend/Nats.Backend.csproj
new file mode 100644
index 00000000000..63b7c0d24fe
--- /dev/null
+++ b/playground/nats/Nats.Backend/Nats.Backend.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
diff --git a/playground/nats/Nats.Backend/Program.cs b/playground/nats/Nats.Backend/Program.cs
new file mode 100644
index 00000000000..189e8e17411
--- /dev/null
+++ b/playground/nats/Nats.Backend/Program.cs
@@ -0,0 +1,45 @@
+using NATS.Client.Core;
+using Nats.Common;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddNatsClient("nats", configureOptions: opts =>
+{
+ var jsonRegistry = new NatsJsonContextSerializerRegistry(AppJsonContext.Default);
+ return opts with { SerializerRegistry = jsonRegistry };
+});
+
+builder.Services.AddHostedService();
+
+var app = builder.Build();
+
+app.Run();
+
+public class AppEventsBackendService(INatsConnection nats, ILogger logger) : IHostedService
+{
+ private readonly CancellationTokenSource _cts = new();
+ private Task? _subscription;
+
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ _subscription = Task.Run(async () =>
+ {
+ await foreach (var msg in nats.SubscribeAsync("events.>", cancellationToken: _cts.Token).ConfigureAwait(false))
+ {
+ logger.LogInformation("Processing event: {Data}", msg.Data);
+ }
+ }, cancellationToken);
+ return Task.CompletedTask;
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await _cts.CancelAsync();
+ if (_subscription != null)
+ {
+ await _subscription;
+ }
+ }
+}
diff --git a/playground/nats/Nats.Backend/Properties/launchSettings.json b/playground/nats/Nats.Backend/Properties/launchSettings.json
new file mode 100644
index 00000000000..c245832a4f4
--- /dev/null
+++ b/playground/nats/Nats.Backend/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7061;http://localhost:5157",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/nats/Nats.Common/AppEvent.cs b/playground/nats/Nats.Common/AppEvent.cs
new file mode 100644
index 00000000000..185a4ffbefc
--- /dev/null
+++ b/playground/nats/Nats.Common/AppEvent.cs
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Serialization;
+
+namespace Nats.Common;
+
+public record AppEvent(string Subject, string Name, string Description, decimal Priority);
+
+[JsonSerializable(typeof(AppEvent))]
+[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
+public partial class AppJsonContext : JsonSerializerContext
+{
+}
diff --git a/playground/nats/Nats.Common/Nats.Common.csproj b/playground/nats/Nats.Common/Nats.Common.csproj
new file mode 100644
index 00000000000..3a635329525
--- /dev/null
+++ b/playground/nats/Nats.Common/Nats.Common.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/spelling.dic b/spelling.dic
index aa0c9108f1a..fa2eff6f45b 100644
--- a/spelling.dic
+++ b/spelling.dic
@@ -32,6 +32,7 @@ microsoft
mongodb
mssql
mysql
+nats
noreferrer
noopener
npgsql
diff --git a/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs b/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs
new file mode 100644
index 00000000000..db2a486f350
--- /dev/null
+++ b/src/Aspire.Hosting/Nats/NatsBuilderExtensions.cs
@@ -0,0 +1,48 @@
+// 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;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding NATS resources to the application model.
+///
+public static class NatsBuilderExtensions
+{
+ ///
+ /// Adds a NATS server resource to the application model. A container is used for local development.
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The host port for NATS server.
+ /// A reference to the .
+ public static IResourceBuilder AddNats(this IDistributedApplicationBuilder builder, string name, int? port = null)
+ {
+ var nats = new NatsServerResource(name);
+ return builder.AddResource(nats)
+ .WithEndpoint(containerPort: 4222, hostPort: port, name: NatsServerResource.PrimaryEndpointName)
+ .WithAnnotation(new ContainerImageAnnotation { Image = "nats" })
+ .WithImageTag("2")
+ .PublishAsContainer();
+ }
+
+ ///
+ /// Adds JetStream support to the NATS server resource.
+ ///
+ /// NATS resource builder.
+ /// Optional mount path providing persistence between restarts.
+ /// A reference to the .
+ public static IResourceBuilder WithJetStream(this IResourceBuilder builder, string? srcMountPath = null)
+ {
+ var args = new List { "-js" };
+ if (srcMountPath != null)
+ {
+ args.Add("-sd");
+ args.Add("/data");
+ builder.WithBindMount(srcMountPath, "/data");
+ }
+
+ return builder.WithArgs(args.ToArray());
+ }
+}
diff --git a/src/Aspire.Hosting/Nats/NatsServerResource.cs b/src/Aspire.Hosting/Nats/NatsServerResource.cs
new file mode 100644
index 00000000000..36aecef11c0
--- /dev/null
+++ b/src/Aspire.Hosting/Nats/NatsServerResource.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a NATS server container.
+///
+/// The name of the resource.
+
+public class NatsServerResource(string name) : ContainerResource(name), IResourceWithConnectionString
+{
+ internal const string PrimaryEndpointName = "tcp";
+ internal const string PrimaryNatsSchemeName = "nats";
+
+ private EndpointReference? _primaryEndpoint;
+
+ ///
+ /// Gets the primary endpoint for the NATS server.
+ ///
+ public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);
+
+ ///
+ /// Gets the connection string expression for the NATS server for the manifest.
+ ///
+ public string? ConnectionStringExpression => $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}";
+
+ ///
+ /// Gets the connection string (NATS_URL) for the NATS server.
+ ///
+ /// A connection string for the NATS server in the form "nats://host:port".
+
+ public string GetConnectionString()
+ {
+ return $"{PrimaryNatsSchemeName}://{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}";
+ }
+}
diff --git a/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj b/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj
new file mode 100644
index 00000000000..38ad39930a3
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/Aspire.NATS.Net.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(NetCurrent)
+ true
+ $(ComponentDatabasePackageTags) nats messaging
+ A NATS client that integrates with Aspire, including health checks, metrics, logging, and telemetry.
+ $(SharedDir)nats-icon.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs b/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs
new file mode 100644
index 00000000000..e403d1923db
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/AspireNatsClientExtensions.cs
@@ -0,0 +1,128 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.NATS.Net;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using NATS.Client.Core;
+using NATS.Client.JetStream;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Extension methods for connecting NATS server with NATS client
+///
+public static class AspireNatsClientExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Nats:Client";
+
+ ///
+ /// Registers service for connecting NATS server with NATS client.
+ /// Configures health check and logging for the NATS client.
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// An optional delegate that can be used for customizing NATS options that aren't exposed as standard configuration.
+ /// Thrown if mandatory is null.
+ /// Thrown when mandatory is not provided.
+ public static void AddNatsClient(this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null, Func? configureOptions = null)
+ => AddNatsClient(builder, configurationSectionName: DefaultConfigSectionName, connectionName: connectionName, serviceKey: null, configureSettings: configureSettings, configureOptions: configureOptions);
+
+ ///
+ /// Registers as a keyed service for given for connecting NATS server with NATS client.
+ /// Configures health check and logging for the NATS client.
+ ///
+ /// The to read config from and add services to.
+ /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// An optional delegate that can be used for customizing NATS options that aren't exposed as standard configuration.
+ /// Thrown when or is null.
+ /// Thrown if mandatory is empty.
+ /// Thrown when mandatory is not provided.
+ public static void AddKeyedNatsClient(this IHostApplicationBuilder builder, string name, Action? configureSettings = null, Func? configureOptions = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ AddNatsClient(builder, configurationSectionName: $"{DefaultConfigSectionName}:{name}", connectionName: name, serviceKey: name, configureSettings: configureSettings, configureOptions: configureOptions);
+ }
+
+ private static void AddNatsClient(this IHostApplicationBuilder builder, string configurationSectionName, string connectionName, object? serviceKey, Action? configureSettings, Func? configureOptions)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ NatsClientSettings settings = new();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ConnectionString = connectionString;
+ }
+
+ configureSettings?.Invoke(settings);
+
+ NatsConnection Factory(IServiceProvider provider)
+ {
+ var options = NatsOpts.Default with
+ {
+ LoggerFactory = provider.GetRequiredService(),
+ };
+
+ if (configureOptions != null)
+ {
+ options = configureOptions(options);
+ }
+
+ if (settings.ConnectionString == null)
+ {
+ throw new InvalidOperationException($"NATS connection string not found: {connectionName}");
+ }
+
+ options = options with { Url = settings.ConnectionString };
+
+ return new NatsConnection(options);
+ }
+
+ if (serviceKey == null)
+ {
+ builder.Services.TryAddSingleton(Factory);
+ builder.Services.TryAddSingleton(static provider => provider.GetRequiredService());
+ }
+ else
+ {
+ builder.Services.TryAddKeyedSingleton(serviceKey, (provider, _) => Factory(provider));
+ builder.Services.TryAddKeyedSingleton(serviceKey, static (provider, key) => provider.GetRequiredKeyedService(key));
+ }
+
+ if (settings.HealthChecks)
+ {
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? "NATS" : $"NATS_{connectionName}",
+ sp => new NatsHealthCheck(serviceKey is null
+ ? sp.GetRequiredService()
+ : sp.GetRequiredKeyedService(serviceKey)),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
+ }
+ }
+
+ ///
+ /// Registers service for NATS JetStream operations.
+ ///
+ /// The to read config from and add services to.
+ /// Thrown if mandatory is null.
+ public static void AddNatsJetStream(this IHostApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ builder.Services.AddSingleton(static provider =>
+ {
+ return new NatsJSContextFactory().CreateContext(provider.GetService()!);
+ });
+ }
+}
diff --git a/src/Components/Aspire.NATS.Net/AssemblyInfo.cs b/src/Components/Aspire.NATS.Net/AssemblyInfo.cs
new file mode 100644
index 00000000000..fa49817fdad
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/AssemblyInfo.cs
@@ -0,0 +1,9 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.NATS.Net;
+
+[assembly: ConfigurationSchema("Aspire:NATS:Net", typeof(NatsClientSettings))]
+
+[assembly: LoggingCategories("NATS")]
diff --git a/src/Components/Aspire.NATS.Net/ConfigurationSchema.json b/src/Components/Aspire.NATS.Net/ConfigurationSchema.json
new file mode 100644
index 00000000000..e5f2e7bca53
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/ConfigurationSchema.json
@@ -0,0 +1,39 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "NATS": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ },
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "NATS": {
+ "type": "object",
+ "properties": {
+ "Net": {
+ "type": "object",
+ "properties": {
+ "ConnectionString": {
+ "type": "string",
+ "description": "Gets or sets the connection string of the NATS cluster to connect to."
+ },
+ "HealthChecks": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the NATS health check is enabled or not.",
+ "default": true
+ }
+ },
+ "description": "Provides the client configuration settings for connecting to a NATS cluster."
+ }
+ }
+ }
+ }
+ }
+ },
+ "type": "object"
+}
diff --git a/src/Components/Aspire.NATS.Net/NatsClientSettings.cs b/src/Components/Aspire.NATS.Net/NatsClientSettings.cs
new file mode 100644
index 00000000000..5b91f6ff060
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/NatsClientSettings.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.NATS.Net;
+
+///
+/// Provides the client configuration settings for connecting to a NATS cluster.
+///
+public sealed class NatsClientSettings
+{
+ ///
+ /// Gets or sets the connection string of the NATS cluster to connect to.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the NATS health check is enabled or not.
+ ///
+ ///
+ /// The default value is .
+ ///
+ public bool HealthChecks { get; set; } = true;
+}
diff --git a/src/Components/Aspire.NATS.Net/NatsHealthCheck.cs b/src/Components/Aspire.NATS.Net/NatsHealthCheck.cs
new file mode 100644
index 00000000000..60abdefcc10
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/NatsHealthCheck.cs
@@ -0,0 +1,23 @@
+// 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.Diagnostics.HealthChecks;
+using NATS.Client.Core;
+
+namespace Aspire.NATS.Net;
+
+internal sealed class NatsHealthCheck(INatsConnection connection) : IHealthCheck
+{
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ var result = connection.ConnectionState switch
+ {
+ NatsConnectionState.Open => HealthCheckResult.Healthy(),
+ NatsConnectionState.Connecting or NatsConnectionState.Reconnecting => HealthCheckResult.Degraded(),
+ NatsConnectionState.Closed => HealthCheckResult.Unhealthy(),
+ _ => new HealthCheckResult(context.Registration.FailureStatus)
+ };
+
+ return Task.FromResult(result);
+ }
+}
diff --git a/src/Components/Aspire.NATS.Net/README.md b/src/Components/Aspire.NATS.Net/README.md
new file mode 100644
index 00000000000..c980f3cdc68
--- /dev/null
+++ b/src/Components/Aspire.NATS.Net/README.md
@@ -0,0 +1,110 @@
+# Aspire.NATS.Net library
+
+Registers [INatsConnection](https://nats-io.github.io/nats.net.v2/api/NATS.Client.Core.INatsConnection.html) in the DI container for connecting NATS server. Enables corresponding health check, metrics, logging and telemetry.
+
+## Getting started
+
+### Prerequisites
+
+- NATS server and the server URL for accessing the server.
+
+### Install the package
+
+Install the .NET Aspire NATS library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.NATS.Net
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddNatsClient` extension method to register a `INatsConnection` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddNatsClient("nats");
+```
+
+You can then retrieve a `INatsConnection` instance using dependency injection. For example, to retrieve a connection from a Web API controller:
+
+```csharp
+private readonly INatsConnection _connection;
+
+public ProductsController(INatsConnection connection)
+{
+ _connection = connection;
+}
+```
+
+## Configuration
+
+The .NET Aspire NATS component provides multiple options to configure the NATS 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.AddNats()`:
+
+```csharp
+builder.AddNats("myConnection");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "myConnection": "nats://nats:4222"
+ }
+}
+```
+
+See the [ConnectionString documentation](https://docs.nats.io/using-nats/developer/connecting#nats-url) for more information on how to format this connection string.
+
+### Use configuration providers
+
+The .NET Aspire NATS component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `NatsClientSettings` from configuration by using the `Aspire:Nats:Client` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "Nats": {
+ "Client": {
+ "HealthChecks": false
+ }
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code:
+
+```csharp
+ builder.AddNats("nats", settings => settings.HealthChecks = false);
+```
+
+## AppHost extensions
+
+In your AppHost project, register a NATS server and consume the connection using the following methods:
+
+```csharp
+var nats = builder.AddNats("nats");
+
+var myService = builder.AddProject()
+ .WithReference(nats);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `nats`. In the _Program.cs_ file of `MyService`, the NATS connection can be consumed using:
+
+```csharp
+builder.AddNatsClient("nats");
+```
+
+## Additional documentation
+
+* https://nats-io.github.io/nats.net.v2/documentation/intro.html
+* https://github.com/dotnet/aspire/tree/main/src/Components/README.md
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md
index 351f36cb6e1..9568c4244da 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -26,6 +26,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As
| Oracle.EntityFrameworkCore | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Confluent.Kafka | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Pomelo.EntityFrameworkCore.MySql | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| NATS.Net | ✅ | ✅ | ✅ | ✅ | ✅ | | | ✅ |
Nomenclature used in the table above:
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index 1201bcb42a7..f3183112356 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -199,6 +199,14 @@ Aspire.MySqlConnector:
- "db.client.connections.timeouts"
- "db.client.connections.usage"
+Aspire.NATS.Net:
+- Log categories:
+ - "NATS"
+- Activity source names:
+ - none (currently not supported by NATS.Net library)
+- Metric names:
+ - none (currently not supported by NATS.Net library)
+
Aspire.Npgsql:
- Log categories:
- "Npgsql.Command"
diff --git a/src/Shared/nats-icon.png b/src/Shared/nats-icon.png
new file mode 100644
index 00000000000..95044a73197
Binary files /dev/null and b/src/Shared/nats-icon.png differ
diff --git a/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs
new file mode 100644
index 00000000000..136537f3ae0
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Nats/AddNatsTests.cs
@@ -0,0 +1,121 @@
+// 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.Utils;
+using System.Net.Sockets;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Aspire.Hosting.Tests.Nats;
+
+public class AddNatsTests
+{
+ [Fact]
+ public void AddNatsContainerWithDefaultsAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+
+ appBuilder.AddNats("nats");
+
+ using var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("nats", containerResource.Name);
+
+ var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.NotNull(manifestAnnotation.Callback);
+
+ var endpoint = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(4222, endpoint.ContainerPort);
+ 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("2", containerAnnotation.Tag);
+ Assert.Equal("nats", containerAnnotation.Image);
+ Assert.Null(containerAnnotation.Registry);
+ }
+
+ [Fact]
+ public void AddNatsContainerAddsAnnotationMetadata()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ appBuilder.AddNats("nats", 1234).WithJetStream(srcMountPath: "/tmp/dev-data");
+
+ var app = appBuilder.Build();
+
+ var appModel = app.Services.GetRequiredService();
+
+ var containerResource = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("nats", containerResource.Name);
+
+ var manifestAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.NotNull(manifestAnnotation.Callback);
+
+ var mountAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal("/tmp/dev-data", mountAnnotation.Source);
+ Assert.Equal("/data", mountAnnotation.Target);
+
+ var argsAnnotation = Assert.Single(containerResource.Annotations.OfType());
+ Assert.NotNull(argsAnnotation.Callback);
+ var args = new List();
+ argsAnnotation.Callback(new CommandLineArgsCallbackContext(args));
+ Assert.Equal("-js -sd /data".Split(' '), args);
+
+ var endpoint = Assert.Single(containerResource.Annotations.OfType());
+ Assert.Equal(4222, endpoint.ContainerPort);
+ Assert.False(endpoint.IsExternal);
+ Assert.Equal("tcp", endpoint.Name);
+ Assert.Equal(1234, 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("2", containerAnnotation.Tag);
+ Assert.Equal("nats", containerAnnotation.Image);
+ Assert.Null(containerAnnotation.Registry);
+ }
+
+ [Fact]
+ public void WithNatsContainerOnMultipleResources()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ builder.AddNats("nats1");
+ builder.AddNats("nats2");
+
+ Assert.Equal(2, builder.Resources.OfType().Count());
+ }
+
+ [Fact]
+ public async Task VerifyManifest()
+ {
+ var appBuilder = DistributedApplication.CreateBuilder();
+ var nats = appBuilder.AddNats("nats");
+
+ var manifest = await ManifestUtils.GetManifest(nats.Resource);
+
+ var expectedManifest = """
+ {
+ "type": "container.v0",
+ "connectionString": "nats://{nats.bindings.tcp.host}:{nats.bindings.tcp.port}",
+ "image": "nats:2",
+ "bindings": {
+ "tcp": {
+ "scheme": "tcp",
+ "protocol": "tcp",
+ "transport": "tcp",
+ "containerPort": 4222
+ }
+ }
+ }
+ """;
+ Assert.Equal(expectedManifest, manifest.ToString());
+ }
+}
diff --git a/tests/Aspire.NATS.Net.Tests/Aspire.NATS.Net.Tests.csproj b/tests/Aspire.NATS.Net.Tests/Aspire.NATS.Net.Tests.csproj
new file mode 100644
index 00000000000..a25c05a63af
--- /dev/null
+++ b/tests/Aspire.NATS.Net.Tests/Aspire.NATS.Net.Tests.csproj
@@ -0,0 +1,14 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs b/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs
new file mode 100644
index 00000000000..c5c3eac9367
--- /dev/null
+++ b/tests/Aspire.NATS.Net.Tests/AspireNatsClientExtensionsTests.cs
@@ -0,0 +1,104 @@
+// 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.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using NATS.Client.Core;
+using Xunit;
+
+namespace Aspire.NATS.Net.Tests;
+
+public class AspireNatsClientExtensionsTests
+{
+ private const string ConnectionString = "nats://apire-host:4222";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadsFromConnectionStringsCorrectly(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:nats", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedNatsClient("nats");
+ }
+ else
+ {
+ builder.AddNatsClient("nats");
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("nats") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.Opts.Url);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionStringCanBeSetInCode(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:nats", "unused")
+ ]);
+
+ static void SetConnectionString(NatsClientSettings settings) => settings.ConnectionString = ConnectionString;
+ if (useKeyed)
+ {
+ builder.AddKeyedNatsClient("nats", SetConnectionString);
+ }
+ else
+ {
+ builder.AddNatsClient("nats", SetConnectionString);
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("nats") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.Opts.Url);
+ // the connection string from config should not be used since code set it explicitly
+ Assert.DoesNotContain("unused", connection.Opts.Url);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionNameWinsOverConfigSection(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ var key = useKeyed ? "nats" : null;
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:Nats:Client", key, "ConnectionString"), "unused"),
+ new KeyValuePair("ConnectionStrings:nats", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedNatsClient("nats");
+ }
+ else
+ {
+ builder.AddNatsClient("nats");
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("nats") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.Opts.Url);
+ // the connection string from config should not be used since it was found in ConnectionStrings
+ Assert.DoesNotContain("unused", connection.Opts.Url);
+ }
+}
diff --git a/tests/Aspire.NATS.Net.Tests/ConfigurationTests.cs b/tests/Aspire.NATS.Net.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000000..e655a498062
--- /dev/null
+++ b/tests/Aspire.NATS.Net.Tests/ConfigurationTests.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Aspire.NATS.Net.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void ConnectionStringIsNullByDefault()
+ => Assert.Null(new NatsClientSettings().ConnectionString);
+
+ [Fact]
+ public void HealthCheckIsEnabledByDefault()
+ => Assert.True(new NatsClientSettings().HealthChecks);
+}
diff --git a/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs
new file mode 100644
index 00000000000..122e359713e
--- /dev/null
+++ b/tests/Aspire.NATS.Net.Tests/ConformanceTests.cs
@@ -0,0 +1,66 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Components.ConformanceTests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using NATS.Client.Core;
+
+namespace Aspire.NATS.Net.Tests;
+
+public class ConformanceTests : ConformanceTests
+{
+ private const string ConnectionSting = "nats://apire-host:4222";
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+ protected override string ActivitySourceName => "Nats.Client";
+ protected override string[] RequiredLogCategories => ["NATS.Client"];
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddNatsClient("nats", configure);
+ }
+ else
+ {
+ builder.AddKeyedNatsClient(key, configure);
+ }
+ }
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ => configuration.AddInMemoryCollection(new KeyValuePair[1]
+ {
+ new KeyValuePair(CreateConfigKey("Aspire:Nats:Client", key, "ConnectionString"), ConnectionSting)
+ });
+
+ protected override bool CanCreateClientWithoutConnectingToServer => false;
+
+ protected override void TriggerActivity(INatsConnection service)
+ {
+ }
+
+ protected override void SetHealthCheck(NatsClientSettings options, bool enabled)
+ => options.HealthChecks = enabled;
+
+ protected override void SetTracing(NatsClientSettings options, bool enabled)
+ => throw new NotImplementedException();
+
+ protected override void SetMetrics(NatsClientSettings options, bool enabled)
+ => throw new NotImplementedException();
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Nats": {
+ "Client": {
+ "ConnectionString": "YOUR_CONNECTION_STRING",
+ "HealthChecks": false,
+ "Tracing": true,
+ "Metrics": true
+ }
+ }
+ }
+ }
+ """;
+}