Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jordiaragonzaragoza/JordiAragonZaragoza.SharedKernel/llms.txt

Use this file to discover all available pages before exploring further.

In a distributed system built around bounded contexts, services cannot share a database or call each other’s internal application layer directly. They communicate through an integration bus — a durable message broker that decouples the sender from all consumers. The JordiAragonZaragoza.SharedKernel.Presentation.Integration package wires up MassTransit over RabbitMQ as the integration bus. It provides strongly-typed producer buses for publishing events, sending commands, and executing request/response queries across service boundaries, as well as base consumer classes and a dynamic consumer-registration mechanism so each bounded context only subscribes to the messages it cares about.
MassTransit licensing. MassTransit v9 and later require a commercial license. SharedKernel deliberately pins its MassTransit dependency to v8.x, which remains free and open source. If you upgrade to v9+ you must obtain the appropriate license from the MassTransit authors.

Packages

Presentation.Integration

MassTransit wiring, producer buses, consumer base classes, and DI registration. Referenced by your service host.

Presentation.Integration.Contracts

Consumer interfaces (IIntegrationEventHandler<T>, IIntegrationCommandHandler<T>) and IBaseIntegrationMessageHandler<T>. Safe to reference from domain or application contract assemblies.

Full Setup Flow

1

Install the NuGet packages

dotnet add package JordiAragonZaragoza.SharedKernel.Presentation.Integration
dotnet add package JordiAragonZaragoza.SharedKernel.Presentation.Integration.Contracts
2

Add RabbitMQ configuration to appsettings.json

The library reads its configuration from the MassTransit:IntegrationBus section. At minimum you must supply the RabbitMQ connection URL.
{
  "MassTransit": {
    "IntegrationBus": {
      "RabbitMq": {
        "Url": "amqp://guest:guest@localhost:5672"
      },
      "MaximumConcurrencyLevel": 10,
      "NumberOfRetries": 3
    }
  }
}
3

Register consumers (optional, per-service)

Before calling AddSharedKernelPresentationIntegrationBusRegistrations, register a MassTransitIntegrationBusConsumersRegistrationConfigurator that wires up the consumers your service needs.
builder.Services.AddSingleton(
    new MassTransitIntegrationBusConsumersRegistrationConfigurator(cfg =>
    {
        cfg.AddConsumer<OrderCreatedIntegrationEventHandler>();
    }));
4

Register the integration bus

Call AddSharedKernelPresentationIntegrationBusRegistrations with the logical name of your service. This name is used as the RabbitMQ connection name for observability.
builder.Services.AddSharedKernelPresentationIntegrationBusRegistrations(
    targetHostName: "OrdersService");
5

Register the producer buses

Register the concrete bus implementations that your application layer depends on via their interfaces.
builder.Services.AddScoped<IIntegrationEventBus,   MassTransitIntegrationEventBus>();
builder.Services.AddScoped<IIntegrationCommandBus, MassTransitIntegrationCommandBus>();
builder.Services.AddScoped<IIntegrationQueryBus,   MassTransitIntegrationQueryBus>();

Configuration

MassTransitIntegrationBusOptions

Bound from the MassTransit:IntegrationBus configuration section. The RabbitMq property is required and validated at startup.
public class MassTransitIntegrationBusOptions
{
    public const string Section = "MassTransit:IntegrationBus";

    [Required]
    public RabbitMqOptions RabbitMq { get; init; } = default!;

    public uint? MaximumConcurrencyLevel { get; init; }

    public uint NumberOfRetries { get; init; } = 2;
}
PropertyRequiredDefaultDescription
RabbitMqRabbitMQ connection options (see below).
MaximumConcurrencyLevelnullCaps concurrent message processing per consumer. null uses MassTransit’s default.
NumberOfRetries2Number of delivery retries before a message moves to the error queue.

RabbitMqOptions

public class RabbitMqOptions
{
    [Required]
    public Uri Url { get; init; } = default!;
}
PropertyRequiredDescription
UrlFull AMQP URI, e.g. amqp://user:pass@rabbitmq-host:5672

DI Registration

MassTransitDependencyInjection.AddSharedKernelPresentationIntegrationBusRegistrations

public static IServiceCollection AddSharedKernelPresentationIntegrationBusRegistrations(
    this IServiceCollection services,
    string targetHostName)
