C# Corner
Build Simple Web UIs with the Nancy Framework
Use the open source Nancy Web framework to expose a Web-based configuration UI for a Windows service.
If you've ever configured a router or used a modern-day network-enabled printer, you've probably noticed a lot of them expose a Web UI for configuration and administration. In this article, I'll show you how I used a similar technique to provide a configuration UI for a Windows service.
Windows Services and UIs
Windows services sit in the background and do work. They'll run whether someone is logged in to the machine or not. Depending on security, they have limited access to the file system. While Windows services can do lots of neat things, the one thing Windows services are not good at is displaying a UI. Because a Windows service can run when no one is logged in, it doesn't make sense for a Windows service to be able to display a UI. However, there are times when a Windows service needs to provide the user with information and, sometimes, allow the user to make changes to the service.
There are a number of possible solutions to this problem:
- Remoting: The Windows service can expose information via remoting and a standard Windows Forms UI can be created to talk to the service via remoting.
- Service Bus: The service can send and receive messages from a service bus such as Microsoft Message Queuing (MSMQ) or NServiceBus. A UI (Windows Forms, Windows Presentation Foundation or others) could be built to communicate with the service via the bus.
- Expose an HTTP endpoint so a browser -- or any other software that can make HTTP calls -- can interact with the service.
While exposing an HTTP endpoint might sound like a daunting task (handling incoming connections, threading, resources and so on), it can be made much easier with the Nancy Web framework.
Nancy Framework
According to the github page, the Nancy framework (NancyFx) provides the "super-duper-happy-path" -- a "lightweight, low-ceremony framework for building HTTP-based services." This is exactly what a Windows service needs for exposing a simple configuration UI.
Normally, you'd plug NancyFx into ASP.NET and use it instead of Web Forms or Model-View-Controller (MVC). But NancyFx supports other hosts such as Windows Communication Foundation (WCF), and even supports the concept of "self-hosting." This is the option I use when I need a Windows service to expose a simple configuration UI.
Getting Started with NancyFx
While my plan is to add a Web UI to a Windows service, I want an easy way to debug the Web interface during development. If I started out by plugging this Web code directly into the service, I'd need to stop the service every time I needed to recompile a change -- and then start it up again for testing.
To make the development process easier, I place all my Web UI code into a standard .NET Class library. I then use a simple Console application to host NancyFx for debugging and testing. When I'm ready, my Windows service just needs to add a reference to the Web UI Class library and I'm ready to deploy.
After using NuGet to add a reference to NancyFx as well as Nancy.Hosting.Self, I can get started with just a few lines of code:
var nancyHost = new Nancy.Hosting.Self.NancyHost(new Uri("http://localhost:9664"));
nancyHost.Start();
Console.WriteLine("Web server running...");
Console.ReadLine();
nancyHost.Stop();
That's all it takes. Well, except that I haven't defined any behavior for my application! NancyFx defines behaviors inside a module. Without a module defined to handle a standard HTTP GET request to http://localhost:9664, nothing is going to happen.
NancyFx Modules
A NancyFx module defines what your application should do based on the request that comes in. Here's the simplest NancyFx module:
public class MainModule : NancyModule
{
public MainModule()
{
Get["/"] = p => View["main.html"];
}
}
Note that there's no need to "register" a module with NancyFx. When starting up, NancyFx automatically finds modules by looking for all classes in the app domain that inherit from NancyModule.
The previous module simply returns an HTML file when an HTTP GET request is made at the root level. By default, NancyFx ships with the "SuperSimpleViewEngine," but other view engines (like Razor) can be plugged in as well. The SuperSimpleViewEngine provides simple output of properties (from a viewmodel), master pages, partial pages, HTML encoding, iterators and conditionals -- everything you need for a simple Web UI.
Delivering the Web UI
Because I want to deliver this Web-based UI solution via a Windows service, I don't want to have to deliver a whole bunch of files -- HTML, CSS, images and so on. Instead, I'd like to simply reference a DLL and be done with it.
Luckily, NancyFx has that route (if you'll pardon the pun) covered. I can place all of my Web files inside my assembly as an embedded resource. I can then tell NancyFx to use a special view locator instead of the default one (which looks for views in the local file system).
NancyFx configuration is done via a bootstrapper. The bootstrapper is used when NancyFx starts up, and by providing a custom boostrapper I can change how NancyFx works. Listing 1 shows how simple it is to use an embedded resource for views.
Listing 1. NancyFx configuration.
public class CustomBootstrapper : DefaultNancyBootstrapper
{
protected override NancyInternalConfiguration InternalConfiguration
{
get
{
return NancyInternalConfiguration.WithOverrides(OnConfigurationBuilder);
}
}
void OnConfigurationBuilder(NancyInternalConfiguration x)
{
x.ViewLocationProvider = typeof(ResourceViewLocationProvider);
}
protected override void ConfigureApplicationContainer(TinyIoCContainer container)
{
base.ConfigureApplicationContainer(container);
ResourceViewLocationProvider.RootNamespaces.Add(
Assembly.GetAssembly(typeof(MainModule)), "VSMDemo.Web.Views");
}
}
The first override in Listing 1 provides a delegate to my code that will change the NancyFx configuration. That delegate simply tells NancyFx to use the ResourceViewLocationProvider -- a built-in class that tells NancyFx to look inside an assembly's embedded resources for views.
Finally, I override ConfigureApplicationContainer and give the ResourceViewLocationProvider a reference to the assembly that contains the views along with the namespace within that assembly where the views can be found. In the code in Listing 1, I tell NancyFx to use the assembly where my MainModule is defined. Inside that assembly, I have a folder called Views inside the VSDemo.Web namespace that contains all of my resources (including HTML, CSS and images).
The key thing to remember when doing this is to set your "Build Action" for each of those resources to "Embedded Resource"! When I first started using this technique with NancyFx, I was always forgetting to set the build action as an embedded resource, and NancyFx would complain at runtime that it couldn't find the resource.
While on the subject of embedded resources, this is a good time to cover static conventions. As I mentioned earlier, all requests are handled by modules. You don't want to have to create a special module to handle requests for static content such as JPG or CSS files. Therefore, NancyFx provides a way to define conventions for how static files are handled.
There are built-in conventions for handling simple directory-based and file-based static conventions. Because I'm placing everything inside the assembly as an embedded resource, those won't work. However, as with everything else in NancyFx, you can define your own conventions for static files. If you look at the sample code, you'll see the CustomBootstrapper also overrides ConfigureConventions and supplies a method called AddStaticResourcePath. This is a method I created that allows me to map incoming requests for a specific route prefix (such as "/content/*") to a specific namespace folder within the assembly and expose the embedded resources within that folder.
Now that my CustomBootstrapper is complete, I have to modify my startup code to use my bootstrapper instead of the default one:
var nancyHost = new Nancy.Hosting.Self.NancyHost(
new Uri("http://localhost:9664"), new CustomBootstrapper());
Doing Something Useful
Up until now, I've showed lot of basic setup and a simple way to view a page. The point of this article is to allow someone with a Web browser the ability to connect to my Windows service and make some changes.
I'll expose a couple of pieces of information that my Windows service uses:
- The name of a server.
- An update interval.
I've created a simple class to maintain my configuration information. For the sake of brevity, I won't go in to how this data is stored or retrieved. Also, note the [Serializable] attribute on the class, which will come into play later:
[Serializable]
public class ConfigInfo
{
public int UpdateInterval { get; set; }
public string ServerName { get; set; }
}
Now I need some way to display this information in a Web UI and allow the user to change it. Behavior in NancyFx is defined in modules, so I'll create a new module for handling configuration.
The constructor for a NancyFx module can take a URL prefix. This can be used to segregate functionality by URL in a REST-ful manner. Because I'm doing a configuration module, I decide I want the prefix to be "/config":
public class ConfigModule : NancyModule
{
public ConfigModule()
: base("/config")
{
Get["/"] = x =>
{
...
};
Post["/update"] = parameters =>
{
...
};
}
}
My GET handler defines no additional path information. With my module prefix as "/config," any GET request for "/config/" will go to my GET handler. Likewise, any data that is POSTed to "/config/update" will be processed by my POST handler. At this point, I hope you'll see that my intention is to have my GET handler expose a standard HTML <form> that will post data to the "/config/update" URL.
NancyFx allows a model to be passed in to the view engine when rendering views. This allows an MVC-like separation of model data and the behavior of the application. I'll use that to push my configuration information into the view for display in the UI. Here's my GET handler:
Get["/"] = x =>
{
var model = new ConfigStatusModel
{
Config = new ConfigInfo()
};
return View["index.html", model];
};
With the Config module having a URL prefix of "/config," NancyFx will look for the module's views inside the "config" folder where my views are stored. At this point, basic HTML can be used to display a Web UI. Here's a reduced snippet from my "/config/index.html" file:
<form action="/config/update" method="POST">
<div class=" caption">Update Interval</div>
<div><input type="text" name="updateInterval" value="@Model.Config.UpdateInterval" /></div>
<div class=" caption">Server Name</div>
<div><input type="text" name="serverName" value="@Model.Config.ServerName" /></div>
<div><input type="submit" value="Save"/></div>
</form>
Notice how the SuperSimpleViewEngine uses "@Model" to pull out model properties -- very similar to Razor syntax.
Binding Data
When it comes to receiving data from an HTTP request, NancyFx comes with a number of binders that can bind data from query strings, route parameters, form data and more. For my application, a simple bind from form data is all I need.
If you go back and look at the previous HTML <form>, you'll notice that the names of the <input> elements match the property names of my ConfigInfo class. By having matching property names, NancyFx can create and bind the data into a ConfigInfo class with a single line of code:
var config = this.Bind<ConfigInfo>();
At this point, I can take the updated information and pass it on to my Windows service. I can also save it in some permanent storage mechanism so it can be retrieved again the next time the service restarts.
Session Management
NancyFx comes with a built-in, cookie-based session manager. By making a small change in my CustomBootstrapper, I can enable a session to be used for storing temporary information in my Web UI:
public class CustomBootstrapper : DefaultNancyBootstrapper
{
...
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
CookieBasedSessions.Enable(pipelines);
}
...
}
With sessions enabled, I can store information that can persist between Web requests. I use this mechanism to display a simple status message after a configuration update, as well as to maintain a copy of the ConfigInfo in memory.
Note than, in NancyFx, the entire contents of the session are serialized out to the cookie. That's why my ConfigInfo had the [Serializable] attribute -- it allows me to easily save an instance of the ConfigInfo between requests. The session data is encrypted, but with Nancy being open source, it wouldn't be very difficult to reverse-engineer the session data by examining the HTTP cookie data being transmitted. Therefore, never place secure information such as passwords or Social Security numbers inside a cookie-based session!
Here's an updated version of my POST handler. This version maintains a cached copy of the ConfigInfo as well as a status message inside the session:
Post["/update"] = parameters =>
{
var config = this.Bind<ConfigInfo>();
// Save information
Session[MessageKey] = "Configuration Updated";
Session[ConfigInfoKey] = config;
return Response.AsRedirect("/config");
};
You should also see how I used a special Response method called AsRedirect. This is a standard redirect to a new URL. In this case, I'm going back to my Config module's GET handler. Here's my updated GET handler that shows how I pull out the status message as well as the ConfigInfo (if it exists):
Get["/"] = x =>
{
var message = Session[MessageKey] != null ? Session[MessageKey].ToString()
: string.Empty;
var model = new ConfigStatusModel
{
Message = message,
Config = Session[ConfigInfoKey] == null ? new ConfigInfo()
: (ConfigInfo)Session[ConfigInfoKey],
};
Session[MessageKey] = string.Empty;
return View["index.html", model];
};
Demo Code
NancyFx is a great framework that makes it quick and easy to add an HTTP interface to any application. The sample project included with this article contains a complete implementation of the configuration service I described here. There's a number of techniques used that I didn't have time to cover in the article, including master pages and conditional processing. If you have other questions on how NancyFx could be used in your application, please don't hesitate to contact me.
About the Author
Patrick Steele is a senior .NET developer with Billhighway in Troy, Mich. A recognized expert on the Microsoft .NET Framework, he’s a former Microsoft MVP award winner and a presenter at conferences and user group meetings.