Cross-Platform Web Services in C# with NancyFX and TopShelf
I love C#. That shouldn’t be a surprise to anyone. I don’t, however, love the state of the community.
I’ve seen and talked to many people  that like the look of the language, but distrust Microsoft and don’t want to be tied down to Windows. Any mention of Mono is scoffed at. This fictional adversary asserts that Mono is wildly incompatibile with ‘real’ .NET and that it would be a futile exercise to try to write anything that’s not strictly Windows-focused in C#.
With that in mind, I wanted to be able to point people to something non-trivial-yet-easily-grokked that shows how little real ongoing effort it is to write cross-platform applications in C#. Smaller projects basically forsake Mono, while larger ones that may embrace it are just too difficult to pick apart to learn anything from.
So I started working on a cross-platfrom web service demo that makes use of a handful of technologies to make cross-platform development, testing, packaging, and distribution/deployment a breeze.
My hope is that people will begin stealing some of this code and these technologies/patterns to help make Mono more of a first-class citizen.
Note: I may break some of these sections off into individual blog posts, but for now I just wanted to do an overview of all important aspects.
I assume that most .NET developers work on Windows boxes (or if on Linux, you probably don’t need this tutorial).
First, ensure you have ruby installed. Along with the
Then, clone the repository. Run
bundle to ensure you have all necessary gems. Now run
rake make. That will restore all nuget dependencies, build the application, and bundle you an MSI. That’s it.
To try it out on Mono/Linux, you need to install VirtualBox and Vagrant. Then run
vagrant up from inside the directory. This will provision your virtual machine. Then, if you have
msysgit installed, you can run
vagrant ssh from a bash prompt in the directory and follow the same instructions as for windows from within that ssh session. If you don’t have a bash prompt handy, you can see this SO answer.
The web service itself is a NancyFX project. Nancy is a very versatile and lightweight web framework for .NET.
Nancy has a few different hosting options that control how the application is run. The two major options are AspNet hosting (which generally implies IIS or Apache/mod_mono) or self hosting (in which the application runs as a stand-alone application).
I chose self-hosting. Primarily to cut down on dependencies and ease of deployment. The
NancyApp class in
Program.cs is the Nancy host in this application.
You’ll see that the Nancy host in
Program.cs is a little bit different from the example in the Nancy docs.
That’s because I’m using Topshelf to provide an actual service framework. You see, having a console application is all well and good, but having to keep one of those running at all times can be a hassle - you end up having to use supervisor frameworks or some such to keep it under control. But Topshelf makes it a lot easier to just use your operating systems native service/daemon functionality, which is worlds better.
Topshelf services can still be run as console apps (in ‘interactive mode’) for troubleshooting and development, but it also bakes in several service control commands right into the executable. That way, you can configure/install/start/stop the service without ever having to touch installutil.
Unfortunately, most of that cool stuff is Windows only. The docs mention Mono compatibility, but it is only Mono on Windows (which I’ve, frankly, never understood).
Luckily, the community came through with a plugin for Topshelf with Linux support. Called, appropriately, Topshelf.Linux.
It is far from feature-complete, but it at least lets you start up your application. You don’t get any fancy service control commands, but I take care of that by bundling an Upstart job and a postinstall/preremove script in my Linux packages that take care of things on the service front. In this way, packaging and deployment is almost identical between Windows and Linux.
Like most projects these days, this demo is a polyglot affair. There is a fair amount of ruby involved in gluing the pieces of the development environment (and build system) together.
Pretty much everyone is on the vagrant train these days. But, just in case, it is an application that lets developers easily create and share disposable development environments.
I know many .NET developers are unfamiliar with Linux, and many Linux distros do not provide a modern version of Mono in their repos, so the Vagrantfile and puppet manifest can take care of that automatically. Just install VirtualBox and Vagrant then
vagrant up in the proejct directory and you are good to start using Mono on Linux.
Packaging things sucks. Making debs and rpms is way more work than it should be. Enter
fpm, a ruby gem that simplifies that process immensely.
I use fpm to bundle a deb and rpm of the application. Upon install, it copies the application to
/opt/TopShelfTest, and it registers and starts the Upstart job for the application.
Honorable mention to
fpm-cookery, a sort of a wrapper around fpm that makes it more scriptable (instead of long, ugly command lines you define it in a ruby DSL). I didn’t make use of that here for the sake of keeping things simple (for now).
The rake task that makes use of fpm is here.
Ugh. If there’s anything worse than Linux packaging, it’s Windows packaging. I mean, I suppose one could just point-and-click to a graphical installer of some kind, but the real Windows equivalent to debs and rpms are msis. And WiX is as good a way as any to make msis.
I’m not going to spend too much time on this, since I can’t pretend to know anything about WiX outside of what I continualy cargo cult. But basically, the installers directory has a few files that determine what files belong to the application and where they’re getting installed to. It also defines information about the package - version, name, unique ID (so upgrades can remove old versions).
Buried in all of that are also a couple of custom actions that install or remove the service on installing or removing the package.
You have too much time on your hands if you made it down this far. But my conclusion is that it is actually relatively trivial to treat Mono/Linux as a first-class citizen these days. There is a bit of an upfront cost, but very little in the way of on-going maintenance.
For a real project, one may wish to plug in mono.gendarme to ensure that the edge-cases of code that works in .NET but no Mono is detected early and automatically. I didn’t bother here (yet) since the actual code is so trivial.