This code defines a MessageBus
used to handle messages from many producers to a single consumer. The bus allows multiple publishers to send messages asynchronously to a single message handler. This is not a Pub-Sub
model, instead, it's a many publisher and single consumer with the ability to send a result back to the publisher. The publisher can await
for the message to be sent using the PostAsync
methods. Or await
for the completion or optional result of the handler by using the SendAsync
methods. All messages and handlers are treated asynchronously regardless of their implementation, however, a synchronous implementation will not block the publisher. This setup centralizes who actually handles messages instead of keeping track of many subscriptions.
Underneath the hood this is using TPL-Dataflow to flow messages and SimpleInjector for IoC and wiring up handlers to messages.
First, the interface definition of the example message bus:
public interface IMessageBus
{
/// <summary>
/// Posts a message to the bus and returns a Task representing the acceptance of the message.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="message"></param>
/// <returns></returns>
Task PostAsync<TMessage>(TMessage message) where TMessage : IMessage;
/// <summary>
/// Posts a message to the bus and returns a Task representing the acceptance of the message with an error handling delegate.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="message"></param>
/// <param name="OnError"></param>
/// <returns></returns>
Task PostAsync<TMessage>(TMessage message, Action<Exception> OnError) where TMessage : IMessage;
/// <summary>
/// Sends a message to the bus and returns a Task representing completion of handling the message.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <param name="message"></param>
/// <returns></returns>
Task SendAsync<TMessage>(TMessage message) where TMessage : IMessage;
/// <summary>
/// Sends a message to the bus and returns a Task representing completion of handling the message and yields the result of the handling.
/// </summary>
/// <typeparam name="TMessage"></typeparam>
/// <typeparam name="TReply"></typeparam>
/// <param name="message"></param>
/// <returns></returns>
Task<TReply> SendAsync<TMessage, TReply>(TMessage message) where TMessage : IMessage<TReply>;
}
And the implementation:
public class MessageBus : IMessageBus
{
private IContainer Container { get; }
public MessageBus(IContainer container) => Container = container ?? throw new ArgumentNullException(nameof(container));
public Task PostAsync<TMessage>(TMessage message) where TMessage : IMessage => PostAsync(message, null);
public Task PostAsync<TMessage>(TMessage message, Action<Exception> OnError) where TMessage : IMessage
{
var block = GetHandlerBlock<TMessage>(async msg =>
{
try
{
var handler = Container.GetInstance<IMessageHandler<TMessage>>();
await handler.HandleAsync(msg);
}
catch (Exception ex)
{
if (OnError == null) return;
OnError(ex);
}
});
return block.SendAsync(message);
}
public Task SendAsync<TMessage>(TMessage message) where TMessage : IMessage
{
var tcs = new TaskCompletionSource<bool>();
var block = GetHandlerBlock<TMessage>(async msg =>
{
try
{
var handler = Container.GetInstance<IMessageHandler<TMessage>>();
await handler.HandleAsync(msg);
tcs.TrySetResult(true);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
block.Post(message);
return tcs.Task;
}
public Task<TReply> SendAsync<TMessage, TReply>(TMessage message) where TMessage : IMessage<TReply>
{
var tcs = new TaskCompletionSource<TReply>();
var block = GetHandlerBlock<TMessage>(async msg =>
{
try
{
var handler = Container.GetInstance<IMessageHandler<TMessage, TReply>>();
var reply = await handler.HandleAsync(msg);
tcs.TrySetResult(reply);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
block.Post(message);
return tcs.Task;
}
private ITargetBlock<TMessage> GetHandlerBlock<TMessage>(Func<TMessage, Task> action) => new ActionBlock<TMessage>(action);
}
The publisher has a few options:
- Post a message, awaiting only for the message to be accepted.
- Post a message with an error handling delegate, awaiting only for the message to be accepted.
- Send a message awaiting completion of processing.
- Send a message awaiting a reply from the handler.
And the supporting interfaces:
public interface IMessage { }
public interface IMessage<TReply> : IMessage { }
public interface IMessageHandler<TMessage> where TMessage : IMessage
{
Task HandleAsync(TMessage message);
}
public interface IMessageHandler<TMessage, TReply> where TMessage : IMessage<TReply>
{
Task<TReply> HandleAsync(TMessage message);
}
public interface IContainer
{
TInstance GetInstance<TInstance>() where TInstance : class;
}