Getting Started
Architecture
NServiceBus
Transports
Persistence
Hosting
ServiceInsight
ServicePulse
ServiceControl
Monitoring
Modernization
Samples

Aspire

The Particular.Aspire.Hosting.ServicePlatform package is an Aspire hosting integration that runs the Particular Service Platform (the ServiceControl instances, ServicePulse, the ServiceControl database, and message transport) as part of an Aspire AppHost. It is intended for developers and technical leads who run the platform locally during development and want the same AppHost to carry through to publish-mode deployments, without maintaining a separate set of infrastructure scripts.

Overview

The Particular.Aspire.Hosting.ServicePlatform package extends an Aspire AppHost so it can run a complete Particular Platform alongside the rest of a distributed application.

A single AddParticularPlatform(...) call registers a platform resource that owns:

  • ServiceControl error, audit, and monitoring instances
  • ServicePulse
  • A managed RavenDB database for ServiceControl, or one supplied by the AppHost
  • The configured message transport
  • The platform license

Transport and licensing are configured once on the platform resource and propagated to every component, so the containers start in the correct order. ServiceControl's database is configured on the same resource and shared by the Error and Audit instances that require it. NServiceBus endpoints attach with WithParticularPlatform(...) and pick up the same transport connection string and license without additional wiring, and configure their own persistence separately.

var builder = DistributedApplication.CreateBuilder(args);

var platform = builder
    .AddParticularPlatform("particular")
    .AddDefaultComponents();

var sales = builder.AddProject<Projects.Sales>("Sales")
    .WithParticularPlatform(platform);

builder.AddProject<Projects.ClientUI>("ClientUI")
    .WaitFor(sales)
    .WithParticularPlatform(platform);

builder.Build().Run();

Supported components

The integration is being built out across the Particular Platform in stages. The tables below show what is currently wired through the AppHost.

If you are using the Particular Service Platform with Aspire today and would like to see more Aspire support, reach out.

Transport

TransportStatus
LearningSupported (Development only)
Azure Service BusSupported
RabbitMQSupported
Amazon SQSSupported
Azure Storage QueuesNot yet supported
Microsoft SQL ServerNot yet supported
PostgreSQLNot yet supported
IBM MQNot yet supported

ServiceControl database

This is the database that backs the ServiceControl instances. It is separate from the persistence an NServiceBus endpoint uses for sagas, outbox, and subscriptions, which the integration does not manage.

DatabaseStatus
RavenDBSupported

Prerequisites

  • The Aspire CLI and the .NET 10 SDK, used to build and run the AppHost project.
  • A container runtime. The platform components are pulled from Docker Hub as particular/servicecontrol, particular/servicecontrol-audit, particular/servicecontrol-monitoring, particular/servicecontrol-ravendb, and particular/servicepulse.
  • A Particular Platform license. See Configuring the license for the license sources the integration accepts.

Installation

If the solution does not yet have an Aspire AppHost project, follow the Build your first Aspire app quickstart to scaffold one.

Then add the integration package to the AppHost project:

dotnet add package Particular.Aspire.Hosting.ServicePlatform

Only the AppHost project needs this reference. NServiceBus endpoint projects pick up the platform license and transport connection string from environment variables injected by WithParticularPlatform(...), so they do not need their own reference to this package.

Quick start

AddDefaultComponents() wires up the Learning transport, a managed RavenDB instance, the ServiceControl error, audit, and monitoring instances, and ServicePulse with sensible defaults:

var builder = DistributedApplication.CreateBuilder(args);

var platform = builder
    .AddParticularPlatform("particular")
    .AddDefaultComponents();

builder.Build().Run();

To attach an NServiceBus endpoint so it picks up the platform's license and transport connection string, chain WithParticularPlatform(platform) on the endpoint's project resource:

builder.AddProject<Projects.MyEndpoint>("my-endpoint")
       .WithParticularPlatform(platform);

These defaults are intended for local development. For production deployments, configure an explicit transport, ServiceControl database, and license as described in Configuring the transport, Configuring ServiceControl database, and Configuring the license.