Internally this method:
  1. Binds and validates MassTransitIntegrationBusOptions from configuration.
  2. Registers a default MassTransitIntegrationBusConsumersRegistrationConfigurator (no-op) if one was not already added.
  3. Calls AddMassTransit<IIntegrationBus> to create a named bus (IIntegrationBus) separate from any in-process bus (IBus) your application layer may already have — this is MassTransit’s MultiBus pattern.
  4. Configures RabbitMQ as the transport using the URL from options, sets the connection name to targetHostName, and applies kebab-case endpoint naming.
  5. Runs the caller-supplied consumer configuration via MassTransitIntegrationBusConsumersRegistrationConfigurator.
// Program.cs
builder.Services.AddSharedKernelPresentationIntegrationBusRegistrations(
    targetHostName: "CatalogService");
The bus starts automatically because AutoStart = true is set on the RabbitMQ factory configurator.

IIntegrationBus

A marker interface that extends MassTransit’s IBus. It is required for the MultiBus pattern so MassTransit can inject the correct bus instance when multiple buses are registered in one host.
/// <summary>
/// This marker interface is required to implement MassTransit MultiBus.
/// See https://masstransit.io/documentation/configuration/multibus for more information.
/// </summary>
public interface IIntegrationBus : IBus
{
}
You rarely use IIntegrationBus directly. Instead inject IIntegrationEventBus, IIntegrationCommandBus, or IIntegrationQueryBus from your application layer.

Producer Buses

MassTransitIntegrationEventBus

Implements IIntegrationEventBus. Publishes an integration event to all subscribers via MassTransit’s IPublishEndpoint, stamps DateDispatchedOnUtc, and logs at Debug level on success and Error level on failure.
public class MassTransitIntegrationEventBus : IIntegrationEventBus
{
    public async Task PublishAsync<T>(T @event, CancellationToken cancellationToken = default)
        where T : class, IIntegrationEvent
    {
        ArgumentNullException.ThrowIfNull(@event);

        @event.DateDispatchedOnUtc = this.dateTime.UtcNow;

        await this.publishEndPoint.Value.Publish(@event, cancellationToken);

        this.logger.LogDebug(
            "Published integration event {Event} {Id} at {DateTime}",
            @event.GetType().Name,
            @event.Id,
            this.dateTime.UtcNow);
    }
}
Usage inside a domain-event handler:
public sealed class OrderCreatedDomainEventHandler : INotificationHandler<OrderCreatedDomainEvent>
{
    private readonly IIntegrationEventBus integrationEventBus;

    public OrderCreatedDomainEventHandler(IIntegrationEventBus integrationEventBus)
        => this.integrationEventBus = integrationEventBus;

    public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
    {
        var integrationEvent = new OrderCreatedIntegrationEvent(
            notification.OrderId,
            notification.CustomerId,
            notification.TotalAmount);

        await this.integrationEventBus.PublishAsync(integrationEvent, cancellationToken);
    }
}

MassTransitIntegrationCommandBus

Implements IIntegrationCommandBus. Uses MassTransit’s request-client pattern to send a command and await a Result (or Result<TResponse>) reply.
public class MassTransitIntegrationCommandBus : IIntegrationCommandBus
{
    // Send a command and receive Result
    public async Task<Result> SendAsync<TCommand>(
        TCommand command,
        CancellationToken cancellationToken = default)
        where TCommand : class, IIntegrationCommand

    // Send a command and receive Result<TResponse>
    public async Task<Result<TResponse>> SendAsync<TCommand, TResponse>(
        TCommand command,
        CancellationToken cancellationToken = default)
        where TCommand : class, IIntegrationCommand
        where TResponse : notnull
}
A bounded context should not consume integration commands from another bounded context to enforce state changes. This introduces tight coupling and violates service autonomy. Prefer integration events for cross-context reactions. The IIntegrationCommandHandler interface carries this guidance in its XML docs.

MassTransitIntegrationQueryBus

