diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusResource.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusResource.cs
new file mode 100644
index 000000000..0ab1db5c7
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociAzureServiceBusResource.cs
@@ -0,0 +1,66 @@
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Represents the Service Bus AMQP data plane exposed by a Floci Azure emulator resource.
+///
+///
+/// floci-az serves Service Bus AMQP from an Artemis sidecar container that publishes the
+/// configured ports directly on the Docker host. The resource models those host ports as
+/// proxyless Aspire endpoints so DCP can allocate them without trying to proxy traffic to the
+/// parent container.
+///
+/// The name of the resource.
+/// The parent Floci Azure emulator resource.
+[AspireExport(ExposeProperties = true)]
+public class FlociAzureServiceBusResource(
+ string name,
+ FlociAzureContainerResource parent) : Resource(name),
+ IResourceWithParent,
+ IResourceWithConnectionString,
+ IResourceWithEndpoints
+{
+ internal const string DefaultName = "servicebus";
+ internal const string AmqpEndpointName = "amqp";
+ internal const string AmqpTlsEndpointName = "amqps";
+
+ private EndpointReference? _amqpEndpoint;
+ private EndpointReference? _amqpTlsEndpoint;
+
+ // Placeholder from the official Service Bus emulator's connection-string shape; floci-az
+ // does not enforce authentication, the SDK only requires the component to be present.
+ internal const string DefaultSasKey = "SAS_KEY_VALUE";
+
+ ///
+ /// Gets the parent Floci Azure emulator resource.
+ ///
+ public FlociAzureContainerResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));
+
+ ///
+ /// Gets the Service Bus plain AMQP endpoint.
+ ///
+ public EndpointReference AmqpEndpoint =>
+ _amqpEndpoint ??= new EndpointReference(this, AmqpEndpointName);
+
+ ///
+ /// Gets the Service Bus AMQPS/TLS endpoint.
+ ///
+ public EndpointReference AmqpTlsEndpoint =>
+ _amqpTlsEndpoint ??= new EndpointReference(this, AmqpTlsEndpointName);
+
+ ///
+ /// Gets the Service Bus connection string expression.
+ /// UseDevelopmentEmulator=true makes the Azure SDKs use plain AMQP (no TLS), matching
+ /// the official Service Bus emulator's connection-string shape.
+ ///
+ public ReferenceExpression ConnectionStringExpression =>
+ ReferenceExpression.Create(
+ $"Endpoint={AmqpEndpoint};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={DefaultSasKey};UseDevelopmentEmulator=true;");
+
+ IEnumerable> IResourceWithConnectionString.GetConnectionProperties() =>
+ Parent.CombineProperties([
+ new("Host", ReferenceExpression.Create($"{AmqpEndpoint.Property(EndpointProperty.Host)}")),
+ new("Port", ReferenceExpression.Create($"{AmqpEndpoint.Property(EndpointProperty.Port)}")),
+ new("Uri", ReferenceExpression.Create($"{AmqpEndpoint}")),
+ new("Endpoint", ReferenceExpression.Create($"{AmqpEndpoint}"))
+ ]);
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociContainerImageTags.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociContainerImageTags.cs
index e619061f7..46533afb7 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociContainerImageTags.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociContainerImageTags.cs
@@ -8,7 +8,7 @@ internal static class FlociContainerImageTags
public const string AzureRegistry = "docker.io";
public const string AzureImage = "floci/floci-az";
- public const string AzureTag = "0.11.0";
+ public const string AzureTag = "0.12.0";
public const string GcpRegistry = "docker.io";
public const string GcpImage = "floci/floci-gcp";
diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs
index 90ec94c08..1c763c6a8 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Floci/FlociHostingExtension.Azure.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.Floci;
@@ -73,6 +74,85 @@ public static IResourceBuilder WithReference(
});
+ ///
+ /// Adds a child resource representing the Service Bus AMQP data plane exposed by the Floci
+ /// Azure emulator, enabling the data plane on the emulator (MOCKED=false,
+ /// START_ON_BOOT=true).
+ ///
+ ///
+ /// Reference the returned resource with Aspire's standard WithReference API to inject
+ /// its Service Bus connection string (e.g. for AddAzureServiceBusClient). The Artemis
+ /// sidecar is a separate container floci-az starts via Docker, so the emulator also needs
+ /// .
+ /// Aspire allocates proxyless AMQP host endpoints when ports are not specified. Requires
+ /// floci-az 0.12.0 or later.
+ ///
+ /// Adds a Service Bus child resource to the Floci Azure emulator
+ /// The Floci Azure resource builder.
+ /// The name of the Service Bus resource (default: servicebus).
+ /// Host port for plain AMQP (default: allocated by Aspire).
+ /// Host port for AMQPS/TLS (default: allocated by Aspire).
+ /// A reference to the for further configuration.
+ [AspireExport]
+ public static IResourceBuilder WithServiceBus(
+ this IResourceBuilder builder,
+ [ResourceName] string name = FlociAzureServiceBusResource.DefaultName,
+ int? amqpPort = null,
+ int? amqpTlsPort = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ FlociAzureServiceBusResource? existing = builder.ApplicationBuilder.Resources
+ .OfType()
+ .FirstOrDefault(resource => resource.Parent == builder.Resource);
+ if (existing is not null)
+ {
+ int? existingAmqpPort = existing.AmqpEndpoint.EndpointAnnotation.Port;
+ int? existingAmqpTlsPort = existing.AmqpTlsEndpoint.EndpointAnnotation.Port;
+ if ((amqpPort is not null && amqpPort != existingAmqpPort)
+ || (amqpTlsPort is not null && amqpTlsPort != existingAmqpTlsPort))
+ {
+ throw new InvalidOperationException(
+ $"Service Bus is already configured on '{builder.Resource.Name}' and cannot be reconfigured with different ports.");
+ }
+
+ return builder.ApplicationBuilder.CreateResourceBuilder(existing);
+ }
+
+ var serviceBus = new FlociAzureServiceBusResource(name, builder.Resource);
+ var serviceBusBuilder = builder.ApplicationBuilder
+ .AddResource(serviceBus)
+ .WithEndpoint(
+ port: amqpPort,
+ scheme: "sb",
+ name: FlociAzureServiceBusResource.AmqpEndpointName,
+ isProxied: false)
+ .WithEndpoint(
+ port: amqpTlsPort,
+ scheme: "amqps",
+ name: FlociAzureServiceBusResource.AmqpTlsEndpointName,
+ isProxied: false)
+ .WithParentRelationship(builder);
+
+ builder.WithEnvironment(context =>
+ {
+ if (context.ExecutionContext.IsPublishMode)
+ {
+ return;
+ }
+
+ context.EnvironmentVariables["FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED"] = "false";
+ context.EnvironmentVariables["FLOCI_AZ_SERVICES_SERVICE_BUS_START_ON_BOOT"] = "true";
+ context.EnvironmentVariables["FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT"] =
+ serviceBus.AmqpEndpoint.Port.ToString(CultureInfo.InvariantCulture);
+ context.EnvironmentVariables["FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_TLS_PORT"] =
+ serviceBus.AmqpTlsEndpoint.Port.ToString(CultureInfo.InvariantCulture);
+ });
+
+ return serviceBusBuilder;
+ }
+
///
/// Adds a child resource representing the Cosmos DB API exposed by the Floci Azure emulator.
///
diff --git a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md
index 7c35d914e..d9e73a88f 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Floci/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.Floci/README.md
@@ -107,6 +107,38 @@ builder.AddAzureCosmosClient("cosmos");
The Cosmos child resource is additive, so combine `WithReference(cosmos)` with `WithReference(azure)` when you also want the base endpoint / storage variables. (Talking to the floci Cosmos emulator over HTTP from the .NET SDK still needs the usual client-side settings — Gateway mode, and HTTP/1.1 — which are the app's concern, as with any local Cosmos emulator.)
+For **Service Bus**, use `WithServiceBus()` / `withServiceBus()` to model the AMQP data plane as a child resource, then reference it through Aspire's standard connection-string flow:
+
+```csharp
+var azure = builder.AddFlociAzure("floci-az")
+ .WithDockerSocket();
+var serviceBus = azure.WithServiceBus();
+
+builder.AddProject("api")
+ .WithReference(serviceBus) // ConnectionStrings__servicebus
+ .WaitFor(azure);
+```
+
+```typescript
+const azure = (await builder.addFlociAzure('floci-az')).withDockerSocket();
+const serviceBus = await azure.withServiceBus();
+
+await builder.addProject('api', '../MyApi/MyApi.csproj')
+ .withReference(serviceBus)
+ .waitFor(azure);
+```
+
+App side, this is the standard Aspire flow:
+
+```csharp
+builder.AddAzureServiceBusClient("servicebus");
+```
+
+| Variable | Value |
+|---|---|
+| `ConnectionStrings__{resourceName}` (default `servicebus`) | `Endpoint=sb://localhost:{amqpPort};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;` — the official Service Bus emulator's connection-string shape |
+
+`WithServiceBus` sets `FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED=false` and `FLOCI_AZ_SERVICES_SERVICE_BUS_START_ON_BOOT=true`. Aspire models the sidecar's AMQP and AMQPS host ports as proxyless endpoints and allocates them by default; pass `amqpPort` / `amqpTlsPort` to use fixed ports. The management plane (for example, `ServiceBusAdministrationClient`) remains on the base endpoint from `WithReference(azure)`. Requires `WithDockerSocket()` and floci-az 0.12.0 or later.
**GCP**
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/AzureServiceBusResourceTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/AzureServiceBusResourceTests.cs
new file mode 100644
index 000000000..b60b5ba9b
--- /dev/null
+++ b/tests/CommunityToolkit.Aspire.Hosting.Floci.Tests/AzureServiceBusResourceTests.cs
@@ -0,0 +1,192 @@
+using Aspire.Hosting;
+
+namespace CommunityToolkit.Aspire.Hosting.Floci.Tests;
+
+public class AzureServiceBusResourceTests
+{
+ [Fact]
+ public void WithServiceBusCreatesChildResourceWithAspireAllocatedEndpoints()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var azure = builder.AddFlociAzure("floci-az");
+ azure.WithServiceBus();
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ var serviceBus = Assert.Single(appModel.Resources.OfType());
+ Assert.Equal("servicebus", serviceBus.Name);
+ Assert.Same(azure.Resource, serviceBus.Parent);
+
+ Assert.Collection(
+ serviceBus.Annotations.OfType().OrderBy(endpoint => endpoint.Name),
+ amqp => AssertEndpoint(amqp, "amqp", "sb", null),
+ amqps => AssertEndpoint(amqps, "amqps", "amqps", null));
+ }
+
+ [Fact]
+ public void WithServiceBusHonorsExplicitPorts()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var serviceBus = builder.AddFlociAzure("floci-az")
+ .WithServiceBus(amqpPort: 5673, amqpTlsPort: 5674);
+
+ Assert.Equal(5673, serviceBus.Resource.AmqpEndpoint.EndpointAnnotation.Port);
+ Assert.Equal(5674, serviceBus.Resource.AmqpTlsEndpoint.EndpointAnnotation.Port);
+ }
+
+ [Fact]
+ public async Task WithServiceBusUsesAllocatedEndpointPortsForTheDataPlane()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var azure = builder.AddFlociAzure("floci-az");
+ var serviceBus = azure.WithServiceBus();
+ AllocateEndpoints(serviceBus.Resource, 5673, 5674);
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+
+ var resource = Assert.Single(appModel.Resources.OfType());
+ Assert.True(resource.TryGetAnnotationsOfType(out IEnumerable? envAnnotations));
+
+ var envVars = new Dictionary();
+ var context = new EnvironmentCallbackContext(builder.ExecutionContext, envVars);
+ foreach (var annotation in envAnnotations!)
+ {
+ await annotation.Callback(context);
+ }
+
+ Assert.Equal("false", envVars["FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED"]);
+ Assert.Equal("true", envVars["FLOCI_AZ_SERVICES_SERVICE_BUS_START_ON_BOOT"]);
+ Assert.Equal("5673", envVars["FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT"]);
+ Assert.Equal("5674", envVars["FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_TLS_PORT"]);
+ }
+
+ [Fact]
+ public async Task WithServiceBusDoesNotConfigureTheDataPlaneInPublishMode()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var azure = builder.AddFlociAzure("floci-az");
+ azure.WithServiceBus();
+
+ var envVars = new Dictionary();
+ var executionContext = new DistributedApplicationExecutionContext(
+ new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Publish));
+ var context = new EnvironmentCallbackContext(executionContext, envVars);
+
+ foreach (var annotation in azure.Resource.Annotations.OfType())
+ {
+ await annotation.Callback(context);
+ }
+
+ Assert.DoesNotContain("FLOCI_AZ_SERVICES_SERVICE_BUS_MOCKED", envVars);
+ Assert.DoesNotContain("FLOCI_AZ_SERVICES_SERVICE_BUS_START_ON_BOOT", envVars);
+ Assert.DoesNotContain("FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_PORT", envVars);
+ Assert.DoesNotContain("FLOCI_AZ_SERVICES_SERVICE_BUS_AMQP_TLS_PORT", envVars);
+ }
+
+ [Fact]
+ public async Task ConnectionStringMatchesTheEmulatorShape()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var serviceBus = builder.AddFlociAzure("floci-az").WithServiceBus();
+ AllocateEndpoints(serviceBus.Resource, 5673, 5674);
+
+ string? connectionString = await serviceBus.Resource.ConnectionStringExpression
+ .GetValueAsync(CancellationToken.None);
+
+ Assert.Equal(
+ "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;" +
+ "SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ connectionString);
+ }
+
+ [Fact]
+ public void SecondWithServiceBusReturnsTheExistingChild()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var azure = builder.AddFlociAzure("floci-az");
+ var first = azure.WithServiceBus(amqpPort: 5673);
+ var second = azure.WithServiceBus(amqpPort: 5673);
+
+ Assert.Same(first.Resource, second.Resource);
+
+ using var app = builder.Build();
+ var appModel = app.Services.GetRequiredService();
+ Assert.Single(appModel.Resources.OfType());
+ }
+
+ [Fact]
+ public void ConflictingPortsThrow()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var azure = builder.AddFlociAzure("floci-az");
+ azure.WithServiceBus(amqpPort: 5673);
+
+ Assert.Throws(() => azure.WithServiceBus(amqpPort: 5675));
+ }
+
+ [Fact]
+ public async Task WithReferenceInjectsTheConnectionString()
+ {
+ IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder();
+
+ var serviceBus = builder.AddFlociAzure("floci-az").WithServiceBus();
+ AllocateEndpoints(serviceBus.Resource, 5673, 5674);
+
+ var consumer = builder.AddContainer("api", "my-api-image")
+ .WithReference(serviceBus);
+
+ using var app = builder.Build();
+
+ Assert.True(consumer.Resource.TryGetAnnotationsOfType(
+ out IEnumerable? envAnnotations));
+
+ var envVars = new Dictionary();
+ var context = new EnvironmentCallbackContext(builder.ExecutionContext, envVars);
+ foreach (var annotation in envAnnotations!)
+ {
+ await annotation.Callback(context);
+ }
+
+ object connectionString = envVars["ConnectionStrings__servicebus"];
+ string? value = connectionString is IValueProvider provider
+ ? await provider.GetValueAsync(CancellationToken.None)
+ : connectionString.ToString();
+
+ Assert.Equal(
+ "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;" +
+ "SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ value);
+ }
+
+ private static void AssertEndpoint(
+ EndpointAnnotation endpoint,
+ string name,
+ string scheme,
+ int? port)
+ {
+ Assert.Equal(name, endpoint.Name);
+ Assert.Equal(scheme, endpoint.UriScheme);
+ Assert.Equal(port, endpoint.Port);
+ Assert.False(endpoint.IsExplicitlyProxied);
+ }
+
+ private static void AllocateEndpoints(
+ FlociAzureServiceBusResource serviceBus,
+ int amqpPort,
+ int amqpTlsPort)
+ {
+ serviceBus.AmqpEndpoint.EndpointAnnotation.AllocatedEndpoint =
+ new AllocatedEndpoint(serviceBus.AmqpEndpoint.EndpointAnnotation, "localhost", amqpPort);
+ serviceBus.AmqpTlsEndpoint.EndpointAnnotation.AllocatedEndpoint =
+ new AllocatedEndpoint(serviceBus.AmqpTlsEndpoint.EndpointAnnotation, "localhost", amqpTlsPort);
+ }
+}