{
    var builder = DistributedApplication.CreateBuilder(args);

    //ASB connection string
    var transport = builder.AddConnectionString("transport", "AzureServiceBus_ConnectionString");

    //platform setup with ASB transport and license file
    var platform = builder
        .AddParticularPlatform("particular")
        .WithTransportAzureServiceBus(transport)
        .WithLicenseFromFile("license.xml");

    //ServiceControl database setup
    var serviceControlDb = platform.AddPersistenceRavenDb("particular-persistence");

    //error instance setup
    var servicecontrol = platform.AddServiceControlErrorInstance("particular-error", serviceControlDb)
        .WithErrorQueueName("error")
        .WithThroughputQueue("particular.throughput");

    //audit instance setup
    platform.AddServiceControlAuditInstance("particular-audit", servicecontrol, serviceControlDb)
        .WithAuditQueueName("audit");

    //monitoring instance setup
    var monitoring = platform.AddServiceControlMonitoringInstance("particular-monitoring")
        .WithMonitoringQueueName("Particular.Monitoring")
        .WithThroughputQueueFrom(servicecontrol);

    //servicepulse instance setup
    platform.AddServicePulse("particular-servicepulse", servicecontrol, monitoring);

    builder.Build().Run();
}

Viewing the platform in the Aspire dashboard

The platform appears in the Aspire dashboard as a single ParticularPlatform parent resource. The components the integration creates are nested as children, each surfacing the URL exposed by its primary endpoint:

  • ServicePulse: the web UI
  • ServiceControl Error: the error instance API
  • ServiceControl Audit: the audit instance API
  • ServiceControl Monitoring: the monitoring instance API
  • Management Studio: the RavenDB management UI (when using the managed RavenDB instance)

Externally supplied resources are not nested under the platform. An Azure Service Bus or RabbitMQ broker passed in via AddConnectionString("...") (or any other Add* call), and an existing RavenDB instance attached via WithPersistenceRavenDb(existingRaven), appear as separate top-level resources in the dashboard. The platform does not own their lifecycle, so it does not group them under itself.

The platform node tracks readiness as its children come up. It starts in Starting, transitions to Running once every child reports healthy, and moves to RuntimeUnhealthy if any child stops.

Configuring the transport

The platform uses whichever transport is configured via a WithTransport* extension on the platform resource. The same transport connection string is then propagated to every ServiceControl instance and to any NServiceBus endpoint attached via WithParticularPlatform(...). See Supported components for the transports currently wired through the integration.

Learning transport

The Learning transport stores messages on the local file system. The default storage directory is attached to the platform containers so that they share the transport with your endpoints.

builder
    .AddParticularPlatform("particular")
    .WithTransportLearning();

The Learning transport appears in the Aspire dashboard as a learning-transport connection-string resource nested under the platform, holding the resolved storage path.

Options

OptionDefault
storagePath (string?) parameter on WithTransportLearning.learningtransport (relative to the solution directory)

Azure Service Bus

The platform treats Azure Service Bus as an external resource, so the AppHost only needs a connection string to reach the namespace. Other ways to model the resource in the AppHost (such as AddAzureServiceBus with Aspire provisioning, or AsExisting) are described in Set up Azure Service Bus in the AppHost in the Aspire documentation.

var asb = builder.AddConnectionString("asb");
// var asb = builder.AddAzureServiceBus("asb");

builder
    .AddParticularPlatform("particular")
    .WithTransportAzureServiceBus(asb);

Options

OptionDefault
azureServiceBus (IResourceBuilder<IResourceWithConnectionString>) parameter on WithTransportAzureServiceBusRequired

Azure Service Bus tuning such as topic name, partitioning, hierarchy namespace, and websocket mode is configured on the connection string itself. See Azure Service Bus transport configuration for the full list of connection-string options.

RabbitMQ

The platform treats RabbitMQ as an external resource, so the AppHost only needs a connection string to reach the broker. Other ways to model the resource in the AppHost (such as AddRabbitMQ with Aspire provisioning) are described in Set up RabbitMQ in the AppHost in the Aspire documentation.

Pass the resource to WithTransportRabbitMQ along with a routing topology.

var rabbit = builder.AddConnectionString("rabbit");
// var rabbit = builder.AddRabbitMQ("rabbit");

builder
    .AddParticularPlatform("particular")
    .WithTransportRabbitMQ(RabbitMqRouting.QuorumConventionalRouting, rabbit);

Amazon SQS

