In this series of posts, I'm going to talk a little about Microservices, a little about Nancy, and I'm going to put together a very simple architecture that should serve as a good foundation for your own Microservices projects.

  • Part 1 will focus on getting a single Microservice running.
  • Part 2 will add in a second Microservice that can be consumed by the first service, and provide a simple framework for inter-service communication.
  • Part 3 will explore deployment with Docker, Nomad and Consul.

Microservices

What is microservices architecture? In a nutshell, it's "SOA", but each service endpoint runs in it's own process.

Why do it? There's plenty of articles kicking around as to why, and what the trade-offs are, etc... However, the reasons that I do it are thus:

  • You're baking in a way to efficiently scale, both in terms of team size, and project complexity. By keeping teams small and autonomous, it encourages innovation, and reduces the need for communication that can otherwise kill your productivity as you scale.
  • Individuals within your team don't need to understand as much of the problem domain (and indeed, even as much of "other bits of code") as they usually do. In practice, this basically means less head-scratching and more productivity.
  • It requires automation. Automation is a good thing. You may have to do a bit more than usual to get a production-grade microservices cluster running and maintainable, but it's worth it.
  • Since each Microservice runs in it's own process, does a small number of things, and is loosely coupled to other services, technology choice isn't as important. You could use a different language for every service if you wanted and it wouldn't affect the system as a whole. Early decisions on framework or language don't impact you years down the line in the same way as they would in a monolith - you can change it.

Nancy

What's Nancy? If you have ever done ruby... you've probably heard of a domain-specific language called "Sinatra" - that you might use intead of rails. Nancy is built from the ground up with the same core principles, but in .Net.
Nancy is a low-ceremony web framework that you would use instead of MVC or WebAPI.

Nancy "just works". It gets out of your way and just let's you get on with it. Turns out it's pretty good for building Microservices for that reason. It runs on OWIN, IIS, Self-hosted, Kestrel, whatever. Here's a quick sample taken from the Nancy github readme:

public class Module : NancyModule
{
    public Module()
    {
        Get("/greet/{name}", x => {
            return string.Concat("Hello ", x.name);
        });
    }
}

Sample Solution

To get a true feel for how to do this, we need to create a sample solution with 2 microservices - so we can explore inter-service communication and how that might work.

We'll build 4 projects:
  • Microservice 1: FactorialService - a simple service that calculates the factorial of a number (n!). Eg 4! = 4 * 3 * 2 * 1 = 24
  • Microservice 2: LoggingService - keep a log of activity from the FactorialService
  • FactorialService.Models and LoggingService.Models - the request and response class definitions. These serve as contracts for service calls and are kept separate so that they may be referenced by consuming services.

Note we are only building the FactorialService and FactorialService.Models project in Part 1

We'll use:

Since we're using 100% cross-platform tools, everything we do should work on Mac, PC or Linux.

Solution setup
  • Make a folder... i.e. "MicroservicesDemo" somewhere
  • Launch Visual Studio Code and open the folder you just created
  • Install the following Visual Studio Code Extensions (click "Extensions" in the sidebar): C#, C# Extensions and hit the Restart button once installed
  • Inside the MicroServicesDemo folder, make a folder called "src". All of our projects will live here.
  • Inside the MicroservicesDemo folder, make a json file called "global.json" and put the following content into it:
{
    "projects": [ "./src" ]
}
  • Open the command palette (CMD-Shift-P on Mac, Ctrl-Shift-P on PC) and use the "OmniSharp: Select Project" command. Set it to the MicroservicesDemo folder.

Microservice 1: FactorialService

