Microservices in C# Part 1: Building and Testing

Fork me on GitHub

Microservice Architecture

Microservice Architecture

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:

Microservice Architecture core concept

Microservice Architecture core concept

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:

Anatomy of a Microservice

Anatomy of a Microservice

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:

RSSGitHubTwitter
LinkedInYouTubeGoogle+

14 thoughts on “Microservices in C# Part 1: Building and Testing

    1. Paul Mooney Post author

      While it’s certainly possible to host the Microservice as a console application, I recommend hosting as a Windows service. As a Windows service, you have the added advantage that your Microservice will start automatically after reboot, and can control permissions, etc., at an OS-level. You also have greater control over service failure, and how to handle restarts.
      Hosting as a console application, by contrast, offloads a lot of that responsibility to the Microservice itself.

      Reply
  1. Arun Nair

    Another follow up on this (just starting out on my first microservice in .NET) how would you code/deploy the API Layer/Gateway to microservices? Would this be a WCF service? Basically the microservice (or microservices?) is responsible for Creating, Starting, accepting bets, determine wins, support query etc. for a game? From my understanding one API gateway would expose all these methods, whereas each microservice will carry out individual tasks (Creating game, starting game, accepting bets etc.) and each microservice will point to its own individual database. But from what I have read if a microservice cannot make an async call to another microservice, then both microservices should be combined into one. Since its a gaming applicaiton, response time is of paramount importance, although I get the advantages of a queue like RabbitMQ. Hope the questions make sense?

    Reply
    1. Paul Mooney Post author

      The gateway can be any mechanism that provides a HTTP entry point, to allow customers access to your application. WCF in this case is fine, although as a technology, WCF has largely fallen out of favour against RESTful services.
      There is no reason why each Microservice need point to its own individual database; Microservices can access a shared database without issue, although this is not a requirement.
      You mention communication between Microservices and concurrency. Microservices focus more on parallelism than concurrency. Each Microservice essentially runs in its own process, potentially on a dedicated resource (VM, container, etc.). Is your process workflow-based? Consider that messages can be sent to more than 1 Microservice, and that each Microservice can process messages independently. If a Microservice is reliant on another Microservice, consider building a highly available design, such that the downstream Microservice is always available, and can dispatch responses in a timely fashion. Concatenating Microservices is not prohibited either, although this is something that needs case-by-case assessment.

      Reply
  2. Nik

    Hi Paul,

    Your blog post series on this has been really helpful for me and very interesting. I’ve downloaded the source for this from your GitHub but I’m hitting some issues when running it and was wondering if you could advise?

    If I run the website and just simply let it sit there, eventually (after a couple of minutes), the RabbitMQAdapter falls over in the “Publish” method. This is happening even though no messages are (as far as I’m aware) being posted via the website so I’m left to assume the micro-services are doing something?

    It’s odd because you have this line:
    if (!IsConnected) Connect();
    followed by
    using (var channel = _connection.CreateModel()) {

    The error occurs on during the CreateModel call and says there is no active connection. Checking IsConnected shows that it is currently disconnected. However a deeper dive shows that before the CreateModel method is called IsConnected is true.

    Delving into the error I find the following being thrown by RabbitMQ.Client :

    The AMQP operation was interrupted: AMQP close-reason, initiated by Library, code=541, text=”Unexpected Exception”, classId=0, methodId=0, cause=System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.

    I don’t suppose you’ve seen/resolved this or know what might be causing it if it’s actually a client/consumer side issue?

    Thanks again for a great set of blog posts, hope you can help.

    Nik

    Reply
    1. Paul Mooney Post author

      No, but the library is designed in such a way as to provide a level of abstraction above RabbitMQ. You can implement your own implementation that will satisfy any consuming client.

      Reply
  3. duy

    I’m new to microservice architecture. I’ve followed to your code but I don’t know how to run it (Run what project first or just only using browser.. etc)
    Can you explain more details?
    Thank you!

    Reply
  4. Zeeshan Mehmood

    Hi Paul,
    I am new to Microservices architecture. As you explained “Microservice.cs” is an interface which is implemented by Microservice. Does each Microservice has its own interface or can we share same interface with each Microservice?

    Thank you.

    Reply
    1. Paul Mooney Post author

      The same rules as regards applying interfaces to any code base also apply to Microservices. Apply interfaces as and when needed. Generally, interfaces are required as contracts between Microservices where proxy-based communication is used.

      Reply

Leave a reply to Arun Nair Cancel reply