I love TopShelf. I first encountered it when I was looking at MassTransit. Both MassTransit and TopShelf originated from the same developers (Chris Patterson and Dru Sellers). The NServiceBus host is also built with TopShelf.
What TopShelf allows you to do is to write a service as a console application – which is great for development and testing – but to deploy it later as a Windows service. You do this by simply by passing in some command line parameters to the console application. Fantastic.
“Topshelf is a framework for hosting services written using the .NET framework. The creation of services is simplified, allowing developers to create a simple console application that can be installed as a service using Topshelf. The reason for this is simple: It is far easier to debug a console application than a service. And once the application is tested and ready for production, Topshelf makes it easy to install the application as a service.” – TopShelf Overview
I have been encouraged to look at Nancy by a colleague (thanks Matt!) who has used it on a number of projects. It’s been on my radar for a while but somehow I’ve never got round to looking at it. As it happens for a small side project I’m working on I need to expose some JSON data from a service. How opportune!
“Nancy is a lightweight, low-ceremony, framework for building HTTP based services on .Net and Mono. The goal of the framework is to stay out of the way as much as possible and provide a super-duper-happy-path to all interactions.” – Introduction (Nancy documentation)
Visual Studio and NuGet packages
Firstly I created a Visual Studio solution and a console application project. Once that was done I installed a number of NuGet packages:
- TopShelf
- Ninject (for dependency injection)
- Ninject.Web.Common (so we can scope Ninject bindings to an HTTP request)
- TopShelf.Ninject (which provides extensions to TopShelf so it can painlessly use Ninject)
- Nancy
- Nancy.Hosting.Self (so Nancy can be hosted inside the service)
I also added a few other packages for logging, configuration and JSON serialisation but they aren’t core to the task at hand so I’ll ignore them for now.
Defining the service
The first thing I did was to define a service contract - an interface - for the service that would be hosted by TopShelf.namespace Andy.French.Region.Growing.Venue.Service { /// <summary> /// Classes implementing this interface provide methods for getting /// or manipulating venue data. /// </summary> public interface IVenueService { /// <summary> /// Starts the service. /// </summary> void Start(); /// <summary> /// Stops the service. /// </summary> void Stop(); } }The Start() and Stop() methods align with the TopShelf service configurator that allows you to assign actions when a service is started and stopped. More on that later.
The implementation of the interface would be the service to be hosted by TopShelf and which would spin up the Nancy endpoint. Let’s see what that looked like.
namespace Andy.French.Region.Growing.Venue.Service { using System; using Andy.French.Configuration.Service; using Andy.French.Logging; using global::Nancy.Hosting.Self; /// <summary> /// The venue service. /// </summary> public class VenueService : IVenueService { /// <summary>The logger factory.</summary> private readonly ILoggerFactory loggerFactory; /// <summary>The logger.</summary> private ILogger logger; /// <summary>The nancy host.</summary> private NancyHost nancyHost; /// <summary>The host URL.</summary> private string hostUrl; /// <summary>The JSON file path.</summary> private string jsonFilePath; /// <summary> /// Initialises a new instance of the <see cref="VenueService"/> class. /// </summary> /// <param name="loggerFactory">The logger factory.</param> /// <param name="configurationService">The configuration service.</param> public VenueService(ILoggerFactory loggerFactory, IConfigurationService configurationService) { this.loggerFactory = loggerFactory; this.hostUrl = configurationService.GetString("host.url"); } /// <summary> /// Gets the logger. /// </summary> /// <value>The logger.</value> public ILogger Logger { get { if (this.logger == null) { this.logger = this.loggerFactory.CreateInstance(typeof(VenueService)); } return this.logger; } } /// <summary> /// Starts the service. In this case it starts the Nancy endpoint. /// </summary> public void Start() { this.Logger.Information("The venue service is starting."); this.nancyHost = new NancyHost(new Uri(this.hostUrl)); this.nancyHost.Start(); } /// <summary> /// Stops the service. In this case it stops and disposes of the Nancy host. /// </summary> public void Stop() { this.Logger.Information("The venue service is stopping."); this.nancyHost.Stop(); this.nancyHost.Dispose(); } } }The interesting stuff appears around line 63 in the Start() method. Here I create a Nancy host and start it. The URL passed in (the hostUrl) is loaded from configuration but its actually something like http://localhost:1234. In the Stop() method I stop the Nancy host and dispose of it.
Basically, this is all the code it took to get the Nancy host up-and-running but there was a little bit of plumbing required to set up routes and handlers.
The Nancy module
At this point I had a Nancy host but that was only part of the story. I also needed to define a route and a handler for an HTTP verb (e.g. GET). It’s actually very easy to do this in Nancy and is accomplished using a Nancy module.namespace Andy.French.Region.Growing.Venue.Service.Nancy { using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Andy.French.Configuration.Service; using Andy.French.Region.Growing.Domain; using global::Nancy; using Newtonsoft.Json; /// <summary> /// A Nancy module for the venues service. /// </summary> public class VenuesModule : NancyModule { /// <summary>The venues.</summary> private List<Venue> venues; /// <summary>The JSON file path.</summary> private string jsonFilePath; /// <summary> /// Initialises a new instance of the <see cref="VenuesModule"/> class. /// </summary> /// <param name="configurationService">The configuration service.</param> public VenuesModule(IConfigurationService configurationService) { this.jsonFilePath = configurationService.GetString("json.file.path"); this.Get["/venues", true] = async (x, ct) => { await this.LoadVenues(); return this.Response.AsJson(this.venues); }; } /// <summary> /// Loads the venues. /// </summary> /// <returns>The task to load the venues.</returns> private async Task LoadVenues() { await Task.Factory.StartNew( () => { this.venues = JsonConvert.DeserializeObject<List<Domain.Venue>>(File.ReadAllText(this.jsonFilePath)); }); } } }
You can see that I created a class that extended NancyModule and defined a route and a handler in the constructor (line 33). This basically says that we are expecting GET requests to arrive at ‘/venues’ and defines what we want to do with them. In my naïve service I simply load a JSON file and return the contents in the response as JSON (line 36).
Dependency injection
You’ll notice that the Nancy module and the Venue service both have dependencies passed in on their constructors (e.g. a logger factory or a configuration service). To get these resolved I needed to plug in Ninject. That was done with a simple Ninject module:
namespace Andy.French.Region.Growing.Venue.Service.Ninject { using Andy.French.Configuration.Service; using Andy.French.Logging; using Andy.French.Logging.Log4Net; using global::Ninject.Modules; using global::Ninject.Web.Common; /// <summary> /// A <c>Ninject</c> module. /// </summary> public class VenueServiceModule : NinjectModule { /// <summary> /// Loads the module into the kernel. /// </summary> public override void Load() { this.Bind<ILoggerFactory>().To<LoggerFactory>().InRequestScope(); this.Bind<IVenueService>().To<VenueService>().InRequestScope(); this.Bind<IConfigurationService>().To<AppSettingsConfigurationService>().InRequestScope(); } } }
That was all there was to that.
Wiring up TopShelf
All that remained was to bring everything together with TopShelf. That was done in the main program method:
namespace Andy.French.Region.Growing.Venue.Service { using Andy.French.Region.Growing.Venue.Service.Ninject; using Topshelf; using Topshelf.Ninject; /// <summary> /// The main application program. /// </summary> public class Program { /// <summary> /// Defines the entry point of the application. /// </summary> public static void Main() { HostFactory.Run(hostConfigurator => { hostConfigurator.UseNinject(new VenueServiceModule()); hostConfigurator.Service<IVenueService>(serviceConfigurator => { serviceConfigurator.ConstructUsingNinject(); serviceConfigurator.WhenStarted(service => service.Start()); serviceConfigurator.WhenStopped(service => service.Stop()); }); hostConfigurator.RunAsLocalSystem(); hostConfigurator.SetDescription("The venue service provides venue data."); hostConfigurator.SetDisplayName("Region Growing Venue Service"); hostConfigurator.SetServiceName("VenueService"); }); } } }
Trying it out
Aside from some log4Net configuration that was pretty much it. Hitting F5 in Visual Studio resulted in the console application being launched and the service starting up. You can see the INFO message indicating the Start() method of the service has been called and therefore the Nancy host has been started.Using the Fiddler composer I fired a request at the service.
This resulted in JSON being returned (albeit a small incomplete example at this stage). The content type was correctly set to application/json.
Hitting Ctrl-C caused TopShelf to call the Stop() method of the service which stops and disposes of the Nancy host.
That’s it! All too easy.