Launch the VS Code Integrated Terminal ( Ctrl + ` ) and run:

cd src
mkdir FactorialService
cd FactorialService
dotnet new

Since this article was written, Microsoft have updated the dotnet CLI and the above command no longer works as-is. Please use "dotnet new console" instead

Scaffolding

We need to add a few packages to project.json, modify Program.cs, add a Startup.cs file and a Nancy Bootstrapper.

project.json - modify as shown to add package dependencies


{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.AspNetCore.Hosting": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.Owin": "1.1.0",
    "Nancy": "2.0.0-barneyrubble",
    "Nancy.Validation.FluentValidation": "2.0.0-barneyrubble"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

Since this article was written, microsoft have changed to csproj instead of project.json. You can now use the dotnet CLI to add packages - with: "dotnet add package".

Program.cs - modify to launch Kestrel on a pre-defined port


using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace MicroservicesDemo.FactorialService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                .UseKestrel()
                .UseUrls("http://*:5001/")
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>()
                .Build();

            host.Run();
        }
    }
}

Startup.cs - in full. Here we add a console logger and tell it to use Nancy to process requests. Note that we are passing a new Bootstrapper that we are about to create - this is so that we may copy dependencies from the built-in container to Nancy's TinyIOC.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using Nancy.Owin;

namespace MicroservicesDemo.FactorialService
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            app.UseOwin(pipeline => pipeline.UseNancy(options =>
            {
                options.Bootstrapper = new Bootstrapper(app.ApplicationServices);
            }));
        }
    }
}

Bootstrapper.cs - a place to copy dependencies. At the time of writing this is how you need to copy dependencies into Nancy with .net core. I expect this to improve in future. Note we're also enabling tracing so we can see and diagnose any errors when testing the microservice.

using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Nancy;
using Nancy.TinyIoc;
using Nancy.Configuration;

namespace MicroservicesDemo.FactorialService
{ 
    public class Bootstrapper : DefaultNancyBootstrapper
    {
        readonly IServiceProvider _serviceProvider;

        public Bootstrapper(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public override void Configure(INancyEnvironment environment)
        {
            environment.Tracing(true, true);
        }

        protected override void ConfigureApplicationContainer(TinyIoCContainer container)
        {
            base.ConfigureApplicationContainer(container);
            container.Register(_serviceProvider.GetService<ILoggerFactory>());
        }
    }
}

If you run this (change into FactorialService folder in Terminal) and type:

dotnet restore
dotnet run

You should get this:

Hosting environment: Production
Content root path: D:\Projects\MicroservicesDemo\FactorialService
Now listening on: http://*:5001
Application started. Press Ctrl+C to shut down.

Go ahead and hit Ctrl+C. Now we're fully bootstrapped and we can add the Nancy Module.

Nancy Module

Before we add the module, we want to create a models project to hold the request and response. Note this is overkill in this simple example, but I'm trying to illustrate a pattern that we can use to build a more complex solution.

From terminal, in the src folder, run:

mkdir FactorialService.Models
cd FactorialService.Models
dotnet new -t lib
dotnet restore

Since this article was written, the new command for creating a class library is: "dotnet new classlib --framework netstandard1.6".

Add System.Dynamic.Runtime to the newly generated project.json in the FactorialService.Models project (so we can use the dynamic keyword in our source):


{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable"
  },
  "dependencies": {
    "System.Dynamic.Runtime": "4.3.0"
  },
  "frameworks": {
    "netstandard1.6": {
      "dependencies": {
        "NETStandard.Library": "1.6.1"
      }
    }
  }
}

Since this article was written, microsoft have changed to csproj instead of project.json. You can now use the dotnet CLI to add packages - with: "dotnet add package".

Now, create 2 classes in FactorialService.Models:

FactorialRequest.cs

namespace MicroservicesDemo.FactorialService.Models
{
    public class FactorialRequest
    {
        public int Number { get; set; }
    }
}

FactorialResponse.cs

using System.Collections.Generic;

namespace MicroservicesDemo.FactorialService.Models
{
    public class FactorialResponse
    {
        public int Factorial { get; set; }
        public bool Success { get; set; }
        public IEnumerable<dynamic> Errors { get; set; }
    }
}

We want to reference this newly created library from FactorialService. Update the FactorialService project.json file:


{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true
  },
  "dependencies": {
    "Microsoft.Extensions.Logging.Console": "1.1.0",
    "Microsoft.AspNetCore.Hosting": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.1.0",
    "Microsoft.AspNetCore.Owin": "1.1.0",
    "Nancy": "2.0.0-barneyrubble",
    "Nancy.Validation.FluentValidation": "2.0.0-barneyrubble",
    "FactorialService.Models": "1.0.0-*"
  },
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0"
        }
      },
      "imports": "dnxcore50"
    }
  }
}

Since this article was written, microsoft have changed to csproj instead of project.json. You can now use the dotnet CLI to add references - with: "dotnet add reference".

Go ahead and create a Module.cs file in FactorialService:

using Microsoft.Extensions.Logging;
using Nancy;
using MicroservicesDemo.FactorialService.Models;

namespace MicroservicesDemo.FactorialService
{
    public class Module : NancyModule
    {
        private ILogger<Module> _logger;

        public Module(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<Module>();

            Get("/factorial/{number:int}", p => GetFactorial(new FactorialRequest { Number = p.number }));
        }
        
        FactorialResponse GetFactorial(FactorialRequest request)
        {
            var result = new FactorialResponse { Success = true, Factorial = Factorial(request.Number) };
            _logger.LogInformation($"Generated Factorial for {request.Number}: {result.Factorial}");

            return result;
        }

        int Factorial(int i)
        {
            if (i <= 1)
                return 1;
            return i * Factorial(i - 1);
        }
    }
}

In terminal, within the FactorialService folder, run:

dotnet restore
dotnet run

If you load up Postman, and try to hit the service, you should get this:

Cool! One microservice done. Almost... before we go any further we'll add some validation. We want to restrict the input to a positive integer between 1 and 12 - this is because it'll overflow our integer from 13 and up.

Nancy can find validators in the currently executing assembly. Create a new file in FactorialService called Validators.cs

using FluentValidation;
using MicroservicesDemo.FactorialService.Models;

namespace MicroservicesDemo.FactorialService
{
    public class FactorialRequestValidator : AbstractValidator<FactorialRequest>
    {
        public FactorialRequestValidator()
        {
            RuleFor(request => request.Number).InclusiveBetween(1,12);
        }
    }
}

Hooking it up is very simple. Make the following changes to Module.cs


using Microsoft.Extensions.Logging;
using Nancy;
using MicroservicesDemo.FactorialService.Models;
using Nancy.Validation;

namespace MicroservicesDemo.FactorialService
{
    public class Module : NancyModule
    {
        private ILogger<Module> _logger;

        public Module(ILoggerFactory loggerFactory)
        {
            _logger = loggerFactory.CreateLogger<Module>();

            Get("/factorial/{number:int}", p => GetFactorial(new FactorialRequest { Number = p.number }));
        }
        
        FactorialResponse GetFactorial(FactorialRequest request)
        {
            var validationResult = this.Validate(request);
            if (!validationResult.IsValid)
            {
                _logger.LogInformation($"Failed to generate factorial for {request.Number}");
                return new FactorialResponse { Success = false, Errors = validationResult.FormattedErrors };
            }

            var result = new FactorialResponse { Success = true, Factorial = Factorial(request.Number) };
            _logger.LogInformation($"Generated Factorial for {request.Number}: {result.Factorial}");

            return result;
        }

        int Factorial(int i)
        {
            if (i <= 1)
                return 1;
            return i * Factorial(i - 1);
        }
    }
}

Now if we run our code, it should show errors as appropriate. Note also that we are restricting the request to integers - non integers will throw a 404.

And that is a simple Microservice built using Nancy and .net core. In the next part, I'll introduce a second Microservice for Logging, and implement inter-service communication. In the last part, we'll Dockerize our services and deploy them to a Nomad cluster, with service discovery provided by Consul.

Companion source code is available on Bitbucket. This post continues in Part 2.