The platform requires a pre-configured access token to connect to AmazonSQS, and these values will be passed to endpoints via Environment Variables. It is recommended that parameter resources are used for this.

var sqsAccessId = builder.AddParameter("sqs-access-id");
var sqsSecretKey = builder.AddParameter("sqs-secret-key", secret: true);

builder
    .AddParticularPlatform("particular")
    .WithTransportAmazonSqs("us-west-1", sqsAccessId.Resource, sqsSecretKey.Resource);

Options

OptionDefault
region (string) parameter on WithTransportAmazonSQSRequired
accessKey (IExpressionValue) parameter on WithTransportAmazonSQSRequired
secretAccessKey (IExpressionValue) parameter on WithTransportAmazonSQSRequired
configure (Action<AmazonSQSTransportSettings) parameter on WithTransportAmazonSQSOptional
QueueNamePrefix (string?) property on AmazonSQSTransportSettingsOptional
TopicNamePrefix (string?) property on AmazonSQSTransportSettingsOptional
S3BucketForLargeMessages (IExpressionValue?) property on AmazonSQSTransportSettingsOptional
QueueNamePrefix (string?) property on AmazonSQSTransportSettingsOptional

Configuring ServiceControl database

ServiceControl uses a database to store error and audit data, retry state, and saga audit history. This is ServiceControl's own database; it is separate from any persistence your NServiceBus endpoints use for sagas, outbox, or subscriptions, which you configure in each endpoint as usual. See Supported components for the persisters currently wired through the integration.

Configure persistence on the platform resource, then pass the resulting persistence builder into the ServiceControl Error and Audit instances that need it. The Monitoring instance does not require a database. Follow the managing database guidance when setting up for production use.

RavenDB

RavenDB can be modelled as either a managed child of the platform (the integration provisions and starts the container) or as an external resource declared elsewhere in the AppHost.

Managed RavenDB instance

AddPersistenceRavenDb(name) adds the particular/servicecontrol-ravendb container as a child of the platform. This is the path used by AddDefaultComponents().

var platform = builder.AddParticularPlatform("particular");

var serviceControlDb = platform.AddPersistenceRavenDb("ravendb");

External RavenDB instance

Use WithPersistenceRavenDb(existing) to attach a RavenDB resource declared elsewhere in the AppHost, for example via the Aspire RavenDB integration or AddConnectionString.

var ravenDb = builder.AddConnectionString("ravendb");

builder
    .AddParticularPlatform("particular")
    .WithPersistenceRavenDb(ravenDb);

External RavenDB instances are not nested under the platform in the dashboard, since the platform does not own their lifecycle.

Options

OptionDefault
name (string) parameter on AddPersistenceRavenDbRequired
persistence (IResourceBuilder<IResourceWithConnectionString>) parameter on WithPersistenceRavenDbRequired

Configuring the license

AddParticularPlatform registers an Aspire parameter named <platform-name>-license (marked as a secret) to carry the Particular Platform license. The value is resolved as follows:

  1. Any value supplied through the standard Aspire parameter input mechanisms (user secrets, environment variables, command-line) takes precedence.
  2. Otherwise the default set in code is used, which is either the built-in auto-discovery or whatever WithLicenseFromFile or WithLicenseFromText overrides it to.

Default auto-discovery

If neither WithLicenseFromFile nor WithLicenseFromText is called, the integration searches the standard Particular Platform license file locations at AppHost startup in this order:

  1. The machine-wide %PROGRAMDATA%\ParticularSoftware\license.xml file
  2. The user-specific %LOCALAPPDATA%\ParticularSoftware\license.xml file
  3. The PARTICULARSOFTWARE_LICENSE environment variable

This makes local development work without explicit configuration on machines where the license is already installed.

From a file

Use WithLicenseFromFile(file) to read the license from a specific file path.

builder
    .AddParticularPlatform("particular")
    .WithLicenseFromFile("license.xml");

From inline text

Use WithLicenseFromText(licenseText) to inline the license XML directly. This is useful in tests or when reading the license through IConfiguration.

var licenseText = builder.Configuration["ParticularLicense"]!;

builder
    .AddParticularPlatform("particular")
    .WithLicenseFromText(licenseText);

Options

OptionDefault
file (string) parameter on WithLicenseFromFileRequired
licenseText (string) parameter on WithLicenseFromTextRequired

Adding platform components individually

AddDefaultComponents() is the easiest way to wire up the Error, Audit, Monitoring, and ServicePulse instances. When custom queue names, partial topologies (for example, an Error instance without an Audit), or multiple remote audit instances are required, the components can be added one at a time.

Each Add* method below is an extension on the platform resource builder. Every component automatically receives the platform license and transport configuration. Error and Audit additionally receive the persistence resource.

var platform = builder.AddParticularPlatform("particular");
var serviceControlDb = platform.AddPersistenceRavenDb("ravendb");

var error = platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb);
var monitoring = platform.AddServiceControlMonitoringInstance("monitoring");
var audit = platform.AddServiceControlAuditInstance("audit", error, serviceControlDb);
platform.AddServicePulse("servicepulse", error, monitoring);

ServiceControl Error instance

AddServiceControlErrorInstance(name, persistence) adds a ServiceControl Error instance, running the particular/servicecontrol image, as a child of the platform. The supplied persistence must have been registered on the platform via a WithPersistence* or AddPersistence* extension.

The error queue name defaults to error. Override it with WithErrorQueueName(queueName). Use WithThroughputQueue(queueName) to override the queue on which throughput data is received from the monitoring instance. This is independent of throughput reporting, which separately configures broker-statistics querying.

var error = platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithErrorQueueName("error")
    .WithThroughputQueue("particular.throughput");

Options

OptionDefault
name (string) parameter on AddServiceControlErrorInstanceRequired
persistence (IResourceBuilder<IResource>) parameter on AddServiceControlErrorInstanceRequired
queueName (string) parameter on WithErrorQueueNameerror
queueName (string) parameter on WithThroughputQueueServiceControl.ThroughputData
runMode (PlatformRunMode) parameter on WithRunModeSetupAndRun

ServiceControl Audit instance

AddServiceControlAuditInstance(name, error, persistence) adds a ServiceControl Audit instance, running the particular/servicecontrol-audit image, as a child of the platform. The audit instance is automatically registered as a remote instance on the supplied error instance, so the error instance can query audit data through it.

The audit queue name defaults to audit. Override it with WithAuditQueueName(queueName).

var audit = platform.AddServiceControlAuditInstance("audit", error, serviceControlDb)
    .WithAuditQueueName("audit");

Options

OptionDefault
name (string) parameter on AddServiceControlAuditInstanceRequired
serviceControl (IResourceBuilder<ServiceControlErrorInstanceResource>) parameter on AddServiceControlAuditInstanceRequired
persistence (IResourceBuilder<IResource>) parameter on AddServiceControlAuditInstanceRequired
queueName (string) parameter on WithAuditQueueNameaudit
runMode (PlatformRunMode) parameter on WithRunModeSetupAndRun

ServiceControl Monitoring instance

AddServiceControlMonitoringInstance(name) adds a ServiceControl Monitoring instance, running the particular/servicecontrol-monitoring image, as a child of the platform. The monitoring instance does not require a database.

The monitoring queue name defaults to Particular.Monitoring. Override it with WithMonitoringQueueName(queueName). Use WithThroughputQueueFrom(error) to copy the throughput queue name from the error instance and keep the two aligned without repeating the name; WithThroughputQueue(queueName) sets it explicitly when the queue address needs to differ from the error instance.

var monitoring = platform.AddServiceControlMonitoringInstance("monitoring")
    .WithMonitoringQueueName("Particular.Monitoring")
    .WithThroughputQueueFrom(error);

Options

OptionDefault
name (string) parameter on AddServiceControlMonitoringInstanceRequired
queueName (string) parameter on WithMonitoringQueueNameParticular.Monitoring
errorInstance (IResourceBuilder<ServiceControlErrorInstanceResource>) parameter on WithThroughputQueueFromRequired (when called)
queueName (string) parameter on WithThroughputQueueServiceControl.ThroughputData
runMode (PlatformRunMode) parameter on WithRunModeSetupAndRun

ServicePulse

AddServicePulse(name, servicecontrol, monitoring?) adds ServicePulse, running the particular/servicepulse image, as a child of the platform. The error instance is wired in as the ServiceControl API endpoint. The optional monitoring argument exposes real-time monitoring data in the UI; if omitted, the monitoring panel is unavailable.

