Introduction
A Windows Service is a long running process that sits in the background, executing when needed. Services don't interact with the desktop, and this raises many issues, including the problem of controlling the service to a finer level than simply clicking "start service" "stop service" in the services control panel. This article describes using the NancyFX framework to provide a web browser interface to your Windows Service, giving you much more control on what's going on and managing the internals for the service itself.
(The ever so lovely image on my Windows Form is taken from the NancyFX website. Yes, using it can be as simple as it says!)
The Basics
To get started, and to save the bother of spinning up a service immediately, we will start off the article using a standard Windows Form project, then convert it towards the end. I am calling this starter project "NancyWinForm".
Once you have your Windows Form project created, the first thing to do is open NuGet and add the following packages:
Nancy, Nancy.ViewEngines.Razor, Nancy.Hosting.Self. Nancy is the core package, Hosting.Self is required for self hosting in a DLL, EXE etc. without the need to sit on top of another host, and we are using the Razor engine on this occasion to parse the data in our HTML view pages.
In order to get Nancy up and running, there are a few basics that we need to put in place. Having linked in the NuGet packages, the next thing is to add some, using clauses to the top of the main form.
- using Nancy;
- using Nancy.Hosting.Self;
- using System.Net.Sockets;
The next thing we need to do is to set up a form level object to run Nancy
.
- namespace NancyWinForm{
- public partial class Main : Form
- {
- NancyHost host;
- public Main()
- {
- InitializeComponent();
- }
- }
- }
Next, we initialize the Nancy
host object.
- public Main() {
- InitializeComponent();
- string URL = "http://localhost:8080";
- host = new NancyHost(new Uri(URL));
- host.Start();
- }
We start Nancy
with a constructor that gives it a simple domain "localhost
" and port "8080
". You should pick something that is not already running on your system. For example, if you already have IIS or another web service running, then port 80 is most likely gone. We will discuss multiple binding and ports later in the article. Once we have told dear Nancy what to bind to, we tell her to "start" .... and if we open a browser to the URL we gave her, here's what we see...
The Nancy authors have an ethos they call the “super-duper-happy-path” ... I think we can safely say we're happy.
OK! The next thing is to take that little green monster out of Nancy's way and get in there ourselves..
The next step is to create a new class of type "NancyModule", and give it a default path.
- public class MainMod : NancyModule
-
- public MainMod()
- {
- Get["/"] = x =>
- {
- return "Wee hoo! - no more little green monster...";
- };
-
- }
Here's a small gotcha - in a Windows Form, if you place this code above some that requires the designer (for example, a picture container with a picture of young nance), the compiler will complain that it wants to be first in line .... to fix this, just put the module class at the end of your form file.
A "NancyModule" can be added anywhere in the application and the framework will find it. It does this by iterating through the appdomain on startup looking for any NancyModules and hooking into them when they are located. Inside a NancyModule, we add "routes" or "paths". You can see that in this case, I added a root path "/". To start the ball rolling, I am simply returning a simple string to the browser. When we run, the output is as expected.
Ok, text output is great but not terribly useful. The next thing to do is to add a HTML file we can work with. For this project, I have added a folder called "views" and in there, created a new HTML file "skirts.html" (get with the program, the theme must continue!!).
Let's add some simple content to the HTML file, and adjust the module get route handler to return the file.
- <!DOCTYPE html>
- <html lang="en" xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta charset="utf-8" />
- <title>Nancy's skirts</title>
- </head>
- <body>
-
- Heylow der...
- </body>
- </html>
- public MainMod()
- {
- Get["/"] = x =>
- {
- return View["views/skirts.html"];
- };
-
- }
Hit F5 to run, stand well back...
Oh dear, the little green monster is back ...what's happened is that in Nancy, we need to tell it a bit more than that we would in IIS. In this case, we need to tell the project to include the file in its output folder.
Now, when we run, the monster is gone ... Perfect !
Having seen how to handle a GET, we will now handle POST. For this example, let's create two further HTML files "nice.html" and "very.html" - don't forget to set them to copy to output if newer. In our original HTML file, we will add a form to post...
- <form method="post" action="NiceNancy">
- How nice is nancy? - please select...
-
- <select name="HowNice">
- <option value="1">Quite nice</option>
- <option value="2">Very!</option>
- </select>
-
- <button type="submit">Submit</button>
-
- </form>
The idea is to send back one page if the user selects "quite nice", another if they select "very". Here is the POST
code added to the Nancy
module.
- Post["/NiceNancy"] = y =>
- {
- if (Request.Form["HowNice"].HasValue)
- {
- if (Request.Form["HowNice"] == "1")
- { return View["views/nice.html"]; }
- else if (Request.Form["HowNice"] == "2")
- { return View["views/very.html"]; }
- else return View["views/skirts.html"];
- };
- else return View["views/skirts.html"];
- }
Note that like MVC/ASP, the form data is accessible via the "Request" object.
Giving Nancy the Boot....
Nancy is light, and unlike something heavy like IIS, it does not have as inbuilt facilities that you might expect. One of the things it needs guidance on is things like images and paths. Nancy will automatically serve up files (JS, CSS, Images..), but needs to be told where these things are stored, relative to itself. This is handled using the "Bootstrapper" class.
I am creating a new unit to store this code, "Bootstrapper.cs" - you can place it where you wish. To this file, I am adding the following use clauses:
- using Nancy;
- using Nancy.Session;
- using Nancy.Bootstrapper;
- using Nancy.Conventions;
- using System.Web.Routing;
- using Nancy.TinyIoc;
I also add a project reference to "system.web".
We need to derive the new class from "DefaultNancyBootstrapper" before we can start doing anything useful. We then add an override method "Configure conventions" to hook in the location of folders that will store images, etc.
- public class Bootstrapper : DefaultNancyBootstrapper
- {
- protected override void ConfigureConventions(NancyConventions nancyConventions)
- {
- base.ConfigureConventions(nancyConventions);
- nancyConventions.StaticContentsConventions.Clear();
- nancyConventions.StaticContentsConventions.Add
- (StaticContentConventionBuilder.AddDirectory("css", "/content/css"));
- nancyConventions.StaticContentsConventions.Add
- (StaticContentConventionBuilder.AddDirectory("js", "/content/js"));
- nancyConventions.StaticContentsConventions.Add
- (StaticContentConventionBuilder.AddDirectory("images", "/content/img"));
- nancyConventions.StaticContentsConventions.Add
- (StaticContentConventionBuilder.AddDirectory("fonts", "/content/fonts"));
- }
-
- }
On startup, Nancy is now aware that we have (for example) a local path "/content/img" that is referred to by the virtual path "images". Let's test it from our main HTML file by dropping in an image and referring to it.
- <form method="post" action="NiceNancy">
- <img src="images/SoWhat.jpg" />
- <br />
Yea, works!
To tell Nancy where our images and other resources were stored, we customised things using the "bootstrapper" class. Bootstrapper is useful for all kinds of things as it allows us to hook into the NancyIoC and inject supporting objects into our application. One of the things I wanted to achieve was being able to have an object that would effectively store global variables. To do this, I created a class to load/save data using an XML file, and allow access to the data as needed.
The following is the simple "config-manager" class which I stored in a separate file "shared" (note the addition of the XML using clauses as I am using serialisation to save/load the data quickly).
- using System.Xml;
- using System.Xml.Serialization;
- using System.IO;
-
- namespace NancyWinForm
- {
- public class ConfigInfo
- {
- public String TempFolder { get; set; }
- public String Username { get; set; }
- public String DateFormat { get; set; }
- }
-
- public class PracticeConfigManager
- {
- public ConfigInfo config;
- string _XMLFile = AppDomain.CurrentDomain.BaseDirectory + "MyConfig.xml";
-
- public void LoadConfig()
- {
- if (config == null)
- {
- config = new ConfigInfo();
- }
-
- if (!File.Exists(_XMLFile))
- {
- config.DateFormat = "dd/mmm/yyyy";
- SaveConfig();
- }
-
- XmlSerializer deserializer = new XmlSerializer(typeof(ConfigInfo));
- TextReader reader = new StreamReader(_XMLFile);
- object obj = deserializer.Deserialize(reader);
- config = (ConfigInfo)obj;
- reader.Close();
- }
-
- public void SaveConfig()
- {
- if (config != null)
- {
- XmlSerializer serializer = new XmlSerializer(typeof(ConfigInfo));
- using (TextWriter writer = new StreamWriter(_XMLFile))
- {
- serializer.Serialize(writer, config);
- }
- }
- }
- }
- }
Having setup the class to save/load the config data, we now need to inject this into Nancy
. We do this using the bootstrapper. In our bootstrap class, we override the method "Application Startup", and in here, register the ConfigManager
class in the IoC container, as follows.
- public class Bootstrapper : DefaultNancyBootstrapper
- {
-
- protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
- {
- base.ApplicationStartup(container, pipelines);
- ConfigManager mgr;
- mgr = new ConfigManager();
- mgr.LoadConfig();
- container.Register<configmanager>(mgr);
- }</configmanager>
Having registered it, we now need to be able to access it whenever we get a GET/POST HTTP request. We do this by changing the signature of our main NancyModule.
From,
To,
- public MainMod(ConfigManager mgr)
As our ConfigManager
is created and initialised at Nancy
startup, we can therefore use it directly, now in the NancyModule.
- Get["/config"] = x =>
- {
- return mgr.config.DateFormat;
- };
and the result...
OK, all looking good. The bootstrapper class is an extremely useful part of NancyFX and a place I find myself dipping into often.
When developing for the web, I prefer to steer clear these days of WebForms and use MVC almost exclusively - I like the clean separation of concerns and the ability to be "close to the metal" as it were. Nancy allows us to use Razor syntax in a familiar way to MVC. Let's say, we had some values persisted that we wished to use on the web-page. We work with Razor in Nancy the same way we do in MVC...
1. Create a page "config.html" and add set properties to "add if newer"
- <body>
-
- Model test page<br />
- <hr>
- <form method="post" action="SaveConfig">
-
- <input id="dateFormat" value="@Model.DateFormat" />
- <input id="username" value="@Model.Username" />
- <input id="tempFolder" value="@Model.TempFolder" />
-
- <button type="submit">Submit</button>
-
- </form>
- </body>
2. Add new controller code, passing in the ConfigManager
as the model:
- Get["/config"] = x =>
- {
-
- return View["views/config.html",mgr.config];
- };
So we can see that the model data (the date format) has been rendered correctly into the HTML.
Binding to the IP Stack
Sometimes, when setting up a HTTP server to listen for traffic, you want to specify a particular IP address, but sometimes you want to bind to all available IPs, in other words - a wildcard binding. I found some useful code here that helps this happen. You send the method the port to bind to, it sends back an array of available bindings.
- private Uri[] GetUriParams(int port)
- {
- var uriParams = new List<uri>();
- string hostName = Dns.GetHostName();
-
-
- string hostNameUri = string.Format("http://{0}:{1}", Dns.GetHostName(), port);
- uriParams.Add(new Uri(hostNameUri));
-
-
- var hostEntry = Dns.GetHostEntry(hostName);
- foreach (var ipAddress in hostEntry.AddressList)
- {
- if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
- {
- var addrBytes = ipAddress.GetAddressBytes();
- string hostAddressUri = string.Format("http://{0}.{1}.{2}.{3}:{4}",
- addrBytes[0], addrBytes[1], addrBytes[2], addrBytes[3], port);
- uriParams.Add(new Uri(hostAddressUri));
- }
- }
-
-
- uriParams.Add(new Uri(string.Format("http://localhost:{0}", port)));
- return uriParams.ToArray();
- }
To implement, we need to start Nancy off slight differently... the effect of this is to this force ACL to create network rules for new ports if they do not already exist.
- public Main()
- {
- InitializeComponent();
- int port = 8080;
- var hostConfiguration = new HostConfiguration
- {
- UrlReservations = new UrlReservations() { CreateAutomatically = true }
- };
- host = new NancyHost(hostConfiguration, GetUriParams(port));
- host.Start();
- }
There is a "gotcha" to be aware of ... Windows does not like anyone but admin running on things on anything but default ports - the solution is to open up the ports manually - you can also handle this manually outside in an installation process, like this:
netsh http add urlacl url=http://+:8888/app user=domain\user
(+ means bind to all available IPs)
Nancy Goes into Service...
To facilitate the quick testing of the project, we put it together in a Windows form application. The objective however was to use this very cool little micro web server as a gateway to access and interact more meaningfully with a Windows service. We have already put together the main code previously in the article, so the only thing I am going to do here is show you the setup of the service - it is available for download if you wish to see it in operation or hopefully, use the code yourself!
The normal template codebase for a service program starts like this.
- static class Program
- {
- static void Main()
- {
- ServiceBase[] ServicesToRun;
- ServicesToRun = new ServiceBase[]
- {
- new Service1()
- };
- ServiceBase.Run(ServicesToRun);
- }
- }
We will make two adjustments - first, we will alter the code so that if you start it from the Visual Studio development environment, it will start and allow you to debug, finally, we will add the startup Nancy
code itself.
- static class Program
- {
- static void Main()
- {
- ServiceBase[] ServicesToRun;
- ServicesToRun = new ServiceBase[]
- {
- new Service1()
- };
- ServiceBase.Run(ServicesToRun);
- }
- }
Changes to,
- static void Main()
- {
- NancyHost host;
- string URL = "http://localhost:8080";
- host = new NancyHost(new Uri(URL));
- host.Start();
-
-
- if (!Environment.UserInteractive)
- {
- ServiceBase[] ServicesToRun;
- ServicesToRun = new ServiceBase[]
- {
- new Service1()
- };
- ServiceBase.Run(ServicesToRun);
- }
- else
- {
- Service1 service = new Service1();
-
- System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
- }
- }
To install the service, we need to use the command line.
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" "[Path]NancyService.exe"
(where [Path] is the location of your compiled service executable).
Le Voila, the finished product....
That's it. A useful exercise and something to consider the next time you write a Windows service...
An Aside...
Using Nancy is of particular interest to me as many moons ago in a far and distant planet, I was heavily involved in http://www.indyproject.org/index.en.aspx. Indy was/is an open source sockets library heavily used in the Borland Delphi community and as part of this, I wrote many demos, including a self hosted HTTP Server ... How the tide turns.