Implements IIntegrationQueryBus. Uses the MassTransit request-client to send a query and await a Result<TResponse> reply.
public class MassTransitIntegrationQueryBus : IIntegrationQueryBus
{
    public async Task<Result<TResponse>> SendAsync<TQuery, TResponse>(
        TQuery query,
        CancellationToken cancellationToken = default)
        where TQuery : class, IIntegrationQuery
        where TResponse : notnull
}
// Querying the catalog service for a product price
var query  = new GetProductPriceIntegrationQuery(productId);
var result = await integrationQueryBus.SendAsync<GetProductPriceIntegrationQuery, decimal>(
    query, cancellationToken);

if (result.IsSuccess)
{
    decimal price = result.Value;
}

Consumer Interfaces

IBaseIntegrationMessageHandler<TIntegrationMessage>

The lowest-level contract: a single HandleAsync method that must be implemented for any message type.
/// <summary>
/// Defines a common contract for handling integration messages,
/// regardless of the underlying event bus implementation.
/// </summary>
public interface IBaseIntegrationMessageHandler<in TIntegrationMessage>
    where TIntegrationMessage : IIntegrationMessage
{
    Task HandleAsync(TIntegrationMessage integrationMessage, CancellationToken cancellationToken);
}

IIntegrationEventHandler<TEvent>

Combines IBaseIntegrationMessageHandler<TEvent> with MassTransit’s IConsumer<TEvent> so the type is both a domain-level handler and a registered MassTransit consumer.
public interface IIntegrationEventHandler<in TEvent>
    : IBaseIntegrationMessageHandler<TEvent>, IConsumer<TEvent>
    where TEvent : class, IIntegrationEvent
{
}

IIntegrationCommandHandler<TCommand>

Same pattern for integration commands.
public interface IIntegrationCommandHandler<in TCommand>
    : IBaseIntegrationMessageHandler<TCommand>, IConsumer<TCommand>
    where TCommand : class, IIntegrationCommand
{
}

BaseIntegrationEventHandler<TEvent>

An abstract class that implements IIntegrationEventHandler<TEvent>. It handles the MassTransit Consume contract by unwrapping the ConsumeContext<TEvent> and delegating to the abstract HandleAsync method that you implement.
public abstract class BaseIntegrationEventHandler<TEvent> : IIntegrationEventHandler<TEvent>
    where TEvent : class, IIntegrationEvent
{
    public async Task Consume(ConsumeContext<TEvent> context)
    {
        ArgumentNullException.ThrowIfNull(context);

        await this.HandleAsync(context.Message, CancellationToken.None);
    }

    public abstract Task HandleAsync(TEvent integrationMessage, CancellationToken cancellationToken);
}
Inherit and implement HandleAsync:
public sealed class OrderCreatedIntegrationEventHandler
    : BaseIntegrationEventHandler<OrderCreatedIntegrationEvent>
{
    private readonly ICommandBus commandBus;

    public OrderCreatedIntegrationEventHandler(ICommandBus commandBus)
        => this.commandBus = commandBus;

    public override async Task HandleAsync(
        OrderCreatedIntegrationEvent integrationEvent,
        CancellationToken cancellationToken)
    {
        var command = new ReserveInventoryCommand(
            integrationEvent.OrderId,
            integrationEvent.Items);

        await this.commandBus.SendAsync(command, cancellationToken);
    }
}

MassTransitIntegrationBusConsumersRegistrationConfigurator

Carries an Action<IBusRegistrationConfigurator> delegate that is applied during bus setup. This avoids registering all consumers from all assemblies on every bus — each service host provides only the consumers it needs.
public class MassTransitIntegrationBusConsumersRegistrationConfigurator
{
    private readonly Action<IBusRegistrationConfigurator> configure;

    public MassTransitIntegrationBusConsumersRegistrationConfigurator(
        Action<IBusRegistrationConfigurator> configure)
    {
        this.configure = configure ?? throw new ArgumentNullException(nameof(configure));
    }

    /// <summary>
    /// Applies custom MassTransit bus configuration, such as registering consumers,
    /// sagas, or activities. Delegates the configuration logic defined at the host level
    /// to avoid applying the same configuration to all hosts when not needed.
    /// </summary>
    public void Configure(IBusRegistrationConfigurator configurator)
    {
        this.configure(configurator);
    }
}
Register multiple consumers:
builder.Services.AddSingleton(
    new MassTransitIntegrationBusConsumersRegistrationConfigurator(cfg =>
    {
        cfg.AddConsumer<OrderCreatedIntegrationEventHandler>();
        cfg.AddConsumer<PaymentConfirmedIntegrationEventHandler>();
        cfg.AddConsumer<ShipmentRequestedIntegrationCommandHandler>();
    }));