platform.AddServicePulse("servicepulse", error, monitoring);

Options

OptionDefault
name (string) parameter on AddServicePulseRequired
serviceControl (IResourceBuilder<ServiceControlErrorInstanceResource>) parameter on AddServicePulseRequired
monitoring (IResourceBuilder<ServiceControlMonitoringInstanceResource>?) parameter on AddServicePulsenull

Using standard Aspire extensions

Each container the integration adds (the ServiceControl Error, Audit, and Monitoring instances, ServicePulse, and the managed RavenDB instance) is a regular Aspire container resource, and AddParticularPlatform returns a standard resource builder. Any extension method that applies to those base types can be chained with the integration's own Add* and With* methods. Common examples include:

  • WithImageTag(tag) to pin a specific image version
  • WithImageRegistry(registry) to pull from a private registry mirror
  • WithEnvironment(name, value) to pass through extra environment variables
  • WithVolume(...) or WithBindMount(...) to map storage from the host
  • ExcludeFromManifest() to keep a resource out of publish output
  • WaitFor(other) and WaitForCompletion(other) to gate startup ordering

See the Aspire documentation on resources for the full set of container-resource extensions.

Connecting NServiceBus endpoints to the platform

WithParticularPlatform(platform) is an extension on any Aspire resource that exposes environment variables (typically a project resource added with AddProject<T>(...)). It injects the platform license and the transport connection string into the resource's environment, so an NServiceBus endpoint reading those values picks up the same license and transport that the platform components use, without further wiring in the AppHost.

builder.AddProject<Projects.MyEndpoint>("my-endpoint")
    .WithParticularPlatform(platform);

The endpoint project still configures NServiceBus itself: choosing the transport package (NServiceBus.Transport.AzureServiceBus, NServiceBus.Transport.RabbitMQ, and so on), reading the connection string from IConfiguration or environment, and setting up routing, persistence, and serialization as usual. WithParticularPlatform only delivers the platform-side configuration; it does not replace NServiceBus configuration code in the endpoint.

Environment variables injected

WithParticularPlatform(platform) adds two pieces of configuration to the endpoint resource:

VariableSource
PARTICULARSOFTWARE_LICENSEThe platform license parameter
ConnectionStrings__<transport-name>The transport connection-string resource passed to a WithTransport* call

NServiceBus picks up PARTICULARSOFTWARE_LICENSE automatically at endpoint startup, as described in License management. For the transport, read the connection string from IConfiguration.GetConnectionString("<transport-name>") and pass it to the transport configuration in the usual way.

The <transport-name> portion of the connection-string variable matches the name passed to AddConnectionString(...) (or whichever resource was supplied to WithTransport*). For the Learning transport the integration creates an internal connection-string resource named learning-transport, so endpoints receive ConnectionStrings__learning-transport.

Waiting for the platform

WithParticularPlatform does not add a startup-order dependency on the platform. To delay the endpoint's startup until the platform reports Running, chain WaitFor(platform):

builder.AddProject<Projects.MyEndpoint>("my-endpoint")
    .WithParticularPlatform(platform)
    .WaitFor(platform);

Options

OptionDefault
platform (IResourceBuilder<ParticularPlatformResource> or ParticularPlatformResource) parameter on WithParticularPlatformRequired

Throughput reporting

Throughput reporting lets the ServiceControl Error instance query the broker's management API to collect queue throughput statistics for licensing. The credentials required to make those queries depend on the transport. See Supported components for the transports the integration currently supports for throughput reporting, and the upstream Usage reporting docs for what ServiceControl does with the values.

To enable it on the ServiceControl Error instance, chain WithThroughputReporting(provider) and pass a provider matching the configured transport. The provider throws at configuration time if it is paired with a different transport.

Azure Service Bus

ThroughputReportingAzureServiceBus supplies Azure AD credentials so the Error instance can query Service Bus management APIs. Pass each value as an Aspire parameter; mark the client secret as a secret parameter.

var tenantId = builder.AddParameter("asb-tenant-id");
var subscriptionId = builder.AddParameter("asb-subscription-id");
var clientId = builder.AddParameter("asb-client-id");
var clientSecret = builder.AddParameter("asb-client-secret", secret: true);

platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithThroughputReporting(new ThroughputReportingAzureServiceBus(
        tenantId: ReferenceExpression.Create($"{tenantId.Resource}"),
        subscriptionId: ReferenceExpression.Create($"{subscriptionId.Resource}"),
        clientId: ReferenceExpression.Create($"{clientId.Resource}"),
        clientSecret: ReferenceExpression.Create($"{clientSecret.Resource}")));

See Usage reporting when using the Azure Service Bus transport for what each value is used for in ServiceControl.

RabbitMQ

ThroughputReportingRabbitMq supplies RabbitMQ management API credentials so the Error instance can query broker statistics. All parameters are optional; ServiceControl falls back to the broker connection string for any value not supplied.

var apiUrl = builder.AddParameter("rabbit-api-url");
var username = builder.AddParameter("rabbit-username");
var password = builder.AddParameter("rabbit-password", secret: true);

platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithThroughputReporting(new ThroughputReportingRabbitMq(
        ReferenceExpression.Create($"{apiUrl.Resource}"),
        ReferenceExpression.Create($"{username.Resource}"),
        ReferenceExpression.Create($"{password.Resource}")));

See Usage reporting when using the RabbitMQ transport for what each value is used for in ServiceControl.

Amazon SQS

ThroughputReportingAmazonSQS supplies AmazonSQS overrides so the Error instance can query broker statistics. All parameters are optional; ServiceControl falls back to the values from the transport connection if they are not provided.

var accessKey = builder.AddParameter("access-key");
var secretKey = builder.AddParameter("secret-key", secret: true);

platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithThroughputReporting(new ThroughputReportingAmazonSqs(
        ReferenceExpression.Create($"{accessKey.Resource}"),
        ReferenceExpression.Create($"{secretKey.Resource}"),
        "profileName",
        "region",
        "prefix"));

See Usage reporting when using the Amazon SQS transport for what each value is used for in ServiceControl.

Production considerations

The same AppHost code is intended to run locally and deploy to production. The notes below highlight what to review before promoting the AppHost to publish or deploy mode.

Learning transport in publish mode

The Learning transport is blocked in publish mode by design. Calling WithTransportLearning from an AppHost run with aspire publish or aspire deploy throws at startup. Configure a production-grade transport for non-development deployments. See Configuring the transport for the supported options.

Supplying the license in production

AddParticularPlatform registers the license as a secret Aspire parameter. The in-code defaults set by WithLicenseFromFile, WithLicenseFromText, and the built-in auto-discovery are evaluated when the manifest is generated and the resulting value is embedded into the published manifest. Embedding a production license in the manifest is rarely desirable.

For production deployments, supply the <platform-name>-license parameter through the standard Aspire parameter input mechanisms (user secrets, environment variables, command-line arguments, or a secret-store integration) so the value never lives in the published manifest. The in-code defaults remain useful for local development.

Configuring host ports

Each managed container exposes an endpoint allowing Aspire to manage the exposed endpoint to avoid conflicts with ports already in use by other processes. As these ports are selected randomly by Aspire at startup you can refer to the dashboard for the url to access each component.

In some cases it may be desirable to have a fixed port exposed by these components to provide a stable url (for example http://localhost:9090 for ServicePulse) without having to inspect the Aspire dashboard.

Override a port with the standard Aspire WithEndpoint(endpointName, callback) API:

platform.AddServicePulse("servicepulse", error, monitoring)
    .WithEndpoint("http", e =>
    {
        e.Port = 9091;
    });

Pinning container image versions

The integration uses the latest tag for every managed container image (particular/servicecontrol, particular/servicecontrol-audit, particular/servicecontrol-monitoring, particular/servicecontrol-ravendb, particular/servicepulse). Pin each component to a specific version for production so deployments are reproducible and ServiceControl picks up new major versions intentionally.

The ServiceControl Error, Audit, Monitoring, and RavenDB versions must align. See Managing ServiceControl RavenDB instances via Containers for the version-pairing rules.

var serviceControlVersion = "x.y.z";

var error = platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithImageTag(serviceControlVersion);

platform.AddServiceControlAuditInstance("audit", error, serviceControlDb)
    .WithImageTag(serviceControlVersion);

platform.AddServiceControlMonitoringInstance("monitoring")
    .WithImageTag(serviceControlVersion);

Container run mode

By default, each ServiceControl instance container runs in SetupAndRun mode: on startup it performs setup, creating the queues and database structures it needs, and then runs the instance. This keeps the local development experience working without any manual preparation.

In production, setup is often performed as a separate, controlled step rather than on every container start, for example because the runtime account is not permitted to create queues, or to avoid setup running on each scaled-out replica. Use WithRunMode(PlatformRunMode.Run) to start an instance without performing setup, assuming the queues and database structures already exist:

var error = platform.AddServiceControlErrorInstance("servicecontrol", serviceControlDb)
    .WithRunMode(PlatformRunMode.Run);

platform.AddServiceControlAuditInstance("audit", error, serviceControlDb)
    .WithRunMode(PlatformRunMode.Run);

platform.AddServiceControlMonitoringInstance("monitoring")
    .WithRunMode(PlatformRunMode.Run);

The available modes are:

  • PlatformRunMode.SetupAndRun (default): perform setup, then run the instance.
  • PlatformRunMode.Run: run the instance without performing setup.
  • PlatformRunMode.Setup: perform setup only, then exit. This is useful for running setup as a dedicated step before starting the instances in Run mode.

WithRunMode is configured per ServiceControl instance, so the Error, Audit, and Monitoring instances can each use a different mode when required.

Publishing and deploying

The integration's components are standard Aspire resources, so they participate in aspire publish and aspire deploy like any other resource. The synthetic platform parent resource is excluded from the publish manifest, but every component it owns (the ServiceControl instances, ServicePulse, and the managed RavenDB instance) is included as a normal container resource in the published output. Externally supplied resources, such as an Azure Service Bus broker passed in via AddConnectionString, appear as they would in any other Aspire AppHost.

Aspire supports multiple deployment targets, including Docker Compose, Kubernetes, and Azure Container Apps. See Aspire pipelines for the deployment model and the supported targets.

Two integration-specific points to keep in mind for any publish target:

  • The license parameter default is materialized into the manifest at publish time. Supply the license through Aspire parameter inputs in production so the value does not live in the published output. See Supplying the license in production.
  • The Learning transport is blocked in publish mode.

Troubleshooting

"No transport configured" exception at startup

AddParticularPlatform requires a transport to be configured via one of the WithTransport* extensions before any platform component can be wired. Call WithTransportLearning(), WithTransportAzureServiceBus(asb), or WithTransportRabbitMQ(routing, rabbit) on the platform builder. Alternatively, call AddDefaultComponents(), which falls back to the Learning transport when no transport has been set.

Platform stays in RuntimeUnhealthy with no children

If AddParticularPlatform is called but no platform components are added, the integration marks the platform as RuntimeUnhealthy at startup and logs a warning that no child resources were added. Add components individually (see Adding platform components individually) or call AddDefaultComponents() to wire the standard topology.

ServicePulse shows the monitoring panel as unavailable

AddServicePulse accepts an optional monitoring instance. When the monitoring argument is omitted, or WithMonitoringInstance(null) is called on the ServicePulse builder, ServicePulse runs without the monitoring panel. Pass an IResourceBuilder<ServiceControlMonitoringInstanceResource> to AddServicePulse, or call WithMonitoringInstance(monitoring) on the ServicePulse builder, to enable it. (AddDefaultComponents() always wires monitoring in automatically.)

"Platform [name] has mismatched ServiceControl container image versions" in Aspire host console

This warning appears when the ServiceControl container images (error, audit, and monitoring instances) are configured with different version tags. For example:

platform.AddServiceControlErrorInstance("error", serviceControlDb)
    .WithImage("particular/servicecontrol", "6.0.0");

platform.AddServiceControlMonitoringInstance("monitoring")
    .WithImage("particular/servicecontrol-monitoring", "5.0.0"); // mismatched

All ServiceControl components (error, audit, and monitoring) should run the same supported version to ensure compatibility. The warning lists each component with its configured image and tag so the mismatch is easy to identify.

To resolve the warning, ensure all ServiceControl instances use the same image tag, or remove explicit .WithImage() calls to use the default latest tag, which always refers to a compatible set of images.

Samples