Skip to content

Repository files navigation

InternalEventBroadcaster

InternalEventBroadcaster is a .NET 10 class library for bounded, in-process event distribution. Applications use one interface, IBroadcaster, to broadcast events and manage subscriptions. Internally, broadcasts are written to a bounded Channel<IEvent>. A hosted background service drains that queue and sends each event through a TPL Dataflow BroadcastBlock<IEvent>. Every subscription gets its own bounded BufferBlock<IEvent> before its target consumer.

The library is intended for communication inside one process. It is not a durable message broker and does not persist or replay an event after a process restart.

Why these three bounded stages exist

  • Channel<IEvent> separates fast publishers from the background broadcaster. Publishing is non-blocking; when the queue is full, the new event is rejected and logged at Critical level.
  • BroadcastBlock<IEvent> fans out one event instance to all current subscribers and retains the latest event for a subscriber that joins later.
  • A BufferBlock<IEvent> per subscription lets each consumer run at its own pace for a bounded number of accepted events.

BroadcastBlock<T> has latest-value semantics, not durable backlog semantics. If a subscriber remains slower than the producer after its buffer fills, that subscriber may skip intermediate events and later receive a newer event. Choose a broker with a durable queue per consumer when every event must be retained.

Install and register

Reference the project or the produced NuGet package, then register it with the host:

using InternalEventBroadcaster;

builder.Services.AddInternalEventBroadcaster(options =>
{
    options.QueueCapacity = 1_000;
    options.BroadcastCapacity = 16;
    options.BufferCapacity = 100;
});

All capacities must be greater than zero. Registration exposes IBroadcaster as a singleton. The channel publisher, channel reader, Dataflow broadcaster, and background service are internal implementation details.

Broadcast an event

Events should be immutable because every subscriber receives the same instance.

public sealed record OrderAccepted(Guid OrderId) : IEvent;

public sealed class OrderService(IBroadcaster broadcaster)
{
    public void Accept(Guid orderId, CancellationToken cancellationToken)
    {
        broadcaster.Broadcast(new OrderAccepted(orderId), cancellationToken);
    }
}

IBroadcaster.Broadcast uses the internal channel publisher and ChannelWriter.TryWrite; it never waits for capacity. A full ingress queue causes the incoming event to be dropped and critically logged.

Subscribe

Subscribers supply any TPL Dataflow target. Keep the returned handle and dispose it when the consumer stops.

using System.Threading.Tasks.Dataflow;
using InternalEventBroadcaster;

public sealed class OrderProjection : IHostedService
{
    private readonly IBroadcaster _broadcaster;
    private readonly ActionBlock<IEvent> _consumer;
    private IDisposable? _subscription;

    public OrderProjection(IBroadcaster broadcaster)
    {
        _broadcaster = broadcaster;
        _consumer = new ActionBlock<IEvent>(HandleAsync, new ExecutionDataflowBlockOptions
        {
            MaxDegreeOfParallelism = 1,
            EnsureOrdered = true
        });
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _subscription = _broadcaster.Subscribe(_consumer);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _subscription?.Dispose();
        _consumer.Complete();
        await _consumer.Completion.WaitAsync(cancellationToken);
    }

    private static Task HandleAsync(IEvent @event) =>
        @event is OrderAccepted accepted
            ? UpdateProjectionAsync(accepted)
            : Task.CompletedTask;

    private static Task UpdateProjectionAsync(OrderAccepted @event) => Task.CompletedTask;
}

Subscribe during IHostedService.StartAsync so the subscription exists before producers begin publishing. A late subscription immediately receives the BroadcastBlock's current event, when one exists.

Delivery and concurrency guarantees

  • The channel supports multiple concurrent publishers and has one background reader.
  • Internal Dataflow broadcasts are serialized, and each subscriber buffer preserves accepted-event order.
  • Delivery is in-memory and at-most-once. There are no acknowledgements, retries, persistence, or cross-process delivery.
  • Disposing a subscription prevents future broadcasts from being offered to that subscriber; already accepted work belongs to the consumer.
  • Queue overflow is observable through critical logging. A persistently slow subscriber can miss intermediate values after its bounded buffer fills because of BroadcastBlock semantics.

Build and test

dotnet restore InternalEventBroadcaster.slnx
dotnet build InternalEventBroadcaster.slnx --configuration Release --no-restore
dotnet test InternalEventBroadcaster.slnx --configuration Release --no-build

GitHub Actions runs the same restore, build, and test sequence on pushes to main or master and on pull requests.

About

A class library for bounded, in-process event distribution.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages