Overview
I’m often asked how to design and build a Microservice framework. It’s a tricky concept, considering loose level of coupling between Microservices. Consider the following scenario outlining a simple business process that consists of 3 sub-processes, each managed by a separate Microservice:
In order to test this process, it would seem that we need a Service Bus, or at the very least, a Service Bus mock in order to link the Microservices. How else will the Microservices communicate? Without a Service Bus, each Microservice is effectively offline, and cannot communicate with any component outside its own context. Let’s examine that concept a little bit further…maybe there is a way that we can establish at least some level of testing without a Service Bus.
A Practical Example
First, let’s expand on the previous tutorial and actually implement a simple Microservice-based application. The application will have 2 primary features:
- Take an integer-based input and double its value
- Take a text-based input and reverse it
Both features will be exposed through Microservices. Our first task is to define the Microservice itself. At a fundamental level, Microservices consist of a simple set of functionality:
Consider each Microservice daemon in the above diagram. Essentially, each they consist of
- a Message Dispatcher (publishes messages to a service bus
- an Event Listener (receives messages from a service bus
- Proprietary business logic (to handle inbound and outbound messages)
Let’s design a simple contract that encapsulates this:
internal interface Microservice { void Init(); void OnMessageReceived(object sender, MessageReceivedEventArgs e); void Shutdown(); }
Each Microservice implementation should expose this functionality. Let’s start with a simple math-based Microservice:
public class SimpleMathMicroservice : Microservice { private RabbitMQAdapter _adapter; private RabbitMQConsumerCatchAll _rabbitMQConsumerCatchAll; public void Init() { _adapter = RabbitMQAdapter.Instance; _adapter.Init("localhost", 5672, "guest", "guest", 50); _rabbitMQConsumerCatchAll = new RabbitMQConsumerCatchAll("Math", 10); _rabbitMQConsumerCatchAll.MessageReceived += OnMessageReceived; _adapter.Connect(); _adapter.ConsumeAsync(_rabbitMQConsumerCatchAll); } public void OnMessageReceived(object sender, MessageReceivedEventArgs e) { var input = Convert.ToInt32(e.Message); var result = Functions.Double(input); _adapter.Publish(result.ToString(), "MathResponse"); } public void Shutdown() { if (_adapter == null) return; if (_rabbitMQConsumerCatchAll != null) { _adapter.StopConsumingAsync(_rabbitMQConsumerCatchAll); } _adapter.Disconnect(); } }
Functionality in Detail
Init()
Establishes a connection to RabbitMQ. Remember, as per the previous tutorial, we need only a single connection. This connection is a TCP pipeline designed to funnel all communications to RabbitMQ from the Microservice, and back. Notice the RabbitMQConsumerCatchAll
implementation. Here we’ve decided that in the event of an exception occurring, our Microservice will catch each exception and deal with it accordingly. Alternatively, we could have implemented RabbitMQConsumerCatchOne
, which would cause the Microservice to disengage from the RabbitMQ Queue that it is listening to (essentially a Circuit Breaker, which I’ll talk about in a future post). In this instance, the Microservice is listening to a Queue called “Math”, to which messages will be published from external sources.
OnMessageReceived()
Our core business logic, in this case, multiplying an integer by 2, is implemented here. Once the calculation is complete, the result is dispatched to a Queue called “MathResponse”.
Shutdown()
Gracefully closes the underlying connection to RabbitMQ.
Unit Testing
There are several moving parts here. How do we test this? Let’s extract the business logic from the Microservice. Surely testing this separately from the application will result in a degree of confidence in the inner workings of our Microservice. It’s a good place to start.
Here is the core functionality in our SimpleMathMicroservice
(Functions class in Daishi.Math
):
public static int Double(int input) { return input * 2; }
Introducing a Unit Test as follows ensures that our logic behaves as designed:
[TestFixture] internal class MathTests { [Test] public void InputIsDoubled() { const int input = 5; var output = Functions.Double(input); Assert.AreEqual(10, output); } }
Now our underlying application logic is sufficiently covered from a Unit Testing perspective. Let’s focus on the application once again.
Entrypoint
How will users leverage our application? Do they interface with the Microservice framework through a UI? No, like all web applications, we must provide an API layer, exposing a HTTP channel through which users interact with our application. Let’s create a new ASP.NET application and edit Global.asax
as follows:
# region Microservice Init _simpleMathMicroservice = new SimpleMathMicroservice(); _simpleMathMicroservice.Init(); #endregion #region RabbitMQAdapter Init RabbitMQAdapter.Instance.Init("localhost", 5672, "guest", "guest", 100); RabbitMQAdapter.Instance.Connect(); #endregion
We’re going to run an instance of our SimpleMathMicroservice
alongside our ASP.NET application. This is fine for the purpose of demonstration, however each Microservice should run in its own context, as a daemon (*.exe in Windows) in a production equivalent. In the above code snippet, we initialise our SimpleMathMicroservice
and also establish a separate connection to RabbitMQ to allow the ASP.NET application to publish and receive messages. Essentially, our SimpleMathService
instance will run silently, listening for incoming messages on the “Math” Queue. Our adjacent ASP.NET application will publish messages to the “Math” Queue, and attempt to retrieve responses from SimpleMathService
by listening to the “MathResponse” Queue. Let’s implement a new ASP.NET Controller class to achieve this:
public class MathController : ApiController { public string Get(int id) { RabbitMQAdapter.Instance.Publish(id.ToString(), "Math"); string message; BasicDeliverEventArgs args; var responded = RabbitMQAdapter.Instance.TryGetNextMessage("MathResponse", out message, out args, 5000); if (responded) { return message; } throw new HttpResponseException(HttpStatusCode.BadGateway); } }
Navigating to http://localhost:{port}/api/math/100 will initiate the following process flow:
- ASP.NET application publishes the integer value 100 to the “Math” Queue
- ASP.NET application immediately polls the “MathResponse” Queue, awaiting a response from SimpleMathMicroservice
- SimpleMathMicroservice receives the message, and invokes Math.Functions.Double on the integer value 100
- SimpleMathService publishes the result to “MathResponse”
- ASP.NET application receives and returns the response to the browser.
Summary
In this example, we have provided a HTTP endpoint to access our SimpleMathMicroservice
, and have abstracted SimpleMathMicroservice’ core logic, and applied Unit Testing to achieve sufficient coverage. This is an entry-level requirement in terms of building Microservices. The next step, which I will cover in Part 2, focuses on ensuring reliable message delivery.
Connect with me: