View on GitHub

Acquaintance

Internal messaging framework for loosely-coupled .NET applications.

Acquaintance Overview

Acquaintance is a .NET library for intra-process communications. It’s like an in-memory message bus that loosely-coupled parts of your application can use to communicate with each other. At the heart, Acquaintance implements 3 basic communication patterns: Publish/Subscribe, Request/Response and Scatter/Gather. In addition to these three basic patterns, Acquaintance has a few other tricks and features which can help most medium to large applications stay organized and safe.

Get Acquaintance from nuget:

Install-Package Acquaintance

When you’re ready to code, create a message bus:

var messageBus = new MessageBus();

Publish/Subscribe

Publish/Subscribe is a pattern where you have events of interest being generated by one part of the system, and other parts need to be aware of it. Published messages are delivered to all interested subscribers.

// Create a subscription
messageBus.Subscribe(s => s
    .WithChannelName("test event")
    .Invoke(e => Console.WriteLine(e.Message)));

// Publish a message
messageBus.Publish("test event", new MyEvent {
    Message = "Hello World"
});

Unlike the C# event mechanism, the message bus solves the chicken-and-egg problem by allowing the event producer and the event consumers to be created in any order, at any time.

Unlike callback delegates, Acquaintance will automatically dispatch your request onto a worker thread so it doesn’t block other processing.

With Acquaintance it’s easy to interact with other communications mechanisms be sending to and receiving from other communication channels such as ZeroMQ, RabbitMQ, NServiceBus or Kafka.

Request/Response

Request/Response is basically an abstracted method call or local RPC mechanism. One part of your code makes a request, and a single listener fulfills it. This can be an improvement over Dependency Injection or Service Location patterns in some cases, such as when you only need a calculation result and do not want to manage and maintain references to the objects which produce that result.

// Setup a Listener
messageBus.Listen<MyRequest, MyResponse>(l => l
    .WithChannelName("test")
    .Invoke(req => new MyResponse { 
        Message = "Hello " + req.Message"
    }));

// Send a request
var response = messageBus.Request<MyRequest, MyResponse>("test", new MyRequest {
    Message = "World"
});
Console.WriteLine(response.Message);

Again, Acquaintance allows the channel to be constructed in any order and also allows request processing to be dispatched to an appropriate worker thread. With the right settings, you can easily serialize requests to a resource which is not thread-safe, or provide yourself some protections against a resource which is error-prone.

Scatter/Gather

Scatter/Gather is very similar to Request/Response except the channel may have many listeners or participants, which may return several responses. These responses are all returned together.

// Setup a Participant
messageBus.Participate<MyRequest, MyResponse>(p => p
    .WithChannelName("test")
    .Invoke(req => new[] { new MyResponse { 
        Message = "Hello " + req.Message"
    }}));

// Scatter the request message to all participants
var response = messageBus.Scatter<MyRequest, MyResponse>("test", new MyRequest {
    Message = "World"
});
foreach (var message in response.Responses)
    Console.WriteLine(response);

Scatter/Gather can be used to implement a Map/Reduce system or it can be used in cases where multiple bids need to be accepted or multiple values need to be gathered and compared.

Testing and Monitoring Features

Acquaintance provides several in-built features to support unit testing scenarios, such as mocking of communication channels and expectations of events.

Acquaintance provides eavesdropping features, where the final results of a Request/Response or Scatter/Gather conversation can be monitored.

Acquaintance provides some features for managing problematic resources, such as an implementation of the Circuit Breaker pattern, error eventing and retry/failover features.

Processing Networks and Pipelines

Acquaintance provides features to construct processing networks where several independent processing units need to be arranged into a network or pipeline. Each node in the network can subscribe directly to the output of other nodes, where complex calculations can be built up from small simple parts.

Use-Cases

Here are some common use-cases which Acquaintance was explicitly designed to handle:

  1. Simplifying complex constructor signatures which may arise from use of Dependency Injection, and avoiding the creation of many redundant adaptor types.
  2. Providing serialized access to resources which are not intrinsically thread-safe, such as sockets and files
  3. Round-Robin dispatch and predicate-based routing of events and requests to multiple handlers
  4. Serving as an easy wiring mechanism for loosely-coupled and plugin-based applications
  5. Serving as an intermediate step for monolithic applications which are in the process of being decomposed into microservices
  6. Serving as an easy bridge for external resources, queues and Enterprise Service Buses
  7. Acting as the communication mechanism for domain events in a Domain-Driven solution

Contraindications

Acquaintance is not a silver-bullet solution, and comparing it to other solutions and patterns is going to yield some positive and some negative points. It is important to consider the specific needs of your application before deciding whether to use Acquaintance or an alternative pattern or library.

At it’s heart, Acquaintance runs contrary to some important Object-Oriented Programming principles and ideas. It conflicts with Dependency Injection, for example, and it suffers from the same discoverability problems that often make “Service Locator” an anti-pattern. Using Acquaintance for too much runs the risk of your message bus looking like a “God Object”, and loose-coupling of this sort can make your code impervious to code analysis and refactoring tools. Adding Acquaintance into software which is a big mess and doesn’t follow good OOP design best practices might just make things worse. Look for places in your system which demand loose-coupling and require one or more of the use-cases described above.

Message-passing in Acquaintance is cheaper than making a remote call to a separate service, but it’s much more expensive than passing a message in a language like Objective-C or Erlang. You don’t want to use Acquaintance for every single method call, and you want to keep performance in mind whenever you employ it.

Acquaintance works with threading primitives to provide dispatching behaviors, and it is entirely possible for you to configure Acquaintance to produce bottlenecks, resource starvation and soft-deadlocks if you are not paying enough attention to system design.

All this being said, there are many situations where Acquaintance can be much more help than hinderance, and many teams which can find real benefit in it’s features.