• blog
  • portfolio
  • about
  • contact
 
Get Some Web In Your Topshelf With NancyFx
I recently wrote a windows service, and I wanted to have a web dashboard. Luckily, NancyFx makes that really easy to do.

Topshelf makes windows services a breeze. But, I'm a web oriented guy and I like using a browser to get an inside look at what my service is doing. For that I chose to use NancyFx (mostly as a learning experience and excuse to use something new).

UPDATE: Andreas had some great suggestions to improve my example, which I have updated the post with.

UPDATE: I have made the example code for this post available on bitbucket.

NuGet Packages

For this set up, I used the following NuGet packages:

Configuring Topshelf

The service we'll be using polls for new tweets on a specific search term. Here's some code to set up the service:

private static void StartHosting()
{
    var stopwatch = new Stopwatch();
    stopwatch.Start();
    using (Container)
    {
        var host = HostFactory.New(config =>
        {
            config.EnableDashboard();
            config.Service<PghDotNetTwitterWatcherService>(s =>
            {
                s.SetServiceName("NancyTopshelfExample.PghDotNetTwitterWatcherService");
                s.ConstructUsing(c => Container.Get<PghDotNetTwitterWatcherService>());
                s.WhenStarted(n => StartService(n, stopwatch));
                s.WhenStopped(n => StopService(n, stopwatch));
            });
            config.RunAsLocalSystem();
            config.SetDescription("A service for watching twitter for new tweets about pghdotnet.");
            config.SetDisplayName("NancyTopshelfExample.PghDotNetTwitterWatcherService");
            config.SetInstanceName("NancyTopshelfExample.PghDotNetTwitterWatcherService");
            config.SetServiceName("NancyTopshelfExample.PghDotNetTwitterWatcherService");
        });
        host.Run();
    }
}

This really isn't all that interesting, except for the fact that Nancy will being running at the same time as this service. Nancy will be able to talk to the service on each page request to find out how things are going, and then return them in a view to the user's browser.

Example Service

The service doesn't do much. Once every 5 seconds, it makes an http request to get tweets. It adds new tweets to the WorkLog, and increments some counters to keep track of successes and failures. The Nancy module will be using these counters.

public class PghDotNetTwitterWatcherService : IService
{
    private readonly Dictionary<string, string> _tweets;
    private bool _stopped;
    private Task _task;

    public List<string> WorkLog { get; private set; }
    public long TotalRequestsMade { get; private set; }
    public long TotalFailures { get; private set; }

    public PghDotNetTwitterWatcherService()
    {
        _tweets = new Dictionary<string, string>();
        WorkLog = new List<string>();
        Tick();
    }

    private void Tick()
    {
        var sb = new StringBuilder();

        try
        {
            GetTweets("pghdotnet");
            TotalRequestsMade++;
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
            WorkLog.Add(exception.Message);
            TotalFailures++;
        }

        if (sb.Length > 0)
        {
            AddToWorkLog(sb);
        }

        Thread.Sleep(5000);

        if (!_stopped)
            _task = Task.Factory.StartNew(Tick);
    }

    private void AddToWorkLog(StringBuilder stringBuilder)
    {
        WorkLog.Add(stringBuilder.ToString());
        var last5 = WorkLog.AsQueryable().Reverse().Take(5).ToArray();
        WorkLog.RemoveAll(s => !last5.Contains(s));
    }

    protected void GetTweets(string term)
    {
        // omitted for brevity, but go get tweets from twitter
        // check for new ones in the results
        // write new tweets to the WorkLog
    }

    public void Start()
    {
        Console.WriteLine("Starting PghDotNetTwitterWatcherService...");
    }

    public void Stop()
    {
        _stopped = true;
        Console.WriteLine("Stopping PghDotNetTwitterWatcherService...");
        _task.Wait();
    }
}

I really only want one instance of this service running, so I configured the PghDotNetTwitterWatcherService class to be a singleton. Since I'm using Ninject to resolve the service with Topshelf, I'll need to make use of the Ninject bootstrapper package for Nancy. More on that in a bit.

Running Nancy

I chose to use WCF as my Nancy host, and Razor as my view engine. I wrapped it in an IWebComponent interface with simple Start and Stop methods:

public class NancyComponent : IWebComponent
{
    private WebServiceHost _nancyHost;

    public Uri BaseUri { get; private set; }

    public NancyComponent(Uri baseUri)
    {
        BaseUri = baseUri;
    }

    public void Start()
    {
        var service = new NancyWcfGenericService();
        _nancyHost = new WebServiceHost(service, BaseUri);
        _nancyHost.AddServiceEndpoint(typeof(NancyWcfGenericService), new WebHttpBinding(), "");

        // this can fail if there isn't a urlacl for the desired port
        // if it fails, open a cmd with "run as administrator" and run:
        // netsh http add urlacl url=http://+:8585/ user=\Everyone
        _nancyHost.Open();
        Console.WriteLine("Nancy: Listening to requests on {0}", BaseUri);
    }

    public void Stop()
    {
        _nancyHost.Close();
    }
}

Starting Nancy up under WCF is easy, a staggering 4 lines of code. Not much of a big deal. Now that there's hosting, we have to have a NancyModule to host.

public class MainModule : NancyModule
{
    private readonly PghDotNetTwitterWatcherService _service;

    public MainModule(PghDotNetTwitterWatcherService service)
    {
        _service = service;

        Get["/"] = parameters =>
        {
            if (!Request.Headers.Host.StartsWith("localhost:"))
                return 404;

            var indexViewModel = new IndexViewModel { CurrentDateTime = DateTime.Now, WorkLog = _service.WorkLog };

            return View["index", indexViewModel];
        };

        Get["/style/{file}"] = p =>
        {
            return Response.AsCss("content/themes/base/" + p.file as String);
        };

        Get["/status"] = parameters =>
        {
            return Response.AsJson(new { _service.TotalCommitsProcessed, _service.TotalFailures });
        };
    }
}

This module goes out to the IoC container, and pulls back an instance of the service. There's a few ways to do this, the choice is yours. Essentially, I only want the local machine to have access to this status page. To do this I look at the host header to see if it starts with localhost. If not, return a 404. Otherwise, a view model is prepared and returned to the client. You could also use an authentication provider to limit this access, but this was simple for the example.

You'll also need to wire up some views. After struggling with this for a short while, I settled on using this:

public class AssemblyLocationRootPathProvider : IRootPathProvider
{
    public string GetRootPath()
    {
        return new FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName;
    }
}

With this approach, you'll have to set views to copy during your build. Otherwise, you could take advantage of Nancy's support for embedded views.

The module above has a dependency, but won't know how to get the singleton instance that Ninject is holding onto. In this case, we'll make great use of the Ninject bootstrapper. As Andreas indicated, you just need to derive a custom bootstrapper from the NinjectNancyBootstrapper.

public class NinjectCustomBootstrapper : NinjectNancyBootstrapper
{
    protected override Ninject.IKernel GetApplicationContainer()
    {
        return Program.Container;
    }
}

Extras

As you may have noticed, I also had an action mapped for /status in my Nancy module. It returns a JSON object with some counter values. Endpoints like this one are useful for allowing service health monitoring from a widget or similar.

In the service I wrote using this approach, I was interacting with a database which made Dapper a great addition. What's more, it would be great to work in MiniProfiler to get some extra insight on performance (it would probably have to be adapted to work with Nancy). Maybe someday.

If anyone is interested in this code, I'll gladly add it to my example code repository.

Posted on 09/09/2011 10:16:36

johncoder

I'm a C# developer at NBC News Digital. I love my job.

The content and opinions of this blog are my own.

extras