By Endy Tjahjono. Last update 27 Jun 2013.
I am currently learning Nancy and this is my braindump. If you don’t yet know what Nancy is, Nancy Homepage provides a good explanation. I want to try it because it looks more compositional than ASP.NET MVC.
I have been using ASP.NET MVC since 2010 so when learning Nancy I see it from the point of view of an ASP.NET MVC developer.
I did this on June 2013: current version of Visual Studio is Visual Studio 2012, and the latest stable release of Nancy on Nuget is version 0.17.1.
This is the blog post that inspired me to compare Nancy with MVC.
ASP.NET Empty Web Application
template.Install-Package Nancy.Hosting.Aspnet
. This will nuget Nancy as dependency.Install-Package Nancy.Viewengines.Razor
.The project is ready, containing only a Web.config file.
In MVC there are controllers. In Nancy there are modules. Here’s a simple module:
// module name is Hello, with 'Module' suffix
public class HelloModule : Nancy.NancyModule
{
// module constructor is very important in Nancy
public HelloModule()
{
Get["/"] = Index;
// assign a function to a route 'pattern' to handle requests that match the pattern
// there are Get and Post collections
}
// this is action
private dynamic Index(dynamic parameters)
{
return View["Index"];
}
}
In MVC controller name ends with ‘Controller’. In Nancy module name ends with ‘Module’.
In MVC all routing rules are defined in ‘App_Start\RouteConfig.cs’. In Nancy each module specifies what kind of route pattern it will handle.
In MVC action’s return type is ActionResult. In Nancy action’s return type is dynamic.
In Nancy there is always only one action parameter, and the parameter type is dynamic.
In MVC controllers are in Controllers folder and views are in Views folder. You can copy this organization strategy in Nancy. Continuing the HelloModule example above:
But Nancy allows different source code organization. My favorite is to have one folder for each module (the folder name matches the module name) and put all relevant views and viewmodels into the same folder. For example the HelloModule:
// viewmodel name ends with 'Model'
public class IndexModel
{
public string Code { get; set; }
...
}
Prepare the viewmodel from an action method:
private dynamic Index(dynamic parameters)
{
var data = new IndexModel { ... };
return data;
}
Nancy will look for a razor file with file name that matches the class name of the viewmodel.
For fun, try not to have any razor file to handle an action. This cute little fella will show up:
Nancy.RequestExecutionException: Oh noes! ---> Nancy.ViewEngines.ViewNotFoundException: Unable to locate view 'Index'
Currently available view engine extensions: sshtml,html,htm,cshtml,vbhtml
Locations inspected: ,,,,,,,,views/Hello/Index-en-US,views/Hello/Index,Hello/Index-en-US,Hello/Index,views/Index-en-US,views/Index,Index-en-US,Index
As you can see, Nancy looked for the razor file in a lot of places:
Nancy requires this line at the top of a razor file if you want to activate intellisense for viewmodel:
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<IndexModel>
For example:
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<IndexModel>
<!DOCTYPE html>
<html>
<body>
<p>@Model.Code</p>
...
</body>
</html>
When adding a HTML file into the project, don’t forget to set the ‘Build Action’ to ‘Content’. The default build action is none so the file will not be included when you publish.
What about model binding? How do I populate a model from query parameters, form data, and request body?
using Nancy.ModelBinding;
public class HelloModule : Nancy.NancyModule
{
public HelloModule()
{
Get["/"] = Index;
}
private dynamic Index(dynamic parameters)
{
var input = this.Bind<IndexModel>();
// or
// IndexModel input = this.Bind();
...
}
}
using Nancy.ModelBinding;
this.Bind
.In MVC it is possible to bind to a list of objects or to a dictionary with query parameters that look like:
list[0].code=A&list[0].name=peanut&list[1].code=B&list[1].name=butter
dict[0].key=X&dict[0].value=fish&dict[1].key=Y&dict[1].value=chips
This is not yet supported in Nancy. So for dictionary or a list of objects, the payload has to be in the request body as JSON or XML.
In MVC there are ViewData and ViewBag. Nancy has ViewBag, but not ViewData. Usage is very similar to MVC:
public class HelloModule : Nancy.NancyModule
{
public HelloModule()
{
Get["/"] = Index;
}
private dynamic Index(dynamic parameters)
{
this.ViewBag.data = "send some data";
return View["Index"];
}
}
In the razor view:
<p>@ViewBag.data</p>
using Nancy;
public class HelloModule : Nancy.NancyModule
{
public HelloModule()
{
Get["/"] = Index;
Get["/newlocation"] = _ => "Shifted!";
}
private dynamic Index(dynamic parameters)
{
// note the ~ at the beginning
return Response.AsRedirect("~/newlocation");
// don't do this because the ~ won't work
// return new Nancy.Responses.RedirectResponse("~/newlocation");
}
}
Looks like that is the one and only way to redirect in Nancy. Since routes don’t have names in Nancy, there is no equivalent of MVC’s RedirectToAction.
According to my limited research there is only one built in session mechanism in Nancy. It uses browser cookie instead of server memory (good decision in my opinion). By default it is not enabled. To enable it, the default Nancy bootstrapper needs to be overridden.
public class CustomNancyBootStrapper : Nancy.DefaultNancyBootstrapper
{
protected override void ApplicationStartup(
Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
Nancy.Session.CookieBasedSessions.Enable(pipelines); // this is the line
}
}
To use the session:
public class HelloModule : Nancy.NancyModule
{
public HelloModule()
{
Get["/"] = Index;
Get["/newlocation"] = _ =>
{
var sesdata = Session["a"]; // get session data
Session.Delete("a"); // remove data from session
return sesdata; // will return 'brown fox' to browser
};
}
private dynamic Index(dynamic parameters)
{
Session["a"] = "brown fox"; // save to session
return new Nancy.Responses.RedirectResponse("/newlocation");
}
}
Looks like there is no TempData equivalent in Nancy for now, so message passing in Post-Redirect-Get scenario should use session.
Razor master page and partial page is available in Nancy. For example there is a layout page in Layout\Base.cshtml
containing:
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<dynamic>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>@ViewBag.title</title>
@RenderSection("ExtraHeaders", required: false)
</head>
<body>
@RenderBody()
</body>
</html>
And a razor page in Home\WithMaster.cshtml
containing:
@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<dynamic>
@{
Layout = "Layout/Base.cshtml"; // Use slash instead of backslash
// Don't begin with slash
}
<p>@ViewBag.title</p>
@Html.Partial("SmallPartial")
@section ExtraHeaders{
<link rel="stylesheet" href="custom.css" />
}
And a partial page in Home\SmallPartial.cshtml
containing:
<p>This is from partial</p>
These pages are called from this module:
public class HomeModule : Nancy.NancyModule
{
public HomeModule()
{
Get["/"] = _ =>
{
ViewBag.title = "Yeah!";
return View["WithMaster"];
};
}
}
Html.ActionLink, Html.BeginForm, Html.TextBox, etc are not available.
But! For creating links, you can just use ~
. For example: <a href="~/otherpage">Go there</a>
. The ~
can be used in other places too, like the form tag: <form action="~/theurl">
.
Nancy has BeforeRequest
, AfterRequest
, and OnError
. Filters are set up in bootstrapper’s RequestStartup or ApplicationStartup.
UPDATE: Filters can also be defined inside a module and they will apply only for that module (thanks Phillip Haydon!).
public class CustomNancyBootStrapper : Nancy.DefaultNancyBootstrapper
{
protected override void RequestStartup(
Nancy.TinyIoc.TinyIoCContainer container,
Nancy.Bootstrapper.IPipelines pipelines,
Nancy.NancyContext context
)
{
base.RequestStartup(container, pipelines, context);
// hooking up filters
pipelines.BeforeRequest += CheckSomething;
pipelines.AfterRequest += ModifyResult;
pipelines.OnError += HandleThisError;
}
protected override void ApplicationStartup(
Nancy.TinyIoc.TinyIoCContainer container, Nancy.Bootstrapper.IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
// where to put the filter? in RequestStartup or ApplicationStartup?
// example: if in the filter you need a database session object
// that is request scoped, put it in RequestStartup
pipelines.BeforeRequest += ...
}
// example of request filter
private Nancy.Response CheckSomething(Nancy.NancyContext ctx)
{
if (ctx.Request.Headers["Magic-Word"].FirstOrDefault() != "Please")
{
// if you return a response object, nancy will not proceed to modules
return new Nancy.Response { StatusCode = Nancy.HttpStatusCode.BadRequest };
}
// if you return null, nancy will proceed to module
return null;
}
// example of response filter. Not returning anything
private void ModifyResult(Nancy.NancyContext ctx)
{
// you can modify response
ctx.Response.Headers["X-Powered-By"] = "Nancy";
}
// example of error filter
private Nancy.Response HandleThisError(Nancy.NancyContext ctx, Exception ex)
{
return null;
}
}
Example of a filter inside a module:
public class HomeModule : Nancy.NancyModule
{
public HomeModule()
{
Get["/"] = _ => "Hello!";
Before += ctx => null; // a Before filter
}
}
Unhandled module errors can be caught with OnError
filter.
Returning a HTTP error from a module can be done with something like this:
public class HomeModule : Nancy.NancyModule
{
public HomeModule()
{
Get["/"] = _ =>
{
return new Nancy.Response { StatusCode = Nancy.HttpStatusCode.NotFound };
};
}
}
Can be implemented as BeforeRequest
filter.
As far as I know, there is no built in one. But there is a sample code on how to implement output cache.
In MVC, there is a ‘File’ response type. It is used like this:
return File(output, "application/pdf");
Here is how to do that in Nancy (source):
public class HomeModule : Nancy.NancyModule
{
public HomeModule()
{
Get["/"] = _ =>
{
var file = new Nancy.Response();
file.Headers["Content-Disposition"] = "attachment; filename=afile.pdf";
file.ContentType = "application/pdf";
file.Contents = str =>
{
using (var writer = new System.IO.StreamWriter(str))
{
writer.Write( ... );
};
};
return file;
};
}
}