Skip to content

Getting Started

This guide walks you through building a minimal distributed system with ServiceConnect: a sender that submits work and a consumer that processes it. You will install the NuGet packages, start RabbitMQ locally with Docker, and run two console applications that talk to each other over a queue.

When you finish, you will have a working message flow you can extend, and enough vocabulary to read the rest of the Learn track.

  • .NET SDK 8.0 or 10.0. The library packages multi-target net8.0 (previous LTS) and net10.0 (current LTS); use whichever SDK matches your application’s target. Older runtimes (netstandard2.x, net6.0, net7.0) are not supported — see the Releases page for the rationale and migration guidance.
  • Docker (or a local RabbitMQ 3.7+ install). Docker is easier — the command below starts a throwaway broker in 5 seconds.

ServiceConnect speaks AMQP 0.9.1 against RabbitMQ. Start a broker with the management UI enabled:

Terminal window
docker run --rm -d \
--name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:3-management

The broker is ready when you can open http://localhost:15672 (login: guest / guest).

We’ll build three projects in a single solution:

ProjectPurpose
GettingStarted.ContractsShared message types referenced by both services.
GettingStarted.SenderSends a message and exits.
GettingStarted.ConsumerStarts the bus and handles incoming messages.

Create the solution and projects:

Terminal window
mkdir GettingStarted && cd GettingStarted
dotnet new sln
dotnet new classlib -n GettingStarted.Contracts
dotnet new console -n GettingStarted.Sender
dotnet new console -n GettingStarted.Consumer
dotnet sln add GettingStarted.Contracts GettingStarted.Sender GettingStarted.Consumer
dotnet add GettingStarted.Sender reference GettingStarted.Contracts
dotnet add GettingStarted.Consumer reference GettingStarted.Contracts

Add the ServiceConnect packages to both console apps:

Terminal window
dotnet add GettingStarted.Sender package ServiceConnect
dotnet add GettingStarted.Sender package ServiceConnect.Client.RabbitMQ
dotnet add GettingStarted.Consumer package ServiceConnect
dotnet add GettingStarted.Consumer package ServiceConnect.Client.RabbitMQ

Messages are the contracts exchanged between services. Every ServiceConnect message derives from the Message base class, which carries a correlation id used to link related messages across the system.

In GettingStarted.Contracts, replace the default Class1.cs with WorkSubmitted.cs:

GettingStarted.Contracts/WorkSubmitted.cs
using ServiceConnect.Interfaces;
namespace GettingStarted.Contracts;
public sealed class WorkSubmitted(Guid correlationId) : Message(correlationId)
{
public string WorkId { get; init; } = string.Empty;
}

Contracts needs the ServiceConnect interfaces to reference Message:

Terminal window
dotnet add GettingStarted.Contracts package ServiceConnect.Interfaces

Keep message types simple DTOs — public properties, no behaviour. See Messages for the full contract design guidance.

The sender builds a ServiceCollection, registers ServiceConnect pointing at RabbitMQ, resolves IBus, and calls SendAsync.

GettingStarted.Sender/Program.cs
using GettingStarted.Contracts;
using Microsoft.Extensions.DependencyInjection;
using ServiceConnect;
using ServiceConnect.Client.RabbitMQ;
using ServiceConnect.DependencyInjection;
using ServiceConnect.Interfaces;
using ServiceConnect.Interfaces.Options;
var services = new ServiceCollection();
services.AddLogging();
services.AddServiceConnect(builder =>
{
builder.UseRabbitMQ(transport =>
{
transport.Host = "localhost";
transport.Username = "guest";
transport.Password = "guest";
transport.SslEnabled = false; // TLS is on by default; set false against the plaintext local broker only.
});
builder.ConfigureQueues(queues => queues.QueueName = "getting-started-sender");
});
await using var provider = services.BuildServiceProvider();
var bus = provider.GetRequiredService<IBus>();
await bus.SendAsync(
new WorkSubmitted(Guid.NewGuid()) { WorkId = "work-001" },
new SendOptions { EndPoint = "getting-started-consumer" });
Console.WriteLine("Sent work-001");

