NoSQL Zone is brought to you in partnership with:

Software developer and frequent open-source contributor. Writing mostly for .NET, but also Java and C/C++. Really likes fiddling with data, texts especially, so he frequently finds himself working on databases or search engines, usually combining both. Itamar is a DZone MVB and is not an employee of DZone and has posted 31 posts at DZone. You can read more from them at their website. View Full User Profile

NancyFx: Live editable views with RavenDB

02.07.2013
| 1465 views |
  • submit to reddit

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:

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;
        }
    }
}

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

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
    }
}



 

Published at DZone with permission of Itamar Syn-hershko, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)