NancyFx: Live editable views with RavenDB

.NET, English posts, NancyFX, RavenDB


3 min read

When building a website with NancyFX by default views are loaded from the file system - pretty much like with all MVC-based websites. While NancyFX also supports loading views embedded in assemblies as resources , both options require re-deploying of actual files when something in the view needs updating. Even with fully CI environments, that is still sort of a PITA.

Here is how to use RavenDB to override views in a live website without re-deploying anything. Basically what this does, thanks to NancyFX's modular and flexible design, is take the default ViewLocationProvider and encapsulate it, reading all the views from the original location (file system or assembly resources), and give precedence to views loaded from RavenDB.

The code featured here makes 2 assumptions:

1. A document name convention for view documents is preserved - basically some prefix (for example "MyWebsite/") used to prevent polluting the document store and then the full view name (location + name + extension). When loading all available views, we use a filter to make sure we load only views with a supported extension (determined by the installee view-engines). A view-template document in RavenDB will then have an ID similar to "WebsiteViews/Views/Home/Read.cshtml".

2. There are less than 1024 views stored to RavenDB. This is probably safe to assume, or you have some monstrous website.

There's one bit missing here - view-cache invalidation. By default NancyFx will cache all views it loaded indefinitely, as far as I can tell. That's bad for us, because when you update a template in your RavenDB store you do want to invalidate all caches, or at least one specific template you updated. This is something I'll keep for another post.

This is the custom ViewLocationProvider class:

[code lang="csharp"] using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using NSemble.Core.Models; using Nancy; using Nancy.ViewEngines; namespace NSemble.Core.Nancy { public class RavenViewLocationProvider : IViewLocationProvider { private readonly IViewLocationProvider defaultViewLocationProvider; public RavenViewLocationProvider(IRootPathProvider rootPathProvider) { defaultViewLocationProvider = new FileSystemViewLocationProvider(rootPathProvider); } public RavenViewLocationProvider(IRootPathProvider rootPathProvider, IFileSystemReader fileSystemReader) { defaultViewLocationProvider = new FileSystemViewLocationProvider(rootPathProvider, fileSystemReader); } public IEnumerable<ViewLocationResult> GetLocatedViews(IEnumerable<string> supportedViewExtensions) { var sb = new StringBuilder(); // Make sure to only load saved views with supported extensions foreach (var s in supportedViewExtensions) { if (sb.Length > 0) sb.Append("|"); sb.Append("*."); sb.Append(s); } ViewTemplate[] views = null; using (var session = NSembleModule.DocumentStore.OpenSession()) { // It's probably safe to assume we will have no more than 1024 views, so no reason to bother with paging views = session.Advanced.LoadStartingWith<ViewTemplate>(Constants.RavenViewDocumentPrefix, sb.ToString(), 0, 1024); } // Read the views from the default location IEnumerable<ViewLocationResult> defaultViews = defaultViewLocationProvider.GetLocatedViews(supportedViewExtensions); if (views.Length == 0) return defaultViews; var ret = new HashSet<ViewLocationResult>(from v in views where supportedViewExtensions.Contains(v.Extension) select new ViewLocationResult( v.Location, v.Name, v.Extension, () => new StringReader(v.Contents))); foreach (var v in defaultViews) ret.Add(v); return ret; } } } [/code]

You will need to register it in your Nancy Boostrapper class:

[code lang="csharp"] protected override NancyInternalConfiguration InternalConfiguration { get { return NancyInternalConfiguration .WithOverrides(x => x.ViewLocationProvider = typeof (RavenViewLocationProvider)) .WithIgnoredAssembly(asm => asm.FullName.StartsWith("RavenDB", StringComparison.InvariantCulture)); // or override ConfigureApplicationContainer to set AutoRegister to false } } [/code]

Comments are now closed