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.
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 atCriticallevel.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.
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.
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.
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.
- 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
BroadcastBlocksemantics.
dotnet restore InternalEventBroadcaster.slnx
dotnet build InternalEventBroadcaster.slnx --configuration Release --no-restore
dotnet test InternalEventBroadcaster.slnx --configuration Release --no-buildGitHub Actions runs the same restore, build, and test sequence on pushes to main or master and on pull requests.