Two things to notice:

  • The sender has its own queue name (getting-started-sender). Even a service that only sends needs a queue — that is where replies, errors, and audit copies land.
  • SendOptions.EndPoint names the destination queue. ServiceConnect does not guess routing; you tell it where a message goes, or you configure a queue mapping in advance (see Endpoints).

The consumer registers a handler for WorkSubmitted and starts the bus.

First, the handler:

GettingStarted.Consumer/WorkSubmittedHandler.cs
using GettingStarted.Contracts;
using ServiceConnect.Interfaces;
namespace GettingStarted.Consumer;
public sealed class WorkSubmittedHandler : IMessageHandler<WorkSubmitted>
{
public Task HandleAsync(WorkSubmitted message, IConsumeContext context, CancellationToken cancellationToken = default)
{
Console.WriteLine($"Processed {message.WorkId}");
return Task.CompletedTask;
}
}

Then the bootstrap:

GettingStarted.Consumer/Program.cs
using GettingStarted.Consumer;
using GettingStarted.Contracts;
using Microsoft.Extensions.DependencyInjection;
using ServiceConnect;
using ServiceConnect.Client.RabbitMQ;
using ServiceConnect.DependencyInjection;
using ServiceConnect.Interfaces;
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IReadOnlyList<HandlerReference>>(new List<HandlerReference>
{
new() { HandlerType = typeof(WorkSubmittedHandler), MessageType = typeof(WorkSubmitted) }
});
services.AddServiceConnect(builder =>
{
builder.UseRabbitMQ(transport =>
{
transport.Host = "localhost";
transport.Username = "guest";
transport.Password = "guest";
transport.SslEnabled = false; // TLS is on by default; set false against the plaintext local broker only.
});
builder.ConfigureQueues(queues => queues.QueueName = "getting-started-consumer");
builder.ConfigureBus(bus => bus.ScanForMessageHandlers = false);
});
await using var provider = services.BuildServiceProvider();
var bus = provider.GetRequiredService<IBus>();
await bus.StartConsumingAsync();
Console.WriteLine("Consumer ready. Ctrl-C to exit.");
await Task.Delay(Timeout.InfiniteTimeSpan);

We register handlers explicitly via HandlerReference and disable ScanForMessageHandlers. The scanner works for full applications; explicit registration is clearer for a first example and shows exactly what ServiceConnect needs to know.

Open two terminals in the GettingStarted directory.

Terminal 1 — start the consumer:

Terminal window
dotnet run --project GettingStarted.Consumer

You should see:

Consumer ready. Ctrl-C to exit.

Terminal 2 — send one message:

Terminal window
dotnet run --project GettingStarted.Sender

The sender prints Sent work-001 and exits. In the consumer terminal you should now see:

Processed work-001

That message travelled from the sender process, through RabbitMQ, into the consumer process, and into your handler. Stop the consumer with Ctrl-C.

  1. The consumer declared a queue named getting-started-consumer on RabbitMQ when the bus started consuming.
  2. The sender produced a message of type WorkSubmitted and, because SendOptions.EndPoint was set, routed it directly to getting-started-consumer.
  3. RabbitMQ delivered the message to the consumer’s queue.
  4. ServiceConnect deserialised the message, matched it to WorkSubmittedHandler via the handler registry, and invoked HandleAsync.

Each step maps to a Core Concept page you can read next.

  • The Bus — what IBus is and how it fits into the host lifecycle.
  • Messages — how to design contracts that travel cleanly.
  • Handlers — how incoming messages reach your code.
  • Endpoints — queue names, routing, and how Send and Publish choose a destination.

Or skip ahead to Messaging Patterns to see what else the bus can do.