Register the MassTransitIntegrationBusConsumersRegistrationConfigurator singleton before calling AddSharedKernelPresentationIntegrationBusRegistrations. The DI setup reads it from the service provider during bus configuration.

End-to-End Example

The following example shows a complete cross-service flow: the Orders service publishes an OrderCreatedIntegrationEvent and the Inventory service consumes it.

1. Define the integration event (shared contracts assembly)

public class OrderCreatedIntegrationEvent : BaseIntegrationEvent
{
    public OrderCreatedIntegrationEvent(Guid orderId, Guid customerId, decimal totalAmount)
    {
        OrderId      = orderId;
        CustomerId   = customerId;
        TotalAmount  = totalAmount;
    }

    public Guid    OrderId     { get; }
    public Guid    CustomerId  { get; }
    public decimal TotalAmount { get; }
}

2. Publish from the Orders service

// Orders.Application / OrderCreatedDomainEventHandler.cs
public sealed class OrderCreatedDomainEventHandler
    : INotificationHandler<OrderCreatedDomainEvent>
{
    private readonly IIntegrationEventBus integrationEventBus;

    public OrderCreatedDomainEventHandler(IIntegrationEventBus integrationEventBus)
        => this.integrationEventBus = integrationEventBus;

    public async Task Handle(
        OrderCreatedDomainEvent notification,
        CancellationToken cancellationToken)
    {
        var integrationEvent = new OrderCreatedIntegrationEvent(
            notification.OrderId,
            notification.CustomerId,
            notification.TotalAmount);

        await this.integrationEventBus.PublishAsync(integrationEvent, cancellationToken);
    }
}

3. Consume in the Inventory service

// Inventory.Presentation / Consumers / OrderCreatedIntegrationEventHandler.cs
public sealed class OrderCreatedIntegrationEventHandler
    : BaseIntegrationEventHandler<OrderCreatedIntegrationEvent>
{
    private readonly ICommandBus commandBus;

    public OrderCreatedIntegrationEventHandler(ICommandBus commandBus)
        => this.commandBus = commandBus;

    public override async Task HandleAsync(
        OrderCreatedIntegrationEvent integrationEvent,
        CancellationToken cancellationToken)
    {
        var command = new ReserveInventoryCommand(
            integrationEvent.OrderId,
            integrationEvent.Items);

        await this.commandBus.SendAsync(command, cancellationToken);
    }
}

4. Wire up the Inventory service Program.cs

var builder = WebApplication.CreateBuilder(args);

// Register the consumer BEFORE AddSharedKernelPresentationIntegrationBusRegistrations
builder.Services.AddSingleton(
    new MassTransitIntegrationBusConsumersRegistrationConfigurator(cfg =>
    {
        cfg.AddConsumer<OrderCreatedIntegrationEventHandler>();
    }));

builder.Services.AddSharedKernelPresentationIntegrationBusRegistrations(
    targetHostName: "InventoryService");

builder.Services.AddScoped<IIntegrationEventBus,   MassTransitIntegrationEventBus>();
builder.Services.AddScoped<IIntegrationCommandBus, MassTransitIntegrationCommandBus>();
builder.Services.AddScoped<IIntegrationQueryBus,   MassTransitIntegrationQueryBus>();

var app = builder.Build();
app.MapControllers();
app.Run();

Architecture Notes

SharedKernel follows MassTransit’s MultiBus pattern. Your application layer may already use an in-process MassTransit bus (bound to IBus) for dispatching domain events. The integration bus is a separate RabbitMQ-backed bus bound to IIntegrationBus. Using Bind<IIntegrationBus, T> in the producer constructors ensures MassTransit resolves the correct publish endpoint and client factory for the integration bus, not the in-process bus.
The IIntegrationCommandHandler interface documentation explicitly warns: a bounded context should not consume commands from another bounded context to enforce state changes. Commands imply ownership and intent. Sending a command from Service A to Service B means Service A is dictating what Service B must do — creating tight behavioral coupling. Prefer publishing an event from Service A and letting Service B react to it autonomously.

Build docs developers (and LLMs) love