This is a .NET wrapper around RabbitMQ that allows an easy way to setup RPC communication.
For more information refer to the RabbitMQ documentation.
The RPC server should expose some sort of contract in the form of an interface. The contract has the following limitations:
- No two methods of the contract may have the same name. The reason for that is that the fully qualified name of the interface followed by the name of each method is used to uniquely identify a queue name via which the request communication will happen.
- Both the parameter types and the return types of all methods should be JSON serializable. The reason for that is because both the arguments and the result of a method will be serialized to be transfered as RabbitMQ messages.
public interface ILibraryService
{
Book? GetBook(string isbn);
IEnumerable<Book> GetBooksByAuthor(string author);
void AddBook(Book book);
void DeleteBook(string isbn);
}
public record Book(string ISBN, string Name, string Author, DateTime PublicationDate);
Of course both the server and the client require a connection to a working RabbitMQ instance. To create one you can follow the RabbitMQ tutorial.
For your convenience we have added a simple extension method that lets you configure and inject a connection so you won't have to deal with the RabbitMQ.Client package at all.
using Byteology.RabbitRpc;
using Microsoft.Extensions.DependencyInjection;
IServiceCollection services;
/// ...
services.AddRabbitMQ(connectionFactory =>
{
connectionFactory.HostName = "localhost";
connectionFactory.Port = 5672;
connectionFactory.UserName = "guest";
connectionFactory.Password = "guest";
});
First of all the server should naturally provide an implementation of the contract. This is the business logic of your server.
public class LibraryService : ILibraryService
{
private List<Book> _books = new() {
new Book("0345339703", "The Lord of the Rings: The Fellowship of the Ring", "J.R.R.Tolkien", new DateTime(1986, 08, 12)),
new Book("0345339711", "The Lord of the Rings: The Two Towers", "J.R.R.Tolkien", new DateTime(1986, 08, 12)),
new Book("0345339738", "The Lord of the Rings: The Return of the King", "J.R.R.Tolkien", new DateTime(1986, 07, 12)),
new Book("0553293354", "Foundation", "Isaac Asimov", new DateTime(1991, 10, 01))
};
public void AddBook(Book book) => _books.Add(book);
public void DeleteBook(string isbn) => _books.RemoveAll(x => x.ISBN == isbn);
public Book? GetBook(string isbn) => _books.FirstOrDefault(x => x.ISBN == isbn);
public IEnumerable<Book> GetBooksByAuthor(string author) => _books.Where(x => x.Author == author);
}
You can start an RPC server by creating an instance of RpcServer<>
. Just make sure it won't be garbage collected.
IConnection rabbitMqConnection;
// Initialize the RabbitMQ connection
// ...
RpcServer<ILibraryService> server = new (rabbitMqConnection, new LibraryService());
server.Start();
Alternatively can start an RPC server during startup. To do that you should inject a RabbitMQ connection as well as a contract implementation and then call the StartRpcServer<>()
method on the IHost
with the type parameter of the contract.
using Byteology.RabbitRpc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
await Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddRabbitMQ(connectionFactory =>
{
connectionFactory.HostName = "localhost";
connectionFactory.Port = 5672;
connectionFactory.UserName = "guest";
connectionFactory.Password = "guest";
});
services.AddSingleton<ILibraryService, LibraryService>();
})
.Build()
.StartRpcServer<ILibraryService>()
.RunAsync();
To use an RPC client you would generally want to inject it. That can be achieved by the AddRpcClient<>
extension method.
using Byteology.RabbitRpc;
services
.AddRabbitMQ(/* config connection to RabbitMQ */)
.AddRpcClient<ILibraryService>();
Once you have registered an RPC Client you can resolve the contract interface and use it as if you were doing local calls.
public class SimpleExample
{
private readonly ILibraryService _libraryService;
public SimpleExample(ILibraryService libraryService)
{
_libraryService = libraryService;
}
public void CallServer()
{
IEnumerable<Book> tolkienBooks = _libraryService.GetBooksByAuthor("J.R.R.Tolkien");
foreach (Book book in tolkienBooks)
Console.WriteLine(book.Name);
}
}
The above example does not provide any asynchronous capabilities nor a way to cancel the remote procedure call. If you need anything simillar you should resolve an RpcClient<>
object instead.
public class AdvancedExample
{
private readonly RpcClient<ILibraryService> _libraryClient;
public AdvancedExample(RpcClient<ILibraryService> libraryClient)
{
_libraryClient = libraryClient;
}
public async void CallServer()
{
IEnumerable<Book> tolkienBooks = await _libraryClient.CallAsync(x => x.GetBooksByAuthor("J.R.R.Tolkien"));
foreach (Book book in tolkienBooks)
Console.WriteLine(book.Name);
}
}
The CallAsync
method additionally accepts an optional CancellationToken
argument which allows you to cancel the call. Note that the call might already be queued or executed by the server in which case the token will only cancel the consumption of the response and it will clear it from the response queue.
The RpcCleint<>
class can be directly constructed by simply providing an open RabbitMQ connection
IConnection rabbitMqConnection;
// Initialize the RabbitMQ connection
// ...
RpcServer<ILibraryService> client = new (rabbitMqConnection);
A full example of this library's usage can be found here.