diff --git a/Directory.Packages.props b/Directory.Packages.props
index 96ca5f181..02d73f9e3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -77,6 +77,7 @@
+
diff --git a/examples/floci/CommunityToolkit.Aspire.Hosting.Floci.AppHost.TypeScript/apphost.mts b/examples/floci/CommunityToolkit.Aspire.Hosting.Floci.AppHost.TypeScript/apphost.mts
index 4c498a58d..0a9f7ccce 100644
--- a/examples/floci/CommunityToolkit.Aspire.Hosting.Floci.AppHost.TypeScript/apphost.mts
+++ b/examples/floci/CommunityToolkit.Aspire.Hosting.Floci.AppHost.TypeScript/apphost.mts
@@ -1,3 +1,5 @@
+import { mkdirSync } from "node:fs";
+import { randomUUID } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createBuilder } from './.aspire/modules/aspire.mjs';
@@ -9,6 +11,13 @@ const flociAws = await builder.addFlociAws('floci-aws');
const flociAzure = await builder.addFlociAzure('floci-az');
const flociGcp = await builder.addFlociGcp('floci-gcp');
+await flociAzure.withDockerSocket();
+await flociAzure.withEnvironment('FLOCI_AZ_DOCKER_RESOURCE_NAMESPACE', `sample-${randomUUID()}`);
+const serviceBus = await flociAzure.withServiceBus({ name: 'servicebus' });
+await serviceBus.amqpEndpoint();
+await serviceBus.amqpTlsEndpoint();
+await serviceBus.connectionStringExpression();
+
// A single Floci UI console browses all three clouds — flociAws.withFlociUI() creates the
// console wired to AWS, then withCloudReference* attaches the Azure and GCP resources to it.
await flociAws.withFlociUI({
@@ -28,10 +37,42 @@ const apiService = await builder.addProject("floci-api", apiServiceProjectPath)
.withFlociAwsReference(flociAws)
.withFlociAzureReference(flociAzure)
.withFlociGcpReference(flociGcp)
+ .withFlociAzureServiceBusReference(serviceBus)
.waitFor(flociAws)
.waitFor(flociAzure)
.waitFor(flociGcp);
+// Resolve the returned child handle from a real container and require an AMQP response.
+await builder.addContainer('servicebus-probe', 'node:22-alpine')
+ .withFlociAzureServiceBusReference(serviceBus)
+ .withHttpEndpoint({ targetPort: 8080 })
+ .withHttpHealthCheck({ path: '/' })
+ .waitFor(flociAzure)
+ .withArgs(['-e', `
+ const net = require('node:net');
+ const http = require('node:http');
+ const assert = require('node:assert/strict');
+ const connectionString = process.env.ConnectionStrings__servicebus;
+ const endpoint = new URL(connectionString.split(';')[0].slice('Endpoint='.length));
+ const header = Buffer.from([65, 77, 81, 80, 0, 1, 0, 0]);
+ let ready = false;
+ const socket = net.connect({ host: endpoint.hostname, port: Number(endpoint.port) }, () => socket.write(header));
+ socket.setTimeout(20000, () => { throw new Error('AMQP handshake timed out'); });
+ let response = Buffer.alloc(0);
+ socket.on('data', data => {
+ response = Buffer.concat([response, data]);
+ if (response.length >= header.length) {
+ assert.deepEqual(response.subarray(0, header.length), header);
+ ready = true;
+ socket.destroy();
+ }
+ });
+ http.createServer((request, response) => {
+ response.writeHead(ready ? 200 : 503);
+ response.end();
+ }).listen(8080, '0.0.0.0');
+ `]);
+
// ── Custom port and region ────────────────────────────────────────────────────
await builder.addFlociAws('floci-custom', {
port: 14566,
@@ -51,7 +92,9 @@ await flociPersistent.withDataVolume('floci-data');
// ── Persistent storage — bind mount ───────────────────────────────────────────
const flociMount = await builder.addFlociAws('floci-mount');
-await flociMount.withDataBindMount('/tmp/floci-data');
+const flociDataPath = path.join(appHostDirectory, '.aspire', 'floci-data');
+mkdirSync(flociDataPath, { recursive: true });
+await flociMount.withDataBindMount(flociDataPath);
// ── Compile-time coverage ─────────────────────────────────────────────────────
// Guards with false so these are type-checked but never executed.
@@ -82,6 +125,7 @@ if (includeCompileOnlyScenarios) {
await _azurePodman.withDockerSocket({
socketPath: '/run/user/1000/podman/podman.sock'
});
+ await _azurePodman.withServiceBus({ name: 'podman-servicebus', amqpPort: 15673, amqpTlsPort: 15674 });
const _gcpPodman = await builder.addFlociGcp('floci-gcp-podman');
await _gcpPodman.withDockerSocket({
diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusConnectionString.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusConnectionString.cs
new file mode 100644
index 000000000..860888705
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusConnectionString.cs
@@ -0,0 +1,21 @@
+using Aspire.Hosting.ApplicationModel;
+
+namespace CommunityToolkit.Aspire.Hosting.Floci;
+
+// The sidecar publishes directly on the Docker host. Expose the parent as the dependency
+// so Aspire does not create a container tunnel for the child's proxyless listeners.
+internal sealed class FlociAzureServiceBusConnectionString(FlociAzureServiceBusResource resource)
+ : IValueProvider, IManifestExpressionProvider, IValueWithReferences
+{
+ internal const string ContainerHost = "floci-servicebus-host.internal";
+
+ public async ValueTask GetValueAsync(CancellationToken cancellationToken = default)
+ {
+ string? port = await resource.AmqpEndpoint.Property(EndpointProperty.Port).GetValueAsync(cancellationToken);
+ return $"Endpoint=sb://{ContainerHost}:{port};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={FlociAzureServiceBusResource.DefaultSasKey};UseDevelopmentEmulator=true;";
+ }
+
+ public string ValueExpression => throw new NotSupportedException("Floci Service Bus references are only supported in run mode.");
+
+ public IEnumerable
+
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/TypeScriptAppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/TypeScriptAppHostTests.cs
index b63854c2d..f19eb4b10 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/TypeScriptAppHostTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/TypeScriptAppHostTests.cs
@@ -13,7 +13,7 @@ await TypeScriptAppHostTest.Run(
appHostProject: "CommunityToolkit.Aspire.Hosting.Floci.AppHost.TypeScript",
packageName: "CommunityToolkit.Aspire.Hosting.Floci",
exampleName: "floci",
- waitForResources: ["floci-aws", "floci-az", "floci-gcp", "floci-aws-ui", "floci-custom", "floci-gcp-custom", "floci-persistent", "floci-mount"],
+ waitForResources: ["floci-aws", "floci-az", "floci-gcp", "floci-aws-ui", "floci-custom", "floci-gcp-custom", "floci-persistent", "floci-mount", "servicebus-probe"],
cancellationToken: TestContext.Current.CancellationToken);
}
}