This is a follow-on from Part 1.

Even though we already have console logging, we'll add a LoggingService so that we can figure out how Microservices might talk to each other. We'll be invoking the service directly over HTTP via a pseudo-RPC mechanism.

Note that we aren't considering a number of things that you'll ordinarily want to consider for a larger microservices project such as the use of a correlation ID, retries, fire and forget, or an event driven architecture.

Whilst we aren't adding service discovery, we'll cover that in Part 3

You can get the starter code for this from Bitbucket - go ahead and grab it, or use your code from Part 1.

Dev Environment

If you haven't followed part 1, here's a rundown:

  • The dev environment, all tools & code will run on Mac, PC or Linux
  • We are using Visual Studio Code
  • Open the Solution root folder in vscode
  • Install the following Visual Studio Code Extensions (click "Extensions" in the sidebar): C#, C# Extensions and hit the Restart button once installed
  • 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 Solution root folder.

Migrate old projects to csproj

Since Part 1 was written, Microsoft have updated their dotnet tools to remove support for project.json (and xproj). Instead, they've reverted back to the MSBuild-based .csproj, but it's much less bloaty. Make sure your .net core SDK is up to date

Navigate into the src/FactorialService and src/FactorialService.Models folders and type "dotnet migrate" in a terminal. This will replace the project.json files with csproj files.

Create new projects

In the "src" folder, either from VSCode built in terminal, or a separate prompt, type:

dotnet new console --framework netcoreapp1.1 --output LoggingService
dotnet new classlib --framework netstandard1.6 --output LoggingService.Models

We use netstandard for the models because it's for libraries that can be consumed by different frameworks, .net core apps or .net 4.6 for example.

Delete the "Class1.cs" file from the models project.

Add packages

Another welcome new feature in the dotnet CLI is the ability to add packages.

In LoggingService, run:
dotnet add package Nancy -v 2.0.0-clinteastwood
dotnet add package Nancy.Validation.FluentValidation -v 2.0.0-clinteastwood
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.AspNetCore.Hosting
dotnet add package Microsoft.AspNetCore.Server.Kestrel
dotnet add package Microsoft.AspNetCore.Owin
In LoggingService.Models, run:
dotnet add package System.Dynamic.Runtime

Create the Models

In LoggingService.Models, add a new C# file, called LogRequest.cs, and add a "Message" property.

namespace LoggingService.Models
{
    public class LogRequest
    {
        public string Message { get; set; }
    }
}

Then create one more C# file, called LogResponse.cs, add a "Success" property and a property to hold any errors.

using System.Collections.Generic;

namespace LoggingService.Models
{
    public class LogResponse
    {
        public bool Success { get; set; }
        public IEnumerable<dynamic> Errors { get; set; }
    }
}

Note that the use of a models library is questionable for this use-case. It's implemented this way to illustrate a design pattern that you can use to build larger microservices.

Reference LoggingService.Models from LoggingService

Run this command-line from the LoggingService folder:

dotnet add reference ../LoggingService.Models/LoggingService.Models.csproj

Reference LoggingService.Models from FactorialService

We'll be referencing the logging service models (but not the service itself) from FactorialService. This is the "contract".

Run the same command as above, from the FactorialService folder:

dotnet add reference ../LoggingService.Models/LoggingService.Models.csproj

For larger projects, you should consider consumer-driven contracts, and testing them with something like Pact

Create the LoggingService

Similar to FactorialService, we need 4 new c-sharp source files in our LoggingService: a startup file, the nancy bootstrapper, the nancy module, and the validations. We already have a Program.cs file that we will change. This simple structure is copied from our first service, and easily repeatable.

Startup.cs

Typical asp.net core boilerplate. Here we add a console logger and tell our microservice to serve requests with Nancy, and with our Nancy Bootstrapper that we are about to create.

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

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

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

Again, typical boilerplate. We want to serve requests on port 5002 so as to not conflict with our first service, that serves on 5001. Note the asterisk means "any hostname" and is useful for ensuring the same code works in any environment.

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

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

            host.Run();
        }
    }
}
Bootstrapper.cs

The nancy bootstrapper is used to copy dependencies from .net core's DI system into Nancy's TinyIOC.

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

namespace LoggingService
{
    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>());
        }
    }
}
Module.cs

This is the guts of the service. For this implementation, we're simply logging the received message to console. However, you may want to write to file system or a database in a proper implementation.

using Microsoft.Extensions.Logging;
using Nancy;
using Nancy.ModelBinding;
using LoggingService.Models;
using Nancy.Validation;

namespace LoggingService
{
    public class Module : NancyModule
    {
        private ILogger<Module> _logger;

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

            Post("/log", p => PostLogRequest());
        }

        LogResponse PostLogRequest()
        {
            var request = this.Bind<LogRequest>();
            var validationResult = this.Validate(request);

            if (!validationResult.IsValid)
            {
                _logger.LogInformation($"Failed to log message '{request.Message}'");
                return new LogResponse { Success = false, Errors = validationResult.FormattedErrors };
            }
            
            _logger.LogInformation($"Logging Message: '{request.Message}'");

            return new LogResponse { Success = true };
        }

    }
}
Validators.cs

We'll just make sure the message we want to log isn't empty.

using FluentValidation;
using LoggingService.Models;

namespace LoggingService
{
    public class FactorialRequestValidator : AbstractValidator<LogRequest>
    {
        public FactorialRequestValidator()
        {
            RuleFor(request => request.Message).NotEmpty();
        }
    }
}

Testing LoggingService

To test, we simply need to restore packages, and run. In a terminal, from LoggingService:

dotnet restore
dotnet run

You should see:

Hosting environment: Production
Content root path: src\LoggingService
Now listening on: http://*:5002
Application started. Press Ctrl+C to shut down.

Now we can test in Postman. Launch it, and test our endpoint.

Failure case

Success case

Console output

Invoking LoggingService from FactorialService

Hopefully that all works as-above. Now we simply need to build some plumbing to allow the Factorial service to invoke the Logging service. Normally you might put this plumbing into a small helper assembly, where you could add policies using something like Polly. For now, we'll just add it straight into our FactorialService.

Add Newtonsoft.Json package

Since we're dealing with JSON, we need this. In a terminal, in the FactorialService directory:

dotnet add package Newtonsoft.Json
ServiceClient.cs

Since we're only calling post, we'll just implement the post method here... but you should implement the other methods as needed, plus any policies you want to implement. This implementation will work for any service call that has a request and response object and takes a POST.

You'll note below that I'm instantiating a new HttpClient for every post. This is bad. HttpClient is designed specifically to be persisted and reused for multiple requests. Don't use BaseAddress if you are calling different hosts with the same client.

Add a new class to FactorialService:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace MicroservicesDemo.FactorialService
{
    public class ServiceClient<TRequest, TResponse>
    {
        readonly string _baseUrl;
        readonly string _path;
        
        public ServiceClient(string baseUrl, string path)
        {
            _baseUrl = baseUrl;
            _path = path;
        }

        public async Task<TResponse> Post(TRequest request)
        {
            var client = new HttpClient { BaseAddress = new Uri (_baseUrl) };
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var requestJson = JsonConvert.SerializeObject(request);
            var response = await client.PostAsync(_path, new StringContent(requestJson, Encoding.UTF8, "application/json"));
            var responseJson = await response.Content.ReadAsStringAsync();

            return JsonConvert.DeserializeObject<TResponse>(responseJson);
        }
    }
}

I've not included auth or policies - if the calling service isn't available it'll cascade. Bearer token headers could also be wrapped into the client, and policies can be implemented with something like Polly

Inject our client

We need to inject our client - firstly into .net core's application services, then into Nancy's TinyIOC. 2 small changes required.

In FactorialService's Startup class:


using LoggingService.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Nancy.Owin;

namespace MicroservicesDemo.FactorialService
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ServiceClient<LogRequest, LogResponse>>(new ServiceClient<LogRequest,LogResponse>("http://localhost:5002", "log"));
        }

        public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

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

We could wrap our messy looking AddSingleton into a neat extension method. Also note the hardcoded string for the URL, we'll change this in part 3 to use service discovery.

In FactorialService's Bootstrapper:


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

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>());
            container.Register(_serviceProvider.GetService<ServiceClient<LogRequest, LogResponse>>());
        }
    }
}
Change Log statements

We replace the log statements in our factorial service with code to call the Logging microservice. Note I've left the old ILogger in there to make it easier to see changes, but it can be safely removed.

In FactorialService's Module.cs


using Microsoft.Extensions.Logging;
using Nancy;
using MicroservicesDemo.FactorialService.Models;
using Nancy.Validation;
using LoggingService.Models;
using System.Threading.Tasks;

namespace MicroservicesDemo.FactorialService
{
    public class Module : NancyModule
    {
        private ILogger<Module> _logger;
        private ServiceClient<LogRequest, LogResponse> _logServiceClient;

        public Module(ILoggerFactory loggerFactory, ServiceClient<LogRequest, LogResponse> logServiceClient)
        {
            _logger = loggerFactory.CreateLogger<Module>();
            _logServiceClient = logServiceClient;

            Get("/factorial/{number:int}", p => GetFactorial(new FactorialRequest { Number = p.number }));
        }

        async Task<FactorialResponse> GetFactorial(FactorialRequest request)
        {
            var validationResult = this.Validate(request);
            if (!validationResult.IsValid)
            {
                await _logServiceClient.Post(new LogRequest { Message = $"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) };
            await _logServiceClient.Post(new LogRequest { Message = $"Generated Factorial for {request.Number}: {result.Factorial}"});

            return result;
        }

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

Done! Testing time

All that's left is to test that it all works. Open 2 terminal windows and start both of our services with:

Run this in FactorialService and LoggingService folders, in each terminal:

dotnet restore
dotnet run

Now, test our FactorialService in Postman:

Success case

Failure case

LoggingService console output

There you go! 2 microservices in .net core with nancy; talking to each other over REST. In the last part we'll:

  • Create a nomad cluster in a vagrant VM
  • Configure service discovery with consul
  • Add Dockerfiles to our solution and deploy the lot to our nomad cluster.

The source code above is available in bitbucket

Quick update re Part 3: Whilst well overdue, it is in fact coming in the near future.